dd-trace 5.86.0 → 5.87.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 (38) hide show
  1. package/index.d.ts +18 -3
  2. package/package.json +1 -1
  3. package/packages/datadog-instrumentations/src/cucumber.js +14 -0
  4. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  5. package/packages/datadog-instrumentations/src/http/client.js +119 -1
  6. package/packages/datadog-instrumentations/src/jest.js +104 -4
  7. package/packages/datadog-instrumentations/src/mocha/utils.js +6 -0
  8. package/packages/datadog-instrumentations/src/mysql2.js +131 -64
  9. package/packages/datadog-instrumentations/src/playwright.js +8 -0
  10. package/packages/datadog-instrumentations/src/stripe.js +92 -0
  11. package/packages/datadog-instrumentations/src/vitest.js +11 -0
  12. package/packages/datadog-plugin-azure-functions/src/index.js +53 -37
  13. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +7 -0
  14. package/packages/dd-trace/src/appsec/addresses.js +11 -0
  15. package/packages/dd-trace/src/appsec/channels.js +5 -1
  16. package/packages/dd-trace/src/appsec/downstream_requests.js +302 -0
  17. package/packages/dd-trace/src/appsec/index.js +103 -0
  18. package/packages/dd-trace/src/appsec/rasp/ssrf.js +66 -4
  19. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +14 -1
  20. package/packages/dd-trace/src/config/defaults.js +2 -0
  21. package/packages/dd-trace/src/config/index.js +6 -0
  22. package/packages/dd-trace/src/config/supported-configurations.json +2 -0
  23. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +47 -2
  24. package/packages/dd-trace/src/debugger/devtools_client/index.js +75 -23
  25. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +23 -1
  26. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +3 -3
  27. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +168 -36
  28. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +18 -0
  29. package/packages/dd-trace/src/exporters/common/agents.js +1 -1
  30. package/packages/dd-trace/src/llmobs/constants/writers.js +1 -1
  31. package/packages/dd-trace/src/llmobs/sdk.js +34 -5
  32. package/packages/dd-trace/src/plugins/database.js +42 -43
  33. package/packages/dd-trace/src/plugins/outbound.js +27 -2
  34. package/packages/dd-trace/src/plugins/tracing.js +39 -4
  35. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +7 -0
  36. package/packages/dd-trace/src/plugins/util/web.js +8 -7
  37. package/packages/dd-trace/src/startup-log.js +2 -2
  38. package/packages/dd-trace/src/plugins/util/serverless.js +0 -8
@@ -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
+ }
@@ -30,6 +30,9 @@ const {
30
30
  routerParam,
31
31
  fastifyResponseChannel,
32
32
  fastifyPathParams,
33
+ stripeCheckoutSessionCreate,
34
+ stripePaymentIntentCreate,
35
+ stripeConstructEvent,
33
36
  } = require('./channels')
34
37
  const waf = require('./waf')
35
38
  const addresses = require('./addresses')
@@ -92,6 +95,9 @@ function enable (_config) {
92
95
  fastifyResponseChannel.subscribe(onResponseBody)
93
96
  responseWriteHead.subscribe(onResponseWriteHead)
94
97
  responseSetHeader.subscribe(onResponseSetHeader)
98
+ stripeCheckoutSessionCreate.subscribe(onStripeCheckoutSessionCreate)
99
+ stripePaymentIntentCreate.subscribe(onStripePaymentIntentCreate)
100
+ stripeConstructEvent.subscribe(onStripeConstructEvent)
95
101
 
96
102
  isEnabled = true
97
103
  config = _config
@@ -382,6 +388,100 @@ function onResponseSetHeader ({ res, abortController }) {
382
388
  }
383
389
  }
384
390
 
