dd-trace 3.1.0 → 3.3.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 (61) hide show
  1. package/LICENSE-3rdparty.csv +2 -0
  2. package/README.md +4 -0
  3. package/ext/tags.d.ts +2 -1
  4. package/ext/tags.js +2 -1
  5. package/index.d.ts +43 -20
  6. package/package.json +6 -4
  7. package/packages/datadog-instrumentations/src/crypto.js +32 -0
  8. package/packages/datadog-instrumentations/src/grpc/server.js +15 -7
  9. package/packages/datadog-instrumentations/src/helpers/hooks.js +4 -0
  10. package/packages/datadog-instrumentations/src/http/server.js +1 -1
  11. package/packages/datadog-instrumentations/src/jest.js +136 -14
  12. package/packages/datadog-instrumentations/src/mocha.js +77 -31
  13. package/packages/datadog-instrumentations/src/net.js +13 -0
  14. package/packages/datadog-instrumentations/src/next.js +7 -3
  15. package/packages/datadog-plugin-jest/src/index.js +106 -6
  16. package/packages/datadog-plugin-mocha/src/index.js +15 -7
  17. package/packages/datadog-plugin-mongodb-core/src/index.js +19 -10
  18. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +4 -0
  19. package/packages/dd-trace/src/appsec/iast/analyzers/index.js +20 -0
  20. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +48 -0
  21. package/packages/dd-trace/src/appsec/iast/analyzers/weak-cipher-analyzer.js +27 -0
  22. package/packages/dd-trace/src/appsec/iast/analyzers/weak-hash-analyzer.js +24 -0
  23. package/packages/dd-trace/src/appsec/iast/iast-context.js +50 -0
  24. package/packages/dd-trace/src/appsec/iast/index.js +59 -0
  25. package/packages/dd-trace/src/appsec/iast/overhead-controller.js +94 -0
  26. package/packages/dd-trace/src/appsec/iast/path-line.js +70 -0
  27. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +121 -0
  28. package/packages/dd-trace/src/appsec/recommended.json +1144 -275
  29. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +1 -1
  30. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -3
  31. package/packages/dd-trace/src/config.js +90 -10
  32. package/packages/dd-trace/src/constants.js +9 -1
  33. package/packages/dd-trace/src/encode/0.4.js +7 -1
  34. package/packages/dd-trace/src/encode/0.5.js +7 -1
  35. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +2 -2
  36. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +32 -20
  37. package/packages/dd-trace/src/encode/span-stats.js +155 -0
  38. package/packages/dd-trace/src/exporters/agent/index.js +14 -2
  39. package/packages/dd-trace/src/exporters/agent/writer.js +6 -3
  40. package/packages/dd-trace/src/exporters/common/request.js +9 -5
  41. package/packages/dd-trace/src/exporters/span-stats/index.js +20 -0
  42. package/packages/dd-trace/src/exporters/span-stats/writer.js +54 -0
  43. package/packages/dd-trace/src/format.js +2 -0
  44. package/packages/dd-trace/src/iitm.js +11 -0
  45. package/packages/dd-trace/src/opentracing/propagation/text_map.js +71 -0
  46. package/packages/dd-trace/src/opentracing/tracer.js +1 -1
  47. package/packages/dd-trace/src/plugin_manager.js +12 -2
  48. package/packages/dd-trace/src/plugins/index.js +3 -0
  49. package/packages/dd-trace/src/plugins/log_plugin.js +16 -9
  50. package/packages/dd-trace/src/plugins/util/ip_blocklist.js +51 -0
  51. package/packages/dd-trace/src/plugins/util/web.js +100 -2
  52. package/packages/dd-trace/src/priority_sampler.js +36 -1
  53. package/packages/dd-trace/src/proxy.js +3 -0
  54. package/packages/dd-trace/src/ritm.js +10 -1
  55. package/packages/dd-trace/src/span_processor.js +7 -1
  56. package/packages/dd-trace/src/span_stats.js +210 -0
  57. package/packages/dd-trace/src/telemetry/dependencies.js +83 -0
  58. package/packages/dd-trace/src/{telemetry.js → telemetry/index.js} +10 -65
  59. package/packages/dd-trace/src/telemetry/send-data.js +35 -0
  60. package/packages/dd-trace/src/plugins/util/redis.js +0 -74
  61. package/packages/dd-trace/src/plugins/util/tx.js +0 -75
