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.
Files changed (105) hide show
  1. package/LICENSE-3rdparty.csv +60 -32
  2. package/ext/exporters.d.ts +1 -0
  3. package/ext/exporters.js +1 -0
  4. package/index.d.ts +243 -7
  5. package/package.json +9 -6
  6. package/packages/datadog-instrumentations/src/ai.js +54 -90
  7. package/packages/datadog-instrumentations/src/cucumber.js +14 -0
  8. package/packages/datadog-instrumentations/src/helpers/hook.js +17 -11
  9. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +55 -14
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +15 -13
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +103 -0
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.js +108 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -1
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/transformer.js +21 -0
  16. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +138 -12
  17. package/packages/datadog-instrumentations/src/http/client.js +119 -1
  18. package/packages/datadog-instrumentations/src/jest.js +179 -15
  19. package/packages/datadog-instrumentations/src/kafkajs.js +20 -17
  20. package/packages/datadog-instrumentations/src/mocha/utils.js +6 -0
  21. package/packages/datadog-instrumentations/src/mysql2.js +131 -64
  22. package/packages/datadog-instrumentations/src/playwright.js +9 -1
  23. package/packages/datadog-instrumentations/src/stripe.js +92 -0
  24. package/packages/datadog-instrumentations/src/vitest.js +11 -0
  25. package/packages/datadog-plugin-amqplib/src/consumer.js +14 -10
  26. package/packages/datadog-plugin-amqplib/src/producer.js +23 -19
  27. package/packages/datadog-plugin-azure-functions/src/index.js +53 -37
  28. package/packages/datadog-plugin-bullmq/src/consumer.js +33 -11
  29. package/packages/datadog-plugin-bullmq/src/producer.js +60 -31
  30. package/packages/datadog-plugin-cucumber/src/index.js +9 -6
  31. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +33 -0
  32. package/packages/datadog-plugin-cypress/src/support.js +48 -8
  33. package/packages/datadog-plugin-jest/src/index.js +12 -2
  34. package/packages/datadog-plugin-jest/src/util.js +2 -1
  35. package/packages/datadog-plugin-kafkajs/src/consumer.js +22 -12
  36. package/packages/datadog-plugin-kafkajs/src/producer.js +33 -22
  37. package/packages/datadog-plugin-mocha/src/index.js +9 -6
  38. package/packages/datadog-plugin-playwright/src/index.js +10 -6
  39. package/packages/datadog-plugin-vitest/src/index.js +13 -8
  40. package/packages/dd-trace/src/appsec/addresses.js +11 -0
  41. package/packages/dd-trace/src/appsec/channels.js +5 -1
  42. package/packages/dd-trace/src/appsec/downstream_requests.js +302 -0
  43. package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +1 -1
  44. package/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js +1 -1
  45. package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +1 -1
  46. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +4 -5
  47. package/packages/dd-trace/src/appsec/iast/path-line.js +36 -25
  48. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +1 -1
  49. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
  50. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +3 -2
  51. package/packages/dd-trace/src/appsec/index.js +103 -0
  52. package/packages/dd-trace/src/appsec/rasp/ssrf.js +66 -4
  53. package/packages/dd-trace/src/azure_metadata.js +0 -2
  54. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +14 -1
  55. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  56. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +2 -0
  57. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -1
  58. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -1
  59. package/packages/dd-trace/src/ci-visibility/requests/request.js +236 -0
  60. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  61. package/packages/dd-trace/src/config/defaults.js +148 -195
  62. package/packages/dd-trace/src/config/helper.js +43 -1
  63. package/packages/dd-trace/src/config/index.js +42 -14
  64. package/packages/dd-trace/src/config/supported-configurations.json +4115 -510
  65. package/packages/dd-trace/src/constants.js +0 -2
  66. package/packages/dd-trace/src/crashtracking/crashtracker.js +10 -3
  67. package/packages/dd-trace/src/datastreams/pathway.js +22 -3
  68. package/packages/dd-trace/src/datastreams/processor.js +14 -1
  69. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +47 -2
  70. package/packages/dd-trace/src/debugger/devtools_client/index.js +75 -23
  71. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +23 -1
  72. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +3 -3
  73. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +168 -36
  74. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +18 -0
  75. package/packages/dd-trace/src/encode/agentless-json.js +141 -0
  76. package/packages/dd-trace/src/exporter.js +2 -0
  77. package/packages/dd-trace/src/exporters/agent/writer.js +22 -8
  78. package/packages/dd-trace/src/exporters/agentless/index.js +89 -0
  79. package/packages/dd-trace/src/exporters/agentless/writer.js +184 -0
  80. package/packages/dd-trace/src/exporters/common/agents.js +1 -1
  81. package/packages/dd-trace/src/exporters/common/request.js +4 -4
  82. package/packages/dd-trace/src/llmobs/constants/writers.js +1 -1
  83. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -3
  84. package/packages/dd-trace/src/llmobs/sdk.js +34 -5
  85. package/packages/dd-trace/src/opentelemetry/context_manager.js +19 -46
  86. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +3 -4
  87. package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -5
  88. package/packages/dd-trace/src/opentracing/span.js +6 -4
  89. package/packages/dd-trace/src/plugins/ci_plugin.js +57 -5
  90. package/packages/dd-trace/src/plugins/database.js +57 -45
  91. package/packages/dd-trace/src/plugins/outbound.js +27 -2
  92. package/packages/dd-trace/src/plugins/tracing.js +39 -4
  93. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +7 -0
  94. package/packages/dd-trace/src/plugins/util/test.js +48 -0
  95. package/packages/dd-trace/src/plugins/util/web.js +8 -7
  96. package/packages/dd-trace/src/profiling/exporter_cli.js +1 -0
  97. package/packages/dd-trace/src/propagation-hash/index.js +145 -0
  98. package/packages/dd-trace/src/proxy.js +4 -0
  99. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
  100. package/packages/dd-trace/src/startup-log.js +3 -3
  101. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json +0 -106
  102. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +0 -741
  103. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +0 -11
  104. package/packages/dd-trace/src/plugins/util/serverless.js +0 -8
  105. 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