391
+ function onStripeCheckoutSessionCreate (payload) {
392
+ if (payload?.mode !== 'payment') return
393
+
394
+ waf.run({
395
+ persistent: {
396
+ [addresses.PAYMENT_CREATION]: {
397
+ integration: 'stripe',
398
+ id: payload.id,
399
+ amount_total: payload.amount_total,
400
+ client_reference_id: payload.client_reference_id,
401
+ currency: payload.currency,
402
+ 'discounts.coupon': payload.discounts?.[0]?.coupon,
403
+ 'discounts.promotion_code': payload.discounts?.[0]?.promotion_code,
404
+ livemode: payload.livemode,
405
+ 'total_details.amount_discount': payload.total_details?.amount_discount,
406
+ 'total_details.amount_shipping': payload.total_details?.amount_shipping,
407
+ },
408
+ },
409
+ })
410
+ }
411
+
412
+ function onStripePaymentIntentCreate (payload) {
413
+ if (payload === null || typeof payload !== 'object') return
414
+
415
+ waf.run({
416
+ persistent: {
417
+ [addresses.PAYMENT_CREATION]: {
418
+ integration: 'stripe',
419
+ id: payload.id,
420
+ amount: payload.amount,
421
+ currency: payload.currency,
422
+ livemode: payload.livemode,
423
+ payment_method: payload.payment_method,
424
+ },
425
+ },
426
+ })
427
+ }
428
+
429
+ function onStripeConstructEvent (payload) {
430
+ const object = payload?.data?.object
431
+ if (object === null || typeof object !== 'object') return
432
+
433
+ let persistent
434
+
435
+ switch (payload.type) {
436
+ case 'payment_intent.succeeded':
437
+ persistent = {
438
+ [addresses.PAYMENT_SUCCESS]: {
439
+ integration: 'stripe',
440
+ id: object.id,
441
+ amount: object.amount,
442
+ currency: object.currency,
443
+ livemode: object.livemode,
444
+ payment_method: object.payment_method,
445
+ },
446
+ }
447
+ break
448
+
449
+ case 'payment_intent.payment_failed':
450
+ persistent = {
451
+ [addresses.PAYMENT_FAILURE]: {
452
+ integration: 'stripe',
453
+ id: object.id,
454
+ amount: object.amount,
455
+ currency: object.currency,
456
+ 'last_payment_error.code': object.last_payment_error?.code,
457
+ 'last_payment_error.decline_code': object.last_payment_error?.decline_code,
458
+ 'last_payment_error.payment_method.id': object.last_payment_error?.payment_method?.id,
459
+ 'last_payment_error.payment_method.type': object.last_payment_error?.payment_method?.type,
460
+ livemode: object.livemode,
461
+ },
462
+ }
463
+ break
464
+
465
+ case 'payment_intent.canceled':
466
+ persistent = {
467
+ [addresses.PAYMENT_CANCELLATION]: {
468
+ integration: 'stripe',
469
+ id: object.id,
470
+ amount: object.amount,
471
+ cancellation_reason: object.cancellation_reason,
472
+ currency: object.currency,
473
+ livemode: object.livemode,
474
+ },
475
+ }
476
+ break
477
+
478
+ default:
479
+ return
480
+ }
481
+
482
+ waf.run({ persistent })
483
+ }
484
+
385
485
  function handleResults (actions, req, res, rootSpan, abortController) {
386
486
  if (!actions || !req || !res || !rootSpan || !abortController) return
387
487
 
@@ -427,6 +527,9 @@ function disable () {
427
527
  if (fastifyResponseChannel.hasSubscribers) fastifyResponseChannel.unsubscribe(onResponseBody)
428
528
  if (responseWriteHead.hasSubscribers) responseWriteHead.unsubscribe(onResponseWriteHead)
429
529
  if (responseSetHeader.hasSubscribers) responseSetHeader.unsubscribe(onResponseSetHeader)
530
+ if (stripeCheckoutSessionCreate.hasSubscribers) stripeCheckoutSessionCreate.unsubscribe(onStripeCheckoutSessionCreate)
531
+ if (stripePaymentIntentCreate.hasSubscribers) stripePaymentIntentCreate.unsubscribe(onStripePaymentIntentCreate)
532
+ if (stripeConstructEvent.hasSubscribers) stripeConstructEvent.unsubscribe(onStripeConstructEvent)
430
533
  }
431
534
 
432
535
  // this is faster than Object.keys().length === 0
@@ -1,21 +1,32 @@
1
1
  'use strict'
2
2
 
3
3
  const { format } = require('url')
4
- const { httpClientRequestStart } = require('../channels')
4
+ const {
5
+ httpClientRequestStart,
6
+ httpClientResponseFinish,
7
+ } = require('../channels')
5
8
  const { storage } = require('../../../../datadog-core')
6
9
  const addresses = require('../addresses')
7
10
  const waf = require('../waf')
11
+ const downstream = require('../downstream_requests')
12
+ const { updateRaspRuleMatchMetricTags } = require('../telemetry')
8
13
  const { RULE_TYPES, handleResult } = require('./utils')
9
14
 
10
15
  let config
11
16
 
12
17
  function enable (_config) {
13
18
  config = _config
19
+ downstream.enable(_config)
20
+
14
21
  httpClientRequestStart.subscribe(analyzeSsrf)
22
+ httpClientResponseFinish.subscribe(handleResponseFinish)
15
23
  }
16
24
 
17
25
  function disable () {
26
+ downstream.disable()
27
+
18
28
  if (httpClientRequestStart.hasSubscribers) httpClientRequestStart.unsubscribe(analyzeSsrf)
29
+ if (httpClientResponseFinish.hasSubscribers) httpClientResponseFinish.unsubscribe(handleResponseFinish)
19
30
  }
20
31
 
21
32
  function analyzeSsrf (ctx) {
@@ -25,16 +36,67 @@ function analyzeSsrf (ctx) {
25
36
 
26
37
  if (!req || !outgoingUrl) return
27
38
 
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
+ const requestAddresses = downstream.extractRequestData(ctx)
43
+
28
44
  const ephemeral = {
29
45
  [addresses.HTTP_OUTGOING_URL]: outgoingUrl,
46
+ ...requestAddresses,
30
47
  }
31
48
 
32
- const raspRule = { type: RULE_TYPES.SSRF }
49
+ const raspRule = { type: RULE_TYPES.SSRF, variant: 'request' }
33
50
 
34
51
  const result = waf.run({ ephemeral }, req, raspRule)
35
52
 
36
- const res = store?.res
37
- handleResult(result, req, res, ctx.abortController, config, raspRule)
53
+ handleResult(result, req, store?.res, ctx.abortController, config, raspRule)
54
+
55
+ downstream.incrementDownstreamAnalysisCount(req)
56
+ }
57
+
58
+ /**
59
+ * 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.
65
+ */
66
+ function handleResponseFinish ({ ctx, res, body }) {
67
+ // downstream response object
68
+ if (!res) return
69
+
70
+ const store = storage('legacy').getStore()
71
+ const originatingRequest = store?.req
72
+ if (!originatingRequest) return
73
+
74
+ // Skip body analysis for redirect responses
75
+ const evaluateBody = ctx.shouldCollectBody && !downstream.handleRedirectResponse(originatingRequest, res)
76
+ const responseBody = evaluateBody ? body : null
77
+ runResponseEvaluation(res, originatingRequest, responseBody)
78
+ }
79
+
80
+ /**
81
+ * Evaluates the downstream response and records telemetry.
82
+ * @param {import('http').IncomingMessage} res incoming response from downstream service.
83
+ * @param {import('http').IncomingMessage} req originating request.
84
+ * @param {string|Buffer|null} responseBody collected downstream response body.
85
+ */
86
+ function runResponseEvaluation (res, req, responseBody) {
87
+ const responseAddresses = downstream.extractResponseData(res, responseBody)
88
+
89
+ if (!Object.keys(responseAddresses).length) return
90
+
91
+ const raspRule = { type: RULE_TYPES.SSRF, variant: 'response' }
92
+ const result = waf.run({ ephemeral: responseAddresses }, req, raspRule)
93
+
94
+ // TODO: this should be done in the waf functions directly instead of calling it everywhere
95
+ const ruleTriggered = !!result?.events?.length
96
+
97
+ if (ruleTriggered) {
98
+ updateRaspRuleMatchMetricTags(req, raspRule, false, false)
99
+ }
38
100
  }
39
101
 
40
102
  module.exports = { enable, disable }
@@ -16,6 +16,12 @@ const session = require('../../../debugger/devtools_client/session')
16
16
  const { getGeneratedPosition } = require('../../../debugger/devtools_client/source-maps')
17
17
  // TODO: move debugger/devtools_client/snapshot to common place
18
18
  const { getLocalStateForCallFrame } = require('../../../debugger/devtools_client/snapshot')
19
+ const {
20
+ DEFAULT_MAX_REFERENCE_DEPTH,
21
+ DEFAULT_MAX_COLLECTION_SIZE,
22
+ DEFAULT_MAX_FIELD_COUNT,
23
+ DEFAULT_MAX_LENGTH,
24
+ } = require('../../../debugger/devtools_client/snapshot/constants')
19
25
  // TODO: move debugger/devtools_client/state to common place
20
26
  const {
21
27
  findScriptFromPartialPath,
@@ -29,6 +35,13 @@ let sessionStarted = false
29
35
  const breakpointIdToProbe = new Map()
30
36
  const probeIdToBreakpointId = new Map()
31
37
 
38
+ const limits = {
39
+ maxReferenceDepth: DEFAULT_MAX_REFERENCE_DEPTH,
40
+ maxCollectionSize: DEFAULT_MAX_COLLECTION_SIZE,
41
+ maxFieldCount: DEFAULT_MAX_FIELD_COUNT,
42
+ maxLength: DEFAULT_MAX_LENGTH,
43
+ }
44
+
32
45
  session.on('Debugger.paused', async ({ params: { hitBreakpoints: [hitBreakpoint], callFrames } }) => {
33
46
  const probe = breakpointIdToProbe.get(hitBreakpoint)
34
47
  if (!probe) {
@@ -38,7 +51,7 @@ session.on('Debugger.paused', async ({ params: { hitBreakpoints: [hitBreakpoint]
38
51
 
39
52
  const stack = await getStackFromCallFrames(callFrames)
40
53
 
41
- const { processLocalState } = await getLocalStateForCallFrame(callFrames[0])
54
+ const { processLocalState } = await getLocalStateForCallFrame(callFrames[0], limits)
42
55
 
43
56
  await session.post('Debugger.resume')
44
57
 
@@ -26,6 +26,8 @@ module.exports = {
26
26
  'appsec.apiSecurity.sampleDelay': 30,
27
27
  'appsec.apiSecurity.endpointCollectionEnabled': true,
28
28
  'appsec.apiSecurity.endpointCollectionMessageLimit': 300,
29
+ 'appsec.apiSecurity.downstreamBodyAnalysisSampleRate': 0.5,
30
+ 'appsec.apiSecurity.maxDownstreamRequestBodyAnalysis': 1,
29
31
  'appsec.blockedTemplateGraphql': undefined,
30
32
  'appsec.blockedTemplateHtml': undefined,
31
33
  'appsec.blockedTemplateJson': undefined,
@@ -255,6 +255,8 @@ class Config {
255
255
  DD_API_SECURITY_SAMPLE_DELAY,
256
256
  DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED,
257
257
  DD_API_SECURITY_ENDPOINT_COLLECTION_MESSAGE_LIMIT,
258
+ DD_API_SECURITY_DOWNSTREAM_BODY_ANALYSIS_SAMPLE_RATE,
259
+ DD_API_SECURITY_MAX_DOWNSTREAM_REQUEST_BODY_ANALYSIS,
258
260
  DD_APM_TRACING_ENABLED,
259
261
  DD_APP_KEY,
260
262
  DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE,
@@ -540,6 +542,10 @@ class Config {
540
542
  unprocessedTarget['appsec.stackTrace.maxStackTraces'] = DD_APPSEC_MAX_STACK_TRACES
541
543
  target['appsec.wafTimeout'] = maybeInt(DD_APPSEC_WAF_TIMEOUT)
542
544
  unprocessedTarget['appsec.wafTimeout'] = DD_APPSEC_WAF_TIMEOUT
545
+ target['appsec.apiSecurity.downstreamBodyAnalysisSampleRate'] =
546
+ maybeFloat(DD_API_SECURITY_DOWNSTREAM_BODY_ANALYSIS_SAMPLE_RATE)
547
+ target['appsec.apiSecurity.maxDownstreamRequestBodyAnalysis'] =
548
+ maybeInt(DD_API_SECURITY_MAX_DOWNSTREAM_REQUEST_BODY_ANALYSIS)
543
549
  target.baggageMaxBytes = DD_TRACE_BAGGAGE_MAX_BYTES
544
550
  target.baggageMaxItems = DD_TRACE_BAGGAGE_MAX_ITEMS
545
551
  target.baggageTagKeys = DD_TRACE_BAGGAGE_TAG_KEYS
@@ -15,6 +15,8 @@
15
15
  "DD_API_SECURITY_SAMPLE_DELAY": ["A"],
16
16
  "DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED": ["A"],
17
17
  "DD_API_SECURITY_ENDPOINT_COLLECTION_MESSAGE_LIMIT": ["A"],
18
+ "DD_API_SECURITY_DOWNSTREAM_BODY_ANALYSIS_SAMPLE_RATE": ["A"],
19
+ "DD_API_SECURITY_MAX_DOWNSTREAM_REQUEST_BODY_ANALYSIS": ["A"],
18
20
  "DD_APM_FLUSH_DEADLINE_MILLISECONDS": ["A"],
19
21
  "DD_APM_TRACING_ENABLED": ["A"],
20
22
  "DD_APP_KEY": ["A"],
@@ -3,8 +3,14 @@
3
3
  const mutex = require('../../../../../vendor/dist/mutexify/promise')()
4
4
  const { getGeneratedPosition } = require('./source-maps')
5
5
  const session = require('./session')
6
- const { compile: compileCondition, compileSegments, templateRequiresEvaluation } = require('./condition')
6
+ const { compile, compileSegments, templateRequiresEvaluation } = require('./condition')
7
7
  const { MAX_SNAPSHOTS_PER_SECOND_PER_PROBE, MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE } = require('./defaults')
8
+ const {
9
+ DEFAULT_MAX_REFERENCE_DEPTH,
10
+ DEFAULT_MAX_COLLECTION_SIZE,
11
+ DEFAULT_MAX_FIELD_COUNT,
12
+ DEFAULT_MAX_LENGTH,
13
+ } = require('./snapshot/constants')
8
14
  const {
9
15
  findScriptFromPartialPath,
10
16
  clearState,
@@ -99,7 +105,7 @@ async function addBreakpoint (probe) {
99
105
  }
100
106
 
101
107
  try {
102
- probe.condition = probe.when?.json && compileCondition(probe.when.json)
108
+ probe.condition = probe.when?.json && compile(probe.when.json)
103
109
  } catch (err) {
104
110
  throw new Error(
105
111
  `Cannot compile expression: ${probe.when.dsl} (probe: ${probe.id}, version: ${probe.version})`,
@@ -107,6 +113,45 @@ async function addBreakpoint (probe) {
107
113
  )
108
114
  }
109
115
 
116
+ if (probe.captureSnapshot) {
117
+ probe.capture = {
118
+ maxReferenceDepth: probe.capture?.maxReferenceDepth ?? DEFAULT_MAX_REFERENCE_DEPTH,
119
+ maxCollectionSize: probe.capture?.maxCollectionSize ?? DEFAULT_MAX_COLLECTION_SIZE,
120
+ maxFieldCount: probe.capture?.maxFieldCount ?? DEFAULT_MAX_FIELD_COUNT,
121
+ maxLength: probe.capture?.maxLength ?? DEFAULT_MAX_LENGTH,
122
+ }
123
+ }
124
+
125
+ if (probe.captureExpressions?.length > 0) {
126
+ probe.compiledCaptureExpressions = []
127
+ for (const captureExpr of probe.captureExpressions) {
128
+ let expression
129
+ try {
130
+ expression = compile(captureExpr.expr.json)
131
+ } catch (err) {
132
+ throw new Error(
133
+ `Cannot compile capture expression: ${captureExpr.name} (probe: ${probe.id}, version: ${probe.version})`,
134
+ { cause: err }
135
+ )
136
+ }
137
+
138
+ probe.compiledCaptureExpressions.push({
139
+ name: captureExpr.name,
140
+ expression,
141
+ limits: {
142
+ maxReferenceDepth: captureExpr.capture?.maxReferenceDepth ??
143
+ probe.capture?.maxReferenceDepth ?? DEFAULT_MAX_REFERENCE_DEPTH,
144
+ maxCollectionSize: captureExpr.capture?.maxCollectionSize ??
145
+ probe.capture?.maxCollectionSize ?? DEFAULT_MAX_COLLECTION_SIZE,
146
+ maxFieldCount: captureExpr.capture?.maxFieldCount ??
147
+ probe.capture?.maxFieldCount ?? DEFAULT_MAX_FIELD_COUNT,
148
+ maxLength: captureExpr.capture?.maxLength ??
149
+ probe.capture?.maxLength ?? DEFAULT_MAX_LENGTH,
150
+ },
151
+ })
152
+ }
153
+ }
154
+
110
155
  const locationKey = generateLocationKey(scriptId, lineNumber, columnNumber)
111
156
  const breakpoint = locationToBreakpoint.get(locationKey)
112
157