dd-trace 5.18.0 → 5.20.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 (75) hide show
  1. package/LICENSE-3rdparty.csv +0 -2
  2. package/ext/formats.d.ts +1 -0
  3. package/ext/formats.js +2 -1
  4. package/index.d.ts +61 -39
  5. package/init.js +3 -15
  6. package/package.json +9 -12
  7. package/packages/datadog-instrumentations/src/child_process.js +2 -2
  8. package/packages/datadog-instrumentations/src/fs.js +1 -1
  9. package/packages/datadog-instrumentations/src/hapi.js +1 -1
  10. package/packages/datadog-instrumentations/src/helpers/register.js +13 -11
  11. package/packages/datadog-instrumentations/src/http/client.js +8 -2
  12. package/packages/datadog-instrumentations/src/http/server.js +50 -13
  13. package/packages/datadog-instrumentations/src/jest.js +17 -2
  14. package/packages/datadog-instrumentations/src/kafkajs.js +1 -1
  15. package/packages/datadog-instrumentations/src/ldapjs.js +2 -2
  16. package/packages/datadog-instrumentations/src/mocha/main.js +21 -8
  17. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  18. package/packages/datadog-instrumentations/src/next.js +1 -1
  19. package/packages/datadog-instrumentations/src/pg.js +2 -2
  20. package/packages/datadog-instrumentations/src/playwright.js +46 -32
  21. package/packages/datadog-instrumentations/src/process.js +29 -0
  22. package/packages/datadog-instrumentations/src/restify.js +1 -1
  23. package/packages/datadog-instrumentations/src/vitest.js +98 -28
  24. package/packages/datadog-plugin-aws-sdk/src/base.js +16 -2
  25. package/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js +1 -1
  26. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +1 -1
  27. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +1 -1
  28. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +3 -3
  29. package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +1 -1
  30. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +6 -4
  31. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +114 -48
  32. package/packages/datadog-plugin-cypress/src/plugin.js +4 -3
  33. package/packages/datadog-plugin-fs/src/index.js +1 -1
  34. package/packages/datadog-plugin-jest/src/index.js +7 -1
  35. package/packages/datadog-plugin-kafkajs/src/producer.js +1 -1
  36. package/packages/datadog-plugin-mongodb-core/src/index.js +1 -1
  37. package/packages/datadog-plugin-openai/src/index.js +5 -5
  38. package/packages/datadog-plugin-playwright/src/index.js +4 -1
  39. package/packages/datadog-plugin-sharedb/src/index.js +1 -1
  40. package/packages/datadog-plugin-vitest/src/index.js +19 -7
  41. package/packages/dd-trace/src/analytics_sampler.js +1 -1
  42. package/packages/dd-trace/src/appsec/blocking.js +10 -1
  43. package/packages/dd-trace/src/appsec/channels.js +4 -1
  44. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
  45. package/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js +16 -0
  46. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +1 -1
  47. package/packages/dd-trace/src/appsec/iast/analyzers/weak-hash-analyzer.js +2 -0
  48. package/packages/dd-trace/src/appsec/iast/taint-tracking/csi-methods.js +2 -1
  49. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  50. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugins/kafka.js +2 -2
  51. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +11 -0
  52. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/code-injection-sensitive-analyzer.js +25 -0
  53. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +2 -0
  54. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +2 -2
  55. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +3 -1
  56. package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
  57. package/packages/dd-trace/src/appsec/index.js +15 -10
  58. package/packages/dd-trace/src/appsec/passport.js +1 -1
  59. package/packages/dd-trace/src/appsec/rasp.js +121 -7
  60. package/packages/dd-trace/src/appsec/recommended.json +220 -2
  61. package/packages/dd-trace/src/appsec/reporter.js +0 -4
  62. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +1 -1
  63. package/packages/dd-trace/src/config.js +68 -66
  64. package/packages/dd-trace/src/data_streams.js +44 -0
  65. package/packages/dd-trace/src/datastreams/pathway.js +4 -2
  66. package/packages/dd-trace/src/datastreams/processor.js +1 -1
  67. package/packages/dd-trace/src/log/index.js +32 -0
  68. package/packages/dd-trace/src/opentelemetry/span.js +1 -1
  69. package/packages/dd-trace/src/opentelemetry/tracer.js +6 -0
  70. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +43 -0
  71. package/packages/dd-trace/src/opentracing/tracer.js +10 -6
  72. package/packages/dd-trace/src/plugins/ci_plugin.js +9 -2
  73. package/packages/dd-trace/src/plugins/plugin.js +12 -1
  74. package/packages/dd-trace/src/proxy.js +1 -0
  75. package/packages/dd-trace/src/tracer.js +2 -0
