dd-trace 3.36.0 → 3.38.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 (76) hide show
  1. package/LICENSE-3rdparty.csv +2 -0
  2. package/ext/tags.d.ts +1 -0
  3. package/ext/tags.js +1 -0
  4. package/index.d.ts +1 -0
  5. package/package.json +9 -6
  6. package/packages/datadog-esbuild/index.js +30 -25
  7. package/packages/datadog-instrumentations/src/body-parser.js +4 -3
  8. package/packages/datadog-instrumentations/src/cookie-parser.js +37 -0
  9. package/packages/datadog-instrumentations/src/cucumber.js +24 -4
  10. package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +45 -0
  11. package/packages/datadog-instrumentations/src/express.js +3 -2
  12. package/packages/datadog-instrumentations/src/graphql.js +5 -0
  13. package/packages/datadog-instrumentations/src/helpers/hooks.js +5 -1
  14. package/packages/datadog-instrumentations/src/http/server.js +1 -1
  15. package/packages/datadog-instrumentations/src/jest.js +20 -11
  16. package/packages/datadog-instrumentations/src/knex.js +62 -1
  17. package/packages/datadog-instrumentations/src/mocha.js +19 -4
  18. package/packages/datadog-instrumentations/src/mongodb.js +63 -0
  19. package/packages/datadog-instrumentations/src/mongoose.js +140 -1
  20. package/packages/datadog-instrumentations/src/next.js +62 -80
  21. package/packages/datadog-instrumentations/src/pg.js +14 -15
  22. package/packages/datadog-instrumentations/src/playwright.js +26 -5
  23. package/packages/datadog-plugin-cucumber/src/index.js +17 -5
  24. package/packages/datadog-plugin-cypress/src/plugin.js +38 -8
  25. package/packages/datadog-plugin-jest/src/index.js +19 -4
  26. package/packages/datadog-plugin-jest/src/util.js +45 -2
  27. package/packages/datadog-plugin-memcached/src/index.js +10 -5
  28. package/packages/datadog-plugin-mocha/src/index.js +19 -6
  29. package/packages/datadog-plugin-mysql/src/index.js +2 -2
  30. package/packages/datadog-plugin-next/src/index.js +14 -5
  31. package/packages/datadog-plugin-pg/src/index.js +2 -2
  32. package/packages/dd-trace/src/appsec/channels.js +4 -1
  33. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
  34. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +166 -0
  35. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +21 -1
  36. package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +3 -3
  37. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +1 -2
  38. package/packages/dd-trace/src/appsec/iast/iast-plugin.js +4 -0
  39. package/packages/dd-trace/src/appsec/iast/taint-tracking/operations.js +25 -12
  40. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +4 -4
  41. package/packages/dd-trace/src/appsec/iast/taint-tracking/secure-marks-generator.js +13 -0
  42. package/packages/dd-trace/src/appsec/iast/taint-tracking/source-types.js +2 -1
  43. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/json-sensitive-analyzer.js +16 -0
  44. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
  45. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +9 -0
  46. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +13 -1
  47. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +169 -0
  48. package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
  49. package/packages/dd-trace/src/appsec/index.js +45 -14
  50. package/packages/dd-trace/src/appsec/recommended.json +549 -24
  51. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +2 -1
  52. package/packages/dd-trace/src/appsec/remote_config/index.js +2 -0
  53. package/packages/dd-trace/src/appsec/remote_config/manager.js +11 -3
  54. package/packages/dd-trace/src/appsec/reporter.js +7 -5
  55. package/packages/dd-trace/src/appsec/telemetry.js +2 -2
  56. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +18 -5
  57. package/packages/dd-trace/src/appsec/waf/waf_manager.js +5 -4
  58. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-itr-configuration.js +1 -14
  59. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -13
  60. package/packages/dd-trace/src/config.js +8 -0
  61. package/packages/dd-trace/src/datastreams/processor.js +6 -2
  62. package/packages/dd-trace/src/format.js +9 -1
  63. package/packages/dd-trace/src/opentracing/propagation/text_map.js +2 -2
  64. package/packages/dd-trace/src/opentracing/tracer.js +0 -2
  65. package/packages/dd-trace/src/plugin_manager.js +4 -3
  66. package/packages/dd-trace/src/plugins/database.js +14 -4
  67. package/packages/dd-trace/src/plugins/index.js +1 -0
  68. package/packages/dd-trace/src/plugins/outbound.js +4 -3
  69. package/packages/dd-trace/src/plugins/util/ci.js +17 -0
  70. package/packages/dd-trace/src/plugins/util/git.js +26 -4
  71. package/packages/dd-trace/src/plugins/util/test.js +16 -1
  72. package/packages/dd-trace/src/profiling/config.js +36 -5
  73. package/packages/dd-trace/src/profiling/profilers/wall.js +7 -1
  74. package/packages/dd-trace/src/service-naming/extra-services.js +24 -0
  75. package/packages/dd-trace/src/telemetry/index.js +10 -1
  76. package/packages/dd-trace/src/telemetry/metrics.js +0 -5
