dd-trace 5.108.0 → 5.110.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 (174) hide show
  1. package/ci/init.js +4 -3
  2. package/index.d.ts +88 -25
  3. package/loader-hook.mjs +88 -2
  4. package/package.json +19 -14
  5. package/packages/datadog-esbuild/index.js +26 -0
  6. package/packages/datadog-esbuild/src/utils.js +46 -3
  7. package/packages/datadog-instrumentations/src/ai.js +43 -48
  8. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js-context-methods.js +18 -0
  9. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +111 -0
  10. package/packages/datadog-instrumentations/src/aws-sdk.js +3 -1
  11. package/packages/datadog-instrumentations/src/connect.js +4 -3
  12. package/packages/datadog-instrumentations/src/electron.js +1 -1
  13. package/packages/datadog-instrumentations/src/fastify.js +4 -12
  14. package/packages/datadog-instrumentations/src/graphql.js +17 -6
  15. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +81 -8
  16. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  17. package/packages/datadog-instrumentations/src/helpers/instrument.js +27 -2
  18. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +65 -0
  19. package/packages/datadog-instrumentations/src/helpers/register.js +6 -4
  20. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +17 -0
  21. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/aws-durable-execution-sdk-js.js +31 -0
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +18 -2
  24. package/packages/datadog-instrumentations/src/http/client.js +12 -2
  25. package/packages/datadog-instrumentations/src/ioredis.js +0 -1
  26. package/packages/datadog-instrumentations/src/iovalkey.js +1 -2
  27. package/packages/datadog-instrumentations/src/koa.js +3 -2
  28. package/packages/datadog-instrumentations/src/mariadb.js +4 -2
  29. package/packages/datadog-instrumentations/src/mocha/main.js +3 -3
  30. package/packages/datadog-instrumentations/src/next.js +44 -10
  31. package/packages/datadog-instrumentations/src/openai.js +77 -18
  32. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +7 -6
  33. package/packages/datadog-instrumentations/src/playwright.js +1 -1
  34. package/packages/datadog-instrumentations/src/redis.js +0 -1
  35. package/packages/datadog-instrumentations/src/restify.js +4 -3
  36. package/packages/datadog-instrumentations/src/router.js +18 -5
  37. package/packages/datadog-instrumentations/src/selenium.js +1 -2
  38. package/packages/datadog-instrumentations/src/tedious.js +28 -0
  39. package/packages/datadog-instrumentations/src/vitest.js +323 -7
  40. package/packages/datadog-plugin-avsc/src/schema_iterator.js +2 -2
  41. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/checkpoint.js +31 -0
  42. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +55 -0
  43. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +114 -0
  44. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +128 -0
  45. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/index.js +19 -0
  46. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +224 -0
  47. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +43 -0
  48. package/packages/datadog-plugin-aws-sdk/src/base.js +14 -16
  49. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +43 -6
  50. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +100 -37
  51. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +108 -99
  52. package/packages/datadog-plugin-bullmq/src/filter.js +35 -0
  53. package/packages/datadog-plugin-bullmq/src/producer.js +84 -4
  54. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +9 -1
  55. package/packages/datadog-plugin-fs/src/index.js +1 -0
  56. package/packages/datadog-plugin-http/src/client.js +8 -3
  57. package/packages/datadog-plugin-http2/src/client.js +5 -2
  58. package/packages/datadog-plugin-mysql/src/index.js +7 -2
  59. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +2 -2
  60. package/packages/datadog-plugin-redis/src/index.js +1 -2
  61. package/packages/datadog-plugin-undici/src/index.js +5 -2
  62. package/packages/datadog-plugin-vitest/src/index.js +11 -2
  63. package/packages/datadog-webpack/index.js +17 -1
  64. package/packages/datadog-webpack/src/optional-peer-loader.js +17 -0
  65. package/packages/dd-trace/src/aiguard/channels.js +0 -1
  66. package/packages/dd-trace/src/aiguard/index.js +11 -49
  67. package/packages/dd-trace/src/aiguard/integrations/evaluate.js +46 -0
  68. package/packages/dd-trace/src/aiguard/integrations/openai.js +66 -0
  69. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +78 -0
  70. package/packages/{datadog-instrumentations/src/helpers/ai-messages.js → dd-trace/src/aiguard/messages/openai.js} +85 -193
  71. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +185 -0
  72. package/packages/dd-trace/src/aiguard/sdk.js +13 -1
  73. package/packages/dd-trace/src/appsec/api_security/index.js +55 -0
  74. package/packages/dd-trace/src/appsec/api_security/sampler.js +147 -0
  75. package/packages/dd-trace/src/appsec/channels.js +1 -0
  76. package/packages/dd-trace/src/appsec/downstream_requests.js +111 -58
  77. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +54 -12
  78. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +5 -1
  79. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +29 -4
  80. package/packages/dd-trace/src/appsec/index.js +10 -6
  81. package/packages/dd-trace/src/appsec/rasp/ssrf.js +19 -11
  82. package/packages/dd-trace/src/appsec/reporter.js +1 -0
  83. package/packages/dd-trace/src/appsec/telemetry/api_security.js +34 -0
  84. package/packages/dd-trace/src/appsec/telemetry/index.js +26 -0
  85. package/packages/dd-trace/src/ci-visibility/requests/request.js +1 -17
  86. package/packages/dd-trace/src/config/defaults.js +22 -3
  87. package/packages/dd-trace/src/config/generated-config-types.d.ts +538 -1
  88. package/packages/dd-trace/src/config/helper.js +133 -33
  89. package/packages/dd-trace/src/config/index.js +4 -1
  90. package/packages/dd-trace/src/config/parsers.js +19 -1
  91. package/packages/dd-trace/src/config/supported-configurations.json +45 -5
  92. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +49 -35
  93. package/packages/dd-trace/src/debugger/devtools_client/index.js +31 -66
  94. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +83 -0
  95. package/packages/dd-trace/src/debugger/devtools_client/state.js +1 -0
  96. package/packages/dd-trace/src/debugger/index.js +5 -0
  97. package/packages/dd-trace/src/debugger/probe_sampler.js +112 -0
  98. package/packages/dd-trace/src/debugger/probe_sampler_constants.js +19 -0
  99. package/packages/dd-trace/src/dogstatsd.js +15 -8
  100. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +77 -26
  101. package/packages/dd-trace/src/exporters/agentless/index.js +7 -5
  102. package/packages/dd-trace/src/exporters/agentless/intake.js +43 -0
  103. package/packages/dd-trace/src/exporters/agentless/writer.js +5 -4
  104. package/packages/dd-trace/src/exporters/common/request.js +1 -20
  105. package/packages/dd-trace/src/exporters/common/url.js +34 -0
  106. package/packages/dd-trace/src/exporters/span-stats/writer.js +1 -3
  107. package/packages/dd-trace/src/guardrails/index.js +8 -5
  108. package/packages/dd-trace/src/index.js +6 -4
  109. package/packages/dd-trace/src/llmobs/constants/tags.js +6 -0
  110. package/packages/dd-trace/src/llmobs/index.js +16 -5
  111. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +3 -3
  112. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +5 -1
  113. package/packages/dd-trace/src/llmobs/sdk.js +5 -3
  114. package/packages/dd-trace/src/llmobs/span_processor.js +4 -0
  115. package/packages/dd-trace/src/llmobs/tagger.js +55 -0
  116. package/packages/dd-trace/src/llmobs/telemetry.js +1 -0
  117. package/packages/dd-trace/src/log/index.js +18 -20
  118. package/packages/dd-trace/src/openfeature/flagging_provider.js +3 -1
  119. package/packages/dd-trace/src/opentelemetry/bridge-span-base.js +5 -4
  120. package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +2 -1
  121. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +4 -3
  122. package/packages/dd-trace/src/opentelemetry/metrics/time.js +19 -0
  123. package/packages/dd-trace/src/opentelemetry/otlp/common.proto +2 -2
  124. package/packages/dd-trace/src/opentelemetry/otlp/logs.proto +1 -1
  125. package/packages/dd-trace/src/opentelemetry/otlp/logs_service.proto +1 -1
  126. package/packages/dd-trace/src/opentelemetry/otlp/metrics.proto +1 -1
  127. package/packages/dd-trace/src/opentelemetry/otlp/metrics_service.proto +1 -1
  128. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +11 -4
  129. package/packages/dd-trace/src/opentelemetry/span-helpers.js +29 -13
  130. package/packages/dd-trace/src/opentelemetry/span.js +14 -3
  131. package/packages/dd-trace/src/opentelemetry/span_context.js +1 -0
  132. package/packages/dd-trace/src/opentelemetry/trace/index.js +2 -1
  133. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +3 -2
  134. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +26 -13
  135. package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -3
  136. package/packages/dd-trace/src/opentracing/span_context.js +17 -0
  137. package/packages/dd-trace/src/plugin_manager.js +7 -1
  138. package/packages/dd-trace/src/plugins/ci_plugin.js +36 -3
  139. package/packages/dd-trace/src/plugins/index.js +3 -0
  140. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +276 -0
  141. package/packages/dd-trace/src/plugins/util/test.js +80 -0
  142. package/packages/dd-trace/src/plugins/util/url.js +68 -1
  143. package/packages/dd-trace/src/plugins/util/web.js +19 -36
  144. package/packages/dd-trace/src/priority_sampler.js +1 -13
  145. package/packages/dd-trace/src/profiling/config.js +129 -198
  146. package/packages/dd-trace/src/profiling/constants.js +4 -7
  147. package/packages/dd-trace/src/profiling/exporter_cli.js +10 -10
  148. package/packages/dd-trace/src/profiling/exporters/agent.js +23 -16
  149. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +45 -14
  150. package/packages/dd-trace/src/profiling/exporters/file.js +16 -5
  151. package/packages/dd-trace/src/profiling/oom.js +71 -0
  152. package/packages/dd-trace/src/profiling/profiler.js +35 -23
  153. package/packages/dd-trace/src/profiling/profilers/events.js +14 -7
  154. package/packages/dd-trace/src/profiling/profilers/space.js +27 -18
  155. package/packages/dd-trace/src/profiling/profilers/wall.js +35 -25
  156. package/packages/dd-trace/src/ritm.js +4 -2
  157. package/packages/dd-trace/src/serverless.js +1 -2
  158. package/packages/dd-trace/src/service-naming/schemas/util.js +36 -5
  159. package/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +12 -0
  160. package/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +12 -0
  161. package/packages/dd-trace/src/service-naming/schemas/v1/web.js +5 -3
  162. package/packages/dd-trace/src/span_format.js +2 -1
  163. package/packages/dd-trace/src/span_processor.js +6 -0
  164. package/packages/dd-trace/src/telemetry/send-data.js +12 -10
  165. package/packages/dd-trace/src/tracer.js +15 -1
  166. package/packages/dd-trace/src/util.js +14 -0
  167. package/register.js +58 -1
  168. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  169. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  170. package/vendor/dist/pprof-format/index.js +1 -1
  171. package/vendor/dist/protobufjs/index.js +1 -1
  172. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  173. package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +0 -284
  174. package/packages/dd-trace/src/appsec/api_security_sampler.js +0 -117