@@ -28,7 +28,8 @@ const {
28
28
  TEST_SOURCE_FILE,
29
29
  TEST_IS_NEW,
30
30
  TEST_IS_RETRY,
31
- TEST_EARLY_FLAKE_ENABLED
31
+ TEST_EARLY_FLAKE_ENABLED,
32
+ NUM_FAILED_TEST_RETRIES
32
33
  } = require('../../dd-trace/src/plugins/util/test')
33
34
  const { isMarkedAsUnskippable } = require('../../datadog-plugin-jest/src/util')
34
35
  const { ORIGIN_KEY, COMPONENT } = require('../../dd-trace/src/constants')
@@ -207,10 +208,40 @@ class CypressPlugin {
207
208
  this.knownTests = []
208
209
  }
209
210
 
211
+ // Init function returns a promise that resolves with the Cypress configuration
212
+ // Depending on the received configuration, the Cypress configuration can be modified:
213
+ // for example, to enable retries for failed tests.
210
214
  init (tracer, cypressConfig) {
211
215
  this._isInit = true
212
216
  this.tracer = tracer
213
217
  this.cypressConfig = cypressConfig
218
+
219
+ this.libraryConfigurationPromise = getLibraryConfiguration(this.tracer, this.testConfiguration)
220
+ .then((libraryConfigurationResponse) => {
221
+ if (libraryConfigurationResponse.err) {
222
+ log.error(libraryConfigurationResponse.err)
223
+ } else {
224
+ const {
225
+ libraryConfig: {
226
+ isSuitesSkippingEnabled,
227
+ isCodeCoverageEnabled,
228
+ isEarlyFlakeDetectionEnabled,
229
+ earlyFlakeDetectionNumRetries,
230
+ isFlakyTestRetriesEnabled
231
+ }
232
+ } = libraryConfigurationResponse
233
+ this.isSuitesSkippingEnabled = isSuitesSkippingEnabled
234
+ this.isCodeCoverageEnabled = isCodeCoverageEnabled
235
+ this.isEarlyFlakeDetectionEnabled = isEarlyFlakeDetectionEnabled
236
+ this.earlyFlakeDetectionNumRetries = earlyFlakeDetectionNumRetries
237
+ this.isFlakyTestRetriesEnabled = isFlakyTestRetriesEnabled
238
+ if (this.isFlakyTestRetriesEnabled) {
239
+ this.cypressConfig.retries.runMode = NUM_FAILED_TEST_RETRIES
240
+ }
241
+ }
242
+ return this.cypressConfig
243
+ })
244
+ return this.libraryConfigurationPromise
214
245
  }
215
246
 
216
247
  getTestSuiteSpan (suite) {
@@ -227,7 +258,7 @@ class CypressPlugin {
227
258
  })
228
259
  }
229
260
 
