dd-trace 6.8.0 → 6.10.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 (209) hide show
  1. package/ci/diagnose.js +27 -25
  2. package/ci/init.js +6 -6
  3. package/ci/runbook.md +114 -20
  4. package/ci/test-optimization-validation/approval-artifacts.js +36 -3
  5. package/ci/test-optimization-validation/approval.js +78 -22
  6. package/ci/test-optimization-validation/blocker-category.js +24 -0
  7. package/ci/test-optimization-validation/ci-discovery.js +23 -17
  8. package/ci/test-optimization-validation/ci-package-scripts.js +34 -6
  9. package/ci/test-optimization-validation/ci-remediation.js +1 -1
  10. package/ci/test-optimization-validation/cli.js +280 -52
  11. package/ci/test-optimization-validation/command-blocker.js +159 -20
  12. package/ci/test-optimization-validation/command-output-policy.js +9 -3
  13. package/ci/test-optimization-validation/command-runner.js +9 -7
  14. package/ci/test-optimization-validation/environment.js +4 -2
  15. package/ci/test-optimization-validation/executable.js +15 -13
  16. package/ci/test-optimization-validation/execution-lock.js +70 -0
  17. package/ci/test-optimization-validation/framework-adapters/cucumber-validation.json +3 -0
  18. package/ci/test-optimization-validation/framework-adapters/cucumber.js +657 -5
  19. package/ci/test-optimization-validation/framework-adapters/vitest.js +517 -0
  20. package/ci/test-optimization-validation/generated-files.js +38 -28
  21. package/ci/test-optimization-validation/generated-test-contract.js +8 -4
  22. package/ci/test-optimization-validation/generated-verifier.js +24 -17
  23. package/ci/test-optimization-validation/literal-glob.js +52 -0
  24. package/ci/test-optimization-validation/manifest-loader.js +3 -0
  25. package/ci/test-optimization-validation/manifest-scaffold.js +300 -50
  26. package/ci/test-optimization-validation/manifest-schema.js +80 -17
  27. package/ci/test-optimization-validation/offline-fixtures.js +13 -4
  28. package/ci/test-optimization-validation/package-check.js +94 -0
  29. package/ci/test-optimization-validation/plan-writer.js +183 -10
  30. package/ci/test-optimization-validation/preflight-runner.js +110 -31
  31. package/ci/test-optimization-validation/project-build-artifact.js +31 -0
  32. package/ci/test-optimization-validation/redaction.js +18 -27
  33. package/ci/test-optimization-validation/report-writer.js +182 -48
  34. package/ci/test-optimization-validation/result-semantics.js +20 -3
  35. package/ci/test-optimization-validation/runner-command.js +40 -20
  36. package/ci/test-optimization-validation/runner-contract.js +79 -43
  37. package/ci/test-optimization-validation/scenarios/basic-reporting.js +59 -5
  38. package/ci/test-optimization-validation/scenarios/ci-wiring.js +141 -35
  39. package/ci/test-optimization-validation/scenarios/helpers.js +30 -6
  40. package/ci/test-optimization-validation/static-diagnosis.js +10 -3
  41. package/ci/test-optimization-validation/test-output.js +2 -1
  42. package/ci/test-optimization-validation/validation-blocker.js +21 -0
  43. package/ci/vitest-no-worker-init-setup.mjs +140 -42
  44. package/index.d.ts +140 -1
  45. package/init.js +1 -17
  46. package/initialize.mjs +11 -0
  47. package/loader-hook.mjs +6 -4
  48. package/package.json +8 -5
  49. package/packages/datadog-core/src/utils/src/parse-tags.js +0 -1
  50. package/packages/datadog-esbuild/src/utils.js +16 -1
  51. package/packages/datadog-instrumentations/src/ai.js +61 -34
  52. package/packages/datadog-instrumentations/src/bluebird.js +6 -6
  53. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +81 -5
  54. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
  55. package/packages/datadog-instrumentations/src/cucumber.js +124 -47
  56. package/packages/datadog-instrumentations/src/dns.js +21 -2
  57. package/packages/datadog-instrumentations/src/electron.js +1 -0
  58. package/packages/datadog-instrumentations/src/fastify.js +3 -1
  59. package/packages/datadog-instrumentations/src/grpc/client.js +4 -2
  60. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +3 -1
  61. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  62. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +629 -0
  63. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +34 -9
  64. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +199 -0
  65. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +20 -2
  66. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +32 -0
  67. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +62 -0
  68. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
  69. package/packages/datadog-instrumentations/src/helpers/router-helper.js +37 -6
  70. package/packages/datadog-instrumentations/src/jest/coverage-backfill.js +1 -1
  71. package/packages/datadog-instrumentations/src/jest.js +980 -234
  72. package/packages/datadog-instrumentations/src/mocha/main.js +108 -10
  73. package/packages/datadog-instrumentations/src/mocha/utils.js +66 -50
  74. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +25 -0
  75. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -18
  76. package/packages/datadog-instrumentations/src/mysql.js +46 -16
  77. package/packages/datadog-instrumentations/src/mysql2.js +119 -18
  78. package/packages/datadog-instrumentations/src/next.js +155 -23
  79. package/packages/datadog-instrumentations/src/openai.js +14 -0
  80. package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
  81. package/packages/datadog-instrumentations/src/pg.js +209 -11
  82. package/packages/datadog-instrumentations/src/playwright.js +173 -79
  83. package/packages/datadog-instrumentations/src/promise-js.js +4 -4
  84. package/packages/datadog-instrumentations/src/promise.js +3 -3
  85. package/packages/datadog-instrumentations/src/router.js +195 -19
  86. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +85 -13
  87. package/packages/datadog-instrumentations/src/vitest-main.js +528 -72
  88. package/packages/datadog-instrumentations/src/vitest-util.js +106 -12
  89. package/packages/datadog-instrumentations/src/vitest-worker.js +382 -40
  90. package/packages/datadog-instrumentations/src/webdriverio.js +212 -22
  91. package/packages/datadog-instrumentations/src/when.js +3 -3
  92. package/packages/datadog-instrumentations/src/ws.js +5 -1
  93. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
  94. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +59 -48
  95. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +2 -5
  96. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
  97. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +1 -3
  98. package/packages/datadog-plugin-cucumber/src/index.js +5 -1
  99. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +51 -47
  100. package/packages/datadog-plugin-cypress/src/support.js +7 -4
  101. package/packages/datadog-plugin-electron/src/net.js +10 -4
  102. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
  103. package/packages/datadog-plugin-http/src/client.js +1 -1
  104. package/packages/datadog-plugin-http2/src/client.js +1 -1
  105. package/packages/datadog-plugin-jest/src/index.js +6 -22
  106. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
  107. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
  108. package/packages/datadog-plugin-kafkajs/src/producer.js +5 -3
  109. package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
  110. package/packages/datadog-plugin-mocha/src/index.js +832 -4
  111. package/packages/datadog-plugin-mysql/src/index.js +37 -0
  112. package/packages/datadog-plugin-next/src/index.js +51 -21
  113. package/packages/datadog-plugin-openai/src/tracing.js +4 -0
  114. package/packages/datadog-plugin-openai-agents/src/integration.js +278 -69
  115. package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
  116. package/packages/datadog-plugin-pg/src/index.js +66 -1
  117. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  118. package/packages/datadog-plugin-vitest/src/index.js +103 -27
  119. package/packages/datadog-plugin-ws/src/util.js +2 -1
  120. package/packages/datadog-shimmer/src/shimmer.js +6 -2
  121. package/packages/dd-trace/src/aiguard/sdk.js +3 -0
  122. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
  123. package/packages/dd-trace/src/appsec/{blocking.js → blocking/index.js} +26 -6
  124. package/packages/dd-trace/src/appsec/handlers/auth.js +62 -0
  125. package/packages/dd-trace/src/appsec/handlers/http-request.js +228 -0
  126. package/packages/dd-trace/src/appsec/handlers/http-response.js +78 -0
  127. package/packages/dd-trace/src/appsec/handlers/http-shared.js +24 -0
  128. package/packages/dd-trace/src/appsec/handlers/payments.js +104 -0
  129. package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +1 -1
  130. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
  131. package/packages/dd-trace/src/appsec/index.js +48 -476
  132. package/packages/dd-trace/src/appsec/sdk/track_event.js +9 -0
  133. package/packages/dd-trace/src/carrier.js +356 -0
  134. package/packages/dd-trace/src/ci-visibility/coverage-report-discovery.js +46 -14
  135. package/packages/dd-trace/src/ci-visibility/efd-retry-policy.js +89 -0
  136. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +27 -13
  137. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +17 -0
  138. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +21 -1
  139. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +22 -7
  140. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -14
  141. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +21 -5
  142. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +1 -1
  143. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +5 -1
  144. package/packages/dd-trace/src/config/generated-config-types.d.ts +5 -0
  145. package/packages/dd-trace/src/config/helper.js +2 -0
  146. package/packages/dd-trace/src/config/index.js +16 -5
  147. package/packages/dd-trace/src/config/remote_config.js +1 -1
  148. package/packages/dd-trace/src/config/supported-configurations.json +18 -0
  149. package/packages/dd-trace/src/datastreams/context.js +6 -1
  150. package/packages/dd-trace/src/datastreams/manager.js +5 -1
  151. package/packages/dd-trace/src/datastreams/pathway.js +23 -22
  152. package/packages/dd-trace/src/datastreams/processor.js +3 -2
  153. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
  154. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +0 -1
  155. package/packages/dd-trace/src/guardrails/apply-pm2-env.js +16 -0
  156. package/packages/dd-trace/src/guardrails/log.js +1 -10
  157. package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
  158. package/packages/dd-trace/src/llmobs/eval-metric.js +249 -0
  159. package/packages/dd-trace/src/llmobs/experiments/index.js +33 -25
  160. package/packages/dd-trace/src/llmobs/experiments/util.js +4 -2
  161. package/packages/dd-trace/src/llmobs/index.js +34 -2
  162. package/packages/dd-trace/src/llmobs/noop.js +2 -0
  163. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +1 -1
  164. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -1
  165. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +5 -5
  166. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +1 -1
  167. package/packages/dd-trace/src/llmobs/sdk.js +149 -68
  168. package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
  169. package/packages/dd-trace/src/llmobs/tagger.js +96 -0
  170. package/packages/dd-trace/src/llmobs/telemetry.js +18 -0
  171. package/packages/dd-trace/src/llmobs/util.js +78 -0
  172. package/packages/dd-trace/src/llmobs/writers/base.js +3 -0
  173. package/packages/dd-trace/src/llmobs/writers/util.js +2 -0
  174. package/packages/dd-trace/src/log/channels.js +1 -9
  175. package/packages/dd-trace/src/log/levels.js +12 -0
  176. package/packages/dd-trace/src/openfeature/writers/exposures.js +1 -1
  177. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  178. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +1 -1
  179. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +8 -4
  180. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -1
  181. package/packages/dd-trace/src/opentelemetry/span.js +4 -0
  182. package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
  183. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
  184. package/packages/dd-trace/src/plugin_manager.js +2 -0
  185. package/packages/dd-trace/src/plugins/ci_plugin.js +21 -5
  186. package/packages/dd-trace/src/plugins/index.js +1 -0
  187. package/packages/dd-trace/src/plugins/util/ci.js +6 -0
  188. package/packages/dd-trace/src/plugins/util/git.js +6 -4
  189. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +2 -2
  190. package/packages/dd-trace/src/plugins/util/ip_extractor.js +1 -1
  191. package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +3 -62
  192. package/packages/dd-trace/src/plugins/util/stacktrace.js +3 -0
  193. package/packages/dd-trace/src/plugins/util/test.js +397 -74
  194. package/packages/dd-trace/src/plugins/util/url.js +1 -1
  195. package/packages/dd-trace/src/plugins/util/web.js +76 -3
  196. package/packages/dd-trace/src/profiler.js +1 -1
  197. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +1 -1
  198. package/packages/dd-trace/src/profiling/profilers/events.js +2 -2
  199. package/packages/dd-trace/src/proxy.js +1 -0
  200. package/packages/dd-trace/src/ritm.js +5 -0
  201. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +4 -2
  202. package/packages/dd-trace/src/sampling_rule.js +4 -2
  203. package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +1 -1
  204. package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +1 -1
  205. package/packages/dd-trace/src/telemetry/logs/index.js +3 -0
  206. package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
  207. package/vendor/dist/pprof-format/index.js +1 -1
  208. package/version.js +10 -8
  209. /package/packages/dd-trace/src/appsec/{blocked_templates.js → blocking/templates.js} +0 -0