@@ -19,7 +19,31 @@ const patched = new WeakSet()
19
19
 
20
20
  const testToAr = new WeakMap()
21
21
  const originalFns = new WeakMap()
22
- const testSuiteToAr = new WeakMap()
22
+ const testFileToSuiteAr = new Map()
23
+
24
+ function getSuitesByTestFile (root) {
25
+ const suitesByTestFile = {}
26
+ function getSuites (suite) {
27
+ if (suite.file) {
28
+ if (suitesByTestFile[suite.file]) {
29
+ suitesByTestFile[suite.file].push(suite)
30
+ } else {
31
+ suitesByTestFile[suite.file] = [suite]
32
+ }
33
+ }
34
+ suite.suites.forEach(suite => {
35
+ getSuites(suite)
36
+ })
37
+ }
38
+ getSuites(root)
39
+
40
+ const numSuitesByTestFile = Object.keys(suitesByTestFile).reduce((acc, testFile) => {
41
+ acc[testFile] = suitesByTestFile[testFile].length
42
+ return acc
43
+ }, {})
44
+
45
+ return { suitesByTestFile, numSuitesByTestFile }
46
+ }
23
47
 
24
48
  function getTestStatus (test) {
25
49
  if (test.pending) {
@@ -56,6 +80,8 @@ function mochaHook (Runner) {
56
80
  return run.apply(this, arguments)
57
81
  }
58
82
 
83
+ const { suitesByTestFile, numSuitesByTestFile } = getSuitesByTestFile(this.suite)
84
+
59
85
  const testRunAsyncResource = new AsyncResource('bound-anonymous-fn')
60
86
 
61
87
  this.once('end', testRunAsyncResource.bind(function () {
@@ -65,6 +91,7 @@ function mochaHook (Runner) {
65
91
  } else if (this.failures !== 0) {
66
92
  status = 'fail'
67
93
  }
94
+ testFileToSuiteAr.clear()
68
95
  testRunFinishCh.publish(status)
69
96
  }))
70
97
 
@@ -75,34 +102,45 @@ function mochaHook (Runner) {
75
102
  }))
76
103
 
