dd-trace 6.14.0 → 6.16.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 +15 -0
- package/ci/test-optimization-validation/framework-adapters/cucumber.js +4 -2
- package/ci/test-optimization-validation/package-check.js +4 -1
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +1 -1
- package/index.d.ts +12 -0
- package/next.d.ts +27 -0
- package/next.js +3 -0
- package/package.json +17 -13
- package/packages/datadog-esbuild/index.js +14 -1
- package/packages/datadog-esbuild/src/resolver.js +284 -0
- package/packages/datadog-esbuild/src/utils.js +163 -119
- package/packages/datadog-instrumentations/src/ai.js +39 -97
- package/packages/datadog-instrumentations/src/anthropic.js +76 -134
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +1 -1
- package/packages/datadog-instrumentations/src/azure-cosmos.js +1 -1
- package/packages/datadog-instrumentations/src/azure-durable-functions.js +23 -9
- package/packages/datadog-instrumentations/src/azure-functions.js +48 -0
- package/packages/datadog-instrumentations/src/bullmq.js +1 -1
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
- package/packages/datadog-instrumentations/src/dd-trace-api.js +2 -2
- package/packages/datadog-instrumentations/src/graphql.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/bundler-register.js +39 -14
- package/packages/datadog-instrumentations/src/helpers/instrument.js +25 -8
- package/packages/datadog-instrumentations/src/helpers/instrumentation-utils.js +72 -0
- package/packages/datadog-instrumentations/src/helpers/register.js +9 -64
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +46 -14
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/azure-durable-functions.js +17 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +1 -5
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +6 -7
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +99 -70
- package/packages/datadog-instrumentations/src/jest.js +141 -25
- package/packages/datadog-instrumentations/src/langchain.js +1 -1
- package/packages/datadog-instrumentations/src/langgraph.js +1 -1
- package/packages/datadog-instrumentations/src/mercurius.js +1 -1
- package/packages/datadog-instrumentations/src/mocha/main.js +41 -5
- package/packages/datadog-instrumentations/src/modelcontextprotocol-sdk.js +1 -1
- package/packages/datadog-instrumentations/src/openai-agents.js +1 -1
- package/packages/datadog-instrumentations/src/openai.js +54 -76
- package/packages/datadog-instrumentations/src/playwright.js +4 -0
- package/packages/datadog-instrumentations/src/vitest-worker.js +7 -10
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +7 -1
- package/packages/datadog-plugin-azure-cosmos/src/index.js +1 -1
- package/packages/datadog-plugin-azure-durable-functions/src/index.js +96 -1
- package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -24
- package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js +2 -36
- package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-request-context.js +31 -0
- package/packages/datadog-plugin-graphql/src/resolve-error.js +33 -11
- package/packages/datadog-turbopack/index.js +212 -0
- package/packages/datadog-turbopack/src/constants.js +5 -0
- package/packages/datadog-turbopack/src/loader.js +500 -0
- package/packages/dd-trace/src/aiguard/index.js +2 -1
- package/packages/dd-trace/src/aiguard/integrations/anthropic.js +53 -25
- package/packages/dd-trace/src/aiguard/integrations/evaluate.js +49 -25
- package/packages/dd-trace/src/aiguard/integrations/index.js +3 -2
- package/packages/dd-trace/src/aiguard/integrations/openai.js +59 -30
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +116 -42
- package/packages/dd-trace/src/aiguard/messages/utils.js +29 -1
- package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +25 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +3 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +531 -70
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +2 -2
- package/packages/dd-trace/src/appsec/telemetry/api_security.js +1 -3
- package/packages/dd-trace/src/appsec/telemetry/common.js +4 -0
- package/packages/dd-trace/src/appsec/telemetry/index.js +23 -5
- package/packages/dd-trace/src/appsec/telemetry/rasp.js +1 -20
- package/packages/dd-trace/src/appsec/telemetry/user.js +1 -3
- package/packages/dd-trace/src/appsec/telemetry/waf.js +1 -20
- package/packages/dd-trace/src/ci-visibility/requests/request.js +11 -0
- package/packages/dd-trace/src/ci-visibility/requests/video-request.js +4 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
- package/packages/dd-trace/src/config/helper.js +10 -2
- package/packages/dd-trace/src/config/index.js +20 -5
- package/packages/dd-trace/src/config/supported-configurations.json +10 -0
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -7
- package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +14 -1
- package/packages/dd-trace/src/debugger/devtools_client/request-options.js +1 -6
- package/packages/dd-trace/src/evp_proxy/direct.js +2 -28
- package/packages/dd-trace/src/evp_proxy/discovery.js +3 -4
- package/packages/dd-trace/src/evp_proxy/path.js +12 -3
- package/packages/dd-trace/src/exporters/agentless/writer.js +3 -1
- package/packages/dd-trace/src/exporters/common/docker.js +4 -1
- package/packages/dd-trace/src/exporters/common/proxy.js +55 -0
- package/packages/dd-trace/src/exporters/common/request.js +11 -3
- package/packages/dd-trace/src/opentelemetry/metrics/index.js +1 -1
- package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +5 -4
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +106 -25
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +12 -1
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +8 -2
- package/packages/dd-trace/src/plugin_manager.js +9 -3
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/dd-trace/src/plugins/util/test.js +4 -1
- package/packages/dd-trace/src/priority_sampler.js +4 -0
- package/packages/dd-trace/src/sampling_rule.js +3 -1
- package/packages/dd-trace/src/span_processor.js +18 -3
- package/packages/dd-trace/src/telemetry/send-data.js +5 -4
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +0 -10
|
@@ -2,84 +2,545 @@
|
|
|
2
2
|
|
|
3
3
|
const log = require('../../../../../log')
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
5
|
+
// Single-pass scanner for sensitive literals and comments in SQL evidence.
|
|
6
|
+
// Well-formed tokens keep their delimiters outside the masked range. Unterminated tokens mask the remaining evidence.
|
|
7
|
+
// Multiline Postgres dollar quotes and Oracle alternative quotes include their delimiters in the masked range.
|
|
8
|
+
|
|
9
|
+
const LINE_FEED = 0x0A
|
|
10
|
+
const CARRIAGE_RETURN = 0x0D
|
|
11
|
+
const DOUBLE_QUOTE = 0x22
|
|
12
|
+
const HASH = 0x23
|
|
13
|
+
const DOLLAR = 0x24
|
|
14
|
+
const SINGLE_QUOTE = 0x27
|
|
15
|
+
const ASTERISK = 0x2A
|
|
16
|
+
const PLUS = 0x2B
|
|
17
|
+
const MINUS = 0x2D
|
|
18
|
+
const DOT = 0x2E
|
|
19
|
+
const SLASH = 0x2F
|
|
20
|
+
const DIGIT_0 = 0x30
|
|
21
|
+
const DIGIT_9 = 0x39
|
|
22
|
+
const UPPER_B = 0x42
|
|
23
|
+
const UPPER_E = 0x45
|
|
24
|
+
const UPPER_Q = 0x51
|
|
25
|
+
const UPPER_X = 0x58
|
|
26
|
+
const BACKSLASH = 0x5C
|
|
27
|
+
const UNDERSCORE = 0x5F
|
|
28
|
+
const LOWER_A = 0x61
|
|
29
|
+
const LOWER_B = 0x62
|
|
30
|
+
const LOWER_E = 0x65
|
|
31
|
+
const LOWER_Q = 0x71
|
|
32
|
+
const LOWER_X = 0x78
|
|
33
|
+
const LOWER_Z = 0x7A
|
|
34
|
+
const LINE_SEPARATOR = 0x20_28
|
|
35
|
+
const PARAGRAPH_SEPARATOR = 0x20_29
|
|
36
|
+
|
|
37
|
+
const ASCII_CASE_BIT = 0x20
|
|
38
|
+
|
|
39
|
+
// Oracle alternative-quote bracket delimiters and their mirrors (by char code); any other char
|
|
40
|
+
// closes on itself.
|
|
41
|
+
const ORACLE_CLOSERS = new Map([[0x3C, 0x3E], [0x28, 0x29], [0x7B, 0x7D], [0x5B, 0x5D]])
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {number} code
|
|
45
|
+
*/
|
|
46
|
+
function isLineTerminator (code) {
|
|
47
|
+
return code === LINE_FEED || code === CARRIAGE_RETURN ||
|
|
48
|
+
code === LINE_SEPARATOR || code === PARAGRAPH_SEPARATOR
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Scans a half-open range for SQL line terminators.
|
|
53
|
+
* @param {string} value
|
|
54
|
+
* @param {number} from
|
|
55
|
+
* @param {number} to
|
|
56
|
+
*/
|
|
57
|
+
function hasLineTerminator (value, from, to) {
|
|
58
|
+
for (let i = from; i < to; i++) {
|
|
59
|
+
if (isLineTerminator(value.charCodeAt(i))) {
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The sticky (`y`) regex checks only `lastIndex`. It handles signs, exponents, radix literals, and decimals embedded
|
|
67
|
+
// in identifiers after `canStartNumber` and `scanPlainNumber` exclude the simpler forms.
|
|
68
|
+
const NUMERIC = /[-+]?(?:x'[\da-f]+'|0x[\da-f]+|b'[\da-f]+'|0b[\da-f]+|\d*\.\d+(?:e[-+]?\d+[fd]?)?|\b\d+(?:e[-+]?\d+[fd]?)?)/iy
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Cheap gate for the numeric regex: a numeric literal can only begin with a sign, a digit, a `.`, or the
|
|
72
|
+
* `x`/`b` radix prefixes — and the latter only when immediately followed by `'` (`x'FF'` / `b'10'`).
|
|
73
|
+
* Everything else (letters, whitespace, punctuation) skips the regex entirely.
|
|
74
|
+
* @param {string} value
|
|
75
|
+
* @param {number} index
|
|
76
|
+
* @param {number} code
|
|
77
|
+
*/
|
|
78
|
+
function canStartNumber (value, index, code) {
|
|
79
|
+
if ((code >= DIGIT_0 && code <= DIGIT_9) || code === PLUS || code === MINUS || code === DOT) {
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
if (code === LOWER_X || code === UPPER_X || code === LOWER_B || code === UPPER_B) {
|
|
83
|
+
return value.charCodeAt(index + 1) === SINGLE_QUOTE
|
|
84
|
+
}
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Treats code units outside the value as non-identifier characters.
|
|
90
|
+
* @param {number} code
|
|
91
|
+
*/
|
|
92
|
+
function isIdentifierChar (code) {
|
|
93
|
+
const folded = code | ASCII_CASE_BIT
|
|
94
|
+
return (code >= DIGIT_0 && code <= DIGIT_9) ||
|
|
95
|
+
(folded >= LOWER_A && folded <= LOWER_Z) ||
|
|
96
|
+
code === UNDERSCORE
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} value
|
|
101
|
+
* @param {number} from
|
|
102
|
+
* @param {number} length
|
|
103
|
+
*/
|
|
104
|
+
function digitRunEnd (value, from, length) {
|
|
105
|
+
let end = from
|
|
106
|
+
while (end < length) {
|
|
107
|
+
const code = value.charCodeAt(end)
|
|
108
|
+
if (code < DIGIT_0 || code > DIGIT_9) {
|
|
109
|
+
break
|
|
110
|
+
}
|
|
111
|
+
end++
|
|
112
|
+
}
|
|
113
|
+
return end
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Handles plain integer and decimal literals. Exponents and radix literals defer to `NUMERIC`.
|
|
118
|
+
* @param {string} value
|
|
119
|
+
* @param {number} intEnd Index after the integer digit run measured by the caller.
|
|
120
|
+
* @param {number} length
|
|
121
|
+
* @returns {number} Index after the literal, or `-1` to defer to the `NUMERIC` regex.
|
|
122
|
+
*/
|
|
123
|
+
function scanPlainNumber (value, intEnd, length) {
|
|
124
|
+
// A digit run touching `x`/`b` is a radix prefix (`0x`, `0b`) — let the regex own it.
|
|
125
|
+
const afterInt = value.charCodeAt(intEnd)
|
|
126
|
+
if (afterInt === LOWER_X || afterInt === UPPER_X || afterInt === LOWER_B || afterInt === UPPER_B) {
|
|
127
|
+
return -1
|
|
128
|
+
}
|
|
129
|
+
if (afterInt === DOT) {
|
|
130
|
+
const fracEnd = digitRunEnd(value, intEnd + 1, length)
|
|
131
|
+
// `\d*\.\d+`: a fractional literal needs at least one digit after the `.`. A trailing `.` with no
|
|
132
|
+
// fraction digits (`3.`) is not part of the number — fall through to the integer-only result below.
|
|
133
|
+
if (fracEnd > intEnd + 1) {
|
|
134
|
+
const afterFrac = value.charCodeAt(fracEnd)
|
|
135
|
+
return afterFrac === LOWER_E || afterFrac === UPPER_E ? -1 : fracEnd
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// `\b\d+`: a pure integer. An `e`/`E` right after it is an exponent the regex must handle.
|
|
139
|
+
return afterInt === LOWER_E || afterInt === UPPER_E ? -1 : intEnd
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// The next index that could begin a token: a numeric start (sign, digit, a `.` before a digit, or
|
|
143
|
+
// `x`/`b`/`q` before a quote), a string/comment delimiter, or a dialect literal opener. Used to skip
|
|
144
|
+
// non-token runs (identifiers, keywords, whitespace) in one native scan instead of a per-character loop.
|
|
145
|
+
// `.` is gated by `(?=\d)` because the only literal it can begin is `.5`; this keeps `a.b` column access
|
|
146
|
+
// — the most common false positive in SQL — out of the scan entirely.
|
|
147
|
+
const CANDIDATE_ANSI = /[-+\d'/]|\.(?=\d)|[xXbB](?=')/g
|
|
148
|
+
const CANDIDATE_MYSQL = /[-+\d'"/#]|\.(?=\d)|[xXbB](?=')/g
|
|
149
|
+
const CANDIDATE_SQLITE = /[-+\d'"/]|\.(?=\d)|[xXbB](?=')/g
|
|
150
|
+
const CANDIDATE_POSTGRES = /[-+\d'$/]|\.(?=\d)|[xXbB](?=')/g
|
|
151
|
+
const CANDIDATE_ORACLE = /[-+\d'/]|\.(?=\d)|[xXbBqQ](?=')/g
|
|
152
|
+
|
|
153
|
+
const BACKSLASH_QUOTES = 1 << 0
|
|
154
|
+
const CONSERVATIVE_BACKSLASH_QUOTES = 1 << 1
|
|
155
|
+
const HASH_COMMENTS = 1 << 2
|
|
156
|
+
const POSTGRES_SYNTAX = 1 << 3
|
|
157
|
+
const ORACLE_SYNTAX = 1 << 4
|
|
158
|
+
|
|
159
|
+
const ANSI_POLICY = { candidates: CANDIDATE_ANSI, features: 0 }
|
|
160
|
+
const MYSQL_POLICY = {
|
|
161
|
+
candidates: CANDIDATE_MYSQL,
|
|
162
|
+
features: BACKSLASH_QUOTES | HASH_COMMENTS,
|
|
163
|
+
}
|
|
164
|
+
const DIALECT_POLICIES = new Map([
|
|
165
|
+
['MYSQL', MYSQL_POLICY],
|
|
166
|
+
['MARIADB', MYSQL_POLICY],
|
|
167
|
+
['SQLITE', { candidates: CANDIDATE_SQLITE, features: CONSERVATIVE_BACKSLASH_QUOTES }],
|
|
168
|
+
['POSTGRES', { candidates: CANDIDATE_POSTGRES, features: POSTGRES_SYNTAX }],
|
|
169
|
+
['ORACLE', { candidates: CANDIDATE_ORACLE, features: ORACLE_SYNTAX }],
|
|
170
|
+
])
|
|
171
|
+
|
|
172
|
+
// Scanner return sentinels (distinct from any real end index, which is always > start >= 0):
|
|
173
|
+
const UNTERMINATED = -1 // literal opened but never closed -> mask raw to the end of the value
|
|
174
|
+
const FALL_THROUGH = -2 // not a literal start -> advance one character and reconsider
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @param {string} value
|
|
178
|
+
* @param {number} from
|
|
179
|
+
* @param {number} length
|
|
180
|
+
* @returns {number} Index of the first line terminator at or after `from`, else `length`.
|
|
181
|
+
*/
|
|
182
|
+
function lineEnd (value, from, length) {
|
|
183
|
+
for (let i = from; i < length; i++) {
|
|
184
|
+
if (isLineTerminator(value.charCodeAt(i))) {
|
|
185
|
+
return i
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return length
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* `'…'` or `"…"` with doubled-quote escaping. Strings may span lines.
|
|
193
|
+
* @param {string} value
|
|
194
|
+
* @param {number} start
|
|
195
|
+
* @param {number} length
|
|
196
|
+
* @param {number} quote Char code of the opening delimiter, `'` or `"`.
|
|
197
|
+
* @param {boolean} conservativeBackslash Whether an odd backslash before a closing quote makes the
|
|
198
|
+
* remainder ambiguous and therefore unterminated.
|
|
199
|
+
* @returns {number} Index after the closing quote, or `UNTERMINATED`.
|
|
200
|
+
*/
|
|
201
|
+
function scanQuotedDoubled (value, start, length, quote, conservativeBackslash) {
|
|
202
|
+
let backslashCount = 0
|
|
203
|
+
for (let i = start + 1; i < length; i++) {
|
|
204
|
+
const code = value.charCodeAt(i)
|
|
205
|
+
if (code === BACKSLASH) {
|
|
206
|
+
backslashCount++
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
if (code === quote) {
|
|
210
|
+
if (value.charCodeAt(i + 1) === quote) {
|
|
211
|
+
i++
|
|
212
|
+
backslashCount = 0
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
if (conservativeBackslash && (backslashCount & 1) === 1) {
|
|
216
|
+
return UNTERMINATED
|
|
217
|
+
}
|
|
218
|
+
return i + 1
|
|
219
|
+
}
|
|
220
|
+
backslashCount = 0
|
|
221
|
+
}
|
|
222
|
+
return UNTERMINATED
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* `'…'` or `"…"` with backslash escaping (MySQL / MariaDB / Postgres escape strings).
|
|
227
|
+
* @param {string} value
|
|
228
|
+
* @param {number} start
|
|
229
|
+
* @param {number} length
|
|
230
|
+
* @param {number} quote Char code of the opening delimiter, `'` or `"`.
|
|
231
|
+
* @param {boolean} doubled Whether two adjacent quotes escape each other.
|
|
232
|
+
* @returns {number} Index after the closing quote, or `UNTERMINATED`.
|
|
233
|
+
*/
|
|
234
|
+
function scanQuotedBackslash (value, start, length, quote, doubled) {
|
|
235
|
+
for (let i = start + 1; i < length; i++) {
|
|
236
|
+
const code = value.charCodeAt(i)
|
|
237
|
+
if (code === BACKSLASH) {
|
|
238
|
+
let backslashCount = 1
|
|
239
|
+
while (value.charCodeAt(i + 1) === BACKSLASH) {
|
|
240
|
+
i++
|
|
241
|
+
backslashCount++
|
|
242
|
+
}
|
|
243
|
+
if ((backslashCount & 1) === 1 && value.charCodeAt(i + 1) === quote) {
|
|
244
|
+
i++
|
|
245
|
+
}
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
if (code === quote) {
|
|
249
|
+
if (doubled && value.charCodeAt(i + 1) === quote) {
|
|
250
|
+
i++
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
return i + 1
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return UNTERMINATED
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @param {string} value
|
|
261
|
+
* @param {number} quoteIndex
|
|
262
|
+
*/
|
|
263
|
+
function isPostgresEscapeString (value, quoteIndex) {
|
|
264
|
+
const prefix = value.charCodeAt(quoteIndex - 1)
|
|
265
|
+
return (prefix === LOWER_E || prefix === UPPER_E) &&
|
|
266
|
+
!isPostgresIdentifierContinuation(value.charCodeAt(quoteIndex - 2))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Oracle `q'X…X'`: `X` is one of `< ( { [` (closed by its mirror) or any other char (closed by
|
|
271
|
+
* itself). The body may span line terminators.
|
|
272
|
+
* @param {string} value
|
|
273
|
+
* @param {number} start
|
|
274
|
+
* @param {number} length
|
|
275
|
+
* @returns {number} Index after the closing `X'`; `FALL_THROUGH` when `q'` is at the end of the value
|
|
276
|
+
* or followed by a line terminator — Oracle forbids whitespace as the delimiter, so that `'` is a
|
|
277
|
+
* plain string, not a quote opener; or `UNTERMINATED`.
|
|
278
|
+
*/
|
|
279
|
+
function scanOracleQuote (value, start, length) {
|
|
280
|
+
const delimiter = value.charCodeAt(start + 2)
|
|
281
|
+
if (Number.isNaN(delimiter) || isLineTerminator(delimiter)) {
|
|
282
|
+
return FALL_THROUGH
|
|
283
|
+
}
|
|
284
|
+
const closer = ORACLE_CLOSERS.get(delimiter) ?? delimiter
|
|
285
|
+
for (let i = start + 3; i < length; i++) {
|
|
286
|
+
if (value.charCodeAt(i) === closer && value.charCodeAt(i + 1) === SINGLE_QUOTE) {
|
|
287
|
+
return i + 2
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return UNTERMINATED
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Postgres `$tag$ … $tag$`, where `tag` is empty or an identifier. The body may span line
|
|
295
|
+
* terminators — PL/pgSQL function bodies routinely do, and treating a newline as the end of the
|
|
296
|
+
* literal would leave the rest of a multi-line body unredacted.
|
|
297
|
+
* @param {string} value
|
|
298
|
+
* @param {number} start
|
|
299
|
+
* @param {number} length
|
|
300
|
+
* @returns {number} Index after the closing tag; `FALL_THROUGH` when no second `$` exists; or
|
|
301
|
+
* `UNTERMINATED` when the opening tag is malformed or has no close.
|
|
302
|
+
*/
|
|
303
|
+
function scanDollarQuote (value, start, length) {
|
|
304
|
+
const tagEnd = value.indexOf('$', start + 1)
|
|
305
|
+
if (tagEnd === -1) {
|
|
306
|
+
return FALL_THROUGH
|
|
307
|
+
}
|
|
308
|
+
if (!isDollarQuoteTag(value, start + 1, tagEnd)) {
|
|
309
|
+
return UNTERMINATED
|
|
310
|
+
}
|
|
311
|
+
const tagLength = tagEnd - start + 1 // both bracket `$` included
|
|
312
|
+
// The closing tag must begin with `$`, so hop `$`-to-`$` and only compare a full tag at each one;
|
|
313
|
+
// the body characters between candidates are skipped natively by `indexOf`.
|
|
314
|
+
for (let i = value.indexOf('$', tagEnd + 1); i !== -1 && i <= length - tagLength;
|
|
315
|
+
i = value.indexOf('$', i + 1)) {
|
|
316
|
+
if (matchesTag(value, start, i, tagLength)) {
|
|
317
|
+
return i + tagLength
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return UNTERMINATED
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* A dollar-quote tag is empty (`$$`) or an identifier. The caller has already consumed the
|
|
325
|
+
* `$`-then-digit form, so a leading digit cannot reach here.
|
|
326
|
+
* @see https://www.postgresql.org/docs/current/sql-syntax-lexical.html
|
|
327
|
+
* @param {string} value
|
|
328
|
+
* @param {number} from Index of the first character after the opening `$`.
|
|
329
|
+
* @param {number} to Index of the closing `$` of the tag.
|
|
330
|
+
*/
|
|
331
|
+
function isDollarQuoteTag (value, from, to) {
|
|
332
|
+
for (let i = from; i < to; i++) {
|
|
333
|
+
const code = value.charCodeAt(i)
|
|
334
|
+
if (!isIdentifierChar(code) && code <= 0x7F) {
|
|
335
|
+
return false
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return true
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* @param {number} code
|
|
343
|
+
*/
|
|
344
|
+
function isPostgresIdentifierContinuation (code) {
|
|
345
|
+
return isIdentifierChar(code) || code === DOLLAR || code > 0x7F
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @param {number} code
|
|
350
|
+
*/
|
|
351
|
+
function isPostgresIdentifierStart (code) {
|
|
352
|
+
const folded = code | ASCII_CASE_BIT
|
|
353
|
+
return (folded >= LOWER_A && folded <= LOWER_Z) || code === UNDERSCORE || code > 0x7F
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* @param {string} value
|
|
358
|
+
* @param {number} index Index of a dollar sign after an identifier continuation character.
|
|
359
|
+
* @param {number} from Earliest unconsumed index; a prior dollar quote cannot be part of this identifier.
|
|
360
|
+
*/
|
|
361
|
+
function isInsidePostgresIdentifier (value, index, from) {
|
|
362
|
+
let start = index
|
|
363
|
+
while (start > from && isPostgresIdentifierContinuation(value.charCodeAt(start - 1))) {
|
|
364
|
+
start--
|
|
365
|
+
}
|
|
366
|
+
return isPostgresIdentifierStart(value.charCodeAt(start))
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* @param {string} value
|
|
371
|
+
* @param {number} from
|
|
372
|
+
* @param {number} length
|
|
373
|
+
*/
|
|
374
|
+
function postgresIdentifierEnd (value, from, length) {
|
|
375
|
+
let end = from
|
|
376
|
+
while (end < length && isPostgresIdentifierContinuation(value.charCodeAt(end))) {
|
|
377
|
+
end++
|
|
378
|
+
}
|
|
379
|
+
return end
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Compares the `tagLength` code units at `at` against the opening tag at `tagStart` in place, so the
|
|
384
|
+
* closing-tag check costs no `slice` allocation and skips re-reading the leading `$`.
|
|
385
|
+
* @param {string} value
|
|
386
|
+
* @param {number} tagStart
|
|
387
|
+
* @param {number} at
|
|
388
|
+
* @param {number} tagLength
|
|
389
|
+
*/
|
|
390
|
+
function matchesTag (value, tagStart, at, tagLength) {
|
|
391
|
+
for (let offset = 1; offset < tagLength - 1; offset++) {
|
|
392
|
+
if (value.charCodeAt(tagStart + offset) !== value.charCodeAt(at + offset)) {
|
|
393
|
+
return false
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return value.charCodeAt(at + tagLength - 1) === DOLLAR
|
|
397
|
+
}
|
|
41
398
|
|
|
42
399
|
module.exports = function extractSensitiveRanges (evidence) {
|
|
43
400
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
let
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
401
|
+
const value = evidence.value
|
|
402
|
+
const length = value.length
|
|
403
|
+
// Select token candidates and syntax features together so dialect aliases cannot drift.
|
|
404
|
+
const { candidates, features } = DIALECT_POLICIES.get(evidence.dialect) ?? ANSI_POLICY
|
|
405
|
+
// The greedy block comment needs the last `*/`, but most evidence has none — find it lazily on the
|
|
406
|
+
// first `/*` rather than scanning the whole value up front on every call.
|
|
407
|
+
let lastBlockClose = -2
|
|
408
|
+
let postgresIdentifierFloor = 0
|
|
409
|
+
const ranges = []
|
|
410
|
+
|
|
411
|
+
let i = 0
|
|
412
|
+
while (i < length) {
|
|
413
|
+
// Jump to the next position that could start a token; skip the non-token run natively.
|
|
414
|
+
candidates.lastIndex = i
|
|
415
|
+
const candidate = candidates.exec(value)
|
|
416
|
+
if (candidate === null) {
|
|
417
|
+
break
|
|
418
|
+
}
|
|
419
|
+
i = candidate.index
|
|
420
|
+
const code = value.charCodeAt(i)
|
|
421
|
+
|
|
422
|
+
if (canStartNumber(value, i, code)) {
|
|
423
|
+
// Measure a digit run once. At a word boundary it is a plain number or a prefix that the regex
|
|
424
|
+
// owns. Inside an identifier only the boundary-free decimal form can match; skip every other
|
|
425
|
+
// run whole instead of retrying the remaining suffix from each digit.
|
|
426
|
+
if (code >= DIGIT_0 && code <= DIGIT_9) {
|
|
427
|
+
const intEnd = digitRunEnd(value, i, length)
|
|
428
|
+
if (isIdentifierChar(value.charCodeAt(i - 1))) {
|
|
429
|
+
const afterInt = value.charCodeAt(intEnd)
|
|
430
|
+
const firstFraction = value.charCodeAt(intEnd + 1)
|
|
431
|
+
if (afterInt !== DOT || firstFraction < DIGIT_0 || firstFraction > DIGIT_9) {
|
|
432
|
+
i = intEnd
|
|
433
|
+
continue
|
|
434
|
+
}
|
|
435
|
+
} else {
|
|
436
|
+
const plainEnd = scanPlainNumber(value, intEnd, length)
|
|
437
|
+
if (plainEnd !== -1) {
|
|
438
|
+
ranges.push({ start: i, end: plainEnd })
|
|
439
|
+
i = plainEnd
|
|
440
|
+
continue
|
|
441
|
+
}
|
|
75
442
|
}
|
|
76
443
|
}
|
|
444
|
+
NUMERIC.lastIndex = i
|
|
445
|
+
if (NUMERIC.exec(value) !== null) {
|
|
446
|
+
// A sticky match starts at `i`, so `lastIndex` is the match end. Numbers are masked whole.
|
|
447
|
+
ranges.push({ start: i, end: NUMERIC.lastIndex })
|
|
448
|
+
i = NUMERIC.lastIndex
|
|
449
|
+
continue
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Dispatch by first character and record how many delimiter characters to trim from the matched token.
|
|
454
|
+
let end = FALL_THROUGH
|
|
455
|
+
let trimStart = 0
|
|
456
|
+
let trimEnd = 0
|
|
457
|
+
if (code === SINGLE_QUOTE) {
|
|
458
|
+
if ((features & BACKSLASH_QUOTES) !== 0) {
|
|
459
|
+
end = scanQuotedBackslash(value, i, length, SINGLE_QUOTE, false)
|
|
460
|
+
} else if ((features & POSTGRES_SYNTAX) !== 0 && isPostgresEscapeString(value, i)) {
|
|
461
|
+
end = scanQuotedBackslash(value, i, length, SINGLE_QUOTE, true)
|
|
462
|
+
} else {
|
|
463
|
+
end = scanQuotedDoubled(
|
|
464
|
+
value,
|
|
465
|
+
i,
|
|
466
|
+
length,
|
|
467
|
+
SINGLE_QUOTE,
|
|
468
|
+
(features & CONSERVATIVE_BACKSLASH_QUOTES) !== 0
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
trimStart = 1
|
|
472
|
+
trimEnd = 1
|
|
473
|
+
} else if (code === DOUBLE_QUOTE) {
|
|
474
|
+
end = (features & BACKSLASH_QUOTES) === 0
|
|
475
|
+
? scanQuotedDoubled(
|
|
476
|
+
value,
|
|
477
|
+
i,
|
|
478
|
+
length,
|
|
479
|
+
DOUBLE_QUOTE,
|
|
480
|
+
(features & CONSERVATIVE_BACKSLASH_QUOTES) !== 0
|
|
481
|
+
)
|
|
482
|
+
: scanQuotedBackslash(value, i, length, DOUBLE_QUOTE, false)
|
|
483
|
+
trimStart = 1
|
|
484
|
+
trimEnd = 1
|
|
485
|
+
} else if (code === DOLLAR && (features & POSTGRES_SYNTAX) !== 0) {
|
|
486
|
+
if (isPostgresIdentifierContinuation(value.charCodeAt(i - 1)) &&
|
|
487
|
+
isInsidePostgresIdentifier(value, i, postgresIdentifierFloor)) {
|
|
488
|
+
// PostgreSQL requires whitespace between an identifier and a dollar-quoted string because
|
|
489
|
+
// `$` is an identifier continuation character.
|
|
490
|
+
i = postgresIdentifierEnd(value, i, length)
|
|
491
|
+
postgresIdentifierFloor = i
|
|
492
|
+
continue
|
|
493
|
+
}
|
|
494
|
+
const parameterEnd = digitRunEnd(value, i + 1, length)
|
|
495
|
+
if (parameterEnd > i + 1) {
|
|
496
|
+
// `$1` is a parameter reference, so the index is syntax rather than data.
|
|
497
|
+
i = parameterEnd
|
|
498
|
+
postgresIdentifierFloor = i
|
|
499
|
+
continue
|
|
500
|
+
}
|
|
501
|
+
end = scanDollarQuote(value, i, length)
|
|
502
|
+
if (end >= 0 && !hasLineTerminator(value, i, end)) {
|
|
503
|
+
// `$tag$ … $tag$`: trim the opening and closing tags unless the body spans a line.
|
|
504
|
+
trimStart = value.indexOf('$', i + 1) - i + 1
|
|
505
|
+
trimEnd = trimStart
|
|
506
|
+
}
|
|
507
|
+
} else if ((code === LOWER_Q || code === UPPER_Q) && (features & ORACLE_SYNTAX) !== 0) {
|
|
508
|
+
end = scanOracleQuote(value, i, length)
|
|
509
|
+
if (end >= 0 && !hasLineTerminator(value, i, end)) {
|
|
510
|
+
trimStart = 3 // q'X
|
|
511
|
+
trimEnd = 2 // X'
|
|
512
|
+
}
|
|
513
|
+
} else if (code === MINUS && value.charCodeAt(i + 1) === MINUS) {
|
|
514
|
+
end = lineEnd(value, i + 2, length)
|
|
515
|
+
trimStart = 2 // --
|
|
516
|
+
} else if (code === HASH && (features & HASH_COMMENTS) !== 0) {
|
|
517
|
+
end = lineEnd(value, i + 1, length)
|
|
518
|
+
trimStart = 1 // #
|
|
519
|
+
} else if (code === SLASH && value.charCodeAt(i + 1) === ASTERISK) {
|
|
520
|
+
if (lastBlockClose === -2) {
|
|
521
|
+
lastBlockClose = value.lastIndexOf('*/')
|
|
522
|
+
}
|
|
523
|
+
end = lastBlockClose >= i + 2 ? lastBlockClose + 2 : UNTERMINATED
|
|
524
|
+
trimStart = 2 // /*
|
|
525
|
+
trimEnd = 2 // */
|
|
77
526
|
}
|
|
78
527
|
|
|
79
|
-
|
|
80
|
-
|
|
528
|
+
if (end === FALL_THROUGH) {
|
|
529
|
+
i++
|
|
530
|
+
} else if (end === UNTERMINATED) {
|
|
531
|
+
// Mask the unterminated literal/comment raw (delimiters included) to the end of the evidence.
|
|
532
|
+
ranges.push({ start: i, end: length })
|
|
533
|
+
break
|
|
534
|
+
} else {
|
|
535
|
+
ranges.push({ start: i + trimStart, end: end - trimEnd })
|
|
536
|
+
i = end
|
|
537
|
+
if (code === DOLLAR && (features & POSTGRES_SYNTAX) !== 0) {
|
|
538
|
+
postgresIdentifierFloor = i
|
|
539
|
+
}
|
|
540
|
+
}
|
|
81
541
|
}
|
|
82
|
-
|
|
542
|
+
|
|
543
|
+
return ranges
|
|
83
544
|
} catch (e) {
|
|
84
545
|
log.debug('[ASM] Error extracting sensitive ranges', e)
|
|
85
546
|
}
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const log = require('../../../../../log')
|
|
4
4
|
|
|
5
|
-
const AUTHORITY =
|
|
5
|
+
const AUTHORITY = String.raw`^(?:[^:]+:)?[\t\r\n]*/[\t\r\n]*/([^@]+)@`
|
|
6
6
|
// The key class excludes `?` and `#` so the greedy quantifier is bounded per fragment.
|
|
7
7
|
// Query keys cannot legitimately contain those characters (they delimit query/fragment
|
|
8
8
|
// boundaries), so excluding them preserves match semantics for valid URLs while keeping
|
|
9
9
|
// the regex linear on arbitrary input.
|
|
10
10
|
const QUERY_FRAGMENT = '[?#&]([^=&;?#]+)=([^?#&]+)'
|
|
11
|
-
const pattern = new RegExp(`${AUTHORITY}|${QUERY_FRAGMENT}`, '
|
|
11
|
+
const pattern = new RegExp(`${AUTHORITY}|${QUERY_FRAGMENT}`, 'g')
|
|
12
12
|
|
|
13
13
|
module.exports = function extractSensitiveRanges (evidence) {
|
|
14
14
|
try {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const telemetryMetrics = require('../../telemetry/metrics')
|
|
4
|
+
|
|
3
5
|
const DD_TELEMETRY_REQUEST_METRICS = Symbol('_dd.appsec.telemetry.request.metrics')
|
|
6
|
+
const appsecMetrics = telemetryMetrics.manager.namespace('appsec')
|
|
4
7
|
|
|
5
8
|
const tags = {
|
|
6
9
|
BLOCK_FAILURE: 'block_failure',
|
|
@@ -22,6 +25,7 @@ function getVersionsTags (wafVersion, rulesVersion) {
|
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
module.exports = {
|
|
28
|
+
appsecMetrics,
|
|
25
29
|
tags,
|
|
26
30
|
getVersionsTags,
|
|
27
31
|
DD_TELEMETRY_REQUEST_METRICS,
|