diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 8fb53ba14fa..833243210ca 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -40,6 +40,13 @@ body: validations: required: false + - type: textarea + attributes: + label: Tracer Config + description: "Please provide the `tracer.init(config)` object and any applicable tracer environment variables" + validations: + required: false + - type: input attributes: label: Operating System diff --git a/.github/workflows/package-size.yml b/.github/workflows/package-size.yml index 628614c7dc5..b6fee75c4c4 100644 --- a/.github/workflows/package-size.yml +++ b/.github/workflows/package-size.yml @@ -17,9 +17,9 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: - node-version: '18' + node-version: '20' - run: yarn - name: Compute module size tree and report uses: qard/heaviest-objects-in-the-universe@v1 diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 4822539ecab..79650e6d473 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -35,6 +35,10 @@ jobs: range: '>=5.12.1' aerospike-image: ce-6.4.0.3 test-image: ubuntu-latest + - node-version: 22 + range: '>=6.0.0' + aerospike-image: ce-6.4.0.3 + test-image: ubuntu-latest runs-on: ${{ matrix.test-image }} services: aerospike: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2b37602ddcc..4a06a83c497 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,8 +6,7 @@ stages: - macrobenchmarks include: - #TODO: Remove this before merge - - remote: https://gitlab-templates.ddbuild.io/libdatadog/include-wip/robertomonteromiguel/k8s_new_scenarios/one-pipeline.yml + - remote: https://gitlab-templates.ddbuild.io/libdatadog/include/one-pipeline.yml - local: ".gitlab/benchmarks.yml" - local: ".gitlab/macrobenchmarks.yml" diff --git a/benchmark/sirun/run-all-variants.js b/benchmark/sirun/run-all-variants.js index 60f6a65992d..85894690354 100755 --- a/benchmark/sirun/run-all-variants.js +++ b/benchmark/sirun/run-all-variants.js @@ -14,25 +14,19 @@ const metaJson = require(path.join(process.cwd(), 'meta.json')) const env = Object.assign({}, process.env, { DD_TRACE_STARTUP_LOGS: 'false' }) ;(async () => { - try { - if (metaJson.variants) { - const variants = metaJson.variants - for (const variant in variants) { - const variantEnv = Object.assign({}, env, { SIRUN_VARIANT: variant }) - await exec('sirun', ['meta-temp.json'], { env: variantEnv, stdio: getStdio() }) - } - } else { - await exec('sirun', ['meta-temp.json'], { env, stdio: getStdio() }) + if (metaJson.variants) { + const variants = metaJson.variants + for (const variant in variants) { + const variantEnv = Object.assign({}, env, { SIRUN_VARIANT: variant }) + await exec('sirun', ['meta-temp.json'], { env: variantEnv, stdio: getStdio() }) } + } else { + await exec('sirun', ['meta-temp.json'], { env, stdio: getStdio() }) + } - try { - fs.unlinkSync(path.join(process.cwd(), 'meta-temp.json')) - } catch (e) { - // it's ok if we can't delete a temp file - } + try { + fs.unlinkSync(path.join(process.cwd(), 'meta-temp.json')) } catch (e) { - setImmediate(() => { - throw e // Older Node versions don't fail on uncaught promise rejections. - }) + // it's ok if we can't delete a temp file } })() diff --git a/benchmark/sirun/run-one-variant.js b/benchmark/sirun/run-one-variant.js index 77bb147c9e7..982c303ceae 100755 --- a/benchmark/sirun/run-one-variant.js +++ b/benchmark/sirun/run-one-variant.js @@ -8,12 +8,4 @@ process.env.DD_INSTRUMENTATION_TELEMETRY_ENABLED = 'false' const env = Object.assign({}, process.env, { DD_TRACE_STARTUP_LOGS: 'false' }) -;(async () => { - try { - await exec('sirun', ['meta-temp.json'], { env, stdio: getStdio() }) - } catch (e) { - setImmediate(() => { - throw e // Older Node versions don't fail on uncaught promise rejections. - }) - } -})() +exec('sirun', ['meta-temp.json'], { env, stdio: getStdio() }) diff --git a/docs/package.json b/docs/package.json index 0ec46d7584a..c68302e3eca 100644 --- a/docs/package.json +++ b/docs/package.json @@ -10,7 +10,7 @@ "license": "BSD-3-Clause", "private": true, "devDependencies": { - "typedoc": "^0.25.8", - "typescript": "^4.6" + "typedoc": "^0.25.13", + "typescript": "^4.9.4" } } diff --git a/docs/test.ts b/docs/test.ts index ce34a23d62b..2c2cbea332e 100644 --- a/docs/test.ts +++ b/docs/test.ts @@ -131,6 +131,7 @@ tracer.init({ requestSampling: 50, maxConcurrentRequests: 4, maxContextOperations: 30, + dbRowsToTaint: 12, deduplicationEnabled: true, redactionEnabled: true, redactionNamePattern: 'password', @@ -147,6 +148,7 @@ tracer.init({ requestSampling: 50, maxConcurrentRequests: 4, maxContextOperations: 30, + dbRowsToTaint: 6, deduplicationEnabled: true, redactionEnabled: true, redactionNamePattern: 'password', diff --git a/docs/yarn.lock b/docs/yarn.lock index 4b011ed3db2..4c517dabb07 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -20,9 +20,9 @@ brace-expansion@^2.0.1: balanced-match "^1.0.0" jsonc-parser@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a" - integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA== + version "3.3.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.3.1.tgz#f2a524b4f7fd11e3d791e559977ad60b98b798b4" + integrity sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ== lunr@^2.3.9: version "2.3.9" @@ -35,9 +35,9 @@ marked@^4.3.0: integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== minimatch@^9.0.3: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: brace-expansion "^2.0.1" @@ -51,17 +51,17 @@ shiki@^0.14.7: vscode-oniguruma "^1.7.0" vscode-textmate "^8.0.0" -typedoc@^0.25.8: - version "0.25.8" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.25.8.tgz#7d0e1bf12d23bf1c459fd4893c82cb855911ff12" - integrity sha512-mh8oLW66nwmeB9uTa0Bdcjfis+48bAjSH3uqdzSuSawfduROQLlXw//WSNZLYDdhmMVB7YcYZicq6e8T0d271A== +typedoc@^0.25.13: + version "0.25.13" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.25.13.tgz#9a98819e3b2d155a6d78589b46fa4c03768f0922" + integrity sha512-pQqiwiJ+Z4pigfOnnysObszLiU3mVLWAExSPf+Mu06G/qsc3wzbuM56SZQvONhHLncLUhYzOVkjFFpFfL5AzhQ== dependencies: lunr "^2.3.9" marked "^4.3.0" minimatch "^9.0.3" shiki "^0.14.7" -typescript@^4.6: +typescript@^4.9.4: version "4.9.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== diff --git a/index.d.ts b/index.d.ts index a41b4aee410..8984d02f81a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -764,7 +764,7 @@ declare namespace tracer { */ maxDepth?: number } - + /** * Configuration enabling LLM Observability. Enablement is superceded by the DD_LLMOBS_ENABLED environment variable. */ @@ -2203,6 +2203,12 @@ declare namespace tracer { */ cookieFilterPattern?: string, + /** + * Defines the number of rows to taint in data coming from databases + * @default 1 + */ + dbRowsToTaint?: number, + /** * Whether to enable vulnerability deduplication */ @@ -2247,7 +2253,7 @@ declare namespace tracer { * Disable LLM Observability tracing. */ disable (): void, - + /** * Instruments a function by automatically creating a span activated on its * scope. @@ -2289,10 +2295,10 @@ declare namespace tracer { /** * Decorate a function in a javascript runtime that supports function decorators. * Note that this is **not** supported in the Node.js runtime, but is in TypeScript. - * + * * In TypeScript, this decorator is only supported in contexts where general TypeScript * function decorators are supported. - * + * * @param options Optional LLM Observability span options. */ decorate (options: llmobs.LLMObsNamelessSpanOptions): any @@ -2309,7 +2315,7 @@ declare namespace tracer { /** * Sets inputs, outputs, tags, metadata, and metrics as provided for a given LLM Observability span. * Note that with the exception of tags, this method will override any existing values for the provided fields. - * + * * For example: * ```javascript * llmobs.trace({ kind: 'llm', name: 'myLLM', modelName: 'gpt-4o', modelProvider: 'openai' }, () => { @@ -2322,7 +2328,7 @@ declare namespace tracer { * }) * }) * ``` - * + * * @param span The span to annotate (defaults to the current LLM Observability span if not provided) * @param options An object containing the inputs, outputs, tags, metadata, and metrics to set on the span. */ @@ -2498,14 +2504,14 @@ declare namespace tracer { * LLM Observability span kind. One of `agent`, `workflow`, `task`, `tool`, `retrieval`, `embedding`, or `llm`. */ kind: llmobs.spanKind, - + /** * The ID of the underlying user session. Required for tracking sessions. */ sessionId?: string, /** - * The name of the ML application that the agent is orchestrating. + * The name of the ML application that the agent is orchestrating. * If not provided, the default value will be set to mlApp provided during initalization, or `DD_LLMOBS_ML_APP`. */ mlApp?: string, diff --git a/integration-tests/debugger/basic.spec.js b/integration-tests/debugger/basic.spec.js index 57c0c4a67a8..f51278bc2ee 100644 --- a/integration-tests/debugger/basic.spec.js +++ b/integration-tests/debugger/basic.spec.js @@ -9,469 +9,570 @@ const { ACKNOWLEDGED, ERROR } = require('../../packages/dd-trace/src/appsec/remo const { version } = require('../../package.json') describe('Dynamic Instrumentation', function () { - const t = setup() + describe('Default env', function () { + const t = setup() - it('base case: target app should work as expected if no test probe has been added', async function () { - const response = await t.axios.get(t.breakpoint.url) - assert.strictEqual(response.status, 200) - assert.deepStrictEqual(response.data, { hello: 'bar' }) - }) + it('base case: target app should work as expected if no test probe has been added', async function () { + const response = await t.axios.get(t.breakpoint.url) + assert.strictEqual(response.status, 200) + assert.deepStrictEqual(response.data, { hello: 'bar' }) + }) - describe('diagnostics messages', function () { - it('should send expected diagnostics messages if probe is received and triggered', function (done) { - let receivedAckUpdate = false - const probeId = t.rcConfig.config.id - const expectedPayloads = [{ - ddsource: 'dd_debugger', - service: 'node', - debugger: { diagnostics: { probeId, probeVersion: 0, status: 'RECEIVED' } } - }, { - ddsource: 'dd_debugger', - service: 'node', - debugger: { diagnostics: { probeId, probeVersion: 0, status: 'INSTALLED' } } - }, { - ddsource: 'dd_debugger', - service: 'node', - debugger: { diagnostics: { probeId, probeVersion: 0, status: 'EMITTING' } } - }] - - t.agent.on('remote-config-ack-update', (id, version, state, error) => { - assert.strictEqual(id, t.rcConfig.id) - assert.strictEqual(version, 1) - assert.strictEqual(state, ACKNOWLEDGED) - assert.notOk(error) // falsy check since error will be an empty string, but that's an implementation detail - - receivedAckUpdate = true - endIfDone() - }) + describe('diagnostics messages', function () { + it('should send expected diagnostics messages if probe is received and triggered', function (done) { + let receivedAckUpdate = false + const probeId = t.rcConfig.config.id + const expectedPayloads = [{ + ddsource: 'dd_debugger', + service: 'node', + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'RECEIVED' } } + }, { + ddsource: 'dd_debugger', + service: 'node', + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'INSTALLED' } } + }, { + ddsource: 'dd_debugger', + service: 'node', + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'EMITTING' } } + }] - t.agent.on('debugger-diagnostics', ({ payload }) => { - const expected = expectedPayloads.shift() - assertObjectContains(payload, expected) - assertUUID(payload.debugger.diagnostics.runtimeId) + t.agent.on('remote-config-ack-update', (id, version, state, error) => { + assert.strictEqual(id, t.rcConfig.id) + assert.strictEqual(version, 1) + assert.strictEqual(state, ACKNOWLEDGED) + assert.notOk(error) // falsy check since error will be an empty string, but that's an implementation detail - if (payload.debugger.diagnostics.status === 'INSTALLED') { - t.axios.get(t.breakpoint.url) - .then((response) => { - assert.strictEqual(response.status, 200) - assert.deepStrictEqual(response.data, { hello: 'bar' }) - }) - .catch(done) - } else { + receivedAckUpdate = true endIfDone() - } - }) + }) - t.agent.addRemoteConfig(t.rcConfig) + t.agent.on('debugger-diagnostics', ({ payload }) => { + payload.forEach((event) => { + const expected = expectedPayloads.shift() + assertObjectContains(event, expected) + assertUUID(event.debugger.diagnostics.runtimeId) + + if (event.debugger.diagnostics.status === 'INSTALLED') { + t.axios.get(t.breakpoint.url) + .then((response) => { + assert.strictEqual(response.status, 200) + assert.deepStrictEqual(response.data, { hello: 'bar' }) + }) + .catch(done) + } else { + endIfDone() + } + }) + }) - function endIfDone () { - if (receivedAckUpdate && expectedPayloads.length === 0) done() - } - }) + t.agent.addRemoteConfig(t.rcConfig) - it('should send expected diagnostics messages if probe is first received and then updated', function (done) { - let receivedAckUpdates = 0 - const probeId = t.rcConfig.config.id - const expectedPayloads = [{ - ddsource: 'dd_debugger', - service: 'node', - debugger: { diagnostics: { probeId, probeVersion: 0, status: 'RECEIVED' } } - }, { - ddsource: 'dd_debugger', - service: 'node', - debugger: { diagnostics: { probeId, probeVersion: 0, status: 'INSTALLED' } } - }, { - ddsource: 'dd_debugger', - service: 'node', - debugger: { diagnostics: { probeId, probeVersion: 1, status: 'RECEIVED' } } - }, { - ddsource: 'dd_debugger', - service: 'node', - debugger: { diagnostics: { probeId, probeVersion: 1, status: 'INSTALLED' } } - }] - const triggers = [ - () => { - t.rcConfig.config.version++ - t.agent.updateRemoteConfig(t.rcConfig.id, t.rcConfig.config) - }, - () => {} - ] - - t.agent.on('remote-config-ack-update', (id, version, state, error) => { - assert.strictEqual(id, t.rcConfig.id) - assert.strictEqual(version, ++receivedAckUpdates) - assert.strictEqual(state, ACKNOWLEDGED) - assert.notOk(error) // falsy check since error will be an empty string, but that's an implementation detail - - endIfDone() + function endIfDone () { + if (receivedAckUpdate && expectedPayloads.length === 0) done() + } }) - t.agent.on('debugger-diagnostics', ({ payload }) => { - const expected = expectedPayloads.shift() - assertObjectContains(payload, expected) - assertUUID(payload.debugger.diagnostics.runtimeId) - if (payload.debugger.diagnostics.status === 'INSTALLED') triggers.shift()() - endIfDone() - }) + it('should send expected diagnostics messages if probe is first received and then updated', function (done) { + let receivedAckUpdates = 0 + const probeId = t.rcConfig.config.id + const expectedPayloads = [{ + ddsource: 'dd_debugger', + service: 'node', + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'RECEIVED' } } + }, { + ddsource: 'dd_debugger', + service: 'node', + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'INSTALLED' } } + }, { + ddsource: 'dd_debugger', + service: 'node', + debugger: { diagnostics: { probeId, probeVersion: 1, status: 'RECEIVED' } } + }, { + ddsource: 'dd_debugger', + service: 'node', + debugger: { diagnostics: { probeId, probeVersion: 1, status: 'INSTALLED' } } + }] + const triggers = [ + () => { + t.rcConfig.config.version++ + t.agent.updateRemoteConfig(t.rcConfig.id, t.rcConfig.config) + }, + () => {} + ] - t.agent.addRemoteConfig(t.rcConfig) + t.agent.on('remote-config-ack-update', (id, version, state, error) => { + assert.strictEqual(id, t.rcConfig.id) + assert.strictEqual(version, ++receivedAckUpdates) + assert.strictEqual(state, ACKNOWLEDGED) + assert.notOk(error) // falsy check since error will be an empty string, but that's an implementation detail - function endIfDone () { - if (receivedAckUpdates === 2 && expectedPayloads.length === 0) done() - } - }) + endIfDone() + }) - it('should send expected diagnostics messages if probe is first received and then deleted', function (done) { - let receivedAckUpdate = false - let payloadsProcessed = false - const probeId = t.rcConfig.config.id - const expectedPayloads = [{ - ddsource: 'dd_debugger', - service: 'node', - debugger: { diagnostics: { probeId, probeVersion: 0, status: 'RECEIVED' } } - }, { - ddsource: 'dd_debugger', - service: 'node', - debugger: { diagnostics: { probeId, probeVersion: 0, status: 'INSTALLED' } } - }] - - t.agent.on('remote-config-ack-update', (id, version, state, error) => { - assert.strictEqual(id, t.rcConfig.id) - assert.strictEqual(version, 1) - assert.strictEqual(state, ACKNOWLEDGED) - assert.notOk(error) // falsy check since error will be an empty string, but that's an implementation detail - - receivedAckUpdate = true - endIfDone() - }) + t.agent.on('debugger-diagnostics', ({ payload }) => { + payload.forEach((event) => { + const expected = expectedPayloads.shift() + assertObjectContains(event, expected) + assertUUID(event.debugger.diagnostics.runtimeId) + if (event.debugger.diagnostics.status === 'INSTALLED') triggers.shift()() + endIfDone() + }) + }) - t.agent.on('debugger-diagnostics', ({ payload }) => { - const expected = expectedPayloads.shift() - assertObjectContains(payload, expected) - assertUUID(payload.debugger.diagnostics.runtimeId) + t.agent.addRemoteConfig(t.rcConfig) - if (payload.debugger.diagnostics.status === 'INSTALLED') { - t.agent.removeRemoteConfig(t.rcConfig.id) - // Wait a little to see if we get any follow-up `debugger-diagnostics` messages - setTimeout(() => { - payloadsProcessed = true - endIfDone() - }, pollInterval * 2 * 1000) // wait twice as long as the RC poll interval + function endIfDone () { + if (receivedAckUpdates === 2 && expectedPayloads.length === 0) done() } }) - t.agent.addRemoteConfig(t.rcConfig) - - function endIfDone () { - if (receivedAckUpdate && payloadsProcessed) done() - } - }) - - const unsupporedOrInvalidProbes = [[ - 'should send expected error diagnostics messages if probe doesn\'t conform to expected schema', - 'bad config!!!', - { status: 'ERROR' } - ], [ - 'should send expected error diagnostics messages if probe type isn\'t supported', - t.generateProbeConfig({ type: 'INVALID_PROBE' }) - ], [ - 'should send expected error diagnostics messages if it isn\'t a line-probe', - t.generateProbeConfig({ where: { foo: 'bar' } }) // TODO: Use valid schema for method probe instead - ]] - - for (const [title, config, customErrorDiagnosticsObj] of unsupporedOrInvalidProbes) { - it(title, function (done) { + it('should send expected diagnostics messages if probe is first received and then deleted', function (done) { let receivedAckUpdate = false - - t.agent.on('remote-config-ack-update', (id, version, state, error) => { - assert.strictEqual(id, `logProbe_${config.id}`) - assert.strictEqual(version, 1) - assert.strictEqual(state, ERROR) - assert.strictEqual(error.slice(0, 6), 'Error:') - - receivedAckUpdate = true - endIfDone() - }) - - const probeId = config.id + let payloadsProcessed = false + const probeId = t.rcConfig.config.id const expectedPayloads = [{ ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: { status: 'RECEIVED' } } + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'RECEIVED' } } }, { ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: customErrorDiagnosticsObj ?? { probeId, probeVersion: 0, status: 'ERROR' } } + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'INSTALLED' } } }] - t.agent.on('debugger-diagnostics', ({ payload }) => { - const expected = expectedPayloads.shift() - assertObjectContains(payload, expected) - const { diagnostics } = payload.debugger - assertUUID(diagnostics.runtimeId) - - if (diagnostics.status === 'ERROR') { - assert.property(diagnostics, 'exception') - assert.hasAllKeys(diagnostics.exception, ['message', 'stacktrace']) - assert.typeOf(diagnostics.exception.message, 'string') - assert.typeOf(diagnostics.exception.stacktrace, 'string') - } + t.agent.on('remote-config-ack-update', (id, version, state, error) => { + assert.strictEqual(id, t.rcConfig.id) + assert.strictEqual(version, 1) + assert.strictEqual(state, ACKNOWLEDGED) + assert.notOk(error) // falsy check since error will be an empty string, but that's an implementation detail + receivedAckUpdate = true endIfDone() }) - t.agent.addRemoteConfig({ - product: 'LIVE_DEBUGGING', - id: `logProbe_${config.id}`, - config + t.agent.on('debugger-diagnostics', ({ payload }) => { + payload.forEach((event) => { + const expected = expectedPayloads.shift() + assertObjectContains(event, expected) + assertUUID(event.debugger.diagnostics.runtimeId) + + if (event.debugger.diagnostics.status === 'INSTALLED') { + t.agent.removeRemoteConfig(t.rcConfig.id) + // Wait a little to see if we get any follow-up `debugger-diagnostics` messages + setTimeout(() => { + payloadsProcessed = true + endIfDone() + }, pollInterval * 2 * 1000) // wait twice as long as the RC poll interval + } + }) }) + t.agent.addRemoteConfig(t.rcConfig) + function endIfDone () { - if (receivedAckUpdate && expectedPayloads.length === 0) done() + if (receivedAckUpdate && payloadsProcessed) done() } }) - } - }) - describe('input messages', function () { - it('should capture and send expected payload when a log line probe is triggered', function (done) { - t.triggerBreakpoint() + const unsupporedOrInvalidProbes = [[ + 'should send expected error diagnostics messages if probe doesn\'t conform to expected schema', + 'bad config!!!', + { status: 'ERROR' } + ], [ + 'should send expected error diagnostics messages if probe type isn\'t supported', + t.generateProbeConfig({ type: 'INVALID_PROBE' }) + ], [ + 'should send expected error diagnostics messages if it isn\'t a line-probe', + t.generateProbeConfig({ where: { foo: 'bar' } }) // TODO: Use valid schema for method probe instead + ]] + + for (const [title, config, customErrorDiagnosticsObj] of unsupporedOrInvalidProbes) { + it(title, function (done) { + let receivedAckUpdate = false + + t.agent.on('remote-config-ack-update', (id, version, state, error) => { + assert.strictEqual(id, `logProbe_${config.id}`) + assert.strictEqual(version, 1) + assert.strictEqual(state, ERROR) + assert.strictEqual(error.slice(0, 6), 'Error:') + + receivedAckUpdate = true + endIfDone() + }) + + const probeId = config.id + const expectedPayloads = [{ + ddsource: 'dd_debugger', + service: 'node', + debugger: { diagnostics: { status: 'RECEIVED' } } + }, { + ddsource: 'dd_debugger', + service: 'node', + debugger: { diagnostics: customErrorDiagnosticsObj ?? { probeId, probeVersion: 0, status: 'ERROR' } } + }] + + t.agent.on('debugger-diagnostics', ({ payload }) => { + payload.forEach((event) => { + const expected = expectedPayloads.shift() + assertObjectContains(event, expected) + const { diagnostics } = event.debugger + assertUUID(diagnostics.runtimeId) + + if (diagnostics.status === 'ERROR') { + assert.property(diagnostics, 'exception') + assert.hasAllKeys(diagnostics.exception, ['message', 'stacktrace']) + assert.typeOf(diagnostics.exception.message, 'string') + assert.typeOf(diagnostics.exception.stacktrace, 'string') + } + + endIfDone() + }) + }) - t.agent.on('debugger-input', ({ payload }) => { - const expected = { - ddsource: 'dd_debugger', - hostname: os.hostname(), - service: 'node', - message: 'Hello World!', - logger: { - name: t.breakpoint.file, - method: 'fooHandler', - version, - thread_name: 'MainThread' + t.agent.addRemoteConfig({ + product: 'LIVE_DEBUGGING', + id: `logProbe_${config.id}`, + config + }) + + function endIfDone () { + if (receivedAckUpdate && expectedPayloads.length === 0) done() + } + }) + } + }) + + describe('input messages', function () { + it( + 'should capture and send expected payload when a log line probe is triggered', + testBasicInputWithDD.bind(null, t) + ) + + it('should respond with updated message if probe message is updated', function (done) { + const expectedMessages = ['Hello World!', 'Hello Updated World!'] + const triggers = [ + async () => { + await t.axios.get(t.breakpoint.url) + t.rcConfig.config.version++ + t.rcConfig.config.template = 'Hello Updated World!' + t.agent.updateRemoteConfig(t.rcConfig.id, t.rcConfig.config) }, - 'debugger.snapshot': { - probe: { - id: t.rcConfig.config.id, - version: 0, - location: { file: t.breakpoint.file, lines: [String(t.breakpoint.line)] } - }, - language: 'javascript' + async () => { + await t.axios.get(t.breakpoint.url) } - } + ] - assertObjectContains(payload, expected) - assert.match(payload.logger.thread_id, /^pid:\d+$/) - assertUUID(payload['debugger.snapshot'].id) - assert.isNumber(payload['debugger.snapshot'].timestamp) - assert.isTrue(payload['debugger.snapshot'].timestamp > Date.now() - 1000 * 60) - assert.isTrue(payload['debugger.snapshot'].timestamp <= Date.now()) - - assert.isArray(payload['debugger.snapshot'].stack) - assert.isAbove(payload['debugger.snapshot'].stack.length, 0) - for (const frame of payload['debugger.snapshot'].stack) { - assert.isObject(frame) - assert.hasAllKeys(frame, ['fileName', 'function', 'lineNumber', 'columnNumber']) - assert.isString(frame.fileName) - assert.isString(frame.function) - assert.isAbove(frame.lineNumber, 0) - assert.isAbove(frame.columnNumber, 0) - } - const topFrame = payload['debugger.snapshot'].stack[0] - // path seems to be prefeixed with `/private` on Mac - assert.match(topFrame.fileName, new RegExp(`${t.appFile}$`)) - assert.strictEqual(topFrame.function, 'fooHandler') - assert.strictEqual(topFrame.lineNumber, t.breakpoint.line) - assert.strictEqual(topFrame.columnNumber, 3) - - done() + t.agent.on('debugger-diagnostics', ({ payload }) => { + payload.forEach((event) => { + if (event.debugger.diagnostics.status === 'INSTALLED') triggers.shift()().catch(done) + }) + }) + + t.agent.on('debugger-input', ({ payload: [payload] }) => { + assert.strictEqual(payload.message, expectedMessages.shift()) + if (expectedMessages.length === 0) done() + }) + + t.agent.addRemoteConfig(t.rcConfig) }) - t.agent.addRemoteConfig(t.rcConfig) - }) + it('should not trigger if probe is deleted', function (done) { + t.agent.on('debugger-diagnostics', ({ payload }) => { + payload.forEach((event) => { + if (event.debugger.diagnostics.status === 'INSTALLED') { + t.agent.once('remote-confg-responded', async () => { + await t.axios.get(t.breakpoint.url) + // We want to wait enough time to see if the client triggers on the breakpoint so that the test can fail + // if it does, but not so long that the test times out. + // TODO: Is there some signal we can use instead of a timer? + setTimeout(done, pollInterval * 2 * 1000) // wait twice as long as the RC poll interval + }) + + t.agent.removeRemoteConfig(t.rcConfig.id) + } + }) + }) - it('should respond with updated message if probe message is updated', function (done) { - const expectedMessages = ['Hello World!', 'Hello Updated World!'] - const triggers = [ - async () => { - await t.axios.get(t.breakpoint.url) - t.rcConfig.config.version++ - t.rcConfig.config.template = 'Hello Updated World!' - t.agent.updateRemoteConfig(t.rcConfig.id, t.rcConfig.config) - }, - async () => { - await t.axios.get(t.breakpoint.url) - } - ] + t.agent.on('debugger-input', () => { + assert.fail('should not capture anything when the probe is deleted') + }) - t.agent.on('debugger-diagnostics', ({ payload }) => { - if (payload.debugger.diagnostics.status === 'INSTALLED') triggers.shift()().catch(done) + t.agent.addRemoteConfig(t.rcConfig) }) + }) - t.agent.on('debugger-input', ({ payload }) => { - assert.strictEqual(payload.message, expectedMessages.shift()) - if (expectedMessages.length === 0) done() - }) + describe('sampling', function () { + it('should respect sampling rate for single probe', function (done) { + let prev, timer + const rcConfig = t.generateRemoteConfig({ sampling: { snapshotsPerSecond: 1 } }) - t.agent.addRemoteConfig(t.rcConfig) - }) + function triggerBreakpointContinuously () { + t.axios.get(t.breakpoint.url).catch(done) + timer = setTimeout(triggerBreakpointContinuously, 10) + } - it('should not trigger if probe is deleted', function (done) { - t.agent.on('debugger-diagnostics', ({ payload }) => { - if (payload.debugger.diagnostics.status === 'INSTALLED') { - t.agent.once('remote-confg-responded', async () => { - await t.axios.get(t.breakpoint.url) - // We want to wait enough time to see if the client triggers on the breakpoint so that the test can fail - // if it does, but not so long that the test times out. - // TODO: Is there some signal we can use instead of a timer? - setTimeout(done, pollInterval * 2 * 1000) // wait twice as long as the RC poll interval + t.agent.on('debugger-diagnostics', ({ payload }) => { + payload.forEach((event) => { + if (event.debugger.diagnostics.status === 'INSTALLED') triggerBreakpointContinuously() }) + }) - t.agent.removeRemoteConfig(t.rcConfig.id) - } - }) + t.agent.on('debugger-input', ({ payload }) => { + payload.forEach(({ 'debugger.snapshot': { timestamp } }) => { + if (prev !== undefined) { + const duration = timestamp - prev + clearTimeout(timer) - t.agent.on('debugger-input', () => { - assert.fail('should not capture anything when the probe is deleted') + // Allow for a variance of +50ms (time will tell if this is enough) + assert.isAtLeast(duration, 1000) + assert.isBelow(duration, 1050) + + // Wait at least a full sampling period, to see if we get any more payloads + timer = setTimeout(done, 1250) + } + prev = timestamp + }) + }) + + t.agent.addRemoteConfig(rcConfig) }) - t.agent.addRemoteConfig(t.rcConfig) - }) - }) + it('should adhere to individual probes sample rate', function (done) { + const rcConfig1 = t.breakpoints[0].generateRemoteConfig({ sampling: { snapshotsPerSecond: 1 } }) + const rcConfig2 = t.breakpoints[1].generateRemoteConfig({ sampling: { snapshotsPerSecond: 1 } }) + const state = { + [rcConfig1.config.id]: { + tiggerBreakpointContinuously () { + t.axios.get(t.breakpoints[0].url).catch(done) + this.timer = setTimeout(this.tiggerBreakpointContinuously.bind(this), 10) + } + }, + [rcConfig2.config.id]: { + tiggerBreakpointContinuously () { + t.axios.get(t.breakpoints[1].url).catch(done) + this.timer = setTimeout(this.tiggerBreakpointContinuously.bind(this), 10) + } + } + } - describe('sampling', function () { - it('should respect sampling rate for single probe', function (done) { - let start, timer - let payloadsReceived = 0 - const rcConfig = t.generateRemoteConfig({ sampling: { snapshotsPerSecond: 1 } }) + t.agent.on('debugger-diagnostics', ({ payload }) => { + payload.forEach((event) => { + const { probeId, status } = event.debugger.diagnostics + if (status === 'INSTALLED') state[probeId].tiggerBreakpointContinuously() + }) + }) - function triggerBreakpointContinuously () { - t.axios.get(t.breakpoint.url).catch(done) - timer = setTimeout(triggerBreakpointContinuously, 10) - } + t.agent.on('debugger-input', ({ payload }) => { + payload.forEach((result) => { + const _state = state[result['debugger.snapshot'].probe.id] + const { timestamp } = result['debugger.snapshot'] + if (_state.prev !== undefined) { + const duration = timestamp - _state.prev + clearTimeout(_state.timer) + + // Allow for a variance of +50ms (time will tell if this is enough) + assert.isAtLeast(duration, 1000) + assert.isBelow(duration, 1050) + + // Wait at least a full sampling period, to see if we get any more payloads + _state.timer = setTimeout(doneWhenCalledTwice, 1250) + } + _state.prev = timestamp + }) + }) - t.agent.on('debugger-diagnostics', ({ payload }) => { - if (payload.debugger.diagnostics.status === 'INSTALLED') triggerBreakpointContinuously() - }) + t.agent.addRemoteConfig(rcConfig1) + t.agent.addRemoteConfig(rcConfig2) - t.agent.on('debugger-input', () => { - payloadsReceived++ - if (payloadsReceived === 1) { - start = Date.now() - } else if (payloadsReceived === 2) { - const duration = Date.now() - start - clearTimeout(timer) - - // Allow for a variance of -5/+50ms (time will tell if this is enough) - assert.isAbove(duration, 995) - assert.isBelow(duration, 1050) - - // Wait at least a full sampling period, to see if we get any more payloads - timer = setTimeout(done, 1250) - } else { - clearTimeout(timer) - done(new Error('Too many payloads received!')) + function doneWhenCalledTwice () { + if (doneWhenCalledTwice.calledOnce) return done() + doneWhenCalledTwice.calledOnce = true } }) - - t.agent.addRemoteConfig(rcConfig) }) - it('should adhere to individual probes sample rate', function (done) { - const rcConfig1 = t.breakpoints[0].generateRemoteConfig({ sampling: { snapshotsPerSecond: 1 } }) - const rcConfig2 = t.breakpoints[1].generateRemoteConfig({ sampling: { snapshotsPerSecond: 1 } }) - const state = { - [rcConfig1.config.id]: { - payloadsReceived: 0, - tiggerBreakpointContinuously () { - t.axios.get(t.breakpoints[0].url).catch(done) - this.timer = setTimeout(this.tiggerBreakpointContinuously.bind(this), 10) - } - }, - [rcConfig2.config.id]: { - payloadsReceived: 0, - tiggerBreakpointContinuously () { - t.axios.get(t.breakpoints[1].url).catch(done) - this.timer = setTimeout(this.tiggerBreakpointContinuously.bind(this), 10) - } - } - } + describe('race conditions', function () { + it('should remove the last breakpoint completely before trying to add a new one', function (done) { + const rcConfig2 = t.generateRemoteConfig() - t.agent.on('debugger-diagnostics', ({ payload }) => { - const { probeId, status } = payload.debugger.diagnostics - if (status === 'INSTALLED') state[probeId].tiggerBreakpointContinuously() - }) + t.agent.on('debugger-diagnostics', ({ payload }) => { + payload.forEach((event) => { + const { probeId, status } = event.debugger.diagnostics + if (status !== 'INSTALLED') return + + if (probeId === t.rcConfig.config.id) { + // First INSTALLED payload: Try to trigger the race condition. + t.agent.removeRemoteConfig(t.rcConfig.id) + t.agent.addRemoteConfig(rcConfig2) + } else { + // Second INSTALLED payload: Perform an HTTP request to see if we successfully handled the race condition. + let finished = false + + // If the race condition occurred, the debugger will have been detached from the main thread and the new + // probe will never trigger. If that's the case, the following timer will fire: + const timer = setTimeout(() => { + done(new Error('Race condition occurred!')) + }, 2000) + + // If we successfully handled the race condition, the probe will trigger, we'll get a probe result and the + // following event listener will be called: + t.agent.once('debugger-input', () => { + clearTimeout(timer) + finished = true + done() + }) + + // Perform HTTP request to try and trigger the probe + t.axios.get(t.breakpoint.url).catch((err) => { + // If the request hasn't fully completed by the time the tests ends and the target app is destroyed, + // Axios will complain with a "socket hang up" error. Hence this sanity check before calling + // `done(err)`. If we later add more tests below this one, this shouuldn't be an issue. + if (!finished) done(err) + }) + } + }) + }) - t.agent.on('debugger-input', ({ payload }) => { - const _state = state[payload['debugger.snapshot'].probe.id] - _state.payloadsReceived++ - if (_state.payloadsReceived === 1) { - _state.start = Date.now() - } else if (_state.payloadsReceived === 2) { - const duration = Date.now() - _state.start - clearTimeout(_state.timer) - - // Allow for a variance of -5/+50ms (time will tell if this is enough) - assert.isAbove(duration, 995) - assert.isBelow(duration, 1050) - - // Wait at least a full sampling period, to see if we get any more payloads - _state.timer = setTimeout(doneWhenCalledTwice, 1250) - } else { - clearTimeout(_state.timer) - done(new Error('Too many payloads received!')) - } + t.agent.addRemoteConfig(t.rcConfig) }) + }) + }) - t.agent.addRemoteConfig(rcConfig1) - t.agent.addRemoteConfig(rcConfig2) + describe('DD_TRACING_ENABLED=true, DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED=true', function () { + const t = setup({ env: { DD_TRACING_ENABLED: true, DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED: true } }) - function doneWhenCalledTwice () { - if (doneWhenCalledTwice.calledOnce) return done() - doneWhenCalledTwice.calledOnce = true - } + describe('input messages', function () { + it( + 'should capture and send expected payload when a log line probe is triggered', + testBasicInputWithDD.bind(null, t) + ) }) }) - describe('race conditions', function () { - it('should remove the last breakpoint completely before trying to add a new one', function (done) { - const rcConfig2 = t.generateRemoteConfig() - - t.agent.on('debugger-diagnostics', ({ payload: { debugger: { diagnostics: { status, probeId } } } }) => { - if (status !== 'INSTALLED') return - - if (probeId === t.rcConfig.config.id) { - // First INSTALLED payload: Try to trigger the race condition. - t.agent.removeRemoteConfig(t.rcConfig.id) - t.agent.addRemoteConfig(rcConfig2) - } else { - // Second INSTALLED payload: Perform an HTTP request to see if we successfully handled the race condition. - let finished = false - - // If the race condition occurred, the debugger will have been detached from the main thread and the new - // probe will never trigger. If that's the case, the following timer will fire: - const timer = setTimeout(() => { - done(new Error('Race condition occurred!')) - }, 1000) - - // If we successfully handled the race condition, the probe will trigger, we'll get a probe result and the - // following event listener will be called: - t.agent.once('debugger-input', () => { - clearTimeout(timer) - finished = true - done() - }) + describe('DD_TRACING_ENABLED=true, DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED=false', function () { + const t = setup({ env: { DD_TRACING_ENABLED: true, DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED: false } }) - // Perform HTTP request to try and trigger the probe - t.axios.get(t.breakpoint.url).catch((err) => { - // If the request hasn't fully completed by the time the tests ends and the target app is destroyed, Axios - // will complain with a "socket hang up" error. Hence this sanity check before calling `done(err)`. If we - // later add more tests below this one, this shouuldn't be an issue. - if (!finished) done(err) - }) - } - }) + describe('input messages', function () { + it( + 'should capture and send expected payload when a log line probe is triggered', + testBasicInputWithDD.bind(null, t) + ) + }) + }) - t.agent.addRemoteConfig(t.rcConfig) + describe('DD_TRACING_ENABLED=false', function () { + const t = setup({ env: { DD_TRACING_ENABLED: false } }) + + describe('input messages', function () { + it( + 'should capture and send expected payload when a log line probe is triggered', + testBasicInputWithoutDD.bind(null, t) + ) }) }) }) + +function testBasicInputWithDD (t, done) { + let traceId, spanId, dd + + t.triggerBreakpoint() + + t.agent.on('message', ({ payload }) => { + const span = payload.find((arr) => arr[0].name === 'fastify.request')?.[0] + if (!span) return + + traceId = span.trace_id.toString() + spanId = span.span_id.toString() + + assertDD() + }) + + t.agent.on('debugger-input', ({ payload }) => { + assertBasicInputPayload(t, payload) + + payload = payload[0] + assert.isObject(payload.dd) + assert.hasAllKeys(payload.dd, ['trace_id', 'span_id']) + assert.typeOf(payload.dd.trace_id, 'string') + assert.typeOf(payload.dd.span_id, 'string') + assert.isAbove(payload.dd.trace_id.length, 0) + assert.isAbove(payload.dd.span_id.length, 0) + dd = payload.dd + + assertDD() + }) + + t.agent.addRemoteConfig(t.rcConfig) + + function assertDD () { + if (!traceId || !spanId || !dd) return + assert.strictEqual(dd.trace_id, traceId) + assert.strictEqual(dd.span_id, spanId) + done() + } +} + +function testBasicInputWithoutDD (t, done) { + t.triggerBreakpoint() + + t.agent.on('debugger-input', ({ payload }) => { + assertBasicInputPayload(t, payload) + assert.doesNotHaveAnyKeys(payload[0], ['dd']) + done() + }) + + t.agent.addRemoteConfig(t.rcConfig) +} + +function assertBasicInputPayload (t, payload) { + assert.isArray(payload) + assert.lengthOf(payload, 1) + payload = payload[0] + + const expected = { + ddsource: 'dd_debugger', + hostname: os.hostname(), + service: 'node', + message: 'Hello World!', + logger: { + name: t.breakpoint.file, + method: 'fooHandler', + version, + thread_name: 'MainThread' + }, + 'debugger.snapshot': { + probe: { + id: t.rcConfig.config.id, + version: 0, + location: { file: t.breakpoint.file, lines: [String(t.breakpoint.line)] } + }, + language: 'javascript' + } + } + + assertObjectContains(payload, expected) + + assert.match(payload.logger.thread_id, /^pid:\d+$/) + + assertUUID(payload['debugger.snapshot'].id) + assert.isNumber(payload['debugger.snapshot'].timestamp) + assert.isTrue(payload['debugger.snapshot'].timestamp > Date.now() - 1000 * 60) + assert.isTrue(payload['debugger.snapshot'].timestamp <= Date.now()) + + assert.isArray(payload['debugger.snapshot'].stack) + assert.isAbove(payload['debugger.snapshot'].stack.length, 0) + for (const frame of payload['debugger.snapshot'].stack) { + assert.isObject(frame) + assert.hasAllKeys(frame, ['fileName', 'function', 'lineNumber', 'columnNumber']) + assert.isString(frame.fileName) + assert.isString(frame.function) + assert.isAbove(frame.lineNumber, 0) + assert.isAbove(frame.columnNumber, 0) + } + const topFrame = payload['debugger.snapshot'].stack[0] + // path seems to be prefeixed with `/private` on Mac + assert.match(topFrame.fileName, new RegExp(`${t.appFile}$`)) + assert.strictEqual(topFrame.function, 'fooHandler') + assert.strictEqual(topFrame.lineNumber, t.breakpoint.line) + assert.strictEqual(topFrame.columnNumber, 3) +} diff --git a/integration-tests/debugger/ddtags.spec.js b/integration-tests/debugger/ddtags.spec.js new file mode 100644 index 00000000000..5f864d71123 --- /dev/null +++ b/integration-tests/debugger/ddtags.spec.js @@ -0,0 +1,56 @@ +'use strict' + +const os = require('os') + +const { assert } = require('chai') +const { setup } = require('./utils') +const { version } = require('../../package.json') + +describe('Dynamic Instrumentation', function () { + describe('ddtags', function () { + const t = setup({ + env: { + DD_ENV: 'test-env', + DD_VERSION: 'test-version', + DD_GIT_COMMIT_SHA: 'test-commit-sha', + DD_GIT_REPOSITORY_URL: 'test-repository-url' + }, + testApp: 'target-app/basic.js' + }) + + it('should add the expected ddtags as a query param to /debugger/v1/input', function (done) { + t.triggerBreakpoint() + + t.agent.on('debugger-input', ({ query }) => { + assert.property(query, 'ddtags') + + // Before: "a:b,c:d" + // After: { a: 'b', c: 'd' } + const ddtags = query.ddtags + .split(',') + .map((tag) => tag.split(':')) + .reduce((acc, [k, v]) => { acc[k] = v; return acc }, {}) + + assert.hasAllKeys(ddtags, [ + 'env', + 'version', + 'debugger_version', + 'host_name', + 'git.commit.sha', + 'git.repository_url' + ]) + + assert.strictEqual(ddtags.env, 'test-env') + assert.strictEqual(ddtags.version, 'test-version') + assert.strictEqual(ddtags.debugger_version, version) + assert.strictEqual(ddtags.host_name, os.hostname()) + assert.strictEqual(ddtags['git.commit.sha'], 'test-commit-sha') + assert.strictEqual(ddtags['git.repository_url'], 'test-repository-url') + + done() + }) + + t.agent.addRemoteConfig(t.rcConfig) + }) + }) +}) diff --git a/integration-tests/debugger/redact.spec.js b/integration-tests/debugger/redact.spec.js new file mode 100644 index 00000000000..62a948b80a8 --- /dev/null +++ b/integration-tests/debugger/redact.spec.js @@ -0,0 +1,49 @@ +'use strict' + +const { assert } = require('chai') +const { setup } = require('./utils') + +// Default settings is tested in unit tests, so we only need to test the env vars here +describe('Dynamic Instrumentation snapshot PII redaction', function () { + describe('DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS=foo,bar', function () { + const t = setup({ env: { DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS: 'foo,bar' } }) + + it('should respect DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS', function (done) { + t.triggerBreakpoint() + + t.agent.on('debugger-input', ({ payload: [{ 'debugger.snapshot': { captures } }] }) => { + const { locals } = captures.lines[t.breakpoint.line] + + assert.deepPropertyVal(locals, 'foo', { type: 'string', notCapturedReason: 'redactedIdent' }) + assert.deepPropertyVal(locals, 'bar', { type: 'string', notCapturedReason: 'redactedIdent' }) + assert.deepPropertyVal(locals, 'baz', { type: 'string', value: 'c' }) + + // existing redaction should not be impacted + assert.deepPropertyVal(locals, 'secret', { type: 'string', notCapturedReason: 'redactedIdent' }) + + done() + }) + + t.agent.addRemoteConfig(t.generateRemoteConfig({ captureSnapshot: true })) + }) + }) + + describe('DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS=secret', function () { + const t = setup({ env: { DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS: 'secret' } }) + + it('should respect DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS', function (done) { + t.triggerBreakpoint() + + t.agent.on('debugger-input', ({ payload: [{ 'debugger.snapshot': { captures } }] }) => { + const { locals } = captures.lines[t.breakpoint.line] + + assert.deepPropertyVal(locals, 'secret', { type: 'string', value: 'shh!' }) + assert.deepPropertyVal(locals, 'password', { type: 'string', notCapturedReason: 'redactedIdent' }) + + done() + }) + + t.agent.addRemoteConfig(t.generateRemoteConfig({ captureSnapshot: true })) + }) + }) +}) diff --git a/integration-tests/debugger/snapshot-pruning.spec.js b/integration-tests/debugger/snapshot-pruning.spec.js index c1ba218dd1c..b94d6afcce3 100644 --- a/integration-tests/debugger/snapshot-pruning.spec.js +++ b/integration-tests/debugger/snapshot-pruning.spec.js @@ -11,7 +11,7 @@ describe('Dynamic Instrumentation', function () { beforeEach(t.triggerBreakpoint) it('should prune snapshot if payload is too large', function (done) { - t.agent.on('debugger-input', ({ payload }) => { + t.agent.on('debugger-input', ({ payload: [payload] }) => { assert.isBelow(Buffer.byteLength(JSON.stringify(payload)), 1024 * 1024) // 1MB assert.deepEqual(payload['debugger.snapshot'].captures, { lines: { diff --git a/integration-tests/debugger/snapshot.spec.js b/integration-tests/debugger/snapshot.spec.js index e3d17b225c4..68b42c97d35 100644 --- a/integration-tests/debugger/snapshot.spec.js +++ b/integration-tests/debugger/snapshot.spec.js @@ -11,15 +11,15 @@ describe('Dynamic Instrumentation', function () { beforeEach(t.triggerBreakpoint) it('should capture a snapshot', function (done) { - t.agent.on('debugger-input', ({ payload: { 'debugger.snapshot': { captures } } }) => { + t.agent.on('debugger-input', ({ payload: [{ 'debugger.snapshot': { captures } }] }) => { assert.deepEqual(Object.keys(captures), ['lines']) assert.deepEqual(Object.keys(captures.lines), [String(t.breakpoint.line)]) const { locals } = captures.lines[t.breakpoint.line] - const { request, fastify, getSomeData } = locals + const { request, fastify, getUndefined } = locals delete locals.request delete locals.fastify - delete locals.getSomeData + delete locals.getUndefined // from block scope assert.deepEqual(locals, { @@ -67,19 +67,19 @@ describe('Dynamic Instrumentation', function () { } }, emptyObj: { type: 'Object', fields: {} }, - fn: { - type: 'Function', - fields: { - length: { type: 'number', value: '0' }, - name: { type: 'string', value: 'fn' } - } - }, p: { type: 'Promise', fields: { '[[PromiseState]]': { type: 'string', value: 'fulfilled' }, '[[PromiseResult]]': { type: 'undefined' } } + }, + arrowFn: { + type: 'Function', + fields: { + length: { type: 'number', value: '0' }, + name: { type: 'string', value: 'arrowFn' } + } } }) @@ -99,11 +99,11 @@ describe('Dynamic Instrumentation', function () { assert.equal(fastify.type, 'Object') assert.typeOf(fastify.fields, 'Object') - assert.deepEqual(getSomeData, { + assert.deepEqual(getUndefined, { type: 'Function', fields: { length: { type: 'number', value: '0' }, - name: { type: 'string', value: 'getSomeData' } + name: { type: 'string', value: 'getUndefined' } } }) @@ -114,11 +114,11 @@ describe('Dynamic Instrumentation', function () { }) it('should respect maxReferenceDepth', function (done) { - t.agent.on('debugger-input', ({ payload: { 'debugger.snapshot': { captures } } }) => { + t.agent.on('debugger-input', ({ payload: [{ 'debugger.snapshot': { captures } }] }) => { const { locals } = captures.lines[t.breakpoint.line] delete locals.request delete locals.fastify - delete locals.getSomeData + delete locals.getUndefined assert.deepEqual(locals, { nil: { type: 'null', isNull: true }, @@ -139,8 +139,8 @@ describe('Dynamic Instrumentation', function () { arr: { type: 'Array', notCapturedReason: 'depth' }, obj: { type: 'Object', notCapturedReason: 'depth' }, emptyObj: { type: 'Object', notCapturedReason: 'depth' }, - fn: { type: 'Function', notCapturedReason: 'depth' }, - p: { type: 'Promise', notCapturedReason: 'depth' } + p: { type: 'Promise', notCapturedReason: 'depth' }, + arrowFn: { type: 'Function', notCapturedReason: 'depth' } }) done() @@ -150,7 +150,7 @@ describe('Dynamic Instrumentation', function () { }) it('should respect maxLength', function (done) { - t.agent.on('debugger-input', ({ payload: { 'debugger.snapshot': { captures } } }) => { + t.agent.on('debugger-input', ({ payload: [{ 'debugger.snapshot': { captures } }] }) => { const { locals } = captures.lines[t.breakpoint.line] assert.deepEqual(locals.lstr, { @@ -167,7 +167,7 @@ describe('Dynamic Instrumentation', function () { }) it('should respect maxCollectionSize', function (done) { - t.agent.on('debugger-input', ({ payload: { 'debugger.snapshot': { captures } } }) => { + t.agent.on('debugger-input', ({ payload: [{ 'debugger.snapshot': { captures } }] }) => { const { locals } = captures.lines[t.breakpoint.line] assert.deepEqual(locals.arr, { @@ -205,14 +205,14 @@ describe('Dynamic Instrumentation', function () { } } - t.agent.on('debugger-input', ({ payload: { 'debugger.snapshot': { captures } } }) => { + t.agent.on('debugger-input', ({ payload: [{ 'debugger.snapshot': { captures } }] }) => { const { locals } = captures.lines[t.breakpoint.line] assert.deepEqual(Object.keys(locals), [ // Up to 3 properties from the local scope 'request', 'nil', 'undef', // Up to 3 properties from the closure scope - 'fastify', 'getSomeData' + 'fastify', 'getUndefined' ]) assert.strictEqual(locals.request.type, 'Request') diff --git a/integration-tests/debugger/target-app/redact.js b/integration-tests/debugger/target-app/redact.js new file mode 100644 index 00000000000..3ac7b51953c --- /dev/null +++ b/integration-tests/debugger/target-app/redact.js @@ -0,0 +1,26 @@ +'use strict' + +require('dd-trace/init') +const Fastify = require('fastify') + +const fastify = Fastify() + +fastify.get('/', function () { + /* eslint-disable no-unused-vars */ + const foo = 'a' + const bar = 'b' + const baz = 'c' + const secret = 'shh!' + const password = 'shh!' + /* eslint-enable no-unused-vars */ + + return { hello: 'world' } // BREAKPOINT: / +}) + +fastify.listen({ port: process.env.APP_PORT }, (err) => { + if (err) { + fastify.log.error(err) + process.exit(1) + } + process.send({ port: process.env.APP_PORT }) +}) diff --git a/integration-tests/debugger/target-app/snapshot.js b/integration-tests/debugger/target-app/snapshot.js index 03cfc758556..63cc6f3d33b 100644 --- a/integration-tests/debugger/target-app/snapshot.js +++ b/integration-tests/debugger/target-app/snapshot.js @@ -5,12 +5,33 @@ const Fastify = require('fastify') const fastify = Fastify() -// Since line probes have hardcoded line numbers, we want to try and keep the line numbers from changing within the -// `handler` function below when making changes to this file. This is achieved by calling `getSomeData` and keeping all -// variable names on the same line as much as possible. fastify.get('/:name', function handler (request) { - // eslint-disable-next-line no-unused-vars - const { nil, undef, bool, num, bigint, str, lstr, sym, regex, arr, obj, emptyObj, fn, p } = getSomeData() + /* eslint-disable no-unused-vars */ + const nil = null + const undef = getUndefined() + const bool = true + const num = 42 + const bigint = 42n + const str = 'foo' + // eslint-disable-next-line @stylistic/js/max-len + const lstr = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' + const sym = Symbol('foo') + const regex = /bar/i + const arr = [1, 2, 3, 4, 5] + const obj = { + foo: { + baz: 42, + nil: null, + undef: undefined, + deep: { nested: { obj: { that: { goes: { on: { forever: true } } } } } } + }, + bar: true + } + const emptyObj = {} + const p = Promise.resolve() + const arrowFn = () => {} + /* eslint-enable no-unused-vars */ + return { hello: request.params.name } // BREAKPOINT: /foo }) @@ -22,30 +43,4 @@ fastify.listen({ port: process.env.APP_PORT }, (err) => { process.send({ port: process.env.APP_PORT }) }) -function getSomeData () { - return { - nil: null, - undef: undefined, - bool: true, - num: 42, - bigint: 42n, - str: 'foo', - // eslint-disable-next-line @stylistic/js/max-len - lstr: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', - sym: Symbol('foo'), - regex: /bar/i, - arr: [1, 2, 3, 4, 5], - obj: { - foo: { - baz: 42, - nil: null, - undef: undefined, - deep: { nested: { obj: { that: { goes: { on: { forever: true } } } } } } - }, - bar: true - }, - emptyObj: {}, - fn: () => {}, - p: Promise.resolve() - } -} +function getUndefined () {} diff --git a/integration-tests/debugger/utils.js b/integration-tests/debugger/utils.js index bca970dea87..9f5175d84fc 100644 --- a/integration-tests/debugger/utils.js +++ b/integration-tests/debugger/utils.js @@ -18,9 +18,9 @@ module.exports = { setup } -function setup () { +function setup ({ env, testApp } = {}) { let sandbox, cwd, appPort - const breakpoints = getBreakpointInfo(1) // `1` to disregard the `setup` function + const breakpoints = getBreakpointInfo({ file: testApp, stackIndex: 1 }) // `1` to disregard the `setup` function const t = { breakpoint: breakpoints[0], breakpoints, @@ -50,9 +50,11 @@ function setup () { function triggerBreakpoint (url) { // Trigger the breakpoint once probe is successfully installed t.agent.on('debugger-diagnostics', ({ payload }) => { - if (payload.debugger.diagnostics.status === 'INSTALLED') { - t.axios.get(url) - } + payload.forEach((event) => { + if (event.debugger.diagnostics.status === 'INSTALLED') { + t.axios.get(url) + } + }) }) } @@ -91,7 +93,8 @@ function setup () { DD_DYNAMIC_INSTRUMENTATION_ENABLED: true, DD_TRACE_AGENT_PORT: t.agent.port, DD_TRACE_DEBUG: process.env.DD_TRACE_DEBUG, // inherit to make debugging the sandbox easier - DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS: pollInterval + DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS: pollInterval, + ...env } }) t.axios = Axios.create({ @@ -107,16 +110,18 @@ function setup () { return t } -function getBreakpointInfo (stackIndex = 0) { - // First, get the filename of file that called this function - const testFile = new Error().stack - .split('\n')[stackIndex + 2] // +2 to skip this function + the first line, which is the error message - .split(' (')[1] - .slice(0, -1) - .split(':')[0] - - // Then, find the corresponding file in which the breakpoint(s) exists - const file = join('target-app', basename(testFile).replace('.spec', '')) +function getBreakpointInfo ({ file, stackIndex = 0 }) { + if (!file) { + // First, get the filename of file that called this function + const testFile = new Error().stack + .split('\n')[stackIndex + 2] // +2 to skip this function + the first line, which is the error message + .split(' (')[1] + .slice(0, -1) + .split(':')[0] + + // Then, find the corresponding file in which the breakpoint(s) exists + file = join('target-app', basename(testFile).replace('.spec', '')) + } // Finally, find the line number(s) of the breakpoint(s) const lines = readFileSync(join(__dirname, file), 'utf8').split('\n') diff --git a/integration-tests/helpers/fake-agent.js b/integration-tests/helpers/fake-agent.js index 4902c80d9a1..317584a5670 100644 --- a/integration-tests/helpers/fake-agent.js +++ b/integration-tests/helpers/fake-agent.js @@ -326,6 +326,7 @@ function buildExpressServer (agent) { res.status(200).send() agent.emit('debugger-input', { headers: req.headers, + query: req.query, payload: req.body }) }) diff --git a/integration-tests/profiler/fstest.js b/integration-tests/profiler/fstest.js new file mode 100644 index 00000000000..c65887c102e --- /dev/null +++ b/integration-tests/profiler/fstest.js @@ -0,0 +1,40 @@ +const fs = require('fs') +const os = require('os') +const path = require('path') + +const tracer = require('dd-trace').init() +tracer.profilerStarted().then(() => { + tracer.trace('x', (_, done) => { + setImmediate(() => { + // Generate 1MB of random data + const buffer = Buffer.alloc(1024 * 1024) + for (let i = 0; i < buffer.length; i++) { + buffer[i] = Math.floor(Math.random() * 256) + } + + // Create a temporary file + const tempFilePath = path.join(os.tmpdir(), 'tempfile.txt') + + fs.writeFile(tempFilePath, buffer, (err) => { + if (err) throw err + + // Read the data back + setImmediate(() => { + fs.readFile(tempFilePath, (err, readData) => { + setImmediate(() => { + // Delete the temporary file + fs.unlink(tempFilePath, (err) => { + if (err) throw err + }) + done() + }) + if (err) throw err + if (Buffer.compare(buffer, readData) !== 0) { + throw new Error('Data read from file is different from data written to file') + } + }) + }) + }) + }) + }) +}) diff --git a/integration-tests/profiler/profiler.spec.js b/integration-tests/profiler/profiler.spec.js index 80be4c8fd36..6c7f4942e1e 100644 --- a/integration-tests/profiler/profiler.spec.js +++ b/integration-tests/profiler/profiler.spec.js @@ -104,7 +104,108 @@ function expectTimeout (messagePromise, allowErrors = false) { ) } +class TimelineEventProcessor { + constructor (strings, encoded) { + this.strings = strings + this.encoded = encoded + } +} + +class NetworkEventProcessor extends TimelineEventProcessor { + constructor (strings, encoded) { + super(strings, encoded) + + this.hostKey = strings.dedup('host') + this.addressKey = strings.dedup('address') + this.portKey = strings.dedup('port') + } + + processLabel (label, processedLabels) { + switch (label.key) { + case this.hostKey: + processedLabels.host = label.str + return true + case this.addressKey: + processedLabels.address = label.str + return true + case this.portKey: + processedLabels.port = label.num + return true + default: + return false + } + } + + decorateEvent (ev, pl) { + // Exactly one of these is defined + assert.isTrue(!!pl.address !== !!pl.host, this.encoded) + if (pl.address) { + ev.address = this.strings.strings[pl.address] + } else { + ev.host = this.strings.strings[pl.host] + } + if (pl.port) { + ev.port = pl.port + } + } +} + async function gatherNetworkTimelineEvents (cwd, scriptFilePath, eventType, args) { + return gatherTimelineEvents(cwd, scriptFilePath, eventType, args, NetworkEventProcessor) +} + +class FilesystemEventProcessor extends TimelineEventProcessor { + constructor (strings, encoded) { + super(strings, encoded) + + this.fdKey = strings.dedup('fd') + this.fileKey = strings.dedup('file') + this.flagKey = strings.dedup('flag') + this.modeKey = strings.dedup('mode') + this.pathKey = strings.dedup('path') + } + + processLabel (label, processedLabels) { + switch (label.key) { + case this.fdKey: + processedLabels.fd = label.num + return true + case this.fileKey: + processedLabels.file = label.str + return true + case this.flagKey: + processedLabels.flag = label.str + return true + case this.modeKey: + processedLabels.mode = label.str + return true + case this.pathKey: + processedLabels.path = label.str + return true + default: + return false + } + } + + decorateEvent (ev, pl) { + ev.fd = pl.fd + ev.file = this.strings.strings[pl.file] + ev.flag = this.strings.strings[pl.flag] + ev.mode = this.strings.strings[pl.mode] + ev.path = this.strings.strings[pl.path] + for (const [k, v] of Object.entries(ev)) { + if (v === undefined) { + delete ev[k] + } + } + } +} + +async function gatherFilesystemTimelineEvents (cwd, scriptFilePath) { + return gatherTimelineEvents(cwd, scriptFilePath, 'fs', [], FilesystemEventProcessor) +} + +async function gatherTimelineEvents (cwd, scriptFilePath, eventType, args, Processor) { const procStart = BigInt(Date.now() * 1000000) const proc = fork(path.join(cwd, scriptFilePath), args, { cwd, @@ -123,36 +224,35 @@ async function gatherNetworkTimelineEvents (cwd, scriptFilePath, eventType, args const strings = profile.stringTable const tsKey = strings.dedup('end_timestamp_ns') const eventKey = strings.dedup('event') - const hostKey = strings.dedup('host') - const addressKey = strings.dedup('address') - const portKey = strings.dedup('port') - const nameKey = strings.dedup('operation') + const operationKey = strings.dedup('operation') const spanIdKey = strings.dedup('span id') const localRootSpanIdKey = strings.dedup('local root span id') const eventValue = strings.dedup(eventType) const events = [] + const processor = new Processor(strings, encoded) for (const sample of profile.sample) { - let ts, event, host, address, port, name, spanId, localRootSpanId + let ts, event, operation, spanId, localRootSpanId + const processedLabels = {} const unexpectedLabels = [] for (const label of sample.label) { switch (label.key) { case tsKey: ts = label.num; break - case nameKey: name = label.str; break + case operationKey: operation = label.str; break case eventKey: event = label.str; break - case hostKey: host = label.str; break - case addressKey: address = label.str; break - case portKey: port = label.num; break case spanIdKey: spanId = label.str; break case localRootSpanIdKey: localRootSpanId = label.str; break - default: unexpectedLabels.push(label.key) + default: + if (!processor.processLabel(label, processedLabels)) { + unexpectedLabels.push(label.key) + } } } - // Gather only DNS events; ignore sporadic GC events + // Timestamp must be defined and be between process start and end time + assert.isDefined(ts, encoded) + assert.isTrue(ts <= procEnd, encoded) + assert.isTrue(ts >= procStart, encoded) + // Gather only tested events if (event === eventValue) { - // Timestamp must be defined and be between process start and end time - assert.isDefined(ts, encoded) - assert.isTrue(ts <= procEnd, encoded) - assert.isTrue(ts >= procStart, encoded) if (process.platform !== 'win32') { assert.isDefined(spanId, encoded) assert.isDefined(localRootSpanId, encoded) @@ -160,23 +260,14 @@ async function gatherNetworkTimelineEvents (cwd, scriptFilePath, eventType, args assert.isUndefined(spanId, encoded) assert.isUndefined(localRootSpanId, encoded) } - assert.isDefined(name, encoded) + assert.isDefined(operation, encoded) if (unexpectedLabels.length > 0) { const labelsStr = JSON.stringify(unexpectedLabels) const labelsStrStr = unexpectedLabels.map(k => strings.strings[k]).join(',') assert.fail(`Unexpected labels: ${labelsStr}\n${labelsStrStr}\n${encoded}`) } - // Exactly one of these is defined - assert.isTrue(!!address !== !!host, encoded) - const ev = { name: strings.strings[name] } - if (address) { - ev.address = strings.strings[address] - } else { - ev.host = strings.strings[host] - } - if (port) { - ev.port = port - } + const ev = { operation: strings.strings[operation] } + processor.decorateEvent(ev, processedLabels) events.push(ev) } } @@ -323,14 +414,30 @@ describe('profiler', () => { assert.equal(endpoints.size, 3, encoded) }) + it('fs timeline events work', async () => { + const fsEvents = await gatherFilesystemTimelineEvents(cwd, 'profiler/fstest.js') + assert.equal(fsEvents.length, 6) + const path = fsEvents[0].path + const fd = fsEvents[1].fd + assert(path.endsWith('tempfile.txt')) + assert.sameDeepMembers(fsEvents, [ + { flag: 'w', mode: '', operation: 'open', path }, + { fd, operation: 'write' }, + { fd, operation: 'close' }, + { file: path, operation: 'writeFile' }, + { operation: 'readFile', path }, + { operation: 'unlink', path } + ]) + }) + it('dns timeline events work', async () => { const dnsEvents = await gatherNetworkTimelineEvents(cwd, 'profiler/dnstest.js', 'dns') assert.sameDeepMembers(dnsEvents, [ - { name: 'lookup', host: 'example.org' }, - { name: 'lookup', host: 'example.com' }, - { name: 'lookup', host: 'datadoghq.com' }, - { name: 'queryA', host: 'datadoghq.com' }, - { name: 'lookupService', address: '13.224.103.60', port: 80 } + { operation: 'lookup', host: 'example.org' }, + { operation: 'lookup', host: 'example.com' }, + { operation: 'lookup', host: 'datadoghq.com' }, + { operation: 'queryA', host: 'datadoghq.com' }, + { operation: 'lookupService', address: '13.224.103.60', port: 80 } ]) }) @@ -366,8 +473,8 @@ describe('profiler', () => { // The profiled program should have two TCP connection events to the two // servers. assert.sameDeepMembers(events, [ - { name: 'connect', host: '127.0.0.1', port: port1 }, - { name: 'connect', host: '127.0.0.1', port: port2 } + { operation: 'connect', host: '127.0.0.1', port: port1 }, + { operation: 'connect', host: '127.0.0.1', port: port2 } ]) } finally { server2.close() diff --git a/package.json b/package.json index cd540cb08a0..fedd38e7312 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ }, "dependencies": { "@datadog/libdatadog": "^0.3.0", - "@datadog/native-appsec": "8.3.0", + "@datadog/native-appsec": "8.4.0", "@datadog/native-iast-rewriter": "2.6.1", "@datadog/native-iast-taint-tracking": "3.2.0", "@datadog/native-metrics": "^3.1.0", @@ -118,7 +118,7 @@ "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.11.1", "@stylistic/eslint-plugin-js": "^2.8.0", - "@types/node": "^16.18.103", + "@types/node": "^16.0.0", "autocannon": "^4.5.2", "aws-sdk": "^2.1446.0", "axios": "^1.7.4", @@ -145,7 +145,7 @@ "jszip": "^3.5.0", "knex": "^2.4.2", "mkdirp": "^3.0.1", - "mocha": "^9", + "mocha": "^10", "msgpack-lite": "^0.1.26", "multer": "^1.4.5-lts.1", "nock": "^11.3.3", diff --git a/packages/datadog-core/src/storage.js b/packages/datadog-core/src/storage.js index 15c9fff239c..fb5d889e555 100644 --- a/packages/datadog-core/src/storage.js +++ b/packages/datadog-core/src/storage.js @@ -21,8 +21,16 @@ class DatadogStorage { this._storage.exit(callback, ...args) } - getStore () { - const handle = this._storage.getStore() + // TODO: Refactor the Scope class to use a span-only store and remove this. + getHandle () { + return this._storage.getStore() + } + + getStore (handle) { + if (!handle) { + handle = this._storage.getStore() + } + return stores.get(handle) } @@ -50,6 +58,7 @@ const storage = function (namespace) { storage.disable = legacyStorage.disable.bind(legacyStorage) storage.enterWith = legacyStorage.enterWith.bind(legacyStorage) storage.exit = legacyStorage.exit.bind(legacyStorage) +storage.getHandle = legacyStorage.getHandle.bind(legacyStorage) storage.getStore = legacyStorage.getStore.bind(legacyStorage) storage.run = legacyStorage.run.bind(legacyStorage) diff --git a/packages/datadog-instrumentations/src/aerospike.js b/packages/datadog-instrumentations/src/aerospike.js index 497a64aaf80..ba310b6e2de 100644 --- a/packages/datadog-instrumentations/src/aerospike.js +++ b/packages/datadog-instrumentations/src/aerospike.js @@ -40,7 +40,7 @@ function wrapProcess (process) { addHook({ name: 'aerospike', file: 'lib/commands/command.js', - versions: ['4', '5'] + versions: ['4', '5', '6'] }, commandFactory => { return shimmer.wrapFunction(commandFactory, f => wrapCreateCommand(f)) diff --git a/packages/datadog-instrumentations/src/fs.js b/packages/datadog-instrumentations/src/fs.js index 9ae201b9860..894c1b6ef33 100644 --- a/packages/datadog-instrumentations/src/fs.js +++ b/packages/datadog-instrumentations/src/fs.js @@ -13,6 +13,9 @@ const errorChannel = channel('apm:fs:operation:error') const ddFhSym = Symbol('ddFileHandle') let kHandle, kDirReadPromisified, kDirClosePromisified +// Update packages/dd-trace/src/profiling/profilers/event_plugins/fs.js if you make changes to param names in any of +// the following objects. + const paramsByMethod = { access: ['path', 'mode'], appendFile: ['path', 'data', 'options'], diff --git a/packages/datadog-instrumentations/src/pg.js b/packages/datadog-instrumentations/src/pg.js index 6c3d621ad00..331557cd239 100644 --- a/packages/datadog-instrumentations/src/pg.js +++ b/packages/datadog-instrumentations/src/pg.js @@ -62,11 +62,11 @@ function wrapQuery (query) { abortController }) - const finish = asyncResource.bind(function (error) { + const finish = asyncResource.bind(function (error, res) { if (error) { errorCh.publish(error) } - finishCh.publish() + finishCh.publish({ result: res?.rows }) }) if (abortController.signal.aborted) { @@ -119,15 +119,15 @@ function wrapQuery (query) { if (newQuery.callback) { const originalCallback = callbackResource.bind(newQuery.callback) newQuery.callback = function (err, res) { - finish(err) + finish(err, res) return originalCallback.apply(this, arguments) } } else if (newQuery.once) { newQuery .once('error', finish) - .once('end', () => finish()) + .once('end', (res) => finish(null, res)) } else { - newQuery.then(() => finish(), finish) + newQuery.then((res) => finish(null, res), finish) } try { diff --git a/packages/datadog-instrumentations/src/sequelize.js b/packages/datadog-instrumentations/src/sequelize.js index 8ba56ee8909..d8e41b17704 100644 --- a/packages/datadog-instrumentations/src/sequelize.js +++ b/packages/datadog-instrumentations/src/sequelize.js @@ -13,7 +13,7 @@ addHook({ name: 'sequelize', versions: ['>=4'] }, Sequelize => { const finishCh = channel('datadog:sequelize:query:finish') shimmer.wrap(Sequelize.prototype, 'query', query => { - return function (sql) { + return function (sql, options) { if (!startCh.hasSubscribers) { return query.apply(this, arguments) } @@ -27,9 +27,14 @@ addHook({ name: 'sequelize', versions: ['>=4'] }, Sequelize => { dialect = this.dialect.name } - function onFinish () { + function onFinish (result) { + const type = options?.type || 'RAW' + if (type === 'RAW' && result?.length > 1) { + result = result[0] + } + asyncResource.bind(function () { - finishCh.publish() + finishCh.publish({ result }) }, this).apply(this) } @@ -40,7 +45,7 @@ addHook({ name: 'sequelize', versions: ['>=4'] }, Sequelize => { }) const promise = query.apply(this, arguments) - promise.then(onFinish, onFinish) + promise.then(onFinish, () => { onFinish() }) return promise }, this).apply(this, arguments) diff --git a/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js b/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js index 4097586b2c5..cbca2192ad6 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js +++ b/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js @@ -1,6 +1,9 @@ 'use strict' const BaseAwsSdkPlugin = require('../base') +const log = require('../../../dd-trace/src/log') +const { DYNAMODB_PTR_KIND, SPAN_POINTER_DIRECTION } = require('../../../dd-trace/src/constants') +const { extractPrimaryKeys, generatePointerHash } = require('../util') class DynamoDb extends BaseAwsSdkPlugin { static get id () { return 'dynamodb' } @@ -48,6 +51,157 @@ class DynamoDb extends BaseAwsSdkPlugin { return tags } + + addSpanPointers (span, response) { + const request = response?.request + const operationName = request?.operation + + const hashes = [] + switch (operationName) { + case 'putItem': { + const hash = DynamoDb.calculatePutItemHash( + request?.params?.TableName, + request?.params?.Item, + this.getPrimaryKeyConfig() + ) + if (hash) hashes.push(hash) + break + } + case 'updateItem': + case 'deleteItem': { + const hash = DynamoDb.calculateHashWithKnownKeys(request?.params?.TableName, request?.params?.Key) + if (hash) hashes.push(hash) + break + } + case 'transactWriteItems': { + const transactItems = request?.params?.TransactItems || [] + for (const item of transactItems) { + if (item.Put) { + const hash = + DynamoDb.calculatePutItemHash(item.Put.TableName, item.Put.Item, this.getPrimaryKeyConfig()) + if (hash) hashes.push(hash) + } else if (item.Update || item.Delete) { + const operation = item.Update ? item.Update : item.Delete + const hash = DynamoDb.calculateHashWithKnownKeys(operation.TableName, operation.Key) + if (hash) hashes.push(hash) + } + } + break + } + case 'batchWriteItem': { + const requestItems = request?.params.RequestItems || {} + for (const [tableName, operations] of Object.entries(requestItems)) { + if (!Array.isArray(operations)) continue + for (const operation of operations) { + if (operation?.PutRequest) { + const hash = + DynamoDb.calculatePutItemHash(tableName, operation.PutRequest.Item, this.getPrimaryKeyConfig()) + if (hash) hashes.push(hash) + } else if (operation?.DeleteRequest) { + const hash = DynamoDb.calculateHashWithKnownKeys(tableName, operation.DeleteRequest.Key) + if (hash) hashes.push(hash) + } + } + } + break + } + } + + for (const hash of hashes) { + span.addSpanPointer(DYNAMODB_PTR_KIND, SPAN_POINTER_DIRECTION.DOWNSTREAM, hash) + } + } + + /** + * Parses primary key config from the `DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS` env var. + * Only runs when needed, and warns when missing or invalid config. + * @returns {Object|undefined} Parsed config from env var or undefined if empty/missing/invalid config. + */ + getPrimaryKeyConfig () { + if (this.dynamoPrimaryKeyConfig) { + // Return cached config if it exists + return this.dynamoPrimaryKeyConfig + } + + const configStr = this._tracerConfig?.aws?.dynamoDb?.tablePrimaryKeys + if (!configStr) { + log.warn('Missing DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS env variable. ' + + 'Please add your table\'s primary keys under this env variable.') + return + } + + try { + const parsedConfig = JSON.parse(configStr) + const config = {} + for (const [tableName, primaryKeys] of Object.entries(parsedConfig)) { + if (Array.isArray(primaryKeys) && primaryKeys.length > 0 && primaryKeys.length <= 2) { + config[tableName] = new Set(primaryKeys) + } else { + log.warn(`Invalid primary key configuration for table: ${tableName}.` + + 'Please fix the DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS env var.') + } + } + + this.dynamoPrimaryKeyConfig = config + return config + } catch (err) { + log.warn('Failed to parse DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS:', err.message) + } + } + + /** + * Calculates a hash for DynamoDB PutItem operations using table's configured primary keys. + * @param {string} tableName - Name of the DynamoDB table. + * @param {Object} item - Complete PutItem item parameter to be put. + * @param {Object.>} primaryKeyConfig - Mapping of table names to Sets of primary key names + * loaded from DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS. + * @returns {string|undefined} Hash combining table name and primary key/value pairs, or undefined if unable. + */ + static calculatePutItemHash (tableName, item, primaryKeyConfig) { + if (!tableName || !item) { + log.debug('Unable to calculate hash because missing required parameters') + return + } + if (!primaryKeyConfig) { + log.warn('Missing DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS env variable') + return + } + const primaryKeySet = primaryKeyConfig[tableName] + if (!primaryKeySet || !(primaryKeySet instanceof Set) || primaryKeySet.size === 0 || primaryKeySet.size > 2) { + log.warn( + `span pointers: failed to extract PutItem span pointer: table ${tableName} ` + + 'not found in primary key names or the DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS env var was invalid.' + + 'Please update the env var.' + ) + return + } + const keyValues = extractPrimaryKeys(primaryKeySet, item) + if (keyValues) { + return generatePointerHash([tableName, ...keyValues]) + } + } + + /** + * Calculates a hash for DynamoDB operations that have keys provided (UpdateItem, DeleteItem). + * @param {string} tableName - Name of the DynamoDB table. + * @param {Object} keysObject - Object containing primary key/value attributes in DynamoDB format. + * (e.g., { userId: { S: "123" }, sortKey: { N: "456" } }) + * @returns {string|undefined} Hash value combining table name and primary key/value pairs, or undefined if unable. + * + * @example + * calculateHashWithKnownKeys('UserTable', { userId: { S: "user123" }, timestamp: { N: "1234567" } }) + */ + static calculateHashWithKnownKeys (tableName, keysObject) { + if (!tableName || !keysObject) { + log.debug('Unable to calculate hash because missing parameters') + return + } + const keyNamesSet = new Set(Object.keys(keysObject)) + const keyValues = extractPrimaryKeys(keyNamesSet, keysObject) + if (keyValues) { + return generatePointerHash([tableName, ...keyValues]) + } + } } module.exports = DynamoDb diff --git a/packages/datadog-plugin-aws-sdk/src/services/s3.js b/packages/datadog-plugin-aws-sdk/src/services/s3.js index 5fcfb6ed165..d860223d67b 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/s3.js +++ b/packages/datadog-plugin-aws-sdk/src/services/s3.js @@ -2,7 +2,7 @@ const BaseAwsSdkPlugin = require('../base') const log = require('../../../dd-trace/src/log') -const { generatePointerHash } = require('../../../dd-trace/src/util') +const { generatePointerHash } = require('../util') const { S3_PTR_KIND, SPAN_POINTER_DIRECTION } = require('../../../dd-trace/src/constants') class S3 extends BaseAwsSdkPlugin { diff --git a/packages/datadog-plugin-aws-sdk/src/util.js b/packages/datadog-plugin-aws-sdk/src/util.js new file mode 100644 index 00000000000..4bb7e86c8cd --- /dev/null +++ b/packages/datadog-plugin-aws-sdk/src/util.js @@ -0,0 +1,92 @@ +'use strict' + +const crypto = require('crypto') +const log = require('../../dd-trace/src/log') + +/** + * Generates a unique hash from an array of strings by joining them with | before hashing. + * Used to uniquely identify AWS requests for span pointers. + * @param {string[]} components - Array of strings to hash + * @returns {string} A 32-character hash uniquely identifying the components + */ +function generatePointerHash (components) { + // If passing S3's ETag as a component, make sure any quotes have already been removed! + const dataToHash = components.join('|') + const hash = crypto.createHash('sha256').update(dataToHash).digest('hex') + return hash.substring(0, 32) +} + +/** + * Encodes a DynamoDB attribute value to Buffer for span pointer hashing. + * @param {Object} valueObject - DynamoDB value in AWS format ({ S: string } or { N: string } or { B: Buffer }) + * @returns {Buffer|undefined} Encoded value as Buffer, or undefined if invalid input. + * + * @example + * encodeValue({ S: "user123" }) -> Buffer("user123") + * encodeValue({ N: "42" }) -> Buffer("42") + * encodeValue({ B: Buffer([1, 2, 3]) }) -> Buffer([1, 2, 3]) + */ +function encodeValue (valueObject) { + if (!valueObject) { + return + } + + try { + const type = Object.keys(valueObject)[0] + const value = valueObject[type] + + switch (type) { + case 'S': + return Buffer.from(value) + case 'N': + return Buffer.from(value.toString()) + case 'B': + return Buffer.isBuffer(value) ? value : Buffer.from(value) + default: + log.debug(`Found unknown type while trying to create DynamoDB span pointer: ${type}`) + } + } catch (err) { + log.debug(`Failed to encode value while trying to create DynamoDB span pointer: ${err.message}`) + } +} + +/** + * Extracts and encodes primary key values from a DynamoDB item. + * Handles tables with single-key and two-key scenarios. + * + * @param {Set} keySet - Set of primary key names. + * @param {Object} keyValuePairs - Object containing key/value pairs. + * @returns {Array|undefined} [key1Name, key1Value, key2Name, key2Value], or undefined if invalid input. + * key2 entries are empty strings in the single-key case. + * @example + * extractPrimaryKeys(new Set(['userId']), {userId: {S: "user123"}}) + * // Returns ["userId", Buffer("user123"), "", ""] + * extractPrimaryKeys(new Set(['userId', 'timestamp']), {userId: {S: "user123"}, timestamp: {N: "1234}}) + * // Returns ["timestamp", Buffer.from("1234"), "userId", Buffer.from("user123")] + */ +const extractPrimaryKeys = (keySet, keyValuePairs) => { + const keyNames = Array.from(keySet) + if (keyNames.length === 0) { + return + } + + if (keyNames.length === 1) { + const value = encodeValue(keyValuePairs[keyNames[0]]) + if (value) { + return [keyNames[0], value, '', ''] + } + } else { + const [key1, key2] = keyNames.sort() + const value1 = encodeValue(keyValuePairs[key1]) + const value2 = encodeValue(keyValuePairs[key2]) + if (value1 && value2) { + return [key1, value1, key2, value2] + } + } +} + +module.exports = { + generatePointerHash, + encodeValue, + extractPrimaryKeys +} diff --git a/packages/datadog-plugin-aws-sdk/test/dynamodb.spec.js b/packages/datadog-plugin-aws-sdk/test/dynamodb.spec.js new file mode 100644 index 00000000000..7fba9babfb0 --- /dev/null +++ b/packages/datadog-plugin-aws-sdk/test/dynamodb.spec.js @@ -0,0 +1,831 @@ +'use strict' + +const agent = require('../../dd-trace/test/plugins/agent') +const { setup } = require('./spec_helpers') +const axios = require('axios') +const { DYNAMODB_PTR_KIND, SPAN_POINTER_DIRECTION } = require('../../dd-trace/src/constants') +const DynamoDb = require('../src/services/dynamodb') +const { generatePointerHash } = require('../src/util') + +/* eslint-disable no-console */ +async function resetLocalStackDynamo () { + try { + await axios.post('http://localhost:4566/reset') + console.log('LocalStack Dynamo reset successful') + } catch (error) { + console.error('Error resetting LocalStack Dynamo:', error.message) + } +} + +describe('Plugin', () => { + describe('aws-sdk (dynamodb)', function () { + setup() + + withVersions('aws-sdk', ['aws-sdk', '@aws-sdk/smithy-client'], (version, moduleName) => { + let tracer + let AWS + let dynamo + + const dynamoClientName = moduleName === '@aws-sdk/smithy-client' ? '@aws-sdk/client-dynamodb' : 'aws-sdk' + + // Test both cases: tables with only partition key and with partition+sort key. + const oneKeyTableName = 'OneKeyTable' + const twoKeyTableName = 'TwoKeyTable' + + describe('with configuration', () => { + before(() => { + tracer = require('../../dd-trace') + tracer.init() + return agent.load('aws-sdk') + }) + + before(async () => { + AWS = require(`../../../versions/${dynamoClientName}@${version}`).get() + dynamo = new AWS.DynamoDB({ endpoint: 'http://127.0.0.1:4566', region: 'us-east-1' }) + + const deleteTable = async (tableName) => { + if (dynamoClientName === '@aws-sdk/client-dynamodb') { + try { + await dynamo.deleteTable({ TableName: tableName }) + await new Promise(resolve => setTimeout(resolve, 1000)) + } catch (err) { + if (err.name !== 'ResourceNotFoundException') { + throw err + } + } + } else { + try { + if (typeof dynamo.deleteTable({}).promise === 'function') { + await dynamo.deleteTable({ TableName: tableName }).promise() + await dynamo.waitFor('tableNotExists', { TableName: tableName }).promise() + } else { + await new Promise((resolve, reject) => { + dynamo.deleteTable({ TableName: tableName }, (err) => { + if (err && err.code !== 'ResourceNotFoundException') { + reject(err) + } else { + resolve() + } + }) + }) + } + } catch (err) { + if (err.code !== 'ResourceNotFoundException') { + throw err + } + } + } + } + + const createTable = async (params) => { + if (dynamoClientName === '@aws-sdk/client-dynamodb') { + await dynamo.createTable(params) + } else { + if (typeof dynamo.createTable({}).promise === 'function') { + await dynamo.createTable(params).promise() + } else { + await new Promise((resolve, reject) => { + dynamo.createTable(params, (err, data) => { + if (err) reject(err) + else resolve(data) + }) + }) + } + } + } + + // Delete existing tables + await deleteTable(oneKeyTableName) + await deleteTable(twoKeyTableName) + + // Create tables + await createTable({ + TableName: oneKeyTableName, + KeySchema: [{ AttributeName: 'name', KeyType: 'HASH' }], + AttributeDefinitions: [{ AttributeName: 'name', AttributeType: 'S' }], + ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 } + }) + + await createTable({ + TableName: twoKeyTableName, + KeySchema: [ + { AttributeName: 'id', KeyType: 'HASH' }, + { AttributeName: 'binary', KeyType: 'RANGE' } + ], + AttributeDefinitions: [ + { AttributeName: 'id', AttributeType: 'N' }, + { AttributeName: 'binary', AttributeType: 'B' } + ], + ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 } + }) + }) + + after(async () => { + await resetLocalStackDynamo() + return agent.close({ ritmReset: false }) + }) + + describe('span pointers', () => { + beforeEach(() => { + DynamoDb.dynamoPrimaryKeyConfig = null + delete process.env.DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS + }) + + function testSpanPointers ({ expectedHashes, operation }) { + let expectedLength = 0 + if (expectedHashes) { + expectedLength = Array.isArray(expectedHashes) ? expectedHashes.length : 1 + } + return (done) => { + operation((err) => { + if (err) { + return done(err) + } + + agent.use(traces => { + try { + const span = traces[0][0] + const links = JSON.parse(span.meta?.['_dd.span_links'] || '[]') + expect(links).to.have.lengthOf(expectedLength) + + if (expectedHashes) { + if (Array.isArray(expectedHashes)) { + expectedHashes.forEach((hash, i) => { + expect(links[i].attributes['ptr.hash']).to.equal(hash) + }) + } else { + expect(links[0].attributes).to.deep.equal({ + 'ptr.kind': DYNAMODB_PTR_KIND, + 'ptr.dir': SPAN_POINTER_DIRECTION.DOWNSTREAM, + 'ptr.hash': expectedHashes, + 'link.kind': 'span-pointer' + }) + } + } + return done() + } catch (error) { + return done(error) + } + }).catch(error => { + done(error) + }) + }) + } + } + + describe('1-key table', () => { + it('should add span pointer for putItem when config is valid', () => { + testSpanPointers({ + expectedHashes: '27f424c8202ab35efbf8b0b444b1928f', + operation: (callback) => { + process.env.DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS = + '{"OneKeyTable": ["name"]}' + dynamo.putItem({ + TableName: oneKeyTableName, + Item: { + name: { S: 'test1' }, + foo: { S: 'bar1' } + } + }, callback) + } + }) + }) + + it('should not add links or error for putItem when config is invalid', () => { + testSpanPointers({ + operation: (callback) => { + process.env.DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS = '{"DifferentTable": ["test"]}' + dynamo.putItem({ + TableName: oneKeyTableName, + Item: { + name: { S: 'test2' }, + foo: { S: 'bar2' } + } + }, callback) + } + }) + }) + + it('should not add links or error for putItem when config is missing', () => { + testSpanPointers({ + operation: (callback) => { + process.env.DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS = null + dynamo.putItem({ + TableName: oneKeyTableName, + Item: { + name: { S: 'test3' }, + foo: { S: 'bar3' } + } + }, callback) + } + }) + }) + + it('should add span pointer for updateItem', () => { + testSpanPointers({ + expectedHashes: '27f424c8202ab35efbf8b0b444b1928f', + operation: (callback) => { + dynamo.updateItem({ + TableName: oneKeyTableName, + Key: { name: { S: 'test1' } }, + AttributeUpdates: { + foo: { + Action: 'PUT', + Value: { S: 'bar4' } + } + } + }, callback) + } + }) + }) + + it('should add span pointer for deleteItem', () => { + testSpanPointers({ + expectedHashes: '27f424c8202ab35efbf8b0b444b1928f', + operation: (callback) => { + dynamo.deleteItem({ + TableName: oneKeyTableName, + Key: { name: { S: 'test1' } } + }, callback) + } + }) + }) + + it('should add span pointers for transactWriteItems', () => { + // Skip for older versions that don't support transactWriteItems + if (typeof dynamo.transactWriteItems !== 'function') { + return + } + testSpanPointers({ + expectedHashes: [ + '955ab85fc7d1d63fe4faf18696514f13', + '856c95a173d9952008a70283175041fc', + '9682c132f1900106a792f166d0619e0b' + ], + operation: (callback) => { + process.env.DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS = '{"OneKeyTable": ["name"]}' + dynamo.transactWriteItems({ + TransactItems: [ + { + Put: { + TableName: oneKeyTableName, + Item: { + name: { S: 'test4' }, + foo: { S: 'bar4' } + } + } + }, + { + Update: { + TableName: oneKeyTableName, + Key: { name: { S: 'test2' } }, + UpdateExpression: 'SET foo = :newfoo', + ExpressionAttributeValues: { + ':newfoo': { S: 'bar5' } + } + } + }, + { + Delete: { + TableName: oneKeyTableName, + Key: { name: { S: 'test3' } } + } + } + ] + }, callback) + } + }) + }) + + it('should add span pointers for batchWriteItem', () => { + // Skip for older versions that don't support batchWriteItem + if (typeof dynamo.batchWriteItem !== 'function') { + return + } + testSpanPointers({ + expectedHashes: [ + '955ab85fc7d1d63fe4faf18696514f13', + '9682c132f1900106a792f166d0619e0b' + ], + operation: (callback) => { + process.env.DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS = '{"OneKeyTable": ["name"]}' + dynamo.batchWriteItem({ + RequestItems: { + [oneKeyTableName]: [ + { + PutRequest: { + Item: { + name: { S: 'test4' }, + foo: { S: 'bar4' } + } + } + }, + { + DeleteRequest: { + Key: { + name: { S: 'test3' } + } + } + } + ] + } + }, callback) + } + }) + }) + }) + + describe('2-key table', () => { + it('should add span pointer for putItem when config is valid', () => { + testSpanPointers({ + expectedHashes: 'cc32f0e49ee05d3f2820ccc999bfe306', + operation: (callback) => { + process.env.DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS = '{"TwoKeyTable": ["id", "binary"]}' + dynamo.putItem({ + TableName: twoKeyTableName, + Item: { + id: { N: '1' }, + binary: { B: Buffer.from('Hello world 1') } + } + }, callback) + } + }) + }) + + it('should not add links or error for putItem when config is invalid', () => { + testSpanPointers({ + operation: (callback) => { + process.env.DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS = '{"DifferentTable": ["test"]}' + dynamo.putItem({ + TableName: twoKeyTableName, + Item: { + id: { N: '2' }, + binary: { B: Buffer.from('Hello world 2') } + } + }, callback) + } + }) + }) + + it('should not add links or error for putItem when config is missing', () => { + testSpanPointers({ + operation: (callback) => { + process.env.DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS = null + dynamo.putItem({ + TableName: twoKeyTableName, + Item: { + id: { N: '3' }, + binary: { B: Buffer.from('Hello world 3') } + } + }, callback) + } + }) + }) + + it('should add span pointer for updateItem', function (done) { + dynamo.putItem({ + TableName: twoKeyTableName, + Item: { + id: { N: '100' }, + binary: { B: Buffer.from('abc') } + } + }, async function (err) { + if (err) { + return done(err) + } + await new Promise(resolve => setTimeout(resolve, 100)) + testSpanPointers({ + expectedHashes: '5dac7d25254d596482a3c2c187e51046', + operation: (callback) => { + dynamo.updateItem({ + TableName: twoKeyTableName, + Key: { + id: { N: '100' }, + binary: { B: Buffer.from('abc') } + }, + AttributeUpdates: { + someOtherField: { + Action: 'PUT', + Value: { S: 'new value' } + } + } + }, callback) + } + })(done) + }) + }) + + it('should add span pointer for deleteItem', function (done) { + dynamo.putItem({ + TableName: twoKeyTableName, + Item: { + id: { N: '200' }, + binary: { B: Buffer.from('Hello world') } + } + }, async function (err) { + if (err) return done(err) + await new Promise(resolve => setTimeout(resolve, 100)) + testSpanPointers({ + expectedHashes: 'c356b0dd48c734d889e95122750c2679', + operation: (callback) => { + dynamo.deleteItem({ + TableName: twoKeyTableName, + Key: { + id: { N: '200' }, + binary: { B: Buffer.from('Hello world') } + } + }, callback) + } + })(done) + }) + }) + + it('should add span pointers for transactWriteItems', () => { + // Skip for older versions that don't support transactWriteItems + if (typeof dynamo.transactWriteItems !== 'function') { + return + } + testSpanPointers({ + expectedHashes: [ + 'dd071963cd90e4b3088043f0b9a9f53c', + '7794824f72d673ac7844353bc3ea25d9', + '8a6f801cc4e7d1d5e0dd37e0904e6316' + ], + operation: (callback) => { + process.env.DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS = '{"TwoKeyTable": ["id", "binary"]}' + dynamo.transactWriteItems({ + TransactItems: [ + { + Put: { + TableName: twoKeyTableName, + Item: { + id: { N: '4' }, + binary: { B: Buffer.from('Hello world 4') } + } + } + }, + { + Update: { + TableName: twoKeyTableName, + Key: { + id: { N: '2' }, + binary: { B: Buffer.from('Hello world 2') } + }, + AttributeUpdates: { + someOtherField: { + Action: 'PUT', + Value: { S: 'new value' } + } + } + } + }, + { + Delete: { + TableName: twoKeyTableName, + Key: { + id: { N: '3' }, + binary: { B: Buffer.from('Hello world 3') } + } + } + } + ] + }, callback) + } + }) + }) + + it('should add span pointers for batchWriteItem', () => { + // Skip for older versions that don't support batchWriteItem + if (typeof dynamo.batchWriteItem !== 'function') { + return + } + testSpanPointers({ + expectedHashes: [ + '1f64650acbe1ae4d8413049c6bd9bbe8', + '8a6f801cc4e7d1d5e0dd37e0904e6316' + ], + operation: (callback) => { + process.env.DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS = '{"TwoKeyTable": ["id", "binary"]}' + dynamo.batchWriteItem({ + RequestItems: { + [twoKeyTableName]: [ + { + PutRequest: { + Item: { + id: { N: '5' }, + binary: { B: Buffer.from('Hello world 5') } + } + } + }, + { + DeleteRequest: { + Key: { + id: { N: '3' }, + binary: { B: Buffer.from('Hello world 3') } + } + } + } + ] + } + }, callback) + } + }) + }) + }) + }) + }) + }) + + describe('getPrimaryKeyConfig', () => { + let dynamoDbInstance + + beforeEach(() => { + dynamoDbInstance = new DynamoDb() + dynamoDbInstance.dynamoPrimaryKeyConfig = null + dynamoDbInstance._tracerConfig = {} + }) + + it('should return cached config if available', () => { + const cachedConfig = { Table1: new Set(['key1']) } + dynamoDbInstance.dynamoPrimaryKeyConfig = cachedConfig + + const result = dynamoDbInstance.getPrimaryKeyConfig() + expect(result).to.equal(cachedConfig) + }) + + it('should return undefined when config str is missing', () => { + const result = dynamoDbInstance.getPrimaryKeyConfig() + expect(result).to.be.undefined + }) + + it('should parse valid config with single table', () => { + const configStr = '{"Table1": ["key1", "key2"]}' + dynamoDbInstance._tracerConfig = { aws: { dynamoDb: { tablePrimaryKeys: configStr } } } + + const result = dynamoDbInstance.getPrimaryKeyConfig() + expect(result).to.deep.equal({ + Table1: new Set(['key1', 'key2']) + }) + }) + + it('should parse valid config with multiple tables', () => { + const configStr = '{"Table1": ["key1"], "Table2": ["key2", "key3"]}' + dynamoDbInstance._tracerConfig = { aws: { dynamoDb: { tablePrimaryKeys: configStr } } } + + const result = dynamoDbInstance.getPrimaryKeyConfig() + expect(result).to.deep.equal({ + Table1: new Set(['key1']), + Table2: new Set(['key2', 'key3']) + }) + }) + }) + + describe('calculatePutItemHash', () => { + it('generates correct hash for single string key', () => { + const tableName = 'UserTable' + const item = { userId: { S: 'user123' }, name: { S: 'John' } } + const keyConfig = { UserTable: new Set(['userId']) } + + const actualHash = DynamoDb.calculatePutItemHash(tableName, item, keyConfig) + const expectedHash = generatePointerHash([tableName, 'userId', 'user123', '', '']) + expect(actualHash).to.equal(expectedHash) + }) + + it('generates correct hash for single number key', () => { + const tableName = 'OrderTable' + const item = { orderId: { N: '98765' }, total: { N: '50.00' } } + const keyConfig = { OrderTable: new Set(['orderId']) } + + const actualHash = DynamoDb.calculatePutItemHash(tableName, item, keyConfig) + const expectedHash = generatePointerHash([tableName, 'orderId', '98765', '', '']) + expect(actualHash).to.equal(expectedHash) + }) + + it('generates correct hash for single binary key', () => { + const tableName = 'BinaryTable' + const binaryData = Buffer.from([1, 2, 3]) + const item = { binaryId: { B: binaryData }, data: { S: 'test' } } + const keyConfig = { BinaryTable: new Set(['binaryId']) } + + const actualHash = DynamoDb.calculatePutItemHash(tableName, item, keyConfig) + const expectedHash = generatePointerHash([tableName, 'binaryId', binaryData, '', '']) + expect(actualHash).to.equal(expectedHash) + }) + + it('generates correct hash for string-string key', () => { + const tableName = 'UserEmailTable' + const item = { + userId: { S: 'user123' }, + email: { S: 'test@example.com' }, + verified: { BOOL: true } + } + const keyConfig = { UserEmailTable: new Set(['userId', 'email']) } + + const actualHash = DynamoDb.calculatePutItemHash(tableName, item, keyConfig) + const expectedHash = generatePointerHash([tableName, 'email', 'test@example.com', 'userId', 'user123']) + expect(actualHash).to.equal(expectedHash) + }) + + it('generates correct hash for string-number key', () => { + const tableName = 'UserActivityTable' + const item = { + userId: { S: 'user123' }, + timestamp: { N: '1234567' }, + action: { S: 'login' } + } + const keyConfig = { UserActivityTable: new Set(['userId', 'timestamp']) } + + const actualHash = DynamoDb.calculatePutItemHash(tableName, item, keyConfig) + const expectedHash = generatePointerHash([tableName, 'timestamp', '1234567', 'userId', 'user123']) + expect(actualHash).to.equal(expectedHash) + }) + + it('generates correct hash for binary-binary key', () => { + const tableName = 'BinaryTable' + const binary1 = Buffer.from('abc') + const binary2 = Buffer.from('1ef230') + const item = { + key1: { B: binary1 }, + key2: { B: binary2 }, + data: { S: 'test' } + } + const keyConfig = { BinaryTable: new Set(['key1', 'key2']) } + + const actualHash = DynamoDb.calculatePutItemHash(tableName, item, keyConfig) + const expectedHash = generatePointerHash([tableName, 'key1', binary1, 'key2', binary2]) + expect(actualHash).to.equal(expectedHash) + }) + + it('generates unique hashes for different tables', () => { + const item = { userId: { S: 'user123' } } + const keyConfig = { + Table1: new Set(['userId']), + Table2: new Set(['userId']) + } + + const hash1 = DynamoDb.calculatePutItemHash('Table1', item, keyConfig) + const hash2 = DynamoDb.calculatePutItemHash('Table2', item, keyConfig) + expect(hash1).to.not.equal(hash2) + }) + + describe('edge cases', () => { + it('returns undefined for unknown table', () => { + const tableName = 'UnknownTable' + const item = { userId: { S: 'user123' } } + const keyConfig = { KnownTable: new Set(['userId']) } + + const result = DynamoDb.calculatePutItemHash(tableName, item, keyConfig) + expect(result).to.be.undefined + }) + + it('returns undefined for empty primary key config', () => { + const tableName = 'UserTable' + const item = { userId: { S: 'user123' } } + + const result = DynamoDb.calculatePutItemHash(tableName, item, {}) + expect(result).to.be.undefined + }) + + it('returns undefined for invalid primary key config', () => { + const tableName = 'UserTable' + const item = { userId: { S: 'user123' } } + const invalidConfig = { UserTable: ['userId'] } // Array instead of Set + + const result = DynamoDb.calculatePutItemHash(tableName, item, invalidConfig) + expect(result).to.be.undefined + }) + + it('returns undefined when missing attributes in item', () => { + const tableName = 'UserTable' + const item = { someOtherField: { S: 'value' } } + const keyConfig = { UserTable: new Set(['userId']) } + + const actualHash = DynamoDb.calculatePutItemHash(tableName, item, keyConfig) + expect(actualHash).to.be.undefined + }) + + it('returns undefined for Set with more than 2 keys', () => { + const tableName = 'TestTable' + const item = { key1: { S: 'value1' }, key2: { S: 'value2' }, key3: { S: 'value3' } } + const keyConfig = { TestTable: new Set(['key1', 'key2', 'key3']) } + + const result = DynamoDb.calculatePutItemHash(tableName, item, keyConfig) + expect(result).to.be.undefined + }) + + it('returns undefined for empty keyConfig', () => { + const result = DynamoDb.calculatePutItemHash('TestTable', {}, {}) + expect(result).to.be.undefined + }) + + it('returns undefined for undefined keyConfig', () => { + const result = DynamoDb.calculatePutItemHash('TestTable', {}, undefined) + expect(result).to.be.undefined + }) + }) + }) + + describe('calculateHashWithKnownKeys', () => { + it('generates correct hash for single string key', () => { + const tableName = 'UserTable' + const keys = { userId: { S: 'user123' } } + const actualHash = DynamoDb.calculateHashWithKnownKeys(tableName, keys) + const expectedHash = generatePointerHash([tableName, 'userId', 'user123', '', '']) + expect(actualHash).to.equal(expectedHash) + }) + + it('generates correct hash for single number key', () => { + const tableName = 'OrderTable' + const keys = { orderId: { N: '98765' } } + const actualHash = DynamoDb.calculateHashWithKnownKeys(tableName, keys) + const expectedHash = generatePointerHash([tableName, 'orderId', '98765', '', '']) + expect(actualHash).to.equal(expectedHash) + }) + + it('generates correct hash for single binary key', () => { + const tableName = 'BinaryTable' + const binaryData = Buffer.from([1, 2, 3]) + const keys = { binaryId: { B: binaryData } } + const actualHash = DynamoDb.calculateHashWithKnownKeys(tableName, keys) + const expectedHash = generatePointerHash([tableName, 'binaryId', binaryData, '', '']) + expect(actualHash).to.equal(expectedHash) + }) + + it('generates correct hash for string-string key', () => { + const tableName = 'UserEmailTable' + const keys = { + userId: { S: 'user123' }, + email: { S: 'test@example.com' } + } + const actualHash = DynamoDb.calculateHashWithKnownKeys(tableName, keys) + const expectedHash = generatePointerHash([tableName, 'email', 'test@example.com', 'userId', 'user123']) + expect(actualHash).to.equal(expectedHash) + }) + + it('generates correct hash for string-number key', () => { + const tableName = 'UserActivityTable' + const keys = { + userId: { S: 'user123' }, + timestamp: { N: '1234567' } + } + const actualHash = DynamoDb.calculateHashWithKnownKeys(tableName, keys) + const expectedHash = generatePointerHash([tableName, 'timestamp', '1234567', 'userId', 'user123']) + expect(actualHash).to.equal(expectedHash) + }) + + it('generates correct hash for binary-binary key', () => { + const tableName = 'BinaryTable' + const binary1 = Buffer.from('abc') + const binary2 = Buffer.from('1ef230') + const keys = { + key1: { B: binary1 }, + key2: { B: binary2 } + } + const actualHash = DynamoDb.calculateHashWithKnownKeys(tableName, keys) + const expectedHash = generatePointerHash([tableName, 'key1', binary1, 'key2', binary2]) + expect(actualHash).to.equal(expectedHash) + }) + + it('generates unique hashes', () => { + const keys = { userId: { S: 'user123' } } + const hash1 = DynamoDb.calculateHashWithKnownKeys('Table1', keys) + const hash2 = DynamoDb.calculateHashWithKnownKeys('Table2', keys) + expect(hash1).to.not.equal(hash2) + }) + + describe('edge cases', () => { + it('handles empty keys object', () => { + const tableName = 'UserTable' + const hash = DynamoDb.calculateHashWithKnownKeys(tableName, {}) + expect(hash).to.be.undefined + }) + + it('handles invalid key types', () => { + const tableName = 'UserTable' + const keys = { userId: { INVALID: 'user123' } } + const hash = DynamoDb.calculateHashWithKnownKeys(tableName, keys) + expect(hash).to.be.undefined + }) + + it('handles null keys object', () => { + const hash = DynamoDb.calculateHashWithKnownKeys('TestTable', null) + expect(hash).to.be.undefined + }) + + it('handles undefined keys object', () => { + const hash = DynamoDb.calculateHashWithKnownKeys('TestTable', undefined) + expect(hash).to.be.undefined + }) + + it('handles mixed valid and invalid key types', () => { + const keys = { + validKey: { S: 'test' }, + invalidKey: { INVALID: 'value' } + } + const hash = DynamoDb.calculateHashWithKnownKeys('TestTable', keys) + expect(hash).to.be.undefined + }) + }) + }) + }) +}) diff --git a/packages/datadog-plugin-aws-sdk/test/util.spec.js b/packages/datadog-plugin-aws-sdk/test/util.spec.js new file mode 100644 index 00000000000..68bf57a7bfc --- /dev/null +++ b/packages/datadog-plugin-aws-sdk/test/util.spec.js @@ -0,0 +1,213 @@ +const { generatePointerHash, encodeValue, extractPrimaryKeys } = require('../src/util') + +describe('generatePointerHash', () => { + describe('should generate a valid hash for S3 object with', () => { + it('basic values', () => { + const hash = generatePointerHash(['some-bucket', 'some-key.data', 'ab12ef34']) + expect(hash).to.equal('e721375466d4116ab551213fdea08413') + }) + + it('non-ascii key', () => { + const hash = generatePointerHash(['some-bucket', 'some-key.你好', 'ab12ef34']) + expect(hash).to.equal('d1333a04b9928ab462b5c6cadfa401f4') + }) + + it('multipart-upload', () => { + const hash = generatePointerHash(['some-bucket', 'some-key.data', 'ab12ef34-5']) + expect(hash).to.equal('2b90dffc37ebc7bc610152c3dc72af9f') + }) + }) + + describe('should generate a valid hash for DynamoDB item with', () => { + it('one string primary key', () => { + const hash = generatePointerHash(['some-table', 'some-key', 'some-value', '', '']) + expect(hash).to.equal('7f1aee721472bcb48701d45c7c7f7821') + }) + + it('one buffered binary primary key', () => { + const hash = generatePointerHash(['some-table', 'some-key', Buffer.from('some-value'), '', '']) + expect(hash).to.equal('7f1aee721472bcb48701d45c7c7f7821') + }) + + it('one number primary key', () => { + const hash = generatePointerHash(['some-table', 'some-key', '123.456', '', '']) + expect(hash).to.equal('434a6dba3997ce4dbbadc98d87a0cc24') + }) + + it('one buffered number primary key', () => { + const hash = generatePointerHash(['some-table', 'some-key', Buffer.from('123.456'), '', '']) + expect(hash).to.equal('434a6dba3997ce4dbbadc98d87a0cc24') + }) + + it('string and number primary key', () => { + // sort primary keys lexicographically + const hash = generatePointerHash(['some-table', 'other-key', '123', 'some-key', 'some-value']) + expect(hash).to.equal('7aa1b80b0e49bd2078a5453399f4dd67') + }) + + it('buffered string and number primary key', () => { + const hash = generatePointerHash([ + 'some-table', + 'other-key', + Buffer.from('123'), + 'some-key', Buffer.from('some-value') + ]) + expect(hash).to.equal('7aa1b80b0e49bd2078a5453399f4dd67') + }) + }) +}) + +describe('encodeValue', () => { + describe('basic type handling', () => { + it('handles string (S) type correctly', () => { + const result = encodeValue({ S: 'hello world' }) + expect(Buffer.isBuffer(result)).to.be.true + expect(result).to.deep.equal(Buffer.from('hello world')) + }) + + it('handles number (N) as string type correctly', () => { + const result = encodeValue({ N: '123.45' }) + expect(Buffer.isBuffer(result)).to.be.true + expect(result).to.deep.equal(Buffer.from('123.45')) + }) + + it('handles number (N) as type string or number the same', () => { + const result1 = encodeValue({ N: 456.78 }) + const result2 = encodeValue({ N: '456.78' }) + expect(Buffer.isBuffer(result1)).to.be.true + expect(result1).to.deep.equal(result2) + }) + + it('handles binary (B) type correctly', () => { + const binaryData = Buffer.from([1, 2, 3]) + const result = encodeValue({ B: binaryData }) + expect(Buffer.isBuffer(result)).to.be.true + expect(result).to.deep.equal(binaryData) + }) + }) + + describe('edge cases', () => { + it('returns undefined for null input', () => { + const result = encodeValue(null) + expect(result).to.be.undefined + }) + + it('returns undefined for undefined input', () => { + const result = encodeValue(undefined) + expect(result).to.be.undefined + }) + + it('returns undefined for unsupported type', () => { + const result = encodeValue({ A: 'abc' }) + expect(result).to.be.undefined + }) + + it('returns undefined for malformed input', () => { + const result = encodeValue({}) + expect(result).to.be.undefined + }) + + it('handles empty string values', () => { + const result = encodeValue({ S: '' }) + expect(Buffer.isBuffer(result)).to.be.true + expect(result.length).to.equal(0) + }) + + it('handles empty buffer', () => { + const result = encodeValue({ B: Buffer.from([]) }) + expect(Buffer.isBuffer(result)).to.be.true + expect(result.length).to.equal(0) + }) + }) +}) + +describe('extractPrimaryKeys', () => { + describe('single key table', () => { + it('handles string key', () => { + const keySet = new Set(['userId']) + const item = { userId: { S: 'user123' } } + const result = extractPrimaryKeys(keySet, item) + expect(result).to.deep.equal(['userId', Buffer.from('user123'), '', '']) + }) + + it('handles number key', () => { + const keySet = new Set(['timestamp']) + const item = { timestamp: { N: '1234567' } } + const result = extractPrimaryKeys(keySet, item) + expect(result).to.deep.equal(['timestamp', Buffer.from('1234567'), '', '']) + }) + + it('handles binary key', () => { + const keySet = new Set(['binaryId']) + const binaryData = Buffer.from([1, 2, 3]) + const item = { binaryId: { B: binaryData } } + const result = extractPrimaryKeys(keySet, item) + expect(result).to.deep.equal(['binaryId', binaryData, '', '']) + }) + }) + + describe('double key table', () => { + it('handles and sorts string-string keys', () => { + const keySet = new Set(['userId', 'email']) + const item = { + userId: { S: 'user123' }, + email: { S: 'test@example.com' } + } + const result = extractPrimaryKeys(keySet, item) + expect(result).to.deep.equal(['email', Buffer.from('test@example.com'), 'userId', Buffer.from('user123')]) + }) + + it('handles and sorts string-number keys', () => { + const keySet = new Set(['timestamp', 'userId']) + const item = { + timestamp: { N: '1234567' }, + userId: { S: 'user123' } + } + const result = extractPrimaryKeys(keySet, item) + expect(result).to.deep.equal(['timestamp', Buffer.from('1234567'), 'userId', Buffer.from('user123')]) + }) + }) + + describe('edge cases', () => { + it('returns undefined when missing values', () => { + const keySet = new Set(['userId', 'timestamp']) + const item = { userId: { S: 'user123' } } // timestamp missing + const result = extractPrimaryKeys(keySet, item) + expect(result).to.be.undefined + }) + + it('returns undefined when invalid value types', () => { + const keySet = new Set(['userId', 'timestamp']) + const item = { + userId: { S: 'user123' }, + timestamp: { INVALID: '1234567' } + } + const result = extractPrimaryKeys(keySet, item) + expect(result).to.be.undefined + }) + + it('handles empty Set input', () => { + const result = extractPrimaryKeys(new Set([]), {}) + expect(result).to.be.undefined + }) + + it('returns undefined when null values in item', () => { + const keySet = new Set(['key1', 'key2']) + const item = { + key1: null, + key2: { S: 'value2' } + } + const result = extractPrimaryKeys(keySet, item) + expect(result).to.be.undefined + }) + + it('returns undefined when undefined values in item', () => { + const keySet = new Set(['key1', 'key2']) + const item = { + key2: { S: 'value2' } + } + const result = extractPrimaryKeys(keySet, item) + expect(result).to.be.undefined + }) + }) +}) diff --git a/packages/dd-trace/src/appsec/addresses.js b/packages/dd-trace/src/appsec/addresses.js index a492a5e454f..20290baf9c4 100644 --- a/packages/dd-trace/src/appsec/addresses.js +++ b/packages/dd-trace/src/appsec/addresses.js @@ -31,6 +31,7 @@ module.exports = { DB_STATEMENT: 'server.db.statement', DB_SYSTEM: 'server.db.system', + EXEC_COMMAND: 'server.sys.exec.cmd', SHELL_COMMAND: 'server.sys.shell.cmd', LOGIN_SUCCESS: 'server.business_logic.users.login.success', diff --git a/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js index f8937417e42..3741c12ef8f 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js @@ -11,6 +11,10 @@ class CodeInjectionAnalyzer extends InjectionAnalyzer { onConfigure () { this.addSub('datadog:eval:call', ({ script }) => this.analyze(script)) } + + _areRangesVulnerable () { + return true + } } module.exports = new CodeInjectionAnalyzer() diff --git a/packages/dd-trace/src/appsec/iast/analyzers/injection-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/injection-analyzer.js index cb4bc2866b0..f0d42bf95ae 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/injection-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/injection-analyzer.js @@ -1,12 +1,15 @@ 'use strict' const Analyzer = require('./vulnerability-analyzer') -const { isTainted, getRanges } = require('../taint-tracking/operations') +const { getRanges } = require('../taint-tracking/operations') +const { SQL_ROW_VALUE } = require('../taint-tracking/source-types') class InjectionAnalyzer extends Analyzer { _isVulnerable (value, iastContext) { - if (value) { - return isTainted(iastContext, value) + const ranges = value && getRanges(iastContext, value) + if (ranges?.length > 0) { + return this._areRangesVulnerable(ranges) } + return false } @@ -14,6 +17,10 @@ class InjectionAnalyzer extends Analyzer { const ranges = getRanges(iastContext, value) return { value, ranges } } + + _areRangesVulnerable (ranges) { + return ranges?.some(range => range.iinfo.type !== SQL_ROW_VALUE) + } } module.exports = InjectionAnalyzer diff --git a/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js index 4d302ece1b6..8f7ca5a39ed 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js @@ -82,6 +82,10 @@ class SqlInjectionAnalyzer extends InjectionAnalyzer { return knexDialect.toUpperCase() } } + + _areRangesVulnerable () { + return true + } } module.exports = new SqlInjectionAnalyzer() diff --git a/packages/dd-trace/src/appsec/iast/analyzers/template-injection-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/template-injection-analyzer.js index 1be35933223..8a5af919b2d 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/template-injection-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/template-injection-analyzer.js @@ -13,6 +13,10 @@ class TemplateInjectionAnalyzer extends InjectionAnalyzer { this.addSub('datadog:handlebars:register-partial:start', ({ partial }) => this.analyze(partial)) this.addSub('datadog:pug:compile:start', ({ source }) => this.analyze(source)) } + + _areRangesVulnerable () { + return true + } } module.exports = new TemplateInjectionAnalyzer() diff --git a/packages/dd-trace/src/appsec/iast/iast-plugin.js b/packages/dd-trace/src/appsec/iast/iast-plugin.js index 10dcde340c3..42dab0a4af1 100644 --- a/packages/dd-trace/src/appsec/iast/iast-plugin.js +++ b/packages/dd-trace/src/appsec/iast/iast-plugin.js @@ -98,7 +98,8 @@ class IastPlugin extends Plugin { } } - enable () { + enable (iastConfig) { + this.iastConfig = iastConfig this.configure(true) } 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 5c7109c4cda..b541629f3b7 100644 --- a/packages/dd-trace/src/appsec/iast/taint-tracking/index.js +++ b/packages/dd-trace/src/appsec/iast/taint-tracking/index.js @@ -18,10 +18,10 @@ module.exports = { enableTaintTracking (config, telemetryVerbosity) { enableRewriter(telemetryVerbosity) enableTaintOperations(telemetryVerbosity) - taintTrackingPlugin.enable() + taintTrackingPlugin.enable(config) - kafkaContextPlugin.enable() - kafkaConsumerPlugin.enable() + kafkaContextPlugin.enable(config) + kafkaConsumerPlugin.enable(config) setMaxTransactions(config.maxConcurrentRequests) }, 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 62fdd46d027..9e236666619 100644 --- a/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +++ b/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js @@ -12,7 +12,8 @@ const { HTTP_REQUEST_HEADER_NAME, HTTP_REQUEST_PARAMETER, HTTP_REQUEST_PATH_PARAM, - HTTP_REQUEST_URI + HTTP_REQUEST_URI, + SQL_ROW_VALUE } = require('./source-types') const { EXECUTED_SOURCE } = require('../telemetry/iast-metric') @@ -26,6 +27,16 @@ class TaintTrackingPlugin extends SourceIastPlugin { this._taintedURLs = new WeakMap() } + configure (config) { + super.configure(config) + + let rowsToTaint = this.iastConfig?.dbRowsToTaint + if (typeof rowsToTaint !== 'number') { + rowsToTaint = 1 + } + this._rowsToTaint = rowsToTaint + } + onConfigure () { const onRequestBody = ({ req }) => { const iastContext = getIastContext(storage.getStore()) @@ -73,6 +84,16 @@ class TaintTrackingPlugin extends SourceIastPlugin { ({ cookies }) => this._cookiesTaintTrackingHandler(cookies) ) + this.addSub( + { channelName: 'datadog:sequelize:query:finish', tag: SQL_ROW_VALUE }, + ({ result }) => this._taintDatabaseResult(result, 'sequelize') + ) + + this.addSub( + { channelName: 'apm:pg:query:finish', tag: SQL_ROW_VALUE }, + ({ result }) => this._taintDatabaseResult(result, 'pg') + ) + this.addSub( { channelName: 'datadog:express:process_params:start', tag: HTTP_REQUEST_PATH_PARAM }, ({ req }) => { @@ -184,6 +205,32 @@ class TaintTrackingPlugin extends SourceIastPlugin { this.taintHeaders(req.headers, iastContext) this.taintUrl(req, iastContext) } + + _taintDatabaseResult (result, dbOrigin, iastContext = getIastContext(storage.getStore()), name) { + if (!iastContext) return result + + if (this._rowsToTaint === 0) return result + + if (Array.isArray(result)) { + for (let i = 0; i < result.length && i < this._rowsToTaint; i++) { + const nextName = name ? `${name}.${i}` : '' + i + result[i] = this._taintDatabaseResult(result[i], dbOrigin, iastContext, nextName) + } + } else if (result && typeof result === 'object') { + if (dbOrigin === 'sequelize' && result.dataValues) { + result.dataValues = this._taintDatabaseResult(result.dataValues, dbOrigin, iastContext, name) + } else { + for (const key in result) { + const nextName = name ? `${name}.${key}` : key + result[key] = this._taintDatabaseResult(result[key], dbOrigin, iastContext, nextName) + } + } + } else if (typeof result === 'string') { + result = newTaintedString(iastContext, result, name, SQL_ROW_VALUE) + } + + return result + } } module.exports = new TaintTrackingPlugin() 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 f5c2ca2e8b0..f3ccf0505c3 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 @@ -11,5 +11,6 @@ module.exports = { HTTP_REQUEST_PATH_PARAM: 'http.request.path.parameter', HTTP_REQUEST_URI: 'http.request.uri', KAFKA_MESSAGE_KEY: 'kafka.message.key', - KAFKA_MESSAGE_VALUE: 'kafka.message.value' + KAFKA_MESSAGE_VALUE: 'kafka.message.value', + SQL_ROW_VALUE: 'sql.row.value' } diff --git a/packages/dd-trace/src/appsec/rasp/command_injection.js b/packages/dd-trace/src/appsec/rasp/command_injection.js index 8d6d977aace..62546e2b6a6 100644 --- a/packages/dd-trace/src/appsec/rasp/command_injection.js +++ b/packages/dd-trace/src/appsec/rasp/command_injection.js @@ -25,19 +25,26 @@ function disable () { } function analyzeCommandInjection ({ file, fileArgs, shell, abortController }) { - if (!file || !shell) return + if (!file) return const store = storage.getStore() const req = store?.req if (!req) return - const commandParams = fileArgs ? [file, ...fileArgs] : file - - const persistent = { - [addresses.SHELL_COMMAND]: commandParams + const persistent = {} + const raspRule = { type: RULE_TYPES.COMMAND_INJECTION } + const params = fileArgs ? [file, ...fileArgs] : file + + if (shell) { + persistent[addresses.SHELL_COMMAND] = params + raspRule.variant = 'shell' + } else { + const commandParams = Array.isArray(params) ? params : [params] + persistent[addresses.EXEC_COMMAND] = commandParams + raspRule.variant = 'exec' } - const result = waf.run({ persistent }, req, RULE_TYPES.COMMAND_INJECTION) + const result = waf.run({ persistent }, req, raspRule) const res = store?.res handleResult(result, req, res, abortController, config) diff --git a/packages/dd-trace/src/appsec/rasp/lfi.js b/packages/dd-trace/src/appsec/rasp/lfi.js index 1190734064d..657369ad0fd 100644 --- a/packages/dd-trace/src/appsec/rasp/lfi.js +++ b/packages/dd-trace/src/appsec/rasp/lfi.js @@ -58,7 +58,9 @@ function analyzeLfi (ctx) { [FS_OPERATION_PATH]: path } - const result = waf.run({ persistent }, req, RULE_TYPES.LFI) + const raspRule = { type: RULE_TYPES.LFI } + + const result = waf.run({ persistent }, req, raspRule) handleResult(result, req, res, ctx.abortController, config) }) } diff --git a/packages/dd-trace/src/appsec/rasp/sql_injection.js b/packages/dd-trace/src/appsec/rasp/sql_injection.js index d4a165d8615..157723258f7 100644 --- a/packages/dd-trace/src/appsec/rasp/sql_injection.js +++ b/packages/dd-trace/src/appsec/rasp/sql_injection.js @@ -72,7 +72,9 @@ function analyzeSqlInjection (query, dbSystem, abortController) { [addresses.DB_SYSTEM]: dbSystem } - const result = waf.run({ persistent }, req, RULE_TYPES.SQL_INJECTION) + const raspRule = { type: RULE_TYPES.SQL_INJECTION } + + const result = waf.run({ persistent }, req, raspRule) handleResult(result, req, res, abortController, config) } diff --git a/packages/dd-trace/src/appsec/rasp/ssrf.js b/packages/dd-trace/src/appsec/rasp/ssrf.js index 38a3c150d74..7d429d74549 100644 --- a/packages/dd-trace/src/appsec/rasp/ssrf.js +++ b/packages/dd-trace/src/appsec/rasp/ssrf.js @@ -29,7 +29,9 @@ function analyzeSsrf (ctx) { [addresses.HTTP_OUTGOING_URL]: outgoingUrl } - const result = waf.run({ persistent }, req, RULE_TYPES.SSRF) + const raspRule = { type: RULE_TYPES.SSRF } + + const result = waf.run({ persistent }, req, raspRule) const res = store?.res handleResult(result, req, res, ctx.abortController, config) diff --git a/packages/dd-trace/src/appsec/remote_config/capabilities.js b/packages/dd-trace/src/appsec/remote_config/capabilities.js index 16034f5f9ee..5057d38de43 100644 --- a/packages/dd-trace/src/appsec/remote_config/capabilities.js +++ b/packages/dd-trace/src/appsec/remote_config/capabilities.js @@ -25,5 +25,6 @@ module.exports = { ASM_AUTO_USER_INSTRUM_MODE: 1n << 31n, ASM_ENDPOINT_FINGERPRINT: 1n << 32n, ASM_NETWORK_FINGERPRINT: 1n << 34n, - ASM_HEADER_FINGERPRINT: 1n << 35n + ASM_HEADER_FINGERPRINT: 1n << 35n, + ASM_RASP_CMDI: 1n << 37n } diff --git a/packages/dd-trace/src/appsec/remote_config/index.js b/packages/dd-trace/src/appsec/remote_config/index.js index 7884175abb0..6bebe40e142 100644 --- a/packages/dd-trace/src/appsec/remote_config/index.js +++ b/packages/dd-trace/src/appsec/remote_config/index.js @@ -101,6 +101,7 @@ function enableWafUpdate (appsecConfig) { rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SSRF, true) rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_LFI, true) rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SHI, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_CMDI, true) } // TODO: delete noop handlers and kPreUpdate and replace with batched handlers @@ -133,6 +134,7 @@ function disableWafUpdate () { rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SSRF, false) rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_LFI, false) rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SHI, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_CMDI, false) rc.removeProductHandler('ASM_DATA') rc.removeProductHandler('ASM_DD') diff --git a/packages/dd-trace/src/appsec/remote_config/manager.js b/packages/dd-trace/src/appsec/remote_config/manager.js index 75c72690503..19ed709b27f 100644 --- a/packages/dd-trace/src/appsec/remote_config/manager.js +++ b/packages/dd-trace/src/appsec/remote_config/manager.js @@ -9,6 +9,7 @@ const log = require('../../log') const { getExtraServices } = require('../../service-naming/extra-services') const { UNACKNOWLEDGED, ACKNOWLEDGED, ERROR } = require('./apply_states') const Scheduler = require('./scheduler') +const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../../plugins/util/tags') const clientId = uuid() @@ -33,6 +34,14 @@ class RemoteConfigManager extends EventEmitter { port: config.port })) + const tags = config.repositoryUrl + ? { + ...config.tags, + [GIT_REPOSITORY_URL]: config.repositoryUrl, + [GIT_COMMIT_SHA]: config.commitSHA + } + : config.tags + this._handlers = new Map() const appliedConfigs = this.appliedConfigs = new Map() @@ -67,7 +76,8 @@ class RemoteConfigManager extends EventEmitter { service: config.service, env: config.env, app_version: config.version, - extra_services: [] + extra_services: [], + tags: Object.entries(tags).map((pair) => pair.join(':')) }, capabilities: DEFAULT_CAPABILITY // updated by `updateCapabilities()` }, diff --git a/packages/dd-trace/src/appsec/reporter.js b/packages/dd-trace/src/appsec/reporter.js index 57519e5bc79..c2f9bac6cbc 100644 --- a/packages/dd-trace/src/appsec/reporter.js +++ b/packages/dd-trace/src/appsec/reporter.js @@ -101,7 +101,7 @@ function reportWafInit (wafVersion, rulesVersion, diagnosticsRules = {}) { incrementWafInitMetric(wafVersion, rulesVersion) } -function reportMetrics (metrics, raspRuleType) { +function reportMetrics (metrics, raspRule) { const store = storage.getStore() const rootSpan = store?.req && web.root(store.req) if (!rootSpan) return @@ -109,8 +109,8 @@ function reportMetrics (metrics, raspRuleType) { if (metrics.rulesVersion) { rootSpan.setTag('_dd.appsec.event_rules.version', metrics.rulesVersion) } - if (raspRuleType) { - updateRaspRequestsMetricTags(metrics, store.req, raspRuleType) + if (raspRule) { + updateRaspRequestsMetricTags(metrics, store.req, raspRule) } else { updateWafRequestsMetricTags(metrics, store.req) } diff --git a/packages/dd-trace/src/appsec/telemetry.js b/packages/dd-trace/src/appsec/telemetry.js index 8e9a2518f80..08f435b9c0e 100644 --- a/packages/dd-trace/src/appsec/telemetry.js +++ b/packages/dd-trace/src/appsec/telemetry.js @@ -79,7 +79,7 @@ function getOrCreateMetricTags (store, versionsTags) { return metricTags } -function updateRaspRequestsMetricTags (metrics, req, raspRuleType) { +function updateRaspRequestsMetricTags (metrics, req, raspRule) { if (!req) return const store = getStore(req) @@ -89,7 +89,12 @@ function updateRaspRequestsMetricTags (metrics, req, raspRuleType) { if (!enabled) return - const tags = { rule_type: raspRuleType, waf_version: metrics.wafVersion } + const tags = { rule_type: raspRule.type, waf_version: metrics.wafVersion } + + if (raspRule.variant) { + tags.rule_variant = raspRule.variant + } + appsecMetrics.count('rasp.rule.eval', tags).inc(1) if (metrics.wafTimeout) { diff --git a/packages/dd-trace/src/appsec/waf/index.js b/packages/dd-trace/src/appsec/waf/index.js index 3b2bc9e2a13..a14a5313a92 100644 --- a/packages/dd-trace/src/appsec/waf/index.js +++ b/packages/dd-trace/src/appsec/waf/index.js @@ -46,7 +46,7 @@ function update (newRules) { } } -function run (data, req, raspRuleType) { +function run (data, req, raspRule) { if (!req) { const store = storage.getStore() if (!store || !store.req) { @@ -59,7 +59,7 @@ function run (data, req, raspRuleType) { const wafContext = waf.wafManager.getWAFContext(req) - return wafContext.run(data, raspRuleType) + return wafContext.run(data, raspRule) } function disposeContext (req) { diff --git a/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js b/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js index 6a90b8f89bb..54dbd16e1be 100644 --- a/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +++ b/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js @@ -21,7 +21,7 @@ class WAFContextWrapper { this.knownAddresses = knownAddresses } - run ({ persistent, ephemeral }, raspRuleType) { + run ({ persistent, ephemeral }, raspRule) { if (this.ddwafContext.disposed) { log.warn('[ASM] Calling run on a disposed context') return @@ -87,7 +87,7 @@ class WAFContextWrapper { blockTriggered, wafVersion: this.wafVersion, wafTimeout: result.timeout - }, raspRuleType) + }, raspRule) if (ruleTriggered) { Reporter.reportAttack(JSON.stringify(result.events)) diff --git a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js index ec6e2a1fd75..8cf52e709f6 100644 --- a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +++ b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js @@ -1,7 +1,7 @@ 'use strict' const { join } = require('path') -const { Worker } = require('worker_threads') +const { Worker, threadId: parentThreadId } = require('worker_threads') const { randomUUID } = require('crypto') const log = require('../../log') @@ -46,29 +46,47 @@ class TestVisDynamicInstrumentation { return this._readyPromise } - start () { + start (config) { if (this.worker) return const { NODE_OPTIONS, ...envWithoutNodeOptions } = process.env log.debug('Starting Test Visibility - Dynamic Instrumentation client...') + const rcChannel = new MessageChannel() // mock channel + const configChannel = new MessageChannel() // mock channel + this.worker = new Worker( join(__dirname, 'worker', 'index.js'), { execArgv: [], env: envWithoutNodeOptions, workerData: { + config: config.serialize(), + parentThreadId, + rcPort: rcChannel.port1, + configPort: configChannel.port1, breakpointSetChannel: this.breakpointSetChannel.port1, breakpointHitChannel: this.breakpointHitChannel.port1 }, - transferList: [this.breakpointSetChannel.port1, this.breakpointHitChannel.port1] + transferList: [ + rcChannel.port1, + configChannel.port1, + this.breakpointSetChannel.port1, + this.breakpointHitChannel.port1 + ] } ) this.worker.on('online', () => { log.debug('Test Visibility - Dynamic Instrumentation client is ready') this._onReady() }) + this.worker.on('error', (err) => { + log.error('Test Visibility - Dynamic Instrumentation worker error', err) + }) + this.worker.on('messageerror', (err) => { + log.error('Test Visibility - Dynamic Instrumentation worker messageerror', err) + }) // Allow the parent to exit even if the worker is still running this.worker.unref() diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index beb15ebc010..09ce9d5fd66 100644 --- a/packages/dd-trace/src/config.js +++ b/packages/dd-trace/src/config.js @@ -473,6 +473,8 @@ class Config { this._setValue(defaults, 'dogstatsd.port', '8125') this._setValue(defaults, 'dsmEnabled', false) this._setValue(defaults, 'dynamicInstrumentationEnabled', false) + this._setValue(defaults, 'dynamicInstrumentationRedactedIdentifiers', []) + this._setValue(defaults, 'dynamicInstrumentationRedactionExcludedIdentifiers', []) this._setValue(defaults, 'env', undefined) this._setValue(defaults, 'experimental.enableGetRumData', false) this._setValue(defaults, 'experimental.exporter', undefined) @@ -485,6 +487,7 @@ class Config { this._setValue(defaults, 'headerTags', []) this._setValue(defaults, 'hostname', '127.0.0.1') this._setValue(defaults, 'iast.cookieFilterPattern', '.{32,}') + this._setValue(defaults, 'iast.dbRowsToTaint', 1) this._setValue(defaults, 'iast.deduplicationEnabled', true) this._setValue(defaults, 'iast.enabled', false) this._setValue(defaults, 'iast.maxConcurrentRequests', 2) @@ -565,6 +568,7 @@ class Config { this._setValue(defaults, 'url', undefined) this._setValue(defaults, 'version', pkg.version) this._setValue(defaults, 'instrumentation_config_id', undefined) + this._setValue(defaults, 'aws.dynamoDb.tablePrimaryKeys', undefined) } _applyEnvironment () { @@ -589,6 +593,7 @@ class Config { DD_APPSEC_RASP_ENABLED, DD_APPSEC_TRACE_RATE_LIMIT, DD_APPSEC_WAF_TIMEOUT, + DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS, DD_CRASHTRACKING_ENABLED, DD_CODE_ORIGIN_FOR_SPANS_ENABLED, DD_DATA_STREAMS_ENABLED, @@ -597,6 +602,8 @@ class Config { DD_DOGSTATSD_HOST, DD_DOGSTATSD_PORT, DD_DYNAMIC_INSTRUMENTATION_ENABLED, + DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS, + DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS, DD_ENV, DD_EXPERIMENTAL_API_SECURITY_ENABLED, DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED, @@ -605,6 +612,7 @@ class Config { DD_GRPC_SERVER_ERROR_STATUSES, JEST_WORKER_ID, DD_IAST_COOKIE_FILTER_PATTERN, + DD_IAST_DB_ROWS_TO_TAINT, DD_IAST_DEDUPLICATION_ENABLED, DD_IAST_ENABLED, DD_IAST_MAX_CONCURRENT_REQUESTS, @@ -743,6 +751,12 @@ class Config { this._setString(env, 'dogstatsd.port', DD_DOGSTATSD_PORT) this._setBoolean(env, 'dsmEnabled', DD_DATA_STREAMS_ENABLED) this._setBoolean(env, 'dynamicInstrumentationEnabled', DD_DYNAMIC_INSTRUMENTATION_ENABLED) + this._setArray(env, 'dynamicInstrumentationRedactedIdentifiers', DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS) + this._setArray( + env, + 'dynamicInstrumentationRedactionExcludedIdentifiers', + DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS + ) this._setString(env, 'env', DD_ENV || tags.env) this._setBoolean(env, 'traceEnabled', DD_TRACE_ENABLED) this._setBoolean(env, 'experimental.enableGetRumData', DD_TRACE_EXPERIMENTAL_GET_RUM_DATA_ENABLED) @@ -757,6 +771,7 @@ class Config { this._setArray(env, 'headerTags', DD_TRACE_HEADER_TAGS) this._setString(env, 'hostname', coalesce(DD_AGENT_HOST, DD_TRACE_AGENT_HOSTNAME)) this._setString(env, 'iast.cookieFilterPattern', DD_IAST_COOKIE_FILTER_PATTERN) + this._setValue(env, 'iast.dbRowsToTaint', maybeInt(DD_IAST_DB_ROWS_TO_TAINT)) this._setBoolean(env, 'iast.deduplicationEnabled', DD_IAST_DEDUPLICATION_ENABLED) this._setBoolean(env, 'iast.enabled', DD_IAST_ENABLED) this._setValue(env, 'iast.maxConcurrentRequests', maybeInt(DD_IAST_MAX_CONCURRENT_REQUESTS)) @@ -876,6 +891,7 @@ class Config { this._setBoolean(env, 'tracing', DD_TRACING_ENABLED) this._setString(env, 'version', DD_VERSION || tags.version) this._setBoolean(env, 'inferredProxyServicesEnabled', DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED) + this._setString(env, 'aws.dynamoDb.tablePrimaryKeys', DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS) } _applyOptions (options) { @@ -921,6 +937,16 @@ class Config { } this._setBoolean(opts, 'dsmEnabled', options.dsmEnabled) this._setBoolean(opts, 'dynamicInstrumentationEnabled', options.experimental?.dynamicInstrumentationEnabled) + this._setArray( + opts, + 'dynamicInstrumentationRedactedIdentifiers', + options.experimental?.dynamicInstrumentationRedactedIdentifiers + ) + this._setArray( + opts, + 'dynamicInstrumentationRedactionExcludedIdentifiers', + options.experimental?.dynamicInstrumentationRedactionExcludedIdentifiers + ) this._setString(opts, 'env', options.env || tags.env) this._setBoolean(opts, 'experimental.enableGetRumData', options.experimental?.enableGetRumData) this._setString(opts, 'experimental.exporter', options.experimental?.exporter) @@ -932,6 +958,7 @@ class Config { this._setArray(opts, 'headerTags', options.headerTags) this._setString(opts, 'hostname', options.hostname) this._setString(opts, 'iast.cookieFilterPattern', options.iast?.cookieFilterPattern) + this._setValue(opts, 'iast.dbRowsToTaint', maybeInt(options.iast?.dbRowsToTaint)) this._setBoolean(opts, 'iast.deduplicationEnabled', options.iast && options.iast.deduplicationEnabled) this._setBoolean(opts, 'iast.enabled', options.iast && (options.iast === true || options.iast.enabled === true)) @@ -1305,6 +1332,22 @@ class Config { this.sampler.sampleRate = this.sampleRate updateConfig(changes, this) } + + // TODO: Refactor the Config class so it never produces any config objects that are incompatible with MessageChannel + /** + * Serializes the config object so it can be passed over a Worker Thread MessageChannel. + * @returns {Object} The serialized config object. + */ + serialize () { + // URL objects cannot be serialized over the MessageChannel, so we need to convert them to strings first + if (this.url instanceof URL) { + const config = { ...this } + config.url = this.url.toString() + return config + } + + return this + } } function maybeInt (number) { diff --git a/packages/dd-trace/src/constants.js b/packages/dd-trace/src/constants.js index 4e7faf669d4..3c93480df9f 100644 --- a/packages/dd-trace/src/constants.js +++ b/packages/dd-trace/src/constants.js @@ -47,6 +47,7 @@ module.exports = { SCHEMA_NAME: 'schema.name', GRPC_CLIENT_ERROR_STATUSES: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], GRPC_SERVER_ERROR_STATUSES: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + DYNAMODB_PTR_KIND: 'aws.dynamodb.item', S3_PTR_KIND: 'aws.s3.object', SPAN_POINTER_DIRECTION: Object.freeze({ UPSTREAM: 'u', diff --git a/packages/dd-trace/src/debugger/devtools_client/config.js b/packages/dd-trace/src/debugger/devtools_client/config.js index 7783bc84d75..4880bbe5fdb 100644 --- a/packages/dd-trace/src/debugger/devtools_client/config.js +++ b/packages/dd-trace/src/debugger/devtools_client/config.js @@ -5,11 +5,14 @@ const { format } = require('node:url') const log = require('../../log') const config = module.exports = { + dynamicInstrumentationRedactedIdentifiers: parentConfig.dynamicInstrumentationRedactedIdentifiers, + dynamicInstrumentationRedactionExcludedIdentifiers: parentConfig.dynamicInstrumentationRedactionExcludedIdentifiers, runtimeId: parentConfig.tags['runtime-id'], service: parentConfig.service, commitSHA: parentConfig.commitSHA, repositoryUrl: parentConfig.repositoryUrl, - parentThreadId + parentThreadId, + maxTotalPayloadSize: 5 * 1024 * 1024 // 5MB } updateUrl(parentConfig) diff --git a/packages/dd-trace/src/debugger/devtools_client/index.js b/packages/dd-trace/src/debugger/devtools_client/index.js index 7ca828786ac..89c96db18c6 100644 --- a/packages/dd-trace/src/debugger/devtools_client/index.js +++ b/packages/dd-trace/src/debugger/devtools_client/index.js @@ -13,6 +13,12 @@ const { version } = require('../../../../../package.json') require('./remote_config') +// Expression to run on a call frame of the paused thread to get its active trace and span id. +const expression = ` + const context = global.require('dd-trace').scope().active()?.context(); + ({ trace_id: context?.toTraceId(), span_id: context?.toSpanId() }) +` + // There doesn't seem to be an official standard for the content of these fields, so we're just populating them with // something that should be useful to a Node.js developer. const threadId = parentThreadId === 0 ? `pid:${process.pid}` : `pid:${process.pid};tid:${parentThreadId}` @@ -59,6 +65,7 @@ session.on('Debugger.paused', async ({ params }) => { } const timestamp = Date.now() + const dd = await getDD(params.callFrames[0].callFrameId) let processLocalState if (captureSnapshotForProbe !== null) { @@ -122,9 +129,8 @@ session.on('Debugger.paused', async ({ params }) => { } // TODO: Process template (DEBUG-2628) - send(probe.template, logger, snapshot, (err) => { - if (err) log.error('Debugger error', err) - else ackEmitting(probe) + send(probe.template, logger, dd, snapshot, () => { + ackEmitting(probe) }) } }) @@ -132,3 +138,23 @@ session.on('Debugger.paused', async ({ params }) => { function highestOrUndefined (num, max) { return num === undefined ? max : Math.max(num, max ?? 0) } + +async function getDD (callFrameId) { + // TODO: Consider if an `objectGroup` should be used, so it can be explicitly released using + // `Runtime.releaseObjectGroup` + const { result } = await session.post('Debugger.evaluateOnCallFrame', { + callFrameId, + expression, + returnByValue: true, + includeCommandLineAPI: true + }) + + if (result?.value?.trace_id === undefined) { + if (result?.subtype === 'error') { + log.error('[debugger:devtools_client] Error getting trace/span id:', result.description) + } + return + } + + return result.value +} diff --git a/packages/dd-trace/src/debugger/devtools_client/json-buffer.js b/packages/dd-trace/src/debugger/devtools_client/json-buffer.js new file mode 100644 index 00000000000..5010aafac3d --- /dev/null +++ b/packages/dd-trace/src/debugger/devtools_client/json-buffer.js @@ -0,0 +1,36 @@ +'use strict' + +class JSONBuffer { + constructor ({ size, timeout, onFlush }) { + this._maxSize = size + this._timeout = timeout + this._onFlush = onFlush + this._reset() + } + + _reset () { + clearTimeout(this._timer) + this._timer = null + this._partialJson = null + } + + _flush () { + const json = `${this._partialJson}]` + this._reset() + this._onFlush(json) + } + + write (str, size = Buffer.byteLength(str)) { + if (this._timer === null) { + this._partialJson = `[${str}` + this._timer = setTimeout(() => this._flush(), this._timeout) + } else if (Buffer.byteLength(this._partialJson) + size + 2 > this._maxSize) { + this._flush() + this.write(str, size) + } else { + this._partialJson += `,${str}` + } + } +} + +module.exports = JSONBuffer diff --git a/packages/dd-trace/src/debugger/devtools_client/send.js b/packages/dd-trace/src/debugger/devtools_client/send.js index f2ba5befd46..12d9b8cad84 100644 --- a/packages/dd-trace/src/debugger/devtools_client/send.js +++ b/packages/dd-trace/src/debugger/devtools_client/send.js @@ -4,44 +4,49 @@ const { hostname: getHostname } = require('os') const { stringify } = require('querystring') const config = require('./config') +const JSONBuffer = require('./json-buffer') const request = require('../../exporters/common/request') const { GIT_COMMIT_SHA, GIT_REPOSITORY_URL } = require('../../plugins/util/tags') +const log = require('../../log') +const { version } = require('../../../../../package.json') module.exports = send -const MAX_PAYLOAD_SIZE = 1024 * 1024 // 1MB +const MAX_LOG_PAYLOAD_SIZE = 1024 * 1024 // 1MB const ddsource = 'dd_debugger' const hostname = getHostname() const service = config.service const ddtags = [ + ['env', process.env.DD_ENV], + ['version', process.env.DD_VERSION], + ['debugger_version', version], + ['host_name', hostname], [GIT_COMMIT_SHA, config.commitSHA], [GIT_REPOSITORY_URL, config.repositoryUrl] ].map((pair) => pair.join(':')).join(',') const path = `/debugger/v1/input?${stringify({ ddtags })}` -function send (message, logger, snapshot, cb) { - const opts = { - method: 'POST', - url: config.url, - path, - headers: { 'Content-Type': 'application/json; charset=utf-8' } - } +let callbacks = [] +const jsonBuffer = new JSONBuffer({ size: config.maxTotalPayloadSize, timeout: 1000, onFlush }) +function send (message, logger, dd, snapshot, cb) { const payload = { ddsource, hostname, service, message, logger, + dd, 'debugger.snapshot': snapshot } let json = JSON.stringify(payload) + let size = Buffer.byteLength(json) - if (Buffer.byteLength(json) > MAX_PAYLOAD_SIZE) { + if (size > MAX_LOG_PAYLOAD_SIZE) { // TODO: This is a very crude way to handle large payloads. Proper pruning will be implemented later (DEBUG-2624) const line = Object.values(payload['debugger.snapshot'].captures.lines)[0] line.locals = { @@ -49,7 +54,26 @@ function send (message, logger, snapshot, cb) { size: Object.keys(line.locals).length } json = JSON.stringify(payload) + size = Buffer.byteLength(json) + } + + jsonBuffer.write(json, size) + callbacks.push(cb) +} + +function onFlush (payload) { + const opts = { + method: 'POST', + url: config.url, + path, + headers: { 'Content-Type': 'application/json; charset=utf-8' } } - request(json, opts, cb) + const _callbacks = callbacks + callbacks = [] + + request(payload, opts, (err) => { + if (err) log.error('Could not send debugger payload', err) + else _callbacks.forEach(cb => cb()) + }) } diff --git a/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js b/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js index ea52939ab0e..a7b14987987 100644 --- a/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +++ b/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js @@ -1,6 +1,7 @@ 'use strict' const { collectionSizeSym, fieldCountSym } = require('./symbols') +const { normalizeName, REDACTED_IDENTIFIERS } = require('./redaction') module.exports = { processRawState: processProperties @@ -24,7 +25,14 @@ function processProperties (props, maxLength) { return result } +// TODO: Improve performance of redaction algorithm. +// This algorithm is probably slower than if we embedded the redaction logic inside the functions below. +// That way we didn't have to traverse objects that will just be redacted anyway. function getPropertyValue (prop, maxLength) { + return redact(prop, getPropertyValueRaw(prop, maxLength)) +} + +function getPropertyValueRaw (prop, maxLength) { // Special case for getters and setters which does not have a value property if ('get' in prop) { const hasGet = prop.get.type !== 'undefined' @@ -185,8 +193,11 @@ function toMap (type, pairs, maxLength) { // `pair.value` is a special wrapper-object with subtype `internal#entry`. This can be skipped and we can go // directly to its children, of which there will always be exactly two, the first containing the key, and the // second containing the value of this entry of the Map. + const shouldRedact = shouldRedactMapValue(pair.value.properties[0]) const key = getPropertyValue(pair.value.properties[0], maxLength) - const val = getPropertyValue(pair.value.properties[1], maxLength) + const val = shouldRedact + ? notCapturedRedacted(pair.value.properties[1].value.type) + : getPropertyValue(pair.value.properties[1], maxLength) result.entries[i++] = [key, val] } @@ -240,6 +251,25 @@ function arrayBufferToString (bytes, size) { return buf.toString() } +function redact (prop, obj) { + const name = getNormalizedNameFromProp(prop) + return REDACTED_IDENTIFIERS.has(name) ? notCapturedRedacted(obj.type) : obj +} + +function shouldRedactMapValue (key) { + const isSymbol = key.value.type === 'symbol' + if (!isSymbol && key.value.type !== 'string') return false // WeakMaps uses objects as keys + const name = normalizeName( + isSymbol ? key.value.description : key.value.value, + isSymbol + ) + return REDACTED_IDENTIFIERS.has(name) +} + +function getNormalizedNameFromProp (prop) { + return normalizeName(prop.name, 'symbol' in prop) +} + function setNotCaptureReasonOnCollection (result, collection) { if (collectionSizeSym in collection) { result.notCapturedReason = 'collectionSize' @@ -250,3 +280,7 @@ function setNotCaptureReasonOnCollection (result, collection) { function notCapturedDepth (type) { return { type, notCapturedReason: 'depth' } } + +function notCapturedRedacted (type) { + return { type, notCapturedReason: 'redactedIdent' } +} diff --git a/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js b/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js new file mode 100644 index 00000000000..5ccb58f4053 --- /dev/null +++ b/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js @@ -0,0 +1,116 @@ +'use strict' + +const config = require('../config') + +const excludedIdentifiers = config.dynamicInstrumentationRedactionExcludedIdentifiers.map((name) => normalizeName(name)) + +const REDACTED_IDENTIFIERS = new Set( + [ + '2fa', + '_csrf', + '_csrf_token', + '_session', + '_xsrf', + 'access_token', + 'address', + 'aiohttp_session', + 'api_key', + 'apisecret', + 'apisignature', + 'applicationkey', + 'appkey', + 'auth', + 'authtoken', + 'authorization', + 'cc_number', + 'certificatepin', + 'cipher', + 'client_secret', + 'clientid', + 'config', + 'connect.sid', + 'connectionstring', + 'cookie', + 'credentials', + 'creditcard', + 'csrf', + 'csrf_token', + 'cvv', + 'databaseurl', + 'db_url', + 'email', + 'encryption_key', + 'encryptionkeyid', + 'geo_location', + 'gpg_key', + 'ip_address', + 'jti', + 'jwt', + 'license_key', + 'masterkey', + 'mysql_pwd', + 'nonce', + 'oauth', + 'oauthtoken', + 'otp', + 'passhash', + 'passwd', + 'password', + 'passwordb', + 'pem_file', + 'pgp_key', + 'PHPSESSID', + 'phonenumber', + 'pin', + 'pincode', + 'pkcs8', + 'private_key', + 'publickey', + 'pwd', + 'recaptcha_key', + 'refresh_token', + 'remote_addr', + 'routingnumber', + 'salt', + 'secret', + 'secretKey', + 'securitycode', + 'security_answer', + 'security_question', + 'serviceaccountcredentials', + 'session', + 'sessionid', + 'sessionkey', + 'set_cookie', + 'signature', + 'signaturekey', + 'ssh_key', + 'ssn', + 'symfony', + 'token', + 'transactionid', + 'twilio_token', + 'user_session', + 'uuid', + 'voterid', + 'x-auth-token', + 'x_api_key', + 'x_csrftoken', + 'x_forwarded_for', + 'x_real_ip', + 'XSRF-TOKEN', + ...config.dynamicInstrumentationRedactedIdentifiers + ] + .map((name) => normalizeName(name)) + .filter((name) => excludedIdentifiers.includes(name) === false) +) + +function normalizeName (name, isSymbol) { + if (isSymbol) name = name.slice(7, -1) // Remove `Symbol(` and `)` + return name.toLowerCase().replace(/[-_@$.]/g, '') +} + +module.exports = { + REDACTED_IDENTIFIERS, + normalizeName +} diff --git a/packages/dd-trace/src/debugger/devtools_client/status.js b/packages/dd-trace/src/debugger/devtools_client/status.js index b228d7e50b7..7a7db799e53 100644 --- a/packages/dd-trace/src/debugger/devtools_client/status.js +++ b/packages/dd-trace/src/debugger/devtools_client/status.js @@ -2,6 +2,7 @@ const LRUCache = require('lru-cache') const config = require('./config') +const JSONBuffer = require('./json-buffer') const request = require('../../exporters/common/request') const FormData = require('../../exporters/common/form-data') const log = require('../../log') @@ -25,6 +26,8 @@ const cache = new LRUCache({ ttlAutopurge: true }) +const jsonBuffer = new JSONBuffer({ size: config.maxTotalPayloadSize, timeout: 1000, onFlush }) + const STATUSES = { RECEIVED: 'RECEIVED', INSTALLED: 'INSTALLED', @@ -71,11 +74,15 @@ function ackError (err, { id: probeId, version }) { } function send (payload) { + jsonBuffer.write(JSON.stringify(payload)) +} + +function onFlush (payload) { const form = new FormData() form.append( 'event', - JSON.stringify(payload), + payload, { filename: 'event.json', contentType: 'application/json; charset=utf-8' } ) @@ -87,7 +94,7 @@ function send (payload) { } request(form, options, (err) => { - if (err) log.error('[debugger:devtools_client] Error sending debugger payload', err) + if (err) log.error('[debugger:devtools_client] Error sending probe payload', err) }) } diff --git a/packages/dd-trace/src/debugger/index.js b/packages/dd-trace/src/debugger/index.js index fee514f32f1..a1a94d9e321 100644 --- a/packages/dd-trace/src/debugger/index.js +++ b/packages/dd-trace/src/debugger/index.js @@ -48,7 +48,7 @@ function start (config, rc) { execArgv: [], // Avoid worker thread inheriting the `-r` command line argument env, // Avoid worker thread inheriting the `NODE_OPTIONS` environment variable (in case it contains `-r`) workerData: { - config: serializableConfig(config), + config: config.serialize(), parentThreadId, rcPort: rcChannel.port1, configPort: configChannel.port1 @@ -88,16 +88,5 @@ function start (config, rc) { function configure (config) { if (configChannel === null) return - configChannel.port2.postMessage(serializableConfig(config)) -} - -// TODO: Refactor the Config class so it never produces any config objects that are incompatible with MessageChannel -function serializableConfig (config) { - // URL objects cannot be serialized over the MessageChannel, so we need to convert them to strings first - if (config.url instanceof URL) { - config = { ...config } - config.url = config.url.toString() - } - - return config + configChannel.port2.postMessage(config.serialize()) } diff --git a/packages/dd-trace/src/llmobs/sdk.js b/packages/dd-trace/src/llmobs/sdk.js index 91fe1e8f70a..2a6d548d656 100644 --- a/packages/dd-trace/src/llmobs/sdk.js +++ b/packages/dd-trace/src/llmobs/sdk.js @@ -1,12 +1,12 @@ 'use strict' -const { SPAN_KIND, OUTPUT_VALUE } = require('./constants/tags') +const { SPAN_KIND, OUTPUT_VALUE, INPUT_VALUE } = require('./constants/tags') const { getFunctionArguments, validateKind } = require('./util') -const { isTrue } = require('../util') +const { isTrue, isError } = require('../util') const { storage } = require('./storage') @@ -134,29 +134,63 @@ class LLMObs extends NoopLLMObs { function wrapped () { const span = llmobs._tracer.scope().active() - - const result = llmobs._activate(span, { kind, options: llmobsOptions }, () => { - if (!['llm', 'embedding'].includes(kind)) { - llmobs.annotate(span, { inputData: getFunctionArguments(fn, arguments) }) + const fnArgs = arguments + + const lastArgId = fnArgs.length - 1 + const cb = fnArgs[lastArgId] + const hasCallback = typeof cb === 'function' + + if (hasCallback) { + const scopeBoundCb = llmobs._bind(cb) + fnArgs[lastArgId] = function () { + // it is standard practice to follow the callback signature (err, result) + // however, we try to parse the arguments to determine if the first argument is an error + // if it is not, and is not undefined, we will use that for the output value + const maybeError = arguments[0] + const maybeResult = arguments[1] + + llmobs._autoAnnotate( + span, + kind, + getFunctionArguments(fn, fnArgs), + isError(maybeError) || maybeError == null ? maybeResult : maybeError + ) + + return scopeBoundCb.apply(this, arguments) } + } - return fn.apply(this, arguments) - }) + try { + const result = llmobs._activate(span, { kind, options: llmobsOptions }, () => fn.apply(this, fnArgs)) + + if (result && typeof result.then === 'function') { + return result.then( + value => { + if (!hasCallback) { + llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs), value) + } + return value + }, + err => { + llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs)) + throw err + } + ) + } - if (result && typeof result.then === 'function') { - return result.then(value => { - if (value && !['llm', 'retrieval'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[OUTPUT_VALUE]) { - llmobs.annotate(span, { outputData: value }) - } - return value - }) - } + // it is possible to return a value and have a callback + // however, since the span finishes when the callback is called, it is possible that + // the callback is called before the function returns (although unlikely) + // we do not want to throw for "annotating a finished span" in this case + if (!hasCallback) { + llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs), result) + } - if (result && !['llm', 'retrieval'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[OUTPUT_VALUE]) { - llmobs.annotate(span, { outputData: result }) + return result + } catch (e) { + llmobs._autoAnnotate(span, kind, getFunctionArguments(fn, fnArgs)) + throw e } - - return result } return this._tracer.wrap(name, spanOptions, wrapped) @@ -333,20 +367,34 @@ class LLMObs extends NoopLLMObs { flushCh.publish() } + _autoAnnotate (span, kind, input, output) { + const annotations = {} + if (input && !['llm', 'embedding'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[INPUT_VALUE]) { + annotations.inputData = input + } + + if (output && !['llm', 'retrieval'].includes(kind) && !LLMObsTagger.tagMap.get(span)?.[OUTPUT_VALUE]) { + annotations.outputData = output + } + + this.annotate(span, annotations) + } + _active () { const store = storage.getStore() return store?.span } - _activate (span, { kind, options } = {}, fn) { + _activate (span, options, fn) { const parent = this._active() if (this.enabled) storage.enterWith({ span }) - this._tagger.registerLLMObsSpan(span, { - ...options, - parent, - kind - }) + if (options) { + this._tagger.registerLLMObsSpan(span, { + ...options, + parent + }) + } try { return fn() @@ -355,6 +403,22 @@ class LLMObs extends NoopLLMObs { } } + // bind function to active LLMObs span + _bind (fn) { + if (typeof fn !== 'function') return fn + + const llmobs = this + const activeSpan = llmobs._active() + + const bound = function () { + return llmobs._activate(activeSpan, null, () => { + return fn.apply(this, arguments) + }) + } + + return bound + } + _extractOptions (options) { const { modelName, diff --git a/packages/dd-trace/src/log/channels.js b/packages/dd-trace/src/log/channels.js index 545fef4195a..b3b10624705 100644 --- a/packages/dd-trace/src/log/channels.js +++ b/packages/dd-trace/src/log/channels.js @@ -3,7 +3,7 @@ const { channel } = require('dc-polyfill') const Level = { - trace: 20, + trace: 10, debug: 20, info: 30, warn: 40, @@ -12,6 +12,7 @@ const Level = { off: 100 } +const traceChannel = channel('datadog:log:trace') const debugChannel = channel('datadog:log:debug') const infoChannel = channel('datadog:log:info') const warnChannel = channel('datadog:log:warn') @@ -31,6 +32,9 @@ class LogChannel { } subscribe (logger) { + if (Level.trace >= this._level) { + traceChannel.subscribe(logger.trace) + } if (Level.debug >= this._level) { debugChannel.subscribe(logger.debug) } @@ -46,6 +50,9 @@ class LogChannel { } unsubscribe (logger) { + if (traceChannel.hasSubscribers) { + traceChannel.unsubscribe(logger.trace) + } if (debugChannel.hasSubscribers) { debugChannel.unsubscribe(logger.debug) } @@ -63,7 +70,7 @@ class LogChannel { module.exports = { LogChannel, - + traceChannel, debugChannel, infoChannel, warnChannel, diff --git a/packages/dd-trace/src/log/index.js b/packages/dd-trace/src/log/index.js index 3a5392340df..db3a475e120 100644 --- a/packages/dd-trace/src/log/index.js +++ b/packages/dd-trace/src/log/index.js @@ -1,8 +1,9 @@ 'use strict' const coalesce = require('koalas') +const { inspect } = require('util') const { isTrue } = require('../util') -const { debugChannel, infoChannel, warnChannel, errorChannel } = require('./channels') +const { traceChannel, debugChannel, infoChannel, warnChannel, errorChannel } = require('./channels') const logWriter = require('./writer') const { Log } = require('./log') @@ -56,6 +57,24 @@ const log = { return this }, + trace (...args) { + if (traceChannel.hasSubscribers) { + const logRecord = {} + + Error.captureStackTrace(logRecord, this.trace) + + const stack = logRecord.stack.split('\n') + const fn = stack[1].replace(/^\s+at ([^\s]+) .+/, '$1') + const options = { depth: 2, breakLength: Infinity, compact: true, maxArrayLength: Infinity } + const params = args.map(a => inspect(a, options)).join(', ') + + stack[0] = `Trace: ${fn}(${params})` + + traceChannel.publish(Log.parse(stack.join('\n'))) + } + return this + }, + debug (...args) { if (debugChannel.hasSubscribers) { debugChannel.publish(Log.parse(...args)) diff --git a/packages/dd-trace/src/log/writer.js b/packages/dd-trace/src/log/writer.js index 4724253244b..322c703b2b3 100644 --- a/packages/dd-trace/src/log/writer.js +++ b/packages/dd-trace/src/log/writer.js @@ -23,7 +23,7 @@ function withNoop (fn) { } function unsubscribeAll () { - logChannel.unsubscribe({ debug: onDebug, info: onInfo, warn: onWarn, error: onError }) + logChannel.unsubscribe({ trace: onTrace, debug: onDebug, info: onInfo, warn: onWarn, error: onError }) } function toggleSubscription (enable, level) { @@ -31,7 +31,7 @@ function toggleSubscription (enable, level) { if (enable) { logChannel = new LogChannel(level) - logChannel.subscribe({ debug: onDebug, info: onInfo, warn: onWarn, error: onError }) + logChannel.subscribe({ trace: onTrace, debug: onDebug, info: onInfo, warn: onWarn, error: onError }) } } @@ -88,6 +88,14 @@ function onDebug (log) { if (cause) withNoop(() => logger.debug(cause)) } +function onTrace (log) { + const { formatted, cause } = getErrorLog(log) + // Using logger.debug() because not all loggers have trace level, + // and console.trace() has a completely different meaning. + if (formatted) withNoop(() => logger.debug(formatted)) + if (cause) withNoop(() => logger.debug(cause)) +} + function error (...args) { onError(Log.parse(...args)) } @@ -110,4 +118,8 @@ function debug (...args) { onDebug(Log.parse(...args)) } -module.exports = { use, toggle, reset, error, warn, info, debug } +function trace (...args) { + onTrace(Log.parse(...args)) +} + +module.exports = { use, toggle, reset, error, warn, info, debug, trace } diff --git a/packages/dd-trace/src/noop/proxy.js b/packages/dd-trace/src/noop/proxy.js index ec8671a371e..5ab209e612c 100644 --- a/packages/dd-trace/src/noop/proxy.js +++ b/packages/dd-trace/src/noop/proxy.js @@ -10,7 +10,7 @@ const noopAppsec = new NoopAppsecSdk() const noopDogStatsDClient = new NoopDogStatsDClient() const noopLLMObs = new NoopLLMObsSDK(noop) -class Tracer { +class NoopProxy { constructor () { this._tracer = noop this.appsec = noopAppsec @@ -91,4 +91,4 @@ class Tracer { } } -module.exports = Tracer +module.exports = NoopProxy diff --git a/packages/dd-trace/src/noop/span.js b/packages/dd-trace/src/noop/span.js index 1a431d090ea..554fe7423ba 100644 --- a/packages/dd-trace/src/noop/span.js +++ b/packages/dd-trace/src/noop/span.js @@ -6,7 +6,7 @@ const { storage } = require('../../../datadog-core') // TODO: noop storage? class NoopSpan { constructor (tracer, parent) { - this._store = storage.getStore() + this._store = storage.getHandle() this._noopTracer = tracer this._noopContext = this._createContext(parent) } diff --git a/packages/dd-trace/src/opentracing/span.js b/packages/dd-trace/src/opentracing/span.js index 00fd51da027..2c464b2ed1a 100644 --- a/packages/dd-trace/src/opentracing/span.js +++ b/packages/dd-trace/src/opentracing/span.js @@ -14,6 +14,7 @@ const { storage } = require('../../../datadog-core') const telemetryMetrics = require('../telemetry/metrics') const { channel } = require('dc-polyfill') const spanleak = require('../spanleak') +const util = require('util') const tracerMetrics = telemetryMetrics.manager.namespace('tracers') @@ -64,7 +65,7 @@ class DatadogSpan { this._debug = debug this._processor = processor this._prioritySampler = prioritySampler - this._store = storage.getStore() + this._store = storage.getHandle() this._duration = undefined this._events = [] @@ -105,9 +106,18 @@ class DatadogSpan { } } + [util.inspect.custom] () { + return { + ...this, + _parentTracer: `[${this._parentTracer.constructor.name}]`, + _prioritySampler: `[${this._prioritySampler.constructor.name}]`, + _processor: `[${this._processor.constructor.name}]` + } + } + toString () { const spanContext = this.context() - const resourceName = spanContext._tags['resource.name'] + const resourceName = spanContext._tags['resource.name'] || '' const resource = resourceName.length > 100 ? `${resourceName.substring(0, 97)}...` : resourceName diff --git a/packages/dd-trace/src/opentracing/span_context.js b/packages/dd-trace/src/opentracing/span_context.js index 223348bfd55..1cdfeea1ae8 100644 --- a/packages/dd-trace/src/opentracing/span_context.js +++ b/packages/dd-trace/src/opentracing/span_context.js @@ -1,5 +1,6 @@ 'use strict' +const util = require('util') const { AUTO_KEEP } = require('../../../../ext/priority') // the lowercase, hex encoded upper 64 bits of a 128-bit trace id, if present @@ -31,6 +32,17 @@ class DatadogSpanContext { this._otelSpanContext = undefined } + [util.inspect.custom] () { + return { + ...this, + _trace: { + ...this._trace, + started: '[Array]', + finished: '[Array]' + } + } + } + toTraceId (get128bitId = false) { if (get128bitId) { return this._traceId.toBuffer().length <= 8 && this._trace.tags[TRACE_ID_128] diff --git a/packages/dd-trace/src/priority_sampler.js b/packages/dd-trace/src/priority_sampler.js index f9968a41194..7497f1f919c 100644 --- a/packages/dd-trace/src/priority_sampler.js +++ b/packages/dd-trace/src/priority_sampler.js @@ -1,5 +1,6 @@ 'use strict' +const log = require('./log') const RateLimiter = require('./rate_limiter') const Sampler = require('./sampler') const { setSamplingRules } = require('./startup-log') @@ -44,16 +45,19 @@ class PrioritySampler { this.update({}) } - configure (env, { sampleRate, provenance = undefined, rateLimit = 100, rules = [] } = {}) { + configure (env, opts = {}) { + const { sampleRate, provenance = undefined, rateLimit = 100, rules = [] } = opts this._env = env this._rules = this._normalizeRules(rules, sampleRate, rateLimit, provenance) this._limiter = new RateLimiter(rateLimit) + log.trace(env, opts) setSamplingRules(this._rules) } isSampled (span) { const priority = this._getPriorityFromAuto(span) + log.trace(span) return priority === USER_KEEP || priority === AUTO_KEEP } @@ -67,6 +71,8 @@ class PrioritySampler { if (context._sampling.priority !== undefined) return if (!root) return // noop span + log.trace(span, auto) + const tag = this._getPriorityFromTags(context._tags, context) if (this.validate(tag)) { @@ -94,6 +100,8 @@ class PrioritySampler { samplers[DEFAULT_KEY] = samplers[DEFAULT_KEY] || defaultSampler this._samplers = samplers + + log.trace(rates) } validate (samplingPriority) { @@ -112,11 +120,15 @@ class PrioritySampler { if (!span || !this.validate(samplingPriority)) return const context = this._getContext(span) + const root = context._trace.started[0] + + if (!root) return // noop span context._sampling.priority = samplingPriority context._sampling.mechanism = mechanism - const root = context._trace.started[0] + log.trace(span, samplingPriority, mechanism) + this._addDecisionMaker(root) } diff --git a/packages/dd-trace/src/profiling/profilers/event_plugins/event.js b/packages/dd-trace/src/profiling/profilers/event_plugins/event.js index 48e430ba607..eace600a9aa 100644 --- a/packages/dd-trace/src/profiling/profilers/event_plugins/event.js +++ b/packages/dd-trace/src/profiling/profilers/event_plugins/event.js @@ -32,11 +32,11 @@ class EventPlugin extends TracingPlugin { if (!store) return const { startEvent, startTime, error } = store - if (error) { - return // don't emit perf events for failed operations + if (error || this.ignoreEvent(startEvent)) { + return // don't emit perf events for failed operations or ignored events } - const duration = performance.now() - startTime + const duration = performance.now() - startTime const event = { entryType: this.entryType, startTime, @@ -53,6 +53,10 @@ class EventPlugin extends TracingPlugin { this.eventHandler(this.extendEvent(event, startEvent)) } + + ignoreEvent () { + return false + } } module.exports = EventPlugin diff --git a/packages/dd-trace/src/profiling/profilers/event_plugins/fs.js b/packages/dd-trace/src/profiling/profilers/event_plugins/fs.js new file mode 100644 index 00000000000..34eb7b52353 --- /dev/null +++ b/packages/dd-trace/src/profiling/profilers/event_plugins/fs.js @@ -0,0 +1,49 @@ +const EventPlugin = require('./event') + +// Values taken from parameter names in datadog-instrumentations/src/fs.js. +// Known param names that are disallowed because they can be strings and have arbitrary sizes: +// 'data' +// Known param names that are disallowed because they are never a string or number: +// 'buffer', 'buffers', 'listener' +const allowedParams = new Set([ + 'atime', 'dest', + 'existingPath', 'fd', 'file', + 'flag', 'gid', 'len', + 'length', 'mode', 'mtime', + 'newPath', 'offset', 'oldPath', + 'operation', 'options', 'path', + 'position', 'prefix', 'src', + 'target', 'type', 'uid' +]) + +class FilesystemPlugin extends EventPlugin { + static get id () { + return 'fs' + } + + static get operation () { + return 'operation' + } + + static get entryType () { + return 'fs' + } + + ignoreEvent (event) { + // Don't care about sync events, they show up in the event loop samples anyway + return event.operation?.endsWith('Sync') + } + + extendEvent (event, detail) { + const d = { ...detail } + Object.entries(d).forEach(([k, v]) => { + if (!(allowedParams.has(k) && (typeof v === 'string' || typeof v === 'number'))) { + delete d[k] + } + }) + event.detail = d + + return event + } +} +module.exports = FilesystemPlugin diff --git a/packages/dd-trace/src/profiling/profilers/events.js b/packages/dd-trace/src/profiling/profilers/events.js index 2200eaadd2e..8ff1748ceda 100644 --- a/packages/dd-trace/src/profiling/profilers/events.js +++ b/packages/dd-trace/src/profiling/profilers/events.js @@ -133,11 +133,32 @@ class NetDecorator { } } +class FilesystemDecorator { + constructor (stringTable) { + this.stringTable = stringTable + } + + decorateSample (sampleInput, item) { + const labels = sampleInput.label + const stringTable = this.stringTable + Object.entries(item.detail).forEach(([k, v]) => { + switch (typeof v) { + case 'string': + labels.push(labelFromStrStr(stringTable, k, v)) + break + case 'number': + labels.push(new Label({ key: stringTable.dedup(k), num: v })) + } + }) + } +} + // Keys correspond to PerformanceEntry.entryType, values are constructor // functions for type-specific decorators. const decoratorTypes = { - gc: GCDecorator, + fs: FilesystemDecorator, dns: DNSDecorator, + gc: GCDecorator, net: NetDecorator } @@ -255,7 +276,7 @@ class NodeApiEventSource { class DatadogInstrumentationEventSource { constructor (eventHandler, eventFilter) { - this.plugins = ['dns_lookup', 'dns_lookupservice', 'dns_resolve', 'dns_reverse', 'net'].map(m => { + this.plugins = ['dns_lookup', 'dns_lookupservice', 'dns_resolve', 'dns_reverse', 'fs', 'net'].map(m => { const Plugin = require(`./event_plugins/${m}`) return new Plugin(eventHandler, eventFilter) }) diff --git a/packages/dd-trace/src/proxy.js b/packages/dd-trace/src/proxy.js index fd814c9d6e3..874945eeecc 100644 --- a/packages/dd-trace/src/proxy.js +++ b/packages/dd-trace/src/proxy.js @@ -184,7 +184,7 @@ class Tracer extends NoopProxy { if (config.isTestDynamicInstrumentationEnabled) { const testVisibilityDynamicInstrumentation = require('./ci-visibility/dynamic-instrumentation') - testVisibilityDynamicInstrumentation.start() + testVisibilityDynamicInstrumentation.start(config) } } catch (e) { log.error('Error initialising tracer', e) diff --git a/packages/dd-trace/src/runtime_metrics.js b/packages/dd-trace/src/runtime_metrics.js index a9036612a67..f16b227ca18 100644 --- a/packages/dd-trace/src/runtime_metrics.js +++ b/packages/dd-trace/src/runtime_metrics.js @@ -361,7 +361,7 @@ function startGCObserver () { gcObserver = new PerformanceObserver(list => { for (const entry of list.getEntries()) { - const type = gcType(entry.kind) + const type = gcType(entry.detail?.kind || entry.kind) runtimeMetrics.histogram('runtime.node.gc.pause.by.type', entry.duration, `gc_type:${type}`) runtimeMetrics.histogram('runtime.node.gc.pause', entry.duration) diff --git a/packages/dd-trace/src/scope.js b/packages/dd-trace/src/scope.js index fb279ae0266..9b96ff565ea 100644 --- a/packages/dd-trace/src/scope.js +++ b/packages/dd-trace/src/scope.js @@ -17,7 +17,7 @@ class Scope { if (typeof callback !== 'function') return callback const oldStore = storage.getStore() - const newStore = span ? span._store : oldStore + const newStore = span ? storage.getStore(span._store) : oldStore storage.enterWith({ ...newStore, span }) diff --git a/packages/dd-trace/src/telemetry/index.js b/packages/dd-trace/src/telemetry/index.js index eb1fe376c67..9328186a82a 100644 --- a/packages/dd-trace/src/telemetry/index.js +++ b/packages/dd-trace/src/telemetry/index.js @@ -307,6 +307,8 @@ function updateConfig (changes, config) { if (!config.telemetry.enabled) return if (changes.length === 0) return + logger.trace(changes) + const application = createAppObject(config) const host = createHostObject() diff --git a/packages/dd-trace/src/util.js b/packages/dd-trace/src/util.js index 8cfa3d6f58c..de3618fcd27 100644 --- a/packages/dd-trace/src/util.js +++ b/packages/dd-trace/src/util.js @@ -1,6 +1,5 @@ 'use strict' -const crypto = require('crypto') const path = require('path') function isTrue (str) { @@ -78,25 +77,11 @@ function hasOwn (object, prop) { return Object.prototype.hasOwnProperty.call(object, prop) } -/** - * Generates a unique hash from an array of strings by joining them with | before hashing. - * Used to uniquely identify AWS requests for span pointers. - * @param {string[]} components - Array of strings to hash - * @returns {string} A 32-character hash uniquely identifying the components - */ -function generatePointerHash (components) { - // If passing S3's ETag as a component, make sure any quotes have already been removed! - const dataToHash = components.join('|') - const hash = crypto.createHash('sha256').update(dataToHash).digest('hex') - return hash.substring(0, 32) -} - module.exports = { isTrue, isFalse, isError, globMatch, calculateDDBasePath, - hasOwn, - generatePointerHash + hasOwn } diff --git a/packages/dd-trace/test/appsec/iast/analyzers/code-injection-analyzer.express.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/code-injection-analyzer.express.plugin.spec.js index 4177dc78aba..64e15b9161b 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/code-injection-analyzer.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/code-injection-analyzer.express.plugin.spec.js @@ -6,6 +6,10 @@ const path = require('path') const os = require('os') const fs = require('fs') const { clearCache } = require('../../../../src/appsec/iast/vulnerability-reporter') +const { newTaintedString } = require('../../../../src/appsec/iast/taint-tracking/operations') +const { SQL_ROW_VALUE } = require('../../../../src/appsec/iast/taint-tracking/source-types') +const { storage } = require('../../../../../datadog-core') +const iastContextFunctions = require('../../../../src/appsec/iast/iast-context') describe('Code injection vulnerability', () => { withVersions('express', 'express', '>4.18.0', version => { @@ -29,7 +33,6 @@ describe('Code injection vulnerability', () => { (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { testThatRequestHasVulnerability({ fn: (req, res) => { - // eslint-disable-next-line no-eval res.send(require(evalFunctionsPath).runEval(req.query.script, 'test-result')) }, vulnerability: 'CODE_INJECTION', @@ -42,6 +45,19 @@ describe('Code injection vulnerability', () => { } }) + testThatRequestHasVulnerability({ + fn: (req, res) => { + const source = '1 + 2' + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const str = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + + res.send(require(evalFunctionsPath).runEval(str, 'test-result')) + }, + vulnerability: 'CODE_INJECTION', + testDescription: 'Should detect CODE_INJECTION vulnerability with DB source' + }) + testThatRequestHasNoVulnerability({ fn: (req, res) => { res.send('' + require(evalFunctionsPath).runFakeEval(req.query.script)) diff --git a/packages/dd-trace/test/appsec/iast/analyzers/ldap-injection-analyzer.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/ldap-injection-analyzer.spec.js index 59413db0a4f..c8af2de6846 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/ldap-injection-analyzer.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/ldap-injection-analyzer.spec.js @@ -1,14 +1,27 @@ 'use strict' const proxyquire = require('proxyquire') +const { HTTP_REQUEST_PARAMETER } = require('../../../../src/appsec/iast/taint-tracking/source-types') describe('ldap-injection-analyzer', () => { const NOT_TAINTED_QUERY = 'no vulnerable query' const TAINTED_QUERY = 'vulnerable query' const TaintTrackingMock = { - isTainted: (iastContext, string) => { + getRanges: (iastContext, string) => { return string === TAINTED_QUERY + ? [ + { + start: 0, + end: string.length, + iinfo: { + parameterName: 'param', + parameterValue: string, + type: HTTP_REQUEST_PARAMETER + } + } + ] + : [] } } diff --git a/packages/dd-trace/test/appsec/iast/analyzers/path-traversal-analyzer.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/path-traversal-analyzer.spec.js index 6c39799f916..3fe86dacd8d 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/path-traversal-analyzer.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/path-traversal-analyzer.spec.js @@ -12,6 +12,7 @@ const { newTaintedString } = require('../../../../src/appsec/iast/taint-tracking const { prepareTestServerForIast } = require('../utils') const fs = require('fs') +const { HTTP_REQUEST_PARAMETER } = require('../../../../src/appsec/iast/taint-tracking/source-types') const iastContext = { rootSpan: { @@ -25,26 +26,23 @@ const iastContext = { } } -const TaintTrackingMock = { - isTainted: sinon.stub() +const getRanges = (ctx, val) => { + return [ + { + start: 0, + end: val.length, + iinfo: { + parameterName: 'param', + parameterValue: val, + type: HTTP_REQUEST_PARAMETER + } + } + ] } -const getIastContext = sinon.stub() -const hasQuota = sinon.stub() -const addVulnerability = sinon.stub() - -const ProxyAnalyzer = proxyquire('../../../../src/appsec/iast/analyzers/vulnerability-analyzer', { - '../iast-context': { getIastContext }, - '../overhead-controller': { hasQuota }, - '../vulnerability-reporter': { addVulnerability } -}) - -const InjectionAnalyzer = proxyquire('../../../../src/appsec/iast/analyzers/injection-analyzer', { - './vulnerability-analyzer': ProxyAnalyzer, - '../taint-tracking/operations': TaintTrackingMock -}) - describe('path-traversal-analyzer', () => { + let TaintTrackingMock, getIastContext, hasQuota, addVulnerability, ProxyAnalyzer, InjectionAnalyzer + before(() => { pathTraversalAnalyzer.enable() }) @@ -53,6 +51,28 @@ describe('path-traversal-analyzer', () => { pathTraversalAnalyzer.disable() }) + beforeEach(() => { + TaintTrackingMock = { + isTainted: sinon.stub(), + getRanges: sinon.stub() + } + + getIastContext = sinon.stub() + hasQuota = sinon.stub() + addVulnerability = sinon.stub() + + ProxyAnalyzer = proxyquire('../../../../src/appsec/iast/analyzers/vulnerability-analyzer', { + '../iast-context': { getIastContext }, + '../overhead-controller': { hasQuota }, + '../vulnerability-reporter': { addVulnerability } + }) + + InjectionAnalyzer = proxyquire('../../../../src/appsec/iast/analyzers/injection-analyzer', { + './vulnerability-analyzer': ProxyAnalyzer, + '../taint-tracking/operations': TaintTrackingMock + }) + }) + it('Analyzer should be subscribed to proper channel', () => { expect(pathTraversalAnalyzer._subscriptions).to.have.lengthOf(1) expect(pathTraversalAnalyzer._subscriptions[0]._channel.name).to.equals('apm:fs:operation:start') @@ -72,26 +92,25 @@ describe('path-traversal-analyzer', () => { }) it('if context exists but value is not a string it should not call isTainted', () => { - const isTainted = sinon.stub() + const getRanges = sinon.stub() const iastContext = {} const proxyPathAnalyzer = proxyquire('../../../../src/appsec/iast/analyzers/path-traversal-analyzer', { - '../taint-tracking': { isTainted } + '../taint-tracking': { getRanges } }) proxyPathAnalyzer._isVulnerable(undefined, iastContext) - expect(isTainted).not.to.have.been.called + expect(getRanges).not.to.have.been.called }) it('if context and value are valid it should call isTainted', () => { - // const isTainted = sinon.stub() const iastContext = {} const proxyPathAnalyzer = proxyquire('../../../../src/appsec/iast/analyzers/path-traversal-analyzer', { './injection-analyzer': InjectionAnalyzer }) - TaintTrackingMock.isTainted.returns(false) + TaintTrackingMock.getRanges.returns([]) const result = proxyPathAnalyzer._isVulnerable('test', iastContext) expect(result).to.be.false - expect(TaintTrackingMock.isTainted).to.have.been.calledOnce + expect(TaintTrackingMock.getRanges).to.have.been.calledOnce }) it('Should report proper vulnerability type', () => { @@ -102,7 +121,7 @@ describe('path-traversal-analyzer', () => { getIastContext.returns(iastContext) hasQuota.returns(true) - TaintTrackingMock.isTainted.returns(true) + TaintTrackingMock.getRanges.callsFake(getRanges) proxyPathAnalyzer.analyze(['test']) expect(addVulnerability).to.have.been.calledOnce @@ -116,9 +135,8 @@ describe('path-traversal-analyzer', () => { '../iast-context': { getIastContext: () => iastContext } }) - addVulnerability.reset() getIastContext.returns(iastContext) - TaintTrackingMock.isTainted.returns(true) + TaintTrackingMock.getRanges.callsFake(getRanges) hasQuota.returns(true) proxyPathAnalyzer.analyze(['taintedArg1', 'taintedArg2']) @@ -132,11 +150,10 @@ describe('path-traversal-analyzer', () => { '../iast-context': { getIastContext: () => iastContext } }) - addVulnerability.reset() - TaintTrackingMock.isTainted.reset() getIastContext.returns(iastContext) - TaintTrackingMock.isTainted.onFirstCall().returns(false) - TaintTrackingMock.isTainted.onSecondCall().returns(true) + + TaintTrackingMock.getRanges.onFirstCall().returns([]) + TaintTrackingMock.getRanges.onSecondCall().callsFake(getRanges) hasQuota.returns(true) proxyPathAnalyzer.analyze(['arg1', 'taintedArg2']) @@ -155,10 +172,8 @@ describe('path-traversal-analyzer', () => { return { path: mockPath, line: 3 } } - addVulnerability.reset() - TaintTrackingMock.isTainted.reset() getIastContext.returns(iastContext) - TaintTrackingMock.isTainted.returns(true) + TaintTrackingMock.getRanges.callsFake(getRanges) hasQuota.returns(true) proxyPathAnalyzer.analyze(['arg1']) diff --git a/packages/dd-trace/test/appsec/iast/analyzers/sql-injection-analyzer.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/sql-injection-analyzer.spec.js index de662075cf3..8c4d26103d3 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/sql-injection-analyzer.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/sql-injection-analyzer.spec.js @@ -4,14 +4,27 @@ const proxyquire = require('proxyquire') const log = require('../../../../src/log') const dc = require('dc-polyfill') +const { HTTP_REQUEST_PARAMETER } = require('../../../../src/appsec/iast/taint-tracking/source-types') describe('sql-injection-analyzer', () => { const NOT_TAINTED_QUERY = 'no vulnerable query' const TAINTED_QUERY = 'vulnerable query' const TaintTrackingMock = { - isTainted: (iastContext, string) => { + getRanges: (iastContext, string) => { return string === TAINTED_QUERY + ? [ + { + start: 0, + end: string.length, + iinfo: { + parameterName: 'param', + parameterValue: string, + type: HTTP_REQUEST_PARAMETER + } + } + ] + : [] } } diff --git a/packages/dd-trace/test/appsec/iast/analyzers/template-injection-analyzer.handlebars.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/template-injection-analyzer.handlebars.plugin.spec.js index 4152f4ab6e9..b3398543a04 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/template-injection-analyzer.handlebars.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/template-injection-analyzer.handlebars.plugin.spec.js @@ -4,6 +4,7 @@ const { prepareTestServerForIast } = require('../utils') const { storage } = require('../../../../../datadog-core') const iastContextFunctions = require('../../../../src/appsec/iast/iast-context') const { newTaintedString } = require('../../../../src/appsec/iast/taint-tracking/operations') +const { SQL_ROW_VALUE } = require('../../../../src/appsec/iast/taint-tracking/source-types') describe('template-injection-analyzer with handlebars', () => { withVersions('handlebars', 'handlebars', version => { @@ -27,6 +28,14 @@ describe('template-injection-analyzer with handlebars', () => { lib.compile(template) }, 'TEMPLATE_INJECTION') + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const template = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + lib.compile(template) + }, 'TEMPLATE_INJECTION', undefined, undefined, undefined, + 'Should detect TEMPLATE_INJECTION vulnerability with DB source') + testThatRequestHasNoVulnerability(() => { lib.compile(source) }, 'TEMPLATE_INJECTION') @@ -48,6 +57,14 @@ describe('template-injection-analyzer with handlebars', () => { lib.precompile(template) }, 'TEMPLATE_INJECTION') + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const template = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + lib.precompile(template) + }, 'TEMPLATE_INJECTION', undefined, undefined, undefined, + 'Should detect TEMPLATE_INJECTION vulnerability with DB source') + testThatRequestHasNoVulnerability(() => { lib.precompile(source) }, 'TEMPLATE_INJECTION') @@ -70,6 +87,15 @@ describe('template-injection-analyzer with handlebars', () => { lib.registerPartial('vulnerablePartial', partial) }, 'TEMPLATE_INJECTION') + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const partial = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + + lib.registerPartial('vulnerablePartial', partial) + }, 'TEMPLATE_INJECTION', undefined, undefined, undefined, + 'Should detect TEMPLATE_INJECTION vulnerability with DB source') + testThatRequestHasNoVulnerability(() => { lib.registerPartial('vulnerablePartial', source) }, 'TEMPLATE_INJECTION') diff --git a/packages/dd-trace/test/appsec/iast/analyzers/template-injection-analyzer.pug.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/template-injection-analyzer.pug.plugin.spec.js index 412da3a62f0..574f256fd53 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/template-injection-analyzer.pug.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/template-injection-analyzer.pug.plugin.spec.js @@ -4,6 +4,7 @@ const { prepareTestServerForIast } = require('../utils') const { storage } = require('../../../../../datadog-core') const iastContextFunctions = require('../../../../src/appsec/iast/iast-context') const { newTaintedString } = require('../../../../src/appsec/iast/taint-tracking/operations') +const { SQL_ROW_VALUE } = require('../../../../src/appsec/iast/taint-tracking/source-types') describe('template-injection-analyzer with pug', () => { withVersions('pug', 'pug', version => { @@ -27,6 +28,14 @@ describe('template-injection-analyzer with pug', () => { lib.compile(template) }, 'TEMPLATE_INJECTION') + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const template = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + lib.compile(template) + }, 'TEMPLATE_INJECTION', undefined, undefined, undefined, + 'Should detect TEMPLATE_INJECTION vulnerability with DB source') + testThatRequestHasNoVulnerability(() => { const template = lib.compile(source) template() @@ -49,6 +58,14 @@ describe('template-injection-analyzer with pug', () => { lib.compileClient(template) }, 'TEMPLATE_INJECTION') + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const template = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + lib.compileClient(template) + }, 'TEMPLATE_INJECTION', undefined, undefined, undefined, + 'Should detect TEMPLATE_INJECTION vulnerability with DB source') + testThatRequestHasNoVulnerability(() => { lib.compileClient(source) }, 'TEMPLATE_INJECTION') @@ -70,6 +87,14 @@ describe('template-injection-analyzer with pug', () => { lib.compileClientWithDependenciesTracked(template, {}) }, 'TEMPLATE_INJECTION') + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const template = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + lib.compileClientWithDependenciesTracked(template, {}) + }, 'TEMPLATE_INJECTION', undefined, undefined, undefined, + 'Should detect TEMPLATE_INJECTION vulnerability with DB source') + testThatRequestHasNoVulnerability(() => { lib.compileClient(source) }, 'TEMPLATE_INJECTION') @@ -91,6 +116,14 @@ describe('template-injection-analyzer with pug', () => { lib.render(str) }, 'TEMPLATE_INJECTION') + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const str = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + lib.render(str) + }, 'TEMPLATE_INJECTION', undefined, undefined, undefined, + 'Should detect TEMPLATE_INJECTION vulnerability with DB source') + testThatRequestHasNoVulnerability(() => { lib.render(source) }, 'TEMPLATE_INJECTION') diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.spec.js index 5f9c4f4860f..af575ce9652 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.spec.js @@ -8,8 +8,10 @@ const { HTTP_REQUEST_COOKIE_VALUE, HTTP_REQUEST_HEADER_VALUE, HTTP_REQUEST_PATH_PARAM, - HTTP_REQUEST_URI + HTTP_REQUEST_URI, + SQL_ROW_VALUE } = require('../../../../src/appsec/iast/taint-tracking/source-types') +const Config = require('../../../../src/config') const middlewareNextChannel = dc.channel('apm:express:middleware:next') const queryReadFinishChannel = dc.channel('datadog:query:read:finish') @@ -17,6 +19,7 @@ const bodyParserFinishChannel = dc.channel('datadog:body-parser:read:finish') const cookieParseFinishCh = dc.channel('datadog:cookie:parse:finish') const processParamsStartCh = dc.channel('datadog:express:process_params:start') const routerParamStartCh = dc.channel('datadog:router:param:start') +const sequelizeFinish = dc.channel('datadog:sequelize:query:finish') describe('IAST Taint tracking plugin', () => { let taintTrackingPlugin @@ -34,7 +37,8 @@ describe('IAST Taint tracking plugin', () => { './operations': sinon.spy(taintTrackingOperations), '../../../../../datadog-core': datadogCore }) - taintTrackingPlugin.enable() + const config = new Config() + taintTrackingPlugin.enable(config.iast) }) afterEach(() => { @@ -43,18 +47,20 @@ describe('IAST Taint tracking plugin', () => { }) it('Should subscribe to body parser, qs, cookie and process_params channel', () => { - expect(taintTrackingPlugin._subscriptions).to.have.lengthOf(11) + expect(taintTrackingPlugin._subscriptions).to.have.lengthOf(13) expect(taintTrackingPlugin._subscriptions[0]._channel.name).to.equals('datadog:body-parser:read:finish') expect(taintTrackingPlugin._subscriptions[1]._channel.name).to.equals('datadog:multer:read:finish') expect(taintTrackingPlugin._subscriptions[2]._channel.name).to.equals('datadog:query:read:finish') expect(taintTrackingPlugin._subscriptions[3]._channel.name).to.equals('datadog:express:query:finish') expect(taintTrackingPlugin._subscriptions[4]._channel.name).to.equals('apm:express:middleware:next') expect(taintTrackingPlugin._subscriptions[5]._channel.name).to.equals('datadog:cookie:parse:finish') - expect(taintTrackingPlugin._subscriptions[6]._channel.name).to.equals('datadog:express:process_params:start') - expect(taintTrackingPlugin._subscriptions[7]._channel.name).to.equals('datadog:router:param:start') - expect(taintTrackingPlugin._subscriptions[8]._channel.name).to.equals('apm:graphql:resolve:start') - expect(taintTrackingPlugin._subscriptions[9]._channel.name).to.equals('datadog:url:parse:finish') - expect(taintTrackingPlugin._subscriptions[10]._channel.name).to.equals('datadog:url:getter:finish') + expect(taintTrackingPlugin._subscriptions[6]._channel.name).to.equals('datadog:sequelize:query:finish') + expect(taintTrackingPlugin._subscriptions[7]._channel.name).to.equals('apm:pg:query:finish') + expect(taintTrackingPlugin._subscriptions[8]._channel.name).to.equals('datadog:express:process_params:start') + expect(taintTrackingPlugin._subscriptions[9]._channel.name).to.equals('datadog:router:param:start') + expect(taintTrackingPlugin._subscriptions[10]._channel.name).to.equals('apm:graphql:resolve:start') + expect(taintTrackingPlugin._subscriptions[11]._channel.name).to.equals('datadog:url:parse:finish') + expect(taintTrackingPlugin._subscriptions[12]._channel.name).to.equals('datadog:url:getter:finish') }) describe('taint sources', () => { @@ -271,5 +277,259 @@ describe('IAST Taint tracking plugin', () => { HTTP_REQUEST_URI ) }) + + describe('taint database sources', () => { + it('Should not taint if config is set to 0', () => { + taintTrackingPlugin.disable() + const config = new Config() + config.dbRowsToTaint = 0 + taintTrackingPlugin.enable(config) + + const result = [ + { + id: 1, + name: 'string value 1' + }, + { + id: 2, + name: 'string value 2' + }] + sequelizeFinish.publish({ result }) + + expect(taintTrackingOperations.newTaintedString).to.not.have.been.called + }) + + describe('with default config', () => { + it('Should taint first database row coming from sequelize', () => { + const result = [ + { + id: 1, + name: 'string value 1' + }, + { + id: 2, + name: 'string value 2' + }] + sequelizeFinish.publish({ result }) + + expect(taintTrackingOperations.newTaintedString).to.be.calledOnceWith( + iastContext, + 'string value 1', + '0.name', + SQL_ROW_VALUE + ) + }) + + it('Should taint whole object', () => { + const result = { id: 1, description: 'value' } + sequelizeFinish.publish({ result }) + + expect(taintTrackingOperations.newTaintedString).to.be.calledOnceWith( + iastContext, + 'value', + 'description', + SQL_ROW_VALUE + ) + }) + + it('Should taint first row in nested objects', () => { + const result = [ + { + id: 1, + description: 'value', + children: [ + { + id: 11, + name: 'child1' + }, + { + id: 12, + name: 'child2' + } + ] + }, + { + id: 2, + description: 'value', + children: [ + { + id: 21, + name: 'child3' + }, + { + id: 22, + name: 'child4' + } + ] + } + ] + sequelizeFinish.publish({ result }) + + expect(taintTrackingOperations.newTaintedString).to.be.calledTwice + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'value', + '0.description', + SQL_ROW_VALUE + ) + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'child1', + '0.children.0.name', + SQL_ROW_VALUE + ) + }) + }) + + describe('with config set to 2', () => { + beforeEach(() => { + taintTrackingPlugin.disable() + const config = new Config() + config.dbRowsToTaint = 2 + taintTrackingPlugin.enable(config) + }) + + it('Should taint first database row coming from sequelize', () => { + const result = [ + { + id: 1, + name: 'string value 1' + }, + { + id: 2, + name: 'string value 2' + }, + { + id: 3, + name: 'string value 2' + }] + sequelizeFinish.publish({ result }) + + expect(taintTrackingOperations.newTaintedString).to.be.calledTwice + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'string value 1', + '0.name', + SQL_ROW_VALUE + ) + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'string value 2', + '1.name', + SQL_ROW_VALUE + ) + }) + + it('Should taint whole object', () => { + const result = { id: 1, description: 'value' } + sequelizeFinish.publish({ result }) + + expect(taintTrackingOperations.newTaintedString).to.be.calledOnceWith( + iastContext, + 'value', + 'description', + SQL_ROW_VALUE + ) + }) + + it('Should taint first row in nested objects', () => { + const result = [ + { + id: 1, + description: 'value', + children: [ + { + id: 11, + name: 'child1' + }, + { + id: 12, + name: 'child2' + }, + { + id: 13, + name: 'child3' + } + ] + }, + { + id: 2, + description: 'value2', + children: [ + { + id: 21, + name: 'child4' + }, + { + id: 22, + name: 'child5' + }, + { + id: 23, + name: 'child6' + } + ] + }, + { + id: 3, + description: 'value3', + children: [ + { + id: 31, + name: 'child7' + }, + { + id: 32, + name: 'child8' + }, + { + id: 33, + name: 'child9' + } + ] + } + ] + sequelizeFinish.publish({ result }) + + expect(taintTrackingOperations.newTaintedString).to.callCount(6) + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'value', + '0.description', + SQL_ROW_VALUE + ) + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'child1', + '0.children.0.name', + SQL_ROW_VALUE + ) + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'child2', + '0.children.1.name', + SQL_ROW_VALUE + ) + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'value2', + '1.description', + SQL_ROW_VALUE + ) + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'child4', + '1.children.0.name', + SQL_ROW_VALUE + ) + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'child5', + '1.children.1.name', + SQL_ROW_VALUE + ) + }) + }) + }) }) }) diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/sources/sql_row.pg.plugin.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/sql_row.pg.plugin.spec.js new file mode 100644 index 00000000000..69e73b0ccb0 --- /dev/null +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/sql_row.pg.plugin.spec.js @@ -0,0 +1,113 @@ +'use strict' + +const { prepareTestServerForIast } = require('../../utils') + +const connectionData = { + host: '127.0.0.1', + user: 'postgres', + password: 'postgres', + database: 'postgres', + application_name: 'test' +} + +describe('db sources with pg', () => { + let pg + withVersions('pg', 'pg', '>=8.0.3', version => { + let client + beforeEach(async () => { + pg = require(`../../../../../../../versions/pg@${version}`).get() + client = new pg.Client(connectionData) + await client.connect() + + await client.query(`CREATE TABLE IF NOT EXISTS examples ( + id INT, + name VARCHAR(50), + query VARCHAR(100), + command VARCHAR(50))`) + + await client.query(`INSERT INTO examples (id, name, query, command) + VALUES (1, 'Item1', 'SELECT 1', 'ls'), + (2, 'Item2', 'SELECT 1', 'ls'), + (3, 'Item3', 'SELECT 1', 'ls')`) + }) + + afterEach(async () => { + await client.query('DROP TABLE examples') + client.end() + }) + + prepareTestServerForIast('sequelize', (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + describe('using pg.Client', () => { + testThatRequestHasVulnerability(async (req, res) => { + const result = await client.query('SELECT * FROM examples') + + const firstItem = result.rows[0] + + await client.query(firstItem.query) + + res.end() + }, 'SQL_INJECTION', { occurrences: 1 }, null, null, + 'Should have SQL_INJECTION using the first row of the result') + + testThatRequestHasNoVulnerability(async (req, res) => { + const result = await client.query('SELECT * FROM examples') + + const secondItem = result.rows[1] + + await client.query(secondItem.query) + + res.end() + }, 'SQL_INJECTION', null, 'Should not taint the second row of a query with default configuration') + + testThatRequestHasNoVulnerability(async (req, res) => { + const result = await client.query('SELECT * from examples') + const firstItem = result.rows[0] + + const childProcess = require('child_process') + childProcess.execSync(firstItem.command) + + res.end('OK') + }, 'COMMAND_INJECTION', null, 'Should not detect COMMAND_INJECTION with database source') + }) + + describe('using pg.Pool', () => { + let pool + + beforeEach(() => { + pool = new pg.Pool(connectionData) + }) + + testThatRequestHasVulnerability(async (req, res) => { + const result = await pool.query('SELECT * FROM examples') + + const firstItem = result.rows[0] + + await client.query(firstItem.query) + + res.end() + }, 'SQL_INJECTION', { occurrences: 1 }, null, null, + 'Should have SQL_INJECTION using the first row of the result') + + testThatRequestHasNoVulnerability(async (req, res) => { + const result = await pool.query('SELECT * FROM examples') + + const secondItem = result.rows[1] + + await client.query(secondItem.query) + + res.end() + }, 'SQL_INJECTION', null, 'Should not taint the second row of a query with default configuration') + + testThatRequestHasNoVulnerability(async (req, res) => { + const result = await pool.query('SELECT * from examples') + const firstItem = result.rows[0] + + const childProcess = require('child_process') + childProcess.execSync(firstItem.command) + + res.end('OK') + }, 'COMMAND_INJECTION', null, 'Should not detect COMMAND_INJECTION with database source') + }) + }) + }) +}) diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/sources/sql_row.sequelize.plugin.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/sql_row.sequelize.plugin.spec.js new file mode 100644 index 00000000000..0e1e84888c7 --- /dev/null +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/sql_row.sequelize.plugin.spec.js @@ -0,0 +1,106 @@ +'use strict' + +const { prepareTestServerForIast } = require('../../utils') + +describe('db sources with sequelize', () => { + withVersions('sequelize', 'sequelize', sequelizeVersion => { + prepareTestServerForIast('sequelize', (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + let Sequelize, sequelize + + beforeEach(async () => { + Sequelize = require(`../../../../../../../versions/sequelize@${sequelizeVersion}`).get() + sequelize = new Sequelize('database', 'username', 'password', { + dialect: 'sqlite', + logging: false + }) + await sequelize.query(`CREATE TABLE examples ( + id INT, + name VARCHAR(50), + query VARCHAR(100), + command VARCHAR(50), + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, + updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP )`) + + await sequelize.query(`INSERT INTO examples (id, name, query, command) + VALUES (1, 'Item1', 'SELECT 1', 'ls'), + (2, 'Item2', 'SELECT 1', 'ls'), + (3, 'Item3', 'SELECT 1', 'ls')`) + }) + + afterEach(() => { + return sequelize.close() + }) + + describe('using query method', () => { + testThatRequestHasVulnerability(async (req, res) => { + const result = await sequelize.query('SELECT * from examples') + + await sequelize.query(result[0][0].query) + + res.end('OK') + }, 'SQL_INJECTION', { occurrences: 1 }, null, null, + 'Should have SQL_INJECTION using the first row of the result') + + testThatRequestHasNoVulnerability(async (req, res) => { + const result = await sequelize.query('SELECT * from examples') + + await sequelize.query(result[0][1].query) + + res.end('OK') + }, 'SQL_INJECTION', null, 'Should not taint the second row of a query with default configuration') + + testThatRequestHasNoVulnerability(async (req, res) => { + const result = await sequelize.query('SELECT * from examples') + + const childProcess = require('child_process') + childProcess.execSync(result[0][0].command) + + res.end('OK') + }, 'COMMAND_INJECTION', null, 'Should not detect COMMAND_INJECTION with database source') + }) + + describe('using Model', () => { + // let Model + let Example + + beforeEach(() => { + Example = sequelize.define('example', { + id: { + type: Sequelize.DataTypes.INTEGER, + primaryKey: true + }, + name: Sequelize.DataTypes.STRING, + query: Sequelize.DataTypes.STRING, + command: Sequelize.DataTypes.STRING + }) + }) + + testThatRequestHasVulnerability(async (req, res) => { + const examples = await Example.findAll() + + await sequelize.query(examples[0].query) + + res.end('OK') + }, 'SQL_INJECTION', { occurrences: 1 }, null, null, + 'Should have SQL_INJECTION using the first row of the result') + + testThatRequestHasNoVulnerability(async (req, res) => { + const examples = await Example.findAll() + + await sequelize.query(examples[1].query) + + res.end('OK') + }, 'SQL_INJECTION', null, 'Should not taint the second row of a query with default configuration') + + testThatRequestHasNoVulnerability(async (req, res) => { + const examples = await Example.findAll() + + const childProcess = require('child_process') + childProcess.execSync(examples[0].command) + + res.end('OK') + }, 'COMMAND_INJECTION', null, 'Should not detect COMMAND_INJECTION with database source') + }) + }) + }) +}) diff --git a/packages/dd-trace/test/appsec/iast/utils.js b/packages/dd-trace/test/appsec/iast/utils.js index 6e427bcb629..01274dd954e 100644 --- a/packages/dd-trace/test/appsec/iast/utils.js +++ b/packages/dd-trace/test/appsec/iast/utils.js @@ -256,8 +256,8 @@ function prepareTestServerForIast (description, tests, iastConfig) { }) } - function testThatRequestHasNoVulnerability (fn, vulnerability, makeRequest) { - it(`should not have ${vulnerability} vulnerability`, function (done) { + function testThatRequestHasNoVulnerability (fn, vulnerability, makeRequest, description) { + it(description || `should not have ${vulnerability} vulnerability`, function (done) { app = fn checkNoVulnerabilityInRequest(vulnerability, config, done, makeRequest) }) diff --git a/packages/dd-trace/test/appsec/rasp/command_injection.express.plugin.spec.js b/packages/dd-trace/test/appsec/rasp/command_injection.express.plugin.spec.js index 3943bd0c3c3..d7609367ab9 100644 --- a/packages/dd-trace/test/appsec/rasp/command_injection.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/rasp/command_injection.express.plugin.spec.js @@ -5,42 +5,25 @@ const appsec = require('../../../src/appsec') const Config = require('../../../src/config') const path = require('path') const Axios = require('axios') -const { getWebSpan, checkRaspExecutedAndHasThreat, checkRaspExecutedAndNotThreat } = require('./utils') +const { checkRaspExecutedAndHasThreat, checkRaspExecutedAndNotThreat } = require('./utils') const { assert } = require('chai') describe('RASP - command_injection', () => { withVersions('express', 'express', expressVersion => { let app, server, axios + function testShellBlockingAndSafeRequests () { + it('should block the threat', async () => { + try { + await axios.get('/?dir=$(cat /etc/passwd 1>%262 ; echo .)') + } catch (e) { + if (!e.response) { + throw e + } - async function testBlockingRequest () { - try { - await axios.get('/?dir=$(cat /etc/passwd 1>%262 ; echo .)') - } catch (e) { - if (!e.response) { - throw e - } - - return checkRaspExecutedAndHasThreat(agent, 'rasp-command_injection-rule-id-3') - } - - assert.fail('Request should be blocked') - } - - function checkRaspNotExecutedAndNotThreat (agent, checkRuleEval = true) { - return agent.use((traces) => { - const span = getWebSpan(traces) - - assert.notProperty(span.meta, '_dd.appsec.json') - assert.notProperty(span.meta_struct || {}, '_dd.stack') - if (checkRuleEval) { - assert.notProperty(span.metrics, '_dd.appsec.rasp.rule.eval') + return checkRaspExecutedAndHasThreat(agent, 'rasp-command_injection-rule-id-3') } - }) - } - function testBlockingAndSafeRequests () { - it('should block the threat', async () => { - await testBlockingRequest() + assert.fail('Request should be blocked') }) it('should not block safe request', async () => { @@ -50,17 +33,25 @@ describe('RASP - command_injection', () => { }) } - function testSafeInNonShell () { - it('should not block the threat', async () => { - await axios.get('/?dir=$(cat /etc/passwd 1>%262 ; echo .)') + function testNonShellBlockingAndSafeRequests () { + it('should block the threat', async () => { + try { + await axios.get('/?command=/usr/bin/reboot') + } catch (e) { + if (!e.response) { + throw e + } - return checkRaspNotExecutedAndNotThreat(agent) + return checkRaspExecutedAndHasThreat(agent, 'rasp-command_injection-rule-id-4') + } + + assert.fail('Request should be blocked') }) it('should not block safe request', async () => { - await axios.get('/?dir=.') + await axios.get('/?command=.') - return checkRaspNotExecutedAndNotThreat(agent) + return checkRaspExecutedAndNotThreat(agent) }) } @@ -116,7 +107,7 @@ describe('RASP - command_injection', () => { } }) - testBlockingAndSafeRequests() + testShellBlockingAndSafeRequests() }) describe('with promise', () => { @@ -137,7 +128,7 @@ describe('RASP - command_injection', () => { } }) - testBlockingAndSafeRequests() + testShellBlockingAndSafeRequests() }) describe('with event emitter', () => { @@ -158,7 +149,7 @@ describe('RASP - command_injection', () => { } }) - testBlockingAndSafeRequests() + testShellBlockingAndSafeRequests() }) describe('execSync', () => { @@ -178,7 +169,7 @@ describe('RASP - command_injection', () => { } }) - testBlockingAndSafeRequests() + testShellBlockingAndSafeRequests() }) }) @@ -199,7 +190,7 @@ describe('RASP - command_injection', () => { } }) - testBlockingAndSafeRequests() + testShellBlockingAndSafeRequests() }) describe('with promise', () => { @@ -220,7 +211,7 @@ describe('RASP - command_injection', () => { } }) - testBlockingAndSafeRequests() + testShellBlockingAndSafeRequests() }) describe('with event emitter', () => { @@ -241,7 +232,7 @@ describe('RASP - command_injection', () => { } }) - testBlockingAndSafeRequests() + testShellBlockingAndSafeRequests() }) describe('execFileSync', () => { @@ -261,7 +252,7 @@ describe('RASP - command_injection', () => { } }) - testBlockingAndSafeRequests() + testShellBlockingAndSafeRequests() }) }) @@ -271,7 +262,7 @@ describe('RASP - command_injection', () => { app = (req, res) => { const childProcess = require('child_process') - childProcess.execFile('ls', [req.query.dir], function (e) { + childProcess.execFile(req.query.command, function (e) { if (e?.name === 'DatadogRaspAbortError') { res.writeHead(500) } @@ -281,7 +272,7 @@ describe('RASP - command_injection', () => { } }) - testSafeInNonShell() + testNonShellBlockingAndSafeRequests() }) describe('with promise', () => { @@ -291,7 +282,7 @@ describe('RASP - command_injection', () => { const execFile = util.promisify(require('child_process').execFile) try { - await execFile('ls', [req.query.dir]) + await execFile([req.query.command]) } catch (e) { if (e.name === 'DatadogRaspAbortError') { res.writeHead(500) @@ -302,15 +293,14 @@ describe('RASP - command_injection', () => { } }) - testSafeInNonShell() + testNonShellBlockingAndSafeRequests() }) describe('with event emitter', () => { beforeEach(() => { app = (req, res) => { const childProcess = require('child_process') - - const child = childProcess.execFile('ls', [req.query.dir]) + const child = childProcess.execFile(req.query.command) child.on('error', (e) => { if (e.name === 'DatadogRaspAbortError') { res.writeHead(500) @@ -323,7 +313,7 @@ describe('RASP - command_injection', () => { } }) - testSafeInNonShell() + testNonShellBlockingAndSafeRequests() }) describe('execFileSync', () => { @@ -332,7 +322,7 @@ describe('RASP - command_injection', () => { const childProcess = require('child_process') try { - childProcess.execFileSync('ls', [req.query.dir]) + childProcess.execFileSync([req.query.command]) } catch (e) { if (e.name === 'DatadogRaspAbortError') { res.writeHead(500) @@ -343,7 +333,7 @@ describe('RASP - command_injection', () => { } }) - testSafeInNonShell() + testNonShellBlockingAndSafeRequests() }) }) }) @@ -368,7 +358,7 @@ describe('RASP - command_injection', () => { } }) - testBlockingAndSafeRequests() + testShellBlockingAndSafeRequests() }) describe('spawnSync', () => { @@ -385,7 +375,7 @@ describe('RASP - command_injection', () => { } }) - testBlockingAndSafeRequests() + testShellBlockingAndSafeRequests() }) }) @@ -395,7 +385,7 @@ describe('RASP - command_injection', () => { app = (req, res) => { const childProcess = require('child_process') - const child = childProcess.spawn('ls', [req.query.dir]) + const child = childProcess.spawn(req.query.command) child.on('error', (e) => { if (e.name === 'DatadogRaspAbortError') { res.writeHead(500) @@ -408,7 +398,7 @@ describe('RASP - command_injection', () => { } }) - testSafeInNonShell() + testNonShellBlockingAndSafeRequests() }) describe('spawnSync', () => { @@ -416,7 +406,7 @@ describe('RASP - command_injection', () => { app = (req, res) => { const childProcess = require('child_process') - const child = childProcess.spawnSync('ls', [req.query.dir]) + const child = childProcess.spawnSync(req.query.command) if (child.error?.name === 'DatadogRaspAbortError') { res.writeHead(500) } @@ -425,7 +415,7 @@ describe('RASP - command_injection', () => { } }) - testSafeInNonShell() + testNonShellBlockingAndSafeRequests() }) }) }) diff --git a/packages/dd-trace/test/appsec/rasp/command_injection.integration.spec.js b/packages/dd-trace/test/appsec/rasp/command_injection.integration.spec.js index 4ebb8c4910a..d6fe4015202 100644 --- a/packages/dd-trace/test/appsec/rasp/command_injection.integration.spec.js +++ b/packages/dd-trace/test/appsec/rasp/command_injection.integration.spec.js @@ -42,6 +42,7 @@ describe('RASP - command_injection - integration', () => { APP_PORT: appPort, DD_APPSEC_ENABLED: 'true', DD_APPSEC_RASP_ENABLED: 'true', + DD_TELEMETRY_HEARTBEAT_INTERVAL: 1, DD_APPSEC_RULES: path.join(cwd, 'resources', 'rasp_rules.json') } }) @@ -52,7 +53,7 @@ describe('RASP - command_injection - integration', () => { await agent.stop() }) - async function testRequestBlocked (url) { + async function testRequestBlocked (url, ruleId = 3, variant = 'shell') { try { await axios.get(url) } catch (e) { @@ -61,28 +62,72 @@ describe('RASP - command_injection - integration', () => { } assert.strictEqual(e.response.status, 403) - return await agent.assertMessageReceived(({ headers, payload }) => { + + let appsecTelemetryReceived = false + + const checkMessages = await agent.assertMessageReceived(({ headers, payload }) => { assert.property(payload[0][0].meta, '_dd.appsec.json') - assert.include(payload[0][0].meta['_dd.appsec.json'], '"rasp-command_injection-rule-id-3"') + assert.include(payload[0][0].meta['_dd.appsec.json'], `"rasp-command_injection-rule-id-${ruleId}"`) }) + + const checkTelemetry = await agent.assertTelemetryReceived(({ headers, payload }) => { + const namespace = payload.payload.namespace + + // Only check telemetry received in appsec namespace and ignore others + if (namespace === 'appsec') { + appsecTelemetryReceived = true + const series = payload.payload.series + const evalSerie = series.find(s => s.metric === 'rasp.rule.eval') + const matchSerie = series.find(s => s.metric === 'rasp.rule.match') + + assert.exists(evalSerie, 'eval serie should exist') + assert.include(evalSerie.tags, 'rule_type:command_injection') + assert.include(evalSerie.tags, `rule_variant:${variant}`) + assert.strictEqual(evalSerie.type, 'count') + + assert.exists(matchSerie, 'match serie should exist') + assert.include(matchSerie.tags, 'rule_type:command_injection') + assert.include(matchSerie.tags, `rule_variant:${variant}`) + assert.strictEqual(matchSerie.type, 'count') + } + }, 30_000, 'generate-metrics', 2) + + const checks = await Promise.all([checkMessages, checkTelemetry]) + assert.equal(appsecTelemetryReceived, true) + + return checks } throw new Error('Request should be blocked') } - it('should block using execFileSync and exception handled by express', async () => { - await testRequestBlocked('/shi/execFileSync?dir=$(cat /etc/passwd 1>%262 ; echo .)') - }) + describe('with shell', () => { + it('should block using execFileSync and exception handled by express', async () => { + await testRequestBlocked('/shi/execFileSync?dir=$(cat /etc/passwd 1>%262 ; echo .)') + }) - it('should block using execFileSync and unhandled exception', async () => { - await testRequestBlocked('/shi/execFileSync/out-of-express-scope?dir=$(cat /etc/passwd 1>%262 ; echo .)') - }) + it('should block using execFileSync and unhandled exception', async () => { + await testRequestBlocked('/shi/execFileSync/out-of-express-scope?dir=$(cat /etc/passwd 1>%262 ; echo .)') + }) + + it('should block using execSync and exception handled by express', async () => { + await testRequestBlocked('/shi/execSync?dir=$(cat /etc/passwd 1>%262 ; echo .)') + }) - it('should block using execSync and exception handled by express', async () => { - await testRequestBlocked('/shi/execSync?dir=$(cat /etc/passwd 1>%262 ; echo .)') + it('should block using execSync and unhandled exception', async () => { + await testRequestBlocked('/shi/execSync/out-of-express-scope?dir=$(cat /etc/passwd 1>%262 ; echo .)') + }) }) - it('should block using execSync and unhandled exception', async () => { - await testRequestBlocked('/shi/execSync/out-of-express-scope?dir=$(cat /etc/passwd 1>%262 ; echo .)') + describe('without shell', () => { + it('should block using execFileSync and exception handled by express', async () => { + await testRequestBlocked('/cmdi/execFileSync?command=cat /etc/passwd 1>&2 ; echo .', 4, 'exec') + }) + + it('should block using execFileSync and unhandled exception', async () => { + await testRequestBlocked( + '/cmdi/execFileSync/out-of-express-scope?command=cat /etc/passwd 1>&2 ; echo .', 4, 'exec' + ) + }) }) }) diff --git a/packages/dd-trace/test/appsec/rasp/command_injection.spec.js b/packages/dd-trace/test/appsec/rasp/command_injection.spec.js index 785b155a113..bf920940c7a 100644 --- a/packages/dd-trace/test/appsec/rasp/command_injection.spec.js +++ b/packages/dd-trace/test/appsec/rasp/command_injection.spec.js @@ -49,49 +49,6 @@ describe('RASP - command_injection.js', () => { }) describe('analyzeCommandInjection', () => { - it('should analyze command_injection without arguments', () => { - const ctx = { - file: 'cmd', - shell: true - } - const req = {} - datadogCore.storage.getStore.returns({ req }) - - start.publish(ctx) - - const persistent = { [addresses.SHELL_COMMAND]: 'cmd' } - sinon.assert.calledOnceWithExactly(waf.run, { persistent }, req, 'command_injection') - }) - - it('should analyze command_injection with arguments', () => { - const ctx = { - file: 'cmd', - fileArgs: ['arg0', 'arg1'], - shell: true - } - const req = {} - datadogCore.storage.getStore.returns({ req }) - - start.publish(ctx) - - const persistent = { [addresses.SHELL_COMMAND]: ['cmd', 'arg0', 'arg1'] } - sinon.assert.calledOnceWithExactly(waf.run, { persistent }, req, 'command_injection') - }) - - it('should not analyze command_injection when it is not shell', () => { - const ctx = { - file: 'cmd', - fileArgs: ['arg0', 'arg1'], - shell: false - } - const req = {} - datadogCore.storage.getStore.returns({ req }) - - start.publish(ctx) - - sinon.assert.notCalled(waf.run) - }) - it('should not analyze command_injection if rasp is disabled', () => { commandInjection.disable() const ctx = { @@ -139,18 +96,102 @@ describe('RASP - command_injection.js', () => { sinon.assert.notCalled(waf.run) }) - it('should call handleResult', () => { - const abortController = { abort: 'abort' } - const ctx = { file: 'cmd', abortController, shell: true } - const wafResult = { waf: 'waf' } - const req = { req: 'req' } - const res = { res: 'res' } - waf.run.returns(wafResult) - datadogCore.storage.getStore.returns({ req, res }) - - start.publish(ctx) + describe('command_injection with shell', () => { + it('should analyze command_injection without arguments', () => { + const ctx = { + file: 'cmd', + shell: true + } + const req = {} + datadogCore.storage.getStore.returns({ req }) + + start.publish(ctx) + + const persistent = { [addresses.SHELL_COMMAND]: 'cmd' } + sinon.assert.calledOnceWithExactly( + waf.run, { persistent }, req, { type: 'command_injection', variant: 'shell' } + ) + }) + + it('should analyze command_injection with arguments', () => { + const ctx = { + file: 'cmd', + fileArgs: ['arg0', 'arg1'], + shell: true + } + const req = {} + datadogCore.storage.getStore.returns({ req }) + + start.publish(ctx) + + const persistent = { [addresses.SHELL_COMMAND]: ['cmd', 'arg0', 'arg1'] } + sinon.assert.calledOnceWithExactly( + waf.run, { persistent }, req, { type: 'command_injection', variant: 'shell' } + ) + }) + + it('should call handleResult', () => { + const abortController = { abort: 'abort' } + const ctx = { file: 'cmd', abortController, shell: true } + const wafResult = { waf: 'waf' } + const req = { req: 'req' } + const res = { res: 'res' } + waf.run.returns(wafResult) + datadogCore.storage.getStore.returns({ req, res }) + + start.publish(ctx) + + sinon.assert.calledOnceWithExactly(utils.handleResult, wafResult, req, res, abortController, config) + }) + }) - sinon.assert.calledOnceWithExactly(utils.handleResult, wafResult, req, res, abortController, config) + describe('command_injection without shell', () => { + it('should analyze command injection without arguments', () => { + const ctx = { + file: 'ls', + shell: false + } + const req = {} + datadogCore.storage.getStore.returns({ req }) + + start.publish(ctx) + + const persistent = { [addresses.EXEC_COMMAND]: ['ls'] } + sinon.assert.calledOnceWithExactly( + waf.run, { persistent }, req, { type: 'command_injection', variant: 'exec' } + ) + }) + + it('should analyze command injection with arguments', () => { + const ctx = { + file: 'ls', + fileArgs: ['-la', '/tmp'], + shell: false + } + const req = {} + datadogCore.storage.getStore.returns({ req }) + + start.publish(ctx) + + const persistent = { [addresses.EXEC_COMMAND]: ['ls', '-la', '/tmp'] } + sinon.assert.calledOnceWithExactly( + waf.run, { persistent }, req, { type: 'command_injection', variant: 'exec' } + ) + }) + + it('should call handleResult', () => { + const abortController = { abort: 'abort' } + const ctx = { file: 'cmd', abortController, shell: false } + const wafResult = { waf: 'waf' } + const req = { req: 'req' } + const res = { res: 'res' } + waf.run.returns(wafResult) + datadogCore.storage.getStore.returns({ req, res }) + + start.publish(ctx) + + sinon.assert.calledOnceWithExactly(utils.handleResult, wafResult, req, res, abortController, config) + }) }) }) }) diff --git a/packages/dd-trace/test/appsec/rasp/lfi.spec.js b/packages/dd-trace/test/appsec/rasp/lfi.spec.js index 405311ae0d3..0a1328e2c52 100644 --- a/packages/dd-trace/test/appsec/rasp/lfi.spec.js +++ b/packages/dd-trace/test/appsec/rasp/lfi.spec.js @@ -111,7 +111,7 @@ describe('RASP - lfi.js', () => { fsOperationStart.publish(ctx) const persistent = { [FS_OPERATION_PATH]: path } - sinon.assert.calledOnceWithExactly(waf.run, { persistent }, req, 'lfi') + sinon.assert.calledOnceWithExactly(waf.run, { persistent }, req, { type: 'lfi' }) }) it('should NOT analyze lfi for child fs operations', () => { diff --git a/packages/dd-trace/test/appsec/rasp/resources/rasp_rules.json b/packages/dd-trace/test/appsec/rasp/resources/rasp_rules.json index daca47d8d20..c0396bd9871 100644 --- a/packages/dd-trace/test/appsec/rasp/resources/rasp_rules.json +++ b/packages/dd-trace/test/appsec/rasp/resources/rasp_rules.json @@ -110,7 +110,7 @@ }, { "id": "rasp-command_injection-rule-id-3", - "name": "Command injection exploit", + "name": "Shell command injection exploit", "tags": { "type": "command_injection", "category": "vulnerability_trigger", @@ -156,6 +156,55 @@ "block", "stack_trace" ] + }, + { + "id": "rasp-command_injection-rule-id-4", + "name": "OS command injection exploit", + "tags": { + "type": "command_injection", + "category": "vulnerability_trigger", + "cwe": "77", + "capec": "1000/152/248/88", + "confidence": "0", + "module": "rasp" + }, + "conditions": [ + { + "parameters": { + "resource": [ + { + "address": "server.sys.exec.cmd" + } + ], + "params": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ] + }, + "operator": "cmdi_detector" + } + ], + "transformers": [], + "on_match": [ + "block", + "stack_trace" + ] } ] } diff --git a/packages/dd-trace/test/appsec/rasp/resources/shi-app/index.js b/packages/dd-trace/test/appsec/rasp/resources/shi-app/index.js index a6714bd2148..133c57dfb2b 100644 --- a/packages/dd-trace/test/appsec/rasp/resources/shi-app/index.js +++ b/packages/dd-trace/test/appsec/rasp/resources/shi-app/index.js @@ -39,6 +39,20 @@ app.get('/shi/execSync/out-of-express-scope', async (req, res) => { }) }) +app.get('/cmdi/execFileSync', async (req, res) => { + childProcess.execFileSync('sh', ['-c', req.query.command]) + + res.end('OK') +}) + +app.get('/cmdi/execFileSync/out-of-express-scope', async (req, res) => { + process.nextTick(() => { + childProcess.execFileSync('sh', ['-c', req.query.command]) + + res.end('OK') + }) +}) + app.listen(port, () => { process.send({ port }) }) diff --git a/packages/dd-trace/test/appsec/rasp/sql_injection.pg.plugin.spec.js b/packages/dd-trace/test/appsec/rasp/sql_injection.pg.plugin.spec.js index 8f05158c22d..2d4dd779c17 100644 --- a/packages/dd-trace/test/appsec/rasp/sql_injection.pg.plugin.spec.js +++ b/packages/dd-trace/test/appsec/rasp/sql_injection.pg.plugin.spec.js @@ -219,7 +219,7 @@ describe('RASP - sql_injection', () => { await axios.get('/') - assert.equal(run.args.filter(arg => arg[1] === 'sql_injection').length, 1) + assert.equal(run.args.filter(arg => arg[1]?.type === 'sql_injection').length, 1) }) it('should call to waf twice for sql injection with two different queries in pg Pool', async () => { @@ -232,7 +232,7 @@ describe('RASP - sql_injection', () => { await axios.get('/') - assert.equal(run.args.filter(arg => arg[1] === 'sql_injection').length, 2) + assert.equal(run.args.filter(arg => arg[1]?.type === 'sql_injection').length, 2) }) it('should call to waf twice for sql injection and same query when input address is updated', async () => { @@ -254,7 +254,7 @@ describe('RASP - sql_injection', () => { await axios.get('/') - assert.equal(run.args.filter(arg => arg[1] === 'sql_injection').length, 2) + assert.equal(run.args.filter(arg => arg[1]?.type === 'sql_injection').length, 2) }) it('should call to waf once for sql injection and same query when input address is updated', async () => { @@ -276,7 +276,7 @@ describe('RASP - sql_injection', () => { await axios.get('/') - assert.equal(run.args.filter(arg => arg[1] === 'sql_injection').length, 1) + assert.equal(run.args.filter(arg => arg[1]?.type === 'sql_injection').length, 1) }) }) }) diff --git a/packages/dd-trace/test/appsec/rasp/sql_injection.spec.js b/packages/dd-trace/test/appsec/rasp/sql_injection.spec.js index d713521e986..fe7c9af082d 100644 --- a/packages/dd-trace/test/appsec/rasp/sql_injection.spec.js +++ b/packages/dd-trace/test/appsec/rasp/sql_injection.spec.js @@ -57,7 +57,7 @@ describe('RASP - sql_injection', () => { [addresses.DB_STATEMENT]: 'SELECT 1', [addresses.DB_SYSTEM]: 'postgresql' } - sinon.assert.calledOnceWithExactly(waf.run, { persistent }, req, 'sql_injection') + sinon.assert.calledOnceWithExactly(waf.run, { persistent }, req, { type: 'sql_injection' }) }) it('should not analyze sql injection if rasp is disabled', () => { @@ -128,7 +128,7 @@ describe('RASP - sql_injection', () => { [addresses.DB_STATEMENT]: 'SELECT 1', [addresses.DB_SYSTEM]: 'mysql' } - sinon.assert.calledOnceWithExactly(waf.run, { persistent }, req, 'sql_injection') + sinon.assert.calledOnceWithExactly(waf.run, { persistent }, req, { type: 'sql_injection' }) }) it('should not analyze sql injection if rasp is disabled', () => { diff --git a/packages/dd-trace/test/appsec/rasp/ssrf.spec.js b/packages/dd-trace/test/appsec/rasp/ssrf.spec.js index c40867ea254..98d5c8a0104 100644 --- a/packages/dd-trace/test/appsec/rasp/ssrf.spec.js +++ b/packages/dd-trace/test/appsec/rasp/ssrf.spec.js @@ -54,7 +54,7 @@ describe('RASP - ssrf.js', () => { httpClientRequestStart.publish(ctx) const persistent = { [addresses.HTTP_OUTGOING_URL]: 'http://example.com' } - sinon.assert.calledOnceWithExactly(waf.run, { persistent }, req, 'ssrf') + sinon.assert.calledOnceWithExactly(waf.run, { persistent }, req, { type: 'ssrf' }) }) it('should not analyze ssrf if rasp is disabled', () => { diff --git a/packages/dd-trace/test/appsec/remote_config/index.spec.js b/packages/dd-trace/test/appsec/remote_config/index.spec.js index f3cc6a32dac..4d296d100d1 100644 --- a/packages/dd-trace/test/appsec/remote_config/index.spec.js +++ b/packages/dd-trace/test/appsec/remote_config/index.spec.js @@ -244,6 +244,8 @@ describe('Remote Config index', () => { .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_RASP_LFI, true) expect(rc.updateCapabilities) .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_RASP_SHI, true) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_RASP_CMDI, true) expect(rc.setProductHandler).to.have.been.calledWith('ASM_DATA') expect(rc.setProductHandler).to.have.been.calledWith('ASM_DD') @@ -288,6 +290,8 @@ describe('Remote Config index', () => { .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_RASP_LFI, true) expect(rc.updateCapabilities) .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_RASP_SHI, true) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_RASP_CMDI, true) expect(rc.setProductHandler).to.have.been.calledWith('ASM_DATA') expect(rc.setProductHandler).to.have.been.calledWith('ASM_DD') @@ -334,6 +338,8 @@ describe('Remote Config index', () => { .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_RASP_LFI, true) expect(rc.updateCapabilities) .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_RASP_SHI, true) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_RASP_CMDI, true) }) it('should not activate rasp capabilities if rasp is disabled', () => { @@ -375,6 +381,8 @@ describe('Remote Config index', () => { .to.not.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_RASP_LFI) expect(rc.updateCapabilities) .to.not.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_RASP_SHI) + expect(rc.updateCapabilities) + .to.not.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_RASP_CMDI) }) }) @@ -416,6 +424,8 @@ describe('Remote Config index', () => { .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_RASP_LFI, false) expect(rc.updateCapabilities) .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_RASP_SHI, false) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_RASP_CMDI, false) expect(rc.removeProductHandler).to.have.been.calledWith('ASM_DATA') expect(rc.removeProductHandler).to.have.been.calledWith('ASM_DD') diff --git a/packages/dd-trace/test/appsec/remote_config/manager.spec.js b/packages/dd-trace/test/appsec/remote_config/manager.spec.js index 2a32e834e06..8d0c82d0dc9 100644 --- a/packages/dd-trace/test/appsec/remote_config/manager.spec.js +++ b/packages/dd-trace/test/appsec/remote_config/manager.spec.js @@ -98,7 +98,8 @@ describe('RemoteConfigManager', () => { service: config.service, env: config.env, app_version: config.version, - extra_services: [] + extra_services: [], + tags: ['runtime-id:runtimeId'] }, capabilities: 'AA==' }, @@ -108,6 +109,20 @@ describe('RemoteConfigManager', () => { expect(rc.appliedConfigs).to.be.an.instanceOf(Map) }) + it('should add git metadata to tags if present', () => { + const configWithGit = { + ...config, + repositoryUrl: 'https://github.com/DataDog/dd-trace-js', + commitSHA: '1234567890' + } + const rc = new RemoteConfigManager(configWithGit) + expect(rc.state.client.client_tracer.tags).to.deep.equal([ + 'runtime-id:runtimeId', + 'git.repository_url:https://github.com/DataDog/dd-trace-js', + 'git.commit.sha:1234567890' + ]) + }) + describe('updateCapabilities', () => { it('should set multiple capabilities to true', () => { rc.updateCapabilities(Capabilities.ASM_ACTIVATION, true) diff --git a/packages/dd-trace/test/appsec/reporter.spec.js b/packages/dd-trace/test/appsec/reporter.spec.js index cd7cc9a1581..a38092e728d 100644 --- a/packages/dd-trace/test/appsec/reporter.spec.js +++ b/packages/dd-trace/test/appsec/reporter.spec.js @@ -192,13 +192,15 @@ describe('reporter', () => { expect(telemetry.updateRaspRequestsMetricTags).to.not.have.been.called }) - it('should call updateRaspRequestsMetricTags when ruleType if provided', () => { + it('should call updateRaspRequestsMetricTags when raspRule is provided', () => { const metrics = { rulesVersion: '1.2.3' } const store = storage.getStore() - Reporter.reportMetrics(metrics, 'rule_type') + const raspRule = { type: 'rule_type', variant: 'rule_variant' } - expect(telemetry.updateRaspRequestsMetricTags).to.have.been.calledOnceWithExactly(metrics, store.req, 'rule_type') + Reporter.reportMetrics(metrics, raspRule) + + expect(telemetry.updateRaspRequestsMetricTags).to.have.been.calledOnceWithExactly(metrics, store.req, raspRule) expect(telemetry.updateWafRequestsMetricTags).to.not.have.been.called }) }) diff --git a/packages/dd-trace/test/ci-visibility/dynamic-instrumentation/target-app/test-visibility-dynamic-instrumentation-script.js b/packages/dd-trace/test/ci-visibility/dynamic-instrumentation/target-app/test-visibility-dynamic-instrumentation-script.js index fedfaefdc6c..39382ea0089 100644 --- a/packages/dd-trace/test/ci-visibility/dynamic-instrumentation/target-app/test-visibility-dynamic-instrumentation-script.js +++ b/packages/dd-trace/test/ci-visibility/dynamic-instrumentation/target-app/test-visibility-dynamic-instrumentation-script.js @@ -3,11 +3,12 @@ const path = require('path') const tvDynamicInstrumentation = require('../../../../src/ci-visibility/dynamic-instrumentation') const sum = require('./di-dependency') +const Config = require('../../../../src/config') // keep process alive const intervalId = setInterval(() => {}, 5000) -tvDynamicInstrumentation.start() +tvDynamicInstrumentation.start(new Config()) tvDynamicInstrumentation.isReady().then(() => { const [ diff --git a/packages/dd-trace/test/config.spec.js b/packages/dd-trace/test/config.spec.js index ca1a8bcb575..49e691afae8 100644 --- a/packages/dd-trace/test/config.spec.js +++ b/packages/dd-trace/test/config.spec.js @@ -232,6 +232,8 @@ describe('Config', () => { expect(config).to.have.property('logLevel', 'debug') expect(config).to.have.nested.property('codeOriginForSpans.enabled', false) expect(config).to.have.property('dynamicInstrumentationEnabled', false) + expect(config).to.have.deep.property('dynamicInstrumentationRedactedIdentifiers', []) + expect(config).to.have.deep.property('dynamicInstrumentationRedactionExcludedIdentifiers', []) expect(config).to.have.property('traceId128BitGenerationEnabled', true) expect(config).to.have.property('traceId128BitLoggingEnabled', false) expect(config).to.have.property('spanAttributeSchema', 'v0') @@ -314,6 +316,8 @@ describe('Config', () => { { name: 'dogstatsd.port', value: '8125', origin: 'default' }, { name: 'dsmEnabled', value: false, origin: 'default' }, { name: 'dynamicInstrumentationEnabled', value: false, origin: 'default' }, + { name: 'dynamicInstrumentationRedactedIdentifiers', value: [], origin: 'default' }, + { name: 'dynamicInstrumentationRedactionExcludedIdentifiers', value: [], origin: 'default' }, { name: 'env', value: undefined, origin: 'default' }, { name: 'experimental.enableGetRumData', value: false, origin: 'default' }, { name: 'experimental.exporter', value: undefined, origin: 'default' }, @@ -324,6 +328,7 @@ describe('Config', () => { { name: 'headerTags', value: [], origin: 'default' }, { name: 'hostname', value: '127.0.0.1', origin: 'default' }, { name: 'iast.cookieFilterPattern', value: '.{32,}', origin: 'default' }, + { name: 'iast.dbRowsToTaint', value: 1, origin: 'default' }, { name: 'iast.deduplicationEnabled', value: true, origin: 'default' }, { name: 'iast.enabled', value: false, origin: 'default' }, { name: 'iast.maxConcurrentRequests', value: 2, origin: 'default' }, @@ -456,6 +461,8 @@ describe('Config', () => { process.env.DD_TRACE_REPORT_HOSTNAME = 'true' process.env.DD_ENV = 'test' process.env.DD_DYNAMIC_INSTRUMENTATION_ENABLED = 'true' + process.env.DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS = 'foo,bar' + process.env.DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS = 'a,b,c' process.env.DD_TRACE_GLOBAL_TAGS = 'foo:bar,baz:qux' process.env.DD_TRACE_SAMPLE_RATE = '0.5' process.env.DD_TRACE_RATE_LIMIT = '-1' @@ -504,6 +511,7 @@ describe('Config', () => { process.env.DD_IAST_MAX_CONCURRENT_REQUESTS = '3' process.env.DD_IAST_MAX_CONTEXT_OPERATIONS = '4' process.env.DD_IAST_COOKIE_FILTER_PATTERN = '.*' + process.env.DD_IAST_DB_ROWS_TO_TAINT = 2 process.env.DD_IAST_DEDUPLICATION_ENABLED = false process.env.DD_IAST_REDACTION_ENABLED = false process.env.DD_IAST_REDACTION_NAME_PATTERN = 'REDACTION_NAME_PATTERN' @@ -550,6 +558,8 @@ describe('Config', () => { expect(config).to.have.property('reportHostname', true) expect(config).to.have.nested.property('codeOriginForSpans.enabled', true) expect(config).to.have.property('dynamicInstrumentationEnabled', true) + expect(config).to.have.deep.property('dynamicInstrumentationRedactedIdentifiers', ['foo', 'bar']) + expect(config).to.have.deep.property('dynamicInstrumentationRedactionExcludedIdentifiers', ['a', 'b', 'c']) expect(config).to.have.property('env', 'test') expect(config).to.have.property('sampleRate', 0.5) expect(config).to.have.property('traceEnabled', true) @@ -615,6 +625,7 @@ describe('Config', () => { expect(config).to.have.nested.property('iast.maxConcurrentRequests', 3) expect(config).to.have.nested.property('iast.maxContextOperations', 4) expect(config).to.have.nested.property('iast.cookieFilterPattern', '.*') + expect(config).to.have.nested.property('iast.dbRowsToTaint', 2) expect(config).to.have.nested.property('iast.deduplicationEnabled', false) expect(config).to.have.nested.property('iast.redactionEnabled', false) expect(config).to.have.nested.property('iast.redactionNamePattern', 'REDACTION_NAME_PATTERN') @@ -653,12 +664,15 @@ describe('Config', () => { { name: 'dogstatsd.hostname', value: 'dsd-agent', origin: 'env_var' }, { name: 'dogstatsd.port', value: '5218', origin: 'env_var' }, { name: 'dynamicInstrumentationEnabled', value: true, origin: 'env_var' }, + { name: 'dynamicInstrumentationRedactedIdentifiers', value: ['foo', 'bar'], origin: 'env_var' }, + { name: 'dynamicInstrumentationRedactionExcludedIdentifiers', value: ['a', 'b', 'c'], origin: 'env_var' }, { name: 'env', value: 'test', origin: 'env_var' }, { name: 'experimental.enableGetRumData', value: true, origin: 'env_var' }, { name: 'experimental.exporter', value: 'log', origin: 'env_var' }, { name: 'experimental.runtimeId', value: true, origin: 'env_var' }, { name: 'hostname', value: 'agent', origin: 'env_var' }, { name: 'iast.cookieFilterPattern', value: '.*', origin: 'env_var' }, + { name: 'iast.dbRowsToTaint', value: 2, origin: 'env_var' }, { name: 'iast.deduplicationEnabled', value: false, origin: 'env_var' }, { name: 'iast.enabled', value: true, origin: 'env_var' }, { name: 'iast.maxConcurrentRequests', value: '3', origin: 'env_var' }, @@ -847,6 +861,8 @@ describe('Config', () => { experimental: { b3: true, dynamicInstrumentationEnabled: true, + dynamicInstrumentationRedactedIdentifiers: ['foo', 'bar'], + dynamicInstrumentationRedactionExcludedIdentifiers: ['a', 'b', 'c'], traceparent: true, runtimeId: true, exporter: 'log', @@ -857,6 +873,7 @@ describe('Config', () => { maxConcurrentRequests: 4, maxContextOperations: 5, cookieFilterPattern: '.*', + dbRowsToTaint: 2, deduplicationEnabled: false, redactionEnabled: false, redactionNamePattern: 'REDACTION_NAME_PATTERN', @@ -891,6 +908,8 @@ describe('Config', () => { expect(config).to.have.property('service', 'service') expect(config).to.have.property('version', '0.1.0') expect(config).to.have.property('dynamicInstrumentationEnabled', true) + expect(config).to.have.deep.property('dynamicInstrumentationRedactedIdentifiers', ['foo', 'bar']) + expect(config).to.have.deep.property('dynamicInstrumentationRedactionExcludedIdentifiers', ['a', 'b', 'c']) expect(config).to.have.property('env', 'test') expect(config).to.have.property('sampleRate', 0.5) expect(config).to.have.property('logger', logger) @@ -929,6 +948,7 @@ describe('Config', () => { expect(config).to.have.nested.property('iast.maxConcurrentRequests', 4) expect(config).to.have.nested.property('iast.maxContextOperations', 5) expect(config).to.have.nested.property('iast.cookieFilterPattern', '.*') + expect(config).to.have.nested.property('iast.dbRowsToTaint', 2) expect(config).to.have.nested.property('iast.deduplicationEnabled', false) expect(config).to.have.nested.property('iast.redactionEnabled', false) expect(config).to.have.nested.property('iast.redactionNamePattern', 'REDACTION_NAME_PATTERN') @@ -968,6 +988,8 @@ describe('Config', () => { { name: 'dogstatsd.hostname', value: 'agent-dsd', origin: 'code' }, { name: 'dogstatsd.port', value: '5218', origin: 'code' }, { name: 'dynamicInstrumentationEnabled', value: true, origin: 'code' }, + { name: 'dynamicInstrumentationRedactedIdentifiers', value: ['foo', 'bar'], origin: 'code' }, + { name: 'dynamicInstrumentationRedactionExcludedIdentifiers', value: ['a', 'b', 'c'], origin: 'code' }, { name: 'env', value: 'test', origin: 'code' }, { name: 'experimental.enableGetRumData', value: true, origin: 'code' }, { name: 'experimental.exporter', value: 'log', origin: 'code' }, @@ -976,6 +998,7 @@ describe('Config', () => { { name: 'flushMinSpans', value: 500, origin: 'code' }, { name: 'hostname', value: 'agent', origin: 'code' }, { name: 'iast.cookieFilterPattern', value: '.*', origin: 'code' }, + { name: 'iast.dbRowsToTaint', value: 2, origin: 'code' }, { name: 'iast.deduplicationEnabled', value: false, origin: 'code' }, { name: 'iast.enabled', value: true, origin: 'code' }, { name: 'iast.maxConcurrentRequests', value: 4, origin: 'code' }, @@ -1168,6 +1191,8 @@ describe('Config', () => { process.env.DD_TRACE_REPORT_HOSTNAME = 'true' process.env.DD_ENV = 'test' process.env.DD_DYNAMIC_INSTRUMENTATION_ENABLED = 'true' + process.env.DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS = 'foo,bar' + process.env.DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS = 'a,b,c' process.env.DD_API_KEY = '123' process.env.DD_TRACE_SPAN_ATTRIBUTE_SCHEMA = 'v0' process.env.DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED = 'false' @@ -1201,6 +1226,7 @@ describe('Config', () => { process.env.DD_API_SECURITY_ENABLED = 'false' process.env.DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS = 11 process.env.DD_IAST_ENABLED = 'false' + process.env.DD_IAST_DB_ROWS_TO_TAINT = '2' process.env.DD_IAST_COOKIE_FILTER_PATTERN = '.*' process.env.DD_IAST_REDACTION_NAME_PATTERN = 'name_pattern_to_be_overriden_by_options' process.env.DD_IAST_REDACTION_VALUE_PATTERN = 'value_pattern_to_be_overriden_by_options' @@ -1245,6 +1271,8 @@ describe('Config', () => { experimental: { b3: false, dynamicInstrumentationEnabled: false, + dynamicInstrumentationRedactedIdentifiers: ['foo2', 'bar2'], + dynamicInstrumentationRedactionExcludedIdentifiers: ['a2', 'b2'], traceparent: false, runtimeId: false, exporter: 'agent', @@ -1278,6 +1306,7 @@ describe('Config', () => { iast: { enabled: true, cookieFilterPattern: '.{10,}', + dbRowsToTaint: 3, redactionNamePattern: 'REDACTION_NAME_PATTERN', redactionValuePattern: 'REDACTION_VALUE_PATTERN' }, @@ -1309,6 +1338,8 @@ describe('Config', () => { expect(config).to.have.property('version', '1.0.0') expect(config).to.have.nested.property('codeOriginForSpans.enabled', false) expect(config).to.have.property('dynamicInstrumentationEnabled', false) + expect(config).to.have.deep.property('dynamicInstrumentationRedactedIdentifiers', ['foo2', 'bar2']) + expect(config).to.have.deep.property('dynamicInstrumentationRedactionExcludedIdentifiers', ['a2', 'b2']) expect(config).to.have.property('env', 'development') expect(config).to.have.property('clientIpEnabled', true) expect(config).to.have.property('clientIpHeader', 'x-true-client-ip') @@ -1346,6 +1377,7 @@ describe('Config', () => { expect(config).to.have.nested.property('iast.requestSampling', 30) expect(config).to.have.nested.property('iast.maxConcurrentRequests', 2) expect(config).to.have.nested.property('iast.maxContextOperations', 2) + expect(config).to.have.nested.property('iast.dbRowsToTaint', 3) expect(config).to.have.nested.property('iast.deduplicationEnabled', true) expect(config).to.have.nested.property('iast.cookieFilterPattern', '.{10,}') expect(config).to.have.nested.property('iast.redactionEnabled', true) @@ -1383,6 +1415,7 @@ describe('Config', () => { maxConcurrentRequests: 3, maxContextOperations: 4, cookieFilterPattern: '.*', + dbRowsToTaint: 3, deduplicationEnabled: false, redactionEnabled: false, redactionNamePattern: 'REDACTION_NAME_PATTERN', @@ -1416,6 +1449,7 @@ describe('Config', () => { maxConcurrentRequests: 6, maxContextOperations: 7, cookieFilterPattern: '.{10,}', + dbRowsToTaint: 2, deduplicationEnabled: true, redactionEnabled: true, redactionNamePattern: 'IGNORED_REDACTION_NAME_PATTERN', @@ -1464,6 +1498,7 @@ describe('Config', () => { maxConcurrentRequests: 3, maxContextOperations: 4, cookieFilterPattern: '.*', + dbRowsToTaint: 3, deduplicationEnabled: false, redactionEnabled: false, redactionNamePattern: 'REDACTION_NAME_PATTERN', diff --git a/packages/dd-trace/test/debugger/devtools_client/json-buffer.spec.js b/packages/dd-trace/test/debugger/devtools_client/json-buffer.spec.js new file mode 100644 index 00000000000..34312f808dd --- /dev/null +++ b/packages/dd-trace/test/debugger/devtools_client/json-buffer.spec.js @@ -0,0 +1,45 @@ +'use strict' + +require('../../setup/mocha') + +const JSONBuffer = require('../../../src/debugger/devtools_client/json-buffer') + +const MAX_SAFE_SIGNED_INTEGER = 2 ** 31 - 1 + +describe('JSONBuffer', () => { + it('should call onFlush with the expected payload when the timeout is reached', function (done) { + const onFlush = (json) => { + const diff = Date.now() - start + expect(json).to.equal('[{"message":1},{"message":2},{"message":3}]') + expect(diff).to.be.within(95, 110) + done() + } + + const jsonBuffer = new JSONBuffer({ size: Infinity, timeout: 100, onFlush }) + + const start = Date.now() + jsonBuffer.write(JSON.stringify({ message: 1 })) + jsonBuffer.write(JSON.stringify({ message: 2 })) + jsonBuffer.write(JSON.stringify({ message: 3 })) + }) + + it('should call onFlush with the expected payload when the size is reached', function (done) { + const expectedPayloads = [ + '[{"message":1},{"message":2}]', + '[{"message":3},{"message":4}]' + ] + + const onFlush = (json) => { + expect(json).to.equal(expectedPayloads.shift()) + if (expectedPayloads.length === 0) done() + } + + const jsonBuffer = new JSONBuffer({ size: 30, timeout: MAX_SAFE_SIGNED_INTEGER, onFlush }) + + jsonBuffer.write(JSON.stringify({ message: 1 })) // size: 15 + jsonBuffer.write(JSON.stringify({ message: 2 })) // size: 29 + jsonBuffer.write(JSON.stringify({ message: 3 })) // size: 15 (flushed, and re-added) + jsonBuffer.write(JSON.stringify({ message: 4 })) // size: 29 + jsonBuffer.write(JSON.stringify({ message: 5 })) // size: 15 (flushed, and re-added) + }) +}) diff --git a/packages/dd-trace/test/debugger/devtools_client/send.spec.js b/packages/dd-trace/test/debugger/devtools_client/send.spec.js new file mode 100644 index 00000000000..ea4551d8ff6 --- /dev/null +++ b/packages/dd-trace/test/debugger/devtools_client/send.spec.js @@ -0,0 +1,111 @@ +'use strict' + +require('../../setup/mocha') + +const { hostname: getHostname } = require('os') +const { expectWithin, getRequestOptions } = require('./utils') +const JSONBuffer = require('../../../src/debugger/devtools_client/json-buffer') +const { version } = require('../../../../../package.json') + +process.env.DD_ENV = 'my-env' +process.env.DD_VERSION = 'my-version' +const service = 'my-service' +const commitSHA = 'my-commit-sha' +const repositoryUrl = 'my-repository-url' +const url = 'my-url' +const ddsource = 'dd_debugger' +const hostname = getHostname() +const message = { message: true } +const logger = { logger: true } +const dd = { dd: true } +const snapshot = { snapshot: true } + +describe('input message http requests', function () { + let send, request, jsonBuffer + + beforeEach(function () { + request = sinon.spy() + request['@noCallThru'] = true + + class JSONBufferSpy extends JSONBuffer { + constructor (...args) { + super(...args) + jsonBuffer = this + sinon.spy(this, 'write') + } + } + + send = proxyquire('../src/debugger/devtools_client/send', { + './config': { service, commitSHA, repositoryUrl, url, '@noCallThru': true }, + './json-buffer': JSONBufferSpy, + '../../exporters/common/request': request + }) + }) + + it('should buffer instead of calling request directly', function () { + const callback = sinon.spy() + + send(message, logger, dd, snapshot, callback) + expect(request).to.not.have.been.called + expect(jsonBuffer.write).to.have.been.calledOnceWith( + JSON.stringify(getPayload()) + ) + expect(callback).to.not.have.been.called + }) + + it('should call request with the expected payload once the buffer is flushed', function (done) { + const callback1 = sinon.spy() + const callback2 = sinon.spy() + const callback3 = sinon.spy() + + send({ message: 1 }, logger, dd, snapshot, callback1) + send({ message: 2 }, logger, dd, snapshot, callback2) + send({ message: 3 }, logger, dd, snapshot, callback3) + expect(request).to.not.have.been.called + + expectWithin(1200, () => { + expect(request).to.have.been.calledOnceWith(JSON.stringify([ + getPayload({ message: 1 }), + getPayload({ message: 2 }), + getPayload({ message: 3 }) + ])) + + const opts = getRequestOptions(request) + expect(opts).to.have.property('method', 'POST') + expect(opts).to.have.property( + 'path', + '/debugger/v1/input?ddtags=' + + `env%3A${process.env.DD_ENV}%2C` + + `version%3A${process.env.DD_VERSION}%2C` + + `debugger_version%3A${version}%2C` + + `host_name%3A${hostname}%2C` + + `git.commit.sha%3A${commitSHA}%2C` + + `git.repository_url%3A${repositoryUrl}` + ) + + expect(callback1).to.not.have.been.calledOnce + expect(callback2).to.not.have.been.calledOnce + expect(callback3).to.not.have.been.calledOnce + + request.firstCall.callback() + + expect(callback1).to.have.been.calledOnce + expect(callback2).to.have.been.calledOnce + expect(callback3).to.have.been.calledOnce + + done() + }) + }) +}) + +function getPayload (_message = message) { + return { + ddsource, + hostname, + service, + message: _message, + logger, + dd, + 'debugger.snapshot': snapshot + } +} diff --git a/packages/dd-trace/test/debugger/devtools_client/snapshot/redaction.spec.js b/packages/dd-trace/test/debugger/devtools_client/snapshot/redaction.spec.js new file mode 100644 index 00000000000..cd1b4a959a8 --- /dev/null +++ b/packages/dd-trace/test/debugger/devtools_client/snapshot/redaction.spec.js @@ -0,0 +1,90 @@ +'use strict' + +require('../../../setup/mocha') + +const { expect } = require('chai') +const { getTargetCodePath, enable, teardown, assertOnBreakpoint, setAndTriggerBreakpoint } = require('./utils') + +const target = getTargetCodePath(__filename) +const BREAKPOINT_LINE_NUMBER = 32 + +describe('debugger -> devtools client -> snapshot.getLocalStateForCallFrame', function () { + describe('redaction', function () { + beforeEach(enable(__filename)) + + afterEach(teardown) + + // Non-default configuration is tested in the integration tests + it('should replace PII in keys/properties/variables with expected notCapturedReason', function (done) { + assertOnBreakpoint(done, (state) => { + expect(state).to.have.all.keys( + 'nonNormalizedSecretToken', 'foo', 'secret', 'Se_cret_$', 'weakMapKey', 'obj' + ) + + expect(state).to.have.deep.property('foo', { type: 'string', value: 'bar' }) + expect(state).to.have.deep.property('secret', { type: 'string', notCapturedReason: 'redactedIdent' }) + expect(state).to.have.deep.property('Se_cret_$', { type: 'string', notCapturedReason: 'redactedIdent' }) + expect(state).to.have.deep.property('weakMapKey', { + type: 'Object', + fields: { secret: { type: 'string', notCapturedReason: 'redactedIdent' } } + }) + expect(state).to.have.deep.property('obj') + expect(state.obj).to.have.property('type', 'Object') + + const { fields } = state.obj + expect(fields).to.have.all.keys( + 'foo', 'secret', '@Se-cret_$_', 'nested', 'arr', 'map', 'weakmap', 'password', + 'Symbol(secret)', 'Symbol(@Se-cret_$_)' + ) + + expect(fields).to.have.deep.property('foo', { type: 'string', value: 'bar' }) + expect(fields).to.have.deep.property('secret', { type: 'string', notCapturedReason: 'redactedIdent' }) + expect(fields).to.have.deep.property('@Se-cret_$_', { type: 'string', notCapturedReason: 'redactedIdent' }) + expect(fields).to.have.deep.property('nested', { + type: 'Object', + fields: { secret: { type: 'string', notCapturedReason: 'redactedIdent' } } + }) + expect(fields).to.have.deep.property('arr', { + type: 'Array', + elements: [{ type: 'Object', fields: { secret: { type: 'string', notCapturedReason: 'redactedIdent' } } }] + }) + expect(fields).to.have.deep.property('map', { + type: 'Map', + entries: [ + [ + { type: 'string', value: 'foo' }, + { type: 'string', value: 'bar' } + ], + [ + { type: 'string', value: 'secret' }, + { type: 'string', notCapturedReason: 'redactedIdent' } + ], + [ + { type: 'string', value: '@Se-cret_$.' }, + { type: 'string', notCapturedReason: 'redactedIdent' } + ], + [ + { type: 'symbol', value: 'Symbol(secret)' }, + { type: 'string', notCapturedReason: 'redactedIdent' } + ], + [ + { type: 'symbol', value: 'Symbol(@Se-cret_$.)' }, + { notCapturedReason: 'redactedIdent', type: 'string' } + ] + ] + }) + expect(fields).to.have.deep.property('weakmap', { + type: 'WeakMap', + entries: [[ + { type: 'Object', fields: { secret: { type: 'string', notCapturedReason: 'redactedIdent' } } }, + { type: 'number', value: '42' } + ]] + }) + expect(fields).to.have.deep.property('password', { type: 'string', notCapturedReason: 'redactedIdent' }) + expect(fields).to.have.deep.property('Symbol(secret)', { type: 'string', notCapturedReason: 'redactedIdent' }) + }) + + setAndTriggerBreakpoint(target, BREAKPOINT_LINE_NUMBER) + }) + }) +}) diff --git a/packages/dd-trace/test/debugger/devtools_client/snapshot/target-code/redaction.js b/packages/dd-trace/test/debugger/devtools_client/snapshot/target-code/redaction.js new file mode 100644 index 00000000000..45e76a23a9c --- /dev/null +++ b/packages/dd-trace/test/debugger/devtools_client/snapshot/target-code/redaction.js @@ -0,0 +1,35 @@ +'use strict' + +function run () { + const nonNormalizedSecretToken = '@Se-cret_$.' + const foo = 'bar' // eslint-disable-line no-unused-vars + const secret = 'shh!' + const Se_cret_$ = 'shh!' // eslint-disable-line camelcase, no-unused-vars + const weakMapKey = { secret: 'shh!' } + const obj = { + foo: 'bar', + secret, + [nonNormalizedSecretToken]: 'shh!', + nested: { secret: 'shh!' }, + arr: [{ secret: 'shh!' }], + map: new Map([ + ['foo', 'bar'], + ['secret', 'shh!'], + [nonNormalizedSecretToken, 'shh!'], + [Symbol('secret'), 'shh!'], + [Symbol(nonNormalizedSecretToken), 'shh!'] + ]), + weakmap: new WeakMap([[weakMapKey, 42]]), + [Symbol('secret')]: 'shh!', + [Symbol(nonNormalizedSecretToken)]: 'shh!' + } + + Object.defineProperty(obj, 'password', { + value: 'shh!', + enumerable: false + }) + + return obj // breakpoint at this line +} + +module.exports = { run } diff --git a/packages/dd-trace/test/debugger/devtools_client/snapshot/utils.js b/packages/dd-trace/test/debugger/devtools_client/snapshot/utils.js index 215b93a4002..22f7610205f 100644 --- a/packages/dd-trace/test/debugger/devtools_client/snapshot/utils.js +++ b/packages/dd-trace/test/debugger/devtools_client/snapshot/utils.js @@ -10,6 +10,13 @@ session['@noCallThru'] = true proxyquire('../src/debugger/devtools_client/snapshot/collector', { '../session': session }) +proxyquire('../src/debugger/devtools_client/snapshot/redaction', { + '../config': { + dynamicInstrumentationRedactedIdentifiers: [], + dynamicInstrumentationRedactionExcludedIdentifiers: [], + '@noCallThru': true + } +}) const { getLocalStateForCallFrame } = require('../../../../src/debugger/devtools_client/snapshot') @@ -75,16 +82,16 @@ async function setAndTriggerBreakpoint (path, line) { run() } -function assertOnBreakpoint (done, config, callback) { - if (typeof config === 'function') { - callback = config - config = undefined +function assertOnBreakpoint (done, snapshotConfig, callback) { + if (typeof snapshotConfig === 'function') { + callback = snapshotConfig + snapshotConfig = undefined } session.once('Debugger.paused', ({ params }) => { expect(params.hitBreakpoints.length).to.eq(1) - getLocalStateForCallFrame(params.callFrames[0], config).then((process) => { + getLocalStateForCallFrame(params.callFrames[0], snapshotConfig).then((process) => { callback(process()) done() }).catch(done) diff --git a/packages/dd-trace/test/debugger/devtools_client/status.spec.js b/packages/dd-trace/test/debugger/devtools_client/status.spec.js index 365d86d6e96..88edde917e3 100644 --- a/packages/dd-trace/test/debugger/devtools_client/status.spec.js +++ b/packages/dd-trace/test/debugger/devtools_client/status.spec.js @@ -2,12 +2,15 @@ require('../../setup/mocha') +const { expectWithin, getRequestOptions } = require('./utils') +const JSONBuffer = require('../../../src/debugger/devtools_client/json-buffer') + const ddsource = 'dd_debugger' const service = 'my-service' const runtimeId = 'my-runtime-id' -describe('diagnostic message http request caching', function () { - let statusproxy, request +describe('diagnostic message http requests', function () { + let statusproxy, request, jsonBuffer const acks = [ ['ackReceived', 'RECEIVED'], @@ -20,8 +23,17 @@ describe('diagnostic message http request caching', function () { request = sinon.spy() request['@noCallThru'] = true + class JSONBufferSpy extends JSONBuffer { + constructor (...args) { + super(...args) + jsonBuffer = this + sinon.spy(this, 'write') + } + } + statusproxy = proxyquire('../src/debugger/devtools_client/status', { './config': { service, runtimeId, '@noCallThru': true }, + './json-buffer': JSONBufferSpy, '../../exporters/common/request': request }) }) @@ -45,54 +57,85 @@ describe('diagnostic message http request caching', function () { } }) - it('should only call once if no change', function () { + it('should buffer instead of calling request directly', function () { + ackFn({ id: 'foo', version: 0 }) + expect(request).to.not.have.been.called + expect(jsonBuffer.write).to.have.been.calledOnceWith( + JSON.stringify(formatAsDiagnosticsEvent({ probeId: 'foo', version: 0, status, exception })) + ) + }) + + it('should only add to buffer once if no change', function () { ackFn({ id: 'foo', version: 0 }) - expect(request).to.have.been.calledOnce - assertRequestData(request, { probeId: 'foo', version: 0, status, exception }) + expect(jsonBuffer.write).to.have.been.calledOnceWith( + JSON.stringify(formatAsDiagnosticsEvent({ probeId: 'foo', version: 0, status, exception })) + ) ackFn({ id: 'foo', version: 0 }) - expect(request).to.have.been.calledOnce + expect(jsonBuffer.write).to.have.been.calledOnce }) - it('should call again if version changes', function () { + it('should add to buffer again if version changes', function () { ackFn({ id: 'foo', version: 0 }) - expect(request).to.have.been.calledOnce - assertRequestData(request, { probeId: 'foo', version: 0, status, exception }) + expect(jsonBuffer.write).to.have.been.calledOnceWith( + JSON.stringify(formatAsDiagnosticsEvent({ probeId: 'foo', version: 0, status, exception })) + ) ackFn({ id: 'foo', version: 1 }) - expect(request).to.have.been.calledTwice - assertRequestData(request, { probeId: 'foo', version: 1, status, exception }) + expect(jsonBuffer.write).to.have.been.calledTwice + expect(jsonBuffer.write.lastCall).to.have.been.calledWith( + JSON.stringify(formatAsDiagnosticsEvent({ probeId: 'foo', version: 1, status, exception })) + ) }) - it('should call again if probeId changes', function () { + it('should add to buffer again if probeId changes', function () { ackFn({ id: 'foo', version: 0 }) - expect(request).to.have.been.calledOnce - assertRequestData(request, { probeId: 'foo', version: 0, status, exception }) + expect(jsonBuffer.write).to.have.been.calledOnceWith( + JSON.stringify(formatAsDiagnosticsEvent({ probeId: 'foo', version: 0, status, exception })) + ) ackFn({ id: 'bar', version: 0 }) - expect(request).to.have.been.calledTwice - assertRequestData(request, { probeId: 'bar', version: 0, status, exception }) + expect(jsonBuffer.write).to.have.been.calledTwice + expect(jsonBuffer.write.lastCall).to.have.been.calledWith( + JSON.stringify(formatAsDiagnosticsEvent({ probeId: 'bar', version: 0, status, exception })) + ) + }) + + it('should call request with the expected payload once the buffer is flushed', function (done) { + ackFn({ id: 'foo', version: 0 }) + ackFn({ id: 'foo', version: 1 }) + ackFn({ id: 'bar', version: 0 }) + expect(request).to.not.have.been.called + + expectWithin(1200, () => { + expect(request).to.have.been.calledOnce + + const payload = getFormPayload(request) + + expect(payload).to.deep.equal([ + formatAsDiagnosticsEvent({ probeId: 'foo', version: 0, status, exception }), + formatAsDiagnosticsEvent({ probeId: 'foo', version: 1, status, exception }), + formatAsDiagnosticsEvent({ probeId: 'bar', version: 0, status, exception }) + ]) + + const opts = getRequestOptions(request) + expect(opts).to.have.property('method', 'POST') + expect(opts).to.have.property('path', '/debugger/v1/diagnostics') + + done() + }) }) }) } }) -function assertRequestData (request, { probeId, version, status, exception }) { - const payload = getFormPayload(request) +function formatAsDiagnosticsEvent ({ probeId, version, status, exception }) { const diagnostics = { probeId, runtimeId, probeVersion: version, status } // Error requests will also contain an `exception` property if (exception) diagnostics.exception = exception - expect(payload).to.deep.equal({ ddsource, service, debugger: { diagnostics } }) - - const opts = getRequestOptions(request) - expect(opts).to.have.property('method', 'POST') - expect(opts).to.have.property('path', '/debugger/v1/diagnostics') -} - -function getRequestOptions (request) { - return request.lastCall.args[1] + return { ddsource, service, debugger: { diagnostics } } } function getFormPayload (request) { diff --git a/packages/dd-trace/test/debugger/devtools_client/utils.js b/packages/dd-trace/test/debugger/devtools_client/utils.js index e15d567a7c1..2da3216cea1 100644 --- a/packages/dd-trace/test/debugger/devtools_client/utils.js +++ b/packages/dd-trace/test/debugger/devtools_client/utils.js @@ -3,7 +3,21 @@ const { randomUUID } = require('node:crypto') module.exports = { - generateProbeConfig + expectWithin, + generateProbeConfig, + getRequestOptions +} + +function expectWithin (timeout, fn, start = Date.now(), backoff = 1) { + try { + fn() + } catch (e) { + if (Date.now() - start > timeout) { + throw e + } else { + setTimeout(expectWithin, backoff, timeout, fn, start, backoff < 128 ? backoff * 2 : backoff) + } + } } function generateProbeConfig (breakpoint, overrides = {}) { @@ -23,3 +37,7 @@ function generateProbeConfig (breakpoint, overrides = {}) { ...overrides } } + +function getRequestOptions (request) { + return request.lastCall.args[1] +} diff --git a/packages/dd-trace/test/fixtures/telemetry/config_norm_rules.json b/packages/dd-trace/test/fixtures/telemetry/config_norm_rules.json index f00fbc27dcb..c7a2941be88 100644 --- a/packages/dd-trace/test/fixtures/telemetry/config_norm_rules.json +++ b/packages/dd-trace/test/fixtures/telemetry/config_norm_rules.json @@ -1,741 +1,814 @@ { - "aas_app_type": "aas_app_type", - "aas_configuration_error": "aas_configuration_error", - "aas_functions_runtime_version": "aas_functions_runtime_version", - "aas_siteextensions_version": "aas_site_extensions_version", - "activity_listener_enabled": "activity_listener_enabled", - "agent_transport": "agent_transport", - "DD_AGENT_TRANSPORT": "agent_transport", - "agent_url": "trace_agent_url", - "analytics_enabled": "analytics_enabled", - "autoload_no_compile": "autoload_no_compile", - "cloud_hosting": "cloud_hosting_provider", - "code_hotspots_enabled": "code_hotspots_enabled", - "data_streams_enabled": "data_streams_enabled", - "dsmEnabled": "data_streams_enabled", - "enabled": "trace_enabled", - "environment_fulltrust_appdomain": "environment_fulltrust_appdomain_enabled", - "logInjection_enabled": "logs_injection_enabled", - "partialflush_enabled": "trace_partial_flush_enabled", - "partialflush_minspans": "trace_partial_flush_min_spans", - "platform": "platform", - "profiler_loaded": "profiler_loaded", - "routetemplate_expansion_enabled": "trace_route_template_expansion_enabled", - "routetemplate_resourcenames_enabled": "trace_route_template_resource_names_enabled", - "runtimemetrics_enabled": "runtime_metrics_enabled", - "runtime.metrics.enabled": "runtime_metrics_enabled", - "sample_rate": "trace_sample_rate", - "sampling_rules": "trace_sample_rules", - "span_sampling_rules": "span_sample_rules", - "spanattributeschema": "trace_span_attribute_schema", - "security_enabled": "appsec_enabled", - "stats_computation_enabled": "trace_stats_computation_enabled", - "native_tracer_version": "native_tracer_version", - "managed_tracer_framework": "managed_tracer_framework", - "wcf_obfuscation_enabled": "trace_wcf_obfuscation_enabled", - "data.streams.enabled": "data_streams_enabled", - "dynamic.instrumentation.enabled": "dynamic_instrumentation_enabled", - "dynamic_instrumentation.enabled": "dynamic_instrumentation_enabled", - "HOSTNAME": "agent_hostname", - "dd_agent_host": "agent_host", - "instrumentation.telemetry.enabled": "instrumentation_telemetry_enabled", - "integrations.enabled": "trace_integrations_enabled", - "logs.injection": "logs_injection_enabled", - "logs.mdc.tags.injection": "logs_mdc_tags_injection_enabled", - "os.name": "os_name", - "openai_service": "open_ai_service", - "openai_logs_enabled": "open_ai_logs_enabled", - "openAiLogsEnabled": "open_ai_logs_enabled", - "openai_span_char_limit": "open_ai_span_char_limit", - "openaiSpanCharLimit": "open_ai_span_char_limit", - "openai_span_prompt_completion_sample_rate": "open_ai_span_prompt_completion_sample_rate", - "openai_log_prompt_completion_sample_rate": "open_ai_log_prompt_completion_sample_rate", - "openai_metrics_enabled": "open_ai_metrics_enabled", - "priority.sampling": "trace_priority_sample_enabled", - "profiling.allocation.enabled": "profiling_allocation_enabled", - "profiling.enabled": "profiling_enabled", - "profiling.start-force-first": "profiling_start_force_first", - "remote_config.enabled": "remote_config_enabled", - "remoteConfig.enabled": "remote_config_enabled", - "remoteConfig.pollInterval": "remote_config_poll_interval", - "trace.agent.port": "trace_agent_port", - "trace.agent.v0.5.enabled": "trace_agent_v0.5_enabled", - "trace.analytics.enabled": "trace_analytics_enabled", - "trace.enabled": "trace_enabled", - "trace.client-ip.enabled": "trace_client_ip_enabled", - "trace.jms.propagation.enabled": "trace_jms_propagation_enabled", - "trace.x-datadog-tags.max.length": "trace_x_datadog_tags_max_length", - "trace.kafka.client.propagation.enabled": "trace_kafka_client_propagation_enabled", - "trace.laravel_queue_distributed_tracing": "trace_laravel_queue_distributed_tracing", - "trace.symfony_messenger_distributed_tracing": "trace_symfony_messenger_distributed_tracing", - "trace.symfony_messenger_middlewares": "trace_symfony_messenger_middlewares", - "trace.sources_path": "trace_sources_path", - "trace.log_file": "trace_log_file", - "trace.log_level": "trace_log_level", - "kafka.client.base64.decoding.enabled": "trace_kafka_client_base64_decoding_enabled", - "trace.aws-sdk.propagation.enabled": "trace_aws_sdk_propagation_enabled", - "trace.aws-sdk.legacy.tracing.enabled": "trace_aws_sdk_legacy_tracing_enabled", - "trace.servlet.principal.enabled": "trace_servlet_principal_enabled", - "trace.servlet.async-timeout.error": "trace_servlet_async_timeout_error_enabled", - "trace.rabbit.propagation.enabled": "trace_rabbit_propagation_enabled", - "trace.partial.flush.min.spans": "trace_partial_flush_min_spans", - "trace.sample.rate": "trace_sample_rate", - "trace.sqs.propagation.enabled": "trace_sqs_propagation_enabled", - "trace.peerservicetaginterceptor.enabled": "trace_peer_service_tag_interceptor_enabled", - "dd_trace_sample_rate": "trace_sample_rate", - "trace_methods": "trace_methods", - "tracer_instance_count": "trace_instance_count", - "trace.db.client.split-by-instance": "trace_db_client_split_by_instance", - "trace.db.client.split-by-instance.type.suffix": "trace_db_client_split_by_instance_type_suffix", - "trace.http.client.split-by-domain" : "trace_http_client_split_by_domain", - "trace.agent.timeout": "trace_agent_timeout", - "trace.header.tags.legacy.parsing.enabled": "trace_header_tags_legacy_parsing_enabled", - "trace.client-ip.resolver.enabled": "trace_client_ip_resolver_enabled", - "trace.play.report-http-status": "trace_play_report_http_status", - "trace.jmxfetch.tomcat.enabled": "trace_jmxfetch_tomcat_enabled", - "trace.jmxfetch.kafka.enabled": "trace_jmxfetch_kafka_enabled", - "trace.scope.depth.limit": "trace_scope_depth_limit", - "inferredProxyServicesEnabled": "inferred_proxy_services_enabled", - "resolver.use.loadclass": "resolver_use_loadclass", - "resolver.outline.pool.enabled": "resolver_outline_pool_enabled", - "appsec.apiSecurity.enabled": "api_security_enabled", - "appsec.apiSecurity.requestSampling": "api_security_request_sample_rate", - "appsec.enabled": "appsec_enabled", - "appsec.eventTracking": "appsec_auto_user_events_tracking", - "appsec.eventTracking.mode": "appsec_auto_user_events_tracking", - "appsec.testing": "appsec_testing", - "appsec.trace.rate.limit": "appsec_trace_rate_limit", - "appsec.obfuscatorKeyRegex": "appsec_obfuscation_parameter_key_regexp", - "appsec.obfuscatorValueRegex": "appsec_obfuscation_parameter_value_regexp", - "appsec.rasp.enabled": "appsec_rasp_enabled", - "appsec.rateLimit": "appsec_rate_limit", - "appsec.rules": "appsec_rules", - "appsec.sca_enabled": "appsec_sca_enabled", - "appsec.wafTimeout": "appsec_waf_timeout", - "appsec.sca.enabled": "appsec_sca_enabled", - "clientIpHeader": "trace_client_ip_header", - "clientIpEnabled": "trace_client_ip_enabled", - "clientIpHeaderDisabled": "client_ip_header_disabled", - "debug": "trace_debug_enabled", - "dd.trace.debug": "trace_debug_enabled", - "dogstatsd.hostname": "dogstatsd_hostname", - "dogstatsd.port": "dogstatsd_port", - "dogstatsd.start-delay": "dogstatsd_start_delay", - "env": "env", - "experimental.b3": "experimental_b3", - "experimental.enableGetRumData": "experimental_enable_get_rum_data", - "experimental.exporter": "experimental_exporter", - "experimental.runtimeId": "experimental_runtime_id", - "experimental.sampler.rateLimit": "experimental_sampler_rate_limit", - "experimental.sampler.sampleRate": "experimental_sampler_sample_rate", - "experimental.traceparent": "experimental_traceparent", - "flushInterval": "flush_interval", - "flushMinSpans": "flush_min_spans", - "hostname": "agent_hostname", - "iast.enabled": "iast_enabled", - "iast.cookieFilterPattern": "iast_cookie_filter_pattern", - "iast.deduplication.enabled": "iast_deduplication_enabled", - "iast.maxConcurrentRequests": "iast_max_concurrent_requests", - "iast.max-concurrent-requests": "iast_max_concurrent_requests", - "iast.maxContextOperations": "iast_max_context_operations", - "iast.requestSampling": "iast_request_sampling", - "iast.request-sampling": "iast_request_sampling", - "iast.debug.enabled": "iast_debug_enabled", - "iast.vulnerabilities-per-request": "iast_vulnerability_per_request", - "iast.deduplicationEnabled": "iast_deduplication_enabled", - "iast.redactionEnabled": "iast_redaction_enabled", - "iast.redactionNamePattern": "iast_redaction_name_pattern", - "iast.redactionValuePattern": "iast_redaction_value_pattern", - "iast.telemetryVerbosity": "iast_telemetry_verbosity", - "isAzureFunction": "azure_function", - "isGitUploadEnabled": "git_upload_enabled", - "isIntelligentTestRunnerEnabled": "intelligent_test_runner_enabled", - "logger": "logger", - "logInjection": "logs_injection_enabled", - "logLevel": "trace_log_level", - "memcachedCommandEnabled": "memchached_command_enabled", - "lookup": "lookup", - "plugins": "plugins", - "port": "trace_agent_port", - "profiling.exporters": "profiling_exporters", - "profiling.sourceMap": "profiling_source_map_enabled", - "protocolVersion": "trace_agent_protocol_version", - "querystringObfuscation": "trace_obfuscation_query_string_regexp", - "reportHostname": "trace_report_hostname", - "trace.report-hostname": "trace_report_hostname", - "runtimeMetrics": "runtime_metrics_enabled", - "sampler.rateLimit": "trace_rate_limit", - "trace.rate.limit": "trace_rate_limit", - "sampler.sampleRate": "trace_sample_rate", - "sampleRate": "trace_sample_rate", - "scope": "scope", - "service": "service", - "serviceMapping": "dd_service_mapping", - "site": "site", - "startupLogs": "trace_startup_logs_enabled", - "stats.enabled": "stats_enabled", - "DD_TRACE_HEADER_TAGS": "trace_header_tags", - "tagsHeaderMaxLength": "trace_header_tags_max_length", - "telemetryEnabled": "instrumentation_telemetry_enabled", - "otel_enabled": "trace_otel_enabled", - "trace.otel.enabled": "trace_otel_enabled", - "trace.otel_enabled": "trace_otel_enabled", - "tracing": "trace_enabled", - "url": "trace_agent_url", - "version": "application_version", - "trace.tracer.metrics.enabled": "trace_metrics_enabled", - "trace.perf.metrics.enabled": "trace_perf_metrics_enabled", - "trace.health.metrics.enabled": "trace_health_metrics_enabled", - "trace.health.metrics.statsd.port": "trace_health_metrics_statsd_port", - "trace.grpc.server.trim-package-resource": "trace_grpc_server_trim_package_resource_enabled", - "DD_TRACE_DEBUG": "trace_debug_enabled", - "profiling.start-delay": "profiling_start_delay", - "profiling.upload.period": "profiling_upload_period", - "profiling.async.enabled": "profiling_async_enabled", - "profiling.async.alloc.enabled": "profiling_async_alloc_enabled", - "profiling.directallocation.enabled": "profiling_direct_allocation_enabled", - "profiling.hotspots.enabled": "profiling_hotspots_enabled", - "profiling.async.cpu.enabled": "profiling_async_cpu_enabled", - "profiling.async.memleak.enabled": "profiling_async_memleak_enabled", - "profiling.async.wall.enabled": "profiling_async_wall_enabled", - "profiling.ddprof.enabled": "profiling_ddprof_enabled", - "profiling.heap.enabled": "profiling_heap_enabled", - "profiling.legacy.tracing.integration": "profiling_legacy_tracing_integration_enabled", - "queryStringObfuscation": "trace_obfuscation_query_string_regexp", - "dbmPropagationMode": "dbm_propagation_mode", - "rcPollingInterval": "rc_polling_interval", - "jmxfetch.initial-refresh-beans-period": "jmxfetch_initial_refresh_beans_period", - "jmxfetch.refresh-beans-period": "jmxfetch_initial_refresh_beans_period", - "jmxfetch.multiple-runtime-services.enabled": "jmxfetch_multiple_runtime_services_enabled", - "jmxfetch.enabled": "jmxfetch_enabled", - "jmxfetch.statsd.port": "jmxfetch_statsd_port", - "jmxfetch.check-period": "jmxfetch_check_period", - "appsec.blockedTemplateGraphql": "appsec_blocked_template_graphql", - "appsec.blockedTemplateHtml": "appsec_blocked_template_html", - "appsec.blockedTemplateJson": "appsec_blocked_template_json", - "appsec.waf.timeout": "appsec_waf_timeout", - "civisibility.enabled": "ci_visibility_enabled", - "civisibility.agentless.enabled": "ci_visibility_agentless_enabled", - "isCiVisibility": "ci_visibility_enabled", - "cws.enabled": "cws_enabled", - "AWS_LAMBDA_INITIALIZATION_TYPE": "aws_lambda_initialization_type", - "http.server.tag.query-string": "trace_http_server_tag_query_string", - "http.server.route-based-naming": "trace_http_server_route_based_naming_enabled", - "http.client.tag.query-string": "trace_http_client_tag_query_string", - "DD_TRACE_HTTP_CLIENT_TAG_QUERY_STRING": "trace_http_client_tag_query_string", - "hystrix.tags.enabled": "hystrix_tags_enabled", - "hystrix.measured.enabled": "hystrix_measured_enabled", - "ignite.cache.include_keys": "ignite_cache_include_keys_enabled", - "dynamic.instrumentation.classfile.dump.enabled": "dynamic_instrumentation_classfile_dump_enabled", - "dynamic.instrumentation.metrics.enabled": "dynamic_instrumentation_metrics_enabled", - "message.broker.split-by-destination": "message_broker_split_by_destination", - "agent_feature_drop_p0s": "agent_feature_drop_p0s", - "appsec.rules.metadata.rules_version": "appsec_rules_metadata_rules_version", - "appsec.rules.version": "appsec_rules_version", - "appsec.customRulesProvided": "appsec_rules_custom_provided", - "dogstatsd_addr": "dogstatsd_url", - "lambda_mode": "lambda_mode", - "profiling.ddprof.alloc.enabled": "profiling_ddprof_alloc_enabled", - "profiling.ddprof.cpu.enabled": "profiling_ddprof_cpu_enabled", - "profiling.ddprof.memleak.enabled": "profiling_ddprof_memleak_enabled", - "profiling.ddprof.wall.enabled": "profiling_ddprof_wall_enabled", - "profiling_endpoints_enabled": "profiling_endpoints_enabled", - "send_retries": "trace_send_retries", - "telemetry.enabled": "instrumentation_telemetry_enabled", - "telemetry.debug": "instrumentation_telemetry_debug_enabled", - "telemetry.logCollection": "instrumentation_telemetry_log_collection_enabled", - "telemetry.metrics": "instrumentation_telemetry_metrics_enabled", - "telemetry.metricsInterval": "instrumentation_telemetry_metrics_interval", - "telemetry.heartbeat.interval": "instrumentation_telemetry_heartbeat_interval", - "telemetry_heartbeat_interval": "instrumentation_telemetry_heartbeat_interval", - "universal_version": "universal_version_enabled", - "global_tag_version": "version", - "traceId128BitGenerationEnabled": "trace_128_bits_id_enabled", - "traceId128BitLoggingEnabled": "trace_128_bits_id_logging_enabled", - "trace.status404decorator.enabled": "trace_status_404_decorator_enabled", - "trace.status404rule.enabled": "trace_status_404_rule_enabled", - "discovery": "agent_discovery_enabled", - "repositoryurl": "repository_url", - "gitmetadataenabled": "git_metadata_enabled", - "commitsha": "commit_sha", - "isgcpfunction": "is_gcp_function", - "isGCPFunction": "is_gcp_function", - "legacy.installer.enabled": "legacy_installer_enabled", - "trace.request_init_hook": "trace_request_init_hook", - "dogstatsd_url": "dogstatsd_url", - "distributed_tracing": "trace_distributed_trace_enabled", - "autofinish_spans": "trace_auto_finish_spans_enabled", - "trace.url_as_resource_names_enabled": "trace_url_as_resource_names_enabled", - "integrations_disabled": "trace_disabled_integrations", - "priority_sampling": "trace_priority_sampling_enabled", - "trace.auto_flush_enabled": "trace_auto_flush_enabled", - "trace.measure_compile_time": "trace_measure_compile_time_enabled", - "trace.measure_peak_memory_usage": "trace_measure_peak_memory_usage_enabled", - "trace.health_metrics_heartbeat_sample_rate": "trace_health_metrics_heartbeat_sample_rate", - "trace.redis_client_split_by_host": "trace_redis_client_split_by_host_enabled", - "trace.memory_limit": "trace_memory_limit", - "trace.flush_collect_cycles": "trace_flush_collect_cycles_enabled", - "trace.resource_uri_fragment_regex": "trace_resource_uri_fragment_regex", - "trace.resource_uri_mapping_incoming": "trace_resource_uri_mapping_incoming", - "trace.resource_uri_mapping_outgoing": "trace_resource_uri_mapping_outgoing", - "trace.resource_uri_query_param_allowed": "trace_resource_uri_query_param_allowed", - "trace.http_url_query_param_allowed": "trace_http_url_query_param_allowed", - "trace.http_post_data_param_allowed": "trace_http_post_data_param_allowed", - "trace.sampling_rules": "trace_sample_rules", - "span_sampling_rules_file": "span_sample_rules_file", - "trace.propagation_style_extract": "trace_propagation_style_extract", - "trace.propagation_style_inject": "trace_propagation_style_inject", - "trace.propagation_style": "trace_propagation_style", - "trace.propagation_extract_first": "trace_propagation_extract_first", - "tracePropagationExtractFirst": "trace_propagation_extract_first", - "tracePropagationStyle.extract": "trace_propagation_style_extract", - "tracePropagationStyle.inject": "trace_propagation_style_inject", - "tracePropagationStyle,otelPropagators": "trace_propagation_style_otel_propagators", - "tracing.distributed_tracing.propagation_extract_style": "trace_propagation_style_extract", - "tracing.distributed_tracing.propagation_inject_style": "trace_propagation_style_inject", - "trace.traced_internal_functions": "trace_traced_internal_functions", - "trace.agent_connect_timeout": "trace_agent_connect_timeout", - "trace.debug_prng_seed": "trace_debug_prng_seed", - "log_backtrace": "trace_log_backtrace_enabled", - "trace.generate_root_span": "trace_generate_root_span_enabled", - "trace.spans_limit": "trace_spans_limit", - "trace.128_bit_traceid_generation_enabled": "trace_128_bits_id_enabled", - "trace.agent_max_consecutive_failures": "trace_send_retries", - "trace.agent_attempt_retry_time_msec": "trace_agent_attempt_retry_time_msec", - "trace.bgs_connect_timeout": "trace_bgs_connect_timeout", - "trace.bgs_timeout": "trace_bgs_timeout", - "trace.agent_flush_interval": "trace_agent_flush_interval", - "trace.agent_flush_after_n_requests": "trace_agent_flush_after_n_requests", - "trace.shutdown_timeout": "trace_shutdown_timeout", - "trace.agent_debug_verbose_curl": "trace_agent_debug_verbose_curl_enabled", - "trace.debug_curl_output": "trace_debug_curl_output_enabled", - "trace.beta_high_memory_pressure_percent": "trace_beta_high_memory_pressure_percent", - "trace.warn_legacy_dd_trace": "trace_warn_legacy_dd_trace_enabled", - "trace.retain_thread_capabilities": "trace_retain_thread_capabilities_enabled", - "trace.client_ip_header": "client_ip_header", - "trace.forked_process": "trace_forked_process_enabled", - "trace.hook_limit": "trace_hook_limit", - "trace.agent_max_payload_size": "trace_agent_max_payload_size", - "trace.agent_stack_initial_size": "trace_agent_stack_initial_size", - "trace.agent_stack_backlog": "trace_agent_stack_backlog", - "trace.agent_retries": "trace_send_retries", - "trace.agent_test_session_token": "trace_agent_test_session_token", - "trace.propagate_user_id_default": "trace_propagate_user_id_default_enabled", - "dbm_propagation_mode": "dbm_propagation_mode", - "trace.remove_root_span_laravel_queue": "trace_remove_root_span_laravel_queue_enabled", - "trace.remove_root_span_symfony_messenger": "trace_remove_root_span_symfony_messenger_enabled", - "trace.remove_autoinstrumentation_orphans": "trace_remove_auto_instrumentation_orphans_enabled", - "trace.memcached_obfuscation": "trace_memcached_obfuscation_enabled", - "DD_TRACE_CONFIG_FILE": "trace_config_file", - "DD_DOTNET_TRACER_CONFIG_FILE": "trace_config_file", - "DD_ENV": "env", - "DD_SERVICE": "service", - "DD_SERVICE_NAME": "service", - "DD_VERSION": "application_version", - "DD_GIT_REPOSITORY_URL": "repository_url", - "git_repository_url": "repository_url", - "DD_GIT_COMMIT_SHA": "commit_sha", - "DD_TRACE_GIT_METADATA_ENABLED": "git_metadata_enabled", - "trace.git_metadata_enabled": "git_metadata_enabled", - "git_commit_sha": "commit_sha", - "DD_TRACE_ENABLED": "trace_enabled", - "DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED": "experimental_appsec_standalone_enabled", - "DD_INTERNAL_WAIT_FOR_DEBUGGER_ATTACH": "internal_wait_for_debugger_attach_enabled", - "DD_INTERNAL_WAIT_FOR_NATIVE_DEBUGGER_ATTACH": "internal_wait_for_native_debugger_attach_enabled", - "DD_DISABLED_INTEGRATIONS": "trace_disabled_integrations", - "DD_TRACE_ANALYTICS_ENABLED": "trace_analytics_enabled", - "DD_TRACE_BUFFER_SIZE": "trace_serialization_buffer_size", - "trace.buffer_size": "trace_serialization_buffer_size", - "DD_TRACE_BATCH_INTERVAL": "trace_serialization_batch_interval", - "DD_LOG_INJECTION": "logs_injection_enabled", - "DD_LOGS_INJECTION": "logs_injection_enabled", - "DD_TRACE_RATE_LIMIT": "trace_rate_limit", - "DD_MAX_TRACES_PER_SECOND": "trace_rate_limit", - "DD_TRACE_SAMPLING_RULES": "trace_sample_rules", - "DD_SPAN_SAMPLING_RULES": "span_sample_rules", - "DD_TRACE_SAMPLE_RATE": "trace_sample_rate", + "AWS_LAMBDA_INITIALIZATION_TYPE": "aws_lambda_initialization_type", + "COMPUTERNAME": "aas_instance_name", + "DATADOG_TRACE_AGENT_HOSTNAME": "agent_host", + "DATADOG_TRACE_AGENT_PORT": "trace_agent_port", + "DD_AAS_DOTNET_EXTENSION_VERSION": "aas_site_extensions_version", + "DD_AAS_ENABLE_CUSTOM_METRICS": "aas_custom_metrics_enabled", + "DD_AAS_ENABLE_CUSTOM_TRACING": "aas_custom_tracing_enabled", + "DD_AGENT_TRANSPORT": "agent_transport", + "DD_API_SECURITY_ENABLED": "api_security_enabled", + "DD_API_SECURITY_MAX_CONCURRENT_REQUESTS": "api_security_max_concurrent_requests", + "DD_API_SECURITY_REQUEST_SAMPLE_RATE": "api_security_request_sample_rate", + "DD_API_SECURITY_SAMPLE_DELAY": "api_security_sample_delay", "DD_APM_ENABLE_RARE_SAMPLER": "trace_rare_sampler_enabled", - "DD_TRACE_METRICS_ENABLED": "trace_metrics_enabled", - "DD_RUNTIME_METRICS_ENABLED": "runtime_metrics_enabled", - "DD_TRACE_AGENT_PATH": "agent_trace_agent_excecutable_path", - "DD_TRACE_AGENT_ARGS": "agent_trace_agent_excecutable_args", - "DD_DOGSTATSD_PATH": "agent_dogstatsd_executable_path", - "DD_DOGSTATSD_ARGS": "agent_dogstatsd_executable_args", - "DD_DIAGNOSTIC_SOURCE_ENABLED": "trace_diagnostic_source_enabled", - "DD_SITE": "site", - "DD_TRACE_HTTP_CLIENT_EXCLUDED_URL_SUBSTRINGS": "trace_http_client_excluded_urls", - "DD_HTTP_SERVER_ERROR_STATUSES": "trace_http_server_error_statuses", - "DD_HTTP_CLIENT_ERROR_STATUSES": "trace_http_client_error_statuses", - "DD_TRACE_HTTP_SERVER_ERROR_STATUSES": "trace_http_server_error_statuses", - "DD_TRACE_HTTP_CLIENT_ERROR_STATUSES": "trace_http_client_error_statuses", - "DD_TRACE_CLIENT_IP_HEADER": "trace_client_ip_header", - "DD_TRACE_CLIENT_IP_ENABLED": "trace_client_ip_enabled", - "DD_TRACE_KAFKA_CREATE_CONSUMER_SCOPE_ENABLED": "trace_kafka_create_consumer_scope_enabled", - "DD_TRACE_EXPAND_ROUTE_TEMPLATES_ENABLED": "trace_route_template_expansion_enabled", - "DD_TRACE_STATS_COMPUTATION_ENABLED": "trace_stats_computation_enabled", - "_DD_TRACE_STATS_COMPUTATION_INTERVAL": "trace_stats_computation_interval", - "DD_TRACE_PROPAGATION_STYLE_INJECT": "trace_propagation_style_inject", - "DD_PROPAGATION_STYLE_INJECT": "trace_propagation_style_inject", - "DD_TRACE_PROPAGATION_STYLE_EXTRACT": "trace_propagation_style_extract", - "DD_PROPAGATION_STYLE_EXTRACT": "trace_propagation_style_extract", - "DD_TRACE_PROPAGATION_STYLE": "trace_propagation_style", - "DD_TRACE_PROPAGATION_EXTRACT_FIRST": "trace_propagation_extract_first", - "DD_TRACE_METHODS": "trace_methods", - "DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP": "trace_obfuscation_query_string_regexp", - "DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP_TIMEOUT": "trace_obfuscation_query_string_regexp_timeout", - "DD_HTTP_SERVER_TAG_QUERY_STRING_SIZE": "trace_http_server_tag_query_string_size", - "DD_HTTP_SERVER_TAG_QUERY_STRING": "trace_http_server_tag_query_string_enabled", - "DD_DBM_PROPAGATION_MODE": "dbm_propagation_mode", - "DD_TRACE_SPAN_ATTRIBUTE_SCHEMA": "trace_span_attribute_schema", - "DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED": "trace_peer_service_defaults_enabled", - "DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED": "trace_remove_integration_service_names_enabled", - "DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH": "trace_x_datadog_tags_max_length", - "DD_DATA_STREAMS_ENABLED": "data_streams_enabled", - "DD_DATA_STREAMS_LEGACY_HEADERS": "data_streams_legacy_headers", - "DD_CIVISIBILITY_ENABLED": "ci_visibility_enabled", + "DD_APM_RECEIVER_PORT": "trace_agent_port", + "DD_APM_RECEIVER_SOCKET": "trace_agent_socket", + "DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING": "appsec_auto_user_events_tracking", + "DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE": "appsec_auto_user_instrumentation_mode", + "DD_APPSEC_ENABLED": "appsec_enabled", + "DD_APPSEC_EXTRA_HEADERS": "appsec_extra_headers", + "DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML": "appsec_blocked_template_html", + "DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON": "appsec_blocked_template_json", + "DD_APPSEC_IPHEADER": "appsec_ip_header", + "DD_APPSEC_KEEP_TRACES": "appsec_force_keep_traces_enabled", + "DD_APPSEC_MAX_STACK_TRACES": "appsec_max_stack_traces", + "DD_APPSEC_MAX_STACK_TRACE_DEPTH": "appsec_max_stack_trace_depth", + "DD_APPSEC_MAX_STACK_TRACE_DEPTH_TOP_PERCENT": "appsec_max_stack_trace_depth_top_percent", + "DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP": "appsec_obfuscation_parameter_key_regexp", + "DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP": "appsec_obfuscation_parameter_value_regexp", + "DD_APPSEC_RASP_ENABLED": "appsec_rasp_enabled", + "DD_APPSEC_RULES": "appsec_rules", + "DD_APPSEC_SCA_ENABLED": "appsec_sca_enabled", + "DD_APPSEC_STACK_TRACE_ENABLED": "appsec_stack_trace_enabled", + "DD_APPSEC_TRACE_RATE_LIMIT": "appsec_trace_rate_limit", + "DD_APPSEC_WAF_DEBUG": "appsec_waf_debug_enabled", + "DD_APPSEC_WAF_TIMEOUT": "appsec_waf_timeout", + "DD_AZURE_APP_SERVICES": "aas_enabled", + "DD_CALL_BASIC_CONFIG": "dd_call_basic_config", "DD_CIVISIBILITY_AGENTLESS_ENABLED": "ci_visibility_agentless_enabled", "DD_CIVISIBILITY_AGENTLESS_URL": "ci_visibility_agentless_url", - "DD_CIVISIBILITY_LOGS_ENABLED": "ci_visibility_logs_enabled", + "DD_CIVISIBILITY_CODE_COVERAGE_COLLECTORPATH": "ci_visibility_code_coverage_collectorpath", "DD_CIVISIBILITY_CODE_COVERAGE_ENABLED": "ci_visibility_code_coverage_enabled", - "DD_CIVISIBILITY_CODE_COVERAGE_MODE": "ci_visibility_code_coverage_mode", - "DD_CIVISIBILITY_CODE_COVERAGE_SNK_FILEPATH": "ci_visibility_code_coverage_snk_path", "DD_CIVISIBILITY_CODE_COVERAGE_ENABLE_JIT_OPTIMIZATIONS": "ci_visibility_code_coverage_jit_optimisations_enabled", + "DD_CIVISIBILITY_CODE_COVERAGE_MODE": "ci_visibility_code_coverage_mode", "DD_CIVISIBILITY_CODE_COVERAGE_PATH": "ci_visibility_code_coverage_path", - "DD_CIVISIBILITY_GIT_UPLOAD_ENABLED": "ci_visibility_git_upload_enabled", - "DD_CIVISIBILITY_TESTSSKIPPING_ENABLED": "ci_visibility_test_skipping_enabled", - "DD_CIVISIBILITY_ITR_ENABLED": "ci_visibility_intelligent_test_runner_enabled", - "DD_CIVISIBILITY_FORCE_AGENT_EVP_PROXY": "ci_visibility_force_agent_evp_proxy_enabled", + "DD_CIVISIBILITY_CODE_COVERAGE_SNK_FILEPATH": "ci_visibility_code_coverage_snk_path", + "DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED": "ci_visibility_early_flake_detection_enabled", + "DD_CIVISIBILITY_ENABLED": "ci_visibility_enabled", "DD_CIVISIBILITY_EXTERNAL_CODE_COVERAGE_PATH": "ci_visibility_code_coverage_external_path", + "DD_CIVISIBILITY_FLAKY_RETRY_COUNT": "ci_visibility_flaky_retry_count", + "DD_CIVISIBILITY_FLAKY_RETRY_ENABLED": "ci_visibility_flaky_retry_enabled", + "DD_CIVISIBILITY_FORCE_AGENT_EVP_PROXY": "ci_visibility_force_agent_evp_proxy_enabled", "DD_CIVISIBILITY_GAC_INSTALL_ENABLED": "ci_visibility_gac_install_enabled", - "DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED": "ci_visibility_early_flake_detection_enabled", - "DD_CIVISIBILITY_CODE_COVERAGE_COLLECTORPATH": "ci_visibility_code_coverage_collectorpath", + "DD_CIVISIBILITY_GIT_UPLOAD_ENABLED": "ci_visibility_git_upload_enabled", + "DD_CIVISIBILITY_ITR_ENABLED": "ci_visibility_intelligent_test_runner_enabled", + "DD_CIVISIBILITY_LOGS_ENABLED": "ci_visibility_logs_enabled", "DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS": "ci_visibility_rum_flush_wait_millis", - "DD_CIVISIBILITY_FLAKY_RETRY_ENABLED": "ci_visibility_flaky_retry_enabled", - "DD_CIVISIBILITY_FLAKY_RETRY_COUNT": "ci_visibility_flaky_retry_count", + "DD_CIVISIBILITY_TESTSSKIPPING_ENABLED": "ci_visibility_test_skipping_enabled", "DD_CIVISIBILITY_TOTAL_FLAKY_RETRY_COUNT": "ci_visibility_total_flaky_retry_count", - "DD_TEST_SESSION_NAME": "test_session_name", + "DD_CODE_ORIGIN_FOR_SPANS_ENABLED": "code_origin_for_spans_enabled", + "DD_CODE_ORIGIN_FOR_SPANS_MAX_USER_FRAMES": "code_origin_for_spans_max_user_frames", + "DD_DATA_STREAMS_ENABLED": "data_streams_enabled", + "DD_DATA_STREAMS_LEGACY_HEADERS": "data_streams_legacy_headers", + "DD_DBM_PROPAGATION_MODE": "dbm_propagation_mode", + "DD_DEBUGGER_DIAGNOSTICS_INTERVAL": "dynamic_instrumentation_diagnostics_interval", + "DD_DEBUGGER_MAX_DEPTH_TO_SERIALIZE": "dynamic_instrumentation_serialization_max_depth", + "DD_DEBUGGER_MAX_TIME_TO_SERIALIZE": "dynamic_instrumentation_serialization_max_duration", + "DD_DEBUGGER_UPLOAD_BATCH_SIZE": "dynamic_instrumentation_upload_batch_size", + "DD_DEBUGGER_UPLOAD_FLUSH_INTERVAL": "dynamic_instrumentation_upload_interval", + "DD_DIAGNOSTIC_SOURCE_ENABLED": "trace_diagnostic_source_enabled", + "DD_DISABLED_INTEGRATIONS": "trace_disabled_integrations", + "DD_DOGSTATSD_ARGS": "agent_dogstatsd_executable_args", + "DD_DOGSTATSD_PATH": "agent_dogstatsd_executable_path", + "DD_DOGSTATSD_PIPE_NAME": "dogstatsd_named_pipe", + "DD_DOGSTATSD_PORT": "dogstatsd_port", + "DD_DOGSTATSD_SOCKET": "dogstatsd_socket", + "DD_DOGSTATSD_URL": "dogstatsd_url", + "DD_DOTNET_TRACER_CONFIG_FILE": "trace_config_file", + "DD_DYNAMIC_INSTRUMENTATION_DIAGNOSTICS_INTERVAL": "dynamic_instrumentation_diagnostics_interval", + "DD_DYNAMIC_INSTRUMENTATION_ENABLED": "dynamic_instrumentation_enabled", + "DD_DYNAMIC_INSTRUMENTATION_MAX_DEPTH_TO_SERIALIZE": "dynamic_instrumentation_serialization_max_depth", + "DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE": "dynamic_instrumentation_serialization_max_duration", + "DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS": "dynamic_instrumentation_redacted_identifiers", + "DD_DYNAMIC_INSTRUMENTATION_REDACTED_TYPES": "dynamic_instrumentation_redacted_types", + "DD_DYNAMIC_INSTRUMENTATION_SYMBOL_DATABASE_BATCH_SIZE_BYTES": "dynamic_instrumentation_symbol_database_batch_size_bytes", + "DD_DYNAMIC_INSTRUMENTATION_SYMBOL_DATABASE_UPLOAD_ENABLED": "dynamic_instrumentation_symbol_database_upload_enabled", + "DD_DYNAMIC_INSTRUMENTATION_UPLOAD_BATCH_SIZE": "dynamic_instrumentation_upload_batch_size", + "DD_DYNAMIC_INSTRUMENTATION_UPLOAD_FLUSH_INTERVAL": "dynamic_instrumentation_upload_interval", + "DD_ENV": "env", + "DD_EXCEPTION_DEBUGGING_CAPTURE_FULL_CALLSTACK_ENABLED": "dd_exception_debugging_capture_full_callstack_enabled", + "DD_EXCEPTION_DEBUGGING_ENABLED": "dd_exception_debugging_enabled", + "DD_EXCEPTION_DEBUGGING_MAX_EXCEPTION_ANALYSIS_LIMIT": "dd_exception_debugging_max_exception_analysis_limit", + "DD_EXCEPTION_DEBUGGING_MAX_FRAMES_TO_CAPTURE": "dd_exception_debugging_max_frames_to_capture", + "DD_EXCEPTION_DEBUGGING_RATE_LIMIT_SECONDS": "dd_exception_debugging_rate_limit_seconds", + "DD_EXCEPTION_REPLAY_CAPTURE_FULL_CALLSTACK_ENABLED": "dd_exception_replay_capture_full_callstack_enabled", + "DD_EXCEPTION_REPLAY_ENABLED": "dd_exception_replay_enabled", + "DD_EXCEPTION_REPLAY_MAX_EXCEPTION_ANALYSIS_LIMIT": "dd_exception_replay_max_exception_analysis_limit", + "DD_EXCEPTION_REPLAY_MAX_FRAMES_TO_CAPTURE": "dd_exception_replay_max_frames_to_capture", + "DD_EXCEPTION_REPLAY_RATE_LIMIT_SECONDS": "dd_exception_replay_rate_limit_seconds", + "DD_EXPERIMENTAL_API_SECURITY_ENABLED": "experimental_api_security_enabled", + "DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED": "experimental_appsec_standalone_enabled", + "DD_EXPERIMENTAL_APPSEC_USE_UNSAFE_ENCODER": "appsec_use_unsafe_encoder", + "DD_GIT_COMMIT_SHA": "commit_sha", + "DD_GIT_REPOSITORY_URL": "repository_url", + "DD_GRPC_CLIENT_ERROR_STATUSES": "trace_grpc_client_error_statuses", + "DD_GRPC_SERVER_ERROR_STATUSES": "trace_grpc_server_error_statuses", + "DD_HTTP_CLIENT_ERROR_STATUSES": "trace_http_client_error_statuses", + "DD_HTTP_SERVER_ERROR_STATUSES": "trace_http_server_error_statuses", + "DD_HTTP_SERVER_TAG_QUERY_STRING": "trace_http_server_tag_query_string_enabled", + "DD_HTTP_SERVER_TAG_QUERY_STRING_SIZE": "trace_http_server_tag_query_string_size", + "DD_IAST_COOKIE_FILTER_PATTERN": "iast_cookie_filter_pattern", + "DD_IAST_DB_ROWS_TO_TAINT": "iast_db_rows_to_taint", + "DD_IAST_DEDUPLICATION_ENABLED": "iast_deduplication_enabled", + "DD_IAST_ENABLED": "iast_enabled", + "DD_IAST_EXPERIMENTAL_PROPAGATION_ENABLED": "iast_experimental_propagation_enabled", + "DD_IAST_MAX_CONCURRENT_REQUESTS": "iast_max_concurrent_requests", + "DD_IAST_MAX_RANGE_COUNT": "iast_max_range_count", + "DD_IAST_REDACTION_ENABLED": "iast_redaction_enabled", + "DD_IAST_REDACTION_KEYS_REGEXP": "iast_redaction_keys_regexp", + "DD_IAST_REDACTION_NAME_PATTERN": "iast_redaction_name_pattern", + "DD_IAST_REDACTION_REGEXP_TIMEOUT": "iast_redaction_regexp_timeout", + "DD_IAST_REDACTION_VALUES_REGEXP": "iast_redaction_values_regexp", + "DD_IAST_REDACTION_VALUE_PATTERN": "iast_redaction_value_pattern", + "DD_IAST_REGEXP_TIMEOUT": "iast_regexp_timeout", + "DD_IAST_REQUEST_SAMPLING": "iast_request_sampling_percentage", + "DD_IAST_STACK_TRACE_ENABLED": "appsec_stack_trace_enabled", + "DD_IAST_TELEMETRY_VERBOSITY": "iast_telemetry_verbosity", + "DD_IAST_TRUNCATION_MAX_VALUE_LENGTH": "iast_truncation_max_value_length", + "DD_IAST_VULNERABILITIES_PER_REQUEST": "iast_vulnerability_per_request", + "DD_IAST_WEAK_CIPHER_ALGORITHMS": "iast_weak_cipher_algorithms", + "DD_IAST_WEAK_HASH_ALGORITHMS": "iast_weak_hash_algorithms", + "DD_INJECTION_ENABLED": "ssi_injection_enabled", + "DD_INJECT_FORCE": "ssi_forced_injection_enabled", + "DD_INJECT_FORCED": "dd_lib_injection_forced", + "DD_INSTRUMENTATION_TELEMETRY_AGENTLESS_ENABLED": "instrumentation_telemetry_agentless_enabled", + "DD_INSTRUMENTATION_TELEMETRY_AGENT_PROXY_ENABLED": "instrumentation_telemetry_agent_proxy_enabled", + "DD_INSTRUMENTATION_TELEMETRY_ENABLED": "instrumentation_telemetry_enabled", + "DD_INSTRUMENTATION_TELEMETRY_URL": "instrumentation_telemetry_agentless_url", + "DD_INTERAL_FORCE_SYMBOL_DATABASE_UPLOAD": "internal_force_symbol_database_upload", + "DD_INTERNAL_RCM_POLL_INTERVAL": "remote_config_poll_interval", + "DD_INTERNAL_TELEMETRY_DEBUG_ENABLED": "instrumentation_telemetry_debug_enabled", + "DD_INTERNAL_TELEMETRY_V2_ENABLED": "instrumentation_telemetry_v2_enabled", + "DD_INTERNAL_WAIT_FOR_DEBUGGER_ATTACH": "internal_wait_for_debugger_attach_enabled", + "DD_INTERNAL_WAIT_FOR_NATIVE_DEBUGGER_ATTACH": "internal_wait_for_native_debugger_attach_enabled", + "DD_LIB_INJECTED": "dd_lib_injected", + "DD_LIB_INJECTION_ATTEMPTED": "dd_lib_injection_attempted", + "DD_LOGS_DIRECT_SUBMISSION_BATCH_PERIOD_SECONDS": "logs_direct_submission_batch_period_seconds", + "DD_LOGS_DIRECT_SUBMISSION_HOST": "logs_direct_submission_host", + "DD_LOGS_DIRECT_SUBMISSION_INTEGRATIONS": "logs_direct_submission_integrations", + "DD_LOGS_DIRECT_SUBMISSION_MAX_BATCH_SIZE": "logs_direct_submission_max_batch_size", + "DD_LOGS_DIRECT_SUBMISSION_MAX_QUEUE_SIZE": "logs_direct_submission_max_queue_size", + "DD_LOGS_DIRECT_SUBMISSION_MINIMUM_LEVEL": "logs_direct_submission_minimum_level", + "DD_LOGS_DIRECT_SUBMISSION_SOURCE": "logs_direct_submission_source", + "DD_LOGS_DIRECT_SUBMISSION_TAGS": "logs_direct_submission_tags", + "DD_LOGS_DIRECT_SUBMISSION_URL": "logs_direct_submission_url", + "DD_LOGS_INJECTION": "logs_injection_enabled", + "DD_LOG_INJECTION": "logs_injection_enabled", + "DD_LOG_LEVEL": "agent_log_level", + "DD_MAX_LOGFILE_SIZE": "trace_log_file_max_size", + "DD_MAX_TRACES_PER_SECOND": "trace_rate_limit", + "DD_PROFILING_CODEHOTSPOTS_ENABLED": "profiling_codehotspots_enabled", + "DD_PROFILING_ENABLED": "profiling_enabled", + "DD_PROFILING_ENDPOINT_COLLECTION_ENABLED": "profiling_endpoint_collection_enabled", + "DD_PROPAGATION_STYLE_EXTRACT": "trace_propagation_style_extract", + "DD_PROPAGATION_STYLE_INJECT": "trace_propagation_style_inject", "DD_PROXY_HTTPS": "proxy_https", "DD_PROXY_NO_PROXY": "proxy_no_proxy", - "DD_TRACE_DEBUG_LOOKUP_MDTOKEN": "trace_lookup_mdtoken_enabled", + "DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS": "remote_config_poll_interval", + "DD_RUNTIME_METRICS_ENABLED": "runtime_metrics_enabled", + "DD_SERVICE": "service", + "DD_SERVICE_MAPPING": "dd_service_mapping", + "DD_SERVICE_NAME": "service", + "DD_SITE": "site", + "DD_SPAN_SAMPLING_RULES": "span_sample_rules", + "DD_SPAN_SAMPLING_RULES_FILE": "dd_span_sampling_rules_file", + "DD_SYMBOL_DATABASE_BATCH_SIZE_BYTES": "symbol_database_batch_size_bytes", + "DD_SYMBOL_DATABASE_THIRD_PARTY_DETECTION_EXCLUDES": "symbol_database_third_party_detection_excludes", + "DD_SYMBOL_DATABASE_THIRD_PARTY_DETECTION_INCLUDES": "symbol_database_third_party_detection_includes", + "DD_SYMBOL_DATABASE_UPLOAD_ENABLED": "symbol_database_upload_enabled", + "DD_SYMBOL_DATABASE_COMPRESSION_ENABLED": "symbol_database_compression_enabled", + "DD_TAGS": "agent_tags", + "DD_TELEMETRY_DEPENDENCY_COLLECTION_ENABLED": "instrumentation_telemetry_dependency_collection_enabled", + "DD_TELEMETRY_HEARTBEAT_INTERVAL": "instrumentation_telemetry_heartbeat_interval", + "DD_TELEMETRY_LOG_COLLECTION_ENABLED": "instrumentation_telemetry_log_collection_enabled", + "DD_TELEMETRY_METRICS_ENABLED": "instrumentation_telemetry_metrics_enabled", + "DD_TEST_SESSION_NAME": "test_session_name", + "DD_THIRD_PARTY_DETECTION_EXCLUDES": "third_party_detection_excludes", + "DD_THIRD_PARTY_DETECTION_INCLUDES": "third_party_detection_includes", + "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED": "trace_128_bits_id_enabled", + "DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED": "trace_128_bits_id_logging_enabled", + "DD_TRACE_ACTIVITY_LISTENER_ENABLED": "trace_activity_listener_enabled", + "DD_TRACE_AGENT_ARGS": "agent_trace_agent_excecutable_args", + "DD_TRACE_AGENT_HOSTNAME": "agent_host", + "DD_TRACE_AGENT_PATH": "agent_trace_agent_excecutable_path", + "DD_TRACE_AGENT_PORT": "trace_agent_port", + "DD_TRACE_AGENT_URL": "trace_agent_url", + "DD_TRACE_ANALYTICS_ENABLED": "trace_analytics_enabled", + "DD_TRACE_BAGGAGE_MAX_BYTES": "trace_baggage_max_bytes", + "DD_TRACE_BAGGAGE_MAX_ITEMS": "trace_baggage_max_items", + "DD_TRACE_BATCH_INTERVAL": "trace_serialization_batch_interval", + "DD_TRACE_BUFFER_SIZE": "trace_serialization_buffer_size", + "DD_TRACE_CLIENT_IP_ENABLED": "trace_client_ip_enabled", + "DD_TRACE_CLIENT_IP_HEADER": "trace_client_ip_header", + "DD_TRACE_COMMANDS_COLLECTION_ENABLED": "trace_commands_collection_enabled", + "DD_TRACE_COMPUTE_STATS": "dd_trace_compute_stats", + "DD_TRACE_CONFIG_FILE": "trace_config_file", + "DD_TRACE_DEBUG": "trace_debug_enabled", "DD_TRACE_DEBUG_LOOKUP_FALLBACK": "trace_lookup_fallback_enabled", - "DD_TRACE_ROUTE_TEMPLATE_RESOURCE_NAMES_ENABLED": "trace_route_template_resource_names_enabled", + "DD_TRACE_DEBUG_LOOKUP_MDTOKEN": "trace_lookup_mdtoken_enabled", "DD_TRACE_DELAY_WCF_INSTRUMENTATION_ENABLED": "trace_delay_wcf_instrumentation_enabled", - "DD_TRACE_WCF_WEB_HTTP_RESOURCE_NAMES_ENABLED": "trace_wcf_web_http_resource_names_enabled", - "DD_TRACE_WCF_RESOURCE_OBFUSCATION_ENABLED": "trace_wcf_obfuscation_enabled", + "DD_TRACE_DELEGATE_SAMPLING": "trace_sample_delegation", + "DD_TRACE_DISABLED_ADONET_COMMAND_TYPES": "trace_disabled_adonet_command_types", + "DD_TRACE_ENABLED": "trace_enabled", + "DD_TRACE_EXPAND_ROUTE_TEMPLATES_ENABLED": "trace_route_template_expansion_enabled", + "DD_TRACE_GIT_METADATA_ENABLED": "git_metadata_enabled", + "DD_TRACE_GLOBAL_TAGS": "trace_tags", + "DD_TRACE_HEADER_TAGS": "trace_header_tags", "DD_TRACE_HEADER_TAG_NORMALIZATION_FIX_ENABLED": "trace_header_tag_normalization_fix_enabled", - "DD_TRACE_OTEL_ENABLED": "trace_otel_enabled", - "DD_TRACE_OTEL_LEGACY_OPERATION_NAME_ENABLED": "trace_otel_legacy_operation_name_enabled", - "DD_TRACE_ACTIVITY_LISTENER_ENABLED": "trace_activity_listener_enabled", - "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED": "trace_128_bits_id_enabled", - "DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED": "trace_128_bits_id_logging_enabled", "DD_TRACE_HEALTH_METRICS_ENABLED": "dd_trace_health_metrics_enabled", - "DD_LIB_INJECTION_ATTEMPTED": "dd_lib_injection_attempted", - "DD_LIB_INJECTED": "dd_lib_injected", - "DD_INJECT_FORCED": "dd_lib_injection_forced", - "DD_SPAN_SAMPLING_RULES_FILE": "dd_span_sampling_rules_file", - "DD_TRACE_COMPUTE_STATS": "dd_trace_compute_stats", - "DD_EXCEPTION_DEBUGGING_ENABLED": "dd_exception_debugging_enabled", - "DD_EXCEPTION_DEBUGGING_MAX_FRAMES_TO_CAPTURE": "dd_exception_debugging_max_frames_to_capture", - "DD_EXCEPTION_DEBUGGING_CAPTURE_FULL_CALLSTACK_ENABLED": "dd_exception_debugging_capture_full_callstack_enabled", - "DD_EXCEPTION_DEBUGGING_RATE_LIMIT_SECONDS": "dd_exception_debugging_rate_limit_seconds", - "DD_EXCEPTION_DEBUGGING_MAX_EXCEPTION_ANALYSIS_LIMIT": "dd_exception_debugging_max_exception_analysis_limit", - "DD_EXCEPTION_REPLAY_ENABLED": "dd_exception_replay_enabled", - "DD_EXCEPTION_REPLAY_MAX_FRAMES_TO_CAPTURE": "dd_exception_replay_max_frames_to_capture", - "DD_EXCEPTION_REPLAY_CAPTURE_FULL_CALLSTACK_ENABLED": "dd_exception_replay_capture_full_callstack_enabled", - "DD_EXCEPTION_REPLAY_RATE_LIMIT_SECONDS": "dd_exception_replay_rate_limit_seconds", - "DD_EXCEPTION_REPLAY_MAX_EXCEPTION_ANALYSIS_LIMIT": "dd_exception_replay_max_exception_analysis_limit", - "exception_replay_capture_interval_seconds": "dd_exception_replay_capture_interval_seconds", - "exception_replay_capture_max_frames": "dd_exception_replay_capture_max_frames", - "exception_replay_enabled": "dd_exception_replay_enabled", + "DD_TRACE_HTTP_CLIENT_ERROR_STATUSES": "trace_http_client_error_statuses", + "DD_TRACE_HTTP_CLIENT_EXCLUDED_URL_SUBSTRINGS": "trace_http_client_excluded_urls", + "DD_TRACE_HTTP_CLIENT_TAG_QUERY_STRING": "trace_http_client_tag_query_string", + "DD_TRACE_HTTP_SERVER_ERROR_STATUSES": "trace_http_server_error_statuses", + "DD_TRACE_KAFKA_CREATE_CONSUMER_SCOPE_ENABLED": "trace_kafka_create_consumer_scope_enabled", + "DD_TRACE_LOGFILE_RETENTION_DAYS": "trace_log_file_retention_days", + "DD_TRACE_LOGGING_RATE": "trace_log_rate", + "DD_TRACE_LOG_DIRECTORY": "trace_log_directory", + "DD_TRACE_LOG_PATH": "trace_log_path", + "DD_TRACE_LOG_SINKS": "trace_log_sinks", + "DD_TRACE_METHODS": "trace_methods", + "DD_TRACE_METRICS_ENABLED": "trace_metrics_enabled", "DD_TRACE_OBFUSCATION_QUERY_STRING_PATTERN": "dd_trace_obfuscation_query_string_pattern", - "DD_CALL_BASIC_CONFIG": "dd_call_basic_config", - "DD_SERVICE_MAPPING": "dd_service_mapping", - "DD_INSTRUMENTATION_TELEMETRY_ENABLED": "instrumentation_telemetry_enabled", - "DD_INSTRUMENTATION_TELEMETRY_AGENTLESS_ENABLED": "instrumentation_telemetry_agentless_enabled", - "DD_INSTRUMENTATION_TELEMETRY_AGENT_PROXY_ENABLED": "instrumentation_telemetry_agent_proxy_enabled", - "DD_INSTRUMENTATION_TELEMETRY_URL": "instrumentation_telemetry_agentless_url", - "DD_TELEMETRY_HEARTBEAT_INTERVAL": "instrumentation_telemetry_heartbeat_interval", - "DD_TELEMETRY_DEPENDENCY_COLLECTION_ENABLED": "instrumentation_telemetry_dependency_collection_enabled", - "DD_TELEMETRY_LOG_COLLECTION_ENABLED": "instrumentation_telemetry_log_collection_enabled", - "DD_TELEMETRY_METRICS_ENABLED": "instrumentation_telemetry_metrics_enabled", - "DD_INTERNAL_TELEMETRY_V2_ENABLED": "instrumentation_telemetry_v2_enabled", - "DD_INTERNAL_TELEMETRY_DEBUG_ENABLED": "instrumentation_telemetry_debug_enabled", - "DD_APPSEC_ENABLED": "appsec_enabled", - "DD_APPSEC_RULES": "appsec_rules", - "DD_APPSEC_IPHEADER": "appsec_ip_header", - "DD_APPSEC_EXTRA_HEADERS": "appsec_extra_headers", - "DD_APPSEC_KEEP_TRACES": "appsec_force_keep_traces_enabled", - "DD_APPSEC_TRACE_RATE_LIMIT": "appsec_trace_rate_limit", - "DD_APPSEC_WAF_TIMEOUT": "appsec_waf_timeout", - "DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP": "appsec_obfuscation_parameter_key_regexp", - "DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP": "appsec_obfuscation_parameter_value_regexp", - "DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML": "appsec_blocked_template_html", - "DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON": "appsec_blocked_template_json", - "DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING": "appsec_auto_user_events_tracking", - "DD_APPSEC_RASP_ENABLED": "appsec_rasp_enabled", - "DD_APPSEC_STACK_TRACE_ENABLED": "appsec_stack_trace_enabled", - "DD_APPSEC_MAX_STACK_TRACES": "appsec_max_stack_traces", - "DD_APPSEC_MAX_STACK_TRACE_DEPTH": "appsec_max_stack_trace_depth", - "DD_APPSEC_MAX_STACK_TRACE_DEPTH_TOP_PERCENT": "appsec_max_stack_trace_depth_top_percent", - "DD_APPSEC_SCA_ENABLED": "appsec_sca_enabled", - "DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE": "appsec_auto_user_instrumentation_mode", - "DD_EXPERIMENTAL_APPSEC_USE_UNSAFE_ENCODER": "appsec_use_unsafe_encoder", - "DD_API_SECURITY_REQUEST_SAMPLE_RATE":"api_security_request_sample_rate", - "DD_API_SECURITY_MAX_CONCURRENT_REQUESTS":"api_security_max_concurrent_requests", - "DD_API_SECURITY_SAMPLE_DELAY":"api_security_sample_delay", - "DD_API_SECURITY_ENABLED":"api_security_enabled", - "DD_EXPERIMENTAL_API_SECURITY_ENABLED":"experimental_api_security_enabled", - "DD_APPSEC_WAF_DEBUG": "appsec_waf_debug_enabled", - "DD_AZURE_APP_SERVICES": "aas_enabled", - "DD_AAS_DOTNET_EXTENSION_VERSION": "aas_site_extensions_version", - "WEBSITE_OWNER_NAME": "aas_website_owner_name", - "WEBSITE_RESOURCE_GROUP": "aas_website_resource_group", - "WEBSITE_SITE_NAME": "aas_website_site_name", + "DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP": "trace_obfuscation_query_string_regexp", + "DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP_TIMEOUT": "trace_obfuscation_query_string_regexp_timeout", + "DD_TRACE_OTEL_ENABLED": "trace_otel_enabled", + "DD_TRACE_OTEL_LEGACY_OPERATION_NAME_ENABLED": "trace_otel_legacy_operation_name_enabled", + "DD_TRACE_PARTIAL_FLUSH_ENABLED": "trace_partial_flush_enabled", + "DD_TRACE_PARTIAL_FLUSH_MIN_SPANS": "trace_partial_flush_min_spans", + "DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED": "trace_peer_service_defaults_enabled", + "DD_TRACE_PEER_SERVICE_MAPPING": "trace_peer_service_mapping", + "DD_TRACE_PIPE_NAME": "trace_agent_named_pipe", + "DD_TRACE_PIPE_TIMEOUT_MS": "trace_agent_named_pipe_timeout_ms", + "DD_TRACE_PROPAGATION_EXTRACT_FIRST": "trace_propagation_extract_first", + "DD_TRACE_PROPAGATION_STYLE": "trace_propagation_style", + "DD_TRACE_PROPAGATION_STYLE_EXTRACT": "trace_propagation_style_extract", + "DD_TRACE_PROPAGATION_STYLE_INJECT": "trace_propagation_style_inject", + "DD_TRACE_RATE_LIMIT": "trace_rate_limit", + "DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED": "trace_remove_integration_service_names_enabled", + "DD_TRACE_ROUTE_TEMPLATE_RESOURCE_NAMES_ENABLED": "trace_route_template_resource_names_enabled", + "DD_TRACE_SAMPLING_RULES": "trace_sample_rules", + "DD_TRACE_SAMPLING_RULES_FORMAT": "trace_sampling_rules_format", + "DD_TRACE_SPAN_ATTRIBUTE_SCHEMA": "trace_span_attribute_schema", + "DD_TRACE_STARTUP_LOGS": "trace_startup_logs_enabled", + "DD_TRACE_STATS_COMPUTATION_ENABLED": "trace_stats_computation_enabled", + "DD_TRACE_WCF_RESOURCE_OBFUSCATION_ENABLED": "trace_wcf_obfuscation_enabled", + "DD_TRACE_WCF_WEB_HTTP_RESOURCE_NAMES_ENABLED": "trace_wcf_web_http_resource_names_enabled", + "DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH": "trace_x_datadog_tags_max_length", + "DD_VERSION": "application_version", "FUNCTIONS_EXTENSION_VERSION": "aas_functions_runtime_version", "FUNCTIONS_WORKER_RUNTIME": "aas_functions_worker_runtime", - "COMPUTERNAME": "aas_instance_name", - "WEBSITE_INSTANCE_ID": "aas_website_instance_id", - "WEBSITE_OS": "aas_website_os", - "WEBSITE_SKU": "aas_website_sku", "FUNCTION_NAME": "gcp_deprecated_function_name", + "FUNCTION_TARGET": "gcp_function_target", "GCP_PROJECT": "gcp_deprecated_project", "K_SERVICE": "gcp_function_name", - "FUNCTION_TARGET": "gcp_function_target", - "DD_AAS_ENABLE_CUSTOM_TRACING": "aas_custom_tracing_enabled", - "DD_AAS_ENABLE_CUSTOM_METRICS": "aas_custom_metrics_enabled", - "DD_DYNAMIC_INSTRUMENTATION_ENABLED": "dynamic_instrumentation_enabled", - "DD_DEBUGGER_MAX_DEPTH_TO_SERIALIZE": "dynamic_instrumentation_serialization_max_depth", - "DD_DEBUGGER_MAX_TIME_TO_SERIALIZE": "dynamic_instrumentation_serialization_max_duration", - "DD_DEBUGGER_UPLOAD_BATCH_SIZE": "dynamic_instrumentation_upload_batch_size", - "DD_DEBUGGER_DIAGNOSTICS_INTERVAL": "dynamic_instrumentation_diagnostics_interval", - "DD_DEBUGGER_UPLOAD_FLUSH_INTERVAL": "dynamic_instrumentation_upload_interval", - "DD_DYNAMIC_INSTRUMENTATION_MAX_DEPTH_TO_SERIALIZE": "dynamic_instrumentation_serialization_max_depth", - "DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE": "dynamic_instrumentation_serialization_max_duration", - "DD_DYNAMIC_INSTRUMENTATION_UPLOAD_BATCH_SIZE": "dynamic_instrumentation_upload_batch_size", - "DD_DYNAMIC_INSTRUMENTATION_DIAGNOSTICS_INTERVAL": "dynamic_instrumentation_diagnostics_interval", - "DD_DYNAMIC_INSTRUMENTATION_UPLOAD_FLUSH_INTERVAL": "dynamic_instrumentation_upload_interval", - "DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS": "dynamic_instrumentation_redacted_identifiers", - "DD_DYNAMIC_INSTRUMENTATION_REDACTED_TYPES": "dynamic_instrumentation_redacted_types", - "dynamic_instrumentation.redacted_types": "dynamic_instrumentation_redacted_types", + "OTEL_LOGS_EXPORTER": "otel_logs_exporter", + "OTEL_LOG_LEVEL": "otel_log_level", + "OTEL_METRICS_EXPORTER": "otel_metrics_exporter", + "OTEL_PROPAGATORS": "otel_propagators", + "OTEL_RESOURCE_ATTRIBUTES": "otel_resource_attributes", + "OTEL_SDK_DISABLED": "otel_sdk_disabled", + "OTEL_SERVICE_NAME": "otel_service_name", + "OTEL_TRACES_EXPORTER": "otel_traces_exporter", + "OTEL_TRACES_SAMPLER": "otel_traces_sampler", + "OTEL_TRACES_SAMPLER_ARG": "otel_traces_sampler_arg", + "WEBSITE_INSTANCE_ID": "aas_website_instance_id", + "WEBSITE_OS": "aas_website_os", + "WEBSITE_OWNER_NAME": "aas_website_owner_name", + "WEBSITE_RESOURCE_GROUP": "aas_website_resource_group", + "WEBSITE_SITE_NAME": "aas_website_site_name", + "WEBSITE_SKU": "aas_website_sku", + "_DD_TRACE_STATS_COMPUTATION_INTERVAL": "trace_stats_computation_interval", + "_dd_appsec_deduplication_enabled": "appsec_deduplication_enabled", + "_dd_iast_debug": "iast_debug_enabled", + "_dd_iast_lazy_taint": "iast_lazy_taint", + "_dd_iast_propagation_debug": "iast_propagation_debug", + "_dd_inject_was_attempted": "trace_inject_was_attempted", + "_dd_llmobs_evaluator_sampling_rules": "llmobs_evaluator_sampling_rules", + "aas_app_type": "aas_app_type", + "aas_configuration_error": "aas_configuration_error", + "aas_functions_runtime_version": "aas_functions_runtime_version", + "aas_siteextensions_version": "aas_site_extensions_version", + "activity_listener_enabled": "activity_listener_enabled", + "agent_feature_drop_p0s": "agent_feature_drop_p0s", + "agent_transport": "agent_transport", + "agent_url": "trace_agent_url", + "analytics_enabled": "analytics_enabled", + "appsec.apiSecurity.enabled": "api_security_enabled", + "appsec.apiSecurity.requestSampling": "api_security_request_sample_rate", + "appsec.apiSecurity.sampleDelay": "api_security_sample_delay", + "appsec.blockedTemplateGraphql": "appsec_blocked_template_graphql", + "appsec.blockedTemplateHtml": "appsec_blocked_template_html", + "appsec.blockedTemplateJson": "appsec_blocked_template_json", + "appsec.customRulesProvided": "appsec_rules_custom_provided", + "appsec.enabled": "appsec_enabled", + "appsec.eventTracking": "appsec_auto_user_events_tracking", + "appsec.eventTracking.mode": "appsec_auto_user_events_tracking", + "appsec.obfuscatorKeyRegex": "appsec_obfuscation_parameter_key_regexp", + "appsec.obfuscatorValueRegex": "appsec_obfuscation_parameter_value_regexp", + "appsec.rasp.enabled": "appsec_rasp_enabled", + "appsec.rasp_enabled": "appsec_rasp_enabled", + "appsec.rateLimit": "appsec_rate_limit", + "appsec.rules": "appsec_rules", + "appsec.rules.metadata.rules_version": "appsec_rules_metadata_rules_version", + "appsec.rules.version": "appsec_rules_version", + "appsec.sca.enabled": "appsec_sca_enabled", + "appsec.sca_enabled": "appsec_sca_enabled", + "appsec.stackTrace.enabled": "appsec_stack_trace_enabled", + "appsec.stackTrace.maxDepth": "appsec_max_stack_trace_depth", + "appsec.stackTrace.maxStackTraces": "appsec_max_stack_traces", + "appsec.standalone.enabled": "experimental_appsec_standalone_enabled", + "appsec.testing": "appsec_testing", + "appsec.trace.rate.limit": "appsec_trace_rate_limit", + "appsec.waf.timeout": "appsec_waf_timeout", + "appsec.wafTimeout": "appsec_waf_timeout", + "autofinish_spans": "trace_auto_finish_spans_enabled", + "autoload_no_compile": "autoload_no_compile", + "aws.dynamoDb.tablePrimaryKeys": "aws_dynamodb_table_primary_keys", + "baggageMaxBytes": "trace_baggage_max_bytes", + "baggageMaxItems": "trace_baggage_max_items", + "ciVisAgentlessLogSubmissionEnabled": "ci_visibility_agentless_enabled", + "ciVisibilityTestSessionName": "test_session_name", + "civisibility.agentless.enabled": "ci_visibility_agentless_enabled", + "civisibility.enabled": "ci_visibility_enabled", + "clientIpEnabled": "trace_client_ip_enabled", + "clientIpHeader": "trace_client_ip_header", + "clientIpHeaderDisabled": "client_ip_header_disabled", + "cloudPayloadTagging.maxDepth": "cloud_payload_tagging_max_depth", + "cloudPayloadTagging.requestsEnabled": "cloud_payload_tagging_requests_enabled", + "cloudPayloadTagging.responsesEnabled": "cloud_payload_tagging_responses_enabled", + "cloudPayloadTagging.rules.aws.eventbridge.expand": "cloud_payload_tagging_rules_aws_eventbridge_expand", + "cloudPayloadTagging.rules.aws.eventbridge.request": "cloud_payload_tagging_rules_aws_eventbridge_request", + "cloudPayloadTagging.rules.aws.eventbridge.response": "cloud_payload_tagging_rules_aws_eventbridge_response", + "cloudPayloadTagging.rules.aws.kinesis.expand": "cloud_payload_tagging_rules_aws_kinesis_expand", + "cloudPayloadTagging.rules.aws.kinesis.request": "cloud_payload_tagging_rules_aws_kinesis_request", + "cloudPayloadTagging.rules.aws.kinesis.response": "cloud_payload_tagging_rules_aws_kinesis_response", + "cloudPayloadTagging.rules.aws.s3.expand": "cloud_payload_tagging_rules_aws_s3_expand", + "cloudPayloadTagging.rules.aws.s3.request": "cloud_payload_tagging_rules_aws_s3_request", + "cloudPayloadTagging.rules.aws.s3.response": "cloud_payload_tagging_rules_aws_s3_response", + "cloudPayloadTagging.rules.aws.sns.expand": "cloud_payload_tagging_rules_aws_sns_expand", + "cloudPayloadTagging.rules.aws.sns.request": "cloud_payload_tagging_rules_aws_sns_request", + "cloudPayloadTagging.rules.aws.sns.response": "cloud_payload_tagging_rules_aws_sns_response", + "cloudPayloadTagging.rules.aws.sqs.expand": "cloud_payload_tagging_rules_aws_sqs_expand", + "cloudPayloadTagging.rules.aws.sqs.request": "cloud_payload_tagging_rules_aws_sqs_request", + "cloudPayloadTagging.rules.aws.sqs.response": "cloud_payload_tagging_rules_aws_sqs_response", + "cloud_hosting": "cloud_hosting_provider", + "codeOriginForSpans.enabled": "code_origin_for_spans_enabled", + "code_hotspots_enabled": "code_hotspots_enabled", + "commitSHA": "commit_sha", + "crashtracking.enabled": "crashtracking_enabled", + "crashtracking_alt_stack": "crashtracking_alt_stack", + "crashtracking_available": "crashtracking_available", + "crashtracking_debug_url": "crashtracking_debug_url", + "crashtracking_enabled": "crashtracking_enabled", + "crashtracking_stacktrace_resolver": "crashtracking_stacktrace_resolver", + "crashtracking_started": "crashtracking_started", + "crashtracking_stderr_filename": "crashtracking_stderr_filename", + "crashtracking_stdout_filename": "crashtracking_stdout_filename", + "cws.enabled": "cws_enabled", + "data.streams.enabled": "data_streams_enabled", + "data_streams_enabled": "data_streams_enabled", + "dbmPropagationMode": "dbm_propagation_mode", + "dbm_propagation_mode": "dbm_propagation_mode", + "dd.trace.debug": "trace_debug_enabled", + "dd_agent_host": "agent_host", + "dd_agent_port": "trace_agent_port", + "dd_analytics_enabled": "analytics_enabled", + "dd_api_security_parse_response_body": "appsec_parse_response_body", + "dd_appsec_automated_user_events_tracking_enabled": "appsec_auto_user_events_tracking_enabled", + "dd_civisibility_log_level": "ci_visibility_log_level", + "dd_crashtracking_create_alt_stack": "crashtracking_create_alt_stack", + "dd_crashtracking_debug_url": "crashtracking_debug_url", + "dd_crashtracking_enabled": "crashtracking_enabled", + "dd_crashtracking_stacktrace_resolver": "crashtracking_stacktrace_resolver", + "dd_crashtracking_stderr_filename": "crashtracking_stderr_filename", + "dd_crashtracking_stdout_filename": "crashtracking_stdout_filename", + "dd_crashtracking_tags": "crashtracking_tags", + "dd_crashtracking_use_alt_stack": "crashtracking_alt_stack", + "dd_crashtracking_wait_for_receiver": "crashtracking_wait_for_receiver", + "dd_dynamic_instrumentation_max_payload_size": "dynamic_instrumentation_max_payload_size", + "dd_dynamic_instrumentation_metrics_enabled": "dynamic_instrumentation_metrics_enabled", + "dd_dynamic_instrumentation_upload_timeout": "dynamic_instrumentation_upload_timeout", + "dd_http_client_tag_query_string": "trace_http_client_tag_query_string", + "dd_iast_redaction_value_numeral": "iast_redaction_value_numeral", + "dd_instrumentation_install_id": "instrumentation_install_id", + "dd_instrumentation_install_type": "instrumentation_install_type", + "dd_llmobs_agentless_enabled": "llmobs_agentless_enabled", + "dd_llmobs_enabled": "llmobs_enabled", + "dd_llmobs_ml_app": "llmobs_ml_app", + "dd_llmobs_sample_rate": "llmobs_sample_rate", + "dd_priority_sampling": "trace_priority_sampling_enabled", + "dd_profiling_agentless": "profiling_agentless", + "dd_profiling_api_timeout": "profiling_api_timeout", + "dd_profiling_capture_pct": "profiling_capture_pct", + "dd_profiling_enable_asserts": "profiling_enable_asserts", + "dd_profiling_enable_code_provenance": "profiling_enable_code_provenance", + "dd_profiling_export_libdd_enabled": "profiling_export_libdd_enabled", + "dd_profiling_export_py_enabled": "profiling_export_py_enabled", + "dd_profiling_force_legacy_exporter": "profiling_force_legacy_exporter", + "dd_profiling_heap_enabled": "profiling_heap_enabled", + "dd_profiling_heap_sample_size": "profiling_heap_sample_size", + "dd_profiling_ignore_profiler": "profiling_ignore_profiler", + "dd_profiling_lock_enabled": "profiling_lock_enabled", + "dd_profiling_lock_name_inspect_dir": "profiling_lock_name_inspect_dir", + "dd_profiling_max_events": "profiling_max_events", + "dd_profiling_max_frames": "profiling_max_frames", + "dd_profiling_max_time_usage_pct": "profiling_max_time_usage_pct", + "dd_profiling_memory_enabled": "profiling_memory_enabled", + "dd_profiling_memory_events_buffer": "profiling_memory_events_buffer", + "dd_profiling_output_pprof": "profiling_output_pprof", + "dd_profiling_sample_pool_capacity": "profiling_sample_pool_capacity", + "dd_profiling_stack_enabled": "profiling_stack_enabled", + "dd_profiling_stack_v2_enabled": "profiling_stack_v2_enabled", + "dd_profiling_tags": "profiling_tags", + "dd_profiling_timeline_enabled": "profiling_timeline_enabled", + "dd_profiling_upload_interval": "profiling_upload_interval", + "dd_remote_configuration_enabled": "remote_config_enabled", + "dd_remoteconfig_poll_seconds": "remote_config_poll_interval", + "dd_symbol_database_includes": "symbol_database_includes", + "dd_testing_raise": "testing_raise", + "dd_trace_agent_timeout_seconds": "trace_agent_timeout", + "dd_trace_api_version": "trace_api_version", + "dd_trace_propagation_http_baggage_enabled": "trace_propagation_http_baggage_enabled", + "dd_trace_report_hostname": "trace_report_hostname", + "dd_trace_sample_rate": "trace_sample_rate", + "dd_trace_span_links_enabled": "trace_span_links_enabled", + "dd_trace_span_traceback_max_size": "trace_span_traceback_max_size", + "dd_trace_writer_buffer_size_bytes": "trace_serialization_buffer_size", + "dd_trace_writer_interval_seconds": "trace_agent_flush_interval", + "dd_trace_writer_max_payload_size_bytes": "trace_agent_max_payload_size", + "dd_trace_writer_reuse_connections": "trace_agent_reuse_connections", + "ddtrace_auto_used": "ddtrace_auto_used", + "ddtrace_bootstrapped": "ddtrace_bootstrapped", + "debug": "trace_debug_enabled", + "debug_stack_enabled": "debug_stack_enabled", + "discovery": "agent_discovery_enabled", + "distributed_tracing": "trace_distributed_trace_enabled", + "dogstatsd.hostname": "dogstatsd_hostname", + "dogstatsd.port": "dogstatsd_port", + "dogstatsd.start-delay": "dogstatsd_start_delay", + "dogstatsd_addr": "dogstatsd_url", + "dogstatsd_url": "dogstatsd_url", + "dsmEnabled": "data_streams_enabled", + "dynamic.instrumentation.classfile.dump.enabled": "dynamic_instrumentation_classfile_dump_enabled", + "dynamic.instrumentation.enabled": "dynamic_instrumentation_enabled", + "dynamic.instrumentation.metrics.enabled": "dynamic_instrumentation_metrics_enabled", + "dynamicInstrumentationEnabled": "dynamic_instrumentation_enabled", + "dynamicInstrumentationRedactedIdentifiers": "dynamic_instrumentation_redacted_identifiers", + "dynamicInstrumentationRedactionExcludedIdentifiers": "dynamic_instrumentation_redaction_excluded_indentifiers", + "dynamic_instrumentation.enabled": "dynamic_instrumentation_enabled", "dynamic_instrumentation.redacted_identifiers": "dynamic_instrumentation_redacted_identifiers", - "DD_SYMBOL_DATABASE_BATCH_SIZE_BYTES": "symbol_database_batch_size_bytes", - "DD_DYNAMIC_INSTRUMENTATION_SYMBOL_DATABASE_BATCH_SIZE_BYTES": "dynamic_instrumentation_symbol_database_batch_size_bytes", - "DD_SYMBOL_DATABASE_UPLOAD_ENABLED": "symbol_database_upload_enabled", - "DD_DYNAMIC_INSTRUMENTATION_SYMBOL_DATABASE_UPLOAD_ENABLED": "dynamic_instrumentation_symbol_database_upload_enabled", - "DD_INTERAL_FORCE_SYMBOL_DATABASE_UPLOAD": "internal_force_symbol_database_upload", - "DD_THIRD_PARTY_DETECTION_INCLUDES": "third_party_detection_includes", - "DD_THIRD_PARTY_DETECTION_EXCLUDES": "third_party_detection_excludes", - "DD_SYMBOL_DATABASE_THIRD_PARTY_DETECTION_INCLUDES": "symbol_database_third_party_detection_includes", - "DD_SYMBOL_DATABASE_THIRD_PARTY_DETECTION_EXCLUDES": "symbol_database_third_party_detection_excludes", - "DD_CODE_ORIGIN_FOR_SPANS_ENABLED": "code_origin_for_spans_enabled", - "DD_CODE_ORIGIN_FOR_SPANS_MAX_USER_FRAMES": "code_origin_for_spans_max_user_frames", - "DD_LOGS_DIRECT_SUBMISSION_INTEGRATIONS": "logs_direct_submission_integrations", - "DD_LOGS_DIRECT_SUBMISSION_HOST": "logs_direct_submission_host", - "DD_LOGS_DIRECT_SUBMISSION_SOURCE": "logs_direct_submission_source", - "DD_LOGS_DIRECT_SUBMISSION_TAGS": "logs_direct_submission_tags", - "DD_LOGS_DIRECT_SUBMISSION_URL": "logs_direct_submission_url", - "DD_LOGS_DIRECT_SUBMISSION_MINIMUM_LEVEL": "logs_direct_submission_minimum_level", - "DD_LOGS_DIRECT_SUBMISSION_MAX_BATCH_SIZE": "logs_direct_submission_max_batch_size", - "DD_LOGS_DIRECT_SUBMISSION_MAX_QUEUE_SIZE": "logs_direct_submission_max_queue_size", - "DD_LOGS_DIRECT_SUBMISSION_BATCH_PERIOD_SECONDS": "logs_direct_submission_batch_period_seconds", - "DD_AGENT_HOST": "agent_host", - "DATADOG_TRACE_AGENT_HOSTNAME": "agent_host", - "DD_TRACE_AGENT_HOSTNAME": "agent_host", - "DD_TRACE_AGENT_PORT": "trace_agent_port", - "DATADOG_TRACE_AGENT_PORT": "trace_agent_port", - "DD_TRACE_PIPE_NAME": "trace_agent_named_pipe", - "DD_TRACE_PIPE_TIMEOUT_MS": "trace_agent_named_pipe_timeout_ms", - "DD_DOGSTATSD_PIPE_NAME": "dogstatsd_named_pipe", - "DD_APM_RECEIVER_PORT": "trace_agent_port", - "DD_TRACE_AGENT_URL": "trace_agent_url", - "DD_DOGSTATSD_PORT": "dogstatsd_port", - "DD_TRACE_PARTIAL_FLUSH_ENABLED": "trace_partial_flush_enabled", - "DD_TRACE_PARTIAL_FLUSH_MIN_SPANS": "trace_partial_flush_min_spans", - "DD_APM_RECEIVER_SOCKET": "trace_agent_socket", - "DD_DOGSTATSD_SOCKET": "dogstatsd_socket", - "DD_DOGSTATSD_URL": "dogstatsd_url", - "DD_IAST_ENABLED": "iast_enabled", - "DD_IAST_WEAK_HASH_ALGORITHMS": "iast_weak_hash_algorithms", - "DD_IAST_WEAK_CIPHER_ALGORITHMS": "iast_weak_cipher_algorithms", - "DD_IAST_DEDUPLICATION_ENABLED": "iast_deduplication_enabled", - "DD_IAST_REQUEST_SAMPLING": "iast_request_sampling_percentage", - "DD_IAST_MAX_CONCURRENT_REQUESTS": "iast_max_concurrent_requests", - "DD_IAST_MAX_RANGE_COUNT": "iast_max_range_count", - "DD_IAST_VULNERABILITIES_PER_REQUEST": "iast_vulnerability_per_request", - "DD_IAST_REDACTION_ENABLED": "iast_redaction_enabled", - "DD_IAST_REDACTION_KEYS_REGEXP": "iast_redaction_keys_regexp", - "DD_IAST_REDACTION_VALUES_REGEXP": "iast_redaction_values_regexp", - "DD_IAST_REDACTION_NAME_PATTERN": "iast_redaction_name_pattern", - "DD_IAST_REDACTION_VALUE_PATTERN": "iast_redaction_value_pattern", - "DD_IAST_REDACTION_REGEXP_TIMEOUT": "iast_redaction_regexp_timeout", - "DD_IAST_REGEXP_TIMEOUT": "iast_regexp_timeout", - "DD_IAST_TELEMETRY_VERBOSITY": "iast_telemetry_verbosity", - "DD_IAST_TRUNCATION_MAX_VALUE_LENGTH": "iast_truncation_max_value_length", - "DD_IAST_DB_ROWS_TO_TAINT": "iast_db_rows_to_taint", - "DD_IAST_COOKIE_FILTER_PATTERN": "iast_cookie_filter_pattern", - "DD_TRACE_STARTUP_LOGS": "trace_startup_logs_enabled", - "DD_TRACE_DISABLED_ADONET_COMMAND_TYPES": "trace_disabled_adonet_command_types", - "DD_MAX_LOGFILE_SIZE": "trace_log_file_max_size", - "DD_TRACE_LOGGING_RATE": "trace_log_rate", - "DD_TRACE_LOG_PATH": "trace_log_path", - "DD_TRACE_LOG_DIRECTORY": "trace_log_directory", - "DD_TRACE_LOGFILE_RETENTION_DAYS": "trace_log_file_retention_days", - "DD_TRACE_LOG_SINKS": "trace_log_sinks", - "DD_TRACE_COMMANDS_COLLECTION_ENABLED": "trace_commands_collection_enabled", - "DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS": "remote_config_poll_interval", + "dynamic_instrumentation.redacted_types": "dynamic_instrumentation_redacted_types", + "enabled": "trace_enabled", + "env": "env", + "environment_fulltrust_appdomain": "environment_fulltrust_appdomain_enabled", + "exception_replay_capture_interval_seconds": "dd_exception_replay_capture_interval_seconds", + "exception_replay_capture_max_frames": "dd_exception_replay_capture_max_frames", + "exception_replay_enabled": "dd_exception_replay_enabled", + "experimental.b3": "experimental_b3", + "experimental.enableGetRumData": "experimental_enable_get_rum_data", + "experimental.exporter": "experimental_exporter", + "experimental.runtimeId": "experimental_runtime_id", + "experimental.sampler.rateLimit": "experimental_sampler_rate_limit", + "experimental.sampler.sampleRate": "experimental_sampler_sample_rate", + "experimental.traceparent": "experimental_traceparent", + "flakyTestRetriesCount": "ci_visibility_flaky_retry_count", + "flushInterval": "flush_interval", + "flushMinSpans": "flush_min_spans", + "gitMetadataEnabled": "git_metadata_enabled", + "git_commit_sha": "commit_sha", + "git_repository_url": "repository_url", + "global_tag_version": "version", + "grpc.client.error.statuses": "trace_grpc_client_error_statuses", + "grpc.server.error.statuses": "trace_grpc_server_error_statuses", + "headerTags": "trace_header_tags", + "hostname": "agent_hostname", + "http.client.tag.query-string": "trace_http_client_tag_query_string", + "http.server.route-based-naming": "trace_http_server_route_based_naming_enabled", + "http.server.tag.query-string": "trace_http_server_tag_query_string", + "http_server_route_based_naming": "http_server_route_based_naming", + "hystrix.measured.enabled": "hystrix_measured_enabled", + "hystrix.tags.enabled": "hystrix_tags_enabled", + "iast.cookieFilterPattern": "iast_cookie_filter_pattern", + "iast.dbRowsToTaint": "iast_db_rows_to_taint", + "iast.debug.enabled": "iast_debug_enabled", + "iast.deduplication.enabled": "iast_deduplication_enabled", + "iast.deduplicationEnabled": "iast_deduplication_enabled", + "iast.enabled": "iast_enabled", + "iast.experimental.propagation.enabled": "iast_experimental_propagation_enabled", + "iast.max-concurrent-requests": "iast_max_concurrent_requests", + "iast.maxConcurrentRequests": "iast_max_concurrent_requests", + "iast.maxContextOperations": "iast_max_context_operations", + "iast.redactionEnabled": "iast_redaction_enabled", + "iast.redactionNamePattern": "iast_redaction_name_pattern", + "iast.redactionValuePattern": "iast_redaction_value_pattern", + "iast.request-sampling": "iast_request_sampling", + "iast.requestSampling": "iast_request_sampling", + "iast.telemetryVerbosity": "iast_telemetry_verbosity", + "iast.vulnerabilities-per-request": "iast_vulnerability_per_request", + "ignite.cache.include_keys": "ignite_cache_include_keys_enabled", + "inferredProxyServicesEnabled": "inferred_proxy_services_enabled", + "inject_force": "ssi_forced_injection_enabled", + "injectionEnabled": "ssi_injection_enabled", + "instrumentation.telemetry.enabled": "instrumentation_telemetry_enabled", + "instrumentation_config_id": "instrumentation_config_id", + "integration_metrics_enabled": "integration_metrics_enabled", + "integrations.enabled": "trace_integrations_enabled", + "integrations_disabled": "trace_disabled_integrations", + "isAzureFunction": "azure_function", + "isCiVisibility": "ci_visibility_enabled", + "isEarlyFlakeDetectionEnabled": "ci_visibility_early_flake_detection_enabled", + "isFlakyTestRetriesEnabled": "ci_visibility_flaky_retry_enabled", + "isGCPFunction": "is_gcp_function", + "isGitUploadEnabled": "git_upload_enabled", + "isIntelligentTestRunnerEnabled": "intelligent_test_runner_enabled", + "isManualApiEnabled": "ci_visibility_manual_api_enabled", + "isTestDynamicInstrumentationEnabled": "ci_visibility_test_dynamic_instrumentation_enabled", + "jmxfetch.check-period": "jmxfetch_check_period", + "jmxfetch.enabled": "jmxfetch_enabled", + "jmxfetch.initial-refresh-beans-period": "jmxfetch_initial_refresh_beans_period", + "jmxfetch.multiple-runtime-services.enabled": "jmxfetch_multiple_runtime_services_enabled", + "jmxfetch.refresh-beans-period": "jmxfetch_initial_refresh_beans_period", + "jmxfetch.statsd.port": "jmxfetch_statsd_port", + "kafka.client.base64.decoding.enabled": "trace_kafka_client_base64_decoding_enabled", + "lambda_mode": "lambda_mode", + "langchain.spanCharLimit": "open_ai_span_char_limit", + "langchain.spanPromptCompletionSampleRate": "open_ai_span_prompt_completion_sample_rate", + "legacy.installer.enabled": "legacy_installer_enabled", + "legacyBaggageEnabled": "trace_legacy_baggage_enabled", + "llmobs.agentlessEnabled": "open_ai_agentless_enabled", + "llmobs.enabled": "open_ai_enabled", + "llmobs.mlApp": "open_ai_ml_app", + "logInjection": "logs_injection_enabled", + "logInjection_enabled": "logs_injection_enabled", + "logLevel": "trace_log_level", + "log_backtrace": "trace_log_backtrace_enabled", + "logger": "logger", + "logs.injection": "logs_injection_enabled", + "logs.mdc.tags.injection": "logs_mdc_tags_injection_enabled", + "lookup": "lookup", + "managed_tracer_framework": "managed_tracer_framework", + "memcachedCommandEnabled": "memchached_command_enabled", + "message.broker.split-by-destination": "message_broker_split_by_destination", + "native_tracer_version": "native_tracer_version", + "openAiLogsEnabled": "open_ai_logs_enabled", + "openaiSpanCharLimit": "open_ai_span_char_limit", + "openai_log_prompt_completion_sample_rate": "open_ai_log_prompt_completion_sample_rate", + "openai_logs_enabled": "open_ai_logs_enabled", + "openai_metrics_enabled": "open_ai_metrics_enabled", + "openai_service": "open_ai_service", + "openai_span_char_limit": "open_ai_span_char_limit", + "openai_span_prompt_completion_sample_rate": "open_ai_span_prompt_completion_sample_rate", + "orchestrion_enabled": "orchestrion_enabled", + "orchestrion_version": "orchestrion_version", + "os.name": "os_name", + "otel_enabled": "trace_otel_enabled", + "partialflush_enabled": "trace_partial_flush_enabled", + "partialflush_minspans": "trace_partial_flush_min_spans", + "peerServiceMapping": "trace_peer_service_mapping", + "platform": "platform", + "plugins": "plugins", + "port": "trace_agent_port", + "priority.sampling": "trace_priority_sample_enabled", + "priority_sampling": "trace_priority_sampling_enabled", + "profiler_loaded": "profiler_loaded", + "profiling.advanced.code_provenance_enabled": "profiling_enable_code_provenance", + "profiling.advanced.endpoint.collection.enabled": "profiling_endpoint_collection_enabled", + "profiling.allocation.enabled": "profiling_allocation_enabled", + "profiling.async.alloc.enabled": "profiling_async_alloc_enabled", + "profiling.async.cpu.enabled": "profiling_async_cpu_enabled", + "profiling.async.enabled": "profiling_async_enabled", + "profiling.async.memleak.enabled": "profiling_async_memleak_enabled", + "profiling.async.wall.enabled": "profiling_async_wall_enabled", + "profiling.ddprof.alloc.enabled": "profiling_ddprof_alloc_enabled", + "profiling.ddprof.cpu.enabled": "profiling_ddprof_cpu_enabled", + "profiling.ddprof.enabled": "profiling_ddprof_enabled", + "profiling.ddprof.memleak.enabled": "profiling_ddprof_memleak_enabled", + "profiling.ddprof.wall.enabled": "profiling_ddprof_wall_enabled", + "profiling.directallocation.enabled": "profiling_direct_allocation_enabled", + "profiling.enabled": "profiling_enabled", + "profiling.exporters": "profiling_exporters", + "profiling.heap.enabled": "profiling_heap_enabled", + "profiling.hotspots.enabled": "profiling_hotspots_enabled", + "profiling.legacy.tracing.integration": "profiling_legacy_tracing_integration_enabled", + "profiling.longLivedThreshold": "profiling_long_lived_threshold", + "profiling.sourceMap": "profiling_source_map_enabled", + "profiling.start-delay": "profiling_start_delay", + "profiling.start-force-first": "profiling_start_force_first", + "profiling.upload.period": "profiling_upload_period", + "profiling_endpoints_enabled": "profiling_endpoints_enabled", + "protocolVersion": "trace_agent_protocol_version", + "queryStringObfuscation": "trace_obfuscation_query_string_regexp", + "rcPollingInterval": "rc_polling_interval", + "remoteConfig.enabled": "remote_config_enabled", + "remoteConfig.pollInterval": "remote_config_poll_interval", + "remote_config.enabled": "remote_config_enabled", "remote_config_poll_interval_seconds": "remote_config_poll_interval", - "DD_INTERNAL_RCM_POLL_INTERVAL": "remote_config_poll_interval", + "reportHostname": "trace_report_hostname", + "repositoryUrl": "repository_url", + "resolver.outline.pool.enabled": "resolver_outline_pool_enabled", + "resolver.use.loadclass": "resolver_use_loadclass", + "retry_interval": "retry_interval", + "routetemplate_expansion_enabled": "trace_route_template_expansion_enabled", + "routetemplate_resourcenames_enabled": "trace_route_template_resource_names_enabled", + "runtime.metrics.enabled": "runtime_metrics_enabled", + "runtimeMetrics": "runtime_metrics_enabled", + "runtime_metrics.enabled": "runtime_metrics_enabled", + "runtime_metrics_v2_enabled": "runtime_metrics_v2_enabled", + "runtimemetrics_enabled": "runtime_metrics_enabled", + "sampleRate": "trace_sample_rate", + "sample_rate": "trace_sample_rate", + "sampler.rateLimit": "trace_rate_limit", + "sampler.rules": "trace_sample_rules", + "sampler.sampleRate": "trace_sample_rate", + "sampler.spanSamplingRules": "span_sample_rules", + "sampling_rules": "trace_sample_rules", + "scope": "scope", + "security_enabled": "appsec_enabled", + "send_retries": "trace_send_retries", + "service": "service", + "serviceMapping": "dd_service_mapping", + "site": "site", + "spanAttributeSchema": "trace_span_attribute_schema", + "spanComputePeerService": "trace_peer_service_defaults_enabled", + "spanLeakDebug": "span_leak_debug", + "spanRemoveIntegrationFromService": "trace_remove_integration_service_names_enabled", + "span_sampling_rules": "span_sample_rules", + "span_sampling_rules_file": "span_sample_rules_file", + "ssi_forced_injection_enabled": "ssi_forced_injection_enabled", + "ssi_injection_enabled": "ssi_injection_enabled", + "startupLogs": "trace_startup_logs_enabled", + "stats.enabled": "stats_enabled", + "stats_computation_enabled": "trace_stats_computation_enabled", + "tagsHeaderMaxLength": "trace_header_tags_max_length", + "telemetry.debug": "instrumentation_telemetry_debug_enabled", + "telemetry.dependencyCollection": "instrumentation_telemetry_dependency_collection_enabled", + "telemetry.enabled": "instrumentation_telemetry_enabled", + "telemetry.heartbeat.interval": "instrumentation_telemetry_heartbeat_interval", + "telemetry.heartbeatInterval": "instrumentation_telemetry_heartbeat_interval", + "telemetry.logCollection": "instrumentation_telemetry_log_collection_enabled", + "telemetry.metrics": "instrumentation_telemetry_metrics_enabled", + "telemetry.metricsInterval": "instrumentation_telemetry_metrics_interval", + "telemetryEnabled": "instrumentation_telemetry_enabled", + "telemetry_heartbeat_interval": "instrumentation_telemetry_heartbeat_interval", + "trace.128_bit_traceid_generation_enabled": "trace_128_bits_id_enabled", "trace.128_bit_traceid_logging_enabled": "trace_128_bits_id_logging_enabled", - "DD_PROFILING_ENABLED": "profiling_enabled", - "DD_PROFILING_CODEHOTSPOTS_ENABLED": "profiling_codehotspots_enabled", - "DD_PROFILING_ENDPOINT_COLLECTION_ENABLED": "profiling_endpoint_collection_enabled", - "DD_LOG_LEVEL": "agent_log_level", - "DD_TAGS": "agent_tags", - "DD_TRACE_GLOBAL_TAGS": "trace_tags", + "trace.agent.port": "trace_agent_port", + "trace.agent.timeout": "trace_agent_timeout", + "trace.agent.v0.5.enabled": "trace_agent_v0.5_enabled", + "trace.agent_attempt_retry_time_msec": "trace_agent_attempt_retry_time_msec", + "trace.agent_connect_timeout": "trace_agent_connect_timeout", + "trace.agent_debug_verbose_curl": "trace_agent_debug_verbose_curl_enabled", + "trace.agent_flush_after_n_requests": "trace_agent_flush_after_n_requests", + "trace.agent_flush_interval": "trace_agent_flush_interval", + "trace.agent_max_consecutive_failures": "trace_send_retries", + "trace.agent_max_payload_size": "trace_agent_max_payload_size", + "trace.agent_port": "trace_agent_port", + "trace.agent_retries": "trace_send_retries", + "trace.agent_stack_backlog": "trace_agent_stack_backlog", + "trace.agent_stack_initial_size": "trace_agent_stack_initial_size", + "trace.agent_test_session_token": "trace_agent_test_session_token", + "trace.agent_timeout": "trace_agent_timeout", "trace.agent_url": "trace_agent_url", + "trace.agentless": "trace_agentless", + "trace.analytics.enabled": "trace_analytics_enabled", + "trace.analytics_enabled": "trace_analytics_enabled", "trace.append_trace_ids_to_logs": "trace_append_trace_ids_to_logs", + "trace.auto_flush_enabled": "trace_auto_flush_enabled", + "trace.aws-sdk.legacy.tracing.enabled": "trace_aws_sdk_legacy_tracing_enabled", + "trace.aws-sdk.propagation.enabled": "trace_aws_sdk_propagation_enabled", + "trace.beta_high_memory_pressure_percent": "trace_beta_high_memory_pressure_percent", + "trace.bgs_connect_timeout": "trace_bgs_connect_timeout", + "trace.bgs_timeout": "trace_bgs_timeout", + "trace.buffer_size": "trace_serialization_buffer_size", + "trace.cli_enabled": "trace_cli_enabled", + "trace.client-ip.enabled": "trace_client_ip_enabled", + "trace.client-ip.resolver.enabled": "trace_client_ip_resolver_enabled", "trace.client_ip_enabled": "trace_client_ip_enabled", - "trace.analytics_enabled": "trace_analytics_enabled", - "trace.rate_limit": "trace_rate_limit", - "trace.report_hostname": "trace_report_hostname", - "trace.http_client_split_by_domain": "trace_http_client_split_by_domain", + "trace.client_ip_header": "client_ip_header", + "trace.db.client.split-by-instance": "trace_db_client_split_by_instance", + "trace.db.client.split-by-instance.type.suffix": "trace_db_client_split_by_instance_type_suffix", + "trace.db_client_split_by_instance": "trace_db_client_split_by_instance", "trace.debug": "trace_debug_enabled", - "trace.agent_timeout": "trace_agent_timeout", - "trace.agent_port": "trace_agent_port", - "trace.x_datadog_tags_max_length": "trace_x_datadog_tags_max_length", + "trace.debug_curl_output": "trace_debug_curl_output_enabled", + "trace.debug_prng_seed": "trace_debug_prng_seed", + "trace.enabled": "trace_enabled", + "trace.flush_collect_cycles": "trace_flush_collect_cycles_enabled", + "trace.forked_process": "trace_forked_process_enabled", + "trace.generate_root_span": "trace_generate_root_span_enabled", + "trace.git_metadata_enabled": "git_metadata_enabled", + "trace.grpc.server.trim-package-resource": "trace_grpc_server_trim_package_resource_enabled", + "trace.header.tags.legacy.parsing.enabled": "trace_header_tags_legacy_parsing_enabled", + "trace.health.metrics.enabled": "trace_health_metrics_enabled", + "trace.health.metrics.statsd.port": "trace_health_metrics_statsd_port", + "trace.health_metrics_enabled": "trace_health_metrics_enabled", + "trace.health_metrics_heartbeat_sample_rate": "trace_health_metrics_heartbeat_sample_rate", + "trace.hook_limit": "trace_hook_limit", + "trace.http.client.split-by-domain": "trace_http_client_split_by_domain", + "trace.http_client_split_by_domain": "trace_http_client_split_by_domain", + "trace.http_post_data_param_allowed": "trace_http_post_data_param_allowed", + "trace.http_url_query_param_allowed": "trace_http_url_query_param_allowed", + "trace.jms.propagation.enabled": "trace_jms_propagation_enabled", + "trace.jmxfetch.kafka.enabled": "trace_jmxfetch_kafka_enabled", + "trace.jmxfetch.tomcat.enabled": "trace_jmxfetch_tomcat_enabled", + "trace.kafka.client.propagation.enabled": "trace_kafka_client_propagation_enabled", + "trace.kafka_distributed_tracing": "trace_kafka_distributed_tracing", + "trace.laravel_queue_distributed_tracing": "trace_laravel_queue_distributed_tracing", + "trace.log_file": "trace_log_file", + "trace.log_level": "trace_log_level", + "trace.measure_compile_time": "trace_measure_compile_time_enabled", + "trace.measure_peak_memory_usage": "trace_measure_peak_memory_usage_enabled", + "trace.memcached_obfuscation": "trace_memcached_obfuscation_enabled", + "trace.memory_limit": "trace_memory_limit", "trace.obfuscation_query_string_regexp": "trace_obfuscation_query_string_regexp", + "trace.once_logs": "trace_once_logs", + "trace.otel.enabled": "trace_otel_enabled", + "trace.otel_enabled": "trace_otel_enabled", + "trace.partial.flush.min.spans": "trace_partial_flush_min_spans", + "trace.peer.service.defaults.enabled": "trace_peer_service_defaults_enabled", + "trace.peer.service.mapping": "trace_peer_service_mapping", "trace.peer_service_defaults_enabled": "trace_peer_service_defaults_enabled", + "trace.peer_service_mapping": "trace_peer_service_mapping", + "trace.peerservicetaginterceptor.enabled": "trace_peer_service_tag_interceptor_enabled", + "trace.perf.metrics.enabled": "trace_perf_metrics_enabled", + "trace.play.report-http-status": "trace_play_report_http_status", "trace.propagate_service": "trace_propagate_service", + "trace.propagate_user_id_default": "trace_propagate_user_id_default_enabled", + "trace.propagation_extract_first": "trace_propagation_extract_first", + "trace.propagation_style": "trace_propagation_style", + "trace.propagation_style_extract": "trace_propagation_style_extract", + "trace.propagation_style_inject": "trace_propagation_style_inject", + "trace.rabbit.propagation.enabled": "trace_rabbit_propagation_enabled", + "trace.rate.limit": "trace_rate_limit", + "trace.rate_limit": "trace_rate_limit", + "trace.redis_client_split_by_host": "trace_redis_client_split_by_host_enabled", + "trace.remove.integration-service-names.enabled": "trace_remove_integration_service_names_enabled", + "trace.remove_autoinstrumentation_orphans": "trace_remove_auto_instrumentation_orphans_enabled", "trace.remove_integration_service_names_enabled": "trace_remove_integration_service_names_enabled", + "trace.remove_root_span_laravel_queue": "trace_remove_root_span_laravel_queue_enabled", + "trace.remove_root_span_symfony_messenger": "trace_remove_root_span_symfony_messenger_enabled", + "trace.report-hostname": "trace_report_hostname", + "trace.report_hostname": "trace_report_hostname", + "trace.request_init_hook": "trace_request_init_hook", + "trace.resource_uri_fragment_regex": "trace_resource_uri_fragment_regex", + "trace.resource_uri_mapping_incoming": "trace_resource_uri_mapping_incoming", + "trace.resource_uri_mapping_outgoing": "trace_resource_uri_mapping_outgoing", + "trace.resource_uri_query_param_allowed": "trace_resource_uri_query_param_allowed", + "trace.retain_thread_capabilities": "trace_retain_thread_capabilities_enabled", + "trace.sample.rate": "trace_sample_rate", "trace.sample_rate": "trace_sample_rate", - "trace.health_metrics_enabled": "trace_health_metrics_enabled", - "trace.telemetry_enabled": "instrumentation_telemetry_enabled", - "trace.cli_enabled": "trace_cli_enabled", - "trace.db_client_split_by_instance": "trace_db_client_split_by_instance", - "trace.startup_logs": "trace_startup_logs", - "http_server_route_based_naming": "http_server_route_based_naming", - "DD_TRACE_PEER_SERVICE_MAPPING": "trace_peer_service_mapping", - "peerServiceMapping": "trace_peer_service_mapping", - "trace.peer.service.mapping": "trace_peer_service_mapping", - "trace.peer_service_mapping": "trace_peer_service_mapping", - "spanComputePeerService": "trace_peer_service_defaults_enabled", - "spanLeakDebug": "span_leak_debug", - "trace.peer.service.defaults.enabled": "trace_peer_service_defaults_enabled", - "spanAttributeSchema": "trace_span_attribute_schema", + "trace.sampling_rules": "trace_sample_rules", + "trace.sampling_rules_format": "trace_sampling_rules_format", + "trace.scope.depth.limit": "trace_scope_depth_limit", + "trace.servlet.async-timeout.error": "trace_servlet_async_timeout_error_enabled", + "trace.servlet.principal.enabled": "trace_servlet_principal_enabled", + "trace.shutdown_timeout": "trace_shutdown_timeout", + "trace.sidecar_trace_sender": "trace_sidecar_trace_sender", + "trace.sources_path": "trace_sources_path", "trace.span.attribute.schema": "trace_span_attribute_schema", - "spanRemoveIntegrationFromService": "trace_remove_integration_service_names_enabled", - "trace.remove.integration-service-names.enabled": "trace_remove_integration_service_names_enabled", - "ddtrace_auto_used": "ddtrace_auto_used", - "ddtrace_bootstrapped": "ddtrace_bootstrapped", - "orchestrion_enabled": "orchestrion_enabled", - "orchestrion_version": "orchestrion_version", - "trace.once_logs": "trace_once_logs", + "trace.spans_limit": "trace_spans_limit", + "trace.sqs.propagation.enabled": "trace_sqs_propagation_enabled", + "trace.startup_logs": "trace_startup_logs", + "trace.status404decorator.enabled": "trace_status_404_decorator_enabled", + "trace.status404rule.enabled": "trace_status_404_rule_enabled", + "trace.symfony_messenger_distributed_tracing": "trace_symfony_messenger_distributed_tracing", + "trace.symfony_messenger_middlewares": "trace_symfony_messenger_middlewares", + "trace.telemetry_enabled": "instrumentation_telemetry_enabled", + "trace.traced_internal_functions": "trace_traced_internal_functions", + "trace.tracer.metrics.enabled": "trace_metrics_enabled", + "trace.url_as_resource_names_enabled": "trace_url_as_resource_names_enabled", + "trace.warn_legacy_dd_trace": "trace_warn_legacy_dd_trace_enabled", + "trace.wordpress_additional_actions": "trace_wordpress_additional_actions", "trace.wordpress_callbacks": "trace_wordpress_callbacks", "trace.wordpress_enhanced_integration": "trace_wordpress_enhanced_integration", - "trace.wordpress_additional_actions": "trace_wordpress_additional_actions", - "trace.sidecar_trace_sender": "trace_sidecar_trace_sender", - "trace.sampling_rules_format": "trace_sampling_rules_format", - "DD_TRACE_SAMPLING_RULES_FORMAT": "trace_sampling_rules_format", - "trace.agentless": "trace_agentless", - "dd_agent_port": "trace_agent_port", - "dd_priority_sampling": "trace_priority_sampling_enabled", - "dd_profiling_capture_pct": "profiling_capture_pct", - "dd_profiling_export_libdd_enabled": "profiling_export_libdd_enabled", - "dd_profiling_heap_enabled": "profiling_heap_enabled", - "dd_profiling_lock_enabled": "profiling_lock_enabled", - "dd_profiling_max_frames": "profiling_max_frames", - "dd_profiling_memory_enabled": "profiling_memory_enabled", - "dd_profiling_stack_enabled": "profiling_stack_enabled", - "dd_profiling_upload_interval": "profiling_upload_interval", - "dd_remote_configuration_enabled": "remote_config_enabled", - "dd_trace_agent_timeout_seconds": "trace_agent_timeout", - "dd_trace_api_version": "trace_api_version", - "dd_trace_writer_buffer_size_bytes": "trace_serialization_buffer_size", - "dd_trace_writer_interval_seconds": "trace_agent_flush_interval", - "dd_trace_writer_max_payload_size_bytes": "trace_agent_max_payload_size", - "dd_trace_writer_reuse_connections": "trace_agent_reuse_connections", - "tracing_enabled": "trace_enabled", - "ssi_injection_enabled": "ssi_injection_enabled", - "DD_INJECTION_ENABLED": "ssi_injection_enabled", - "ssi_forced_injection_enabled": "ssi_forced_injection_enabled", - "DD_INJECT_FORCE": "ssi_forced_injection_enabled", - "inject_force": "ssi_forced_injection_enabled", - "OTEL_LOGS_EXPORTER": "otel_logs_exporter", - "OTEL_LOG_LEVEL": "otel_log_level", - "OTEL_METRICS_EXPORTER": "otel_metrics_exporter", - "integration_metrics_enabled": "integration_metrics_enabled", - "OTEL_SDK_DISABLED": "otel_sdk_disabled", - "OTEL_SERVICE_NAME": "otel_service_name", - "OTEL_PROPAGATORS": "otel_propagators", - "OTEL_RESOURCE_ATTRIBUTES": "otel_resource_attributes", - "OTEL_TRACES_EXPORTER": "otel_traces_exporter", - "OTEL_TRACES_SAMPLER": "otel_traces_sampler", - "OTEL_TRACES_SAMPLER_ARG": "otel_traces_sampler_arg", - "crashtracking_enabled": "crashtracking_enabled", - "crashtracking_available": "crashtracking_available", - "crashtracking_started": "crashtracking_started", - "crashtracking_stdout_filename": "crashtracking_stdout_filename", - "crashtracking_stderr_filename": "crashtracking_stderr_filename", - "crashtracking_alt_stack": "crashtracking_alt_stack", - "crashtracking_stacktrace_resolver": "crashtracking_stacktrace_resolver", - "crashtracking_debug_url": "crashtracking_debug_url", - "debug_stack_enabled": "debug_stack_enabled", - "DD_TRACE_BAGGAGE_MAX_ITEMS": "trace_baggage_max_items", - "DD_TRACE_BAGGAGE_MAX_BYTES": "trace_baggage_max_bytes", - "appsec.apiSecurity.sampleDelay": "api_security_sample_delay", - "appsec.stackTrace.enabled": "appsec_stack_trace_enabled", - "appsec.stackTrace.maxDepth": "appsec_max_stack_trace_depth", - "appsec.stackTrace.maxStackTraces": "appsec_max_stack_traces", - "appsec.standalone.enabled": "experimental_appsec_standalone_enabled", - "baggageMaxBytes": "trace_baggage_max_bytes", - "baggageMaxItems": "trace_baggage_max_items", - "ciVisAgentlessLogSubmissionEnabled": "ci_visibility_agentless_enabled", - "ciVisibilityTestSessionName": "test_session_name", - "cloudPayloadTagging.maxDepth": "cloud_payload_tagging_max_depth", - "cloudPayloadTagging.requestsEnabled": "cloud_payload_tagging_requests_enabled", - "cloudPayloadTagging.responsesEnabled": "cloud_payload_tagging_responses_enabled", - "cloudPayloadTagging.rules.aws.eventbridge.expand": "cloud_payload_tagging_rules_aws_eventbridge_expand", - "cloudPayloadTagging.rules.aws.eventbridge.request": "cloud_payload_tagging_rules_aws_eventbridge_request", - "cloudPayloadTagging.rules.aws.eventbridge.response": "cloud_payload_tagging_rules_aws_eventbridge_response", - "cloudPayloadTagging.rules.aws.kinesis.expand": "cloud_payload_tagging_rules_aws_kinesis_expand", - "cloudPayloadTagging.rules.aws.kinesis.request": "cloud_payload_tagging_rules_aws_kinesis_request", - "cloudPayloadTagging.rules.aws.kinesis.response": "cloud_payload_tagging_rules_aws_kinesis_response", - "cloudPayloadTagging.rules.aws.s3.expand": "cloud_payload_tagging_rules_aws_s3_expand", - "cloudPayloadTagging.rules.aws.s3.request": "cloud_payload_tagging_rules_aws_s3_request", - "cloudPayloadTagging.rules.aws.s3.response": "cloud_payload_tagging_rules_aws_s3_response", - "cloudPayloadTagging.rules.aws.sns.expand": "cloud_payload_tagging_rules_aws_sns_expand", - "cloudPayloadTagging.rules.aws.sns.request": "cloud_payload_tagging_rules_aws_sns_request", - "cloudPayloadTagging.rules.aws.sns.response": "cloud_payload_tagging_rules_aws_sns_response", - "cloudPayloadTagging.rules.aws.sqs.expand": "cloud_payload_tagging_rules_aws_sqs_expand", - "cloudPayloadTagging.rules.aws.sqs.request": "cloud_payload_tagging_rules_aws_sqs_request", - "cloudPayloadTagging.rules.aws.sqs.response": "cloud_payload_tagging_rules_aws_sqs_response", - "codeOriginForSpans.enabled": "code_origin_for_spans_enabled", - "commitSHA": "commit_sha", - "crashtracking.enabled": "crashtracking_enabled", - "dynamicInstrumentationEnabled": "dynamic_instrumentation_enabled", - "flakyTestRetriesCount": "ci_visibility_flaky_retry_count", - "gitMetadataEnabled": "git_metadata_enabled", - "grpc.client.error.statuses": "trace_grpc_client_error_statuses", - "grpc.server.error.statuses": "trace_grpc_server_error_statuses", - "headerTags": "trace_header_tags", - "injectionEnabled": "ssi_injection_enabled", - "instrumentation_config_id": "instrumentation_config_id", - "isEarlyFlakeDetectionEnabled": "ci_visibility_early_flake_detection_enabled", - "isFlakyTestRetriesEnabled": "ci_visibility_flaky_retry_enabled", - "isManualApiEnabled": "ci_visibility_manual_api_enabled", - "isTestDynamicInstrumentationEnabled": "ci_visibility_test_dynamic_instrumentation_enabled", - "langchain.spanCharLimit": "langchain_span_char_limit", - "langchain.spanPromptCompletionSampleRate": "langchain_span_prompt_completion_sample_rate", - "legacyBaggageEnabled": "trace_legacy_baggage_enabled", - "llmobs.agentlessEnabled": "llmobs_agentless_enabled", - "llmobs.enabled": "llmobs_enabled", - "llmobs.mlApp": "llmobs_ml_app", - "profiling.longLivedThreshold": "profiling_long_lived_threshold", - "repositoryUrl": "repository_url", - "sampler.rules": "trace_sample_rules", - "sampler.spanSamplingRules": "span_sample_rules", - "telemetry.dependencyCollection": "instrumentation_telemetry_dependency_collection_enabled", - "telemetry.heartbeatInterval": "instrumentation_telemetry_heartbeat_interval", + "trace.x-datadog-tags.max.length": "trace_x_datadog_tags_max_length", + "trace.x_datadog_tags_max_length": "trace_x_datadog_tags_max_length", "traceEnabled": "trace_enabled", - "tracePropagationStyle.otelPropagators": "trace_propagation_style_otel_propagators" + "traceId128BitGenerationEnabled": "trace_128_bits_id_enabled", + "traceId128BitLoggingEnabled": "trace_128_bits_id_logging_enabled", + "tracePropagationExtractFirst": "trace_propagation_extract_first", + "tracePropagationStyle,otelPropagators": "trace_propagation_style_otel_propagators", + "tracePropagationStyle.extract": "trace_propagation_style_extract", + "tracePropagationStyle.inject": "trace_propagation_style_inject", + "tracePropagationStyle.otelPropagators": "trace_propagation_style_otel_propagators", + "trace_methods": "trace_methods", + "tracer_instance_count": "trace_instance_count", + "tracing": "trace_enabled", + "tracing.auto_instrument.enabled": "trace_auto_instrument_enabled", + "tracing.distributed_tracing.propagation_extract_style": "trace_propagation_style_extract", + "tracing.distributed_tracing.propagation_inject_style": "trace_propagation_style_inject", + "tracing.enabled": "trace_enabled", + "tracing.log_injection": "logs_injection_enabled", + "tracing.opentelemetry.enabled": "trace_otel_enabled", + "tracing.partial_flush.enabled": "trace_partial_flush_enabled", + "tracing.partial_flush.min_spans_threshold": "trace_partial_flush_min_spans", + "tracing.propagation_style_extract": "trace_propagation_style_extract", + "tracing.propagation_style_inject": "trace_propagation_style_inject", + "tracing.report_hostname": "trace_report_hostname", + "tracing.sampling.rate_limit": "trace_sample_rate", + "tracing_enabled": "trace_enabled", + "universal_version": "universal_version_enabled", + "url": "trace_agent_url", + "version": "application_version", + "wcf_obfuscation_enabled": "trace_wcf_obfuscation_enabled" } diff --git a/packages/dd-trace/test/llmobs/sdk/index.spec.js b/packages/dd-trace/test/llmobs/sdk/index.spec.js index 69dad1d60c4..e7cfb81a47d 100644 --- a/packages/dd-trace/test/llmobs/sdk/index.spec.js +++ b/packages/dd-trace/test/llmobs/sdk/index.spec.js @@ -17,6 +17,7 @@ describe('sdk', () => { let LLMObsSDK let llmobs let tracer + let clock before(() => { tracer = require('../../../../dd-trace') @@ -43,6 +44,8 @@ describe('sdk', () => { // remove max listener warnings, we don't care about the writer anyways process.removeAllListeners('beforeExit') + + clock = sinon.useFakeTimers() }) afterEach(() => { @@ -435,6 +438,180 @@ describe('sdk', () => { }) }) + it('does not crash for auto-annotation values that are overriden', () => { + const circular = {} + circular.circular = circular + + let span + function myWorkflow (input) { + span = llmobs._active() + llmobs.annotate({ + inputData: 'circular', + outputData: 'foo' + }) + return '' + } + + const wrappedMyWorkflow = llmobs.wrap({ kind: 'workflow' }, myWorkflow) + wrappedMyWorkflow(circular) + + expect(LLMObsTagger.tagMap.get(span)).to.deep.equal({ + '_ml_obs.meta.span.kind': 'workflow', + '_ml_obs.meta.ml_app': 'mlApp', + '_ml_obs.llmobs_parent_id': 'undefined', + '_ml_obs.meta.input.value': 'circular', + '_ml_obs.meta.output.value': 'foo' + }) + }) + + it('only auto-annotates input on error', () => { + let span + function myTask (foo, bar) { + span = llmobs._active() + throw new Error('error') + } + + const wrappedMyTask = llmobs.wrap({ kind: 'task' }, myTask) + + expect(() => wrappedMyTask('foo', 'bar')).to.throw() + + expect(LLMObsTagger.tagMap.get(span)).to.deep.equal({ + '_ml_obs.meta.span.kind': 'task', + '_ml_obs.meta.ml_app': 'mlApp', + '_ml_obs.llmobs_parent_id': 'undefined', + '_ml_obs.meta.input.value': JSON.stringify({ foo: 'foo', bar: 'bar' }) + }) + }) + + it('only auto-annotates input on error for promises', () => { + let span + function myTask (foo, bar) { + span = llmobs._active() + return Promise.reject(new Error('error')) + } + + const wrappedMyTask = llmobs.wrap({ kind: 'task' }, myTask) + + return wrappedMyTask('foo', 'bar') + .catch(() => { + expect(LLMObsTagger.tagMap.get(span)).to.deep.equal({ + '_ml_obs.meta.span.kind': 'task', + '_ml_obs.meta.ml_app': 'mlApp', + '_ml_obs.llmobs_parent_id': 'undefined', + '_ml_obs.meta.input.value': JSON.stringify({ foo: 'foo', bar: 'bar' }) + }) + }) + }) + + it('auto-annotates the inputs of the callback function as the outputs for the span', () => { + let span + function myWorkflow (input, cb) { + span = llmobs._active() + setTimeout(() => { + cb(null, 'output') + }, 1000) + } + + const wrappedMyWorkflow = llmobs.wrap({ kind: 'workflow' }, myWorkflow) + wrappedMyWorkflow('input', (err, res) => { + expect(err).to.not.exist + expect(res).to.equal('output') + }) + + clock.tick(1000) + + expect(LLMObsTagger.tagMap.get(span)).to.deep.equal({ + '_ml_obs.meta.span.kind': 'workflow', + '_ml_obs.meta.ml_app': 'mlApp', + '_ml_obs.llmobs_parent_id': 'undefined', + '_ml_obs.meta.input.value': JSON.stringify({ input: 'input' }), + '_ml_obs.meta.output.value': 'output' + }) + }) + + it('ignores the error portion of the callback for auto-annotation', () => { + let span + function myWorkflow (input, cb) { + span = llmobs._active() + setTimeout(() => { + cb(new Error('error'), 'output') + }, 1000) + } + + const wrappedMyWorkflow = llmobs.wrap({ kind: 'workflow' }, myWorkflow) + wrappedMyWorkflow('input', (err, res) => { + expect(err).to.exist + expect(res).to.equal('output') + }) + + clock.tick(1000) + + expect(LLMObsTagger.tagMap.get(span)).to.deep.equal({ + '_ml_obs.meta.span.kind': 'workflow', + '_ml_obs.meta.ml_app': 'mlApp', + '_ml_obs.llmobs_parent_id': 'undefined', + '_ml_obs.meta.input.value': JSON.stringify({ input: 'input' }), + '_ml_obs.meta.output.value': 'output' + }) + }) + + it('auto-annotates the first argument of the callback as the output if it is not an error', () => { + let span + function myWorkflow (input, cb) { + span = llmobs._active() + setTimeout(() => { + cb('output', 'ignore') + }, 1000) + } + + const wrappedMyWorkflow = llmobs.wrap({ kind: 'workflow' }, myWorkflow) + wrappedMyWorkflow('input', (res, irrelevant) => { + expect(res).to.equal('output') + expect(irrelevant).to.equal('ignore') + }) + + clock.tick(1000) + + expect(LLMObsTagger.tagMap.get(span)).to.deep.equal({ + '_ml_obs.meta.span.kind': 'workflow', + '_ml_obs.meta.ml_app': 'mlApp', + '_ml_obs.llmobs_parent_id': 'undefined', + '_ml_obs.meta.input.value': JSON.stringify({ input: 'input' }), + '_ml_obs.meta.output.value': 'output' + }) + }) + + it('maintains context consistent with the tracer', () => { + let llmSpan, workflowSpan, taskSpan + + function myLlm (input, cb) { + llmSpan = llmobs._active() + setTimeout(() => { + cb(null, 'output') + }, 1000) + } + const myWrappedLlm = llmobs.wrap({ kind: 'llm' }, myLlm) + + llmobs.trace({ kind: 'workflow', name: 'myWorkflow' }, _workflow => { + workflowSpan = _workflow + tracer.trace('apmOperation', () => { + myWrappedLlm('input', (err, res) => { + expect(err).to.not.exist + expect(res).to.equal('output') + llmobs.trace({ kind: 'task', name: 'afterLlmTask' }, _task => { + taskSpan = _task + + const llmParentId = LLMObsTagger.tagMap.get(llmSpan)['_ml_obs.llmobs_parent_id'] + expect(llmParentId).to.equal(workflowSpan.context().toSpanId()) + + const taskParentId = LLMObsTagger.tagMap.get(taskSpan)['_ml_obs.llmobs_parent_id'] + expect(taskParentId).to.equal(workflowSpan.context().toSpanId()) + }) + }) + }) + }) + }) + // TODO: need span kind optional for this test it.skip('sets the span name to "unnamed-anonymous-function" if no name is provided', () => { let span diff --git a/packages/dd-trace/test/log.spec.js b/packages/dd-trace/test/log.spec.js index a035c864f71..cbe5679414b 100644 --- a/packages/dd-trace/test/log.spec.js +++ b/packages/dd-trace/test/log.spec.js @@ -139,6 +139,31 @@ describe('log', () => { }) }) + describe('trace', () => { + it('should not log to console by default', () => { + log.trace('trace') + + expect(console.debug).to.not.have.been.called + }) + + it('should log to console after setting log level to trace', function foo () { + class Foo { + constructor () { + this.bar = 'baz' + } + } + + log.toggle(true, 'trace') + log.trace('argument', { hello: 'world' }, new Foo()) + + expect(console.debug).to.have.been.calledOnce + expect(console.debug.firstCall.args[0]).to.match( + /^Trace: Test.foo\('argument', { hello: 'world' }, Foo { bar: 'baz' }\)/ + ) + expect(console.debug.firstCall.args[0].split('\n').length).to.be.gte(3) + }) + }) + describe('error', () => { it('should log to console by default', () => { log.error(error) diff --git a/packages/dd-trace/test/plugins/externals.json b/packages/dd-trace/test/plugins/externals.json index 288fb9350c6..73a61536476 100644 --- a/packages/dd-trace/test/plugins/externals.json +++ b/packages/dd-trace/test/plugins/externals.json @@ -30,6 +30,10 @@ "name": "@aws-sdk/client-s3", "versions": [">=3"] }, + { + "name": "@aws-sdk/client-dynamodb", + "versions": [">=3"] + }, { "name": "@aws-sdk/client-sfn", "versions": [">=3"] @@ -416,6 +420,10 @@ { "name": "express", "versions": [">=4"] + }, + { + "name": "sqlite3", + "versions": ["^5.0.8"] } ] } diff --git a/packages/dd-trace/test/priority_sampler.spec.js b/packages/dd-trace/test/priority_sampler.spec.js index 88c134a5758..2c1a2e273bd 100644 --- a/packages/dd-trace/test/priority_sampler.spec.js +++ b/packages/dd-trace/test/priority_sampler.spec.js @@ -490,6 +490,16 @@ describe('PrioritySampler', () => { expect(context._sampling.mechanism).to.equal(SAMPLING_MECHANISM_APPSEC) expect(context._trace.tags[DECISION_MAKER_KEY]).to.equal('-0') }) + + it('should ignore noop spans', () => { + context._trace.started[0] = undefined // noop + + prioritySampler.setPriority(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + + expect(context._sampling.priority).to.undefined + expect(context._sampling.mechanism).to.undefined + expect(context._trace.tags[DECISION_MAKER_KEY]).to.undefined + }) }) describe('keepTrace', () => { diff --git a/packages/dd-trace/test/runtime_metrics.spec.js b/packages/dd-trace/test/runtime_metrics.spec.js index f3f20464630..20ce93112ae 100644 --- a/packages/dd-trace/test/runtime_metrics.spec.js +++ b/packages/dd-trace/test/runtime_metrics.spec.js @@ -13,6 +13,7 @@ suiteDescribe('runtimeMetrics', () => { let runtimeMetrics let config let clock + let setImmediate let client let Client @@ -50,6 +51,7 @@ suiteDescribe('runtimeMetrics', () => { } } + setImmediate = globalThis.setImmediate clock = sinon.useFakeTimers() runtimeMetrics.start(config) @@ -91,71 +93,79 @@ suiteDescribe('runtimeMetrics', () => { }) }) - it('should start collecting runtimeMetrics every 10 seconds', () => { + it('should start collecting runtimeMetrics every 10 seconds', (done) => { runtimeMetrics.stop() runtimeMetrics.start(config) global.gc() - clock.tick(10000) - - expect(client.gauge).to.have.been.calledWith('runtime.node.cpu.user') - expect(client.gauge).to.have.been.calledWith('runtime.node.cpu.system') - expect(client.gauge).to.have.been.calledWith('runtime.node.cpu.total') - - expect(client.gauge).to.have.been.calledWith('runtime.node.mem.rss') - expect(client.gauge).to.have.been.calledWith('runtime.node.mem.heap_total') - expect(client.gauge).to.have.been.calledWith('runtime.node.mem.heap_used') - - expect(client.gauge).to.have.been.calledWith('runtime.node.process.uptime') - - expect(client.gauge).to.have.been.calledWith('runtime.node.heap.total_heap_size') - expect(client.gauge).to.have.been.calledWith('runtime.node.heap.total_heap_size_executable') - expect(client.gauge).to.have.been.calledWith('runtime.node.heap.total_physical_size') - expect(client.gauge).to.have.been.calledWith('runtime.node.heap.total_available_size') - expect(client.gauge).to.have.been.calledWith('runtime.node.heap.total_heap_size') - expect(client.gauge).to.have.been.calledWith('runtime.node.heap.heap_size_limit') - - expect(client.gauge).to.have.been.calledWith('runtime.node.heap.malloced_memory') - expect(client.gauge).to.have.been.calledWith('runtime.node.heap.peak_malloced_memory') - - expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.max') - expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.min') - expect(client.increment).to.have.been.calledWith('runtime.node.event_loop.delay.sum') - expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.avg') - expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.median') - expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.95percentile') - expect(client.increment).to.have.been.calledWith('runtime.node.event_loop.delay.count') - - expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.utilization') - - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.max') - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.min') - expect(client.increment).to.have.been.calledWith('runtime.node.gc.pause.sum') - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.avg') - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.median') - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.95percentile') - expect(client.increment).to.have.been.calledWith('runtime.node.gc.pause.count') - - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.max') - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.min') - expect(client.increment).to.have.been.calledWith('runtime.node.gc.pause.by.type.sum') - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.avg') - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.median') - expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.95percentile') - expect(client.increment).to.have.been.calledWith('runtime.node.gc.pause.by.type.count') - expect(client.increment).to.have.been.calledWith( - 'runtime.node.gc.pause.by.type.count', sinon.match.any, sinon.match(val => { - return val && /^gc_type:[a-z_]+$/.test(val[0]) - }) - ) - - expect(client.gauge).to.have.been.calledWith('runtime.node.heap.size.by.space') - expect(client.gauge).to.have.been.calledWith('runtime.node.heap.used_size.by.space') - expect(client.gauge).to.have.been.calledWith('runtime.node.heap.available_size.by.space') - expect(client.gauge).to.have.been.calledWith('runtime.node.heap.physical_size.by.space') + setImmediate(() => setImmediate(() => { // Wait for GC observer to trigger. + clock.tick(10000) - expect(client.flush).to.have.been.called + try { + expect(client.gauge).to.have.been.calledWith('runtime.node.cpu.user') + expect(client.gauge).to.have.been.calledWith('runtime.node.cpu.system') + expect(client.gauge).to.have.been.calledWith('runtime.node.cpu.total') + + expect(client.gauge).to.have.been.calledWith('runtime.node.mem.rss') + expect(client.gauge).to.have.been.calledWith('runtime.node.mem.heap_total') + expect(client.gauge).to.have.been.calledWith('runtime.node.mem.heap_used') + + expect(client.gauge).to.have.been.calledWith('runtime.node.process.uptime') + + expect(client.gauge).to.have.been.calledWith('runtime.node.heap.total_heap_size') + expect(client.gauge).to.have.been.calledWith('runtime.node.heap.total_heap_size_executable') + expect(client.gauge).to.have.been.calledWith('runtime.node.heap.total_physical_size') + expect(client.gauge).to.have.been.calledWith('runtime.node.heap.total_available_size') + expect(client.gauge).to.have.been.calledWith('runtime.node.heap.total_heap_size') + expect(client.gauge).to.have.been.calledWith('runtime.node.heap.heap_size_limit') + + expect(client.gauge).to.have.been.calledWith('runtime.node.heap.malloced_memory') + expect(client.gauge).to.have.been.calledWith('runtime.node.heap.peak_malloced_memory') + + expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.max') + expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.min') + expect(client.increment).to.have.been.calledWith('runtime.node.event_loop.delay.sum') + expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.avg') + expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.median') + expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.delay.95percentile') + expect(client.increment).to.have.been.calledWith('runtime.node.event_loop.delay.count') + + expect(client.gauge).to.have.been.calledWith('runtime.node.event_loop.utilization') + + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.max') + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.min') + expect(client.increment).to.have.been.calledWith('runtime.node.gc.pause.sum') + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.avg') + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.median') + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.95percentile') + expect(client.increment).to.have.been.calledWith('runtime.node.gc.pause.count') + + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.max') + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.min') + expect(client.increment).to.have.been.calledWith('runtime.node.gc.pause.by.type.sum') + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.avg') + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.median') + expect(client.gauge).to.have.been.calledWith('runtime.node.gc.pause.by.type.95percentile') + expect(client.increment).to.have.been.calledWith('runtime.node.gc.pause.by.type.count') + expect(client.increment).to.have.been.calledWith( + 'runtime.node.gc.pause.by.type.count', sinon.match.any, sinon.match(val => { + return val && /^gc_type:[a-z_]+$/.test(val[0]) + }) + ) + + expect(client.gauge).to.have.been.calledWith('runtime.node.heap.size.by.space') + expect(client.gauge).to.have.been.calledWith('runtime.node.heap.used_size.by.space') + expect(client.gauge).to.have.been.calledWith('runtime.node.heap.available_size.by.space') + expect(client.gauge).to.have.been.calledWith('runtime.node.heap.physical_size.by.space') + + expect(client.flush).to.have.been.called + + done() + } catch (e) { + done(e) + } + })) }) }) diff --git a/packages/dd-trace/test/util.spec.js b/packages/dd-trace/test/util.spec.js index 40b209a96cf..f32b47c0cee 100644 --- a/packages/dd-trace/test/util.spec.js +++ b/packages/dd-trace/test/util.spec.js @@ -3,7 +3,6 @@ require('./setup/tap') const { isTrue, isFalse, globMatch } = require('../src/util') -const { generatePointerHash } = require('../src/util') const TRUES = [ 1, @@ -69,20 +68,3 @@ describe('util', () => { }) }) }) - -describe('generatePointerHash', () => { - it('should generate a valid hash for a basic S3 object', () => { - const hash = generatePointerHash(['some-bucket', 'some-key.data', 'ab12ef34']) - expect(hash).to.equal('e721375466d4116ab551213fdea08413') - }) - - it('should generate a valid hash for an S3 object with a non-ascii key', () => { - const hash1 = generatePointerHash(['some-bucket', 'some-key.你好', 'ab12ef34']) - expect(hash1).to.equal('d1333a04b9928ab462b5c6cadfa401f4') - }) - - it('should generate a valid hash for multipart-uploaded S3 object', () => { - const hash1 = generatePointerHash(['some-bucket', 'some-key.data', 'ab12ef34-5']) - expect(hash1).to.equal('2b90dffc37ebc7bc610152c3dc72af9f') - }) -}) diff --git a/yarn.lock b/yarn.lock index 49411da5f2f..4d8e42d2abc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -406,10 +406,10 @@ resolved "https://registry.yarnpkg.com/@datadog/libdatadog/-/libdatadog-0.3.0.tgz#2fc1e2695872840bc8c356f66acf675da428d6f0" integrity sha512-TbP8+WyXfh285T17FnLeLUOPl4SbkRYMqKgcmknID2mXHNrbt5XJgW9bnDgsrrtu31Q7FjWWw2WolgRLWyzLRA== -"@datadog/native-appsec@8.3.0": - version "8.3.0" - resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-8.3.0.tgz#91afd89d18d386be4da8a1b0e04500f2f8b5eb66" - integrity sha512-RYHbSJ/MwJcJaLzaCaZvUyNLUKFbMshayIiv4ckpFpQJDiq1T8t9iM2k7008s75g1vRuXfsRNX7MaLn4aoFuWA== +"@datadog/native-appsec@8.4.0": + version "8.4.0" + resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-8.4.0.tgz#5c44d949ff8f40a94c334554db79c1c470653bae" + integrity sha512-LC47AnpVLpQFEUOP/nIIs+i0wLb8XYO+et3ACaJlHa2YJM3asR4KZTqQjDQNy08PTAUbVvYWKwfSR1qVsU/BeA== dependencies: node-gyp-build "^3.9.0" @@ -956,10 +956,10 @@ dependencies: undici-types "~5.26.4" -"@types/node@^16.18.103": - version "16.18.103" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.103.tgz#5557c7c32a766fddbec4b933b1d5c365f89b20a4" - integrity sha512-gOAcUSik1nR/CRC3BsK8kr6tbmNIOTpvb1sT+v5Nmmys+Ho8YtnIHP90wEsVK4hTcHndOqPVIlehEGEA5y31bA== +"@types/node@^16.0.0": + version "16.18.122" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.122.tgz#54948ddbe2ddef8144ee16b37f160e3f99c32397" + integrity sha512-rF6rUBS80n4oK16EW8nE75U+9fw0SSUgoPtWSvHhPXdT7itbvmS7UjB/jyM8i3AkvI6yeSM5qCwo+xN0npGDHg== "@types/prop-types@*": version "15.7.5" @@ -1012,11 +1012,6 @@ resolved "https://registry.npmjs.org/@types/yoga-layout/-/yoga-layout-1.9.2.tgz" integrity sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw== -"@ungap/promise-all-settled@1.1.2": - version "1.1.2" - resolved "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz" - integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== - "@ungap/structured-clone@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" @@ -1063,10 +1058,10 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-colors@4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== +ansi-colors@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== ansi-escapes@^4.2.1: version "4.3.2" @@ -1355,6 +1350,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" @@ -1362,9 +1364,9 @@ braces@~3.0.2: dependencies: fill-range "^7.1.1" -browser-stdout@1.3.1: +browser-stdout@^1.3.1: version "1.3.1" - resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== browserslist@^4.21.9: @@ -1533,7 +1535,7 @@ checksum@^1.0.0: dependencies: optimist "~0.3.5" -chokidar@3.5.3, chokidar@^3.3.0: +chokidar@^3.3.0: version "3.5.3" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -1548,6 +1550,21 @@ chokidar@3.5.3, chokidar@^3.3.0: optionalDependencies: fsevents "~2.3.2" +chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + ci-info@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz" @@ -1818,13 +1835,6 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4.3.3: - version "4.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== - dependencies: - ms "2.1.2" - debug@4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" @@ -1846,6 +1856,13 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: dependencies: ms "2.1.2" +debug@^4.3.5: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" @@ -1918,17 +1935,12 @@ detect-newline@^3.0.0: resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" integrity "sha1-V29d/GOuGhkv8ZLYrTr2MImRtlE= sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==" -diff@5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz" - integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== - diff@^4.0.1, diff@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -diff@^5.1.0: +diff@^5.1.0, diff@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== @@ -2116,11 +2128,6 @@ escape-html@~1.0.3: resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== -escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" @@ -2131,6 +2138,11 @@ escape-string-regexp@^2.0.0: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + eslint-compat-utils@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz#7fc92b776d185a70c4070d03fd26fde3d59652e4" @@ -2471,14 +2483,6 @@ find-cache-dir@^3.2.0: make-dir "^3.0.2" pkg-dir "^4.1.0" -find-up@5.0.0, find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" @@ -2487,6 +2491,14 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + findit@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/findit/-/findit-2.0.0.tgz" @@ -2674,29 +2686,28 @@ glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob@7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" - integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== +glob@^7.0.5, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.3: + version "7.2.3" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.4" + minimatch "^3.1.1" once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.5, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.3: - version "7.2.3" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== +glob@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.1.1" + minimatch "^5.0.1" once "^1.3.0" - path-is-absolute "^1.0.0" globals@^11.1.0: version "11.12.0" @@ -2752,11 +2763,6 @@ graphql@0.13.2: dependencies: iterall "^1.2.1" -growl@1.10.5: - version "1.10.5" - resolved "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz" - integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== - has-async-hooks@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/has-async-hooks/-/has-async-hooks-1.0.0.tgz" @@ -2831,9 +2837,9 @@ hdr-histogram-percentiles-obj@^2.0.0: dependencies: hdr-histogram-js "^1.0.0" -he@1.2.0: +he@^1.2.0: version "1.2.0" - resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== html-escaper@^2.0.0: @@ -3292,13 +3298,6 @@ jmespath@0.16.0: resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@4.1.0, js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - js-yaml@^3.13.1: version "3.14.1" resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" @@ -3307,6 +3306,13 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" @@ -3457,7 +3463,7 @@ lodash@^4.17.13, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4: resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-symbols@4.1.0: +log-symbols@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== @@ -3560,13 +3566,6 @@ mimic-fn@^2.1.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -minimatch@4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" - integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== - dependencies: - brace-expansion "^1.1.7" - minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" @@ -3574,6 +3573,13 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1, minimatch@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.6: version "1.2.7" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz" @@ -3603,35 +3609,31 @@ mkdirp@^3.0.1: resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz" integrity "sha1-5E5MVgf7J5wWgkFxPMbg/qmty1A= sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==" -mocha@^9: - version "9.2.2" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9" - integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== - dependencies: - "@ungap/promise-all-settled" "1.1.2" - ansi-colors "4.1.1" - browser-stdout "1.3.1" - chokidar "3.5.3" - debug "4.3.3" - diff "5.0.0" - escape-string-regexp "4.0.0" - find-up "5.0.0" - glob "7.2.0" - growl "1.10.5" - he "1.2.0" - js-yaml "4.1.0" - log-symbols "4.1.0" - minimatch "4.2.1" - ms "2.1.3" - nanoid "3.3.8" - serialize-javascript "6.0.0" - strip-json-comments "3.1.1" - supports-color "8.1.1" - which "2.0.2" - workerpool "6.2.0" - yargs "16.2.0" - yargs-parser "20.2.4" - yargs-unparser "2.0.0" +mocha@^10: + version "10.8.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.8.2.tgz#8d8342d016ed411b12a429eb731b825f961afb96" + integrity sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg== + dependencies: + ansi-colors "^4.1.3" + browser-stdout "^1.3.1" + chokidar "^3.5.3" + debug "^4.3.5" + diff "^5.2.0" + escape-string-regexp "^4.0.0" + find-up "^5.0.0" + glob "^8.1.0" + he "^1.2.0" + js-yaml "^4.1.0" + log-symbols "^4.1.0" + minimatch "^5.1.6" + ms "^2.1.3" + serialize-javascript "^6.0.2" + strip-json-comments "^3.1.1" + supports-color "^8.1.1" + workerpool "^6.5.1" + yargs "^16.2.0" + yargs-parser "^20.2.9" + yargs-unparser "^2.0.0" module-details-from-path@^1.0.3: version "1.0.3" @@ -3653,7 +3655,7 @@ ms@2.1.2: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1, ms@^2.1.2: +ms@2.1.3, ms@^2.1.1, ms@^2.1.2, ms@^2.1.3: version "2.1.3" resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -3681,11 +3683,6 @@ multer@^1.4.5-lts.1: type-is "^1.6.4" xtend "^4.0.0" -nanoid@3.3.8: - version "3.3.8" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" - integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" @@ -4438,10 +4435,10 @@ send@0.19.0: range-parser "~1.2.1" statuses "2.0.1" -serialize-javascript@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" - integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== +serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== dependencies: randombytes "^2.1.0" @@ -4695,18 +4692,11 @@ strip-bom@^4.0.0: resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz" integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== -strip-json-comments@3.1.1, strip-json-comments@^3.1.1: +strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -supports-color@8.1.1: - version "8.1.1" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - supports-color@^5.3.0: version "5.5.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" @@ -4721,6 +4711,13 @@ supports-color@^7.1.0, supports-color@^7.2.0: dependencies: has-flag "^4.0.0" +supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" @@ -5142,7 +5139,7 @@ which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.2: gopd "^1.0.1" has-tostringtag "^1.0.2" -which@2.0.2, which@^2.0.1, which@^2.0.2: +which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== @@ -5166,10 +5163,10 @@ wordwrap@~0.0.2: resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" integrity sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw== -workerpool@6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" - integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== +workerpool@^6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" + integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== wrap-ansi@^6.2.0: version "6.2.0" @@ -5257,11 +5254,6 @@ yaml@^2.5.0: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.0.tgz#c6165a721cf8000e91c36490a41d7be25176cf5d" integrity sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw== -yargs-parser@20.2.4: - version "20.2.4" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz" - integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== - yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz" @@ -5270,14 +5262,14 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.2.2: +yargs-parser@^20.2.2, yargs-parser@^20.2.9: version "20.2.9" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-unparser@2.0.0: +yargs-unparser@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== dependencies: camelcase "^6.0.0" @@ -5285,19 +5277,6 @@ yargs-unparser@2.0.0: flat "^5.0.2" is-plain-obj "^2.1.0" -yargs@16.2.0: - version "16.2.0" - resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - yargs@^15.0.2: version "15.4.1" resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz" @@ -5315,6 +5294,19 @@ yargs@^15.0.2: y18n "^4.0.0" yargs-parser "^18.1.2" +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"