@@ -21,11 +21,14 @@ const {
21
21
  getCoveredFilenamesFromCoverage,
22
22
  getTestSuitePath,
23
23
  addIntelligentTestRunnerSpanTags,
24
- TEST_SKIPPED_BY_ITR
24
+ TEST_SKIPPED_BY_ITR,
25
+ TEST_ITR_UNSKIPPABLE,
26
+ TEST_ITR_FORCED_RUN
25
27
  } = require('../../dd-trace/src/plugins/util/test')
26
28
  const { ORIGIN_KEY, COMPONENT } = require('../../dd-trace/src/constants')
27
29
  const log = require('../../dd-trace/src/log')
28
30
  const NoopTracer = require('../../dd-trace/src/noop/tracer')
31
+ const { isMarkedAsUnskippable } = require('../../datadog-plugin-jest/src/util')
29
32
 
30
33
  const TEST_FRAMEWORK_NAME = 'cypress'
31
34
 
@@ -185,8 +188,11 @@ module.exports = (on, config) => {
185
188
  let isSuitesSkippingEnabled = false
186
189
  let isCodeCoverageEnabled = false
187
190
  let testsToSkip = []
191
+ const unskippableSuites = []
192
+ let hasForcedToRunSuites = false
193
+ let hasUnskippableSuites = false
188
194
 
189
- function getTestSpan (testName, testSuite) {
195
+ function getTestSpan (testName, testSuite, isUnskippable, isForcedToRun) {
190
196
  const testSuiteTags = {
191
197
  [TEST_COMMAND]: command,
192
198
  [TEST_COMMAND]: command,
@@ -212,6 +218,16 @@ module.exports = (on, config) => {
212
218
  testSpanMetadata[TEST_CODE_OWNERS] = codeOwners
213
219
  }
214
220
 
221
+ if (isUnskippable) {
222
+ hasUnskippableSuites = true
223
+ testSpanMetadata[TEST_ITR_UNSKIPPABLE] = 'true'
224
+ }
225
+
226
+ if (isForcedToRun) {
227
+ hasForcedToRunSuites = true
228
+ testSpanMetadata[TEST_ITR_FORCED_RUN] = 'true'
229
+ }
230
+
215
231
  return tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test`, {
216
232
  childOf,
217
233
  tags: {
@@ -233,13 +249,21 @@ module.exports = (on, config) => {
233
249
  isCodeCoverageEnabled = itrConfig.isCodeCoverageEnabled
234
250
  }
235
251
 
236
- getSkippableTests(isSuitesSkippingEnabled, tracer, testConfiguration).then(({ err, skippableTests }) => {
252
+ return getSkippableTests(isSuitesSkippingEnabled, tracer, testConfiguration).then(({ err, skippableTests }) => {
237
253
  if (err) {
238
254
  log.error(err)
239
255
  } else {
240
256
  testsToSkip = skippableTests || []
241
257
  }
242
258
 
259
+ // `details.specs` are test files
260
+ details.specs.forEach(({ absolute, relative }) => {
261
+ const isUnskippableSuite = isMarkedAsUnskippable({ path: absolute })
262
+ if (isUnskippableSuite) {
263
+ unskippableSuites.push(relative)
264
+ }
265
+ })
266
+
243
267
  const childOf = getTestParentSpan(tracer)
244
268
  rootDir = getRootDir(details)
245
269
 
@@ -340,7 +364,9 @@ module.exports = (on, config) => {
340
364
  isSuitesSkippingEnabled,
341
365
  isCodeCoverageEnabled,
342
366
  skippingType: 'test',
343
- skippingCount: skippedTests.length
367
+ skippingCount: skippedTests.length,
368
+ hasForcedToRunSuites,
369
+ hasUnskippableSuites
344
370
  }
345
371
  )
346
372
 
@@ -384,17 +410,21 @@ module.exports = (on, config) => {
384
410
  },
385
411
  'dd:beforeEach': (test) => {
386
412
  const { testName, testSuite } = test
387
- // skip test
388
- if (testsToSkip.find(test => {
413
+ const shouldSkip = !!testsToSkip.find(test => {
389
414
  return testName === test.name && testSuite === test.suite
390
- })) {
415
+ })
416
+ const isUnskippable = unskippableSuites.includes(testSuite)
417
+ const isForcedToRun = shouldSkip && isUnskippable
418
+
419
+ // skip test
420
+ if (shouldSkip && !isUnskippable) {
391
421
  skippedTests.push(test)
392
422
  isTestsSkipped = true
393
423
  return { shouldSkip: true }
394
424
  }
395
425
 
396
426
  if (!activeSpan) {
397
- activeSpan = getTestSpan(testName, testSuite)
427
+ activeSpan = getTestSpan(testName, testSuite, isUnskippable, isForcedToRun)
398
428
  }
399
429
 
400
430
  return activeSpan ? { traceId: activeSpan.context().toTraceId() } : {}
@@ -10,7 +10,9 @@ const {
10
10
  TEST_PARAMETERS,
11
11
  TEST_COMMAND,
12
12
  TEST_FRAMEWORK_VERSION,
13
- TEST_SOURCE_START
13
+ TEST_SOURCE_START,
14
+ TEST_ITR_UNSKIPPABLE,
15
+ TEST_ITR_FORCED_RUN
14
16
  } = require('../../dd-trace/src/plugins/util/test')
15
17
  const { COMPONENT } = require('../../dd-trace/src/constants')
16
18
  const id = require('../../dd-trace/src/id')
@@ -50,7 +52,9 @@ class JestPlugin extends CiPlugin {
50
52
  isSuitesSkippingEnabled,
51
53
  isCodeCoverageEnabled,
52
54
  testCodeCoverageLinesTotal,
53
- numSkippedSuites
55
+ numSkippedSuites,
56
+ hasUnskippableSuites,
57
+ hasForcedToRunSuites
54
58
  }) => {
55
59
  this.testSessionSpan.setTag(TEST_STATUS, status)
56
60
  this.testModuleSpan.setTag(TEST_STATUS, status)
@@ -64,7 +68,9 @@ class JestPlugin extends CiPlugin {
64
68
  isCodeCoverageEnabled,
65
69
  testCodeCoverageLinesTotal,
66
70
  skippingType: 'suite',
67
- skippingCount: numSkippedSuites
71
+ skippingCount: numSkippedSuites,
72
+ hasUnskippableSuites,
73
+ hasForcedToRunSuites
68
74
  }
69
75
  )
70
76
 
@@ -89,7 +95,9 @@ class JestPlugin extends CiPlugin {
89
95
  const {
90
96
  _ddTestSessionId: testSessionId,
91
97
  _ddTestCommand: testCommand,
92
- _ddTestModuleId: testModuleId
98
+ _ddTestModuleId: testModuleId,
99
+ _ddForcedToRun,
100
+ _ddUnskippable
93
101
  } = testEnvironmentOptions
94
102
 
95
103
  const testSessionSpanContext = this.tracer.extract('text_map', {
@@ -99,6 +107,13 @@ class JestPlugin extends CiPlugin {
99
107
 
100
108
  const testSuiteMetadata = getTestSuiteCommonTags(testCommand, frameworkVersion, testSuite, 'jest')
101
109
 
110
+ if (_ddUnskippable) {
111
+ testSuiteMetadata[TEST_ITR_UNSKIPPABLE] = 'true'
112
+ if (_ddForcedToRun) {
113
+ testSuiteMetadata[TEST_ITR_FORCED_RUN] = 'true'
114
+ }
115
+ }
116
+
102
117
  this.testSuiteSpan = this.tracer.startSpan('jest.test_suite', {
103
118
  childOf: testSessionSpanContext,
104
119
  tags: {
@@ -1,4 +1,8 @@
1
+ const { readFileSync } = require('fs')
2
+ const { parse, extract } = require('jest-docblock')
3
+
1
4
  const { getTestSuitePath } = require('../../dd-trace/src/plugins/util/test')
5
+ const log = require('../../dd-trace/src/log')
2
6
 
3
7
  /**
4
8
  * There are two ways to call `test.each` in `jest`:
@@ -47,17 +51,56 @@ function getJestTestName (test) {
47
51
  return titles.join(' ')
48
52
  }
49
53
 
54
+ function isMarkedAsUnskippable (test) {
55
+ let docblocks
56
+
57
+ try {
58
+ const testSource = readFileSync(test.path, 'utf8')
59
+ docblocks = parse(extract(testSource))
60
+ } catch (e) {
61
+ // If we have issues parsing the file, we'll assume no unskippable was passed
62
+ return false
63
+ }
64
+
65
+ // docblocks were correctly parsed but it does not include a @datadog block
66
+ if (!docblocks?.datadog) {
67
+ return false
68
+ }
69
+
70
+ try {
71
+ return JSON.parse(docblocks.datadog).unskippable
72
+ } catch (e) {
73
+ // If the @datadog block comment is present but malformed, we'll run the suite
74
+ log.warn('@datadog block comment is malformed.')
75
+ return true
76
+ }
77
+ }
78
+
50
79
  function getJestSuitesToRun (skippableSuites, originalTests, rootDir) {
51
80
  return originalTests.reduce((acc, test) => {
52
81
  const relativePath = getTestSuitePath(test.path, rootDir)
53
82
  const shouldBeSkipped = skippableSuites.includes(relativePath)
83
+
84
+ if (isMarkedAsUnskippable(test)) {
85
+ acc.suitesToRun.push(test)
86
+ if (test?.context?.config?.testEnvironmentOptions) {
87
+ test.context.config.testEnvironmentOptions['_ddUnskippable'] = true
88
+ acc.hasUnskippableSuites = true
89
+ if (shouldBeSkipped) {
90
+ test.context.config.testEnvironmentOptions['_ddForcedToRun'] = true
91
+ acc.hasForcedToRunSuites = true
92
+ }
93
+ }
94
+ return acc
95
+ }
96
+
54
97
  if (shouldBeSkipped) {
55
98
  acc.skippedSuites.push(relativePath)
56
99
  } else {
57
100
  acc.suitesToRun.push(test)
58
101
  }
59
102
  return acc
60
- }, { skippedSuites: [], suitesToRun: [] })
103
+ }, { skippedSuites: [], suitesToRun: [], hasUnskippableSuites: false, hasForcedToRunSuites: false })
61
104
  }
62
105
 
63
- module.exports = { getFormattedJestTestParameters, getJestTestName, getJestSuitesToRun }
106
+ module.exports = { getFormattedJestTestParameters, getJestTestName, getJestSuitesToRun, isMarkedAsUnskippable }
@@ -9,15 +9,20 @@ class MemcachedPlugin extends CachePlugin {
9
9
  start ({ client, server, query }) {
10
10
  const address = getAddress(client, server, query)
11
11
 
12
+ const meta = {
13
+ 'out.host': address[0],
14
+ [CLIENT_PORT_KEY]: address[1]
15
+ }
16
+
17
+ if (this.config.memcachedCommandEnabled) {
18
+ meta['memcached.command'] = query.command
19
+ }
20
+
12
21
  this.startSpan({
13
22
  service: this.serviceName({ pluginConfig: this.config, system: this.system }),
14
23
  resource: query.type,
15
24
  type: 'memcached',
16
- meta: {
17
- 'memcached.command': query.command,
18
- 'out.host': address[0],
19
- [CLIENT_PORT_KEY]: address[1]
20
- }
25
+ meta
21
26
  })
22
27
  }
23
28
  }
@@ -11,7 +11,9 @@ const {
11
11
  getTestParametersString,
12
12
  getTestSuiteCommonTags,
13
13
  addIntelligentTestRunnerSpanTags,
14
- TEST_SOURCE_START
14
+ TEST_SOURCE_START,
15
+ TEST_ITR_UNSKIPPABLE,
16
+ TEST_ITR_FORCED_RUN
15
17
  } = require('../../dd-trace/src/plugins/util/test')
16
18
  const { COMPONENT } = require('../../dd-trace/src/constants')
17
19
 
@@ -47,14 +49,21 @@ class MochaPlugin extends CiPlugin {
47
49
  this.tracer._exporter.exportCoverage(formattedCoverage)
48
50
  })
49
51
 
50
- this.addSub('ci:mocha:test-suite:start', (suite) => {
52
+ this.addSub('ci:mocha:test-suite:start', ({ testSuite, isUnskippable, isForcedToRun }) => {
51
53
  const store = storage.getStore()
52
54
  const testSuiteMetadata = getTestSuiteCommonTags(
53
55
  this.command,
54
56
  this.frameworkVersion,
55
- getTestSuitePath(suite.file, this.sourceRoot),
57
+ getTestSuitePath(testSuite, this.sourceRoot),
56
58
  'mocha'
57
59
  )
60
+ if (isUnskippable) {
61
+ testSuiteMetadata[TEST_ITR_UNSKIPPABLE] = 'true'
62
+ }
63
+ if (isForcedToRun) {
64
+ testSuiteMetadata[TEST_ITR_FORCED_RUN] = 'true'
65
+ }
66
+
58
67
  const testSuiteSpan = this.tracer.startSpan('mocha.test_suite', {
59
68
  childOf: this.testModuleSpan,
60
69
  tags: {
@@ -64,7 +73,7 @@ class MochaPlugin extends CiPlugin {
64
73
  }
65
74
  })
66
75
  this.enter(testSuiteSpan, store)
67
- this._testSuites.set(suite.file, testSuiteSpan)
76
+ this._testSuites.set(testSuite, testSuiteSpan)
68
77
  })
69
78
 
70
79
  this.addSub('ci:mocha:test-suite:finish', (status) => {
@@ -139,7 +148,9 @@ class MochaPlugin extends CiPlugin {
139
148
  status,
140
149
  isSuitesSkipped,
141
150
  testCodeCoverageLinesTotal,
142
- numSkippedSuites
151
+ numSkippedSuites,
152
+ hasForcedToRunSuites,
153
+ hasUnskippableSuites
143
154
  }) => {
144
155
  if (this.testSessionSpan) {
145
156
  const { isSuitesSkippingEnabled, isCodeCoverageEnabled } = this.itrConfig || {}
@@ -155,7 +166,9 @@ class MochaPlugin extends CiPlugin {
155
166
  isCodeCoverageEnabled,
156
167
  testCodeCoverageLinesTotal,
157
168
  skippingCount: numSkippedSuites,
158
- skippingType: 'suite'
169
+ skippingType: 'suite',
170
+ hasForcedToRunSuites,
171
+ hasUnskippableSuites
159
172
  }
160
173
  )
161
174
 
@@ -9,7 +9,7 @@ class MySQLPlugin extends DatabasePlugin {
9
9
 
10
10
  start (payload) {
11
11
  const service = this.serviceName({ pluginConfig: this.config, dbConfig: payload.conf, system: this.system })
12
- this.startSpan(this.operationName(), {
12
+ const span = this.startSpan(this.operationName(), {
13
13
  service,
14
14
  resource: payload.sql,
15
15
  type: 'sql',
@@ -22,7 +22,7 @@ class MySQLPlugin extends DatabasePlugin {
22
22
  [CLIENT_PORT_KEY]: payload.conf.port
23
23
  }
24
24
  })
25
- payload.sql = this.injectDbmQuery(payload.sql, service)
25
+ payload.sql = this.injectDbmQuery(span, payload.sql, service)
26
26
  }
27
27
  }
28
28
 
@@ -50,7 +50,6 @@ class NextPlugin extends ServerPlugin {
50
50
 
51
51
  const span = store.span
52
52
  const error = span.context()._tags['error']
53
- const page = span.context()._tags['next.page']
54
53
 
55
54
  if (!this.config.validateStatus(res.statusCode) && !error) {
56
55
  span.setTag('error', true)
@@ -60,14 +59,12 @@ class NextPlugin extends ServerPlugin {
60
59
  'http.status_code': res.statusCode
61
60
  })
62
61
 
63
- if (page) web.setRoute(req, page)
64
-
65
62
  this.config.hooks.request(span, req, res)
66
63
 
67
64
  span.finish()
68
65
  }
69
66
 
70
- pageLoad ({ page }) {
67
+ pageLoad ({ page, isAppPath }) {
71
68
  const store = storage.getStore()
72
69
 
73
70
  if (!store) return
@@ -77,15 +74,27 @@ class NextPlugin extends ServerPlugin {
77
74
 
78
75
  // Only use error page names if there's not already a name
79
76
  const current = span.context()._tags['next.page']
80
- if (current && (page === '/404' || page === '/500' || page === '/_error')) {
77
+ if (current && ['/404', '/500', '/_error', '/_not-found'].includes(page)) {
81
78
  return
82
79
  }
83
80
 
81
+ // remove ending /route or /page for appDir projects
82
+ if (isAppPath) page = page.substring(0, page.lastIndexOf('/'))
83
+
84
+ // This is for static files whose 'page' includes the whole file path
85
+ // For normal page matches, like /api/hello/[name] and a req.url like /api/hello/world,
86
+ // nothing should happen
87
+ // For page matches like /User/something/public/text.txt and req.url like /text.txt,
88
+ // it should disregard the extra absolute path Next.js sometimes sets
89
+ if (page.includes(req.url)) page = req.url
90
+
84
91
  span.addTags({
85
92
  [COMPONENT]: this.constructor.id,
86
93
  'resource.name': `${req.method} ${page}`.trim(),
87
94
  'next.page': page
88
95
  })
96
+
97
+ web.setRoute(req, page)
89
98
  }
90
99
 
91
100
  configure (config) {
@@ -12,7 +12,7 @@ class PGPlugin extends DatabasePlugin {
12
12
  const service = this.serviceName({ pluginConfig: this.config, params })
13
13
  const originalStatement = this.maybeTruncate(query.text)
14
14
 
15
- this.startSpan(this.operationName(), {
15
+ const span = this.startSpan(this.operationName(), {
16
16
  service,
17
17
  resource: originalStatement,
18
18
  type: 'sql',
@@ -27,7 +27,7 @@ class PGPlugin extends DatabasePlugin {
27
27
  }
28
28
  })
29
29
 
30
- query.__ddInjectableQuery = this.injectDbmQuery(query.text, service, !!query.name)
30
+ query.__ddInjectableQuery = this.injectDbmQuery(span, query.text, service, !!query.name)
31
31
  }
32
32
  }
33
33
 
@@ -5,10 +5,13 @@ const dc = require('../../../diagnostics_channel')
5
5
  // TODO: use TBD naming convention
6
6
  module.exports = {
7
7
  bodyParser: dc.channel('datadog:body-parser:read:finish'),
8
+ cookieParser: dc.channel('datadog:cookie-parser:read:finish'),
8
9
  graphqlFinishExecute: dc.channel('apm:graphql:execute:finish'),
9
10
  incomingHttpRequestStart: dc.channel('dd-trace:incomingHttpRequestStart'),
10
11
  incomingHttpRequestEnd: dc.channel('dd-trace:incomingHttpRequestEnd'),
11
12
  passportVerify: dc.channel('datadog:passport:verify:finish'),
12
13
  queryParser: dc.channel('datadog:query:read:finish'),
13
- setCookieChannel: dc.channel('datadog:iast:set-cookie')
14
+ setCookieChannel: dc.channel('datadog:iast:set-cookie'),
15
+ nextBodyParsed: dc.channel('apm:next:body-parsed'),
16
+ nextQueryParsed: dc.channel('apm:next:query-parsed')
14
17
  }
@@ -7,6 +7,7 @@ module.exports = {
7
7
  'LDAP_ANALYZER': require('./ldap-injection-analyzer'),
8
8
  'NO_HTTPONLY_COOKIE_ANALYZER': require('./no-httponly-cookie-analyzer'),
9
9
  'NO_SAMESITE_COOKIE_ANALYZER': require('./no-samesite-cookie-analyzer'),
10
+ 'NOSQL_MONGODB_INJECTION': require('./nosql-injection-mongodb-analyzer'),
10
11
  'PATH_TRAVERSAL_ANALYZER': require('./path-traversal-analyzer'),
11
12
  'SQL_INJECTION_ANALYZER': require('./sql-injection-analyzer'),
12
13
  'SSRF': require('./ssrf-analyzer'),
@@ -0,0 +1,166 @@
1
+ 'use strict'
2
+
3
+ const InjectionAnalyzer = require('./injection-analyzer')
4
+ const { NOSQL_MONGODB_INJECTION } = require('../vulnerabilities')
5
+ const { getRanges, addSecureMark } = require('../taint-tracking/operations')
6
+ const { getNodeModulesPaths } = require('../path-line')
7
+ const { getNextSecureMark } = require('../taint-tracking/secure-marks-generator')
8
+ const { storage } = require('../../../../../datadog-core')
9
+ const { getIastContext } = require('../iast-context')
10
+
11
+ const EXCLUDED_PATHS_FROM_STACK = getNodeModulesPaths('mongodb', 'mongoose')
12
+ const MONGODB_NOSQL_SECURE_MARK = getNextSecureMark()
13
+
14
+ function iterateObjectStrings (target, fn, levelKeys = [], depth = 50, visited = new Set()) {
15
+ if (target && typeof target === 'object') {
16
+ Object.keys(target).forEach((key) => {
17
+ const nextLevelKeys = [...levelKeys, key]
18
+ const val = target[key]
19
+
20
+ if (typeof val === 'string') {
21
+ fn(val, nextLevelKeys, target, key)
22
+ } else if (depth > 0 && !visited.has(val)) {
23
+ iterateObjectStrings(val, fn, nextLevelKeys, depth - 1, visited)
24
+ visited.add(val)
25
+ }
26
+ })
27
+ }
28
+ }
29
+
30
+ class NosqlInjectionMongodbAnalyzer extends InjectionAnalyzer {
31
+ constructor () {
32
+ super(NOSQL_MONGODB_INJECTION)
33
+ this.sanitizedObjects = new WeakSet()
34
+ }
35
+
36
+ onConfigure () {
37
+ this.configureSanitizers()
38
+
39
+ this.addSub('datadog:mongodb:collection:filter:start', ({ filters }) => {
40
+ const store = storage.getStore()
41
+ if (store && !store.nosqlAnalyzed && filters?.length) {
42
+ filters.forEach(filter => {
43
+ this.analyze({ filter }, store)
44
+ })
45
+ }
46
+ })
47
+
48
+ this.addSub('datadog:mongoose:model:filter:start', ({ filters }) => {
49
+ const store = storage.getStore()
50
+ if (!store) return
51
+
52
+ if (filters?.length) {
53
+ filters.forEach(filter => {
54
+ this.analyze({ filter }, store)
55
+ })
56
+ }
57
+
58
+ storage.enterWith({ ...store, nosqlAnalyzed: true, mongooseParentStore: store })
59
+ })
60
+
61
+ this.addSub('datadog:mongoose:model:filter:finish', () => {
62
+ const store = storage.getStore()
63
+ if (store?.mongooseParentStore) {
64
+ storage.enterWith(store.mongooseParentStore)
65
+ }
66
+ })
67
+ }
68
+
69
+ configureSanitizers () {
70
+ this.addNotSinkSub('datadog:express-mongo-sanitize:filter:finish', ({ sanitizedProperties, req }) => {
71
+ const store = storage.getStore()
72
+ const iastContext = getIastContext(store)
73
+
74
+ if (iastContext) { // do nothing if we are not in an iast request
75
+ sanitizedProperties.forEach(key => {
76
+ iterateObjectStrings(req[key], function (value, levelKeys) {
77
+ if (typeof value === 'string') {
78
+ let parentObj = req[key]
79
+ const levelsLength = levelKeys.length
80
+
81
+ for (let i = 0; i < levelsLength; i++) {
82
+ const currentLevelKey = levelKeys[i]
83
+
84
+ if (i === levelsLength - 1) {
85
+ parentObj[currentLevelKey] = addSecureMark(iastContext, value, MONGODB_NOSQL_SECURE_MARK)
86
+ } else {
87
+ parentObj = parentObj[currentLevelKey]
88
+ }
89
+ }
90
+ }
91
+ })
92
+ })
93
+ }
94
+ })
95
+
96
+ this.addNotSinkSub('datadog:express-mongo-sanitize:sanitize:finish', ({ sanitizedObject }) => {
97
+ const store = storage.getStore()
98
+ const iastContext = getIastContext(store)
99
+
100
+ if (iastContext) { // do nothing if we are not in an iast request
101
+ iterateObjectStrings(sanitizedObject, function (value, levelKeys, parent, lastKey) {
102
+ try {
103
+ parent[lastKey] = addSecureMark(iastContext, value, MONGODB_NOSQL_SECURE_MARK)
104
+ } catch {
105
+ // if it is a readonly property, do nothing
106
+ }
107
+ })
108
+ }
109
+ })
110
+
111
+ this.addNotSinkSub('datadog:mongoose:sanitize-filter:finish', ({ sanitizedObject }) => {
112
+ this.sanitizedObjects.add(sanitizedObject)
113
+ })
114
+ }
115
+
116
+ _isVulnerable (value, iastContext) {
117
+ if (value?.filter && iastContext) {
118
+ let isVulnerable = false
119
+
120
+ if (this.sanitizedObjects.has(value.filter)) {
121
+ return false
122
+ }
123
+
124
+ const rangesByKey = {}
125
+ const allRanges = []
126
+
127
+ iterateObjectStrings(value.filter, function (val, nextLevelKeys) {
128
+ const ranges = getRanges(iastContext, val)
129
+ if (ranges?.length) {
130
+ const filteredRanges = []
131
+
132
+ for (const range of ranges) {
133
+ if ((range.secureMarks & MONGODB_NOSQL_SECURE_MARK) !== MONGODB_NOSQL_SECURE_MARK) {
134
+ isVulnerable = true
135
+ filteredRanges.push(range)
136
+ }
137
+ }
138
+
139
+ if (filteredRanges.length > 0) {
140
+ rangesByKey[nextLevelKeys.join('.')] = filteredRanges
141
+ allRanges.push(...filteredRanges)
142
+ }
143
+ }
144
+ }, [], 4)
145
+
146
+ if (isVulnerable) {
147
+ value.rangesToApply = rangesByKey
148
+ value.ranges = allRanges
149
+ }
150
+
151
+ return isVulnerable
152
+ }
153
+ return false
154
+ }
155
+
156
+ _getEvidence (value, iastContext) {
157
+ return { value: value.filter, rangesToApply: value.rangesToApply, ranges: value.ranges }
158
+ }
159
+
160
+ _getExcludedPaths () {
161
+ return EXCLUDED_PATHS_FROM_STACK
162
+ }
163
+ }
164
+
165
+ module.exports = new NosqlInjectionMongodbAnalyzer()
166
+ module.exports.MONGODB_NOSQL_SECURE_MARK = MONGODB_NOSQL_SECURE_MARK
@@ -8,7 +8,7 @@ const { getIastContext } = require('../iast-context')
8
8
  const { addVulnerability } = require('../vulnerability-reporter')
9
9
  const { getNodeModulesPaths } = require('../path-line')
10
10
 
11
- const EXCLUDED_PATHS = getNodeModulesPaths('mysql', 'mysql2', 'sequelize', 'pg-pool')
11
+ const EXCLUDED_PATHS = getNodeModulesPaths('mysql', 'mysql2', 'sequelize', 'pg-pool', 'knex')
12
12
 
13
13
  class SqlInjectionAnalyzer extends InjectionAnalyzer {
14
14
  constructor () {
@@ -31,6 +31,12 @@ class SqlInjectionAnalyzer extends InjectionAnalyzer {
31
31
 
32
32
  this.addSub('datadog:mysql:pool:query:start', ({ sql }) => this.getStoreAndAnalyze(sql, 'MYSQL'))
33
33
  this.addSub('datadog:mysql:pool:query:finish', () => this.returnToParentStore())
34
+
35
+ this.addSub('datadog:knex:raw:start', ({ sql, dialect: knexDialect }) => {
36
+ const dialect = this.normalizeKnexDialect(knexDialect)
37
+ this.getStoreAndAnalyze(sql, dialect)
38
+ })
39
+ this.addSub('datadog:knex:raw:finish', () => this.returnToParentStore())
34
40
  }
35
41
 
36
42
  getStoreAndAnalyze (query, dialect) {
@@ -83,6 +89,20 @@ class SqlInjectionAnalyzer extends InjectionAnalyzer {
83
89
  _getExcludedPaths () {
84
90
  return EXCLUDED_PATHS
85
91
  }
92
+
93
+ normalizeKnexDialect (knexDialect) {
94
+ if (knexDialect === 'postgresql') {
95
+ return 'POSTGRES'
96
+ }
97
+
98
+ if (knexDialect === 'sqlite3') {
99
+ return 'SQLITE'
100
+ }
101
+
102
+ if (typeof knexDialect === 'string') {
103
+ return knexDialect.toUpperCase()
104
+ }
105
+ }
86
106
  }
87
107
 
88
108
  module.exports = new SqlInjectionAnalyzer()
@@ -6,8 +6,8 @@ const { getNodeModulesPaths } = require('../path-line')
6
6
  const { getRanges } = require('../taint-tracking/operations')
7
7
  const {
8
8
  HTTP_REQUEST_HEADER_VALUE,
9
- HTTP_REQUEST_PATH,
10
- HTTP_REQUEST_PATH_PARAM
9
+ HTTP_REQUEST_PATH_PARAM,
10
+ HTTP_REQUEST_URI
11
11
  } = require('../taint-tracking/source-types')
12
12
 
13
13
  const EXCLUDED_PATHS = getNodeModulesPaths('express/lib/response.js')
@@ -56,7 +56,7 @@ class UnvalidatedRedirectAnalyzer extends InjectionAnalyzer {
56
56
  }
57
57
 
58
58
  _isUrl (range) {
59
- return range.iinfo.type === HTTP_REQUEST_PATH
59
+ return range.iinfo.type === HTTP_REQUEST_URI
60
60
  }
61
61
 
62
62
  _getExcludedPaths () {
@@ -71,8 +71,7 @@ class Analyzer extends SinkIastPlugin {
71
71
  return store && !iastContext
72
72
  }
73
73
 
74
- analyze (value) {
75
- const store = storage.getStore()
74
+ analyze (value, store = storage.getStore()) {
76
75
  const iastContext = getIastContext(store)
77
76
  if (this._isInvalidContext(store, iastContext)) return
78
77