77
104
  this.on('suite', function (suite) {
78
- if (suite.root) {
105
+ if (suite.root || !suite.tests.length) {
79
106
  return
80
107
  }
81
- const asyncResource = new AsyncResource('bound-anonymous-fn')
82
-
83
- testSuiteToAr.set(suite, asyncResource)
84
-
85
- asyncResource.runInAsyncScope(() => {
86
- testSuiteStartCh.publish(suite)
87
- })
108
+ let asyncResource = testFileToSuiteAr.get(suite.file)
109
+ if (!asyncResource) {
110
+ asyncResource = new AsyncResource('bound-anonymous-fn')
111
+ testFileToSuiteAr.set(suite.file, asyncResource)
112
+ asyncResource.runInAsyncScope(() => {
113
+ testSuiteStartCh.publish(suite)
114
+ })
115
+ }
88
116
  })
89
117
 
90
118
  this.on('suite end', function (suite) {
91
119
  if (suite.root) {
92
120
  return
93
121
  }
122
+ const suitesInTestFile = suitesByTestFile[suite.file]
123
+
124
+ const isLastSuite = --numSuitesByTestFile[suite.file] === 0
125
+ if (!isLastSuite) {
126
+ return
127
+ }
128
+
94
129
  let status = 'pass'
95
- if (suite.pending) {
130
+ if (suitesInTestFile.every(suite => suite.pending)) {
96
131
  status = 'skip'
97
132
  } else {
98
- suite.eachTest(test => {
99
- if (test.state === 'failed' || test.timedOut) {
100
- status = 'fail'
101
- }
133
+ // has to check every test in the test file
134
+ suitesInTestFile.forEach(suite => {
135
+ suite.eachTest(test => {
136
+ if (test.state === 'failed' || test.timedOut) {
137
+ status = 'fail'
138
+ }
139
+ })
102
140
  })
103
141
  }
104
142
 
105
- const asyncResource = testSuiteToAr.get(suite)
143
+ const asyncResource = testFileToSuiteAr.get(suite.file)
106
144
  asyncResource.runInAsyncScope(() => {
107
145
  // get suite status
108
146
  testSuiteFinishCh.publish(status)
@@ -125,7 +163,7 @@ function mochaHook (Runner) {
125
163
  const status = getTestStatus(test)
126
164
 
127
165
  // if there are afterEach to be run, we don't finish the test yet
128
- if (!test.parent._afterEach.length) {
166
+ if (asyncResource && !test.parent._afterEach.length) {
129
167
  asyncResource.runInAsyncScope(() => {
130
168
  testFinishCh.publish(status)
131
169
  })
@@ -148,34 +186,38 @@ function mochaHook (Runner) {
148
186
  })
149
187
 
150
188
  this.on('fail', (testOrHook, err) => {
189
+ const testFile = testOrHook.file
151
190
  let test = testOrHook
152
191
  const isHook = testOrHook.type === 'hook'
153
192
  if (isHook && testOrHook.ctx) {
154
193
  test = testOrHook.ctx.currentTest
155
194
  }
156
- let asyncResource
195
+ let testAsyncResource
157
196
  if (test) {
158
- asyncResource = getTestAsyncResource(test)
197
+ testAsyncResource = getTestAsyncResource(test)
159
198
  }
160
- if (asyncResource) {
161
- asyncResource.runInAsyncScope(() => {
199
+ if (testAsyncResource) {
200
+ testAsyncResource.runInAsyncScope(() => {
162
201
  if (isHook) {
163
- err.message = `${testOrHook.title}: ${err.message}`
202
+ err.message = `${testOrHook.fullTitle()}: ${err.message}`
164
203
  errorCh.publish(err)
165
204
  // if it's a hook and it has failed, 'test end' will not be called
166
205
  testFinishCh.publish('fail')
167
206
  } else {
168
207
  errorCh.publish(err)
169
208
  }
170
- // we propagate the error to the suite
171
- const testSuiteAsyncResource = testSuiteToAr.get(test.parent)
172
- if (testSuiteAsyncResource) {
173
- const testSuiteError = new Error(`Test "${test.fullTitle()}" failed with message "${err.message}"`)
174
- testSuiteError.stack = err.stack
175
- testSuiteAsyncResource.runInAsyncScope(() => {
176
- testSuiteErrorCh.publish(testSuiteError)
177
- })
178
- }
209
+ })
210
+ }
211
+ const testSuiteAsyncResource = testFileToSuiteAr.get(testFile)
212
+
213
+ if (testSuiteAsyncResource) {
214
+ // we propagate the error to the suite
215
+ const testSuiteError = new Error(
216
+ `"${testOrHook.parent.fullTitle()}" failed with message "${err.message}"`
217
+ )
218
+ testSuiteError.stack = err.stack
219
+ testSuiteAsyncResource.runInAsyncScope(() => {
220
+ testSuiteErrorCh.publish(testSuiteError)
179
221
  })
180
222
  }
181
223
  })
@@ -189,7 +231,11 @@ function mochaHook (Runner) {
189
231
  } else {
190
232
  // if there is no async resource, the test has been skipped through `test.skip``
191
233
  const skippedTestAsyncResource = new AsyncResource('bound-anonymous-fn')
192
- testToAr.set(test, skippedTestAsyncResource)
234
+ if (test.fn) {
235
+ testToAr.set(test.fn, skippedTestAsyncResource)
236
+ } else {
237
+ testToAr.set(test, skippedTestAsyncResource)
238
+ }
193
239
  skippedTestAsyncResource.runInAsyncScope(() => {
194
240
  skipCh.publish(test)
195
241
  })
@@ -49,6 +49,19 @@ addHook({ name: 'net' }, net => {
49
49
  setupListeners(this, 'tcp', asyncResource)
50
50
  }
51
51
 
52
+ const emit = this.emit
53
+ this.emit = function (eventName) {
54
+ switch (eventName) {
55
+ case 'ready':
56
+ case 'connect':
57
+ return callbackResource.runInAsyncScope(() => {
58
+ return emit.apply(this, arguments)
59
+ })
60
+ default:
61
+ return emit.apply(this, arguments)
62
+ }
63
+ }
64
+
52
65
  try {
53
66
  return connect.apply(this, arguments)
54
67
  } catch (err) {
@@ -65,21 +65,25 @@ function wrapFindPageComponents (findPageComponents) {
65
65
  const result = findPageComponents.apply(this, arguments)
66
66
 
67
67
  if (result) {
68
- pageLoadChannel.publish({ page: pathname })
68
+ pageLoadChannel.publish({ page: getPagePath(pathname) })
69
69
  }
70
70
 
71
71
  return result
72
72
  }
73
73
  }
74
74
 
75
+ function getPagePath (page) {
76
+ return typeof page === 'object' ? page.pathname : page
77
+ }
78
+
75
79
  function getPageFromPath (page, dynamicRoutes = []) {
76
80
  for (const dynamicRoute of dynamicRoutes) {
77
81
  if (dynamicRoute.page.startsWith('/api') && dynamicRoute.match(page)) {
78
- return dynamicRoute.page
82
+ return getPagePath(dynamicRoute.page)
79
83
  }
80
84
  }
81
85
 
82
- return page
86
+ return getPagePath(page)
83
87
  }
84
88
 
85
89
  function instrument (req, res, handler) {
@@ -9,10 +9,15 @@ const {
9
9
  getTestEnvironmentMetadata,
10
10
  getTestParentSpan,
11
11
  getTestCommonTags,
12
+ getTestSessionCommonTags,
13
+ getTestSuiteCommonTags,
12
14
  TEST_PARAMETERS,
13
15
  getCodeOwnersFileEntries,
14
16
  getCodeOwnersForFilename,
15
- TEST_CODE_OWNERS
17
+ TEST_CODE_OWNERS,
18
+ TEST_SESSION_ID,
19
+ TEST_SUITE_ID,
20
+ TEST_COMMAND
16
21
  } = require('../../dd-trace/src/plugins/util/test')
17
22
 
18
23
  // https://github.com/facebook/jest/blob/d6ad15b0f88a05816c2fe034dd6900d28315d570/packages/jest-worker/src/types.ts#L38
@@ -58,12 +63,93 @@ class JestPlugin extends Plugin {
58
63
  this.testEnvironmentMetadata = getTestEnvironmentMetadata('jest', this.config)
59
64
  this.codeOwnersEntries = getCodeOwnersFileEntries()
60
65
 
61
- this.addSub('ci:jest:test:code-coverage', (coverageFiles) => {
66
+ this.addSub('ci:jest:session:start', (command) => {
67
+ if (!this.config.isAgentlessEnabled) {
68
+ return
69
+ }
70
+ const store = storage.getStore()
71
+ const childOf = getTestParentSpan(this.tracer)
72
+ const testSessionSpanMetadata = getTestSessionCommonTags(command, this.tracer._version)
73
+
74
+ const testSessionSpan = this.tracer.startSpan('jest.test_session', {
75
+ childOf,
76
+ tags: {
77
+ ...this.testEnvironmentMetadata,
78
+ ...testSessionSpanMetadata
79
+ }
80
+ })
81
+ this.enter(testSessionSpan, store)
82
+ })
83
+
84
+ this.addSub('ci:jest:session:finish', (status) => {
85
+ if (!this.config.isAgentlessEnabled) {
86
+ return
87
+ }
88
+ const testSessionSpan = storage.getStore().span
89
+ testSessionSpan.setTag(TEST_STATUS, status)
90
+ testSessionSpan.finish()
91
+ finishAllTraceSpans(testSessionSpan)
92
+ this.tracer._exporter._writer.flush()
93
+ })
94
+
95
+ // Test suites can be run in a different process from jest's main one.
96
+ // This subscriber changes the configuration objects from jest to inject the trace id
97
+ // of the test session to the processes that run the test suites.
98
+ this.addSub('ci:jest:session:configuration', configs => {
99
+ if (!this.config.isAgentlessEnabled) {
100
+ return
101
+ }
102
+ const testSessionSpan = storage.getStore().span
103
+ configs.forEach(config => {
104
+ config._ddTestSessionId = testSessionSpan.context()._traceId.toString(10)
105
+ config._ddTestCommand = testSessionSpan.context()._tags[TEST_COMMAND]
106
+ })
107
+ })
108
+
109
+ this.addSub('ci:jest:test-suite:start', ({ testSuite, testEnvironmentOptions }) => {
110
+ if (!this.config.isAgentlessEnabled) {
111
+ return
112
+ }
113
+
114
+ const { _ddTestSessionId: testSessionId, _ddTestCommand: testCommand } = testEnvironmentOptions
115
+
116
+ const store = storage.getStore()
117
+
118
+ const testSessionSpanContext = this.tracer.extract('text_map', {
119
+ 'x-datadog-trace-id': testSessionId,
120
+ 'x-datadog-parent-id': '0000000000000000'
121
+ })
122
+
123
+ const testSuiteMetadata = getTestSuiteCommonTags(testCommand, this.tracer._version, testSuite)
124
+
125
+ const testSuiteSpan = this.tracer.startSpan('jest.test_suite', {
126
+ childOf: testSessionSpanContext,
127
+ tags: {
128
+ ...this.testEnvironmentMetadata,
129
+ ...testSuiteMetadata
130
+ }
131
+ })
132
+ this.enter(testSuiteSpan, store)
133
+ })
134
+
135
+ this.addSub('ci:jest:test-suite:finish', ({ status, errorMessage }) => {
136
+ if (!this.config.isAgentlessEnabled) {
137
+ return
138
+ }
139
+ const testSuiteSpan = storage.getStore().span
140
+ testSuiteSpan.setTag(TEST_STATUS, status)
141
+ if (errorMessage) {
142
+ testSuiteSpan.setTag('error', new Error(errorMessage))
143
+ }
144
+ testSuiteSpan.finish()
145
+ })
146
+
147
+ this.addSub('ci:jest:test-suite:code-coverage', (coverageFiles) => {
62
148
  if (!this.config.isAgentlessEnabled || !this.config.isIntelligentTestRunnerEnabled) {
63
149
  return
64
150
  }
65
- const testSpan = storage.getStore().span
66
- this.tracer._exporter.exportCoverage({ testSpan, coverageFiles })
151
+ const testSuiteSpan = storage.getStore().span
152
+ this.tracer._exporter.exportCoverage({ span: testSuiteSpan, coverageFiles })
67
153
  })
68
154
 
69
155
  this.addSub('ci:jest:test:start', (test) => {
@@ -96,7 +182,20 @@ class JestPlugin extends Plugin {
96
182
  }
97
183
 
98
184
  startTestSpan (test) {
99
- const { childOf, ...testSpanMetadata } = getTestSpanMetadata(this.tracer, test)
185
+ const suiteTags = {}
186
+ const store = storage.getStore()
187
+ const testSuiteSpan = store ? store.span : undefined
188
+ if (testSuiteSpan) {
189
+ const testSuiteId = testSuiteSpan.context()._spanId.toString(10)
190
+ suiteTags[TEST_SUITE_ID] = testSuiteId
191
+ suiteTags[TEST_SESSION_ID] = testSuiteSpan.context()._traceId.toString(10)
192
+ suiteTags[TEST_COMMAND] = testSuiteSpan.context()._tags[TEST_COMMAND]
193
+ }
194
+
195
+ const {
196
+ childOf,
197
+ ...testSpanMetadata
198
+ } = getTestSpanMetadata(this.tracer, test)
100
199
 
101
200
  const codeOwners = getCodeOwnersForFilename(test.suite, this.codeOwnersEntries)
102
201
 
@@ -109,7 +208,8 @@ class JestPlugin extends Plugin {
109
208
  childOf,
110
209
  tags: {
111
210
  ...this.testEnvironmentMetadata,
112
- ...testSpanMetadata
211
+ ...testSpanMetadata,
212
+ ...suiteTags
113
213
  }
114
214
  })
115
215
 
@@ -47,7 +47,7 @@ class MochaPlugin extends Plugin {
47
47
  constructor (...args) {
48
48
  super(...args)
49
49
 
50
- this._testSuites = new WeakMap()
50
+ this._testSuites = new Map()
51
51
  this._testNameToParams = {}
52
52
  this.testEnvironmentMetadata = getTestEnvironmentMetadata('mocha', this.config)
53
53
  this.sourceRoot = process.cwd()
@@ -75,7 +75,11 @@ class MochaPlugin extends Plugin {
75
75
  return
76
76
  }
77
77
  const store = storage.getStore()
78
- const testSuiteMetadata = getTestSuiteCommonTags(this.command, this.tracer._version, suite.fullTitle())
78
+ const testSuiteMetadata = getTestSuiteCommonTags(
79
+ this.command,
80
+ this.tracer._version,
81
+ getTestSuitePath(suite.file, this.sourceRoot)
82
+ )
79
83
  const testSuiteSpan = this.tracer.startSpan('mocha.test_suite', {
80
84
  childOf: this.testSessionSpan,
81
85
  tags: {
@@ -84,7 +88,7 @@ class MochaPlugin extends Plugin {
84
88
  }
85
89
  })
86
90
  this.enter(testSuiteSpan, store)
87
- this._testSuites.set(suite, testSuiteSpan)
91
+ this._testSuites.set(suite.file, testSuiteSpan)
88
92
  })
89
93
 
90
94
  this.addSub('ci:mocha:test-suite:finish', (status) => {
@@ -92,7 +96,10 @@ class MochaPlugin extends Plugin {
92
96
  return
93
97
  }
94
98
  const span = storage.getStore().span
95
- span.setTag(TEST_STATUS, status)
99
+ // the test status of the suite may have been set in ci:mocha:test-suite:error already
100
+ if (!span.context()._tags[TEST_STATUS]) {
101
+ span.setTag(TEST_STATUS, status)
102
+ }
96
103
  span.finish()
97
104
  })
98
105
 
@@ -102,6 +109,7 @@ class MochaPlugin extends Plugin {
102
109
  }
103
110
  const span = storage.getStore().span
104
111
  span.setTag('error', err)
112
+ span.setTag(TEST_STATUS, 'fail')
105
113
  })
106
114
 
107
115
  this.addSub('ci:mocha:test:start', (test) => {
@@ -158,15 +166,15 @@ class MochaPlugin extends Plugin {
158
166
 
159
167
  startTestSpan (test) {
160
168
  const testSuiteTags = {}
161
- const testSuiteSpan = this._testSuites.get(test.parent)
169
+ const testSuiteSpan = this._testSuites.get(test.parent.file)
162
170
 
163
171
  if (testSuiteSpan) {
164
- const testSuiteId = testSuiteSpan.context()._spanId.toString(16)
172
+ const testSuiteId = testSuiteSpan.context()._spanId.toString(10)
165
173
  testSuiteTags[TEST_SUITE_ID] = testSuiteId
166
174
  }
167
175
 
168
176
  if (this.testSessionSpan) {
169
- const testSessionId = this.testSessionSpan.context()._traceId.toString(16)
177
+ const testSessionId = this.testSessionSpan.context()._traceId.toString(10)
170
178
  testSuiteTags[TEST_SESSION_ID] = testSessionId
171
179
  testSuiteTags[TEST_COMMAND] = this.command
172
180
  }
@@ -73,21 +73,21 @@ function truncate (input) {
73
73
  return input.slice(0, Math.min(input.length, 10000))
74
74
  }
75
75
 
76
- function simplify (input) {
77
- return isBSON(input) ? input.toHexString() : input
78
- }
79
-
80
76
  function shouldSimplify (input) {
81
- return !isObject(input) || isBSON(input)
77
+ return !isObject(input)
82
78
  }
83
79
 
84
80
  function shouldHide (input) {
85
- return Buffer.isBuffer(input) || typeof input === 'function'
81
+ return Buffer.isBuffer(input) || typeof input === 'function' || isBinary(input)
86
82
  }
87
83
 
88
84
  function limitDepth (input) {
85
+ if (isBSON(input)) {
86
+ input = input.toJSON()
87
+ }
88
+
89
89
  if (shouldHide(input)) return '?'
90
- if (shouldSimplify(input)) return simplify(input)
90
+ if (shouldSimplify(input)) return input
91
91
 
92
92
  const output = {}
93
93
  const queue = [{
@@ -104,11 +104,16 @@ function limitDepth (input) {
104
104
  for (const key in input) {
105
105
  if (typeof input[key] === 'function') continue
106
106
 
107
- const child = input[key]
107
+ let child = input[key]
108
+
109
+ if (isBSON(child)) {
110
+ child = child.toJSON()
111
+ }
112
+
108
113
  if (depth >= 10 || shouldHide(child)) {
109
114
  output[key] = '?'
110
115
  } else if (shouldSimplify(child)) {
111
- output[key] = simplify(child)
116
+ output[key] = child
112
117
  } else {
113
118
  queue.push({
114
119
  input: child,
@@ -127,7 +132,11 @@ function isObject (val) {
127
132
  }
128
133
 
129
134
  function isBSON (val) {
130
- return val && val._bsontype
135
+ return val && val._bsontype && !isBinary(val)
136
+ }
137
+
138
+ function isBinary (val) {
139
+ return val && val._bsontype === 'Binary'
131
140
  }
132
141
 
133
142
  module.exports = MongodbCorePlugin
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ 'WEAK_CIPHER_ANALYZER': require('./weak-cipher-analyzer'),
3
+ 'WEAK_HASH_ANALYZER': require('./weak-hash-analyzer')
4
+ }
@@ -0,0 +1,20 @@
1
+ 'use strict'
2
+
3
+ const analyzers = require('./analyzers')
4
+
5
+ function enableAllAnalyzers () {
6
+ for (const analyzer in analyzers) {
7
+ analyzers[analyzer].configure(true)
8
+ }
9
+ }
10
+
11
+ function disableAllAnalyzers () {
12
+ for (const analyzer in analyzers) {
13
+ analyzers[analyzer].configure(false)
14
+ }
15
+ }
16
+
17
+ module.exports = {
18
+ enableAllAnalyzers,
19
+ disableAllAnalyzers
20
+ }
@@ -0,0 +1,48 @@
1
+ 'use strict'
2
+
3
+ const Plugin = require('../../../../src/plugins/plugin')
4
+ const { storage } = require('../../../../../datadog-core')
5
+ const { getFirstNonDDPathAndLine } = require('./../path-line')
6
+ const { createVulnerability, addVulnerability } = require('../vulnerability-reporter')
7
+ const { getIastContext } = require('../iast-context')
8
+ const overheadController = require('../overhead-controller')
9
+
10
+ class Analyzer extends Plugin {
11
+ constructor (type) {
12
+ super()
13
+ this._type = type
14
+ }
15
+
16
+ _isVulnerable (value, context) {
17
+ return false
18
+ }
19
+
20
+ _report (value, context) {
21
+ const evidence = this._getEvidence(value)
22
+ const location = this._getLocation()
23
+ const spanId = context && context.rootSpan && context.rootSpan.context().toSpanId()
24
+ const vulnerability = createVulnerability(this._type, evidence, spanId, location)
25
+ addVulnerability(context, vulnerability)
26
+ }
27
+
28
+ _getEvidence (value) {
29
+ return { value }
30
+ }
31
+
32
+ _getLocation () {
33
+ return getFirstNonDDPathAndLine()
34
+ }
35
+
36
+ analyze (value) {
37
+ const iastContext = getIastContext(storage.getStore())
38
+ if (iastContext && this._isVulnerable(value, iastContext) && this._checkOCE(iastContext)) {
39
+ this._report(value, iastContext)
40
+ }
41
+ }
42
+
43
+ _checkOCE (context) {
44
+ return overheadController.hasQuota(overheadController.OPERATIONS.REPORT_VULNERABILITY, context)
45
+ }
46
+ }
47
+
48
+ module.exports = Analyzer
@@ -0,0 +1,27 @@
1
+ 'use strict'
2
+ const Analyzer = require('./vulnerability-analyzer')
3
+
4
+ const INSECURE_CIPHERS = new Set([
5
+ 'des', 'des-cbc', 'des-cfb', 'des-cfb1', 'des-cfb8', 'des-ecb', 'des-ede', 'des-ede-cbc', 'des-ede-cfb',
6
+ 'des-ede-ecb', 'des-ede-ofb', 'des-ede3', 'des-ede3-cbc', 'des-ede3-cfb', 'des-ede3-cfb1', 'des-ede3-cfb8',
7
+ 'des-ede3-ecb', 'des-ede3-ofb', 'des-ofb', 'des3', 'des3-wrap',
8
+ 'rc2', 'rc2-128', 'rc2-40', 'rc2-40-cbc', 'rc2-64', 'rc2-64-cbc', 'rc2-cbc', 'rc2-cfb', 'rc2-ecb', 'rc2-ofb',
9
+ 'blowfish',
10
+ 'rc4', 'rc4-40', 'rc4-hmac-md5'
11
+ ].map(algorithm => algorithm.toLowerCase()))
12
+
13
+ class WeakCipherAnalyzer extends Analyzer {
14
+ constructor () {
15
+ super('WEAK_CIPHER')
16
+ this.addSub('datadog:crypto:cipher:start', ({ algorithm }) => this.analyze(algorithm))
17
+ }
18
+
19
+ _isVulnerable (algorithm) {
20
+ if (algorithm && typeof algorithm === 'string') {
21
+ return INSECURE_CIPHERS.has(algorithm.toLowerCase())
22
+ }
23
+ return false
24
+ }
25
+ }
26
+
27
+ module.exports = new WeakCipherAnalyzer()
@@ -0,0 +1,24 @@
1
+ 'use strict'
2
+ const Analyzer = require('./vulnerability-analyzer')
3
+
4
+ const INSECURE_HASH_ALGORITHMS = new Set([
5
+ 'md4', 'md4WithRSAEncryption', 'RSA-MD4',
6
+ 'RSA-MD5', 'md5', 'md5-sha1', 'ssl3-md5', 'md5WithRSAEncryption',
7
+ 'RSA-SHA1', 'RSA-SHA1-2', 'sha1', 'md5-sha1', 'sha1WithRSAEncryption', 'ssl3-sha1'
8
+ ].map(algorithm => algorithm.toLowerCase()))
9
+
10
+ class WeakHashAnalyzer extends Analyzer {
11
+ constructor () {
12
+ super('WEAK_HASH')
13
+ this.addSub('datadog:crypto:hashing:start', ({ algorithm }) => this.analyze(algorithm))
14
+ }
15
+
16
+ _isVulnerable (algorithm) {
17
+ if (typeof algorithm === 'string') {
18
+ return INSECURE_HASH_ALGORITHMS.has(algorithm.toLowerCase())
19
+ }
20
+ return false
21
+ }
22
+ }
23
+
24
+ module.exports = new WeakHashAnalyzer()
@@ -0,0 +1,50 @@
1
+ const IAST_CONTEXT_KEY = Symbol('_dd.iast.context')
2
+
3
+ function getIastContext (store) {
4
+ return store && store[IAST_CONTEXT_KEY]
5
+ }
6
+
7
+ /* TODO Fix storage problem when the close event is called without
8
+ finish event to remove `topContext` references
9
+ We have to save the context in two places, because
10
+ clean can be called when the storage store is not available
11
+ */
12
+ function saveIastContext (store, topContext, context) {
13
+ if (store && topContext) {
14
+ store[IAST_CONTEXT_KEY] = context
15
+ topContext[IAST_CONTEXT_KEY] = context
16
+ return store[IAST_CONTEXT_KEY]
17
+ }
18
+ }
19
+
20
+ /* TODO Fix storage problem when the close event is called without
21
+ finish event to remove `topContext` references
22
+ iastContext is currently saved in store and request rootContext
23
+ to fix problems with `close` without `finish` events
24
+ */
25
+ function cleanIastContext (store, context, iastContext) {
26
+ if (store) {
27
+ if (!iastContext) {
28
+ iastContext = store[IAST_CONTEXT_KEY]
29
+ }
30
+ store[IAST_CONTEXT_KEY] = null
31
+ }
32
+ if (context) {
33
+ if (!iastContext) {
34
+ iastContext = context[IAST_CONTEXT_KEY]
35
+ }
36
+ context[IAST_CONTEXT_KEY] = null
37
+ }
38
+ if (iastContext) {
39
+ Object.keys(iastContext).forEach(key => delete iastContext[key])
40
+ return true
41
+ }
42
+ return false
43
+ }
44
+
45
+ module.exports = {
46
+ getIastContext,
47
+ saveIastContext,
48
+ cleanIastContext,
49
+ IAST_CONTEXT_KEY
50
+ }