dd-trace 6.9.0 → 6.11.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.
- package/README.md +0 -17
- package/ci/vitest-no-worker-init-setup.mjs +112 -10
- package/index.d.ts +150 -3
- package/initialize.mjs +28 -17
- package/openfeature.js +2 -2
- package/package.json +14 -10
- package/packages/datadog-esbuild/index.js +0 -34
- package/packages/datadog-instrumentations/src/anthropic.js +210 -15
- package/packages/datadog-instrumentations/src/aws-sdk.js +9 -1
- package/packages/datadog-instrumentations/src/browser-bunyan.js +11 -0
- package/packages/datadog-instrumentations/src/bunyan.js +3 -16
- package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
- package/packages/datadog-instrumentations/src/cucumber.js +26 -11
- package/packages/datadog-instrumentations/src/cypress-config.js +182 -16
- package/packages/datadog-instrumentations/src/cypress-legacy-finalizers.js +30 -0
- package/packages/datadog-instrumentations/src/cypress.js +2 -0
- package/packages/datadog-instrumentations/src/fastify.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/bunyan.js +22 -0
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +634 -0
- package/packages/datadog-instrumentations/src/helpers/register.js +10 -8
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +81 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
- package/packages/datadog-instrumentations/src/jest.js +26 -18
- package/packages/datadog-instrumentations/src/mariadb.js +278 -21
- package/packages/datadog-instrumentations/src/mocha/main.js +540 -56
- package/packages/datadog-instrumentations/src/mocha/utils.js +132 -25
- package/packages/datadog-instrumentations/src/mysql.js +45 -15
- package/packages/datadog-instrumentations/src/mysql2.js +119 -18
- package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
- package/packages/datadog-instrumentations/src/pg.js +116 -92
- package/packages/datadog-instrumentations/src/playwright-reporter.js +99 -0
- package/packages/datadog-instrumentations/src/playwright.js +149 -2
- package/packages/datadog-instrumentations/src/router.js +2 -0
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -4
- package/packages/datadog-instrumentations/src/vitest-main.js +180 -20
- package/packages/datadog-instrumentations/src/vitest-util.js +64 -5
- package/packages/datadog-instrumentations/src/vitest-worker.js +150 -25
- package/packages/datadog-instrumentations/src/webdriverio.js +31 -17
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
- package/packages/datadog-plugin-aws-sdk/src/base.js +1 -0
- package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +163 -44
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +4 -13
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
- package/packages/datadog-plugin-azure-functions/src/index.js +1 -1
- package/packages/datadog-plugin-browser-bunyan/src/index.js +9 -0
- package/packages/datadog-plugin-bunyan/src/index.js +5 -8
- package/packages/datadog-plugin-cucumber/src/index.js +11 -0
- package/packages/datadog-plugin-cypress/src/after-run.js +5 -1
- package/packages/datadog-plugin-cypress/src/after-spec.js +5 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +82 -30
- package/packages/datadog-plugin-cypress/src/finalization.js +76 -0
- package/packages/datadog-plugin-cypress/src/index.js +12 -6
- package/packages/datadog-plugin-cypress/src/plugin.js +16 -3
- package/packages/datadog-plugin-dd-trace-api/src/index.js +4 -0
- package/packages/datadog-plugin-electron/src/index.js +2 -0
- package/packages/datadog-plugin-fs/src/index.js +1 -1
- package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
- package/packages/datadog-plugin-jest/src/index.js +11 -4
- package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
- package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
- package/packages/datadog-plugin-kafkajs/src/producer.js +3 -2
- package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
- package/packages/datadog-plugin-mocha/src/index.js +606 -40
- package/packages/datadog-plugin-mysql/src/index.js +37 -0
- package/packages/datadog-plugin-nats/src/index.js +1 -1
- package/packages/datadog-plugin-next/src/index.js +1 -19
- package/packages/datadog-plugin-openai/src/tracing.js +41 -30
- package/packages/datadog-plugin-openai-agents/src/integration.js +279 -69
- package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
- package/packages/datadog-plugin-pg/src/index.js +1 -0
- package/packages/datadog-plugin-playwright/src/index.js +42 -13
- package/packages/datadog-plugin-vitest/src/index.js +14 -20
- package/packages/datadog-plugin-ws/src/util.js +2 -1
- package/packages/datadog-webpack/index.js +0 -11
- package/packages/dd-trace/index.js +0 -1
- package/packages/dd-trace/src/agent/info.js +6 -5
- package/packages/dd-trace/src/aiguard/client.js +100 -1
- package/packages/dd-trace/src/aiguard/errors.js +41 -0
- package/packages/dd-trace/src/aiguard/evaluation.js +357 -0
- package/packages/dd-trace/src/aiguard/integrations/anthropic.js +70 -0
- package/packages/dd-trace/src/aiguard/integrations/index.js +4 -0
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +423 -0
- package/packages/dd-trace/src/aiguard/messages/openai.js +1 -24
- package/packages/dd-trace/src/aiguard/messages/utils.js +28 -0
- package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +1 -11
- package/packages/dd-trace/src/aiguard/sdk.js +22 -278
- package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
- package/packages/dd-trace/src/appsec/handlers/http-request.js +13 -0
- package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +9 -12
- package/packages/dd-trace/src/carrier.js +356 -0
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +1 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +27 -3
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +22 -6
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +21 -5
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +164 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +22 -6
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +365 -32
- package/packages/dd-trace/src/ci-visibility/exporters/request.js +201 -0
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +5 -3
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -5
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -1
- package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +39 -0
- package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -6
- package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +6 -2
- package/packages/dd-trace/src/config/generated-config-types.d.ts +7 -0
- package/packages/dd-trace/src/config/index.js +13 -2
- package/packages/dd-trace/src/config/major-overrides.js +7 -0
- package/packages/dd-trace/src/config/supported-configurations.json +24 -0
- package/packages/dd-trace/src/datastreams/context.js +6 -1
- package/packages/dd-trace/src/datastreams/index.js +2 -0
- package/packages/dd-trace/src/datastreams/manager.js +14 -3
- package/packages/dd-trace/src/datastreams/pathway.js +23 -22
- package/packages/dd-trace/src/datastreams/processor.js +16 -13
- package/packages/dd-trace/src/datastreams/size.js +5 -0
- package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -2
- package/packages/dd-trace/src/exporters/agent/writer.js +49 -9
- package/packages/dd-trace/src/exporters/common/buffering-exporter.js +6 -4
- package/packages/dd-trace/src/exporters/common/writer.js +11 -6
- package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
- package/packages/dd-trace/src/llmobs/experiments/client.js +163 -5
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +241 -87
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +280 -20
- package/packages/dd-trace/src/llmobs/experiments/index.js +58 -59
- package/packages/dd-trace/src/llmobs/experiments/noop.js +71 -3
- package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
- package/packages/dd-trace/src/llmobs/experiments/util.js +41 -5
- package/packages/dd-trace/src/llmobs/index.js +34 -2
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +12 -3
- package/packages/dd-trace/src/llmobs/plugins/base.js +4 -2
- package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
- package/packages/dd-trace/src/llmobs/tagger.js +96 -0
- package/packages/dd-trace/src/llmobs/util.js +78 -0
- package/packages/dd-trace/src/noop/proxy.js +3 -4
- package/packages/dd-trace/src/openfeature/flagging_provider.js +18 -2
- package/packages/dd-trace/src/openfeature/remote_config.js +1 -1
- package/packages/dd-trace/src/opentelemetry/context_manager.js +7 -7
- package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +2 -1
- package/packages/dd-trace/src/opentelemetry/logs/logger.js +67 -11
- package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +14 -3
- package/packages/dd-trace/src/opentelemetry/metrics/index.js +25 -12
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +2 -5
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +96 -43
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
- package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
- package/packages/dd-trace/src/opentracing/tracer.js +5 -2
- package/packages/dd-trace/src/otel-thread-ctx.js +387 -0
- package/packages/dd-trace/src/plugin_manager.js +5 -3
- package/packages/dd-trace/src/plugins/ci_plugin.js +6 -1
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +5 -65
- package/packages/dd-trace/src/plugins/util/test.js +93 -14
- package/packages/dd-trace/src/plugins/util/web.js +56 -2
- package/packages/dd-trace/src/profiling/profiler.js +1 -1
- package/packages/dd-trace/src/profiling/profilers/space.js +1 -3
- package/packages/dd-trace/src/profiling/profilers/wall.js +37 -90
- package/packages/dd-trace/src/profiling/webspan-utils.js +45 -0
- package/packages/dd-trace/src/proxy.js +38 -29
- package/packages/dd-trace/src/ritm.js +9 -2
- package/packages/dd-trace/src/serverless.js +37 -0
- package/packages/dd-trace/src/span_format.js +1 -0
- package/packages/dd-trace/src/span_stats.js +30 -3
- package/packages/dd-trace/src/storage-channels.js +86 -0
- package/packages/dd-trace/src/tracer.js +9 -2
- package/packages/dd-trace/src/tracer_metadata.js +14 -1
- package/packages/dd-trace/src/web-tags-cache.js +132 -0
- package/vendor/dist/@datadog/openfeature-node-server/LICENSE +201 -0
- package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +0 -65
- package/packages/datadog-webpack/src/optional-peer-loader.js +0 -17
- package/packages/dd-trace/src/feature-registry.js +0 -29
- package/packages/dd-trace/src/openfeature/register.js +0 -35
- package/packages/dd-trace/src/openfeature/require-provider.js +0 -18
|
@@ -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,11 +1,13 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { HTTP_CLIENT_IP } = require('../../../../../ext/tags')
|
|
4
|
+
const log = require('../../log')
|
|
4
5
|
const web = require('../../plugins/util/web')
|
|
5
6
|
const { extractIp } = require('../../plugins/util/ip_extractor')
|
|
6
7
|
const { isEmpty } = require('../../util')
|
|
7
8
|
const addresses = require('../addresses')
|
|
8
9
|
const apiSecurity = require('../api_security')
|
|
10
|
+
const { normalizeRoute } = require('../api_security/normalized-route')
|
|
9
11
|
const { handleResults } = require('../blocking')
|
|
10
12
|
const Reporter = require('../reporter')
|
|
11
13
|
const { getActiveRequest } = require('../store')
|
|
@@ -143,6 +145,17 @@ function incomingHttpEndTranslator ({ req, res }) {
|
|
|
143
145
|
// This hook runs before span finish, so ensure route/endpoint tags are available before API Security sampling runs.
|
|
144
146
|
web.setRouteOrEndpointTag(req)
|
|
145
147
|
|
|
148
|
+
if (config.appsec.DD_API_SECURITY_ENABLED) {
|
|
149
|
+
try {
|
|
150
|
+
const normalized = normalizeRoute(req)
|
|
151
|
+
if (normalized !== null) {
|
|
152
|
+
web.root(req)?.setTag('_dd.appsec.normalized_route', normalized)
|
|
153
|
+
}
|
|
154
|
+
} catch (e) {
|
|
155
|
+
log.debug('[ASM] Unable to compute normalized route: %s', e.message)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
146
159
|
const apiSecSamplingDecision = apiSecurity.sampleRequest(req, res, true)
|
|
147
160
|
if (apiSecSamplingDecision === apiSecurity.SamplingDecision.SAMPLE) {
|
|
148
161
|
persistent[addresses.WAF_CONTEXT_PROCESSOR] = { 'extract-schema': true }
|
|
@@ -97,15 +97,14 @@ function getPrepareStackTraceAccessor () {
|
|
|
97
97
|
|
|
98
98
|
function getCompileMethodFn (compileMethod) {
|
|
99
99
|
let delegate = function (content, filename) {
|
|
100
|
+
if (isDdTrace(filename) || !isPrivateModule(filename) || !config.iast?.enabled) {
|
|
101
|
+
return compileMethod.apply(this, [content, filename])
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// TODO when we have CJS support for orchestrion and taint-tracking, add
|
|
105
|
+
// them here as appropriate
|
|
106
|
+
let rewrittenContent
|
|
100
107
|
try {
|
|
101
|
-
if (isDdTrace(filename)) {
|
|
102
|
-
return compileMethod.apply(this, [content, filename])
|
|
103
|
-
}
|
|
104
|
-
if (!isPrivateModule(filename) || !config.iast?.enabled) {
|
|
105
|
-
return compileMethod.apply(this, [content, filename])
|
|
106
|
-
}
|
|
107
|
-
// TODO when we have CJS support for orchestrion and taint-tracking, add
|
|
108
|
-
// them here as appropriate
|
|
109
108
|
const rewritten = rewriter.rewrite(content, filename, ['iast'])
|
|
110
109
|
|
|
111
110
|
incrementTelemetryIfNeeded(rewritten.metrics)
|
|
@@ -114,13 +113,11 @@ function getCompileMethodFn (compileMethod) {
|
|
|
114
113
|
hardcodedSecretCh.publish(rewritten.literalsResult)
|
|
115
114
|
}
|
|
116
115
|
|
|
117
|
-
|
|
118
|
-
return compileMethod.apply(this, [rewritten.content, filename])
|
|
119
|
-
}
|
|
116
|
+
rewrittenContent = rewritten?.content
|
|
120
117
|
} catch (e) {
|
|
121
118
|
log.error('Error rewriting file %s', filename, e)
|
|
122
119
|
}
|
|
123
|
-
return compileMethod.apply(this, [content, filename])
|
|
120
|
+
return compileMethod.apply(this, [rewrittenContent || content, filename])
|
|
124
121
|
}
|
|
125
122
|
|
|
126
123
|
const shim = function (...args) {
|