dd-trace 6.14.0 → 6.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/index.d.ts +8 -0
  2. package/package.json +3 -3
  3. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +1 -5
  4. package/packages/datadog-instrumentations/src/jest.js +141 -25
  5. package/packages/datadog-instrumentations/src/mocha/main.js +41 -5
  6. package/packages/datadog-instrumentations/src/playwright.js +4 -0
  7. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +3 -1
  8. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +531 -70
  9. package/packages/dd-trace/src/ci-visibility/requests/request.js +11 -0
  10. package/packages/dd-trace/src/ci-visibility/requests/video-request.js +4 -0
  11. package/packages/dd-trace/src/config/supported-configurations.json +2 -0
  12. package/packages/dd-trace/src/debugger/devtools_client/request-options.js +1 -6
  13. package/packages/dd-trace/src/evp_proxy/direct.js +2 -28
  14. package/packages/dd-trace/src/exporters/agentless/writer.js +3 -1
  15. package/packages/dd-trace/src/exporters/common/proxy.js +52 -0
  16. package/packages/dd-trace/src/exporters/common/request.js +11 -3
  17. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +5 -0
  18. package/packages/dd-trace/src/opentracing/propagation/text_map.js +8 -2
  19. package/packages/dd-trace/src/priority_sampler.js +4 -0
  20. package/packages/dd-trace/src/sampling_rule.js +3 -1
  21. package/packages/dd-trace/src/span_processor.js +18 -3
  22. package/packages/dd-trace/src/telemetry/send-data.js +5 -4
@@ -2,84 +2,545 @@
2
2
 
3
3
  const log = require('../../../../../log')
4
4
 
