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.
- package/ci/diagnose.js +27 -25
- package/ci/init.js +6 -6
- package/ci/runbook.md +114 -20
- package/ci/test-optimization-validation/approval-artifacts.js +36 -3
- package/ci/test-optimization-validation/approval.js +78 -22
- package/ci/test-optimization-validation/blocker-category.js +24 -0
- package/ci/test-optimization-validation/ci-discovery.js +23 -17
- package/ci/test-optimization-validation/ci-package-scripts.js +34 -6
- package/ci/test-optimization-validation/ci-remediation.js +1 -1
- package/ci/test-optimization-validation/cli.js +280 -52
- package/ci/test-optimization-validation/command-blocker.js +159 -20
- package/ci/test-optimization-validation/command-output-policy.js +9 -3
- package/ci/test-optimization-validation/command-runner.js +9 -7
- package/ci/test-optimization-validation/environment.js +4 -2
- package/ci/test-optimization-validation/executable.js +15 -13
- package/ci/test-optimization-validation/execution-lock.js +70 -0
- package/ci/test-optimization-validation/framework-adapters/cucumber-validation.json +3 -0
- package/ci/test-optimization-validation/framework-adapters/cucumber.js +657 -5
- package/ci/test-optimization-validation/framework-adapters/vitest.js +517 -0
- package/ci/test-optimization-validation/generated-files.js +38 -28
- package/ci/test-optimization-validation/generated-test-contract.js +8 -4
- package/ci/test-optimization-validation/generated-verifier.js +24 -17
- package/ci/test-optimization-validation/literal-glob.js +52 -0
- package/ci/test-optimization-validation/manifest-loader.js +3 -0
- package/ci/test-optimization-validation/manifest-scaffold.js +300 -50
- package/ci/test-optimization-validation/manifest-schema.js +80 -17
- package/ci/test-optimization-validation/offline-fixtures.js +13 -4
- package/ci/test-optimization-validation/package-check.js +94 -0
- package/ci/test-optimization-validation/plan-writer.js +183 -10
- package/ci/test-optimization-validation/preflight-runner.js +110 -31
- package/ci/test-optimization-validation/project-build-artifact.js +31 -0
- package/ci/test-optimization-validation/redaction.js +18 -27
- package/ci/test-optimization-validation/report-writer.js +182 -48
- package/ci/test-optimization-validation/result-semantics.js +20 -3
- package/ci/test-optimization-validation/runner-command.js +40 -20
- package/ci/test-optimization-validation/runner-contract.js +79 -43
- package/ci/test-optimization-validation/scenarios/basic-reporting.js +59 -5
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +141 -35
- package/ci/test-optimization-validation/scenarios/helpers.js +30 -6
- package/ci/test-optimization-validation/static-diagnosis.js +10 -3
- package/ci/test-optimization-validation/test-output.js +2 -1
- package/ci/test-optimization-validation/validation-blocker.js +21 -0
- package/ci/vitest-no-worker-init-setup.mjs +140 -42
- package/index.d.ts +140 -1
- package/init.js +1 -17
- package/initialize.mjs +11 -0
- package/loader-hook.mjs +6 -4
- package/package.json +8 -5
- package/packages/datadog-core/src/utils/src/parse-tags.js +0 -1
- package/packages/datadog-esbuild/src/utils.js +16 -1
- package/packages/datadog-instrumentations/src/ai.js +61 -34
- package/packages/datadog-instrumentations/src/bluebird.js +6 -6
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +81 -5
- package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
- package/packages/datadog-instrumentations/src/cucumber.js +124 -47
- package/packages/datadog-instrumentations/src/dns.js +21 -2
- package/packages/datadog-instrumentations/src/electron.js +1 -0
- package/packages/datadog-instrumentations/src/fastify.js +3 -1
- package/packages/datadog-instrumentations/src/grpc/client.js +4 -2
- package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +3 -1
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +629 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +34 -9
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +199 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +20 -2
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +32 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +62 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
- package/packages/datadog-instrumentations/src/helpers/router-helper.js +37 -6
- package/packages/datadog-instrumentations/src/jest/coverage-backfill.js +1 -1
- package/packages/datadog-instrumentations/src/jest.js +980 -234
- package/packages/datadog-instrumentations/src/mocha/main.js +108 -10
- package/packages/datadog-instrumentations/src/mocha/utils.js +66 -50
- package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +25 -0
- package/packages/datadog-instrumentations/src/mocha/worker.js +12 -18
- package/packages/datadog-instrumentations/src/mysql.js +46 -16
- package/packages/datadog-instrumentations/src/mysql2.js +119 -18
- package/packages/datadog-instrumentations/src/next.js +155 -23
- package/packages/datadog-instrumentations/src/openai.js +14 -0
- package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
- package/packages/datadog-instrumentations/src/pg.js +209 -11
- package/packages/datadog-instrumentations/src/playwright.js +173 -79
- package/packages/datadog-instrumentations/src/promise-js.js +4 -4
- package/packages/datadog-instrumentations/src/promise.js +3 -3
- package/packages/datadog-instrumentations/src/router.js +195 -19
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +85 -13
- package/packages/datadog-instrumentations/src/vitest-main.js +528 -72
- package/packages/datadog-instrumentations/src/vitest-util.js +106 -12
- package/packages/datadog-instrumentations/src/vitest-worker.js +382 -40
- package/packages/datadog-instrumentations/src/webdriverio.js +212 -22
- package/packages/datadog-instrumentations/src/when.js +3 -3
- package/packages/datadog-instrumentations/src/ws.js +5 -1
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +59 -48
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +2 -5
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
- package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +1 -3
- package/packages/datadog-plugin-cucumber/src/index.js +5 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +51 -47
- package/packages/datadog-plugin-cypress/src/support.js +7 -4
- package/packages/datadog-plugin-electron/src/net.js +10 -4
- package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
- package/packages/datadog-plugin-http/src/client.js +1 -1
- package/packages/datadog-plugin-http2/src/client.js +1 -1
- package/packages/datadog-plugin-jest/src/index.js +6 -22
- 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 +5 -3
- package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
- package/packages/datadog-plugin-mocha/src/index.js +832 -4
- package/packages/datadog-plugin-mysql/src/index.js +37 -0
- package/packages/datadog-plugin-next/src/index.js +51 -21
- package/packages/datadog-plugin-openai/src/tracing.js +4 -0
- package/packages/datadog-plugin-openai-agents/src/integration.js +278 -69
- package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
- package/packages/datadog-plugin-pg/src/index.js +66 -1
- package/packages/datadog-plugin-playwright/src/index.js +1 -1
- package/packages/datadog-plugin-vitest/src/index.js +103 -27
- package/packages/datadog-plugin-ws/src/util.js +2 -1
- package/packages/datadog-shimmer/src/shimmer.js +6 -2
- package/packages/dd-trace/src/aiguard/sdk.js +3 -0
- package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
- package/packages/dd-trace/src/appsec/{blocking.js → blocking/index.js} +26 -6
- package/packages/dd-trace/src/appsec/handlers/auth.js +62 -0
- package/packages/dd-trace/src/appsec/handlers/http-request.js +228 -0
- package/packages/dd-trace/src/appsec/handlers/http-response.js +78 -0
- package/packages/dd-trace/src/appsec/handlers/http-shared.js +24 -0
- package/packages/dd-trace/src/appsec/handlers/payments.js +104 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
- package/packages/dd-trace/src/appsec/index.js +48 -476
- package/packages/dd-trace/src/appsec/sdk/track_event.js +9 -0
- package/packages/dd-trace/src/carrier.js +356 -0
- package/packages/dd-trace/src/ci-visibility/coverage-report-discovery.js +46 -14
- package/packages/dd-trace/src/ci-visibility/efd-retry-policy.js +89 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +27 -13
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +17 -0
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +21 -1
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +22 -7
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -14
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +21 -5
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +1 -1
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +5 -1
- package/packages/dd-trace/src/config/generated-config-types.d.ts +5 -0
- package/packages/dd-trace/src/config/helper.js +2 -0
- package/packages/dd-trace/src/config/index.js +16 -5
- package/packages/dd-trace/src/config/remote_config.js +1 -1
- package/packages/dd-trace/src/config/supported-configurations.json +18 -0
- package/packages/dd-trace/src/datastreams/context.js +6 -1
- package/packages/dd-trace/src/datastreams/manager.js +5 -1
- package/packages/dd-trace/src/datastreams/pathway.js +23 -22
- package/packages/dd-trace/src/datastreams/processor.js +3 -2
- package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
- package/packages/dd-trace/src/encode/coverage-ci-visibility.js +0 -1
- package/packages/dd-trace/src/guardrails/apply-pm2-env.js +16 -0
- package/packages/dd-trace/src/guardrails/log.js +1 -10
- package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
- package/packages/dd-trace/src/llmobs/eval-metric.js +249 -0
- package/packages/dd-trace/src/llmobs/experiments/index.js +33 -25
- package/packages/dd-trace/src/llmobs/experiments/util.js +4 -2
- package/packages/dd-trace/src/llmobs/index.js +34 -2
- package/packages/dd-trace/src/llmobs/noop.js +2 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +5 -5
- package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +1 -1
- package/packages/dd-trace/src/llmobs/sdk.js +149 -68
- 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/telemetry.js +18 -0
- package/packages/dd-trace/src/llmobs/util.js +78 -0
- package/packages/dd-trace/src/llmobs/writers/base.js +3 -0
- package/packages/dd-trace/src/llmobs/writers/util.js +2 -0
- package/packages/dd-trace/src/log/channels.js +1 -9
- package/packages/dd-trace/src/log/levels.js +12 -0
- package/packages/dd-trace/src/openfeature/writers/exposures.js +1 -1
- package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
- package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +1 -1
- package/packages/dd-trace/src/opentelemetry/metrics/meter.js +8 -4
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -1
- package/packages/dd-trace/src/opentelemetry/span.js +4 -0
- 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/plugin_manager.js +2 -0
- package/packages/dd-trace/src/plugins/ci_plugin.js +21 -5
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/dd-trace/src/plugins/util/ci.js +6 -0
- package/packages/dd-trace/src/plugins/util/git.js +6 -4
- package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +2 -2
- package/packages/dd-trace/src/plugins/util/ip_extractor.js +1 -1
- package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +3 -62
- package/packages/dd-trace/src/plugins/util/stacktrace.js +3 -0
- package/packages/dd-trace/src/plugins/util/test.js +397 -74
- package/packages/dd-trace/src/plugins/util/url.js +1 -1
- package/packages/dd-trace/src/plugins/util/web.js +76 -3
- package/packages/dd-trace/src/profiler.js +1 -1
- package/packages/dd-trace/src/profiling/exporters/event_serializer.js +1 -1
- package/packages/dd-trace/src/profiling/profilers/events.js +2 -2
- package/packages/dd-trace/src/proxy.js +1 -0
- package/packages/dd-trace/src/ritm.js +5 -0
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +4 -2
- package/packages/dd-trace/src/sampling_rule.js +4 -2
- package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +1 -1
- package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +1 -1
- package/packages/dd-trace/src/telemetry/logs/index.js +3 -0
- package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
- package/vendor/dist/pprof-format/index.js +1 -1
- package/version.js +10 -8
- /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('
|
|
4
|
-
const log = require('
|
|
5
|
-
const web = require('
|
|
6
|
-
const
|
|
7
|
-
const
|
|
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
|
-
*
|
|
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
|
+
}
|