diff --git a/.eslintignore b/.eslintignore index 4995ab9bc61..6803a9b63d3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,3 +6,4 @@ node_modules versions acmeair-nodejs vendor +integration-tests/esbuild/out.js diff --git a/.eslintrc.json b/.eslintrc.json index 338d0ae1941..44d24240884 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -22,6 +22,7 @@ "import/no-extraneous-dependencies": 2, "standard/no-callback-literal": 0, "no-prototype-builtins": 0, - "n/no-restricted-require": [2, ["diagnostics_channel"]] + "n/no-restricted-require": [2, ["diagnostics_channel"]], + "object-curly-newline": ["error", {"multiline": true, "consistent": true }] } } diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 51c261e0d2a..424e77a170a 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -806,6 +806,23 @@ jobs: uses: ./.github/actions/testagent/logs - uses: codecov/codecov-action@v2 + openai: + runs-on: ubuntu-latest + env: + PLUGINS: openai + steps: + - uses: actions/checkout@v2 + - uses: ./.github/actions/testagent/start + - uses: ./.github/actions/node/setup + - run: yarn install + - uses: ./.github/actions/node/oldest + - run: yarn test:plugins:ci + - uses: ./.github/actions/node/latest + - run: yarn test:plugins:ci + - if: always() + uses: ./.github/actions/testagent/logs + - uses: codecov/codecov-action@v2 + opensearch: runs-on: ubuntu-latest services: diff --git a/README.md b/README.md index 71c9ca046c7..94ee1ce5632 100644 --- a/README.md +++ b/README.md @@ -143,8 +143,8 @@ $ yarn leak:plugins ### Linting -We use [ESLint](https://eslint.org) to make sure that new code is -conform to our coding standards. +We use [ESLint](https://eslint.org) to make sure that new code +conforms to our coding standards. To run the linter, use: diff --git a/benchmark/sirun/get-results.js b/benchmark/sirun/get-results.js index 3ccc70a3f7c..266cc77337a 100644 --- a/benchmark/sirun/get-results.js +++ b/benchmark/sirun/get-results.js @@ -22,10 +22,12 @@ const artifactsUrl = num => function get (url, headers) { return new Promise((resolve, reject) => { - https.get(url, { headers: Object.assign({ - 'user-agent': 'dd-results-retriever', - accept: 'application/json' - }, headers) }, async res => { + https.get(url, { + headers: Object.assign({ + 'user-agent': 'dd-results-retriever', + accept: 'application/json' + }, headers) + }, async res => { if (res.statusCode >= 300 && res.statusCode < 400) { resolve(get(res.headers.location)) return diff --git a/integration-tests/ci-visibility.spec.js b/integration-tests/ci-visibility.spec.js index 8b716d560e3..60ddd12dbd0 100644 --- a/integration-tests/ci-visibility.spec.js +++ b/integration-tests/ci-visibility.spec.js @@ -230,6 +230,28 @@ testFrameworks.forEach(({ }).catch(done) }) }) + it('reports timeout error message', (done) => { + childProcess = fork('ci-visibility/run-jest.js', { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + NODE_OPTIONS: '-r dd-trace/ci/init', + RUN_IN_PARALLEL: true, + TEST_REGEX: 'timeout-test/timeout-test.js' + }, + stdio: 'pipe' + }) + childProcess.stdout.on('data', (chunk) => { + testOutput += chunk.toString() + }) + childProcess.stderr.on('data', (chunk) => { + testOutput += chunk.toString() + }) + childProcess.on('message', () => { + assert.include(testOutput, 'Exceeded timeout of 100 ms for a test while waiting for `done()` to be called.') + done() + }) + }) } it('can run tests and report spans', (done) => { diff --git a/integration-tests/ci-visibility/run-jest.js b/integration-tests/ci-visibility/run-jest.js index 21090204b2e..b8710650029 100644 --- a/integration-tests/ci-visibility/run-jest.js +++ b/integration-tests/ci-visibility/run-jest.js @@ -4,7 +4,7 @@ const options = { projects: [__dirname], testPathIgnorePatterns: ['/node_modules/'], cache: false, - testRegex: /test\/ci-visibility-test/, + testRegex: process.env.TEST_REGEX ? new RegExp(process.env.TEST_REGEX) : /test\/ci-visibility-test/, coverage: true, runInBand: true } diff --git a/integration-tests/ci-visibility/timeout-test/timeout-test.js b/integration-tests/ci-visibility/timeout-test/timeout-test.js new file mode 100644 index 00000000000..5a1691a8728 --- /dev/null +++ b/integration-tests/ci-visibility/timeout-test/timeout-test.js @@ -0,0 +1,9 @@ +/* eslint-disable */ +jest.setTimeout(100) +describe('ci visibility', () => { + it('will timeout', (done) => { + setTimeout(() => { + done() + }, 200) + }) +}) diff --git a/package.json b/package.json index 83e8ceba087..f563614c353 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dd-trace", - "version": "2.41.0", + "version": "2.42.0", "description": "Datadog APM tracing client for JavaScript", "main": "index.js", "typings": "index.d.ts", @@ -70,7 +70,7 @@ "@datadog/native-iast-rewriter": "2.0.1", "@datadog/native-iast-taint-tracking": "^1.5.0", "@datadog/native-metrics": "^1.6.0", - "@datadog/pprof": "3.0.0", + "@datadog/pprof": "3.1.0", "@datadog/sketches-js": "^2.1.0", "@types/node": "<18.13", "@opentelemetry/api": "^1.0.0", diff --git a/packages/datadog-core/src/storage/async_resource.js b/packages/datadog-core/src/storage/async_resource.js index bf64f31126a..6dea8cf2fec 100644 --- a/packages/datadog-core/src/storage/async_resource.js +++ b/packages/datadog-core/src/storage/async_resource.js @@ -5,6 +5,7 @@ const { channel } = require('../../../diagnostics_channel') const beforeCh = channel('dd-trace:storage:before') const afterCh = channel('dd-trace:storage:after') +const enterCh = channel('dd-trace:storage:enter') let PrivateSymbol = Symbol function makePrivateSymbol () { @@ -52,6 +53,7 @@ class AsyncResourceStorage { const resource = this._executionAsyncResource() resource[this._ddResourceStore] = store + enterCh.publish() } run (store, callback, ...args) { @@ -61,11 +63,13 @@ class AsyncResourceStorage { const oldStore = resource[this._ddResourceStore] resource[this._ddResourceStore] = store + enterCh.publish() try { return callback(...args) } finally { resource[this._ddResourceStore] = oldStore + enterCh.publish() } } diff --git a/packages/datadog-instrumentations/src/jest.js b/packages/datadog-instrumentations/src/jest.js index e76240a2dad..00eeea36908 100644 --- a/packages/datadog-instrumentations/src/jest.js +++ b/packages/datadog-instrumentations/src/jest.js @@ -129,8 +129,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { suite: this.testSuite, runner: 'jest-circus', testParameters, - frameworkVersion: jestVersion, - testStartLine: getTestLineStart(event.test.asyncError, this.testSuite) + frameworkVersion: jestVersion }) originalTestFns.set(event.test, event.test.fn) event.test.fn = asyncResource.bind(event.test.fn) @@ -145,7 +144,10 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { const formattedError = formatJestError(event.test.errors[0]) testErrCh.publish(formattedError) } - testRunFinishCh.publish(status) + testRunFinishCh.publish({ + status, + testStartLine: getTestLineStart(event.test.asyncError, this.testSuite) + }) // restore in case it is retried event.test.fn = originalTestFns.get(event.test) }) @@ -471,7 +473,7 @@ function jasmineAsyncInstallWraper (jasmineAsyncInstallExport, jestVersion) { const formattedError = formatJestError(spec.result.failedExpectations[0].error) testErrCh.publish(formattedError) } - testRunFinishCh.publish(specStatusToTestStatus[spec.result.status]) + testRunFinishCh.publish({ status: specStatusToTestStatus[spec.result.status] }) onComplete.apply(this, arguments) }) arguments[0] = callback diff --git a/packages/datadog-plugin-elasticsearch/test/index.spec.js b/packages/datadog-plugin-elasticsearch/test/index.spec.js index 6c2cf25b0e4..4e7fe28b13c 100644 --- a/packages/datadog-plugin-elasticsearch/test/index.spec.js +++ b/packages/datadog-plugin-elasticsearch/test/index.spec.js @@ -319,9 +319,11 @@ describe('Plugin', () => { before(() => { return agent.load('elasticsearch', { service: 'custom', - hooks: { query: (span, params) => { - span.addTags({ 'elasticsearch.params': 'foo', 'elasticsearch.method': params.method }) - } } + hooks: { + query: (span, params) => { + span.addTags({ 'elasticsearch.params': 'foo', 'elasticsearch.method': params.method }) + } + } }) }) diff --git a/packages/datadog-plugin-graphql/src/execute.js b/packages/datadog-plugin-graphql/src/execute.js index b4a817a103d..60cede44e14 100644 --- a/packages/datadog-plugin-graphql/src/execute.js +++ b/packages/datadog-plugin-graphql/src/execute.js @@ -7,6 +7,8 @@ let tools class GraphQLExecutePlugin extends TracingPlugin { static get id () { return 'graphql' } static get operation () { return 'execute' } + static get type () { return 'graphql' } + static get kind () { return 'server' } start ({ operation, args, docSource }) { const type = operation && operation.operation @@ -14,11 +16,11 @@ class GraphQLExecutePlugin extends TracingPlugin { const document = args.document const source = this.config.source && document && docSource - const span = this.startSpan('graphql.execute', { - service: this.config.service, + const span = this.startSpan(this.operationName(), { + service: this.config.service || this.serviceName(), resource: getSignature(document, name, type, this.config.signature), - kind: 'server', - type: 'graphql', + kind: this.constructor.kind, + type: this.constructor.type, meta: { 'graphql.operation.type': type, 'graphql.operation.name': name, diff --git a/packages/datadog-plugin-graphql/test/index.spec.js b/packages/datadog-plugin-graphql/test/index.spec.js index 72df3cdf43e..28a274b5d74 100644 --- a/packages/datadog-plugin-graphql/test/index.spec.js +++ b/packages/datadog-plugin-graphql/test/index.spec.js @@ -4,6 +4,7 @@ const { expect } = require('chai') const semver = require('semver') const agent = require('../../dd-trace/test/plugins/agent') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') +const namingSchema = require('./naming') describe('Plugin', () => { let tracer @@ -175,6 +176,21 @@ describe('Plugin', () => { return agent.close({ ritmReset: false }) }) + withNamingSchema( + () => { + const source = `query MyQuery { hello(name: "world") }` + const variableValues = { who: 'world' } + graphql.graphql({ schema, source, variableValues }) + }, + () => namingSchema.server.opName, + () => namingSchema.server.serviceName, + 'test', + (traces) => { + const spans = sort(traces[0]) + return spans[0] + } + ) + it('should instrument parsing', done => { const source = `query MyQuery { hello(name: "world") }` const variableValues = { who: 'world' } diff --git a/packages/datadog-plugin-graphql/test/naming.js b/packages/datadog-plugin-graphql/test/naming.js new file mode 100644 index 00000000000..41b59ae7796 --- /dev/null +++ b/packages/datadog-plugin-graphql/test/naming.js @@ -0,0 +1,14 @@ +const { resolveNaming } = require('../../dd-trace/test/plugins/helpers') + +module.exports = resolveNaming({ + server: { + v0: { + opName: 'graphql.execute', + serviceName: 'test' + }, + v1: { + opName: 'graphql.server.request', + serviceName: 'test' + } + } +}) diff --git a/packages/datadog-plugin-jest/src/index.js b/packages/datadog-plugin-jest/src/index.js index b22e2862b23..624d640d513 100644 --- a/packages/datadog-plugin-jest/src/index.js +++ b/packages/datadog-plugin-jest/src/index.js @@ -166,9 +166,12 @@ class JestPlugin extends CiPlugin { this.enter(span, store) }) - this.addSub('ci:jest:test:finish', (status) => { + this.addSub('ci:jest:test:finish', ({ status, testStartLine }) => { const span = storage.getStore().span span.setTag(TEST_STATUS, status) + if (testStartLine) { + span.setTag(TEST_SOURCE_START, testStartLine) + } span.finish() finishAllTraceSpans(span) }) @@ -197,8 +200,10 @@ class JestPlugin extends CiPlugin { const extraTags = { [JEST_TEST_RUNNER]: runner, [TEST_PARAMETERS]: testParameters, - [TEST_FRAMEWORK_VERSION]: frameworkVersion, - [TEST_SOURCE_START]: testStartLine + [TEST_FRAMEWORK_VERSION]: frameworkVersion + } + if (testStartLine) { + extraTags[TEST_SOURCE_START] = testStartLine } return super.startTestSpan(name, suite, this.testSuiteSpan, extraTags) diff --git a/packages/datadog-plugin-kafkajs/test/index.spec.js b/packages/datadog-plugin-kafkajs/test/index.spec.js index 18516043ca3..25619d30ffd 100644 --- a/packages/datadog-plugin-kafkajs/test/index.spec.js +++ b/packages/datadog-plugin-kafkajs/test/index.spec.js @@ -298,7 +298,8 @@ describe('Plugin', () => { await consumer.run({ eachMessage: async ({ topic, partition, message, heartbeat, pause }) => { expect(setDataStreamsContextSpy.args[0][0].hash).to.equal(expectedConsumerHash) - } }) + } + }) setDataStreamsContextSpy.restore() }) }) diff --git a/packages/datadog-plugin-openai/src/index.js b/packages/datadog-plugin-openai/src/index.js index 7bce3694948..9ceaad9592d 100644 --- a/packages/datadog-plugin-openai/src/index.js +++ b/packages/datadog-plugin-openai/src/index.js @@ -26,7 +26,9 @@ class OpenApiPlugin extends TracingPlugin { this.sampler = new Sampler(0.1) // default 10% log sampling // hoist the max length env var to avoid making all of these functions a class method - MAX_TEXT_LEN = this._tracerConfig.openaiSpanCharLimit + if (this._tracerConfig) { + MAX_TEXT_LEN = this._tracerConfig.openaiSpanCharLimit + } } configure (config) { @@ -363,6 +365,8 @@ function retrieveModelResponseExtraction (tags, body) { tags['openai.response.parent'] = body.parent tags['openai.response.root'] = body.root + if (!body.permission) return + tags['openai.response.permission.id'] = body.permission[0].id tags['openai.response.permission.created'] = body.permission[0].created tags['openai.response.permission.allow_create_engine'] = body.permission[0].allow_create_engine @@ -382,10 +386,14 @@ function commonLookupFineTuneRequestExtraction (tags, body) { } function listModelsResponseExtraction (tags, body) { + if (!body.data) return + tags['openai.response.count'] = body.data.length } function commonImageResponseExtraction (tags, body) { + if (!body.data) return + tags['openai.response.images_count'] = body.data.length for (let i = 0; i < body.data.length; i++) { @@ -400,7 +408,7 @@ function createAudioResponseExtraction (tags, body) { tags['openai.response.text'] = body.text tags['openai.response.language'] = body.language tags['openai.response.duration'] = body.duration - tags['openai.response.segments_count'] = body.segments.length + tags['openai.response.segments_count'] = defensiveArrayLength(body.segments) } function createFineTuneRequestExtraction (tags, body) { @@ -417,21 +425,24 @@ function createFineTuneRequestExtraction (tags, body) { } function commonFineTuneResponseExtraction (tags, body) { - tags['openai.response.events_count'] = body.events.length + tags['openai.response.events_count'] = defensiveArrayLength(body.events) tags['openai.response.fine_tuned_model'] = body.fine_tuned_model - tags['openai.response.hyperparams.n_epochs'] = body.hyperparams.n_epochs - tags['openai.response.hyperparams.batch_size'] = body.hyperparams.batch_size - tags['openai.response.hyperparams.prompt_loss_weight'] = body.hyperparams.prompt_loss_weight - tags['openai.response.hyperparams.learning_rate_multiplier'] = body.hyperparams.learning_rate_multiplier - tags['openai.response.training_files_count'] = body.training_files.length - tags['openai.response.result_files_count'] = body.result_files.length - tags['openai.response.validation_files_count'] = body.validation_files.length + if (body.hyperparams) { + tags['openai.response.hyperparams.n_epochs'] = body.hyperparams.n_epochs + tags['openai.response.hyperparams.batch_size'] = body.hyperparams.batch_size + tags['openai.response.hyperparams.prompt_loss_weight'] = body.hyperparams.prompt_loss_weight + tags['openai.response.hyperparams.learning_rate_multiplier'] = body.hyperparams.learning_rate_multiplier + } + tags['openai.response.training_files_count'] = defensiveArrayLength(body.training_files) + tags['openai.response.result_files_count'] = defensiveArrayLength(body.result_files) + tags['openai.response.validation_files_count'] = defensiveArrayLength(body.validation_files) tags['openai.response.updated_at'] = body.updated_at tags['openai.response.status'] = body.status } // the OpenAI package appears to stream the content download then provide it all as a singular string function downloadFileResponseExtraction (tags, body) { + if (!body.file) return tags['openai.response.total_bytes'] = body.file.length } @@ -472,6 +483,8 @@ function createRetrieveFileResponseExtraction (tags, body) { function createEmbeddingResponseExtraction (tags, body) { usageExtraction(tags, body) + if (!body.data) return + tags['openai.response.embeddings_count'] = body.data.length for (let i = 0; i < body.data.length; i++) { tags[`openai.response.embedding.${i}.embedding_length`] = body.data[i].embedding.length @@ -479,6 +492,7 @@ function createEmbeddingResponseExtraction (tags, body) { } function commonListCountResponseExtraction (tags, body) { + if (!body.data) return tags['openai.response.count'] = body.data.length } @@ -486,6 +500,9 @@ function commonListCountResponseExtraction (tags, body) { function createModerationResponseExtraction (tags, body) { tags['openai.response.id'] = body.id // tags[`openai.response.model`] = body.model // redundant, already extracted globally + + if (!body.results) return + tags['openai.response.flagged'] = body.results[0].flagged for (const [category, match] of Object.entries(body.results[0].categories)) { @@ -501,6 +518,8 @@ function createModerationResponseExtraction (tags, body) { function commonCreateResponseExtraction (tags, body, store) { usageExtraction(tags, body) + if (!body.choices) return + tags['openai.response.choices_count'] = body.choices.length store.choices = body.choices @@ -530,7 +549,7 @@ function usageExtraction (tags, body) { } function truncateApiKey (apiKey) { - return `sk-...${apiKey.substr(apiKey.length - 4)}` + return apiKey && `sk-...${apiKey.substr(apiKey.length - 4)}` } /** diff --git a/packages/datadog-plugin-openai/src/services.js b/packages/datadog-plugin-openai/src/services.js index 2d35878a35a..58662f59846 100644 --- a/packages/datadog-plugin-openai/src/services.js +++ b/packages/datadog-plugin-openai/src/services.js @@ -1,7 +1,7 @@ 'use strict' const { DogStatsDClient, NoopDogStatsDClient } = require('../../dd-trace/src/dogstatsd') -const ExternalLogger = require('../../dd-trace/src/external-logger/src') +const { ExternalLogger, NoopExternalLogger } = require('../../dd-trace/src/external-logger/src') const FLUSH_INTERVAL = 10 * 1000 @@ -10,7 +10,7 @@ let logger = null let interval = null module.exports.init = function (tracerConfig) { - if (tracerConfig.dogstatsd) { + if (tracerConfig && tracerConfig.dogstatsd) { metrics = new DogStatsDClient({ host: tracerConfig.dogstatsd.hostname, port: tracerConfig.dogstatsd.port, @@ -24,13 +24,17 @@ module.exports.init = function (tracerConfig) { metrics = new NoopDogStatsDClient() } - logger = new ExternalLogger({ - ddsource: 'openai', - hostname: tracerConfig.hostname, - service: tracerConfig.service, - apiKey: tracerConfig.apiKey, - interval: FLUSH_INTERVAL - }) + if (tracerConfig && tracerConfig.apiKey) { + logger = new ExternalLogger({ + ddsource: 'openai', + hostname: tracerConfig.hostname, + service: tracerConfig.service, + apiKey: tracerConfig.apiKey, + interval: FLUSH_INTERVAL + }) + } else { + logger = new NoopExternalLogger() + } interval = setInterval(() => { metrics.flush() diff --git a/packages/datadog-plugin-openai/test/index.spec.js b/packages/datadog-plugin-openai/test/index.spec.js index 04feff0d322..cf3411ef3dd 100644 --- a/packages/datadog-plugin-openai/test/index.spec.js +++ b/packages/datadog-plugin-openai/test/index.spec.js @@ -6,12 +6,15 @@ const { expect } = require('chai') const semver = require('semver') const nock = require('nock') const sinon = require('sinon') +const { spawn } = require('child_process') const agent = require('../../dd-trace/test/plugins/agent') const { DogStatsDClient } = require('../../dd-trace/src/dogstatsd') -const ExternalLogger = require('../../dd-trace/src/external-logger/src') +const { NoopExternalLogger } = require('../../dd-trace/src/external-logger/src') const Sampler = require('../../dd-trace/src/sampler') +const tracerRequirePath = '../../dd-trace' + describe('Plugin', () => { let openai let clock @@ -20,8 +23,10 @@ describe('Plugin', () => { describe('openai', () => { withVersions('openai', 'openai', version => { + const moduleRequirePath = `../../../versions/openai@${version}` + beforeEach(() => { - require('../../dd-trace') + require(tracerRequirePath) }) before(() => { @@ -34,7 +39,7 @@ describe('Plugin', () => { beforeEach(() => { clock = sinon.useFakeTimers() - const { Configuration, OpenAIApi } = require(`../../../versions/openai@${version}`).get() + const { Configuration, OpenAIApi } = require(moduleRequirePath).get() const configuration = new Configuration({ apiKey: 'sk-DATADOG-ACCEPTANCE-TESTS' @@ -43,7 +48,7 @@ describe('Plugin', () => { openai = new OpenAIApi(configuration) metricStub = sinon.stub(DogStatsDClient.prototype, '_add') - externalLoggerStub = sinon.stub(ExternalLogger.prototype, 'log') + externalLoggerStub = sinon.stub(NoopExternalLogger.prototype, 'log') sinon.stub(Sampler.prototype, 'isSampled').returns(true) }) @@ -52,10 +57,29 @@ describe('Plugin', () => { sinon.restore() }) + describe('without initialization', () => { + it('should not error', (done) => { + spawn('node', ['no-init'], { + cwd: __dirname, + stdio: 'inherit', + env: { + ...process.env, + PATH_TO_DDTRACE: tracerRequirePath, + PATH_TO_OPENAI: moduleRequirePath + } + }).on('exit', done) // non-zero exit status fails test + }) + }) + describe('createCompletion()', () => { let scope - before(() => { + after(() => { + nock.removeInterceptor(scope) + scope.done() + }) + + it('makes a successful call', async () => { scope = nock('https://api.openai.com:443') .post('/v1/completions') .reply(200, { @@ -87,14 +111,7 @@ describe('Plugin', () => { 'x-ratelimit-reset-tokens', '3ms', 'x-request-id', '7df89d8afe7bf24dc04e2c4dd4962d7f' ]) - }) - - after(() => { - nock.removeInterceptor(scope) - scope.done() - }) - it('makes a successful call', async () => { const checkTraces = agent .use(traces => { expect(traces[0][0]).to.have.property('name', 'openai.request') @@ -188,6 +205,44 @@ describe('Plugin', () => { ] }) }) + + it('should not throw with empty response body', async () => { + scope = nock('https://api.openai.com:443') + .post('/v1/completions') + .reply(200, {}, [ + 'Date', 'Mon, 15 May 2023 17:24:22 GMT', + 'Content-Type', 'application/json', + 'Content-Length', '349', + 'Connection', 'close', + 'openai-model', 'text-davinci-002', + 'openai-organization', 'kill-9', + 'openai-processing-ms', '442', + 'openai-version', '2020-10-01', + 'x-ratelimit-limit-requests', '3000', + 'x-ratelimit-limit-tokens', '250000', + 'x-ratelimit-remaining-requests', '2999', + 'x-ratelimit-remaining-tokens', '249984', + 'x-ratelimit-reset-requests', '20ms', + 'x-ratelimit-reset-tokens', '3ms', + 'x-request-id', '7df89d8afe7bf24dc04e2c4dd4962d7f' + ]) + + const checkTraces = agent + .use(traces => { + expect(traces[0][0]).to.have.property('name', 'openai.request') + }) + + await openai.createCompletion({ + model: 'text-davinci-002', + prompt: 'Hello, ', + suffix: 'foo', + stream: true + }) + + await checkTraces + + clock.tick(10 * 1000) + }) }) describe('createEmbedding()', () => { @@ -316,7 +371,8 @@ describe('Plugin', () => { 'root': 'babbage', 'parent': null } - ] }, [ + ] + }, [ 'Date', 'Mon, 15 May 2023 23:26:42 GMT', 'Content-Type', 'application/json', 'Content-Length', '63979', @@ -1000,51 +1056,74 @@ describe('Plugin', () => { 'status': 'succeeded', 'fine_tuned_model': 'curie:ft-foo:deleteme-2023-05-18-20-44-56', 'events': [ - { 'object': 'fine-tune-event', + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Created fine-tune: ft-10RCfqSvgyEcauomw7VpiYco', - 'created_at': 1684442489 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442489 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Fine-tune costs $0.00', - 'created_at': 1684442612 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442612 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Fine-tune enqueued. Queue number: 0', - 'created_at': 1684442612 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442612 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Fine-tune started', - 'created_at': 1684442614 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442614 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Completed epoch 1/4', - 'created_at': 1684442677 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442677 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Completed epoch 2/4', - 'created_at': 1684442677 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442677 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Completed epoch 3/4', - 'created_at': 1684442678 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442678 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Completed epoch 4/4', - 'created_at': 1684442679 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442679 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Uploaded model: curie:ft-foo:deleteme-2023-05-18-20-44-56', - 'created_at': 1684442696 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442696 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Uploaded result file: file-bJyf8TM0jeSZueBo4jpodZVQ', - 'created_at': 1684442697 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442697 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Fine-tune succeeded', - 'created_at': 1684442697 } - ] }, [ + 'created_at': 1684442697 + } + ] + }, [ 'Date', 'Thu, 18 May 2023 22:11:53 GMT', 'Content-Type', 'application/json', 'Content-Length', '2727', @@ -1177,51 +1256,74 @@ describe('Plugin', () => { .reply(200, { 'object': 'list', 'data': [ - { 'object': 'fine-tune-event', + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Created fine-tune: ft-10RCfqSvgyEcauomw7VpiYco', - 'created_at': 1684442489 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442489 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Fine-tune costs $0.00', - 'created_at': 1684442612 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442612 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Fine-tune enqueued. Queue number: 0', - 'created_at': 1684442612 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442612 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Fine-tune started', - 'created_at': 1684442614 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442614 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Completed epoch 1/4', - 'created_at': 1684442677 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442677 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Completed epoch 2/4', - 'created_at': 1684442677 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442677 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Completed epoch 3/4', - 'created_at': 1684442678 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442678 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Completed epoch 4/4', - 'created_at': 1684442679 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442679 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Uploaded model: curie:ft-foo:deleteme-2023-05-18-20-44-56', - 'created_at': 1684442696 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442696 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Uploaded result file: file-bJyf8TM0jeSZueBo4jpodZVQ', - 'created_at': 1684442697 }, - { 'object': 'fine-tune-event', + 'created_at': 1684442697 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Fine-tune succeeded', - 'created_at': 1684442697 } - ] }, [ + 'created_at': 1684442697 + } + ] + }, [ 'Date', 'Thu, 18 May 2023 22:47:17 GMT', 'Content-Type', 'application/json', 'Content-Length', '1718', @@ -1341,15 +1443,20 @@ describe('Plugin', () => { 'status': 'cancelled', 'fine_tuned_model': 'idk', 'events': [ - { 'object': 'fine-tune-event', + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Created fine-tune: ft-TVpNqwlvermMegfRVqSOyPyS', - 'created_at': 1684452102 }, - { 'object': 'fine-tune-event', + 'created_at': 1684452102 + }, + { + 'object': 'fine-tune-event', 'level': 'info', 'message': 'Fine-tune cancelled', - 'created_at': 1684452103 } - ] }, [ + 'created_at': 1684452103 + } + ] + }, [ 'Date', 'Thu, 18 May 2023 23:21:43 GMT', 'Content-Type', 'application/json', 'Content-Length', '1042', @@ -1985,7 +2092,8 @@ describe('Plugin', () => { 'avg_logprob': -0.7777707236153739, 'compression_ratio': 0.6363636363636364, 'no_speech_prob': 0.043891049921512604, - 'transient': false }], + 'transient': false + }], 'text': 'Hello, friend.' }, [ 'Date', 'Fri, 19 May 2023 03:19:49 GMT', diff --git a/packages/datadog-plugin-openai/test/no-init.js b/packages/datadog-plugin-openai/test/no-init.js new file mode 100755 index 00000000000..002e07cb03d --- /dev/null +++ b/packages/datadog-plugin-openai/test/no-init.js @@ -0,0 +1,11 @@ +#!/usr/bin/env node + +/** + * Due to the complexity of the service initialization required by openai + * there was a bug where when requiring dd-trace followed by openai + * would result in an error if dd-trace wasn't first initialized. + * + * @see https://github.com/DataDog/dd-trace-js/issues/3357 + */ +require(process.env.PATH_TO_DDTRACE) +require(process.env.PATH_TO_OPENAI).get() diff --git a/packages/datadog-plugin-openai/test/services.spec.js b/packages/datadog-plugin-openai/test/services.spec.js index 925b4a5effd..bb1ef373f80 100644 --- a/packages/datadog-plugin-openai/test/services.spec.js +++ b/packages/datadog-plugin-openai/test/services.spec.js @@ -9,7 +9,7 @@ describe('Plugin', () => { services.shutdown() }) - it('dogstatsd does not throw', () => { + it('dogstatsd does not throw when missing .dogstatsd', () => { const service = services.init({ hostname: 'foo', service: 'bar', @@ -30,6 +30,13 @@ describe('Plugin', () => { service.logger.log('hello') }) + + it('logger does not throw when passing in null', () => { + const service = services.init(null) + + service.metrics.increment('mykey') + service.logger.log('hello') + }) }) }) }) diff --git a/packages/datadog-plugin-opensearch/test/index.spec.js b/packages/datadog-plugin-opensearch/test/index.spec.js index 0932d96da47..8d48fc31d85 100644 --- a/packages/datadog-plugin-opensearch/test/index.spec.js +++ b/packages/datadog-plugin-opensearch/test/index.spec.js @@ -225,9 +225,11 @@ describe('Plugin', () => { before(() => { return agent.load('opensearch', { service: 'custom', - hooks: { query: (span, params) => { - span.addTags({ 'opensearch.params': 'foo', 'opensearch.method': params.method }) - } } + hooks: { + query: (span, params) => { + span.addTags({ 'opensearch.params': 'foo', 'opensearch.method': params.method }) + } + } }) }) diff --git a/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js b/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js index fbb2c22fe53..baf32bdb2e4 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js @@ -2,6 +2,7 @@ module.exports = { 'COMMAND_INJECTION_ANALYZER': require('./command-injection-analyzer'), + 'HSTS_HEADER_MISSING_ANALYZER': require('./hsts-header-missing-analyzer'), 'INSECURE_COOKIE_ANALYZER': require('./insecure-cookie-analyzer'), 'LDAP_ANALYZER': require('./ldap-injection-analyzer'), 'NO_HTTPONLY_COOKIE_ANALYZER': require('./no-httponly-cookie-analyzer'), @@ -11,5 +12,6 @@ module.exports = { 'SSRF': require('./ssrf-analyzer'), 'UNVALIDATED_REDIRECT_ANALYZER': require('./unvalidated-redirect-analyzer'), 'WEAK_CIPHER_ANALYZER': require('./weak-cipher-analyzer'), - 'WEAK_HASH_ANALYZER': require('./weak-hash-analyzer') + 'WEAK_HASH_ANALYZER': require('./weak-hash-analyzer'), + 'XCONTENTTYPE_HEADER_MISSING_ANALYZER': require('./xcontenttype-header-missing-analyzer') } diff --git a/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js new file mode 100644 index 00000000000..87e79e98c8c --- /dev/null +++ b/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js @@ -0,0 +1,45 @@ +'use strict' + +const { HSTS_HEADER_MISSING } = require('../vulnerabilities') +const { MissingHeaderAnalyzer } = require('./missing-header-analyzer') + +const HSTS_HEADER_NAME = 'Strict-Transport-Security' +const HEADER_VALID_PREFIX = 'max-age' +class HstsHeaderMissingAnalyzer extends MissingHeaderAnalyzer { + constructor () { + super(HSTS_HEADER_MISSING, HSTS_HEADER_NAME) + } + _isVulnerableFromRequestAndResponse (req, res) { + const headerToCheck = res.getHeader(HSTS_HEADER_NAME) + return !this._isHeaderValid(headerToCheck) && this._isHttpsProtocol(req) + } + + _isHeaderValid (headerValue) { + if (!headerValue) { + return false + } + headerValue = headerValue.trim() + + if (!headerValue.startsWith(HEADER_VALID_PREFIX)) { + return false + } + + const semicolonIndex = headerValue.indexOf(';') + let timestampString + if (semicolonIndex > -1) { + timestampString = headerValue.substring(HEADER_VALID_PREFIX.length + 1, semicolonIndex) + } else { + timestampString = headerValue.substring(HEADER_VALID_PREFIX.length + 1) + } + + const timestamp = parseInt(timestampString) + // eslint-disable-next-line eqeqeq + return timestamp == timestampString && timestamp > 0 + } + + _isHttpsProtocol (req) { + return req.protocol === 'https' || req.headers['x-forwarded-proto'] === 'https' + } +} + +module.exports = new HstsHeaderMissingAnalyzer() diff --git a/packages/dd-trace/src/appsec/iast/analyzers/index.js b/packages/dd-trace/src/appsec/iast/analyzers/index.js index cfa633b54c1..aa309363185 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/index.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/index.js @@ -3,10 +3,10 @@ const analyzers = require('./analyzers') const setCookiesHeaderInterceptor = require('./set-cookies-header-interceptor') -function enableAllAnalyzers () { - setCookiesHeaderInterceptor.configure(true) +function enableAllAnalyzers (tracerConfig) { + setCookiesHeaderInterceptor.configure({ enabled: true, tracerConfig }) for (const analyzer in analyzers) { - analyzers[analyzer].configure(true) + analyzers[analyzer].configure({ enabled: true, tracerConfig }) } } diff --git a/packages/dd-trace/src/appsec/iast/analyzers/missing-header-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/missing-header-analyzer.js new file mode 100644 index 00000000000..16578b5e427 --- /dev/null +++ b/packages/dd-trace/src/appsec/iast/analyzers/missing-header-analyzer.js @@ -0,0 +1,66 @@ +'use strict' + +const Analyzer = require('./vulnerability-analyzer') + +const SC_MOVED_PERMANENTLY = 301 +const SC_MOVED_TEMPORARILY = 302 +const SC_NOT_MODIFIED = 304 +const SC_TEMPORARY_REDIRECT = 307 +const SC_NOT_FOUND = 404 +const SC_GONE = 410 +const SC_INTERNAL_SERVER_ERROR = 500 + +const IGNORED_RESPONSE_STATUS_LIST = [SC_MOVED_PERMANENTLY, SC_MOVED_TEMPORARILY, SC_NOT_MODIFIED, + SC_TEMPORARY_REDIRECT, SC_NOT_FOUND, SC_GONE, SC_INTERNAL_SERVER_ERROR] +const HTML_CONTENT_TYPES = ['text/html', 'application/xhtml+xml'] + +class MissingHeaderAnalyzer extends Analyzer { + constructor (type, headerName) { + super(type) + + this.headerName = headerName + } + + onConfigure () { + this.addSub({ + channelName: 'datadog:iast:response-end', + moduleName: 'http' + }, (data) => this.analyze(data)) + } + + _getLocation () { + return undefined + } + + _checkOCE (context) { + return true + } + + _createHashSource (type, evidence, location) { + return `${type}:${this.config.tracerConfig.service}` + } + + _getEvidence ({ res }) { + return { value: res.getHeader(this.headerName) } + } + + _isVulnerable ({ req, res }, context) { + if (!IGNORED_RESPONSE_STATUS_LIST.includes(res.statusCode) && this._isResponseHtml(res)) { + return this._isVulnerableFromRequestAndResponse(req, res) + } + return false + } + + _isVulnerableFromRequestAndResponse (req, res) { + return false + } + + _isResponseHtml (res) { + const contentType = res.getHeader('content-type') + return contentType && HTML_CONTENT_TYPES.some(htmlContentType => { + return htmlContentType === contentType || contentType.startsWith(htmlContentType + ';') + }) + } +} + +module.exports = { MissingHeaderAnalyzer } diff --git a/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js index 5324546802d..5cb27659945 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js @@ -4,7 +4,11 @@ const InjectionAnalyzer = require('./injection-analyzer') const { UNVALIDATED_REDIRECT } = require('../vulnerabilities') const { getNodeModulesPaths } = require('../path-line') const { getRanges } = require('../taint-tracking/operations') -const { HTTP_REQUEST_HEADER_VALUE } = require('../taint-tracking/source-types') +const { + HTTP_REQUEST_HEADER_VALUE, + HTTP_REQUEST_PATH, + HTTP_REQUEST_PATH_PARAM +} = require('../taint-tracking/source-types') const EXCLUDED_PATHS = getNodeModulesPaths('express/lib/response.js') @@ -17,9 +21,6 @@ class UnvalidatedRedirectAnalyzer extends InjectionAnalyzer { this.addSub('datadog:http:server:response:set-header:finish', ({ name, value }) => this.analyze(name, value)) } - // TODO: In case the location header value is tainted, this analyzer should check the ranges of the tainted. - // And do not report a vulnerability if source of the ranges (range.iinfo.type) are exclusively url or path params - // to avoid false positives. analyze (name, value) { if (!this.isLocationHeader(name) || typeof value !== 'string') return @@ -34,12 +35,28 @@ class UnvalidatedRedirectAnalyzer extends InjectionAnalyzer { if (!value) return false const ranges = getRanges(iastContext, value) - return ranges && ranges.length > 0 && !this._isRefererHeader(ranges) + return ranges && ranges.length > 0 && !this._areSafeRanges(ranges) } - _isRefererHeader (ranges) { - return ranges && ranges.every(range => range.iinfo.type === HTTP_REQUEST_HEADER_VALUE && - range.iinfo.parameterName && range.iinfo.parameterName.toLowerCase() === 'referer') + // Do not report vulnerability if ranges sources are exclusively url, + // path params or referer header to avoid false positives. + _areSafeRanges (ranges) { + return ranges && ranges.every( + range => this._isPathParam(range) || this._isUrl(range) || this._isRefererHeader(range) + ) + } + + _isRefererHeader (range) { + return range.iinfo.type === HTTP_REQUEST_HEADER_VALUE && + range.iinfo.parameterName && range.iinfo.parameterName.toLowerCase() === 'referer' + } + + _isPathParam (range) { + return range.iinfo.type === HTTP_REQUEST_PATH_PARAM + } + + _isUrl (range) { + return range.iinfo.type === HTTP_REQUEST_PATH } _getExcludedPaths () { diff --git a/packages/dd-trace/src/appsec/iast/analyzers/xcontenttype-header-missing-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/xcontenttype-header-missing-analyzer.js new file mode 100644 index 00000000000..0d10a8952df --- /dev/null +++ b/packages/dd-trace/src/appsec/iast/analyzers/xcontenttype-header-missing-analyzer.js @@ -0,0 +1,19 @@ +'use strict' + +const { XCONTENTTYPE_HEADER_MISSING } = require('../vulnerabilities') +const { MissingHeaderAnalyzer } = require('./missing-header-analyzer') + +const XCONTENTTYPEOPTIONS_HEADER_NAME = 'X-Content-Type-Options' + +class XcontenttypeHeaderMissingAnalyzer extends MissingHeaderAnalyzer { + constructor () { + super(XCONTENTTYPE_HEADER_MISSING, XCONTENTTYPEOPTIONS_HEADER_NAME) + } + + _isVulnerableFromRequestAndResponse (req, res) { + const headerToCheck = res.getHeader(XCONTENTTYPEOPTIONS_HEADER_NAME) + return !headerToCheck || headerToCheck.trim().toLowerCase() !== 'nosniff' + } +} + +module.exports = new XcontenttypeHeaderMissingAnalyzer() diff --git a/packages/dd-trace/src/appsec/iast/index.js b/packages/dd-trace/src/appsec/iast/index.js index 16697979ed9..1abfcadac62 100644 --- a/packages/dd-trace/src/appsec/iast/index.js +++ b/packages/dd-trace/src/appsec/iast/index.js @@ -19,10 +19,11 @@ const iastTelemetry = require('./telemetry') // order of the callbacks can be enforce const requestStart = dc.channel('dd-trace:incomingHttpRequestStart') const requestClose = dc.channel('dd-trace:incomingHttpRequestEnd') +const iastResponseEnd = dc.channel('datadog:iast:response-end') function enable (config, _tracer) { iastTelemetry.configure(config, config.iast && config.iast.telemetryVerbosity) - enableAllAnalyzers() + enableAllAnalyzers(config) enableTaintTracking(config.iast, iastTelemetry.verbosity) requestStart.subscribe(onIncomingHttpRequestStart) requestClose.subscribe(onIncomingHttpRequestEnd) @@ -54,7 +55,7 @@ function onIncomingHttpRequestStart (data) { createTransaction(rootSpan.context().toSpanId(), iastContext) overheadController.initializeRequestContext(iastContext) iastTelemetry.onRequestStart(iastContext) - taintTrackingPlugin.taintHeaders(data.req.headers, iastContext) + taintTrackingPlugin.taintRequest(data.req, iastContext) } if (rootSpan.addTags) { rootSpan.addTags({ @@ -72,6 +73,8 @@ function onIncomingHttpRequestEnd (data) { const topContext = web.getContext(data.req) const iastContext = iastContextFunctions.getIastContext(store, topContext) if (iastContext && iastContext.rootSpan) { + iastResponseEnd.publish(data) + const vulnerabilities = iastContext.vulnerabilities const rootSpan = iastContext.rootSpan vulnerabilityReporter.sendVulnerabilities(vulnerabilities, rootSpan) diff --git a/packages/dd-trace/src/appsec/iast/taint-tracking/index.js b/packages/dd-trace/src/appsec/iast/taint-tracking/index.js index 37747be4bb3..7d6003c9838 100644 --- a/packages/dd-trace/src/appsec/iast/taint-tracking/index.js +++ b/packages/dd-trace/src/appsec/iast/taint-tracking/index.js @@ -1,11 +1,13 @@ 'use strict' const { enableRewriter, disableRewriter } = require('./rewriter') -const { createTransaction, +const { + createTransaction, removeTransaction, setMaxTransactions, enableTaintOperations, - disableTaintOperations } = require('./operations') + disableTaintOperations +} = require('./operations') const taintTrackingPlugin = require('./plugin') diff --git a/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js b/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js index 0ff184867f6..b7fd1b6cec2 100644 --- a/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +++ b/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js @@ -3,7 +3,7 @@ const { SourceIastPlugin } = require('../iast-plugin') const { getIastContext } = require('../iast-context') const { storage } = require('../../../../../datadog-core') -const { taintObject } = require('./operations') +const { taintObject, newTaintedString } = require('./operations') const { HTTP_REQUEST_BODY, HTTP_REQUEST_COOKIE_VALUE, @@ -11,6 +11,7 @@ const { HTTP_REQUEST_HEADER_VALUE, HTTP_REQUEST_HEADER_NAME, HTTP_REQUEST_PARAMETER, + HTTP_REQUEST_PATH, HTTP_REQUEST_PATH_PARAM } = require('./source-types') @@ -89,6 +90,21 @@ class TaintTrackingPlugin extends SourceIastPlugin { }) } + taintUrl (req, iastContext) { + this.execSource({ + handler: function () { + req.url = newTaintedString(iastContext, req.url, 'req.url', HTTP_REQUEST_PATH) + }, + tag: [HTTP_REQUEST_PATH], + iastContext + }) + } + + taintRequest (req, iastContext) { + this.taintHeaders(req.headers, iastContext) + this.taintUrl(req, iastContext) + } + enable () { this.configure(true) } diff --git a/packages/dd-trace/src/appsec/iast/taint-tracking/source-types.js b/packages/dd-trace/src/appsec/iast/taint-tracking/source-types.js index dfc8cc573dc..aad90ef6ad3 100644 --- a/packages/dd-trace/src/appsec/iast/taint-tracking/source-types.js +++ b/packages/dd-trace/src/appsec/iast/taint-tracking/source-types.js @@ -7,5 +7,6 @@ module.exports = { HTTP_REQUEST_HEADER_NAME: 'http.request.header.name', HTTP_REQUEST_HEADER_VALUE: 'http.request.header', HTTP_REQUEST_PARAMETER: 'http.request.parameter', + HTTP_REQUEST_PATH: 'http.request.path', HTTP_REQUEST_PATH_PARAM: 'http.request.path.parameter' } diff --git a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js index 2f6a8103686..989b838bd01 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +++ b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js @@ -54,7 +54,11 @@ class VulnerabilityFormatter { formatEvidence (type, evidence, sourcesIndexes, sources) { if (!evidence.ranges) { - return { value: evidence.value } + if (typeof evidence.value === 'undefined') { + return undefined + } else { + return { value: evidence.value } + } } return this._redactVulnearbilities diff --git a/packages/dd-trace/src/appsec/iast/vulnerabilities.js b/packages/dd-trace/src/appsec/iast/vulnerabilities.js index f89f3b1397c..93fdd9c67c9 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerabilities.js +++ b/packages/dd-trace/src/appsec/iast/vulnerabilities.js @@ -1,5 +1,6 @@ module.exports = { COMMAND_INJECTION: 'COMMAND_INJECTION', + HSTS_HEADER_MISSING: 'HSTS_HEADER_MISSING', INSECURE_COOKIE: 'INSECURE_COOKIE', LDAP_INJECTION: 'LDAP_INJECTION', NO_HTTPONLY_COOKIE: 'NO_HTTPONLY_COOKIE', @@ -9,5 +10,6 @@ module.exports = { SSRF: 'SSRF', UNVALIDATED_REDIRECT: 'UNVALIDATED_REDIRECT', WEAK_CIPHER: 'WEAK_CIPHER', - WEAK_HASH: 'WEAK_HASH' + WEAK_HASH: 'WEAK_HASH', + XCONTENTTYPE_HEADER_MISSING: 'XCONTENTTYPE_HEADER_MISSING' } diff --git a/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js b/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js index 67a0f0855ed..4041a25cc96 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +++ b/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js @@ -28,7 +28,7 @@ function addVulnerability (iastContext, vulnerability) { function isValidVulnerability (vulnerability) { return vulnerability && vulnerability.type && - vulnerability.evidence && vulnerability.evidence.value && + vulnerability.evidence && vulnerability.location && vulnerability.location.spanId } diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index eb901233d46..a436e39f670 100644 --- a/packages/dd-trace/src/config.js +++ b/packages/dd-trace/src/config.js @@ -278,7 +278,7 @@ class Config { process.env.DD_TRACE_EXPERIMENTAL_B3_ENABLED, false ) - const defaultPropagationStyle = ['tracecontext', 'datadog'] + const defaultPropagationStyle = ['datadog', 'tracecontext'] if (isTrue(DD_TRACE_B3_ENABLED)) { defaultPropagationStyle.push('b3') defaultPropagationStyle.push('b3 single header') @@ -318,7 +318,10 @@ class Config { false ) const DD_TRACE_SPAN_ATTRIBUTE_SCHEMA = validateNamingVersion( - process.env.DD_TRACE_SPAN_ATTRIBUTE_SCHEMA + coalesce( + options.spanAttributeSchema, + process.env.DD_TRACE_SPAN_ATTRIBUTE_SCHEMA + ) ) const DD_TRACE_PEER_SERVICE_MAPPING = coalesce( options.peerServiceMapping, @@ -326,11 +329,27 @@ class Config { process.env.DD_TRACE_PEER_SERVICE_MAPPING.split(',').map(x => x.trim().split(':')) ) : {} ) - const DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED = process.env.DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED + + const peerServiceSet = ( + options.hasOwnProperty('spanComputePeerService') || + process.env.hasOwnProperty('DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED') + ) + const peerServiceValue = coalesce( + options.spanComputePeerService, + process.env.DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED + ) + + const DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED = ( + DD_TRACE_SPAN_ATTRIBUTE_SCHEMA === 'v0' + // In v0, peer service is computed only if it is explicitly set to true + ? peerServiceSet && isTrue(peerServiceValue) + // In >v0, peer service is false only if it is explicitly set to false + : (peerServiceSet ? !isFalse(peerServiceValue) : true) + ) const DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED = coalesce( - isTrue(process.env.DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED), - false + options.spanRemoveIntegrationFromService, + isTrue(process.env.DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED) ) const DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH = coalesce( process.env.DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH, @@ -562,11 +581,8 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?) exporters: DD_PROFILING_EXPORTERS } this.spanAttributeSchema = DD_TRACE_SPAN_ATTRIBUTE_SCHEMA - this.spanComputePeerService = (this.spanAttributeSchema === 'v0' - ? isTrue(DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED) - : true - ) - this.traceRemoveIntegrationServiceNamesEnabled = DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED + this.spanComputePeerService = DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED + this.spanRemoveIntegrationFromService = DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED this.peerServiceMapping = DD_TRACE_PEER_SERVICE_MAPPING this.lookup = options.lookup this.startupLogs = isTrue(DD_TRACE_STARTUP_LOGS) diff --git a/packages/dd-trace/src/external-logger/src/index.js b/packages/dd-trace/src/external-logger/src/index.js index fe8bc6fe87b..20a6466874d 100644 --- a/packages/dd-trace/src/external-logger/src/index.js +++ b/packages/dd-trace/src/external-logger/src/index.js @@ -127,4 +127,12 @@ class ExternalLogger { } } -module.exports = ExternalLogger +class NoopExternalLogger { + log () { } + enqueue () { } + shutdown () { } + flush () { } +} + +module.exports.ExternalLogger = ExternalLogger +module.exports.NoopExternalLogger = NoopExternalLogger diff --git a/packages/dd-trace/src/external-logger/test/index.spec.js b/packages/dd-trace/src/external-logger/test/index.spec.js index e856e78be3d..46cd44fa058 100644 --- a/packages/dd-trace/src/external-logger/test/index.spec.js +++ b/packages/dd-trace/src/external-logger/test/index.spec.js @@ -15,7 +15,7 @@ describe('External Logger', () => { beforeEach(() => { errorLog = sinon.spy(tracerLogger, 'error') - const ExternalLogger = proxyquire('../src', { + const { ExternalLogger } = proxyquire('../src', { '../../log': { error: errorLog } diff --git a/packages/dd-trace/src/format.js b/packages/dd-trace/src/format.js index f871818c4cb..297eb1fd7d3 100644 --- a/packages/dd-trace/src/format.js +++ b/packages/dd-trace/src/format.js @@ -107,7 +107,7 @@ function extractTags (trace, span) { } } - setSingleSpanIngestionTags(trace, context._sampling.spanSampling) + setSingleSpanIngestionTags(trace, context._spanSampling) addTag(trace.meta, trace.metrics, 'language', 'javascript') addTag(trace.meta, trace.metrics, PROCESS_ID, process.pid) diff --git a/packages/dd-trace/src/lambda/handler.js b/packages/dd-trace/src/lambda/handler.js index 6279c3d2d20..88457ce9d07 100644 --- a/packages/dd-trace/src/lambda/handler.js +++ b/packages/dd-trace/src/lambda/handler.js @@ -86,6 +86,13 @@ exports.datadog = function datadog (lambdaHandler) { const context = extractContext(args) checkTimeout(context) - return lambdaHandler.apply(this, args).then((res) => { clearTimeout(__lambdaTimeout); return res }) + const result = lambdaHandler.apply(this, args) + if (result && typeof result.then === 'function') { + return result.then((res) => { + clearTimeout(__lambdaTimeout) + return res + }) + } + return result } } diff --git a/packages/dd-trace/src/opentelemetry/span.js b/packages/dd-trace/src/opentelemetry/span.js index f2c3e277c6a..8a8beea4169 100644 --- a/packages/dd-trace/src/opentelemetry/span.js +++ b/packages/dd-trace/src/opentelemetry/span.js @@ -10,6 +10,7 @@ const { timeInputToHrTime } = require('@opentelemetry/core') const tracer = require('../../') const DatadogSpan = require('../opentracing/span') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../constants') +const { SERVICE_NAME, RESOURCE_NAME } = require('../../../../ext/tags') const SpanContext = require('./span_context') @@ -40,7 +41,8 @@ class Span { hostname: _tracer._hostname, integrationName: 'otel', tags: { - 'service.name': _tracer._service + [SERVICE_NAME]: _tracer._service, + [RESOURCE_NAME]: spanName } }, _tracer._debug) diff --git a/packages/dd-trace/src/opentracing/span_context.js b/packages/dd-trace/src/opentracing/span_context.js index 95acf243205..a788d8ab2cd 100644 --- a/packages/dd-trace/src/opentracing/span_context.js +++ b/packages/dd-trace/src/opentracing/span_context.js @@ -12,7 +12,8 @@ class DatadogSpanContext { this._name = props.name this._isFinished = props.isFinished || false this._tags = props.tags || {} - this._sampling = Object.assign({}, props.sampling) + this._sampling = props.sampling || {} + this._spanSampling = undefined this._baggageItems = props.baggageItems || {} this._traceparent = props.traceparent this._tracestate = props.tracestate diff --git a/packages/dd-trace/src/plugins/util/web.js b/packages/dd-trace/src/plugins/util/web.js index 50824bcfda4..acb8a8cc92c 100644 --- a/packages/dd-trace/src/plugins/util/web.js +++ b/packages/dd-trace/src/plugins/util/web.js @@ -103,6 +103,7 @@ const web = { context.res = res this.setConfig(req, config) + addRequestTags(context) return span }, diff --git a/packages/dd-trace/src/profiling/config.js b/packages/dd-trace/src/profiling/config.js index 0f94dcbcb99..3e53c04fc6a 100644 --- a/packages/dd-trace/src/profiling/config.js +++ b/packages/dd-trace/src/profiling/config.js @@ -18,7 +18,6 @@ class Config { const { DD_PROFILING_ENABLED, DD_PROFILING_PROFILERS, - DD_PROFILING_ENDPOINT_COLLECTION_ENABLED, DD_ENV, DD_TAGS, DD_SERVICE, @@ -36,7 +35,9 @@ class Config { DD_PROFILING_EXPERIMENTAL_OOM_MONITORING_ENABLED, DD_PROFILING_EXPERIMENTAL_OOM_HEAP_LIMIT_EXTENSION_SIZE, DD_PROFILING_EXPERIMENTAL_OOM_MAX_HEAP_EXTENSION_COUNT, - DD_PROFILING_EXPERIMENTAL_OOM_EXPORT_STRATEGIES + DD_PROFILING_EXPERIMENTAL_OOM_EXPORT_STRATEGIES, + DD_PROFILING_EXPERIMENTAL_CODEHOTSPOTS_ENABLED, + DD_PROFILING_EXPERIMENTAL_ENDPOINT_COLLECTION_ENABLED } = process.env const enabled = isTrue(coalesce(options.enabled, DD_PROFILING_ENABLED, true)) @@ -51,8 +52,8 @@ class Config { Number(DD_PROFILING_UPLOAD_TIMEOUT), 60 * 1000) const sourceMap = coalesce(options.sourceMap, DD_PROFILING_SOURCE_MAP, true) - const endpointCollection = coalesce(options.endpointCollection, - DD_PROFILING_ENDPOINT_COLLECTION_ENABLED, false) + const endpointCollectionEnabled = coalesce(options.endpointCollection, + DD_PROFILING_EXPERIMENTAL_ENDPOINT_COLLECTION_ENABLED, false) const pprofPrefix = coalesce(options.pprofPrefix, DD_PROFILING_PPROF_PREFIX, '') @@ -73,7 +74,7 @@ class Config { this.uploadTimeout = uploadTimeout this.sourceMap = sourceMap this.debugSourceMaps = isTrue(coalesce(options.debugSourceMaps, DD_PROFILING_DEBUG_SOURCE_MAPS, false)) - this.endpointCollection = endpointCollection + this.endpointCollectionEnabled = endpointCollectionEnabled this.pprofPrefix = pprofPrefix const hostname = coalesce(options.hostname, DD_AGENT_HOST) || 'localhost' @@ -110,6 +111,8 @@ class Config { const profilers = options.profilers ? options.profilers : getProfilers({ DD_PROFILING_HEAP_ENABLED, DD_PROFILING_WALLTIME_ENABLED, DD_PROFILING_PROFILERS }) + this.codeHotspotsEnabled = isTrue(coalesce(options.codeHotspotsEnabled, + DD_PROFILING_EXPERIMENTAL_CODEHOTSPOTS_ENABLED, false)) this.profilers = ensureProfilers(profilers, this) } diff --git a/packages/dd-trace/src/profiling/exporters/agent.js b/packages/dd-trace/src/profiling/exporters/agent.js index 6adb61d17cd..712d03f1406 100644 --- a/packages/dd-trace/src/profiling/exporters/agent.js +++ b/packages/dd-trace/src/profiling/exporters/agent.js @@ -1,7 +1,8 @@ 'use strict' const retry = require('retry') -const { request } = require('http') +const { request: httpRequest } = require('http') +const { request: httpsRequest } = require('https') // TODO: avoid using dd-trace internals. Make this a separate module? const docker = require('../../exporters/common/docker') @@ -12,6 +13,8 @@ const version = require('../../../../../package.json').version const containerId = docker.id() function sendRequest (options, form, callback) { + const request = options.protocol === 'https:' ? httpsRequest : httpRequest + const store = storage.getStore() storage.enterWith({ noop: true }) const req = request(options, res => { diff --git a/packages/dd-trace/src/profiling/profiler.js b/packages/dd-trace/src/profiling/profiler.js index 47dde4c7e57..c72fa3b6fba 100644 --- a/packages/dd-trace/src/profiling/profiler.js +++ b/packages/dd-trace/src/profiling/profiler.js @@ -23,7 +23,7 @@ class Profiler extends EventEmitter { } start (options) { - this._start(options).catch(() => {}) + this._start(options).catch((err) => { if (options.logger) options.logger.error(err) }) return this } diff --git a/packages/dd-trace/src/profiling/profilers/wall.js b/packages/dd-trace/src/profiling/profilers/wall.js index 820c035040f..5ce8c1172b4 100644 --- a/packages/dd-trace/src/profiling/profilers/wall.js +++ b/packages/dd-trace/src/profiling/profilers/wall.js @@ -1,23 +1,111 @@ 'use strict' +const { storage } = require('../../../../datadog-core') + +const dc = require('../../../../diagnostics_channel') + +const beforeCh = dc.channel('dd-trace:storage:before') +const enterCh = dc.channel('dd-trace:storage:enter') + +let kSampleCount + +function getActiveSpan () { + const store = storage.getStore() + return store && store.span +} + +function getStartedSpans (context) { + return context._trace.started +} + +function generateLabels ({ spanId, rootSpanId, webTags, endpoint }) { + const labels = {} + if (spanId) { + labels['span id'] = spanId + } + if (rootSpanId) { + labels['local root span id'] = rootSpanId + } + if (webTags && Object.keys(webTags).length !== 0) { + labels['trace endpoint'] = endpointNameFromTags(webTags) + } else if (endpoint) { + // fallback to endpoint computed when sample was taken + labels['trace endpoint'] = endpoint + } + + return labels +} + +function getSpanContextTags (span) { + return span.context()._tags +} + +function isWebServerSpan (tags) { + return tags['span.type'] === 'web' +} + +function endpointNameFromTags (tags) { + return tags['resource.name'] || [ + tags['http.method'], + tags['http.route'] + ].filter(v => v).join(' ') +} + +function updateContext (context, span, startedSpans, endpointCollectionEnabled) { + context.spanId = span.context().toSpanId() + const rootSpan = startedSpans[0] + if (rootSpan) { + context.rootSpanId = rootSpan.context().toSpanId() + if (endpointCollectionEnabled) { + // Find the first webspan starting from the end: + // There might be several webspans, for example with next.js, http plugin creates a first span + // and then next.js plugin creates a child span, and this child span haves the correct endpoint information. + for (let i = startedSpans.length - 1; i >= 0; i--) { + const tags = getSpanContextTags(startedSpans[i]) + if (isWebServerSpan(tags)) { + context.webTags = tags + // endpoint may not be determined yet, but keep it as fallback + // if tags are not available anymore during serialization + context.endpoint = endpointNameFromTags(tags) + break + } + } + } + } +} + class NativeWallProfiler { constructor (options = {}) { this.type = 'wall' this._samplingIntervalMicros = options.samplingInterval || 1e6 / 99 // 99hz this._flushIntervalMillis = options.flushInterval || 60 * 1e3 // 60 seconds this._codeHotspotsEnabled = !!options.codeHotspotsEnabled + this._endpointCollectionEnabled = !!options.endpointCollectionEnabled this._mapper = undefined this._pprof = undefined + // Bind to this so the same value can be used to unsubscribe later + this._enter = this._enter.bind(this) this._logger = options.logger this._started = false } + codeHotspotsEnabled () { + return this._codeHotspotsEnabled + } + start ({ mapper } = {}) { if (this._started) return + if (this._codeHotspotsEnabled && !this._emittedFFMessage && this._logger) { + this._logger.debug( + `Wall profiler: Enable config_trace_show_breakdown_profiling_for_node feature flag to see code hotspots.`) + this._emittedFFMessage = true + } + this._mapper = mapper this._pprof = require('@datadog/pprof') + kSampleCount = this._pprof.time.constants.kSampleCount // pprof otherwise crashes in worker threads if (!process._startProfilerIdleNotifier) { @@ -31,16 +119,62 @@ class NativeWallProfiler { intervalMicros: this._samplingIntervalMicros, durationMillis: this._flushIntervalMillis, sourceMapper: this._mapper, - customLabels: this._codeHotspotsEnabled, + withContexts: this._codeHotspotsEnabled, lineNumbers: false }) + if (this._codeHotspotsEnabled) { + this._profilerState = this._pprof.time.getState() + this._currentContext = {} + this._pprof.time.setContext(this._currentContext) + this._lastSpan = undefined + this._lastStartedSpans = undefined + this._lastSampleCount = 0 + + beforeCh.subscribe(this._enter) + enterCh.subscribe(this._enter) + } + this._started = true } - profile () { + _enter () { if (!this._started) return - return this._pprof.time.stop(true) + + const sampleCount = this._profilerState[kSampleCount] + if (sampleCount !== this._lastSampleCount) { + this._lastSampleCount = sampleCount + const context = this._currentContext + this._currentContext = {} + this._pprof.time.setContext(this._currentContext) + + if (this._lastSpan) { + updateContext(context, this._lastSpan, this._lastStartedSpans, this._endpointCollectionEnabled) + } + } + + const span = getActiveSpan() + if (span) { + this._lastSpan = span + this._lastStartedSpans = getStartedSpans(span.context()) + } else { + this._lastStartedSpans = undefined + this._lastSpan = undefined + } + } + + _stop (restart) { + if (!this._started) return + if (this._codeHotspotsEnabled) { + // update last sample context if needed + this._enter() + this._lastSampleCount = 0 + } + return this._pprof.time.stop(restart, this._codeHotspotsEnabled ? generateLabels : undefined) + } + + profile () { + return this._stop(true) } encode (profile) { @@ -50,7 +184,13 @@ class NativeWallProfiler { stop () { if (!this._started) return - const profile = this._pprof.time.stop() + const profile = this._stop(false) + if (this._codeHotspotsEnabled) { + beforeCh.unsubscribe(this._enter) + enterCh.subscribe(this._enter) + this._profilerState = undefined + } + this._started = false return profile } diff --git a/packages/dd-trace/src/service-naming/index.js b/packages/dd-trace/src/service-naming/index.js index a3b184e4049..7de60eacfda 100644 --- a/packages/dd-trace/src/service-naming/index.js +++ b/packages/dd-trace/src/service-naming/index.js @@ -3,7 +3,7 @@ const { schemaDefinitions } = require('./schemas') class SchemaManager { constructor () { this.schemas = schemaDefinitions - this.config = { spanAttributeSchema: 'v0', traceRemoveIntegrationServiceNamesEnabled: false } + this.config = { spanAttributeSchema: 'v0', spanRemoveIntegrationFromService: false } } get schema () { @@ -15,7 +15,7 @@ class SchemaManager { } get shouldUseConsistentServiceNaming () { - return this.config.traceRemoveIntegrationServiceNamesEnabled && this.version === 'v0' + return this.config.spanRemoveIntegrationFromService && this.version === 'v0' } opName (type, kind, plugin, ...opNameArgs) { diff --git a/packages/dd-trace/src/service-naming/schemas/v0/graphql.js b/packages/dd-trace/src/service-naming/schemas/v0/graphql.js new file mode 100644 index 00000000000..db0c63778f4 --- /dev/null +++ b/packages/dd-trace/src/service-naming/schemas/v0/graphql.js @@ -0,0 +1,12 @@ +const { identityService } = require('../util') + +const graphql = { + server: { + graphql: { + opName: () => 'graphql.execute', + serviceName: identityService + } + } +} + +module.exports = graphql diff --git a/packages/dd-trace/src/service-naming/schemas/v0/index.js b/packages/dd-trace/src/service-naming/schemas/v0/index.js index e2ee3f60217..c2751a64bf0 100644 --- a/packages/dd-trace/src/service-naming/schemas/v0/index.js +++ b/packages/dd-trace/src/service-naming/schemas/v0/index.js @@ -1,6 +1,7 @@ const SchemaDefinition = require('../definition') const messaging = require('./messaging') const storage = require('./storage') +const graphql = require('./graphql') const web = require('./web') -module.exports = new SchemaDefinition({ messaging, storage, web }) +module.exports = new SchemaDefinition({ messaging, storage, web, graphql }) diff --git a/packages/dd-trace/src/service-naming/schemas/v1/graphql.js b/packages/dd-trace/src/service-naming/schemas/v1/graphql.js new file mode 100644 index 00000000000..1a207d807c2 --- /dev/null +++ b/packages/dd-trace/src/service-naming/schemas/v1/graphql.js @@ -0,0 +1,12 @@ +const { identityService } = require('../util') + +const graphql = { + server: { + graphql: { + opName: () => 'graphql.server.request', + serviceName: identityService + } + } +} + +module.exports = graphql diff --git a/packages/dd-trace/src/service-naming/schemas/v1/index.js b/packages/dd-trace/src/service-naming/schemas/v1/index.js index e2ee3f60217..c2751a64bf0 100644 --- a/packages/dd-trace/src/service-naming/schemas/v1/index.js +++ b/packages/dd-trace/src/service-naming/schemas/v1/index.js @@ -1,6 +1,7 @@ const SchemaDefinition = require('../definition') const messaging = require('./messaging') const storage = require('./storage') +const graphql = require('./graphql') const web = require('./web') -module.exports = new SchemaDefinition({ messaging, storage, web }) +module.exports = new SchemaDefinition({ messaging, storage, web, graphql }) diff --git a/packages/dd-trace/src/span_processor.js b/packages/dd-trace/src/span_processor.js index aea348b11fb..c6e8c300529 100644 --- a/packages/dd-trace/src/span_processor.js +++ b/packages/dd-trace/src/span_processor.js @@ -138,10 +138,6 @@ class SpanProcessor { } } - for (const span of trace.finished) { - span.context()._tags = {} - } - trace.started = active trace.finished = [] } diff --git a/packages/dd-trace/src/span_sampler.js b/packages/dd-trace/src/span_sampler.js index e2a0ecc24a9..d43fba63cc8 100644 --- a/packages/dd-trace/src/span_sampler.js +++ b/packages/dd-trace/src/span_sampler.js @@ -82,7 +82,7 @@ class SpanSampler { const rule = this.findRule(service, name) if (rule && rule.sample()) { - span.context()._sampling.spanSampling = { + span.context()._spanSampling = { sampleRate: rule.sampleRate, maxPerSecond: rule.maxPerSecond } diff --git a/packages/dd-trace/src/telemetry/dependencies.js b/packages/dd-trace/src/telemetry/dependencies.js index 3d190a7cde3..7dbdde5a71f 100644 --- a/packages/dd-trace/src/telemetry/dependencies.js +++ b/packages/dd-trace/src/telemetry/dependencies.js @@ -7,8 +7,10 @@ const { sendData } = require('./send-data') const dc = require('../../../diagnostics_channel') const { fileURLToPath } = require('url') -const savedDependencies = new Set() -const detectedDependencyNames = new Set() +const savedDependenciesToSend = new Set() +const detectedDependencyKeys = new Set() +const detectedDependencyVersions = new Set() + const FILE_URI_START = `file://` const moduleLoadStartChannel = dc.channel('dd-trace:moduleLoadStart') @@ -18,14 +20,14 @@ function waitAndSend (config, application, host) { if (!immediate) { immediate = setImmediate(() => { immediate = null - if (savedDependencies.size > 0) { - const dependencies = Array.from(savedDependencies.values()).splice(0, 1000).map(pair => { - savedDependencies.delete(pair) + if (savedDependenciesToSend.size > 0) { + const dependencies = Array.from(savedDependenciesToSend.values()).splice(0, 1000).map(pair => { + savedDependenciesToSend.delete(pair) const [name, version] = pair.split(' ') return { name, version } }) sendData(config, application, host, 'app-dependencies-loaded', { dependencies }) - if (savedDependencies.size > 0) { + if (savedDependenciesToSend.size > 0) { waitAndSend(config, application, host) } } @@ -46,15 +48,24 @@ function onModuleLoad (data) { } const parseResult = filename && parse(filename) const request = data.request || (parseResult && parseResult.name) - if (filename && request && isDependency(filename, request) && !detectedDependencyNames.has(request)) { - detectedDependencyNames.add(request) + const dependencyKey = parseResult && parseResult.basedir ? parseResult.basedir : request + + if (filename && request && isDependency(filename, request) && !detectedDependencyKeys.has(dependencyKey)) { + detectedDependencyKeys.add(dependencyKey) + if (parseResult) { const { name, basedir } = parseResult if (basedir) { try { const { version } = requirePackageJson(basedir, module) - savedDependencies.add(`${name} ${version}`) - waitAndSend(config, application, host) + const dependencyAndVersion = `${name} ${version}` + + if (!detectedDependencyVersions.has(dependencyAndVersion)) { + savedDependenciesToSend.add(dependencyAndVersion) + detectedDependencyVersions.add(dependencyAndVersion) + + waitAndSend(config, application, host) + } } catch (e) { // can not read the package.json, do nothing } @@ -88,8 +99,9 @@ function stop () { config = null application = null host = null - detectedDependencyNames.clear() - savedDependencies.clear() + detectedDependencyKeys.clear() + savedDependenciesToSend.clear() + detectedDependencyVersions.clear() if (moduleLoadStartChannel.hasSubscribers) { moduleLoadStartChannel.unsubscribe(onModuleLoad) } diff --git a/packages/dd-trace/src/telemetry/metrics.js b/packages/dd-trace/src/telemetry/metrics.js index 7348daaa2f8..fbdb568b260 100644 --- a/packages/dd-trace/src/telemetry/metrics.js +++ b/packages/dd-trace/src/telemetry/metrics.js @@ -25,6 +25,10 @@ function mapToJsonArray (map) { return Array.from(map.values()).map(v => v.toJSON()) } +function hasPoints (metric) { + return metric.points.length > 0 +} + class Metric { constructor (namespace, metric, common, tags) { this.namespace = namespace.toString() @@ -172,10 +176,16 @@ class MetricsCollection extends Map { toJSON () { if (!this.size) return + + const series = mapToJsonArray(this) + .filter(hasPoints) + + if (!series.length) return + const { namespace } = this return { namespace, - series: mapToJsonArray(this) + series } } } diff --git a/packages/dd-trace/test/appsec/iast/analyzers/hsts-header-missing-analyzer.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/hsts-header-missing-analyzer.spec.js new file mode 100644 index 00000000000..f39a537c40e --- /dev/null +++ b/packages/dd-trace/test/appsec/iast/analyzers/hsts-header-missing-analyzer.spec.js @@ -0,0 +1,104 @@ +'use strict' + +const { prepareTestServerForIast } = require('../utils') +const Analyzer = require('../../../../src/appsec/iast/analyzers/vulnerability-analyzer') +const { HSTS_HEADER_MISSING } = require('../../../../src/appsec/iast/vulnerabilities') +const axios = require('axios') +const analyzer = new Analyzer() + +describe('hsts header missing analyzer', () => { + it('Expected vulnerability identifier', () => { + expect(HSTS_HEADER_MISSING).to.be.equals('HSTS_HEADER_MISSING') + }) + + prepareTestServerForIast('hsts header missing analyzer', + (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability, config) => { + function makeRequestWithXFordwardedProtoHeader (done) { + axios.get(`http://localhost:${config.port}/`, { + headers: { + 'X-Forwarded-Proto': 'https' + } + }).catch(done) + } + + testThatRequestHasVulnerability((req, res) => { + res.setHeader('content-type', 'text/html') + res.end('