dd-trace 3.22.1 → 3.24.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.
- package/LICENSE-3rdparty.csv +2 -0
- package/MIGRATING.md +39 -0
- package/README.md +18 -11
- package/index.d.ts +281 -0
- package/package.json +6 -4
- package/packages/datadog-instrumentations/src/cookie.js +21 -0
- package/packages/datadog-instrumentations/src/fetch.js +48 -0
- package/packages/datadog-instrumentations/src/grpc/server.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/register.js +10 -0
- package/packages/datadog-instrumentations/src/jest.js +2 -3
- package/packages/datadog-instrumentations/src/next.js +2 -2
- package/packages/datadog-instrumentations/src/otel-sdk-trace.js +18 -0
- package/packages/datadog-plugin-cypress/src/plugin.js +109 -47
- package/packages/datadog-plugin-cypress/src/support.js +3 -2
- package/packages/datadog-plugin-fetch/src/index.js +36 -0
- package/packages/datadog-plugin-http/src/client.js +24 -8
- package/packages/datadog-plugin-mysql/src/index.js +2 -11
- package/packages/datadog-plugin-tedious/src/index.js +2 -2
- package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +3 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +52 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/insecure-cookie-analyzer.js +3 -22
- package/packages/dd-trace/src/appsec/iast/analyzers/no-httponly-cookie-analyzer.js +12 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/no-samesite-cookie-analyzer.js +12 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/set-cookies-header-interceptor.js +7 -3
- package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +3 -3
- package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +48 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +3 -3
- package/packages/dd-trace/src/appsec/iast/analyzers/weak-hash-analyzer.js +24 -0
- package/packages/dd-trace/src/appsec/iast/index.js +9 -2
- package/packages/dd-trace/src/appsec/iast/path-line.js +13 -0
- package/packages/dd-trace/src/appsec/iast/tags.js +6 -0
- package/packages/dd-trace/src/appsec/iast/taint-tracking/index.js +2 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/operations.js +13 -4
- package/packages/dd-trace/src/appsec/iast/taint-tracking/origin-types.js +5 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +24 -4
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +3 -0
- package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +7 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +4 -3
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +5 -2
- package/packages/dd-trace/src/config.js +13 -0
- package/packages/dd-trace/src/external-logger/src/index.js +126 -0
- package/packages/dd-trace/src/external-logger/test/index.spec.js +147 -0
- package/packages/dd-trace/src/lambda/handler.js +3 -15
- package/packages/dd-trace/src/noop/proxy.js +4 -0
- package/packages/dd-trace/src/opentelemetry/context_manager.js +74 -0
- package/packages/dd-trace/src/opentelemetry/sampler.js +18 -0
- package/packages/dd-trace/src/opentelemetry/span.js +151 -0
- package/packages/dd-trace/src/opentelemetry/span_context.js +44 -0
- package/packages/dd-trace/src/opentelemetry/span_processor.js +50 -0
- package/packages/dd-trace/src/opentelemetry/tracer.js +124 -0
- package/packages/dd-trace/src/opentelemetry/tracer_provider.js +72 -0
- package/packages/dd-trace/src/opentracing/span.js +14 -4
- package/packages/dd-trace/src/plugin_manager.js +10 -7
- package/packages/dd-trace/src/plugins/database.js +7 -3
- package/packages/dd-trace/src/plugins/plugin.js +3 -1
- package/packages/dd-trace/src/plugins/util/exec.js +2 -2
- package/packages/dd-trace/src/plugins/util/git.js +51 -24
- package/packages/dd-trace/src/profiling/config.js +2 -0
- package/packages/dd-trace/src/profiling/profiler.js +13 -4
- package/packages/dd-trace/src/proxy.js +4 -0
- package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +24 -1
- package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +18 -1
- package/packages/dd-trace/src/tracer.js +3 -3
- package/packages/dd-trace/src/util.js +1 -1
- package/version.js +8 -4
|
@@ -133,6 +133,8 @@ module.exports = (on, config) => {
|
|
|
133
133
|
'git.branch': branch
|
|
134
134
|
} = testEnvironmentMetadata
|
|
135
135
|
|
|
136
|
+
const finishedTestsByFile = {}
|
|
137
|
+
|
|
136
138
|
const testConfiguration = {
|
|
137
139
|
repositoryUrl,
|
|
138
140
|
sha,
|
|
@@ -158,6 +160,44 @@ module.exports = (on, config) => {
|
|
|
158
160
|
let isCodeCoverageEnabled = false
|
|
159
161
|
let testsToSkip = []
|
|
160
162
|
|
|
163
|
+
function getTestSpan (testName, testSuite) {
|
|
164
|
+
const testSuiteTags = {
|
|
165
|
+
[TEST_COMMAND]: command,
|
|
166
|
+
[TEST_COMMAND]: command,
|
|
167
|
+
[TEST_MODULE]: TEST_FRAMEWORK_NAME
|
|
168
|
+
}
|
|
169
|
+
if (testSuiteSpan) {
|
|
170
|
+
testSuiteTags[TEST_SUITE_ID] = testSuiteSpan.context().toSpanId()
|
|
171
|
+
}
|
|
172
|
+
if (testSessionSpan && testModuleSpan) {
|
|
173
|
+
testSuiteTags[TEST_SESSION_ID] = testSessionSpan.context().toTraceId()
|
|
174
|
+
testSuiteTags[TEST_MODULE_ID] = testModuleSpan.context().toSpanId()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const {
|
|
178
|
+
childOf,
|
|
179
|
+
resource,
|
|
180
|
+
...testSpanMetadata
|
|
181
|
+
} = getTestSpanMetadata(tracer, testName, testSuite, config)
|
|
182
|
+
|
|
183
|
+
const codeOwners = getCodeOwnersForFilename(testSuite, codeOwnersEntries)
|
|
184
|
+
|
|
185
|
+
if (codeOwners) {
|
|
186
|
+
testSpanMetadata[TEST_CODE_OWNERS] = codeOwners
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test`, {
|
|
190
|
+
childOf,
|
|
191
|
+
tags: {
|
|
192
|
+
[COMPONENT]: TEST_FRAMEWORK_NAME,
|
|
193
|
+
[ORIGIN_KEY]: CI_APP_ORIGIN,
|
|
194
|
+
...testSpanMetadata,
|
|
195
|
+
...testEnvironmentMetadata,
|
|
196
|
+
...testSuiteTags
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
161
201
|
on('before:run', (details) => {
|
|
162
202
|
return getItrConfig(tracer, testConfiguration).then(({ err, itrConfig }) => {
|
|
163
203
|
if (err) {
|
|
@@ -203,6 +243,58 @@ module.exports = (on, config) => {
|
|
|
203
243
|
})
|
|
204
244
|
})
|
|
205
245
|
})
|
|
246
|
+
on('after:spec', (spec, { tests, stats }) => {
|
|
247
|
+
const cypressTests = tests || []
|
|
248
|
+
const finishedTests = finishedTestsByFile[spec.relative] || []
|
|
249
|
+
|
|
250
|
+
// Get tests that didn't go through `dd:afterEach` and haven't been skipped by ITR
|
|
251
|
+
// and create a skipped test span for each of them
|
|
252
|
+
cypressTests.filter(({ title }) => {
|
|
253
|
+
const cypressTestName = title.join(' ')
|
|
254
|
+
const isSkippedByItr = testsToSkip.find(test =>
|
|
255
|
+
cypressTestName === test.name && spec.relative === test.suite
|
|
256
|
+
)
|
|
257
|
+
const isTestFinished = finishedTests.find(({ testName }) => cypressTestName === testName)
|
|
258
|
+
|
|
259
|
+
return !isSkippedByItr && !isTestFinished
|
|
260
|
+
}).forEach(({ title }) => {
|
|
261
|
+
const skippedTestSpan = getTestSpan(title.join(' '), spec.relative)
|
|
262
|
+
skippedTestSpan.setTag(TEST_STATUS, 'skip')
|
|
263
|
+
skippedTestSpan.finish()
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
// Make sure that reported test statuses are the same as Cypress reports.
|
|
267
|
+
// This is not always the case, such as when an `after` hook fails:
|
|
268
|
+
// Cypress will report the last run test as failed, but we don't know that yet at `dd:afterEach`
|
|
269
|
+
let latestError
|
|
270
|
+
finishedTests.forEach((finishedTest) => {
|
|
271
|
+
const cypressTest = cypressTests.find(test => test.title.join(' ') === finishedTest.testName)
|
|
272
|
+
if (!cypressTest) {
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
if (cypressTest.displayError) {
|
|
276
|
+
latestError = new Error(cypressTest.displayError)
|
|
277
|
+
}
|
|
278
|
+
const cypressTestStatus = CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.state]
|
|
279
|
+
// update test status
|
|
280
|
+
if (cypressTestStatus !== finishedTest.testStatus) {
|
|
281
|
+
finishedTest.testSpan.setTag(TEST_STATUS, cypressTestStatus)
|
|
282
|
+
finishedTest.testSpan.setTag('error', latestError)
|
|
283
|
+
}
|
|
284
|
+
finishedTest.testSpan.finish(finishedTest.finishTime)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
if (testSuiteSpan) {
|
|
288
|
+
const status = getSuiteStatus(stats)
|
|
289
|
+
testSuiteSpan.setTag(TEST_STATUS, status)
|
|
290
|
+
|
|
291
|
+
if (latestError) {
|
|
292
|
+
testSuiteSpan.setTag('error', latestError)
|
|
293
|
+
}
|
|
294
|
+
testSuiteSpan.finish()
|
|
295
|
+
testSuiteSpan = null
|
|
296
|
+
}
|
|
297
|
+
})
|
|
206
298
|
|
|
207
299
|
on('after:run', (suiteStats) => {
|
|
208
300
|
if (testSessionSpan && testModuleSpan) {
|
|
@@ -254,15 +346,6 @@ module.exports = (on, config) => {
|
|
|
254
346
|
})
|
|
255
347
|
return null
|
|
256
348
|
},
|
|
257
|
-
'dd:testSuiteFinish': (stats) => {
|
|
258
|
-
if (testSuiteSpan) {
|
|
259
|
-
const status = getSuiteStatus(stats)
|
|
260
|
-
testSuiteSpan.setTag(TEST_STATUS, status)
|
|
261
|
-
testSuiteSpan.finish()
|
|
262
|
-
testSuiteSpan = null
|
|
263
|
-
}
|
|
264
|
-
return null
|
|
265
|
-
},
|
|
266
349
|
'dd:beforeEach': (test) => {
|
|
267
350
|
const { testName, testSuite } = test
|
|
268
351
|
// skip test
|
|
@@ -272,47 +355,14 @@ module.exports = (on, config) => {
|
|
|
272
355
|
return { shouldSkip: true }
|
|
273
356
|
}
|
|
274
357
|
|
|
275
|
-
const testSuiteTags = {
|
|
276
|
-
[TEST_COMMAND]: command,
|
|
277
|
-
[TEST_COMMAND]: command,
|
|
278
|
-
[TEST_MODULE]: TEST_FRAMEWORK_NAME
|
|
279
|
-
}
|
|
280
|
-
if (testSuiteSpan) {
|
|
281
|
-
testSuiteTags[TEST_SUITE_ID] = testSuiteSpan.context().toSpanId()
|
|
282
|
-
}
|
|
283
|
-
if (testSessionSpan && testModuleSpan) {
|
|
284
|
-
testSuiteTags[TEST_SESSION_ID] = testSessionSpan.context().toTraceId()
|
|
285
|
-
testSuiteTags[TEST_MODULE_ID] = testModuleSpan.context().toSpanId()
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const {
|
|
289
|
-
childOf,
|
|
290
|
-
resource,
|
|
291
|
-
...testSpanMetadata
|
|
292
|
-
} = getTestSpanMetadata(tracer, testName, testSuite, config)
|
|
293
|
-
|
|
294
|
-
const codeOwners = getCodeOwnersForFilename(testSuite, codeOwnersEntries)
|
|
295
|
-
|
|
296
|
-
if (codeOwners) {
|
|
297
|
-
testSpanMetadata[TEST_CODE_OWNERS] = codeOwners
|
|
298
|
-
}
|
|
299
|
-
|
|
300
358
|
if (!activeSpan) {
|
|
301
|
-
activeSpan =
|
|
302
|
-
childOf,
|
|
303
|
-
tags: {
|
|
304
|
-
[COMPONENT]: TEST_FRAMEWORK_NAME,
|
|
305
|
-
[ORIGIN_KEY]: CI_APP_ORIGIN,
|
|
306
|
-
...testSpanMetadata,
|
|
307
|
-
...testEnvironmentMetadata,
|
|
308
|
-
...testSuiteTags
|
|
309
|
-
}
|
|
310
|
-
})
|
|
359
|
+
activeSpan = getTestSpan(testName, testSuite)
|
|
311
360
|
}
|
|
361
|
+
|
|
312
362
|
return activeSpan ? { traceId: activeSpan.context().toTraceId() } : {}
|
|
313
363
|
},
|
|
314
364
|
'dd:afterEach': ({ test, coverage }) => {
|
|
315
|
-
const { state, error, isRUMActive, testSourceLine } = test
|
|
365
|
+
const { state, error, isRUMActive, testSourceLine, testSuite, testName } = test
|
|
316
366
|
if (activeSpan) {
|
|
317
367
|
if (coverage && tracer._tracer._exporter.exportCoverage && isCodeCoverageEnabled) {
|
|
318
368
|
const coverageFiles = getCoveredFilenamesFromCoverage(coverage)
|
|
@@ -326,8 +376,9 @@ module.exports = (on, config) => {
|
|
|
326
376
|
}
|
|
327
377
|
tracer._tracer._exporter.exportCoverage(formattedCoverage)
|
|
328
378
|
}
|
|
379
|
+
const testStatus = CYPRESS_STATUS_TO_TEST_STATUS[state]
|
|
380
|
+
activeSpan.setTag(TEST_STATUS, testStatus)
|
|
329
381
|
|
|
330
|
-
activeSpan.setTag(TEST_STATUS, CYPRESS_STATUS_TO_TEST_STATUS[state])
|
|
331
382
|
if (error) {
|
|
332
383
|
activeSpan.setTag('error', error)
|
|
333
384
|
}
|
|
@@ -337,7 +388,18 @@ module.exports = (on, config) => {
|
|
|
337
388
|
if (testSourceLine) {
|
|
338
389
|
activeSpan.setTag(TEST_SOURCE_START, testSourceLine)
|
|
339
390
|
}
|
|
340
|
-
|
|
391
|
+
const finishedTest = {
|
|
392
|
+
testName,
|
|
393
|
+
testStatus,
|
|
394
|
+
finishTime: activeSpan._getTime(), // we store the finish time here
|
|
395
|
+
testSpan: activeSpan
|
|
396
|
+
}
|
|
397
|
+
if (finishedTestsByFile[testSuite]) {
|
|
398
|
+
finishedTestsByFile[testSuite].push(finishedTest)
|
|
399
|
+
} else {
|
|
400
|
+
finishedTestsByFile[testSuite] = [finishedTest]
|
|
401
|
+
}
|
|
402
|
+
// test spans are finished at after:spec
|
|
341
403
|
}
|
|
342
404
|
activeSpan = null
|
|
343
405
|
return null
|
|
@@ -16,9 +16,10 @@ before(() => {
|
|
|
16
16
|
})
|
|
17
17
|
|
|
18
18
|
after(() => {
|
|
19
|
-
cy.task('dd:testSuiteFinish', Cypress.mocha.getRunner().stats)
|
|
20
19
|
cy.window().then(win => {
|
|
21
|
-
win.
|
|
20
|
+
if (win.DD_RUM) {
|
|
21
|
+
win.dispatchEvent(new Event('beforeunload'))
|
|
22
|
+
}
|
|
22
23
|
})
|
|
23
24
|
})
|
|
24
25
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const HttpClientPlugin = require('../../datadog-plugin-http/src/client')
|
|
4
|
+
const { HTTP_HEADERS } = require('../../../ext/formats')
|
|
5
|
+
|
|
6
|
+
class FetchPlugin extends HttpClientPlugin {
|
|
7
|
+
static get id () { return 'fetch' }
|
|
8
|
+
|
|
9
|
+
addTraceSub (eventName, handler) {
|
|
10
|
+
this.addSub(`apm:${this.constructor.id}:${this.operation}:${eventName}`, handler)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
start (message) {
|
|
14
|
+
const req = message.req
|
|
15
|
+
const options = new URL(req.url)
|
|
16
|
+
const headers = options.headers = Object.fromEntries(req.headers.entries())
|
|
17
|
+
|
|
18
|
+
const args = { options }
|
|
19
|
+
|
|
20
|
+
super.start({ args })
|
|
21
|
+
|
|
22
|
+
message.req = new globalThis.Request(req, { headers })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_inject (span, headers) {
|
|
26
|
+
const carrier = {}
|
|
27
|
+
|
|
28
|
+
this.tracer.inject(span, HTTP_HEADERS, carrier)
|
|
29
|
+
|
|
30
|
+
for (const name in carrier) {
|
|
31
|
+
headers.append(name, carrier[name])
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = FetchPlugin
|
|
@@ -24,15 +24,17 @@ class HttpClientPlugin extends ClientPlugin {
|
|
|
24
24
|
this.addSub(`apm:${this.constructor.id}:client:${this.operation}:${eventName}`, handler)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
start ({ args, http }) {
|
|
27
|
+
start ({ args, http = {} }) {
|
|
28
28
|
const store = storage.getStore()
|
|
29
29
|
const options = args.options
|
|
30
|
-
const agent = options.agent || options._defaultAgent || http.globalAgent
|
|
30
|
+
const agent = options.agent || options._defaultAgent || http.globalAgent || {}
|
|
31
31
|
const protocol = options.protocol || agent.protocol || 'http:'
|
|
32
32
|
const hostname = options.hostname || options.host || 'localhost'
|
|
33
33
|
const host = options.port ? `${hostname}:${options.port}` : hostname
|
|
34
|
-
const
|
|
34
|
+
const pathname = options.path || options.pathname
|
|
35
|
+
const path = pathname ? pathname.split(/[?#]/)[0] : '/'
|
|
35
36
|
const uri = `${protocol}//${host}${path}`
|
|
37
|
+
|
|
36
38
|
const allowed = this.config.filter(uri)
|
|
37
39
|
|
|
38
40
|
const method = (options.method || 'GET').toUpperCase()
|
|
@@ -71,9 +73,11 @@ class HttpClientPlugin extends ClientPlugin {
|
|
|
71
73
|
finish ({ req, res }) {
|
|
72
74
|
const span = storage.getStore().span
|
|
73
75
|
if (res) {
|
|
74
|
-
|
|
76
|
+
const status = res.status || res.statusCode
|
|
77
|
+
|
|
78
|
+
span.setTag(HTTP_STATUS_CODE, status)
|
|
75
79
|
|
|
76
|
-
if (!this.config.validateStatus(
|
|
80
|
+
if (!this.config.validateStatus(status)) {
|
|
77
81
|
span.setTag('error', 1)
|
|
78
82
|
}
|
|
79
83
|
|
|
@@ -106,8 +110,14 @@ class HttpClientPlugin extends ClientPlugin {
|
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
function addResponseHeaders (res, span, config) {
|
|
113
|
+
if (!res.headers) return
|
|
114
|
+
|
|
115
|
+
const headers = typeof res.headers.entries === 'function'
|
|
116
|
+
? Object.fromEntries(res.headers.entries())
|
|
117
|
+
: res.headers
|
|
118
|
+
|
|
109
119
|
config.headers.forEach(key => {
|
|
110
|
-
const value =
|
|
120
|
+
const value = headers[key]
|
|
111
121
|
|
|
112
122
|
if (value) {
|
|
113
123
|
span.setTag(`${HTTP_RESPONSE_HEADERS}.${key}`, value)
|
|
@@ -116,8 +126,12 @@ function addResponseHeaders (res, span, config) {
|
|
|
116
126
|
}
|
|
117
127
|
|
|
118
128
|
function addRequestHeaders (req, span, config) {
|
|
129
|
+
const headers = req.headers && typeof req.headers.entries === 'function'
|
|
130
|
+
? Object.fromEntries(req.headers.entries())
|
|
131
|
+
: req.headers || req.getHeaders()
|
|
132
|
+
|
|
119
133
|
config.headers.forEach(key => {
|
|
120
|
-
const value =
|
|
134
|
+
const value = headers[key]
|
|
121
135
|
|
|
122
136
|
if (value) {
|
|
123
137
|
span.setTag(`${HTTP_REQUEST_HEADERS}.${key}`, Array.isArray(value) ? value.toString() : value)
|
|
@@ -193,7 +207,9 @@ function hasAmazonSignature (options) {
|
|
|
193
207
|
}
|
|
194
208
|
}
|
|
195
209
|
|
|
196
|
-
|
|
210
|
+
const search = options.search || options.path
|
|
211
|
+
|
|
212
|
+
return search && search.toLowerCase().indexOf('x-amz-signature=') !== -1
|
|
197
213
|
}
|
|
198
214
|
|
|
199
215
|
function getServiceName (tracer, config, options) {
|
|
@@ -8,9 +8,8 @@ class MySQLPlugin extends DatabasePlugin {
|
|
|
8
8
|
static get system () { return 'mysql' }
|
|
9
9
|
|
|
10
10
|
start (payload) {
|
|
11
|
-
const service =
|
|
12
|
-
|
|
13
|
-
this.startSpan(`${this.system}.query`, {
|
|
11
|
+
const service = this.serviceName(this.config, payload.conf, this.system)
|
|
12
|
+
this.startSpan(this.operationName(), {
|
|
14
13
|
service,
|
|
15
14
|
resource: payload.sql,
|
|
16
15
|
type: 'sql',
|
|
@@ -27,12 +26,4 @@ class MySQLPlugin extends DatabasePlugin {
|
|
|
27
26
|
}
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
function getServiceName (config, dbConfig) {
|
|
31
|
-
if (typeof config.service === 'function') {
|
|
32
|
-
return config.service(dbConfig)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return config.service
|
|
36
|
-
}
|
|
37
|
-
|
|
38
29
|
module.exports = MySQLPlugin
|
|
@@ -9,8 +9,8 @@ class TediousPlugin extends DatabasePlugin {
|
|
|
9
9
|
static get system () { return 'mssql' }
|
|
10
10
|
|
|
11
11
|
start ({ queryOrProcedure, connectionConfig }) {
|
|
12
|
-
this.startSpan(
|
|
13
|
-
service: this.config.
|
|
12
|
+
this.startSpan(this.operationName(), {
|
|
13
|
+
service: this.serviceName(this.config, this.system),
|
|
14
14
|
resource: queryOrProcedure,
|
|
15
15
|
type: 'sql',
|
|
16
16
|
kind: 'client',
|
|
@@ -4,9 +4,12 @@ module.exports = {
|
|
|
4
4
|
'COMMAND_INJECTION_ANALYZER': require('./command-injection-analyzer'),
|
|
5
5
|
'INSECURE_COOKIE_ANALYZER': require('./insecure-cookie-analyzer'),
|
|
6
6
|
'LDAP_ANALYZER': require('./ldap-injection-analyzer'),
|
|
7
|
+
'NO_HTTPONLY_COOKIE_ANALYZER': require('./no-httponly-cookie-analyzer'),
|
|
8
|
+
'NO_SAMESITE_COOKIE_ANALYZER': require('./no-samesite-cookie-analyzer'),
|
|
7
9
|
'PATH_TRAVERSAL_ANALYZER': require('./path-traversal-analyzer'),
|
|
8
10
|
'SQL_INJECTION_ANALYZER': require('./sql-injection-analyzer'),
|
|
9
11
|
'SSRF': require('./ssrf-analyzer'),
|
|
12
|
+
'UNVALIDATED_REDIRECT_ANALYZER': require('./unvalidated-redirect-analyzer'),
|
|
10
13
|
'WEAK_CIPHER_ANALYZER': require('./weak-cipher-analyzer'),
|
|
11
14
|
'WEAK_HASH_ANALYZER': require('./weak-hash-analyzer')
|
|
12
15
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Analyzer = require('./vulnerability-analyzer')
|
|
4
|
+
const { getNodeModulesPaths } = require('../path-line')
|
|
5
|
+
|
|
6
|
+
const EXCLUDED_PATHS = getNodeModulesPaths('express/lib/response.js')
|
|
7
|
+
|
|
8
|
+
class CookieAnalyzer extends Analyzer {
|
|
9
|
+
constructor (type, propertyToBeSafe) {
|
|
10
|
+
super(type)
|
|
11
|
+
this.propertyToBeSafe = propertyToBeSafe.toLowerCase()
|
|
12
|
+
this.addSub('datadog:iast:set-cookie', (cookieInfo) => this.analyze(cookieInfo))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
_isVulnerable ({ cookieProperties, cookieValue }) {
|
|
16
|
+
return cookieValue && !(cookieProperties && cookieProperties
|
|
17
|
+
.map(x => x.toLowerCase().trim()).includes(this.propertyToBeSafe))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_getEvidence ({ cookieName }) {
|
|
21
|
+
return { value: cookieName }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_createHashSource (type, evidence, location) {
|
|
25
|
+
return `${type}:${evidence.value}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_getExcludedPaths () {
|
|
29
|
+
return EXCLUDED_PATHS
|
|
30
|
+
}
|
|
31
|
+
_checkOCE (context, value) {
|
|
32
|
+
if (value && value.location) {
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
35
|
+
return super._checkOCE(context, value)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_getLocation (value) {
|
|
39
|
+
if (!value) {
|
|
40
|
+
return super._getLocation()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (value.location) {
|
|
44
|
+
return value.location
|
|
45
|
+
}
|
|
46
|
+
const location = super._getLocation(value)
|
|
47
|
+
value.location = location
|
|
48
|
+
return location
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = CookieAnalyzer
|
|
@@ -1,30 +1,11 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const Analyzer = require('./vulnerability-analyzer')
|
|
4
3
|
const { INSECURE_COOKIE } = require('../vulnerabilities')
|
|
4
|
+
const CookieAnalyzer = require('./cookie-analyzer')
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class InsecureCookieAnalyzer extends Analyzer {
|
|
6
|
+
class InsecureCookieAnalyzer extends CookieAnalyzer {
|
|
9
7
|
constructor () {
|
|
10
|
-
super(INSECURE_COOKIE)
|
|
11
|
-
this.addSub('datadog:iast:set-cookie', (cookieInfo) => this.analyze(cookieInfo))
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
_isVulnerable ({ cookieProperties, cookieValue }) {
|
|
15
|
-
return cookieValue && !(cookieProperties && cookieProperties.map(x => x.toLowerCase().trim()).includes('secure'))
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
_getEvidence ({ cookieName }) {
|
|
19
|
-
return { value: cookieName }
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
_createHashSource (type, evidence, location) {
|
|
23
|
-
return `${type}:${evidence.value}`
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
_getExcludedPaths () {
|
|
27
|
-
return EXCLUDED_PATHS
|
|
8
|
+
super(INSECURE_COOKIE, 'secure')
|
|
28
9
|
}
|
|
29
10
|
}
|
|
30
11
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { NO_HTTPONLY_COOKIE } = require('../vulnerabilities')
|
|
4
|
+
const CookieAnalyzer = require('./cookie-analyzer')
|
|
5
|
+
|
|
6
|
+
class NoHttponlyCookieAnalyzer extends CookieAnalyzer {
|
|
7
|
+
constructor () {
|
|
8
|
+
super(NO_HTTPONLY_COOKIE, 'HttpOnly')
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = new NoHttponlyCookieAnalyzer()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { NO_SAMESITE_COOKIE } = require('../vulnerabilities')
|
|
4
|
+
const CookieAnalyzer = require('./cookie-analyzer')
|
|
5
|
+
|
|
6
|
+
class NoSamesiteCookieAnalyzer extends CookieAnalyzer {
|
|
7
|
+
constructor () {
|
|
8
|
+
super(NO_SAMESITE_COOKIE, 'SameSite=strict')
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = new NoSamesiteCookieAnalyzer()
|
|
@@ -14,24 +14,28 @@ class SetCookiesHeaderInterceptor extends Plugin {
|
|
|
14
14
|
allCookies = [value]
|
|
15
15
|
}
|
|
16
16
|
const alreadyCheckedCookies = this._getAlreadyCheckedCookiesInResponse(res)
|
|
17
|
+
|
|
18
|
+
let location
|
|
17
19
|
allCookies.forEach(cookieString => {
|
|
18
20
|
if (!alreadyCheckedCookies.includes(cookieString)) {
|
|
19
21
|
alreadyCheckedCookies.push(cookieString)
|
|
20
|
-
|
|
22
|
+
const parsedCookie = this._parseCookie(cookieString, location)
|
|
23
|
+
setCookieChannel.publish(parsedCookie)
|
|
24
|
+
location = parsedCookie.location
|
|
21
25
|
}
|
|
22
26
|
})
|
|
23
27
|
}
|
|
24
28
|
})
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
_parseCookie (cookieString) {
|
|
31
|
+
_parseCookie (cookieString, location) {
|
|
28
32
|
const cookieParts = cookieString.split(';')
|
|
29
33
|
const nameValueParts = cookieParts[0].split('=')
|
|
30
34
|
const cookieName = nameValueParts[0]
|
|
31
35
|
const cookieValue = nameValueParts.slice(1).join('=')
|
|
32
36
|
const cookieProperties = cookieParts.slice(1).map(part => part.trim())
|
|
33
37
|
|
|
34
|
-
return { cookieName, cookieValue, cookieProperties, cookieString }
|
|
38
|
+
return { cookieName, cookieValue, cookieProperties, cookieString, location }
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
_getAlreadyCheckedCookiesInResponse (res) {
|
|
@@ -6,9 +6,9 @@ const { getRanges } = require('../taint-tracking/operations')
|
|
|
6
6
|
const { storage } = require('../../../../../datadog-core')
|
|
7
7
|
const { getIastContext } = require('../iast-context')
|
|
8
8
|
const { addVulnerability } = require('../vulnerability-reporter')
|
|
9
|
+
const { getNodeModulesPaths } = require('../path-line')
|
|
9
10
|
|
|
10
|
-
const EXCLUDED_PATHS =
|
|
11
|
-
'node_modules\\sequelize']
|
|
11
|
+
const EXCLUDED_PATHS = getNodeModulesPaths('mysql2', 'sequelize')
|
|
12
12
|
|
|
13
13
|
class SqlInjectionAnalyzer extends InjectionAnalyzer {
|
|
14
14
|
constructor () {
|
|
@@ -28,7 +28,7 @@ class SqlInjectionAnalyzer extends InjectionAnalyzer {
|
|
|
28
28
|
|
|
29
29
|
this.addSub('datadog:sequelize:query:finish', () => {
|
|
30
30
|
const store = storage.getStore()
|
|
31
|
-
if (store.sequelizeParentStore) {
|
|
31
|
+
if (store && store.sequelizeParentStore) {
|
|
32
32
|
storage.enterWith(store.sequelizeParentStore)
|
|
33
33
|
}
|
|
34
34
|
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const InjectionAnalyzer = require('./injection-analyzer')
|
|
4
|
+
const { UNVALIDATED_REDIRECT } = require('../vulnerabilities')
|
|
5
|
+
const { getNodeModulesPaths } = require('../path-line')
|
|
6
|
+
const { getRanges } = require('../taint-tracking/operations')
|
|
7
|
+
const { HTTP_REQUEST_HEADER_VALUE } = require('../taint-tracking/origin-types')
|
|
8
|
+
|
|
9
|
+
const EXCLUDED_PATHS = getNodeModulesPaths('express/lib/response.js')
|
|
10
|
+
|
|
11
|
+
class UnvalidatedRedirectAnalyzer extends InjectionAnalyzer {
|
|
12
|
+
constructor () {
|
|
13
|
+
super(UNVALIDATED_REDIRECT)
|
|
14
|
+
|
|
15
|
+
this.addSub('datadog:http:server:response:set-header:finish', ({ name, value }) => this.analyze(name, value))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// TODO: In case the location header value is tainted, this analyzer should check the ranges of the tainted.
|
|
19
|
+
// And do not report a vulnerability if source of the ranges (range.iinfo.type) are exclusively url or path params
|
|
20
|
+
// to avoid false positives.
|
|
21
|
+
analyze (name, value) {
|
|
22
|
+
if (!this.isLocationHeader(name) || typeof value !== 'string') return
|
|
23
|
+
|
|
24
|
+
super.analyze(value)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
isLocationHeader (name) {
|
|
28
|
+
return name && name.trim().toLowerCase() === 'location'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_isVulnerable (value, iastContext) {
|
|
32
|
+
if (!value) return false
|
|
33
|
+
|
|
34
|
+
const ranges = getRanges(iastContext, value)
|
|
35
|
+
return ranges && ranges.length > 0 && !this._isRefererHeader(ranges)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_isRefererHeader (ranges) {
|
|
39
|
+
return ranges && ranges.every(range => range.iinfo.type === HTTP_REQUEST_HEADER_VALUE &&
|
|
40
|
+
range.iinfo.parameterName && range.iinfo.parameterName.toLowerCase() === 'referer')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_getExcludedPaths () {
|
|
44
|
+
return EXCLUDED_PATHS
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = new UnvalidatedRedirectAnalyzer()
|
|
@@ -38,7 +38,7 @@ class Analyzer extends Plugin {
|
|
|
38
38
|
|
|
39
39
|
_report (value, context) {
|
|
40
40
|
const evidence = this._getEvidence(value, context)
|
|
41
|
-
const location = this._getLocation()
|
|
41
|
+
const location = this._getLocation(value)
|
|
42
42
|
if (!this._isExcluded(location)) {
|
|
43
43
|
const spanId = context && context.rootSpan && context.rootSpan.context().toSpanId()
|
|
44
44
|
const vulnerability = this._createVulnerability(this._type, evidence, spanId, location)
|
|
@@ -47,7 +47,7 @@ class Analyzer extends Plugin {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
_reportIfVulnerable (value, context) {
|
|
50
|
-
if (this._isVulnerable(value, context) && this._checkOCE(context)) {
|
|
50
|
+
if (this._isVulnerable(value, context) && this._checkOCE(context, value)) {
|
|
51
51
|
this._report(value, context)
|
|
52
52
|
return true
|
|
53
53
|
}
|
|
@@ -78,7 +78,7 @@ class Analyzer extends Plugin {
|
|
|
78
78
|
for (let i = 0; i < values.length; i++) {
|
|
79
79
|
const value = values[i]
|
|
80
80
|
if (this._isVulnerable(value, iastContext)) {
|
|
81
|
-
if (this._checkOCE(iastContext)) {
|
|
81
|
+
if (this._checkOCE(iastContext, value)) {
|
|
82
82
|
this._report(value, iastContext)
|
|
83
83
|
}
|
|
84
84
|
break
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
|
+
|
|
3
|
+
const path = require('path')
|
|
4
|
+
|
|
2
5
|
const Analyzer = require('./vulnerability-analyzer')
|
|
3
6
|
const { WEAK_HASH } = require('../vulnerabilities')
|
|
4
7
|
|
|
@@ -8,6 +11,17 @@ const INSECURE_HASH_ALGORITHMS = new Set([
|
|
|
8
11
|
'RSA-SHA1', 'RSA-SHA1-2', 'sha1', 'md5-sha1', 'sha1WithRSAEncryption', 'ssl3-sha1'
|
|
9
12
|
].map(algorithm => algorithm.toLowerCase()))
|
|
10
13
|
|
|
14
|
+
const EXCLUDED_LOCATIONS = [
|
|
15
|
+
path.join('node_modules', 'etag', 'index.js'),
|
|
16
|
+
path.join('node_modules', 'redlock', 'dist', 'cjs'),
|
|
17
|
+
path.join('node_modules', 'ws', 'lib', 'websocket-server.js'),
|
|
18
|
+
path.join('node_modules', 'mysql2', 'lib', 'auth_41.js'),
|
|
19
|
+
path.join('node_modules', '@mikro-orm', 'core', 'utils', 'Utils.js')
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
const EXCLUDED_PATHS_FROM_STACK = [
|
|
23
|
+
path.join('node_modules', 'object-hash', path.sep)
|
|
24
|
+
]
|
|
11
25
|
class WeakHashAnalyzer extends Analyzer {
|
|
12
26
|
constructor () {
|
|
13
27
|
super(WEAK_HASH)
|
|
@@ -20,6 +34,16 @@ class WeakHashAnalyzer extends Analyzer {
|
|
|
20
34
|
}
|
|
21
35
|
return false
|
|
22
36
|
}
|
|
37
|
+
|
|
38
|
+
_isExcluded (location) {
|
|
39
|
+
return EXCLUDED_LOCATIONS.some(excludedLocation => {
|
|
40
|
+
return location.path.includes(excludedLocation)
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_getExcludedPaths () {
|
|
45
|
+
return EXCLUDED_PATHS_FROM_STACK
|
|
46
|
+
}
|
|
23
47
|
}
|
|
24
48
|
|
|
25
49
|
module.exports = new WeakHashAnalyzer()
|