dd-trace 5.107.0 → 5.109.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 (66) hide show
  1. package/index.d.ts +22 -1
  2. package/package.json +6 -5
  3. package/packages/datadog-instrumentations/src/ai.js +43 -48
  4. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js-context-methods.js +18 -0
  5. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +111 -0
  6. package/packages/datadog-instrumentations/src/aws-sdk.js +3 -1
  7. package/packages/datadog-instrumentations/src/electron.js +1 -1
  8. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/aws-durable-execution-sdk-js.js +31 -0
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  11. package/packages/datadog-instrumentations/src/http/client.js +12 -2
  12. package/packages/datadog-instrumentations/src/ioredis.js +0 -1
  13. package/packages/datadog-instrumentations/src/iovalkey.js +1 -2
  14. package/packages/datadog-instrumentations/src/next.js +34 -0
  15. package/packages/datadog-instrumentations/src/openai.js +77 -18
  16. package/packages/datadog-instrumentations/src/redis.js +0 -1
  17. package/packages/datadog-instrumentations/src/vitest.js +60 -1
  18. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/checkpoint.js +31 -0
  19. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +55 -0
  20. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +114 -0
  21. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +128 -0
  22. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/index.js +19 -0
  23. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +224 -0
  24. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +43 -0
  25. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -7
  26. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +100 -37
  27. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +44 -27
  28. package/packages/datadog-plugin-bullmq/src/filter.js +35 -0
  29. package/packages/datadog-plugin-bullmq/src/producer.js +84 -4
  30. package/packages/datadog-plugin-fs/src/index.js +1 -0
  31. package/packages/datadog-plugin-redis/src/index.js +1 -2
  32. package/packages/datadog-plugin-vitest/src/index.js +4 -1
  33. package/packages/dd-trace/src/aiguard/channels.js +0 -1
  34. package/packages/dd-trace/src/aiguard/index.js +11 -49
  35. package/packages/dd-trace/src/aiguard/integrations/evaluate.js +46 -0
  36. package/packages/dd-trace/src/aiguard/integrations/openai.js +66 -0
  37. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +78 -0
  38. package/packages/{datadog-instrumentations/src/helpers/ai-messages.js → dd-trace/src/aiguard/messages/openai.js} +85 -193
  39. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +185 -0
  40. package/packages/dd-trace/src/appsec/channels.js +1 -0
  41. package/packages/dd-trace/src/appsec/downstream_requests.js +114 -60
  42. package/packages/dd-trace/src/appsec/iast/index.js +3 -2
  43. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +54 -12
  44. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +5 -1
  45. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +29 -4
  46. package/packages/dd-trace/src/appsec/rasp/ssrf.js +21 -12
  47. package/packages/dd-trace/src/appsec/reporter.js +1 -1
  48. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
  49. package/packages/dd-trace/src/config/supported-configurations.json +31 -2
  50. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -0
  51. package/packages/dd-trace/src/dogstatsd.js +15 -8
  52. package/packages/dd-trace/src/exporters/agentless/index.js +7 -5
  53. package/packages/dd-trace/src/exporters/agentless/intake.js +43 -0
  54. package/packages/dd-trace/src/exporters/agentless/writer.js +5 -4
  55. package/packages/dd-trace/src/openfeature/flagging_provider.js +8 -1
  56. package/packages/dd-trace/src/plugins/ci_plugin.js +27 -2
  57. package/packages/dd-trace/src/plugins/index.js +3 -0
  58. package/packages/dd-trace/src/profiling/config.js +2 -0
  59. package/packages/dd-trace/src/profiling/profilers/events.js +26 -4
  60. package/packages/dd-trace/src/profiling/profilers/space.js +3 -1
  61. package/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +12 -0
  62. package/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +12 -0
  63. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  64. package/vendor/dist/protobufjs/index.js +1 -1
  65. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  66. package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +0 -284
@@ -2,6 +2,7 @@
2
2
 
3
3
  const web = require('../plugins/util/web')
4
4
  const log = require('../log')
