dd-trace 3.37.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 (50) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/package.json +4 -3
  3. package/packages/datadog-instrumentations/src/body-parser.js +2 -1
  4. package/packages/datadog-instrumentations/src/cucumber.js +24 -4
  5. package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +45 -0
  6. package/packages/datadog-instrumentations/src/express.js +2 -1
  7. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
  8. package/packages/datadog-instrumentations/src/jest.js +20 -11
  9. package/packages/datadog-instrumentations/src/knex.js +62 -1
  10. package/packages/datadog-instrumentations/src/mocha.js +19 -4
  11. package/packages/datadog-instrumentations/src/mongodb.js +63 -0
  12. package/packages/datadog-instrumentations/src/mongoose.js +140 -1
  13. package/packages/datadog-instrumentations/src/next.js +40 -0
  14. package/packages/datadog-instrumentations/src/playwright.js +11 -2
  15. package/packages/datadog-plugin-cucumber/src/index.js +17 -5
  16. package/packages/datadog-plugin-cypress/src/plugin.js +38 -8
  17. package/packages/datadog-plugin-jest/src/index.js +19 -4
  18. package/packages/datadog-plugin-jest/src/util.js +45 -2
  19. package/packages/datadog-plugin-memcached/src/index.js +10 -5
  20. package/packages/datadog-plugin-mocha/src/index.js +19 -6
  21. package/packages/dd-trace/src/appsec/channels.js +3 -1
  22. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
  23. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +166 -0
  24. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +21 -1
  25. package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +3 -3
  26. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +1 -2
  27. package/packages/dd-trace/src/appsec/iast/iast-plugin.js +4 -0
  28. package/packages/dd-trace/src/appsec/iast/taint-tracking/operations.js +25 -12
  29. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +4 -4
  30. package/packages/dd-trace/src/appsec/iast/taint-tracking/secure-marks-generator.js +13 -0
  31. package/packages/dd-trace/src/appsec/iast/taint-tracking/source-types.js +2 -1
  32. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/json-sensitive-analyzer.js +16 -0
  33. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
  34. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +9 -0
  35. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +13 -1
  36. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +169 -0
  37. package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
  38. package/packages/dd-trace/src/appsec/index.js +31 -13
  39. package/packages/dd-trace/src/appsec/remote_config/manager.js +11 -3
  40. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +14 -1
  41. package/packages/dd-trace/src/config.js +8 -0
  42. package/packages/dd-trace/src/format.js +3 -0
  43. package/packages/dd-trace/src/plugin_manager.js +3 -1
  44. package/packages/dd-trace/src/plugins/util/ci.js +17 -0
  45. package/packages/dd-trace/src/plugins/util/git.js +26 -4
  46. package/packages/dd-trace/src/plugins/util/test.js +16 -1
  47. package/packages/dd-trace/src/profiling/config.js +36 -5
  48. package/packages/dd-trace/src/profiling/profilers/wall.js +7 -1
  49. package/packages/dd-trace/src/service-naming/extra-services.js +24 -0
  50. package/packages/dd-trace/src/telemetry/metrics.js +0 -5
@@ -181,6 +181,15 @@ function dispatcherHook (dispatcherExport) {
181
181
  return dispatcherExport
182
182
  }
183
183
 
