dd-trace 5.86.0 → 5.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-3rdparty.csv +60 -32
- package/ext/exporters.d.ts +1 -0
- package/ext/exporters.js +1 -0
- package/index.d.ts +243 -7
- package/package.json +9 -6
- package/packages/datadog-instrumentations/src/ai.js +54 -90
- package/packages/datadog-instrumentations/src/cucumber.js +14 -0
- package/packages/datadog-instrumentations/src/helpers/hook.js +17 -11
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +55 -14
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +15 -13
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +103 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.js +108 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/transformer.js +21 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +138 -12
- package/packages/datadog-instrumentations/src/http/client.js +119 -1
- package/packages/datadog-instrumentations/src/jest.js +179 -15
- package/packages/datadog-instrumentations/src/kafkajs.js +20 -17
- package/packages/datadog-instrumentations/src/mocha/utils.js +6 -0
- package/packages/datadog-instrumentations/src/mysql2.js +131 -64
- package/packages/datadog-instrumentations/src/playwright.js +9 -1
- package/packages/datadog-instrumentations/src/stripe.js +92 -0
- package/packages/datadog-instrumentations/src/vitest.js +11 -0
- package/packages/datadog-plugin-amqplib/src/consumer.js +14 -10
- package/packages/datadog-plugin-amqplib/src/producer.js +23 -19
- package/packages/datadog-plugin-azure-functions/src/index.js +53 -37
- package/packages/datadog-plugin-bullmq/src/consumer.js +33 -11
- package/packages/datadog-plugin-bullmq/src/producer.js +60 -31
- package/packages/datadog-plugin-cucumber/src/index.js +9 -6
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +33 -0
- package/packages/datadog-plugin-cypress/src/support.js +48 -8
- package/packages/datadog-plugin-jest/src/index.js +12 -2
- package/packages/datadog-plugin-jest/src/util.js +2 -1
- package/packages/datadog-plugin-kafkajs/src/consumer.js +22 -12
- package/packages/datadog-plugin-kafkajs/src/producer.js +33 -22
- package/packages/datadog-plugin-mocha/src/index.js +9 -6
- package/packages/datadog-plugin-playwright/src/index.js +10 -6
- package/packages/datadog-plugin-vitest/src/index.js +13 -8
- package/packages/dd-trace/src/appsec/addresses.js +11 -0
- package/packages/dd-trace/src/appsec/channels.js +5 -1
- package/packages/dd-trace/src/appsec/downstream_requests.js +302 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +4 -5
- package/packages/dd-trace/src/appsec/iast/path-line.js +36 -25
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +3 -2
- package/packages/dd-trace/src/appsec/index.js +103 -0
- package/packages/dd-trace/src/appsec/rasp/ssrf.js +66 -4
- package/packages/dd-trace/src/azure_metadata.js +0 -2
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +14 -1
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +2 -0
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -1
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -1
- package/packages/dd-trace/src/ci-visibility/requests/request.js +236 -0
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
- package/packages/dd-trace/src/config/defaults.js +148 -195
- package/packages/dd-trace/src/config/helper.js +43 -1
- package/packages/dd-trace/src/config/index.js +42 -14
- package/packages/dd-trace/src/config/supported-configurations.json +4115 -510
- package/packages/dd-trace/src/constants.js +0 -2
- package/packages/dd-trace/src/crashtracking/crashtracker.js +10 -3
- package/packages/dd-trace/src/datastreams/pathway.js +22 -3
- package/packages/dd-trace/src/datastreams/processor.js +14 -1
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +47 -2
- package/packages/dd-trace/src/debugger/devtools_client/index.js +75 -23
- package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +23 -1
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +3 -3
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +168 -36
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +18 -0
- package/packages/dd-trace/src/encode/agentless-json.js +141 -0
- package/packages/dd-trace/src/exporter.js +2 -0
- package/packages/dd-trace/src/exporters/agent/writer.js +22 -8
- package/packages/dd-trace/src/exporters/agentless/index.js +89 -0
- package/packages/dd-trace/src/exporters/agentless/writer.js +184 -0
- package/packages/dd-trace/src/exporters/common/agents.js +1 -1
- package/packages/dd-trace/src/exporters/common/request.js +4 -4
- package/packages/dd-trace/src/llmobs/constants/writers.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -3
- package/packages/dd-trace/src/llmobs/sdk.js +34 -5
- package/packages/dd-trace/src/opentelemetry/context_manager.js +19 -46
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +3 -4
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -5
- package/packages/dd-trace/src/opentracing/span.js +6 -4
- package/packages/dd-trace/src/plugins/ci_plugin.js +57 -5
- package/packages/dd-trace/src/plugins/database.js +57 -45
- package/packages/dd-trace/src/plugins/outbound.js +27 -2
- package/packages/dd-trace/src/plugins/tracing.js +39 -4
- package/packages/dd-trace/src/plugins/util/inferred_proxy.js +7 -0
- package/packages/dd-trace/src/plugins/util/test.js +48 -0
- package/packages/dd-trace/src/plugins/util/web.js +8 -7
- package/packages/dd-trace/src/profiling/exporter_cli.js +1 -0
- package/packages/dd-trace/src/propagation-hash/index.js +145 -0
- package/packages/dd-trace/src/proxy.js +4 -0
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
- package/packages/dd-trace/src/startup-log.js +3 -3
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json +0 -106
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +0 -741
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +0 -11
- package/packages/dd-trace/src/plugins/util/serverless.js +0 -8
- package/packages/dd-trace/src/scope/noop/scope.js +0 -21
|
@@ -40,14 +40,18 @@ class KafkajsConsumerPlugin extends ConsumerPlugin {
|
|
|
40
40
|
* @returns {ConsumerBacklog}
|
|
41
41
|
*/
|
|
42
42
|
transformCommit (commit) {
|
|
43
|
-
const { groupId, partition, offset, topic } = commit
|
|
44
|
-
|
|
43
|
+
const { groupId, partition, offset, topic, clusterId } = commit
|
|
44
|
+
const backlog = {
|
|
45
45
|
partition,
|
|
46
46
|
topic,
|
|
47
47
|
type: 'kafka_commit',
|
|
48
48
|
offset: Number(offset),
|
|
49
49
|
consumer_group: groupId,
|
|
50
50
|
}
|
|
51
|
+
if (clusterId) {
|
|
52
|
+
backlog.kafka_cluster_id = clusterId
|
|
53
|
+
}
|
|
54
|
+
return backlog
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
commit (commitList) {
|
|
@@ -65,6 +69,22 @@ class KafkajsConsumerPlugin extends ConsumerPlugin {
|
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
start (ctx) {
|
|
73
|
+
if (!this.config.dsmEnabled) return
|
|
74
|
+
const { topic, message, groupId, clusterId } = ctx.extractedArgs || ctx
|
|
75
|
+
const headers = convertToTextMap(message?.headers)
|
|
76
|
+
if (!headers) return
|
|
77
|
+
|
|
78
|
+
const { span } = ctx.currentStore
|
|
79
|
+
const payloadSize = getMessageSize(message)
|
|
80
|
+
this.tracer.decodeDataStreamsContext(headers)
|
|
81
|
+
const edgeTags = ['direction:in', `group:${groupId}`, `topic:${topic}`, 'type:kafka']
|
|
82
|
+
if (clusterId) {
|
|
83
|
+
edgeTags.push(`kafka_cluster_id:${clusterId}`)
|
|
84
|
+
}
|
|
85
|
+
this.tracer.setCheckpoint(edgeTags, span, payloadSize)
|
|
86
|
+
}
|
|
87
|
+
|
|
68
88
|
bindStart (ctx) {
|
|
69
89
|
const { topic, partition, message, groupId, clusterId } = ctx.extractedArgs || ctx
|
|
70
90
|
|
|
@@ -89,16 +109,6 @@ class KafkajsConsumerPlugin extends ConsumerPlugin {
|
|
|
89
109
|
}, ctx)
|
|
90
110
|
if (message?.offset) span.setTag('kafka.message.offset', message?.offset)
|
|
91
111
|
|
|
92
|
-
if (this.config.dsmEnabled && headers) {
|
|
93
|
-
const payloadSize = getMessageSize(message)
|
|
94
|
-
this.tracer.decodeDataStreamsContext(headers)
|
|
95
|
-
const edgeTags = ['direction:in', `group:${groupId}`, `topic:${topic}`, 'type:kafka']
|
|
96
|
-
if (clusterId) {
|
|
97
|
-
edgeTags.push(`kafka_cluster_id:${clusterId}`)
|
|
98
|
-
}
|
|
99
|
-
this.tracer.setCheckpoint(edgeTags, span, payloadSize)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
112
|
if (afterStartCh.hasSubscribers) {
|
|
103
113
|
afterStartCh.publish({ topic, partition, message, groupId, currentStore: ctx.currentStore })
|
|
104
114
|
}
|
|
@@ -37,16 +37,20 @@ class KafkajsProducerPlugin extends ProducerPlugin {
|
|
|
37
37
|
* @param {ProducerResponseItem} response
|
|
38
38
|
* @returns {ProducerBacklog}
|
|
39
39
|
*/
|
|
40
|
-
transformProduceResponse (response) {
|
|
40
|
+
transformProduceResponse (response, clusterId) {
|
|
41
41
|
// In produce protocol >=v3, the offset key changes from `offset` to `baseOffset`
|
|
42
42
|
const { topicName: topic, partition, offset, baseOffset } = response
|
|
43
43
|
const offsetAsLong = offset || baseOffset
|
|
44
|
-
|
|
44
|
+
const backlog = {
|
|
45
45
|
type: 'kafka_produce',
|
|
46
46
|
partition,
|
|
47
47
|
offset: offsetAsLong ? Number(offsetAsLong) : undefined,
|
|
48
48
|
topic,
|
|
49
49
|
}
|
|
50
|
+
if (clusterId) {
|
|
51
|
+
backlog.kafka_cluster_id = clusterId
|
|
52
|
+
}
|
|
53
|
+
return backlog
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
/**
|
|
@@ -56,6 +60,7 @@ class KafkajsProducerPlugin extends ProducerPlugin {
|
|
|
56
60
|
*/
|
|
57
61
|
commit (ctx) {
|
|
58
62
|
const commitList = ctx.result
|
|
63
|
+
const clusterId = ctx.clusterId
|
|
59
64
|
|
|
60
65
|
if (!this.config.dsmEnabled) return
|
|
61
66
|
if (!commitList || !Array.isArray(commitList)) return
|
|
@@ -65,12 +70,33 @@ class KafkajsProducerPlugin extends ProducerPlugin {
|
|
|
65
70
|
'offset',
|
|
66
71
|
'topic',
|
|
67
72
|
]
|
|
68
|
-
for (const commit of commitList.map(this.transformProduceResponse)) {
|
|
73
|
+
for (const commit of commitList.map(r => this.transformProduceResponse(r, clusterId))) {
|
|
69
74
|
if (keys.some(key => !commit.hasOwnProperty(key))) continue
|
|
70
75
|
this.tracer.setOffset(commit)
|
|
71
76
|
}
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
start (ctx) {
|
|
80
|
+
if (!this.config.dsmEnabled) return
|
|
81
|
+
const { topic, messages, clusterId, disableHeaderInjection, currentStore: { span } } = ctx
|
|
82
|
+
|
|
83
|
+
for (const message of messages) {
|
|
84
|
+
if (message !== null && typeof message === 'object') {
|
|
85
|
+
const payloadSize = getMessageSize(message)
|
|
86
|
+
const edgeTags = ['direction:out', `topic:${topic}`, 'type:kafka']
|
|
87
|
+
|
|
88
|
+
if (clusterId) {
|
|
89
|
+
edgeTags.push(`kafka_cluster_id:${clusterId}`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const dataStreamsContext = this.tracer.setCheckpoint(edgeTags, span, payloadSize)
|
|
93
|
+
if (!disableHeaderInjection) {
|
|
94
|
+
DsmPathwayCodec.encode(dataStreamsContext, message.headers)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
74
100
|
bindStart (ctx) {
|
|
75
101
|
const { topic, messages, bootstrapServers, clusterId, disableHeaderInjection } = ctx
|
|
76
102
|
const span = this.startSpan({
|
|
@@ -89,25 +115,10 @@ class KafkajsProducerPlugin extends ProducerPlugin {
|
|
|
89
115
|
span.setTag(BOOTSTRAP_SERVERS_KEY, bootstrapServers)
|
|
90
116
|
}
|
|
91
117
|
for (const message of messages) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
this.tracer.inject(span, 'text_map', message.headers)
|
|
97
|
-
}
|
|
98
|
-
if (this.config.dsmEnabled) {
|
|
99
|
-
const payloadSize = getMessageSize(message)
|
|
100
|
-
const edgeTags = ['direction:out', `topic:${topic}`, 'type:kafka']
|
|
101
|
-
|
|
102
|
-
if (clusterId) {
|
|
103
|
-
edgeTags.push(`kafka_cluster_id:${clusterId}`)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const dataStreamsContext = this.tracer.setCheckpoint(edgeTags, span, payloadSize)
|
|
107
|
-
if (!disableHeaderInjection) {
|
|
108
|
-
DsmPathwayCodec.encode(dataStreamsContext, message.headers)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
118
|
+
// message headers are not supported for kafka broker versions <0.11
|
|
119
|
+
if (message !== null && typeof message === 'object' && !disableHeaderInjection) {
|
|
120
|
+
message.headers ??= {}
|
|
121
|
+
this.tracer.inject(span, 'text_map', message.headers)
|
|
111
122
|
}
|
|
112
123
|
}
|
|
113
124
|
|
|
@@ -93,12 +93,15 @@ class MochaPlugin extends CiPlugin {
|
|
|
93
93
|
return
|
|
94
94
|
}
|
|
95
95
|
const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.sourceRoot)
|
|
96
|
-
const testSuiteMetadata =
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
const testSuiteMetadata = {
|
|
97
|
+
...getTestSuiteCommonTags(
|
|
98
|
+
this.command,
|
|
99
|
+
this.frameworkVersion,
|
|
100
|
+
testSuite,
|
|
101
|
+
'mocha'
|
|
102
|
+
),
|
|
103
|
+
...this.getSessionRequestErrorTags(),
|
|
104
|
+
}
|
|
102
105
|
if (isUnskippable) {
|
|
103
106
|
testSuiteMetadata[TEST_ITR_UNSKIPPABLE] = 'true'
|
|
104
107
|
this.telemetry.count(TELEMETRY_ITR_UNSKIPPABLE, { testLevel: 'suite' })
|
|
@@ -120,12 +120,15 @@ class PlaywrightPlugin extends CiPlugin {
|
|
|
120
120
|
const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.rootDir)
|
|
121
121
|
const testSourceFile = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
|
|
122
122
|
|
|
123
|
-
const testSuiteMetadata =
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
const testSuiteMetadata = {
|
|
124
|
+
...getTestSuiteCommonTags(
|
|
125
|
+
this.command,
|
|
126
|
+
this.frameworkVersion,
|
|
127
|
+
testSuite,
|
|
128
|
+
'playwright'
|
|
129
|
+
),
|
|
130
|
+
...this.getSessionRequestErrorTags(),
|
|
131
|
+
}
|
|
129
132
|
if (testSourceFile) {
|
|
130
133
|
testSuiteMetadata[TEST_SOURCE_FILE] = testSourceFile
|
|
131
134
|
testSuiteMetadata[TEST_SOURCE_START] = 1
|
|
@@ -222,6 +225,7 @@ class PlaywrightPlugin extends CiPlugin {
|
|
|
222
225
|
// for a test session. They can be passed the same way `DD_PLAYWRIGHT_WORKER` is passed.
|
|
223
226
|
formattedSpan.meta[TEST_SESSION_ID] = this.testSessionSpan.context().toTraceId()
|
|
224
227
|
formattedSpan.meta[TEST_MODULE_ID] = this.testModuleSpan.context().toSpanId()
|
|
228
|
+
Object.assign(formattedSpan.meta, this.getSessionRequestErrorTags())
|
|
225
229
|
formattedSpan.meta[TEST_COMMAND] = this.command
|
|
226
230
|
formattedSpan.meta[TEST_MODULE] = this.constructor.id
|
|
227
231
|
// MISSING _trace.startTime and _trace.ticks - because by now the suite is already serialized
|
|
@@ -275,9 +275,11 @@ class VitestPlugin extends CiPlugin {
|
|
|
275
275
|
this.addBind('ci:vitest:test-suite:start', (ctx) => {
|
|
276
276
|
const { testSuiteAbsolutePath, frameworkVersion } = ctx
|
|
277
277
|
|
|
278
|
+
// TODO: Handle case where the command is not set
|
|
278
279
|
this.command = getValueFromEnvSources('DD_CIVISIBILITY_TEST_COMMAND')
|
|
279
280
|
this.frameworkVersion = frameworkVersion
|
|
280
281
|
const testSessionSpanContext = this.tracer.extract('text_map', {
|
|
282
|
+
// TODO: Handle case where the session ID or module ID is not set
|
|
281
283
|
'x-datadog-trace-id': getValueFromEnvSources('DD_CIVISIBILITY_TEST_SESSION_ID'),
|
|
282
284
|
'x-datadog-parent-id': getValueFromEnvSources('DD_CIVISIBILITY_TEST_MODULE_ID'),
|
|
283
285
|
})
|
|
@@ -301,14 +303,17 @@ class VitestPlugin extends CiPlugin {
|
|
|
301
303
|
}
|
|
302
304
|
|
|
303
305
|
const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
306
|
+
// Request error tags are applied to test spans in the main process (worker-report:trace handler)
|
|
307
|
+
const testSuiteMetadata = {
|
|
308
|
+
...getTestSuiteCommonTags(
|
|
309
|
+
this.command,
|
|
310
|
+
this.frameworkVersion,
|
|
311
|
+
testSuite,
|
|
312
|
+
'vitest'
|
|
313
|
+
),
|
|
314
|
+
[TEST_SOURCE_FILE]: testSuite,
|
|
315
|
+
[TEST_SOURCE_START]: 1,
|
|
316
|
+
}
|
|
312
317
|
|
|
313
318
|
const codeOwners = this.getCodeOwners(testSuiteMetadata)
|
|
314
319
|
if (codeOwners) {
|
|
@@ -27,6 +27,12 @@ module.exports = {
|
|
|
27
27
|
WAF_CONTEXT_PROCESSOR: 'waf.context.processor',
|
|
28
28
|
|
|
29
29
|
HTTP_OUTGOING_URL: 'server.io.net.url',
|
|
30
|
+
HTTP_OUTGOING_METHOD: 'server.io.net.request.method',
|
|
31
|
+
HTTP_OUTGOING_HEADERS: 'server.io.net.request.headers',
|
|
32
|
+
HTTP_OUTGOING_RESPONSE_STATUS: 'server.io.net.response.status',
|
|
33
|
+
HTTP_OUTGOING_RESPONSE_HEADERS: 'server.io.net.response.headers',
|
|
34
|
+
HTTP_OUTGOING_RESPONSE_BODY: 'server.io.net.response.body',
|
|
35
|
+
|
|
30
36
|
FS_OPERATION_PATH: 'server.io.fs.file',
|
|
31
37
|
|
|
32
38
|
DB_STATEMENT: 'server.db.statement',
|
|
@@ -37,4 +43,9 @@ module.exports = {
|
|
|
37
43
|
|
|
38
44
|
LOGIN_SUCCESS: 'server.business_logic.users.login.success',
|
|
39
45
|
LOGIN_FAILURE: 'server.business_logic.users.login.failure',
|
|
46
|
+
|
|
47
|
+
PAYMENT_CREATION: 'server.business_logic.payment.creation',
|
|
48
|
+
PAYMENT_SUCCESS: 'server.business_logic.payment.success',
|
|
49
|
+
PAYMENT_FAILURE: 'server.business_logic.payment.failure',
|
|
50
|
+
PAYMENT_CANCELLATION: 'server.business_logic.payment.cancellation',
|
|
40
51
|
}
|
|
@@ -23,6 +23,7 @@ module.exports = {
|
|
|
23
23
|
fsOperationStart: dc.channel('apm:fs:operation:start'),
|
|
24
24
|
graphqlMiddlewareChannel: dc.tracingChannel('datadog:apollo:middleware'),
|
|
25
25
|
httpClientRequestStart: dc.channel('apm:http:client:request:start'),
|
|
26
|
+
httpClientResponseFinish: dc.channel('apm:http:client:response:finish'),
|
|
26
27
|
incomingHttpRequestEnd: dc.channel('dd-trace:incomingHttpRequestEnd'),
|
|
27
28
|
incomingHttpRequestStart: dc.channel('dd-trace:incomingHttpRequestStart'),
|
|
28
29
|
multerParser: dc.channel('datadog:multer:read:finish'),
|
|
@@ -37,10 +38,13 @@ module.exports = {
|
|
|
37
38
|
responseBody: dc.channel('datadog:express:response:json:start'),
|
|
38
39
|
responseSetHeader: dc.channel('datadog:http:server:response:set-header:start'),
|
|
39
40
|
responseWriteHead: dc.channel('apm:http:server:response:writeHead:start'),
|
|
40
|
-
routerParam: dc.channel('datadog:router:param:start'),
|
|
41
41
|
routerMiddlewareError: dc.channel('apm:router:middleware:error'),
|
|
42
|
+
routerParam: dc.channel('datadog:router:param:start'),
|
|
42
43
|
setCookieChannel: dc.channel('datadog:iast:set-cookie'),
|
|
43
44
|
setUncaughtExceptionCaptureCallbackStart: dc.channel('datadog:process:setUncaughtExceptionCaptureCallback:start'),
|
|
44
45
|
startGraphqlResolve: dc.channel('datadog:graphql:resolver:start'),
|
|
46
|
+
stripeCheckoutSessionCreate: dc.channel('datadog:stripe:checkoutSession:create:finish'),
|
|
47
|
+
stripeConstructEvent: dc.channel('datadog:stripe:constructEvent:finish'),
|
|
48
|
+
stripePaymentIntentCreate: dc.channel('datadog:stripe:paymentIntent:create:finish'),
|
|
45
49
|
wafRunFinished: dc.channel('datadog:waf:run:finish'),
|
|
46
50
|
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const web = require('../plugins/util/web')
|
|
4
|
+
const log = require('../log')
|
|
5
|
+
const {
|
|
6
|
+
HTTP_OUTGOING_METHOD,
|
|
7
|
+
HTTP_OUTGOING_HEADERS,
|
|
8
|
+
HTTP_OUTGOING_RESPONSE_STATUS,
|
|
9
|
+
HTTP_OUTGOING_RESPONSE_HEADERS,
|
|
10
|
+
HTTP_OUTGOING_RESPONSE_BODY,
|
|
11
|
+
} = require('./addresses')
|
|
12
|
+
|
|
13
|
+
const KNUTH_FACTOR = 11400714819323199488n // eslint-disable-line unicorn/numeric-separators-style
|
|
14
|
+
const UINT64_MAX = (1n << 64n) - 1n
|
|
15
|
+
|
|
16
|
+
let config
|
|
17
|
+
let samplingRate
|
|
18
|
+
let globalRequestCounter
|
|
19
|
+
let bodyAnalysisCount
|
|
20
|
+
let downstreamAnalysisCount
|
|
21
|
+
let redirectBodyCollectionDecisions
|
|
22
|
+
|
|
23
|
+
function enable (_config) {
|
|
24
|
+
config = _config
|
|
25
|
+
globalRequestCounter = 0n
|
|
26
|
+
bodyAnalysisCount = new WeakMap()
|
|
27
|
+
downstreamAnalysisCount = new WeakMap()
|
|
28
|
+
redirectBodyCollectionDecisions = new WeakMap()
|
|
29
|
+
|
|
30
|
+
const bodyAnalysisSampleRate = config.appsec.apiSecurity?.downstreamBodyAnalysisSampleRate
|
|
31
|
+
samplingRate = Math.min(Math.max(bodyAnalysisSampleRate, 0), 1)
|
|
32
|
+
|
|
33
|
+
if (samplingRate !== bodyAnalysisSampleRate) {
|
|
34
|
+
log.warn(
|
|
35
|
+
'DD_API_SECURITY_DOWNSTREAM_BODY_ANALYSIS_SAMPLE_RATE value is %s and it\'s out of range',
|
|
36
|
+
bodyAnalysisSampleRate)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function disable () {
|
|
41
|
+
config = null
|
|
42
|
+
globalRequestCounter = null
|
|
43
|
+
bodyAnalysisCount = null
|
|
44
|
+
downstreamAnalysisCount = null
|
|
45
|
+
redirectBodyCollectionDecisions = null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check we have a stored redirect body collection decision for a given URL.
|
|
50
|
+
* @param {import('http').IncomingMessage} req outgoing request.
|
|
51
|
+
* @param {string} outgoingUrl the URL being requested.
|
|
52
|
+
* @returns {boolean} the stored decision
|
|
53
|
+
*/
|
|
54
|
+
function consumeRedirectBodyCollectionDecision (req, outgoingUrl) {
|
|
55
|
+
const decisions = redirectBodyCollectionDecisions.get(req)
|
|
56
|
+
if (!decisions) return false
|
|
57
|
+
|
|
58
|
+
return decisions.delete(outgoingUrl)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Stores a redirect body collection decision for a follow-up request.
|
|
63
|
+
* @param {import('http').IncomingMessage} req outgoing request.
|
|
64
|
+
* @param {string} redirectUrl the URL to redirect to.
|
|
65
|
+
*/
|
|
66
|
+
function storeRedirectBodyCollectionDecision (req, redirectUrl) {
|
|
67
|
+
let decisions = redirectBodyCollectionDecisions.get(req)
|
|
68
|
+
|
|
69
|
+
if (!decisions) {
|
|
70
|
+
decisions = new Set()
|
|
71
|
+
redirectBodyCollectionDecisions.set(req, decisions)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
decisions.add(redirectUrl)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Determines whether the current downstream request/responses bodies should be sampled for analysis.
|
|
79
|
+
* @param {import('http').IncomingMessage} req outgoing request.
|
|
80
|
+
* @param {string} outgoingUrl the URL being requested (to check for redirect decisions).
|
|
81
|
+
* @returns {boolean} true when the downstream response body should be captured.
|
|
82
|
+
*/
|
|
83
|
+
function shouldSampleBody (req, outgoingUrl) {
|
|
84
|
+
// Check if there's a stored decision from a previous redirect
|
|
85
|
+
const storedDecision = consumeRedirectBodyCollectionDecision(req, outgoingUrl)
|
|
86
|
+
if (storedDecision) return true
|
|
87
|
+
|
|
88
|
+
globalRequestCounter = (globalRequestCounter + 1n) & UINT64_MAX
|
|
89
|
+
|
|
90
|
+
const currentCount = bodyAnalysisCount.get(req) || 0
|
|
91
|
+
if (currentCount >= config.appsec.apiSecurity?.maxDownstreamRequestBodyAnalysis) {
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const hashed = (globalRequestCounter * KNUTH_FACTOR) % UINT64_MAX
|
|
96
|
+
// Replace 1000n with the accuraccy that we want to maintain
|
|
97
|
+
const threshold = (UINT64_MAX * BigInt(Math.round(samplingRate * 1000))) / 1000n
|
|
98
|
+
|
|
99
|
+
const shouldCollectBody = hashed <= threshold
|
|
100
|
+
|
|
101
|
+
// Track body analysis count if we're sampling the response body
|
|
102
|
+
if (shouldCollectBody) {
|
|
103
|
+
incrementBodyAnalysisCount(req)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return shouldCollectBody
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Increments the number of downstream body analyses performed for the given request.
|
|
111
|
+
* @param {import('http').IncomingMessage} req outgoing request.
|
|
112
|
+
*/
|
|
113
|
+
function incrementBodyAnalysisCount (req) {
|
|
114
|
+
const currentCount = bodyAnalysisCount.get(req) || 0
|
|
115
|
+
bodyAnalysisCount.set(req, currentCount + 1)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
*
|
|
120
|
+
* @param {object} headers
|
|
121
|
+
* @returns {object} the headers with all keys converted to lowercase
|
|
122
|
+
*/
|
|
123
|
+
function lowercaseHeaderKeys (headers) {
|
|
124
|
+
return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Extracts request data from the context for WAF analysis
|
|
129
|
+
* @param {object} ctx context for the outgoing downstream request.
|
|
130
|
+
* @returns {object} a map of addresses and request data.
|
|
131
|
+
*/
|
|
132
|
+
function extractRequestData (ctx) {
|
|
133
|
+
const addresses = {}
|
|
134
|
+
|
|
135
|
+
const options = ctx?.args?.options || {}
|
|
136
|
+
|
|
137
|
+
addresses[HTTP_OUTGOING_METHOD] = getMethod(options.method)
|
|
138
|
+
|
|
139
|
+
const headers = options?.headers
|
|
140
|
+
if (headers && Object.keys(headers).length > 0) {
|
|
141
|
+
addresses[HTTP_OUTGOING_HEADERS] = lowercaseHeaderKeys(headers)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return addresses
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Checks if a response is a redirect
|
|
149
|
+
* @param {import('http').IncomingMessage} req incoming server request.
|
|
150
|
+
* @param {import('http').IncomingMessage} res downstream response object.
|
|
151
|
+
* @returns {boolean} is redirect.
|
|
152
|
+
*/
|
|
153
|
+
function handleRedirectResponse (req, res) {
|
|
154
|
+
const isRedirect = res.statusCode >= 300 && res.statusCode < 400
|
|
155
|
+
const redirectLocation = res.headers?.location || ''
|
|
156
|
+
|
|
157
|
+
if (isRedirect && redirectLocation) {
|
|
158
|
+
// Store the body collection decision for the redirect target
|
|
159
|
+
storeRedirectBodyCollectionDecision(req, redirectLocation)
|
|
160
|
+
return true
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return false
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Extracts response data for WAF analysis.
|
|
168
|
+
* @param {import('http').IncomingMessage} res downstream response object.
|
|
169
|
+
* @param {Buffer|string|object|null} responseBody response body.
|
|
170
|
+
* @returns {object} a map of addresses and response data.
|
|
171
|
+
*/
|
|
172
|
+
function extractResponseData (res, responseBody) {
|
|
173
|
+
const addresses = {}
|
|
174
|
+
|
|
175
|
+
if (res.statusCode) {
|
|
176
|
+
addresses[HTTP_OUTGOING_RESPONSE_STATUS] = String(res.statusCode)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const headers = res.headers
|
|
180
|
+
if (headers && Object.keys(headers).length > 0) {
|
|
181
|
+
addresses[HTTP_OUTGOING_RESPONSE_HEADERS] = headers
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (responseBody) {
|
|
185
|
+
// Parse the body based on content-type
|
|
186
|
+
const contentType = res.headers?.['content-type']
|
|
187
|
+
const body = parseBody(responseBody, contentType)
|
|
188
|
+
|
|
189
|
+
if (body) {
|
|
190
|
+
addresses[HTTP_OUTGOING_RESPONSE_BODY] = body
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return addresses
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Tracks how many downstream analyses were executed for a given request and updates tracing tags.
|
|
199
|
+
* @param {import('http').IncomingMessage} req outgoing request.
|
|
200
|
+
*/
|
|
201
|
+
function incrementDownstreamAnalysisCount (req) {
|
|
202
|
+
const currentCount = downstreamAnalysisCount.get(req) || 0
|
|
203
|
+
downstreamAnalysisCount.set(req, currentCount + 1)
|
|
204
|
+
|
|
205
|
+
const span = web.root(req)
|
|
206
|
+
|
|
207
|
+
if (span) {
|
|
208
|
+
span.setTag('_dd.appsec.downstream_request', currentCount + 1)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Returns the HTTP method to use for a downstream request, defaulting to GET.
|
|
214
|
+
* @param {string} method method supplied in the outgoing request options.
|
|
215
|
+
* @returns {string} validated HTTP method.
|
|
216
|
+
*/
|
|
217
|
+
function getMethod (method) {
|
|
218
|
+
return typeof method === 'string' && method ? method : 'GET'
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Parses a downstream response body.
|
|
223
|
+
* @param {Buffer|string|object|null} body raw response body
|
|
224
|
+
* @param {string|null} contentType response content-type used to select the parser.
|
|
225
|
+
* @returns {object|null} parsed body object or null when not supported.
|
|
226
|
+
*/
|
|
227
|
+
function parseBody (body, contentType) {
|
|
228
|
+
if (!body || !contentType) {
|
|
229
|
+
return null
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const mime = extractMimeType(contentType)
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
if (mime === 'application/json' || mime === 'text/json') {
|
|
236
|
+
if (typeof body === 'string') {
|
|
237
|
+
return JSON.parse(body)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (Buffer.isBuffer(body)) {
|
|
241
|
+
return JSON.parse(body.toString('utf8'))
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return null
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (mime === 'application/x-www-form-urlencoded') {
|
|
248
|
+
const formBody = Buffer.isBuffer(body) ? body.toString('utf8') : String(body)
|
|
249
|
+
const params = new URLSearchParams(formBody)
|
|
250
|
+
const result = {}
|
|
251
|
+
for (const [key, value] of params.entries()) {
|
|
252
|
+
if (key in result) {
|
|
253
|
+
const existing = result[key]
|
|
254
|
+
if (Array.isArray(existing)) {
|
|
255
|
+
existing.push(value)
|
|
256
|
+
} else {
|
|
257
|
+
result[key] = [existing, value]
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
result[key] = value
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return result
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// multipart/form-data is mentioned in RFC but parsing is complex.
|
|
268
|
+
// Other content-types also discarded per RFC
|
|
269
|
+
|
|
270
|
+
return null
|
|
271
|
+
} catch {
|
|
272
|
+
// Parsing failed: return null to avoid sending malformed body to WAF
|
|
273
|
+
return null
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Extracts the MIME type portion of a content-type header value.
|
|
279
|
+
* @param {string|null} contentType raw content-type header value.
|
|
280
|
+
* @returns {string|null} lowercase mime type
|
|
281
|
+
*/
|
|
282
|
+
function extractMimeType (contentType) {
|
|
283
|
+
if (typeof contentType !== 'string') {
|
|
284
|
+
return null
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return contentType.split(';', 1)[0].trim().toLowerCase()
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
module.exports = {
|
|
291
|
+
enable,
|
|
292
|
+
disable,
|
|
293
|
+
shouldSampleBody,
|
|
294
|
+
handleRedirectResponse,
|
|
295
|
+
incrementDownstreamAnalysisCount,
|
|
296
|
+
extractRequestData,
|
|
297
|
+
extractResponseData,
|
|
298
|
+
// exports for tests
|
|
299
|
+
parseBody,
|
|
300
|
+
getMethod,
|
|
301
|
+
storeRedirectBodyCollectionDecision,
|
|
302
|
+
}
|
|
@@ -12,7 +12,7 @@ class SSRFAnalyzer extends InjectionAnalyzer {
|
|
|
12
12
|
this.addSub('apm:http:client:request:start', ({ args }) => {
|
|
13
13
|
if (typeof args.originalUrl === 'string') {
|
|
14
14
|
this.analyze(args.originalUrl)
|
|
15
|
-
} else if (args.options
|
|
15
|
+
} else if (args.options?.host) {
|
|
16
16
|
this.analyze(args.options.host)
|
|
17
17
|
}
|
|
18
18
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { storage } = require('../../../../../datadog-core')
|
|
4
|
-
const {
|
|
4
|
+
const { getCallSiteFramesForLocation } = require('../path-line')
|
|
5
5
|
const { getIastContext, getIastStackTraceId } = require('../iast-context')
|
|
6
6
|
const overheadController = require('../overhead-controller')
|
|
7
7
|
const { SinkIastPlugin } = require('../iast-plugin')
|
|
@@ -35,9 +35,8 @@ class Analyzer extends SinkIastPlugin {
|
|
|
35
35
|
|
|
36
36
|
_reportEvidence (value, context, evidence) {
|
|
37
37
|
const callSiteFrames = getVulnerabilityCallSiteFrames()
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
const location = this._getLocation(value, nonDDCallSiteFrames)
|
|
38
|
+
const frames = getCallSiteFramesForLocation(callSiteFrames, this._getExcludedPaths())
|
|
39
|
+
const location = this._getLocation(value, frames)
|
|
41
40
|
|
|
42
41
|
if (!this._isExcluded(location)) {
|
|
43
42
|
const originalLocation = this._getOriginalLocation(location)
|
|
@@ -51,7 +50,7 @@ class Analyzer extends SinkIastPlugin {
|
|
|
51
50
|
stackId
|
|
52
51
|
)
|
|
53
52
|
|
|
54
|
-
addVulnerability(context, vulnerability,
|
|
53
|
+
addVulnerability(context, vulnerability, frames)
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
56
|
|