@@ -0,0 +1,147 @@
1
+ 'use strict'
2
+
3
+ const { TTLCache } = require('../../../../../vendor/dist/@isaacs/ttlcache')
4
+ const web = require('../../plugins/util/web')
5
+ const log = require('../../log')
6
+ const { AUTO_REJECT, USER_REJECT } = require('../../../../../ext/priority')
7
+ const { keepTrace } = require('../../priority_sampler')
8
+ const { ASM } = require('../../standalone/product')
9
+ const { isBlocked } = require('../blocking')
10
+
11
+ const MAX_SIZE = 4096
12
+
13
+ const SamplingDecision = Object.freeze({
14
+ SAMPLE: 'sample',
15
+ MISSING_ROUTE: 'missing_route',
16
+ SKIP: 'skip',
17
+ })
18
+
19
+ let enabled
20
+ let asmStandaloneEnabled
21
+
22
+ /**
23
+ * @type {TTLCache}
24
+ */
25
+ let sampledRequests
26
+
27
+ class NoopTTLCache {
28
+ clear () {}
29
+ set (_key, _value) {}
30
+ has (_key) { return false }
31
+ }
32
+
33
+ function configure ({ appsec, apmTracingEnabled }) {
34
+ enabled = appsec.apiSecurity.enabled
35
+ asmStandaloneEnabled = apmTracingEnabled === false
36
+ sampledRequests = appsec.apiSecurity.sampleDelay === 0
37
+ ? new NoopTTLCache()
38
+ : new TTLCache({ max: MAX_SIZE, ttl: appsec.apiSecurity.sampleDelay * 1000 })
39
+ }
40
+
41
+ function disable () {
42
+ enabled = false
43
+ sampledRequests?.clear()
44
+ }
45
+
46
+ /**
47
+ * @param {import('http').IncomingMessage} req
48
+ * @param {import('http').ServerResponse} res
49
+ * @param {boolean} record When true and the decision is SAMPLE, records the endpoint in the TTL cache
50
+ * @returns {'sample' | 'missing_route' | 'skip'}
51
+ */
52
+ function sampleRequest (req, res, record = false) {
53
+ if (!enabled) return SamplingDecision.SKIP
54
+
55
+ const rootSpan = web.root(req)
56
+ if (!rootSpan) return SamplingDecision.SKIP
57
+
58
+ if (!asmStandaloneEnabled) {
59
+ let priority = getSpanPriority(rootSpan)
60
+ if (!priority) {
61
+ rootSpan._prioritySampler?.sample(rootSpan)
62
+ priority = getSpanPriority(rootSpan)
63
+ }
64
+
65
+ if (priority === AUTO_REJECT || priority === USER_REJECT) {
66
+ return SamplingDecision.SKIP
67
+ }
68
+ }
69
+
70
+ const resolved = resolveSamplingKey(req, res)
71
+ if (!resolved) return SamplingDecision.SKIP
72
+
73
+ if (record && resolved.route === null) {
74
+ if (resolved.status === 404 || isBlocked(res)) return SamplingDecision.SKIP
75
+ return SamplingDecision.MISSING_ROUTE
76
+ }
77
+
78
+ if (sampledRequests.has(resolved.key)) return SamplingDecision.SKIP
79
+
80
+ if (asmStandaloneEnabled) {
81
+ keepTrace(rootSpan, ASM)
82
+ }
83
+
84
+ if (record) {
85
+ sampledRequests.set(resolved.key, undefined)
86
+ }
87
+
88
+ return SamplingDecision.SAMPLE
89
+ }
90
+
91
+ /**
92
+ * @param {import('http').IncomingMessage} req
93
+ * @param {import('http').ServerResponse} res
94
+ * @returns {boolean} Whether this request's endpoint is currently recorded in the TTL cache.
95
+ */
96
+ function wasSampled (req, res) {
97
+ const resolved = resolveSamplingKey(req, res)
98
+ return resolved !== null && sampledRequests.has(resolved.key)
99
+ }
100
+
101
+ function resolveSamplingKey (req, res) {
102
+ const method = req.method
103
+ const status = res.statusCode
104
+
105
+ if (!method || !status) {
106
+ log.warn('[ASM] Unsupported groupkey for API security')
107
+ return null
108
+ }
109
+
110
+ const context = web.getContext(req)
111
+ const route = getRouteOrEndpoint(context, status)
112
+
113
+ // route === null signals "no route information at all". An empty string is still a valid
114
+ // route (dd-trace-js represents the express root path '/' as an empty path segment).
115
+ return { method, status, route, key: method + (route ?? '') + status }
116
+ }
117
+
118
+ function getRouteOrEndpoint (context, statusCode) {
119
+ // The router plugin populates `context.paths` whenever the framework matched something.
120
+ // For express's root '/' route the matched path is normalized to '' (see datadog-plugin-router),
121
+ // so `paths.length > 0` is the signal that the framework provided route information — even when
122
+ // the joined string is empty. An empty `paths` array means no router involvement at all.
123
+ const paths = context?.paths
124
+ if (paths !== undefined && paths.length > 0) {
125
+ return paths.join('')
126
+ }
127
+
128
+ if (statusCode === 404) return null
129
+
130
+ const endpoint = context?.span?.context()?.getTag?.('http.endpoint')
131
+ if (endpoint) return endpoint
132
+
133
+ return null
134
+ }
135
+
136
+ function getSpanPriority (span) {
137
+ const spanContext = span.context?.()
138
+ return spanContext._sampling?.priority
139
+ }
140
+
141
+ module.exports = {
142
+ configure,
143
+ disable,
144
+ sampleRequest,
145
+ wasSampled,
146
+ SamplingDecision,
147
+ }
@@ -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
+ httpClientResponseStart: dc.channel('apm:http:client:response:start'),
26
27
  httpClientResponseFinish: dc.channel('apm:http:client:response:finish'),