184
+ function getTestByTestId (dispatcher, testId) {
185
+ if (dispatcher._testById) {
186
+ return dispatcher._testById.get(testId)?.test
187
+ }
188
+ if (dispatcher._allTests) {
189
+ return dispatcher._allTests.find(({ id }) => id === testId)
190
+ }
191
+ }
192
+
184
193
  function dispatcherHookNew (dispatcherExport, runWrapper) {
185
194
  shimmer.wrap(dispatcherExport.Dispatcher.prototype, 'run', runWrapper)
186
195
  shimmer.wrap(dispatcherExport.Dispatcher.prototype, '_createWorker', createWorker => function () {
@@ -188,11 +197,11 @@ function dispatcherHookNew (dispatcherExport, runWrapper) {
188
197
  const worker = createWorker.apply(this, arguments)
189
198
 
190
199
  worker.on('testBegin', ({ testId }) => {
191
- const { test } = dispatcher._testById.get(testId)
200
+ const test = getTestByTestId(dispatcher, testId)
192
201
  testBeginHandler(test)
193
202
  })
194
203
  worker.on('testEnd', ({ testId, status, errors }) => {
195
- const { test } = dispatcher._testById.get(testId)
204
+ const test = getTestByTestId(dispatcher, testId)
196
205
 
197
206
  testEndHandler(test, STATUS_TO_TEST_STATUS[status], errors && errors[0])
198
207
  })
@@ -10,7 +10,9 @@ const {
10
10
  finishAllTraceSpans,
11
11
  getTestSuitePath,
12
12
  getTestSuiteCommonTags,
13
- addIntelligentTestRunnerSpanTags
13
+ addIntelligentTestRunnerSpanTags,
14
+ TEST_ITR_UNSKIPPABLE,
15
+ TEST_ITR_FORCED_RUN
14
16
  } = require('../../dd-trace/src/plugins/util/test')
15
17
  const { RESOURCE_NAME } = require('../../../ext/tags')
16
18
  const { COMPONENT, ERROR_MESSAGE } = require('../../dd-trace/src/constants')
@@ -29,7 +31,9 @@ class CucumberPlugin extends CiPlugin {
29
31
  status,
30
32
  isSuitesSkipped,
31
33
  numSkippedSuites,
32
- testCodeCoverageLinesTotal
34
+ testCodeCoverageLinesTotal,
35
+ hasUnskippableSuites,
36
+ hasForcedToRunSuites
33
37
  }) => {
34
38
  const { isSuitesSkippingEnabled, isCodeCoverageEnabled } = this.itrConfig || {}
35
39
  addIntelligentTestRunnerSpanTags(
@@ -41,7 +45,9 @@ class CucumberPlugin extends CiPlugin {
41
45
  isCodeCoverageEnabled,
42
46
  testCodeCoverageLinesTotal,
43
47
  skippingCount: numSkippedSuites,
44
- skippingType: 'suite'
48
+ skippingType: 'suite',
49
+ hasUnskippableSuites,
50
+ hasForcedToRunSuites
45
51
  }
46
52
  )
47
53
 
@@ -55,13 +61,19 @@ class CucumberPlugin extends CiPlugin {
55
61
  this.tracer._exporter.flush()
56
62
  })
57
63
 
58
- this.addSub('ci:cucumber:test-suite:start', (testSuiteFullPath) => {
64
+ this.addSub('ci:cucumber:test-suite:start', ({ testSuitePath, isUnskippable, isForcedToRun }) => {
59
65
  const testSuiteMetadata = getTestSuiteCommonTags(
60
66
  this.command,
61
67
  this.frameworkVersion,
62
- getTestSuitePath(testSuiteFullPath, this.sourceRoot),
68
+ testSuitePath,
63
69
  'cucumber'
64
70
  )
71
+ if (isUnskippable) {
72
+ testSuiteMetadata[TEST_ITR_UNSKIPPABLE] = 'true'
73
+ }
74
+ if (isForcedToRun) {
75
+ testSuiteMetadata[TEST_ITR_FORCED_RUN] = 'true'
76
+ }
65
77
  this.testSuiteSpan = this.tracer.startSpan('cucumber.test_suite', {
66
78
  childOf: this.testModuleSpan,
67
79
  tags: {
@@ -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
 
@@ -11,5 +11,7 @@ module.exports = {
11
11
  incomingHttpRequestEnd: dc.channel('dd-trace:incomingHttpRequestEnd'),
12
12
  passportVerify: dc.channel('datadog:passport:verify:finish'),
13
13
  queryParser: dc.channel('datadog:query:read:finish'),
14
- 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')
15
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
 
@@ -196,6 +196,10 @@ class SinkIastPlugin extends IastPlugin {
196
196
  addSub (iastPluginSub, handler) {
197
197
  return super.addSub({ tagKey: TagKey.VULNERABILITY_TYPE, ...iastPluginSub }, handler)
198
198
  }
199
+
200
+ addNotSinkSub (iastPluginSub, handler) {
201
+ return super.addSub(iastPluginSub, handler)
202
+ }
199
203
  }
200
204
 
201
205
  module.exports = {