- return {
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
- return {
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
- if (message !== null && typeof message === 'object') {
93
- // message headers are not supported for kafka broker versions <0.11
94
- if (!disableHeaderInjection) {
95
- message.headers ??= {}
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 = getTestSuiteCommonTags(
97
- this.command,
98
- this.frameworkVersion,
99
- testSuite,
100
- 'mocha'
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 = getTestSuiteCommonTags(
124
- this.command,
125
- this.frameworkVersion,
126
- testSuite,
127
- 'playwright'
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
- const testSuiteMetadata = getTestSuiteCommonTags(
305
- this.command,
306
- this.frameworkVersion,
307
- testSuite,
308
- 'vitest'
309
- )
310
- testSuiteMetadata[TEST_SOURCE_FILE] = testSuite
311
- testSuiteMetadata[TEST_SOURCE_START] = 1
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
+ }
@@ -39,7 +39,7 @@ class CookieAnalyzer extends Analyzer {
39
39
  }
40
40
 
41
41
  _checkOCE (context, value) {
42
- if (value && value.location) {
42
+ if (value?.location) {
43
43
  return true
44
44
  }
45
45
  return super._checkOCE(context, value)
@@ -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 && args.options.host) {
15
+ } else if (args.options?.host) {
16
16
  this.analyze(args.options.host)
17
17
  }
18
18
  })
@@ -36,7 +36,7 @@ class UnvalidatedRedirectAnalyzer extends InjectionAnalyzer {
36
36
  }
37
37
 
38
38
  isLocationHeader (name) {
39
- return name && name.trim().toLowerCase() === 'location'
39
+ return name?.trim().toLowerCase() === 'location'
40
40
  }
41
41
 
42
42
  _isVulnerable (value, iastContext) {
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { storage } = require('../../../../../datadog-core')
4
- const { getNonDDCallSiteFrames } = require('../path-line')
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 nonDDCallSiteFrames = getNonDDCallSiteFrames(callSiteFrames, this._getExcludedPaths())
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, nonDDCallSiteFrames)
53
+ addVulnerability(context, vulnerability, frames)
55
54
  }
56
55
  }
57
56