27
28
  incomingHttpRequestEnd: dc.channel('dd-trace:incomingHttpRequestEnd'),
28
29
  incomingHttpRequestStart: dc.channel('dd-trace:incomingHttpRequestStart'),
@@ -14,19 +14,32 @@ const {
14
14
  const KNUTH_FACTOR = 11400714819323199488n // eslint-disable-line unicorn/numeric-separators-style
15
15
  const UINT64_MAX = (1n << 64n) - 1n
16
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
+
17
30
  let config
18
31
  let samplingRate
19
32
  let globalRequestCounter
20
33
  let bodyAnalysisCount
21
34
  let downstreamAnalysisCount
22
- let redirectBodyCollectionDecisions
35
+ let responseBodyIgnoredCount
23
36
 
24
37
  function enable (_config) {
25
38
  config = _config
26
39
  globalRequestCounter = 0n
27
40
  bodyAnalysisCount = new WeakMap()
28
41
  downstreamAnalysisCount = new WeakMap()
29
- redirectBodyCollectionDecisions = new WeakMap()
42
+ responseBodyIgnoredCount = new WeakMap()
30
43
 
31
44
  const bodyAnalysisSampleRate = config.appsec.apiSecurity?.downstreamBodyAnalysisSampleRate
32
45
  samplingRate = Math.min(Math.max(bodyAnalysisSampleRate, 0), 1)
@@ -43,49 +56,84 @@ function disable () {
43
56
  globalRequestCounter = null
44
57
  bodyAnalysisCount = null
45
58
  downstreamAnalysisCount = null
46
- redirectBodyCollectionDecisions = null
59
+ responseBodyIgnoredCount = null
47
60
  }
48
61
 
49
62
  /**
50
- * Check we have a stored redirect body collection decision for a given URL.
51
- * @param {import('http').IncomingMessage} req outgoing request.
52
- * @param {string} outgoingUrl the URL being requested.
53
- * @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.
54
65
  */
55
- function consumeRedirectBodyCollectionDecision (req, outgoingUrl) {
56
- const decisions = redirectBodyCollectionDecisions.get(req)
57
- 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)
58
73
 
59
- return decisions.delete(outgoingUrl)
74
+ if (!Number.isFinite(parsed) || parsed < 0) {
75
+ return null
76
+ }
77
+
78
+ return parsed
60
79
  }
61
80
 
62
81
  /**
63
- * Stores a redirect body collection decision for a follow-up request.
64
- * @param {import('http').IncomingMessage} req outgoing request.
65
- * @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.
66
85
  */
67
- function storeRedirectBodyCollectionDecision (req, redirectUrl) {
68
- let decisions = redirectBodyCollectionDecisions.get(req)
86
+ function recordResponseBodyIgnored (req, tag) {
87
+ const span = web.root(req)
88
+ if (!span) return
69
89
 
70
- if (!decisions) {
71
- decisions = new Set()
72
- redirectBodyCollectionDecisions.set(req, decisions)
90
+ let counts = responseBodyIgnoredCount.get(req)
91
+ if (!counts) {
92
+ counts = {}
93
+ responseBodyIgnoredCount.set(req, counts)
73
94
  }
74
95
 
75
- decisions.add(redirectUrl)
96
+ const current = counts[tag] || 0
97
+ const next = current + 1
98
+ counts[tag] = next
99
+ span.setTag(tag, next)
76
100
  }
77
101
 
78
102
  /**
79
- * Determines whether the current downstream request/responses bodies should be sampled for analysis.
80
- * @param {import('http').IncomingMessage} req outgoing request.
81
- * @param {string} outgoingUrl the URL being requested (to check for redirect decisions).
82
- * @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.
83
106
  */
84
- function shouldSampleBody (req, outgoingUrl) {
85
- // Check if there's a stored decision from a previous redirect
86
- const storedDecision = consumeRedirectBodyCollectionDecision(req, outgoingUrl)
87
- 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
+ }
88
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) {
89
137
  globalRequestCounter = (globalRequestCounter + 1n) & UINT64_MAX
90
138
 
91
139
  const currentCount = bodyAnalysisCount.get(req) || 0
@@ -97,14 +145,43 @@ function shouldSampleBody (req, outgoingUrl) {
97
145
  // Replace 1000n with the accuraccy that we want to maintain
98
146
  const threshold = (UINT64_MAX * BigInt(Math.round(samplingRate * 1000))) / 1000n
99
147
 
100
- 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
+ }
101
172
 
102
- // Track body analysis count if we're sampling the response body
103
- if (shouldCollectBody) {
104
- incrementBodyAnalysisCount(req)
173
+ if (isRedirectResponse(res)) {
174
+ return
105
175
  }
106
176
 
107
- return shouldCollectBody
177
+ if (!shouldSampleBody(originatingReq)) {
178
+ return
179
+ }
180
+
181
+ if (evaluateResponseBodyCollection(originatingReq, res)) {
182
+ ctx.shouldCollectBody = true
183
+ incrementBodyAnalysisCount(originatingReq)
184
+ }
108
185
  }
109
186
 
110
187
  /**
@@ -145,25 +222,6 @@ function extractRequestData (ctx) {
145
222
  return addresses
146
223
  }
147
224
 
148
- /**
149
- * Checks if a response is a redirect
150
- * @param {import('http').IncomingMessage} req incoming server request.
151
- * @param {import('http').IncomingMessage} res downstream response object.
152
- * @returns {boolean} is redirect.
153
- */
154
- function handleRedirectResponse (req, res) {
155
- const isRedirect = res.statusCode >= 300 && res.statusCode < 400
156
- const redirectLocation = res.headers?.location || ''
157
-
158
- if (isRedirect && redirectLocation) {
159
- // Store the body collection decision for the redirect target
160
- storeRedirectBodyCollectionDecision(req, redirectLocation)
161
- return true
162
- }
163
-
164
- return false
165
- }
166
-
167
225
  /**
168
226
  * Extracts response data for WAF analysis.
169
227
  * @param {import('http').IncomingMessage} res downstream response object.
@@ -291,13 +349,8 @@ function extractMimeType (contentType) {
291
349
  module.exports = {
292
350
  enable,
293
351
  disable,
294
- shouldSampleBody,
295
- handleRedirectResponse,
352
+ planResponseBodyCollection,
296
353
  incrementDownstreamAnalysisCount,
297
354
  extractRequestData,
298
355
  extractResponseData,
299
- // exports for tests
300
- parseBody,
301
- getMethod,
302
- storeRedirectBodyCollectionDecision,
303
356
  }
@@ -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
  }
@@ -38,7 +38,7 @@ const waf = require('./waf')
38
38
  const addresses = require('./addresses')
39
39
  const Reporter = require('./reporter')
40
40
  const appsecTelemetry = require('./telemetry')
41
- const apiSecuritySampler = require('./api_security_sampler')
41
+ const apiSecurity = require('./api_security')
42
42
  const { isBlocked, block, callBlockDelegation, setTemplates, getBlockingAction } = require('./blocking')
43
43
  const { getActiveRequest } = require('./store')
44
44
  const UserTracking = require('./user_tracking')
@@ -71,7 +71,7 @@ function enable (_config) {
71
71
 
72
72
  Reporter.init(_config.appsec, _config.inferredProxyServicesEnabled)
73
73
 
74
- apiSecuritySampler.configure(_config)
74
+ apiSecurity.configure(_config)
75
75
 
76
76
  UserTracking.setCollectionMode(_config.appsec.eventTracking.mode, false)
77
77
 
@@ -233,14 +233,18 @@ function incomingHttpEndTranslator ({ req, res }) {
233
233
  // This hook runs before span finish, so ensure route/endpoint tags are available before API Security sampling runs.
234
234
  web.setRouteOrEndpointTag(req)
235
235
 
236
- if (apiSecuritySampler.sampleRequest(req, res, true)) {
236
+ const apiSecSamplingDecision = apiSecurity.sampleRequest(req, res, true)
237
+ if (apiSecSamplingDecision === apiSecurity.SamplingDecision.SAMPLE) {
237
238
  persistent[addresses.WAF_CONTEXT_PROCESSOR] = { 'extract-schema': true }
238
239
  }
239
240
 
241
+ let wafResult
240
242
  if (!isEmpty(persistent)) {
241
- waf.run({ persistent }, req)
243
+ wafResult = waf.run({ persistent }, req)
242
244
  }
243
245
 
246
+ apiSecurity.reportRequest(req, apiSecSamplingDecision, wafResult)
247
+
244
248
  waf.disposeContext(req)
245
249
 
246
250
  const storedHeaders = storedResponseHeaders.get(req) || {}
@@ -339,7 +343,7 @@ function onRequestProcessParams ({ req, res, abortController, params }) {
339
343
 
340
344
  function onResponseBody ({ req, res, body }) {
341
345
  if (!body || typeof body !== 'object') return
342
- if (!apiSecuritySampler.sampleRequest(req, res)) return
346
+ if (apiSecurity.sampleRequest(req, res) !== apiSecurity.SamplingDecision.SAMPLE) return
343
347
 
344
348
  // we don't support blocking at this point, so no results needed
345
349
  waf.run({
@@ -513,7 +517,7 @@ function disable () {
513
517
 
514
518
  appsecRemoteConfig.disableWafUpdate()
515
519
 
516
- apiSecuritySampler.disable()
520
+ apiSecurity.disable()
517
521
 
518
522
  // Channel#unsubscribe() is undefined for non active channels
519
523
  if (bodyParser.hasSubscribers) bodyParser.unsubscribe(onRequestBodyParsed)
@@ -3,6 +3,7 @@
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')
@@ -21,6 +22,7 @@ function enable (_config) {
21
22
  downstream.enable(_config)
22
23
 
23
24
  httpClientRequestStart.subscribe(analyzeSsrf)
25
+ httpClientResponseStart.subscribe(planResponseBodyCollection)
24
26
  httpClientResponseFinish.subscribe(handleResponseFinish)
25
27
  }
26
28
 
@@ -28,6 +30,7 @@ function disable () {
28
30
  downstream.disable()
29
31
 
30
32
  if (httpClientRequestStart.hasSubscribers) httpClientRequestStart.unsubscribe(analyzeSsrf)
33
+ if (httpClientResponseStart.hasSubscribers) httpClientResponseStart.unsubscribe(planResponseBodyCollection)
31
34
  if (httpClientResponseFinish.hasSubscribers) httpClientResponseFinish.unsubscribe(handleResponseFinish)
32
35
  }
33
36
 
@@ -37,9 +40,6 @@ function analyzeSsrf (ctx) {
37
40
 
38
41
  if (!req || !outgoingUrl) return
39
42
 
40
- // Determine if we should collect the response body based on sampling rate and redirect URL
41
- ctx.shouldCollectBody = downstream.shouldSampleBody(req, outgoingUrl)
42
-
43
43
  const requestAddresses = downstream.extractRequestData(ctx)
44
44
 
45
45
  const ephemeral = {
@@ -56,13 +56,23 @@ function analyzeSsrf (ctx) {
56
56
  downstream.incrementDownstreamAnalysisCount(req)
57
57
  }
58
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
+
59
70
  /**
60
71
  * Finalizes body collection for the response and triggers RASP analysis.
61
- * @param {{
62
- * ctx: object,
63
- * res: import('http').IncomingMessage,
64
- * body: string|Buffer|null
65
- * }} 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.
66
76
  */
67
77
  function handleResponseFinish ({ ctx, res, body }) {
68
78
  // downstream response object
@@ -71,9 +81,7 @@ function handleResponseFinish ({ ctx, res, body }) {
71
81
  const originatingRequest = getActiveRequest()
72
82
  if (!originatingRequest) return
73
83
 
74
- // Skip body analysis for redirect responses
75
- const evaluateBody = ctx.shouldCollectBody && !downstream.handleRedirectResponse(originatingRequest, res)
76
- const responseBody = evaluateBody ? body : null
84
+ const responseBody = ctx.shouldCollectBody ? body : null
77
85
  runResponseEvaluation(res, originatingRequest, responseBody)
78
86
  }
79
87
 
@@ -615,4 +615,5 @@ module.exports = {
615
615
  finishRequest,
616
616
  mapHeaderAndTags,
617
617
  truncateRequestBody,
618
+ isSchemaAttribute,
618
619
  }