5
+ const { isEmpty } = require('../util')
5
6
  const {
6
7
  HTTP_OUTGOING_METHOD,
7
8
  HTTP_OUTGOING_HEADERS,
@@ -13,19 +14,32 @@ const {
13
14
  const KNUTH_FACTOR = 11400714819323199488n // eslint-disable-line unicorn/numeric-separators-style
14
15
  const UINT64_MAX = (1n << 64n) - 1n
15
16
 
17
+ const SUPPORTED_RESPONSE_BODY_MIME_TYPES = new Set([
18
+ 'application/json',
19
+ 'text/json',
20
+ 'application/x-www-form-urlencoded',
21
+ ])
22
+
23
+ const RESPONSE_BODY_IGNORED_TAG_CONTENT_TYPE =
24
+ '_dd.appsec.downstream_request.response_body_ignored.content_type_invalid'
25
+ const RESPONSE_BODY_IGNORED_TAG_CONTENT_LENGTH_MISSING =
26
+ '_dd.appsec.downstream_request.response_body_ignored.content_length_missing'
27
+ const RESPONSE_BODY_IGNORED_TAG_CONTENT_LENGTH_TOO_BIG =
28
+ '_dd.appsec.downstream_request.response_body_ignored.content_length_too_big'
29
+
16
30
  let config
17
31
  let samplingRate
18
32
  let globalRequestCounter
19
33
  let bodyAnalysisCount
20
34
  let downstreamAnalysisCount
21
- let redirectBodyCollectionDecisions
35
+ let responseBodyIgnoredCount
22
36
 
23
37
  function enable (_config) {
24
38
  config = _config
25
39
  globalRequestCounter = 0n
26
40
  bodyAnalysisCount = new WeakMap()
27
41
  downstreamAnalysisCount = new WeakMap()
28
- redirectBodyCollectionDecisions = new WeakMap()
42
+ responseBodyIgnoredCount = new WeakMap()
29
43
 
30
44
  const bodyAnalysisSampleRate = config.appsec.apiSecurity?.downstreamBodyAnalysisSampleRate
31
45
  samplingRate = Math.min(Math.max(bodyAnalysisSampleRate, 0), 1)
@@ -42,49 +56,84 @@ function disable () {
42
56
  globalRequestCounter = null
43
57
  bodyAnalysisCount = null
44
58
  downstreamAnalysisCount = null
45
- redirectBodyCollectionDecisions = null
59
+ responseBodyIgnoredCount = null
46
60
  }
47
61
 
48
62
  /**
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
63
+ * @param {string|string[]|undefined} contentLength raw content-length header value.
64
+ * @returns {number|null} parsed content length or null when invalid.
53
65
  */
54
- function consumeRedirectBodyCollectionDecision (req, outgoingUrl) {
55
- const decisions = redirectBodyCollectionDecisions.get(req)
56
- if (!decisions) return false
66
+ function parseContentLengthHeader (contentLength) {
67
+ if (contentLength == null) {
68
+ return null
69
+ }
70
+
71
+ const value = Array.isArray(contentLength) ? contentLength[0] : contentLength
72
+ const parsed = Number.parseInt(String(value), 10)
57
73
 
58
- return decisions.delete(outgoingUrl)
74
+ if (!Number.isFinite(parsed) || parsed < 0) {
75
+ return null
76
+ }
77
+
78
+ return parsed
59
79
  }
60
80
 
61
81
  /**
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.
82
+ * Increments a response-body-ignored counter on the service-entry span.
83
+ * @param {import('http').IncomingMessage} req originating request.
84
+ * @param {string} tag full `_dd.appsec.downstream_request.response_body_ignored.*` span tag.
65
85
  */
66
- function storeRedirectBodyCollectionDecision (req, redirectUrl) {
67
- let decisions = redirectBodyCollectionDecisions.get(req)
86
+ function recordResponseBodyIgnored (req, tag) {
87
+ const span = web.root(req)
88
+ if (!span) return
68
89
 
69
- if (!decisions) {
70
- decisions = new Set()
71
- redirectBodyCollectionDecisions.set(req, decisions)
90
+ let counts = responseBodyIgnoredCount.get(req)
91
+ if (!counts) {
92
+ counts = {}
93
+ responseBodyIgnoredCount.set(req, counts)
72
94
  }
73
95
 
74
- decisions.add(redirectUrl)
96
+ const current = counts[tag] || 0
97
+ const next = current + 1
98
+ counts[tag] = next
99
+ span.setTag(tag, next)
75
100
  }
76
101
 
77
102
  /**
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.
103
+ * @param {import('http').IncomingMessage} originatingReq inbound request (for metrics).
104
+ * @param {import('http').IncomingMessage} res downstream response.
105
+ * @returns {boolean} whether downstream response body should be collected for AppSec.
82
106
  */
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
107
+ function evaluateResponseBodyCollection (originatingReq, res) {
108
+ const maxBytes = config.appsec.apiSecurity.maxDownstreamBodyBytes
109
+
110
+ const mime = extractMimeType(res.headers?.['content-type'])
111
+ if (!mime || !SUPPORTED_RESPONSE_BODY_MIME_TYPES.has(mime)) {
112
+ recordResponseBodyIgnored(originatingReq, RESPONSE_BODY_IGNORED_TAG_CONTENT_TYPE)
113
+ return false
114
+ }
115
+
116
+ const declaredContentLength = parseContentLengthHeader(res.headers?.['content-length'])
117
+ if (declaredContentLength == null || declaredContentLength === 0) {
118
+ recordResponseBodyIgnored(originatingReq, RESPONSE_BODY_IGNORED_TAG_CONTENT_LENGTH_MISSING)
119
+ return false
120
+ }
121
+
122
+ if (declaredContentLength > maxBytes) {
123
+ recordResponseBodyIgnored(originatingReq, RESPONSE_BODY_IGNORED_TAG_CONTENT_LENGTH_TOO_BIG)
124
+ return false
125
+ }
126
+
127
+ return true
128
+ }
87
129
 
130
+ /**
131
+ * Probabilistic gate for downstream response body capture (rate + per-request cap).
132
+ * Only used from {@link planResponseBodyCollection}; does not increment {@link bodyAnalysisCount}.
133
+ * @param {import('http').IncomingMessage} req originating server request.
134
+ * @returns {boolean}
135
+ */
136
+ function shouldSampleBody (req) {
88
137
  globalRequestCounter = (globalRequestCounter + 1n) & UINT64_MAX
89
138
 
90
139
  const currentCount = bodyAnalysisCount.get(req) || 0
@@ -96,14 +145,43 @@ function shouldSampleBody (req, outgoingUrl) {
96
145
  // Replace 1000n with the accuraccy that we want to maintain
97
146
  const threshold = (UINT64_MAX * BigInt(Math.round(samplingRate * 1000))) / 1000n
98
147
 
99
- const shouldCollectBody = hashed <= threshold
148
+ return hashed <= threshold
149
+ }
150
+
151
+ /**
152
+ * @param {import('http').IncomingMessage} res downstream HTTP response.
153
+ * @returns {boolean}
154
+ */
155
+ function isRedirectResponse (res) {
156
+ const location = res.headers?.location || ''
157
+ return res.statusCode >= 300 && res.statusCode < 400 && !!location
158
+ }
159
+
160
+ /**
161
+ * Plans downstream response body capture on the instrumentation ctx when response headers arrive.
162
+ * Redirect responses (3xx + Location) are ignored; each outbound hop is evaluated independently
163
+ * when its own non-redirect response arrives.
164
+ * @param {import('http').IncomingMessage} originatingReq incoming server request.
165
+ * @param {import('http').IncomingMessage} res downstream response.
166
+ * @param {object} ctx http client instrumentation context (mutated).
167
+ */
168
+ function planResponseBodyCollection (originatingReq, res, ctx) {
169
+ if (!config?.appsec.apiSecurity) {
170
+ return
171
+ }
172
+
173
+ if (isRedirectResponse(res)) {
174
+ return
175
+ }
100
176
 
101
- // Track body analysis count if we're sampling the response body
102
- if (shouldCollectBody) {
103
- incrementBodyAnalysisCount(req)
177
+ if (!shouldSampleBody(originatingReq)) {
178
+ return
104
179
  }
105
180
 
106
- return shouldCollectBody
181
+ if (evaluateResponseBodyCollection(originatingReq, res)) {
182
+ ctx.shouldCollectBody = true
183
+ incrementBodyAnalysisCount(originatingReq)
184
+ }
107
185
  }
108
186
 
109
187
  /**
@@ -137,32 +215,13 @@ function extractRequestData (ctx) {
137
215
  addresses[HTTP_OUTGOING_METHOD] = getMethod(options.method)
138
216
 
139
217
  const headers = options?.headers
140
- if (headers && Object.keys(headers).length > 0) {
218
+ if (headers && !isEmpty(headers)) {
141
219
  addresses[HTTP_OUTGOING_HEADERS] = lowercaseHeaderKeys(headers)
142
220
  }
143
221
 
144
222
  return addresses
145
223
  }
146
224
 
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
225
  /**
167
226
  * Extracts response data for WAF analysis.
168
227
  * @param {import('http').IncomingMessage} res downstream response object.
@@ -177,7 +236,7 @@ function extractResponseData (res, responseBody) {
177
236
  }
178
237
 
179
238
  const headers = res.headers
180
- if (headers && Object.keys(headers).length > 0) {
239
+ if (headers && !isEmpty(headers)) {
181
240
  addresses[HTTP_OUTGOING_RESPONSE_HEADERS] = headers
182
241
  }
183
242
 
@@ -290,13 +349,8 @@ function extractMimeType (contentType) {
290
349
  module.exports = {
291
350
  enable,
292
351
  disable,
293
- shouldSampleBody,
294
- handleRedirectResponse,
352
+ planResponseBodyCollection,
295
353
  incrementDownstreamAnalysisCount,
296
354
  extractRequestData,
297
355
  extractResponseData,
298
- // exports for tests
299
- parseBody,
300
- getMethod,
301
- storeRedirectBodyCollectionDecision,
302
356
  }
@@ -3,6 +3,7 @@
3
3
  const dc = require('dc-polyfill')
4
4
  const web = require('../../plugins/util/web')
5
5
  const { storage } = require('../../../../datadog-core')
6
+ const { isEmpty } = require('../../util')
6
7
  const { enable: enableFsPlugin, disable: disableFsPlugin, IAST_MODULE } = require('../rasp/fs-plugin')
7
8
  const { incomingHttpRequestStart, incomingHttpRequestEnd, responseWriteHead } = require('../channels')
8
9
  const vulnerabilityReporter = require('./vulnerability-reporter')
@@ -96,7 +97,7 @@ function onIncomingHttpRequestEnd (data) {
96
97
 
97
98
  iastResponseEnd.publish({ ...data, storedHeaders })
98
99
 
99
- if (Object.keys(storedHeaders).length) {
100
+ if (!isEmpty(storedHeaders)) {
100
101
  collectedResponseHeaders.delete(data.res)
101
102
  }
102
103
 
@@ -118,7 +119,7 @@ function onIncomingHttpRequestEnd (data) {
118
119
  function onResponseWriteHeadCollect ({ res, responseHeaders = {} }) {
119
120
  if (!res) return
120
121
 
121
- if (Object.keys(responseHeaders).length) {
122
+ if (!isEmpty(responseHeaders)) {
122
123
  collectedResponseHeaders.set(res, responseHeaders)
123
124
  }
124
125
  }
@@ -2,24 +2,66 @@
2
2
 
3
3
  const log = require('../../../../../log')
4
4
 
5
- const LDAP_PATTERN = String.raw`\(.*?(?:~=|=|<=|>=)(?<LITERAL>[^)]+)\)`
6
- const pattern = new RegExp(LDAP_PATTERN, 'gmi')
5
+ const OPEN_PAREN = 0x28
6
+ const CLOSE_PAREN = 0x29
7
+ const EQUALS = 0x3D
8
+ const TILDE = 0x7E
9
+ const LESS = 0x3C
10
+ const GREATER = 0x3E
7
11
 
12
+ // Linear scanner for LDAP assertion-filter values. For each parenthesised group
13
+ // "(attr <op> value)" where <op> is "=", "~=", "<=", or ">=" and no nested
14
+ // parenthesis appears before the operator, report the value range [opEnd, ')').
15
+ // The cursor only ever moves forward, so total work is O(input length).
8
16
  module.exports = function extractSensitiveRanges (evidence) {
9
17
  try {
10
- pattern.lastIndex = 0
18
+ const value = evidence?.value
11
19
  const tokens = []
20
+ if (typeof value !== 'string') return tokens
12
21
 
13
- let regexResult = pattern.exec(evidence.value)
14
- while (regexResult != null) {
15
- if (!regexResult.groups.LITERAL) continue
16
- // Computing indices manually since Node.js 12 does not support d flag on regular expressions
17
- // TODO Get indices from group by adding d flag in regular expression
18
- const start = regexResult.index + (regexResult[0].length - regexResult.groups.LITERAL.length - 1)
19
- const end = start + regexResult.groups.LITERAL.length
20
- tokens.push({ start, end })
21
- regexResult = pattern.exec(evidence.value)
22
+ const length = value.length
23
+ let cursor = 0
24
+
25
+ while (cursor < length) {
26
+ const open = value.indexOf('(', cursor)
27
+ if (open === -1) break
28
+
29
+ let scan = open + 1
30
+ let opStart = -1
31
+ let opLen = 0
32
+
33
+ while (scan < length) {
34
+ const code = value.charCodeAt(scan)
35
+ if (code === OPEN_PAREN || code === CLOSE_PAREN) break
36
+ if (code === EQUALS) {
37
+ opStart = scan
38
+ opLen = 1
39
+ break
40
+ }
41
+ if ((code === TILDE || code === LESS || code === GREATER) &&
42
+ scan + 1 < length && value.charCodeAt(scan + 1) === EQUALS) {
43
+ opStart = scan
44
+ opLen = 2
45
+ break
46
+ }
47
+ scan++
48
+ }
49
+
50
+ if (opStart === -1) {
51
+ cursor = open + 1
52
+ continue
53
+ }
54
+
55
+ const close = value.indexOf(')', opStart + opLen)
56
+ if (close === -1) break
57
+
58
+ const start = opStart + opLen
59
+ if (start < close) {
60
+ tokens.push({ start, end: close })
61
+ }
62
+ cursor = close + 1
22
63
  }
64
+
23
65
  return tokens
24
66
  } catch (e) {
25
67
  log.debug('[ASM] Error extracting sensitive ranges', e)
@@ -3,7 +3,11 @@
3
3
  const log = require('../../../../../log')
4
4
 
5
5
  const AUTHORITY = '^(?:[^:]+:)?//([^@]+)@'
6
- const QUERY_FRAGMENT = '[?#&]([^=&;]+)=([^?#&]+)'
6
+ // The key class excludes `?` and `#` so the greedy quantifier is bounded per fragment.
7
+ // Query keys cannot legitimately contain those characters (they delimit query/fragment
8
+ // boundaries), so excluding them preserves match semantics for valid URLs while keeping
9
+ // the regex linear on arbitrary input.
10
+ const QUERY_FRAGMENT = '[?#&]([^=&;?#]+)=([^?#&]+)'
7
11
  const pattern = new RegExp([AUTHORITY, QUERY_FRAGMENT].join('|'), 'gmi')
8
12
 
9
13
  module.exports = function extractSensitiveRanges (evidence) {
@@ -17,6 +17,11 @@ const urlSensitiveAnalyzer = require('./sensitive-analyzers/url-sensitive-analyz
17
17
 
18
18
  const REDACTED_SOURCE_BUFFER = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
19
19
 
20
+ // Upper bound on the evidence-string length the redaction analyzers will scan. Oversized
21
+ // values bypass the analyzer entirely and are emitted as a fully-redacted placeholder.
22
+ // Counted in JS string characters (UTF-16 code units), not bytes.
23
+ const MAX_EVIDENCE_LENGTH = 32_768
24
+
20
25
  class SensitiveHandler {
21
26
  constructor () {
22
27
  this._namePattern = new RegExp(/** @type {string} */ (defaults['iast.redactionNamePattern']), 'gmi')
@@ -53,11 +58,31 @@ class SensitiveHandler {
53
58
 
54
59
  scrubEvidence (vulnerabilityType, evidence, sourcesIndexes, sources) {
55
60
  const sensitiveAnalyzer = this._sensitiveAnalyzers.get(vulnerabilityType)
56
- if (sensitiveAnalyzer) {
57
- const sensitiveRanges = sensitiveAnalyzer(evidence)
58
- if (evidence.ranges || sensitiveRanges?.length) {
59
- return this.toRedactedJson(evidence, sensitiveRanges, sourcesIndexes, sources)
61
+ if (!sensitiveAnalyzer) {
62
+ return null
63
+ }
64
+
65
+ // Oversized evidence: skip the analyzer and emit a fully-redacted placeholder. Mark
66
+ // every source backing this vulnerability as redacted so the formatter strips their
67
+ // raw `value` before they leave the process.
68
+ if (typeof evidence.value === 'string' && evidence.value.length > MAX_EVIDENCE_LENGTH) {
69
+ const redactedSources = []
70
+ for (const sourceIndex of sourcesIndexes) {
71
+ const source = sources[sourceIndex]
72
+ if (source && !source.redacted) {
73
+ source.pattern = ''.padEnd(source.value.length, REDACTED_SOURCE_BUFFER)
74
+ source.redacted = true
75
+ }
76
+ if (!redactedSources.includes(sourceIndex)) {
77
+ redactedSources.push(sourceIndex)
78
+ }
60
79
  }
80
+ return { redactedValueParts: [{ redacted: true }], redactedSources }
81
+ }
82
+
83
+ const sensitiveRanges = sensitiveAnalyzer(evidence)
84
+ if (evidence.ranges || sensitiveRanges?.length) {
85
+ return this.toRedactedJson(evidence, sensitiveRanges, sourcesIndexes, sources)
61
86
  }
62
87
  return null
63
88
  }
@@ -3,12 +3,14 @@
3
3
  const { format } = require('url')
4
4
  const {
5
5
  httpClientRequestStart,
6
+ httpClientResponseStart,
6
7
  httpClientResponseFinish,
7
8
  } = require('../channels')
8
9
  const addresses = require('../addresses')
9
10
  const web = require('../../plugins/util/web')
10
11
  const { getActiveRequest } = require('../store')
11
12
  const waf = require('../waf')
13
+ const { isEmpty } = require('../../util')
12
14
  const downstream = require('../downstream_requests')
13
15
  const { updateRaspRuleMatchMetricTags } = require('../telemetry')
14
16
  const { RULE_TYPES, handleResult } = require('./utils')
@@ -20,6 +22,7 @@ function enable (_config) {
20
22
  downstream.enable(_config)
21
23
 
22
24
  httpClientRequestStart.subscribe(analyzeSsrf)
25
+ httpClientResponseStart.subscribe(planResponseBodyCollection)
23
26
  httpClientResponseFinish.subscribe(handleResponseFinish)
24
27
  }
25
28
 
@@ -27,6 +30,7 @@ function disable () {
27
30
  downstream.disable()
28
31
 
29
32
  if (httpClientRequestStart.hasSubscribers) httpClientRequestStart.unsubscribe(analyzeSsrf)
33
+ if (httpClientResponseStart.hasSubscribers) httpClientResponseStart.unsubscribe(planResponseBodyCollection)
30
34
  if (httpClientResponseFinish.hasSubscribers) httpClientResponseFinish.unsubscribe(handleResponseFinish)
31
35
  }
32
36
 
@@ -36,9 +40,6 @@ function analyzeSsrf (ctx) {
36
40
 
37
41
  if (!req || !outgoingUrl) return
38
42
 
39
- // Determine if we should collect the response body based on sampling rate and redirect URL
40
- ctx.shouldCollectBody = downstream.shouldSampleBody(req, outgoingUrl)
41
-
42
43
  const requestAddresses = downstream.extractRequestData(ctx)
43
44
 
44
45
  const ephemeral = {
@@ -55,13 +56,23 @@ function analyzeSsrf (ctx) {
55
56
  downstream.incrementDownstreamAnalysisCount(req)
56
57
  }
57
58
 
59
+ /**
60
+ * Channel handler: plans downstream response body capture once response headers are available.
61
+ * @param {{ ctx: object, res: import('http').IncomingMessage }} payload channel payload.
62
+ */
63
+ function planResponseBodyCollection ({ ctx, res }) {
64
+ const originatingRequest = getActiveRequest()
65
+ if (!originatingRequest || !res) return
66
+
67
+ downstream.planResponseBodyCollection(originatingRequest, res, ctx)
68
+ }
69
+
58
70
  /**
59
71
  * Finalizes body collection for the response and triggers RASP analysis.
60
- * @param {{
61
- * ctx: object,
62
- * res: import('http').IncomingMessage,
63
- * body: string|Buffer|null
64
- * }} payload event payload from the channel.
72
+ * @param {object} params event payload from the channel.
73
+ * @param {object} params.ctx instrumentation context.
74
+ * @param {import('http').IncomingMessage} params.res downstream response.
75
+ * @param {string|Buffer|null} params.body collected body.
65
76
  */
66
77
  function handleResponseFinish ({ ctx, res, body }) {
67
78
  // downstream response object
@@ -70,9 +81,7 @@ function handleResponseFinish ({ ctx, res, body }) {
70
81
  const originatingRequest = getActiveRequest()
71
82
  if (!originatingRequest) return
72
83
 
73
- // Skip body analysis for redirect responses
74
- const evaluateBody = ctx.shouldCollectBody && !downstream.handleRedirectResponse(originatingRequest, res)
75
- const responseBody = evaluateBody ? body : null
84
+ const responseBody = ctx.shouldCollectBody ? body : null
76
85
  runResponseEvaluation(res, originatingRequest, responseBody)
77
86
  }
78
87
 
@@ -85,7 +94,7 @@ function handleResponseFinish ({ ctx, res, body }) {
85
94
  function runResponseEvaluation (res, req, responseBody) {
86
95
  const responseAddresses = downstream.extractResponseData(res, responseBody)
87
96
 
88
- if (!Object.keys(responseAddresses).length) return
97
+ if (isEmpty(responseAddresses)) return
89
98
 
90
99
  const raspRule = { type: RULE_TYPES.SSRF, variant: 'response' }
91
100
  const result = waf.run({ ephemeral: responseAddresses }, req, raspRule)
@@ -461,7 +461,7 @@ function truncateRequestBody (target, depth = 0) {
461
461
  }
462
462
 
463
463
  function reportRequestBody (rootSpan, requestBody, comesFromRaspAction = false) {
464
- if (!requestBody || Object.keys(requestBody).length === 0) return
464
+ if (!requestBody || isEmpty(requestBody)) return
465
465
 
466
466
  if (!rootSpan.meta_struct) {
467
467
  rootSpan.meta_struct = {}
@@ -11,6 +11,7 @@ export interface GeneratedConfig {
11
11
  enabled: boolean;
12
12
  endpointCollectionEnabled: boolean;
13
13
  endpointCollectionMessageLimit: number;
14
+ maxDownstreamBodyBytes: number;
14
15
  maxDownstreamRequestBodyAnalysis: number;
15
16
  sampleDelay: number;
16
17
  };
@@ -87,6 +88,7 @@ export interface GeneratedConfig {
87
88
  DD_CRASHTRACKING_ENABLED: boolean;
88
89
  DD_CUSTOM_PARENT_ID: string | undefined;
89
90
  DD_CUSTOM_TRACE_ID: string | undefined;
91
+ DD_DURABLE_CROSS_INVOCATION_TRACING_ENABLED: boolean;
90
92
  DD_ENABLE_LAGE_PACKAGE_NAME: boolean;
91
93
  DD_ENABLE_NX_SERVICE_NAME: boolean;
92
94
  DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED: boolean;
@@ -130,6 +132,7 @@ export interface GeneratedConfig {
130
132
  DD_MINI_AGENT_PATH: string | undefined;
131
133
  DD_PIPELINE_EXECUTION_ID: string | undefined;
132
134
  DD_PLAYWRIGHT_WORKER: string | undefined;
135
+ DD_PROFILING_ALLOCATION_ENABLED: boolean;
133
136
  DD_PROFILING_ASYNC_CONTEXT_FRAME_ENABLED: boolean;
134
137
  DD_PROFILING_CODEHOTSPOTS_ENABLED: boolean;
135
138
  DD_PROFILING_CPU_ENABLED: boolean;
@@ -176,6 +179,7 @@ export interface GeneratedConfig {
176
179
  DD_TRACE_APOLLO_SUBGRAPH_ENABLED: boolean;
177
180
  DD_TRACE_AVSC_ENABLED: boolean;
178
181
  DD_TRACE_AWS_ADD_SPAN_POINTERS: boolean;
182
+ DD_TRACE_AWS_DURABLE_EXECUTION_SDK_JS_ENABLED: boolean;
179
183
  DD_TRACE_AWS_SDK_AWS_BATCH_PROPAGATION_ENABLED: boolean;
180
184
  DD_TRACE_AWS_SDK_AWS_ENABLED: boolean;
181
185
  DD_TRACE_AWS_SDK_BATCH_PROPAGATION_ENABLED: boolean;
@@ -167,6 +167,14 @@
167
167
  "default": "1"
168
168
  }
169
169
  ],
170
+ "DD_API_SECURITY_MAX_DOWNSTREAM_BODY_BYTES": [
171
+ {
172
+ "implementation": "A",
173
+ "type": "int",
174
+ "internalPropertyName": "appsec.apiSecurity.maxDownstreamBodyBytes",
175
+ "default": "10485760"
176
+ }
177
+ ],
170
178
  "DD_API_SECURITY_SAMPLE_DELAY": [
171
179
  {
172
180
  "implementation": "A",
@@ -1247,6 +1255,20 @@
1247
1255
  "default": "false"
1248
1256
  }
1249
1257
  ],
1258
+ "DD_TRACE_AWS_DURABLE_EXECUTION_SDK_JS_ENABLED": [
1259
+ {
1260
+ "implementation": "A",
1261
+ "type": "boolean",
1262
+ "default": "true"
1263
+ }
1264
+ ],
1265
+ "DD_DURABLE_CROSS_INVOCATION_TRACING_ENABLED": [
1266
+ {
1267
+ "implementation": "A",
1268
+ "type": "boolean",
1269
+ "default": "true"
1270
+ }
1271
+ ],
1250
1272
  "DD_TRACE_LOG_LEVEL": [
1251
1273
  {
1252
1274
  "implementation": "C",
@@ -1308,6 +1330,13 @@
1308
1330
  "default": null
1309
1331
  }
1310
1332
  ],
1333
+ "DD_PROFILING_ALLOCATION_ENABLED": [
1334
+ {
1335
+ "implementation": "A",
1336
+ "type": "boolean",
1337
+ "default": "false"
1338
+ }
1339
+ ],
1311
1340
  "DD_PROFILING_ASYNC_CONTEXT_FRAME_ENABLED": [
1312
1341
  {
1313
1342
  "implementation": "A",
@@ -2626,9 +2655,9 @@
2626
2655
  ],
2627
2656
  "DD_TRACE_FS_ENABLED": [
2628
2657
  {
2629
- "implementation": "A",
2658
+ "implementation": "B",
2630
2659
  "type": "boolean",
2631
- "default": "true"
2660
+ "default": "false"
2632
2661
  }
2633
2662
  ],
2634
2663
  "DD_TRACE_GCP_PUBSUB_PUSH_ENABLED": [
@@ -269,6 +269,8 @@ async function reEvaluateProbe (probe) {
269
269
  if (probeToLocation.has(probe.id)) {
270
270
  await removeBreakpoint(probe)
271
271
  }
272
+ // TODO: Revisit diagnostic status handling for probes that recover during re-evaluation. A probe can initially
273
+ // report ERROR because no script matched, then attach successfully here without reporting INSTALLED.
272
274
  await addBreakpoint(probe)
273
275
  }
274
276
  }
@@ -3,11 +3,14 @@
3
3
  const dgram = require('dgram')
4
4
  const isIP = require('net').isIP
5
5
 
6
+ const { storage } = require('../../datadog-core')
6
7
  const request = require('./exporters/common/request')
7
8
  const log = require('./log')
8
9
  const Histogram = require('./histogram')
9
10
  const { entityId } = require('./exporters/common/docker')
10
11
 
12
+ const legacyStorage = storage('legacy')
13
+
11
14
  const MAX_BUFFER_SIZE = 1024 // limit from the agent
12
15
 
13
16
  const TYPE_COUNTER = 'c'
@@ -98,14 +101,18 @@ class DogStatsDClient {
98
101
  }
99
102
 
100
103
  _sendUdp (queue) {
101
- if (this._family === 0) {
102
- this.#lookup(this._host, (err, address, family) => {
103
- if (err) return log.error('DogStatsDClient: Host not found', err)
104
- this._sendUdpFromQueue(queue, address, family)
105
- })
106
- } else {
107
- this._sendUdpFromQueue(queue, this._host, this._family)
108
- }
104
+ // dgram resolves the local address via the instrumented dns.lookup when it
105
+ // binds on first send; the noop store keeps that self-traffic off the trace.
106
+ legacyStorage.run({ noop: true }, () => {
107
+ if (this._family === 0) {
108
+ this.#lookup(this._host, (error, address, family) => {
109
+ if (error) return log.error('DogStatsDClient: Host not found', error)
110
+ this._sendUdpFromQueue(queue, address, family)
111
+ })
112
+ } else {
113
+ this._sendUdpFromQueue(queue, this._host, this._family)
114
+ }
115
+ })
109
116
  }
110
117
 
111
118
  _sendUdpFromQueue (queue, address, family) {