@@ -0,0 +1,453 @@
1
+ 'use strict'
2
+
3
+ const { HTTP_ROUTE } = require('../../../../../ext/tags')
4
+ const web = require('../../plugins/util/web')
5
+ const { getParse, getMatch } = require('../../../../datadog-instrumentations/src/path-to-regexp')
6
+
7
+ /**
8
+ * Normalize an HTTP route to the RFC-1103 _dd.appsec.normalized_route format.
9
+ *
10
+ * Rules: (1) starts with /, trailing slash only if declared; (2) no empty elements; (3) static
11
+ * segments keep [A-Za-z0-9.-~_], other chars percent-encoded (UTF-8); (4) dynamic params render as
12
+ * {name} (chars /?#+{} encoded, + reserved), shadowed duplicate names become paramN; (5) one URL
13
+ * segment = one element, multiple params combine as {a+b}, a catch-all is a single terminal element;
14
+ * (6) optional param elements are resolved per request against the matched URL.
15
+ *
16
+ * Design: Express 5 / path-to-regexp 8 only. The route is parsed once by path-to-regexp's `parse()`
17
+ * (token tree → segment templates, cached per route string). Per-request presence of optional
18
+ * groups is resolved with path-to-regexp's own `match()` — we do not re-implement matching. Static-
19
+ * only optional groups (no param to capture) are dropped (rendered absent). Express 4 ships
20
+ * path-to-regexp 0.x, which has no `parse()` → no tag.
21
+ */
22
+
23
+ // path-to-regexp's match() builds a regexp exponential in a route's optional-group count, so cap it;
24
+ // routes with more optional groups are omitted.
25
+ const MAX_OPTIONAL_GROUPS = 8
26
+
27
+ // Keyed on the raw route string — bounded by declared routes (never attacker URLs), so it retains
28
+ // for the process lifetime.
29
+ const routeCache = new Map()
30
+
31
+ const EMPTY_SET = new Set()
32
+
33
+ /**
34
+ * @typedef {{ type: 'slash', group: number }
35
+ * | { type: 'static', text: string, group: number }
36
+ * | { type: 'param', name: string, group: number }
37
+ * | { type: 'wildcard', name: string, group: number }} RouteToken
38
+ *
39
+ * `group` is the id of the innermost enclosing optional `{...}` group (0 = top-level, always present).
40
+ *
41
+ * @typedef {{ group: number, tokens: RouteToken[] }} RouteSegment
42
+ *
43
+ * @typedef {{ type: 'text', value: string } | { type: 'param', name: string }
44
+ * | { type: 'wildcard', name: string } | { type: 'group', tokens: PathToRegexpToken[] }} PathToRegexpToken
45
+ *
46
+ * @typedef {{ segments: RouteSegment[], groupParent: Map<number, number>, optionalGroups: number[],
47
+ * trailingSlash: boolean, precomputed: string | null,
48
+ * matcher: ((url: string) => object | undefined) | null,
49
+ * variants?: Map<number, string> }} CompiledRoute
50
+ */
51
+
52
+ /**
53
+ * Convert path-to-regexp's nested token tree into a flat segment model. Text tokens split on '/'
54
+ * into slash + static tokens; each `{...}` group gets a fresh id (parent tracked in groupParent),
55
+ * its inner tokens inherit it. A trailing empty top-level segment denotes a declared trailing slash.
56
+ * @param {PathToRegexpToken[]} ptTokens
57
+ * @returns {{ segments: RouteSegment[], groupParent: Map<number, number>, trailingSlash: boolean }}
58
+ */
59
+ function tokensToSegments (ptTokens) {
60
+ const groupParent = new Map()
61
+ let nextGroupId = 0
62
+ /** @type {RouteToken[]} */
63
+ const flat = []
64
+
65
+ const walk = (tokens, group) => {
66
+ for (const token of tokens) {
67
+ if (token.type === 'text') {
68
+ const parts = token.value.split('/')
69
+ for (let i = 0; i < parts.length; i++) {
70
+ if (i > 0) flat.push({ type: 'slash', group })
71
+ if (parts[i]) flat.push({ type: 'static', text: parts[i], group })
72
+ }
73
+ } else if (token.type === 'param') {
74
+ flat.push({ type: 'param', name: token.name, group })
75
+ } else if (token.type === 'wildcard') {
76
+ flat.push({ type: 'wildcard', name: token.name, group })
77
+ } else if (token.type === 'group') {
78
+ nextGroupId++
79
+ const groupId = nextGroupId
80
+ groupParent.set(groupId, group)
81
+ walk(token.tokens, groupId)
82
+ }
83
+ }
84
+ }
85
+ walk(ptTokens, 0)
86
+
87
+ /** @type {RouteSegment[]} */
88
+ const segments = []
89
+ let cur = null
90
+ for (const token of flat) {
91
+ if (token.type === 'slash') {
92
+ if (cur) segments.push(cur)
93
+ cur = { group: token.group, tokens: [] }
94
+ } else {
95
+ if (!cur) cur = { group: 0, tokens: [] }
96
+ cur.tokens.push(token)
97
+ }
98
+ }
99
+ if (cur) segments.push(cur)
100
+
101
+ let trailingSlash = false
102
+ const last = segments.at(-1)
103
+ if (last && last.tokens.length === 0 && last.group === 0) {
104
+ trailingSlash = true
105
+ segments.pop()
106
+ }
107
+
108
+ return { segments, groupParent, trailingSlash }
109
+ }
110
+
111
+ /**
112
+ * Compile a raw route string into a CompiledRoute, or null when unsupported.
113
+ * @param {string} route
114
+ * @param {(pattern: string) => { tokens: PathToRegexpToken[] } | undefined} parse
115
+ * @param {(route: string) => ((url: string) => object | undefined) | undefined} makeMatcher
116
+ * @returns {CompiledRoute | null}
117
+ */
118
+ function compileRoute (route, parse, makeMatcher) {
119
+ const parsed = parse(route)
120
+ if (!Array.isArray(parsed?.tokens)) return null
121
+
122
+ const { segments, groupParent, trailingSlash } = tokensToSegments(parsed.tokens)
123
+
124
+ // Empty segments (a bare optional slash like '/users{/}', or '/a//b') carry no element; drop them.
125
+ const cleaned = segments.filter(s => s.tokens.length > 0)
126
+
127
+ if (cleaned.length === 0) {
128
+ return {
129
+ segments: [],
130
+ groupParent: new Map(),
131
+ optionalGroups: [],
132
+ trailingSlash: false,
133
+ precomputed: '/',
134
+ matcher: null,
135
+ }
136
+ }
137
+
138
+ // A catch-all must be the last token of the last segment (rule 5, terminal).
139
+ for (let s = 0; s < cleaned.length; s++) {
140
+ const toks = cleaned[s].tokens
141
+ for (let k = 0; k < toks.length; k++) {
142
+ if (toks[k].type === 'wildcard' && (s !== cleaned.length - 1 || k !== toks.length - 1)) return null
143
+ }
144
+ }
145
+
146
+ // Collapse structural-only groups ('/a{{/b}}'): reparent each group to its nearest represented one.
147
+ const represented = new Set()
148
+ for (const seg of cleaned) {
149
+ if (seg.group !== 0) represented.add(seg.group)
150
+ for (const t of seg.tokens) if (t.group !== 0) represented.add(t.group)
151
+ }
152
+ const effectiveParent = new Map()
153
+ for (const g of represented) {
154
+ let p = groupParent.get(g)
155
+ while (p !== 0 && p !== undefined && !represented.has(p)) p = groupParent.get(p)
156
+ effectiveParent.set(g, p === undefined ? 0 : p)
157
+ }
158
+
159
+ // Group presence is read from match()'s params, so a group is resolvable only via a uniquely named
160
+ // param/wildcard (duplicate names collapse to one key). `paramGroups` = groups holding any param,
161
+ // `detectable` = those we can resolve. Omit the route if a param group is unresolvable (would
162
+ // mis-assign presence); a static-only group has no param, so it renders as absent (dropped).
163
+ const nameCount = new Map()
164
+ for (const seg of cleaned) {
165
+ for (const t of seg.tokens) {
166
+ if (t.type !== 'static') nameCount.set(t.name, (nameCount.get(t.name) ?? 0) + 1)
167
+ }
168
+ }
169
+ const paramGroups = new Set()
170
+ const detectable = new Set()
171
+ for (const seg of cleaned) {
172
+ for (const t of seg.tokens) {
173
+ if (t.type === 'static') continue
174
+ // Resolvable only via a unique param the group holds DIRECTLY: a param in a nested optional
175
+ // subgroup can be absent while this group is present, so it can't prove this group's presence.
176
+ if (t.group !== 0 && nameCount.get(t.name) === 1) detectable.add(t.group)
177
+ for (let g = t.group; g !== 0; g = groupParent.get(g)) paramGroups.add(g)
178
+ }
179
+ }
180
+ for (const g of represented) {
181
+ if (paramGroups.has(g) && !detectable.has(g)) return null
182
+ }
183
+
184
+ // Only resolvable (param) groups become variants; static-only groups stay out and render absent.
185
+ const optionalGroups = [...represented].filter(g => detectable.has(g)).sort((a, b) => a - b)
186
+
187
+ const compiled = {
188
+ segments: cleaned,
189
+ groupParent: effectiveParent,
190
+ optionalGroups,
191
+ trailingSlash,
192
+ precomputed: null,
193
+ matcher: null,
194
+ }
195
+
196
+ // No optional groups → fixed output, computed once (no matcher, so group count is irrelevant here).
197
+ if (optionalGroups.length === 0) {
198
+ compiled.precomputed = renderRoute(compiled, EMPTY_SET)
199
+ return compiled
200
+ }
201
+
202
+ // match()'s regexp is exponential in the route's TOTAL optional-group count (static and structural
203
+ // groups included, not just the resolvable ones), so cap that before building the matcher.
204
+ if (groupParent.size > MAX_OPTIONAL_GROUPS) return null
205
+
206
+ const matcher = makeMatcher(route)
207
+ if (matcher === undefined) return null
208
+ compiled.matcher = matcher
209
+ compiled.variants = new Map()
210
+ return compiled
211
+ }
212
+
213
+ /**
214
+ * True when group `g` (and its whole ancestor chain) is present. Group 0 is always present.
215
+ * @param {number} g
216
+ * @param {Map<number, number>} groupParent
217
+ * @param {Set<number>} present
218
+ * @returns {boolean}
219
+ */
220
+ function groupActive (g, groupParent, present) {
221
+ while (g !== 0) {
222
+ if (!present.has(g)) return false
223
+ g = groupParent.get(g)
224
+ }
225
+ return true
226
+ }
227
+
228
+ /**
229
+ * URL-encode chars outside the RFC-1103 static alphabet [A-Za-z0-9.-~_]. The 'u' flag iterates whole
230
+ * codepoints so surrogate pairs encode as correct multi-byte UTF-8.
231
+ * @param {string} str
232
+ * @returns {string}
233
+ */
234
+ function encodeStaticSegment (str) {
235
+ return str.replaceAll(/[^A-Za-z0-9.\-~_]/gu, c => {
236
+ const code = c.charCodeAt(0)
237
+ if (code < 0x80) return '%' + code.toString(16).toUpperCase().padStart(2, '0')
238
+ try {
239
+ return encodeURIComponent(c)
240
+ } catch {
241
+ return '%EF%BF%BD'
242
+ }
243
+ })
244
+ }
245
+
246
+ /**
247
+ * URL-encode the 6 characters not allowed in a normalized param name (/?#+{}).
248
+ * @param {string} name
249
+ * @returns {string}
250
+ */
251
+ function encodeParamName (name) {
252
+ return name.replaceAll(/[/?#+{}]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'))
253
+ }
254
+
255
+ /**
256
+ * Render the normalized route for a given set of present optional groups.
257
+ * @param {CompiledRoute} compiled
258
+ * @param {Set<number>} present
259
+ * @returns {string}
260
+ */
261
+ function renderRoute (compiled, present) {
262
+ const { segments, groupParent } = compiled
263
+
264
+ // Pass 1: present dynamic tokens (param/wildcard) in declaration order. Activity is per token: a
265
+ // mandatory token can share a segment with an absent optional group (e.g. '.json' in '/x{/:id}.json').
266
+ const dyn = []
267
+ for (const seg of segments) {
268
+ for (const t of seg.tokens) {
269
+ if (t.type !== 'static' && groupActive(t.group, groupParent, present)) dyn.push(t)
270
+ }
271
+ }
272
+
273
+ // Pass 2: a token keeps its (encoded) name iff it is the last present occurrence of that name;
274
+ // earlier duplicates become paramN (skipping surviving names).
275
+ const encodedNames = new Array(dyn.length)
276
+ const lastIndexByName = new Map()
277
+ for (let idx = 0; idx < dyn.length; idx++) {
278
+ encodedNames[idx] = encodeParamName(dyn[idx].name)
279
+ lastIndexByName.set(encodedNames[idx], idx)
280
+ }
281
+ const used = new Set()
282
+ for (let idx = 0; idx < dyn.length; idx++) {
283
+ if (lastIndexByName.get(encodedNames[idx]) === idx) used.add(encodedNames[idx])
284
+ }
285
+ const resolvedNames = new Array(dyn.length)
286
+ let counter = 1
287
+ for (let idx = 0; idx < dyn.length; idx++) {
288
+ if (lastIndexByName.get(encodedNames[idx]) === idx) {
289
+ resolvedNames[idx] = encodedNames[idx]
290
+ } else {
291
+ while (used.has(`param${counter}`)) counter++
292
+ resolvedNames[idx] = `param${counter}`
293
+ used.add(`param${counter}`)
294
+ counter++
295
+ }
296
+ }
297
+
298
+ // Pass 3: build the output elements. Each present leading slash starts a new element; a segment
299
+ // whose leading slash is absent merges its still-present tokens into the current element (so the
300
+ // '.json' in '/x{/:id}.json' survives when :id is absent) rather than being dropped with the slash.
301
+ const out = []
302
+ let dynPtr = 0
303
+ let staticText = ''
304
+ let paramNames = []
305
+ let isCatchAll = false
306
+
307
+ const flush = () => {
308
+ if (isCatchAll) {
309
+ out.push(`{${paramNames.join('+')}}`) // terminal catch-all; static prefix subsumed (rule 5)
310
+ } else if (paramNames.length === 1) {
311
+ out.push(`{${paramNames[0]}}`)
312
+ } else if (paramNames.length > 1) {
313
+ out.push(`{${paramNames.join('+')}}`)
314
+ } else if (staticText !== '') {
315
+ out.push(encodeStaticSegment(staticText))
316
+ }
317
+ staticText = ''
318
+ paramNames = []
319
+ isCatchAll = false
320
+ }
321
+
322
+ for (const seg of segments) {
323
+ if (groupActive(seg.group, groupParent, present)) flush()
324
+ for (const t of seg.tokens) {
325
+ if (!groupActive(t.group, groupParent, present)) continue
326
+ if (t.type === 'static') {
327
+ staticText += t.text
328
+ } else {
329
+ if (t.type === 'wildcard') isCatchAll = true
330
+ paramNames.push(resolvedNames[dynPtr++])
331
+ }
332
+ }
333
+ }
334
+ flush()
335
+
336
+ const normalized = '/' + out.join('/')
337
+ return compiled.trailingSlash && normalized !== '/' ? normalized + '/' : normalized
338
+ }
339
+
340
+ /**
341
+ * Which optional groups are present, from a captured/params object: an optional group is present iff
342
+ * one of its param tokens has a defined own-property value. Marks ancestor groups present too.
343
+ * @param {CompiledRoute} compiled
344
+ * @param {object | null | undefined} params
345
+ * @returns {Set<number>}
346
+ */
347
+ function resolvePresence (compiled, params) {
348
+ const present = new Set()
349
+ if (params == null) return present
350
+ const { segments, groupParent } = compiled
351
+ for (const seg of segments) {
352
+ for (const t of seg.tokens) {
353
+ if (t.group === 0 || t.type === 'static') continue
354
+ if (Object.hasOwn(params, t.name) && params[t.name] !== undefined) {
355
+ let g = t.group
356
+ while (g !== 0 && !present.has(g)) {
357
+ present.add(g)
358
+ g = groupParent.get(g)
359
+ }
360
+ }
361
+ }
362
+ }
363
+ return present
364
+ }
365
+
366
+ /**
367
+ * Stable bitmask key for a present-group set (group ids are small, route-local).
368
+ * @param {number[]} optionalGroups
369
+ * @param {Set<number>} present
370
+ * @returns {number}
371
+ */
372
+ function presenceBitmask (optionalGroups, present) {
373
+ let mask = 0
374
+ for (let i = 0; i < optionalGroups.length; i++) {
375
+ if (present.has(optionalGroups[i])) mask |= (1 << i)
376
+ }
377
+ return mask
378
+ }
379
+
380
+ /**
381
+ * Normalize an Express route string. Express 5 only: `parse`/`makeMatcher` are path-to-regexp 8's
382
+ * `parse()` and a `match()` factory; returns null when either is unavailable (Express 4).
383
+ * @param {string} route - route string (the http.route value)
384
+ * @param {object|null|undefined} params - req.params (fallback when the URL doesn't match)
385
+ * @param {string|undefined} urlPath - request URL (query stripped here)
386
+ * @param {(pattern: string) => { tokens: PathToRegexpToken[] } | undefined} parse
387
+ * @param {(route: string) => ((url: string) => object | undefined) | undefined} makeMatcher
388
+ * @returns {string|null}
389
+ */
390
+ function normalizeRouteExpress (route, params, urlPath, parse, makeMatcher) {
391
+ if (typeof route !== 'string' || !route) return null
392
+ if (typeof parse !== 'function' || typeof makeMatcher !== 'function') return null
393
+
394
+ let entry = routeCache.get(route)
395
+ if (entry === undefined) {
396
+ entry = compileRoute(route, parse, makeMatcher)
397
+ routeCache.set(route, entry)
398
+ }
399
+ if (entry === null) return null
400
+ if (entry.precomputed !== null) return entry.precomputed
401
+
402
+ // The matched URL is authoritative (handles mergeParams=false sub-routers); fall back to
403
+ // req.params when there is no URL or it doesn't match.
404
+ let matched
405
+ if (urlPath) {
406
+ const qIdx = urlPath.indexOf('?')
407
+ matched = entry.matcher(qIdx === -1 ? urlPath : urlPath.slice(0, qIdx))
408
+ }
409
+ const present = resolvePresence(entry, matched ?? params)
410
+
411
+ const mask = presenceBitmask(entry.optionalGroups, present)
412
+ const cached = entry.variants.get(mask)
413
+ if (cached !== undefined) return cached
414
+
415
+ const result = renderRoute(entry, present)
416
+ entry.variants.set(mask, result)
417
+ return result
418
+ }
419
+
420
+ /**
421
+ * Normalize an HTTP route from a request. Reads the framework from the span component tag.
422
+ * @param {object} req
423
+ * @returns {string|null}
424
+ */
425
+ function normalizeRoute (req) {
426
+ const spanContext = web.root(req)?.context()
427
+ const component = spanContext?.getTag?.('component')
428
+
429
+ // eslint-disable-next-line sonarjs/no-small-switch
430
+ switch (component) {
431
+ case 'express': {
432
+ // Routes are read as Express 5 grammar, and 4's differs ('/a{2}' matches '/aa' there), so skip
433
+ // Express 4. Decided per request off the serving app — a process can load both majors, and the
434
+ // parser adapters are process-wide — via the legacy `app.del` alias that Express 5 removed.
435
+ if (typeof req.app?.del === 'function') return null
436
+
437
+ const parse = getParse()
438
+ const makeMatcher = getMatch()
439
+ if (parse === undefined || makeMatcher === undefined) return null
440
+ // Reuse the http.route tag set by web.setRouteOrEndpointTag just before this hook.
441
+ const route = spanContext.getTag(HTTP_ROUTE)
442
+ return normalizeRouteExpress(route, req.params, req.originalUrl || req.url, parse, makeMatcher)
443
+ }
444
+ default:
445
+ return null
446
+ }
447
+ }
448
+
449
+ module.exports = {
450
+ normalizeRoute,
451
+ // exported for unit testing (framework-agnostic core)
452
+ normalizeRouteExpress,
453
+ }
@@ -1,10 +1,10 @@
1
1
  'use strict'
2
2
 
3
- const { LRUCache } = require('../../../../vendor/dist/lru-cache')
4
- const log = require('../log')
5
- const web = require('../plugins/util/web')
6
- const blockedTemplates = require('./blocked_templates')
7
- const { updateBlockFailureMetric } = require('./telemetry')
3
+ const { LRUCache } = require('../../../../../vendor/dist/lru-cache')
4
+ const log = require('../../log')
5
+ const web = require('../../plugins/util/web')
6
+ const { updateBlockFailureMetric } = require('../telemetry')
7
+ const blockedTemplates = require('./templates')
8
8
 
9
9
  // Bounded by the LRU as defense-in-depth: getSpecificKey already keys on the
10
10
  // resolved route (or the path with the query string stripped) so cardinality
@@ -182,7 +182,26 @@ function getBlockingAction (actions) {
182
182
  }
183
183
 
184
184
  /**
185
- * @param {import('../config/config-base')} [config] - Tracer configuration
185
+ * Turns a WAF result's actions into a blocking response, when one is requested.
186
+ * Single seam from WAF output to {@link block}, shared by every request/response/auth handler.
187
+ *
188
+ * @param {Record<string, unknown>|undefined} actions WAF result actions.
189
+ * @param {import('http').IncomingMessage} req
190
+ * @param {import('http').ServerResponse} res
191
+ * @param {object} rootSpan
192
+ * @param {AbortController} abortController
193
+ */
194
+ function handleResults (actions, req, res, rootSpan, abortController) {
195
+ if (!actions || !req || !res || !rootSpan || !abortController) return
196
+
197
+ const blockingAction = getBlockingAction(actions)
198
+ if (blockingAction) {
199
+ block(req, res, rootSpan, abortController, blockingAction)
200
+ }
201
+ }
202
+
203
+ /**
204
+ * @param {import('../../config/config-base')} [config] - Tracer configuration
186
205
  */
187
206
  function setTemplates (config) {
188
207
  templates.html.body = config?.appsec?.blockedTemplateHtml
@@ -233,6 +252,7 @@ module.exports = {
233
252
  specificBlockingTypes,
234
253
  getBlockingData,
235
254
  getBlockingAction,
255
+ handleResults,
236
256
  setTemplates,
237
257
  isBlocked,
238
258
  setDefaultBlockingActionParameters,
@@ -0,0 +1,62 @@
1
+ 'use strict'
2
+
3
+ const log = require('../../log')
4
+ const web = require('../../plugins/util/web')
5
+ const addresses = require('../addresses')
6
+ const { handleResults } = require('../blocking')
7
+ const { getActiveRequest } = require('../store')
8
+ const UserTracking = require('../user_tracking')
9
+ const waf = require('../waf')
10
+
11
+ function onPassportVerify ({ framework, login, user, success, abortController }) {
12
+ const req = getActiveRequest()
13
+ const rootSpan = req && web.root(req)
14
+
15
+ if (!rootSpan) {
16
+ log.warn('[ASM] No rootSpan found in onPassportVerify')
17
+ return
18
+ }
19
+
20
+ const results = UserTracking.trackLogin(framework, login, user, success, rootSpan)
21
+
22
+ handleResults(results?.actions, req, web.getContext(req)?.res, rootSpan, abortController)
23
+ }
24
+
25
+ function onPassportDeserializeUser ({ user, abortController }) {
26
+ const req = getActiveRequest()
27
+ const rootSpan = req && web.root(req)
28
+
29
+ if (!rootSpan) {
30
+ log.warn('[ASM] No rootSpan found in onPassportDeserializeUser')
31
+ return
32
+ }
33
+
34
+ const results = UserTracking.trackUser(user, rootSpan)
35
+
36
+ handleResults(results?.actions, req, web.getContext(req)?.res, rootSpan, abortController)
37
+ }
38
+
39
+ function onExpressSession ({ req, res, sessionId, abortController }) {
40
+ const rootSpan = web.root(req)
41
+ if (!rootSpan) {
42
+ log.warn('[ASM] No rootSpan found in onExpressSession')
43
+ return
44
+ }
45
+
46
+ const isSdkCalled = rootSpan.context().getTag('usr.session_id')
47
+ if (isSdkCalled) return
48
+
49
+ const results = waf.run({
50
+ persistent: {
51
+ [addresses.USER_SESSION_ID]: sessionId,
52
+ },
53
+ }, req)
54
+
55
+ handleResults(results?.actions, req, res, rootSpan, abortController)
56
+ }
57
+
58
+ module.exports = {
59
+ onPassportVerify,
60
+ onPassportDeserializeUser,
61
+ onExpressSession,
62
+ }