5
- const STRING_LITERAL = '\'(?:\'\'|[^\'])*\''
6
- const POSTGRESQL_ESCAPED_LITERAL = String.raw`\$([^$]*)\$.*?\$\1\$`
7
- const MYSQL_STRING_LITERAL = String.raw`"(?:\\"|[^"])*"|'(?:\\'|[^'])*'`
8
- const LINE_COMMENT = '--.*$'
9
- const BLOCK_COMMENT = String.raw`/\*[\s\S]*\*/`
10
- const EXPONENT = String.raw`(?:E[-+]?\d+[fd]?)?`
11
- const INTEGER_NUMBER = String.raw`\b\d+`
12
- const DECIMAL_NUMBER = String.raw`\d*\.\d+`
13
- const HEX_NUMBER = 'x\'[0-9a-f]+\'|0x[0-9a-f]+'
14
- const BIN_NUMBER = 'b\'[0-9a-f]+\'|0b[0-9a-f]+'
15
- const NUMERIC_LITERAL =
16
- `[-+]?(?:${HEX_NUMBER}|${BIN_NUMBER}|${DECIMAL_NUMBER + EXPONENT}|${INTEGER_NUMBER + EXPONENT})`
17
- const ORACLE_ESCAPED_LITERAL = String.raw`q'<.*?>'|q'\(.*?\)'|q'\{.*?\}'|q'\[.*?\]'|q'(?<ESCAPE>.).*?\k<ESCAPE>'`
18
-
19
- const patterns = {
20
- ANSI: new RegExp( // Default
21
- `${NUMERIC_LITERAL}|${STRING_LITERAL}|${LINE_COMMENT}|${BLOCK_COMMENT}`,
22
- 'gmi'
23
- ),
24
- MYSQL: new RegExp(
25
- `${NUMERIC_LITERAL}|${MYSQL_STRING_LITERAL}|${LINE_COMMENT}|${BLOCK_COMMENT}`,
26
- 'gmi'
27
- ),
28
- POSTGRES: new RegExp(
29
- `${NUMERIC_LITERAL}|${POSTGRESQL_ESCAPED_LITERAL}|${STRING_LITERAL}|${LINE_COMMENT}|${BLOCK_COMMENT}`,
30
- 'gmi'
31
- ),
32
- ORACLE: new RegExp(
33
- // The capture owns the quote delimiter while the lazy quantifier owns the body.
34
- // eslint-disable-next-line regexp/optimal-quantifier-concatenation
35
- `${NUMERIC_LITERAL}|${ORACLE_ESCAPED_LITERAL}|${STRING_LITERAL}|${LINE_COMMENT}|${BLOCK_COMMENT}`,
36
- 'gmi'
37
- ),
38
- }
39
- patterns.SQLITE = patterns.MYSQL
40
- patterns.MARIADB = patterns.MYSQL
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
- let pattern = patterns[evidence.dialect]
45
- if (!pattern) {
46
- pattern = patterns.ANSI
47
- }
48
- pattern.lastIndex = 0
49
- const tokens = []
50
-
51
- let regexResult = pattern.exec(evidence.value)
52
- while (regexResult != null) {
53
- let start = regexResult.index
54
- let end = regexResult.index + regexResult[0].length
55
- const startChar = evidence.value.charAt(start)
56
- if (startChar === '\'' || startChar === '"') {
57
- start++
58
- end--
59
- } else if (end > start + 1) {
60
- const nextChar = evidence.value.charAt(start + 1)
61
- if (startChar === '/' && nextChar === '*') {
62
- start += 2
63
- end -= 2
64
- } else if (startChar === '-' && startChar === nextChar) {
65
- start += 2
66
- } else if (startChar.toLowerCase() === 'q' && nextChar === '\'') {
67
- start += 3
68
- end -= 2
69
- } else if (startChar === '$') {
70
- const match = regexResult[0]
71
- const size = match.indexOf('$', 1) + 1
72
- if (size > 1) {
73
- start += size
74
- end -= size
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
- tokens.push({ start, end })
80
- regexResult = pattern.exec(evidence.value)
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
- return tokens
542
+
543
+ return ranges
83
544
  } catch (e) {
84
545
  log.debug('[ASM] Error extracting sensitive ranges', e)
85
546
  }
@@ -13,6 +13,7 @@ const {
13
13
  singleJitteredDelay,
14
14
  } = require('../../exporters/common/retry')
15
15
  const { parseUrl } = require('../../exporters/common/url')
16
+ const { getHttpsProxyAgent } = require('../../exporters/common/proxy')
16
17
  const { getRateLimitResetDelay } = require('./rate-limit')
17
18
 
18
19
  const legacyStorage = storage('legacy')
@@ -58,6 +59,16 @@ function request (data, options, callback) {
58
59
  opts.agent = isSecure ? httpsAgent : httpAgent
59
60
  }
60
61
 
62
+ const hasApiKey = headers['dd-api-key'] !== undefined || headers['DD-API-KEY'] !== undefined
63
+ if (hasApiKey && isSecure) {
64
+ try {
65
+ opts.agent = getHttpsProxyAgent(opts, opts.agent)
66
+ } catch (error) {
67
+ callback(error)
68
+ return
69
+ }
70
+ }
71
+
61
72
  let hasRetried = false
62
73
  let firstStatusCode = null
63
74
 
@@ -6,6 +6,7 @@ const https = require('node:https')
6
6
  const { storage } = require('../../../../datadog-core')
7
7
  const log = require('../../log')
8
8
  const { markEndpointReached } = require('../../exporters/common/retry')
9
+ const { getHttpsProxyAgent } = require('../../exporters/common/proxy')
9
10
  const { isLoopbackHost, parseUrl } = require('../../exporters/common/url')
10
11
 
11
12
  const legacyStorage = storage('legacy')
@@ -129,6 +130,9 @@ function getRequestOptions (options) {
129
130
  delete requestOptions.headers['dd-api-key']
130
131
  delete requestOptions.headers['DD-API-KEY']
131
132
  }
133
+ if (hasApiKey && requestOptions.protocol === 'https:') {
134
+ requestOptions.agent = getHttpsProxyAgent(requestOptions, requestOptions.agent)
135
+ }
132
136
 
133
137
  delete requestOptions.deadline
134
138
  delete requestOptions.retry
@@ -744,6 +744,7 @@
744
744
  "configurationNames": [
745
745
  "dogstatsd.port"
746
746
  ],
747
+ "allowed": "[1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5]",
747
748
  "default": "8125"
748
749
  }
749
750
  ],
@@ -2028,6 +2029,7 @@
2028
2029
  "configurationNames": [
2029
2030
  "port"
2030
2031
  ],
2032
+ "allowed": "[1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5]",
2031
2033
  "default": "8126"
2032
2034
  }
2033
2035
  ],