230
- getTestSpan (testName, testSuite, isUnskippable, isForcedToRun) {
261
+ getTestSpan ({ testName, testSuite, isUnskippable, isForcedToRun, testSourceFile }) {
231
262
  const testSuiteTags = {
232
263
  [TEST_COMMAND]: this.command,
233
264
  [TEST_COMMAND]: this.command,
@@ -251,8 +282,11 @@ class CypressPlugin {
251
282
  ...testSpanMetadata
252
283
  } = getTestCommonTags(testName, testSuite, this.cypressConfig.version, TEST_FRAMEWORK_NAME)
253
284
 
254
- const codeOwners = getCodeOwnersForFilename(testSuite, this.codeOwnersEntries)
285
+ if (testSourceFile) {
286
+ testSpanMetadata[TEST_SOURCE_FILE] = testSourceFile
287
+ }
255
288
 
289
+ const codeOwners = this.getTestCodeOwners({ testSuite, testSourceFile })
256
290
  if (codeOwners) {
257
291
  testSpanMetadata[TEST_CODE_OWNERS] = codeOwners
258
292
  }
@@ -297,29 +331,13 @@ class CypressPlugin {
297
331
  }
298
332
 
299
333
  async beforeRun (details) {
334
+ // We need to make sure that the plugin is initialized before running the tests
335
+ // This is for the case where the user has not returned the promise from the init function
336
+ await this.libraryConfigurationPromise
300
337
  this.command = getCypressCommand(details)
301
338
  this.frameworkVersion = getCypressVersion(details)
302
339
  this.rootDir = getRootDir(details)
303
340
 
304
- const libraryConfigurationResponse = await getLibraryConfiguration(this.tracer, this.testConfiguration)
305
-
306
- if (libraryConfigurationResponse.err) {
307
- log.error(libraryConfigurationResponse.err)
308
- } else {
309
- const {
310
- libraryConfig: {
311
- isSuitesSkippingEnabled,
312
- isCodeCoverageEnabled,
313
- isEarlyFlakeDetectionEnabled,
314
- earlyFlakeDetectionNumRetries
315
- }
316
- } = libraryConfigurationResponse
317
- this.isSuitesSkippingEnabled = isSuitesSkippingEnabled
318
- this.isCodeCoverageEnabled = isCodeCoverageEnabled
319
- this.isEarlyFlakeDetectionEnabled = isEarlyFlakeDetectionEnabled
320
- this.earlyFlakeDetectionNumRetries = earlyFlakeDetectionNumRetries
321
- }
322
-
323
341
  if (this.isEarlyFlakeDetectionEnabled) {
324
342
  const knownTestsResponse = await getKnownTests(
325
343
  this.tracer,
@@ -465,12 +483,16 @@ class CypressPlugin {
465
483
  const isSkippedByItr = this.testsToSkip.find(test =>
466
484
  cypressTestName === test.name && spec.relative === test.suite
467
485
  )
468
- const skippedTestSpan = this.getTestSpan(cypressTestName, spec.relative)
486
+ let testSourceFile
487
+
469
488
  if (spec.absolute && this.repositoryRoot) {
470
- skippedTestSpan.setTag(TEST_SOURCE_FILE, getTestSuitePath(spec.absolute, this.repositoryRoot))
489
+ testSourceFile = getTestSuitePath(spec.absolute, this.repositoryRoot)
471
490
  } else {
472
- skippedTestSpan.setTag(TEST_SOURCE_FILE, spec.relative)
491
+ testSourceFile = spec.relative
473
492
  }
493
+
494
+ const skippedTestSpan = this.getTestSpan({ testName: cypressTestName, testSuite: spec.relative, testSourceFile })
495
+
474
496
  skippedTestSpan.setTag(TEST_STATUS, 'skip')
475
497
  if (isSkippedByItr) {
476
498
  skippedTestSpan.setTag(TEST_SKIPPED_BY_ITR, 'true')
@@ -485,29 +507,61 @@ class CypressPlugin {
485
507
  // This is not always the case, such as when an `after` hook fails:
486
508
  // Cypress will report the last run test as failed, but we don't know that yet at `dd:afterEach`
487
509
  let latestError
488
- finishedTests.forEach((finishedTest) => {
489
- const cypressTest = cypressTests.find(test => test.title.join(' ') === finishedTest.testName)
490
- if (!cypressTest) {
491
- return
492
- }
493
- if (cypressTest.displayError) {
494
- latestError = new Error(cypressTest.displayError)
495
- }
496
- const cypressTestStatus = CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.state]
497
- // update test status
498
- if (cypressTestStatus !== finishedTest.testStatus) {
499
- finishedTest.testSpan.setTag(TEST_STATUS, cypressTestStatus)
500
- finishedTest.testSpan.setTag('error', latestError)
501
- }
502
- if (this.itrCorrelationId) {
503
- finishedTest.testSpan.setTag(ITR_CORRELATION_ID, this.itrCorrelationId)
504
- }
505
- if (spec.absolute && this.repositoryRoot) {
506
- finishedTest.testSpan.setTag(TEST_SOURCE_FILE, getTestSuitePath(spec.absolute, this.repositoryRoot))
507
- } else {
508
- finishedTest.testSpan.setTag(TEST_SOURCE_FILE, spec.relative)
510
+
511
+ const finishedTestsByTestName = finishedTests.reduce((acc, finishedTest) => {
512
+ if (!acc[finishedTest.testName]) {
513
+ acc[finishedTest.testName] = []
509
514
  }
510
- finishedTest.testSpan.finish(finishedTest.finishTime)
515
+ acc[finishedTest.testName].push(finishedTest)
516
+ return acc
517
+ }, {})
518
+
519
+ Object.entries(finishedTestsByTestName).forEach(([testName, finishedTestAttempts]) => {
520
+ finishedTestAttempts.forEach((finishedTest, attemptIndex) => {
521
+ // TODO: there could be multiple if there have been retries!
522
+ // potentially we need to match the test status!
523
+ const cypressTest = cypressTests.find(test => test.title.join(' ') === testName)
524
+ if (!cypressTest) {
525
+ return
526
+ }
527
+ // finishedTests can include multiple tests with the same name if they have been retried
528
+ // by early flake detection. Cypress is unaware of this so .attempts does not necessarily have
529
+ // the same length as `finishedTestAttempts`
530
+ let cypressTestStatus = CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.state]
531
+ if (cypressTest.attempts && cypressTest.attempts[attemptIndex]) {
532
+ cypressTestStatus = CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.attempts[attemptIndex].state]
533
+ if (attemptIndex > 0) {
534
+ finishedTest.testSpan.setTag(TEST_IS_RETRY, 'true')
535
+ }
536
+ }
537
+ if (cypressTest.displayError) {
538
+ latestError = new Error(cypressTest.displayError)
539
+ }
540
+ // Update test status
541
+ if (cypressTestStatus !== finishedTest.testStatus) {
542
+ finishedTest.testSpan.setTag(TEST_STATUS, cypressTestStatus)
543
+ finishedTest.testSpan.setTag('error', latestError)
544
+ }
545
+ if (this.itrCorrelationId) {
546
+ finishedTest.testSpan.setTag(ITR_CORRELATION_ID, this.itrCorrelationId)
547
+ }
548
+ let testSourceFile
549
+ if (spec.absolute && this.repositoryRoot) {
550
+ testSourceFile = getTestSuitePath(spec.absolute, this.repositoryRoot)
551
+ } else {
552
+ testSourceFile = spec.relative
553
+ }
554
+ if (testSourceFile) {
555
+ finishedTest.testSpan.setTag(TEST_SOURCE_FILE, testSourceFile)
556
+ }
557
+ const codeOwners = this.getTestCodeOwners({ testSuite: spec.relative, testSourceFile })
558
+
559
+ if (codeOwners) {
560
+ finishedTest.testSpan.setTag(TEST_CODE_OWNERS, codeOwners)
561
+ }
562
+
563
+ finishedTest.testSpan.finish(finishedTest.finishTime)
564
+ })
511
565
  })
512
566
 
513
567
  if (this.testSuiteSpan) {
@@ -554,7 +608,12 @@ class CypressPlugin {
554
608
  }
555
609
 
556
610
  if (!this.activeTestSpan) {
557
- this.activeTestSpan = this.getTestSpan(testName, testSuite, isUnskippable, isForcedToRun)
611
+ this.activeTestSpan = this.getTestSpan({
612
+ testName,
613
+ testSuite,
614
+ isUnskippable,
615
+ isForcedToRun
616
+ })
558
617
  }
559
618
 
560
619
  return this.activeTestSpan ? { traceId: this.activeTestSpan.context().toTraceId() } : {}
@@ -621,6 +680,13 @@ class CypressPlugin {
621
680
  }
622
681
  }
623
682
  }
683
+
684
+ getTestCodeOwners ({ testSuite, testSourceFile }) {
685
+ if (testSourceFile) {
686
+ return getCodeOwnersForFilename(testSourceFile, this.codeOwnersEntries)
687
+ }
688
+ return getCodeOwnersForFilename(testSuite, this.codeOwnersEntries)
689
+ }
624
690
  }
625
691
 
626
692
  module.exports = new CypressPlugin()
@@ -22,13 +22,14 @@ module.exports = (on, config) => {
22
22
  // The tracer was not init correctly for whatever reason (such as invalid DD_SITE)
23
23
  if (tracer._tracer instanceof NoopTracer) {
24
24
  // We still need to register these tasks or the support file will fail
25
- return on('task', noopTask)
25
+ on('task', noopTask)
26
+ return config
26
27
  }
27
28
 
28
- cypressPlugin.init(tracer, config)
29
-
30
29
  on('before:run', cypressPlugin.beforeRun.bind(cypressPlugin))
31
30
  on('after:spec', cypressPlugin.afterSpec.bind(cypressPlugin))
32
31
  on('after:run', cypressPlugin.afterRun.bind(cypressPlugin))
33
32
  on('task', cypressPlugin.getTasks())
33
+
34
+ return cypressPlugin.init(tracer, config)
34
35
  }
@@ -29,7 +29,7 @@ class FsPlugin extends TracingPlugin {
29
29
  resource: operation,
30
30
  kind: 'internal',
31
31
  meta: {
32
- 'file.descriptor': (typeof fd === 'object' || typeof fd === 'number') ? fd.toString() : '',
32
+ 'file.descriptor': ((fd !== null && typeof fd === 'object') || typeof fd === 'number') ? fd.toString() : '',
33
33
  'file.dest': params.dest || params.newPath || (params.target && params.path),
34
34
  'file.flag': String(flag || defaultFlag || ''),
35
35
  'file.gid': gid || '',
@@ -148,6 +148,7 @@ class JestPlugin extends CiPlugin {
148
148
  config._ddIsEarlyFlakeDetectionEnabled = !!this.libraryConfig?.isEarlyFlakeDetectionEnabled
149
149
  config._ddEarlyFlakeDetectionNumRetries = this.libraryConfig?.earlyFlakeDetectionNumRetries ?? 0
150
150
  config._ddRepositoryRoot = this.repositoryRoot
151
+ config._ddIsFlakyTestRetriesEnabled = this.libraryConfig?.isFlakyTestRetriesEnabled ?? false
151
152
  })
152
153
  })
153
154
 
@@ -324,7 +325,8 @@ class JestPlugin extends CiPlugin {
324
325
  testStartLine,
325
326
  testSourceFile,
326
327
  isNew,
327
- isEfdRetry
328
+ isEfdRetry,
329
+ isJestRetry
328
330
  } = test
329
331
 
330
332
  const extraTags = {
@@ -349,6 +351,10 @@ class JestPlugin extends CiPlugin {
349
351
  }
350
352
  }
351
353
 
354
+ if (isJestRetry) {
355
+ extraTags[TEST_IS_RETRY] = 'true'
356
+ }
357
+
352
358
  return super.startTestSpan(name, suite, this.testSuiteSpan, extraTags)
353
359
  }
354
360
  }
@@ -81,7 +81,7 @@ class KafkajsProducerPlugin extends ProducerPlugin {
81
81
  span.setTag(BOOTSTRAP_SERVERS_KEY, bootstrapServers)
82
82
  }
83
83
  for (const message of messages) {
84
- if (typeof message === 'object') {
84
+ if (message !== null && typeof message === 'object') {
85
85
  this.tracer.inject(span, 'text_map', message.headers)
86
86
  if (this.config.dsmEnabled) {
87
87
  const payloadSize = getMessageSize(message)
@@ -115,7 +115,7 @@ function limitDepth (input) {
115
115
  }
116
116
 
117
117
  function isObject (val) {
118
- return typeof val === 'object' && val !== null && !(val instanceof Array)
118
+ return val !== null && typeof val === 'object' && !Array.isArray(val)
119
119
  }
120
120
 
121
121
  function isBSON (val) {
@@ -118,7 +118,7 @@ class OpenApiPlugin extends TracingPlugin {
118
118
  }
119
119
 
120
120
  // createChatCompletion, createCompletion
121
- if (typeof payload.logit_bias === 'object' && payload.logit_bias) {
121
+ if (payload.logit_bias !== null && typeof payload.logit_bias === 'object') {
122
122
  for (const [tokenId, bias] of Object.entries(payload.logit_bias)) {
123
123
  tags[`openai.request.logit_bias.${tokenId}`] = bias
124
124
  }
@@ -427,14 +427,14 @@ function createChatCompletionRequestExtraction (tags, payload, store) {
427
427
  function commonCreateImageRequestExtraction (tags, payload, store) {
428
428
  // createImageEdit, createImageVariation
429
429
  const img = payload.file || payload.image
430
- if (img && typeof img === 'object' && img.path) {
430
+ if (img !== null && typeof img === 'object' && img.path) {
431
431
  const file = path.basename(img.path)
432
432
  tags['openai.request.image'] = file
433
433
  store.file = file
434
434
  }
435
435
 
436
436
  // createImageEdit
437
- if (payload.mask && typeof payload.mask === 'object' && payload.mask.path) {
437
+ if (payload.mask !== null && typeof payload.mask === 'object' && payload.mask.path) {
438
438
  const mask = path.basename(payload.mask.path)
439
439
  tags['openai.request.mask'] = mask
440
440
  store.mask = mask
@@ -633,7 +633,7 @@ function commonCreateAudioRequestExtraction (tags, body, store) {
633
633
  tags['openai.request.response_format'] = body.response_format
634
634
  tags['openai.request.language'] = body.language
635
635
 
636
- if (body.file && typeof body.file === 'object' && body.file.path) {
636
+ if (body.file !== null && typeof body.file === 'object' && body.file.path) {
637
637
  const filename = path.basename(body.file.path)
638
638
  tags['openai.request.filename'] = filename
639
639
  store.file = filename
@@ -646,7 +646,7 @@ function commonFileRequestExtraction (tags, body) {
646
646
  // User can provider either exact file contents or a file read stream
647
647
  // With the stream we extract the filepath
648
648
  // This is a best effort attempt to extract the filename during the request
649
- if (body.file && typeof body.file === 'object' && body.file.path) {
649
+ if (body.file !== null && typeof body.file === 'object' && body.file.path) {
650
650
  tags['openai.request.filename'] = path.basename(body.file.path)
651
651
  }
652
652
  }
@@ -116,7 +116,7 @@ class PlaywrightPlugin extends CiPlugin {
116
116
 
117
117
  this.enter(span, store)
118
118
  })
119
- this.addSub('ci:playwright:test:finish', ({ testStatus, steps, error, extraTags, isNew, isEfdRetry }) => {
119
+ this.addSub('ci:playwright:test:finish', ({ testStatus, steps, error, extraTags, isNew, isEfdRetry, isRetry }) => {
120
120
  const store = storage.getStore()
121
121
  const span = store && store.span
122
122
  if (!span) return
@@ -135,6 +135,9 @@ class PlaywrightPlugin extends CiPlugin {
135
135
  span.setTag(TEST_IS_RETRY, 'true')
136
136
  }
137
137
  }
138
+ if (isRetry) {
139
+ span.setTag(TEST_IS_RETRY, 'true')
140
+ }
138
141
 
139
142
  steps.forEach(step => {
140
143
  const stepStartTime = step.startTime.getTime()
@@ -54,7 +54,7 @@ function sanitize (input) {
54
54
  }
55
55
 
56
56
  function isObject (val) {
57
- return typeof val === 'object' && val !== null && !(val instanceof Array)
57
+ return val !== null && typeof val === 'object' && !Array.isArray(val)
58
58
  }
59
59
 
60
60
  module.exports = SharedbPlugin
@@ -6,7 +6,8 @@ const {
6
6
  finishAllTraceSpans,
7
7
  getTestSuitePath,
8
8
  getTestSuiteCommonTags,
9
- TEST_SOURCE_FILE
9
+ TEST_SOURCE_FILE,
10
+ TEST_IS_RETRY
10
11
  } = require('../../dd-trace/src/plugins/util/test')
11
12
  const { COMPONENT } = require('../../dd-trace/src/constants')
12
13
 
@@ -25,16 +26,22 @@ class VitestPlugin extends CiPlugin {
25
26
 
26
27
  this.taskToFinishTime = new WeakMap()
27
28
 
28
- this.addSub('ci:vitest:test:start', ({ testName, testSuiteAbsolutePath }) => {
29
+ this.addSub('ci:vitest:test:start', ({ testName, testSuiteAbsolutePath, isRetry }) => {
29
30
  const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
30
31
  const store = storage.getStore()
32
+
33
+ const extraTags = {
34
+ [TEST_SOURCE_FILE]: testSuite
35
+ }
36
+ if (isRetry) {
37
+ extraTags[TEST_IS_RETRY] = 'true'
38
+ }
39
+
31
40
  const span = this.startTestSpan(
32
41
  testName,
33
42
  testSuite,
34
43
  this.testSuiteSpan,
35
- {
36
- [TEST_SOURCE_FILE]: testSuite
37
- }
44
+ extraTags
38
45
  )
39
46
 
40
47
  this.enter(span, store)
@@ -73,7 +80,11 @@ class VitestPlugin extends CiPlugin {
73
80
  if (error) {
74
81
  span.setTag('error', error)
75
82
  }
76
- span.finish(span._startTime + duration - MILLISECONDS_TO_SUBTRACT_FROM_FAILED_TEST_DURATION) // milliseconds
83
+ if (duration) {
84
+ span.finish(span._startTime + duration - MILLISECONDS_TO_SUBTRACT_FROM_FAILED_TEST_DURATION) // milliseconds
85
+ } else {
86
+ span.finish() // retries will not have a duration
87
+ }
77
88
  finishAllTraceSpans(span)
78
89
  }
79
90
  })
@@ -91,7 +102,8 @@ class VitestPlugin extends CiPlugin {
91
102
  ).finish()
92
103
  })
93
104
 
94
- this.addSub('ci:vitest:test-suite:start', (testSuiteAbsolutePath) => {
105
+ this.addSub('ci:vitest:test-suite:start', ({ testSuiteAbsolutePath, frameworkVersion }) => {
106
+ this.frameworkVersion = frameworkVersion
95
107
  const testSessionSpanContext = this.tracer.extract('text_map', {
96
108
  'x-datadog-trace-id': process.env.DD_CIVISIBILITY_TEST_SESSION_ID,
97
109
  'x-datadog-parent-id': process.env.DD_CIVISIBILITY_TEST_MODULE_ID
@@ -4,7 +4,7 @@ const { MEASURED } = require('../../../ext/tags')
4
4
 
5
5
  module.exports = {
6
6
  sample (span, measured, measuredByDefault) {
7
- if (typeof measured === 'object') {
7
+ if (measured !== null && typeof measured === 'object') {
8
8
  this.sample(span, measured[span.context()._name], measuredByDefault)
9
9
  } else if (measured !== undefined) {
10
10
  span.setTag(MEASURED, !!measured)
@@ -9,6 +9,8 @@ let templateHtml = blockedTemplates.html
9
9
  let templateJson = blockedTemplates.json
10
10
  let templateGraphqlJson = blockedTemplates.graphqlJson
11
11
 
12
+ const responseBlockedSet = new WeakSet()
13
+
12
14
  const specificBlockingTypes = {
13
15
  GRAPHQL: 'graphql'
14
16
  }
@@ -117,6 +119,8 @@ function block (req, res, rootSpan, abortController, actionParameters) {
117
119
 
118
120
  res.writeHead(statusCode, headers).end(body)
119
121
 
122
+ responseBlockedSet.add(res)
123
+
120
124
  abortController?.abort()
121
125
  }
122
126
 
@@ -144,11 +148,16 @@ function setTemplates (config) {
144
148
  }
145
149
  }
146
150
 
151
+ function isBlocked (res) {
152
+ return responseBlockedSet.has(res)
153
+ }
154
+
147
155
  module.exports = {
148
156
  addSpecificEndpoint,
149
157
  block,
150
158
  specificBlockingTypes,
151
159
  getBlockingData,
152
160
  getBlockingAction,
153
- setTemplates
161
+ setTemplates,
162
+ isBlocked
154
163
  }
@@ -19,5 +19,8 @@ module.exports = {
19
19
  nextQueryParsed: dc.channel('apm:next:query-parsed'),
20
20
  responseBody: dc.channel('datadog:express:response:json:start'),
21
21
  responseWriteHead: dc.channel('apm:http:server:response:writeHead:start'),
22
- httpClientRequestStart: dc.channel('apm:http:client:request:start')
22
+ httpClientRequestStart: dc.channel('apm:http:client:request:start'),
23
+ responseSetHeader: dc.channel('datadog:http:server:response:set-header:start'),
24
+ setUncaughtExceptionCaptureCallbackStart: dc.channel('datadog:process:setUncaughtExceptionCaptureCallback:start')
25
+
23
26
  }
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  module.exports = {
4
+ CODE_INJECTION_ANALYZER: require('./code-injection-analyzer'),
4
5
  COMMAND_INJECTION_ANALYZER: require('./command-injection-analyzer'),
5
6
  HARCODED_PASSWORD_ANALYZER: require('./hardcoded-password-analyzer'),
6
7
  HARCODED_SECRET_ANALYZER: require('./hardcoded-secret-analyzer'),
@@ -0,0 +1,16 @@
1
+ 'use strict'
2
+
3
+ const InjectionAnalyzer = require('./injection-analyzer')
4
+ const { CODE_INJECTION } = require('../vulnerabilities')
5
+
6
+ class CodeInjectionAnalyzer extends InjectionAnalyzer {
7
+ constructor () {
8
+ super(CODE_INJECTION)
9
+ }
10
+
11
+ onConfigure () {
12
+ this.addSub('datadog:eval:call', ({ script }) => this.analyze(script))
13
+ }
14
+ }
15
+
16
+ module.exports = new CodeInjectionAnalyzer()
@@ -13,7 +13,7 @@ const EXCLUDED_PATHS_FROM_STACK = getNodeModulesPaths('mongodb', 'mongoose', 'mq
13
13
  const MONGODB_NOSQL_SECURE_MARK = getNextSecureMark()
14
14
 
15
15
  function iterateObjectStrings (target, fn, levelKeys = [], depth = 20, visited = new Set()) {
16
- if (target && typeof target === 'object') {
16
+ if (target !== null && typeof target === 'object') {
17
17
  Object.keys(target).forEach((key) => {
18
18
  const nextLevelKeys = [...levelKeys, key]
19
19
  const val = target[key]
@@ -47,6 +47,8 @@ class WeakHashAnalyzer extends Analyzer {
47
47
  }
48
48
 
49
49
  _isExcluded (location) {
50
+ if (!location) return false
51
+
50
52
  return EXCLUDED_LOCATIONS.some(excludedLocation => {
51
53
  return location.path.includes(excludedLocation)
52
54
  })
@@ -14,7 +14,8 @@ const csiMethods = [
14
14
  { src: 'toUpperCase', dst: 'stringCase' },
15
15
  { src: 'trim' },
16
16
  { src: 'trimEnd' },
17
- { src: 'trimStart', dst: 'trim' }
17
+ { src: 'trimStart', dst: 'trim' },
18
+ { src: 'eval', allowedWithoutCallee: true }
18
19
  ]
19
20
 
20
21
  module.exports = {
@@ -45,7 +45,7 @@ class TaintTrackingPlugin extends SourceIastPlugin {
45
45
  this.addSub(
46
46
  { channelName: 'apm:express:middleware:next', tag: HTTP_REQUEST_BODY },
47
47
  ({ req }) => {
48
- if (req && req.body && typeof req.body === 'object') {
48
+ if (req && req.body !== null && typeof req.body === 'object') {
49
49
  const iastContext = getIastContext(storage.getStore())
50
50
  if (iastContext && iastContext.body !== req.body) {
51
51
  this._taintTrackingHandler(HTTP_REQUEST_BODY, req, 'body', iastContext)
@@ -63,7 +63,7 @@ class TaintTrackingPlugin extends SourceIastPlugin {
63
63
  this.addSub(
64
64
  { channelName: 'datadog:express:process_params:start', tag: HTTP_REQUEST_PATH_PARAM },
65
65
  ({ req }) => {
66
- if (req && req.params && typeof req.params === 'object') {
66
+ if (req && req.params !== null && typeof req.params === 'object') {
67
67
  this._taintTrackingHandler(HTTP_REQUEST_PATH_PARAM, req, 'params')
68
68
  }
69
69
  }
@@ -27,14 +27,14 @@ class KafkaConsumerIastPlugin extends SourceIastPlugin {
27
27
  if (iastContext && message) {
28
28
  const { key, value } = message
29
29
 
30
- if (key && typeof key === 'object') {
30
+ if (key !== null && typeof key === 'object') {
31
31
  shimmer.wrap(key, 'toString',
32
32
  toString => this.getToStringWrap(toString, iastContext, KAFKA_MESSAGE_KEY))
33
33
 
34
34
  newTaintedObject(iastContext, key, undefined, KAFKA_MESSAGE_KEY)
35
35
  }
36
36
 
37
- if (value && typeof value === 'object') {
37
+ if (value !== null && typeof value === 'object') {
38
38
  shimmer.wrap(value, 'toString',
39
39
  toString => this.getToStringWrap(toString, iastContext, KAFKA_MESSAGE_VALUE))
40
40
 
@@ -10,6 +10,7 @@ const { isDebugAllowed } = require('../telemetry/verbosity')
10
10
  const { taintObject } = require('./operations-taint-object')
11
11
 
12
12
  const mathRandomCallCh = dc.channel('datadog:random:call')
13
+ const evalCallCh = dc.channel('datadog:eval:call')
13
14
 
14
15
  const JSON_VALUE = 'json.value'
15
16
 
@@ -18,6 +19,7 @@ function noop (res) { return res }
18
19
  // Otherwise you may end up rewriting a method and not providing its rewritten implementation
19
20
  const TaintTrackingNoop = {
20
21
  concat: noop,
22
+ eval: noop,
21
23
  join: noop,
22
24
  parse: noop,
23
25
  plusOperator: noop,
@@ -136,6 +138,15 @@ function csiMethodsOverrides (getContext) {
136
138
  return res
137
139
  },
138
140
 
141
+ eval: function (res, fn, target, script) {
142
+ // eslint-disable-next-line no-eval
143
+ if (evalCallCh.hasSubscribers && fn === globalThis.eval) {
144
+ evalCallCh.publish({ script })
145
+ }
146
+
147
+ return res
148
+ },
149
+
139
150
  parse: function (res, fn, target, json) {
140
151
  if (fn === JSON.parse) {
141
152
  try {
@@ -0,0 +1,25 @@
1
+ 'use strict'
2
+
3
+ module.exports = function extractSensitiveRanges (evidence) {
4
+ const newRanges = []
5
+ if (evidence.ranges[0].start > 0) {
6
+ newRanges.push({
7
+ start: 0,
8
+ end: evidence.ranges[0].start
9
+ })
10
+ }
11
+
12
+ for (let i = 0; i < evidence.ranges.length; i++) {
13
+ const currentRange = evidence.ranges[i]
14
+ const nextRange = evidence.ranges[i + 1]
15
+
16
+ const start = currentRange.end
17
+ const end = nextRange?.start || evidence.value.length
18
+
19
+ if (start < end) {
20
+ newRanges.push({ start, end })
21
+ }
22
+ }
23
+
24
+ return newRanges
25
+ }