dd-trace 5.19.0 → 5.21.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/ext/formats.d.ts +1 -0
- package/ext/formats.js +2 -1
- package/index.d.ts +2 -1
- package/init.js +3 -15
- package/package.json +5 -4
- package/packages/datadog-instrumentations/src/body-parser.js +14 -2
- package/packages/datadog-instrumentations/src/cucumber.js +10 -0
- package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -2
- package/packages/datadog-instrumentations/src/helpers/register.js +21 -12
- package/packages/datadog-instrumentations/src/http/client.js +7 -1
- package/packages/datadog-instrumentations/src/http/server.js +50 -13
- package/packages/datadog-instrumentations/src/mocha/main.js +111 -78
- package/packages/datadog-instrumentations/src/nyc.js +23 -0
- package/packages/datadog-instrumentations/src/process.js +29 -0
- package/packages/datadog-instrumentations/src/vitest.js +65 -25
- package/packages/datadog-plugin-aws-sdk/src/base.js +15 -1
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +1 -1
- package/packages/datadog-plugin-aws-sdk/src/services/sns.js +1 -1
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +3 -3
- package/packages/datadog-plugin-cucumber/src/index.js +12 -2
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +53 -12
- package/packages/datadog-plugin-jest/src/index.js +17 -4
- package/packages/datadog-plugin-mocha/src/index.js +25 -6
- package/packages/datadog-plugin-nyc/src/index.js +35 -0
- package/packages/datadog-plugin-playwright/src/index.js +9 -4
- package/packages/datadog-plugin-vitest/src/index.js +32 -5
- package/packages/dd-trace/src/appsec/blocking.js +10 -1
- package/packages/dd-trace/src/appsec/channels.js +4 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js +16 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/weak-hash-analyzer.js +2 -0
- package/packages/dd-trace/src/appsec/iast/taint-tracking/csi-methods.js +2 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +11 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/code-injection-sensitive-analyzer.js +25 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +2 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +2 -2
- package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
- package/packages/dd-trace/src/appsec/index.js +12 -7
- package/packages/dd-trace/src/appsec/rasp.js +121 -7
- package/packages/dd-trace/src/appsec/recommended.json +220 -2
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +40 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +2 -4
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +2 -4
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +8 -7
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -4
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +2 -4
- package/packages/dd-trace/src/ci-visibility/telemetry.js +29 -2
- package/packages/dd-trace/src/config.js +158 -153
- package/packages/dd-trace/src/data_streams.js +44 -0
- package/packages/dd-trace/src/datastreams/pathway.js +4 -2
- package/packages/dd-trace/src/log/index.js +32 -0
- package/packages/dd-trace/src/opentelemetry/context_manager.js +22 -39
- package/packages/dd-trace/src/opentelemetry/span_context.js +2 -2
- package/packages/dd-trace/src/opentelemetry/tracer.js +23 -14
- package/packages/dd-trace/src/opentelemetry/tracer_provider.js +9 -1
- package/packages/dd-trace/src/opentracing/propagation/log.js +1 -1
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +60 -0
- package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +43 -0
- package/packages/dd-trace/src/opentracing/span_context.js +1 -0
- package/packages/dd-trace/src/opentracing/tracer.js +10 -6
- package/packages/dd-trace/src/plugins/ci_plugin.js +11 -4
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/dd-trace/src/plugins/plugin.js +12 -1
- package/packages/dd-trace/src/plugins/util/git.js +14 -1
- package/packages/dd-trace/src/proxy.js +1 -0
- package/packages/dd-trace/src/telemetry/index.js +1 -1
- package/packages/dd-trace/src/tracer.js +2 -0
|
@@ -7,9 +7,16 @@ const {
|
|
|
7
7
|
getTestSuitePath,
|
|
8
8
|
getTestSuiteCommonTags,
|
|
9
9
|
TEST_SOURCE_FILE,
|
|
10
|
-
TEST_IS_RETRY
|
|
10
|
+
TEST_IS_RETRY,
|
|
11
|
+
TEST_CODE_COVERAGE_LINES_PCT,
|
|
12
|
+
TEST_CODE_OWNERS
|
|
11
13
|
} = require('../../dd-trace/src/plugins/util/test')
|
|
12
14
|
const { COMPONENT } = require('../../dd-trace/src/constants')
|
|
15
|
+
const {
|
|
16
|
+
TELEMETRY_EVENT_CREATED,
|
|
17
|
+
TELEMETRY_EVENT_FINISHED,
|
|
18
|
+
TELEMETRY_TEST_SESSION
|
|
19
|
+
} = require('../../dd-trace/src/ci-visibility/telemetry')
|
|
13
20
|
|
|
14
21
|
// Milliseconds that we subtract from the error test duration
|
|
15
22
|
// so that they do not overlap with the following test
|
|
@@ -64,6 +71,9 @@ class VitestPlugin extends CiPlugin {
|
|
|
64
71
|
const span = store?.span
|
|
65
72
|
|
|
66
73
|
if (span) {
|
|
74
|
+
this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', {
|
|
75
|
+
hasCodeowners: !!span.context()._tags[TEST_CODE_OWNERS]
|
|
76
|
+
})
|
|
67
77
|
span.setTag(TEST_STATUS, 'pass')
|
|
68
78
|
span.finish(this.taskToFinishTime.get(task))
|
|
69
79
|
finishAllTraceSpans(span)
|
|
@@ -75,6 +85,9 @@ class VitestPlugin extends CiPlugin {
|
|
|
75
85
|
const span = store?.span
|
|
76
86
|
|
|
77
87
|
if (span) {
|
|
88
|
+
this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', {
|
|
89
|
+
hasCodeowners: !!span.context()._tags[TEST_CODE_OWNERS]
|
|
90
|
+
})
|
|
78
91
|
span.setTag(TEST_STATUS, 'fail')
|
|
79
92
|
|
|
80
93
|
if (error) {
|
|
@@ -91,7 +104,7 @@ class VitestPlugin extends CiPlugin {
|
|
|
91
104
|
|
|
92
105
|
this.addSub('ci:vitest:test:skip', ({ testName, testSuiteAbsolutePath }) => {
|
|
93
106
|
const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
|
|
94
|
-
this.startTestSpan(
|
|
107
|
+
const testSpan = this.startTestSpan(
|
|
95
108
|
testName,
|
|
96
109
|
testSuite,
|
|
97
110
|
this.testSuiteSpan,
|
|
@@ -99,10 +112,15 @@ class VitestPlugin extends CiPlugin {
|
|
|
99
112
|
[TEST_SOURCE_FILE]: testSuite,
|
|
100
113
|
[TEST_STATUS]: 'skip'
|
|
101
114
|
}
|
|
102
|
-
)
|
|
115
|
+
)
|
|
116
|
+
this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', {
|
|
117
|
+
hasCodeowners: !!testSpan.context()._tags[TEST_CODE_OWNERS]
|
|
118
|
+
})
|
|
119
|
+
testSpan.finish()
|
|
103
120
|
})
|
|
104
121
|
|
|
105
|
-
this.addSub('ci:vitest:test-suite:start', (testSuiteAbsolutePath) => {
|
|
122
|
+
this.addSub('ci:vitest:test-suite:start', ({ testSuiteAbsolutePath, frameworkVersion }) => {
|
|
123
|
+
this.frameworkVersion = frameworkVersion
|
|
106
124
|
const testSessionSpanContext = this.tracer.extract('text_map', {
|
|
107
125
|
'x-datadog-trace-id': process.env.DD_CIVISIBILITY_TEST_SESSION_ID,
|
|
108
126
|
'x-datadog-parent-id': process.env.DD_CIVISIBILITY_TEST_MODULE_ID
|
|
@@ -123,6 +141,7 @@ class VitestPlugin extends CiPlugin {
|
|
|
123
141
|
...testSuiteMetadata
|
|
124
142
|
}
|
|
125
143
|
})
|
|
144
|
+
this.telemetry.ciVisEvent(TELEMETRY_EVENT_CREATED, 'suite')
|
|
126
145
|
const store = storage.getStore()
|
|
127
146
|
this.enter(testSuiteSpan, store)
|
|
128
147
|
this.testSuiteSpan = testSuiteSpan
|
|
@@ -136,6 +155,7 @@ class VitestPlugin extends CiPlugin {
|
|
|
136
155
|
span.finish()
|
|
137
156
|
finishAllTraceSpans(span)
|
|
138
157
|
}
|
|
158
|
+
this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
|
|
139
159
|
// TODO: too frequent flush - find for method in worker to decrease frequency
|
|
140
160
|
this.tracer._exporter.flush(onFinish)
|
|
141
161
|
})
|
|
@@ -149,16 +169,23 @@ class VitestPlugin extends CiPlugin {
|
|
|
149
169
|
}
|
|
150
170
|
})
|
|
151
171
|
|
|
152
|
-
this.addSub('ci:vitest:session:finish', ({ status, onFinish, error }) => {
|
|
172
|
+
this.addSub('ci:vitest:session:finish', ({ status, onFinish, error, testCodeCoverageLinesTotal }) => {
|
|
153
173
|
this.testSessionSpan.setTag(TEST_STATUS, status)
|
|
154
174
|
this.testModuleSpan.setTag(TEST_STATUS, status)
|
|
155
175
|
if (error) {
|
|
156
176
|
this.testModuleSpan.setTag('error', error)
|
|
157
177
|
this.testSessionSpan.setTag('error', error)
|
|
158
178
|
}
|
|
179
|
+
if (testCodeCoverageLinesTotal) {
|
|
180
|
+
this.testModuleSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal)
|
|
181
|
+
this.testSessionSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal)
|
|
182
|
+
}
|
|
159
183
|
this.testModuleSpan.finish()
|
|
184
|
+
this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
|
|
160
185
|
this.testSessionSpan.finish()
|
|
186
|
+
this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session')
|
|
161
187
|
finishAllTraceSpans(this.testSessionSpan)
|
|
188
|
+
this.telemetry.count(TELEMETRY_TEST_SESSION, { provider: this.ciProviderName })
|
|
162
189
|
this.tracer._exporter.flush(onFinish)
|
|
163
190
|
})
|
|
164
191
|
}
|
|
@@ -9,6 +9,8 @@ let templateHtml = blockedTemplates.html
|
|
|
9
9
|
let templateJson = blockedTemplates.json
|
|
10
10
|
let templateGraphqlJson = blockedTemplates.graphqlJson
|
|
11
11
|
|
|
12
|
+
const responseBlockedSet = new WeakSet()
|
|
13
|
+
|
|
12
14
|
const specificBlockingTypes = {
|
|
13
15
|
GRAPHQL: 'graphql'
|
|
14
16
|
}
|
|
@@ -117,6 +119,8 @@ function block (req, res, rootSpan, abortController, actionParameters) {
|
|
|
117
119
|
|
|
118
120
|
res.writeHead(statusCode, headers).end(body)
|
|
119
121
|
|
|
122
|
+
responseBlockedSet.add(res)
|
|
123
|
+
|
|
120
124
|
abortController?.abort()
|
|
121
125
|
}
|
|
122
126
|
|
|
@@ -144,11 +148,16 @@ function setTemplates (config) {
|
|
|
144
148
|
}
|
|
145
149
|
}
|
|
146
150
|
|
|
151
|
+
function isBlocked (res) {
|
|
152
|
+
return responseBlockedSet.has(res)
|
|
153
|
+
}
|
|
154
|
+
|
|
147
155
|
module.exports = {
|
|
148
156
|
addSpecificEndpoint,
|
|
149
157
|
block,
|
|
150
158
|
specificBlockingTypes,
|
|
151
159
|
getBlockingData,
|
|
152
160
|
getBlockingAction,
|
|
153
|
-
setTemplates
|
|
161
|
+
setTemplates,
|
|
162
|
+
isBlocked
|
|
154
163
|
}
|
|
@@ -19,5 +19,8 @@ module.exports = {
|
|
|
19
19
|
nextQueryParsed: dc.channel('apm:next:query-parsed'),
|
|
20
20
|
responseBody: dc.channel('datadog:express:response:json:start'),
|
|
21
21
|
responseWriteHead: dc.channel('apm:http:server:response:writeHead:start'),
|
|
22
|
-
httpClientRequestStart: dc.channel('apm:http:client:request:start')
|
|
22
|
+
httpClientRequestStart: dc.channel('apm:http:client:request:start'),
|
|
23
|
+
responseSetHeader: dc.channel('datadog:http:server:response:set-header:start'),
|
|
24
|
+
setUncaughtExceptionCaptureCallbackStart: dc.channel('datadog:process:setUncaughtExceptionCaptureCallback:start')
|
|
25
|
+
|
|
23
26
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
|
+
CODE_INJECTION_ANALYZER: require('./code-injection-analyzer'),
|
|
4
5
|
COMMAND_INJECTION_ANALYZER: require('./command-injection-analyzer'),
|
|
5
6
|
HARCODED_PASSWORD_ANALYZER: require('./hardcoded-password-analyzer'),
|
|
6
7
|
HARCODED_SECRET_ANALYZER: require('./hardcoded-secret-analyzer'),
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const InjectionAnalyzer = require('./injection-analyzer')
|
|
4
|
+
const { CODE_INJECTION } = require('../vulnerabilities')
|
|
5
|
+
|
|
6
|
+
class CodeInjectionAnalyzer extends InjectionAnalyzer {
|
|
7
|
+
constructor () {
|
|
8
|
+
super(CODE_INJECTION)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
onConfigure () {
|
|
12
|
+
this.addSub('datadog:eval:call', ({ script }) => this.analyze(script))
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = new CodeInjectionAnalyzer()
|
|
@@ -14,7 +14,8 @@ const csiMethods = [
|
|
|
14
14
|
{ src: 'toUpperCase', dst: 'stringCase' },
|
|
15
15
|
{ src: 'trim' },
|
|
16
16
|
{ src: 'trimEnd' },
|
|
17
|
-
{ src: 'trimStart', dst: 'trim' }
|
|
17
|
+
{ src: 'trimStart', dst: 'trim' },
|
|
18
|
+
{ src: 'eval', allowedWithoutCallee: true }
|
|
18
19
|
]
|
|
19
20
|
|
|
20
21
|
module.exports = {
|
|
@@ -10,6 +10,7 @@ const { isDebugAllowed } = require('../telemetry/verbosity')
|
|
|
10
10
|
const { taintObject } = require('./operations-taint-object')
|
|
11
11
|
|
|
12
12
|
const mathRandomCallCh = dc.channel('datadog:random:call')
|
|
13
|
+
const evalCallCh = dc.channel('datadog:eval:call')
|
|
13
14
|
|
|
14
15
|
const JSON_VALUE = 'json.value'
|
|
15
16
|
|
|
@@ -18,6 +19,7 @@ function noop (res) { return res }
|
|
|
18
19
|
// Otherwise you may end up rewriting a method and not providing its rewritten implementation
|
|
19
20
|
const TaintTrackingNoop = {
|
|
20
21
|
concat: noop,
|
|
22
|
+
eval: noop,
|
|
21
23
|
join: noop,
|
|
22
24
|
parse: noop,
|
|
23
25
|
plusOperator: noop,
|
|
@@ -136,6 +138,15 @@ function csiMethodsOverrides (getContext) {
|
|
|
136
138
|
return res
|
|
137
139
|
},
|
|
138
140
|
|
|
141
|
+
eval: function (res, fn, target, script) {
|
|
142
|
+
// eslint-disable-next-line no-eval
|
|
143
|
+
if (evalCallCh.hasSubscribers && fn === globalThis.eval) {
|
|
144
|
+
evalCallCh.publish({ script })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return res
|
|
148
|
+
},
|
|
149
|
+
|
|
139
150
|
parse: function (res, fn, target, json) {
|
|
140
151
|
if (fn === JSON.parse) {
|
|
141
152
|
try {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
module.exports = function extractSensitiveRanges (evidence) {
|
|
4
|
+
const newRanges = []
|
|
5
|
+
if (evidence.ranges[0].start > 0) {
|
|
6
|
+
newRanges.push({
|
|
7
|
+
start: 0,
|
|
8
|
+
end: evidence.ranges[0].start
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
for (let i = 0; i < evidence.ranges.length; i++) {
|
|
13
|
+
const currentRange = evidence.ranges[i]
|
|
14
|
+
const nextRange = evidence.ranges[i + 1]
|
|
15
|
+
|
|
16
|
+
const start = currentRange.end
|
|
17
|
+
const end = nextRange?.start || evidence.value.length
|
|
18
|
+
|
|
19
|
+
if (start < end) {
|
|
20
|
+
newRanges.push({ start, end })
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return newRanges
|
|
25
|
+
}
|
|
@@ -5,6 +5,7 @@ const vulnerabilities = require('../../vulnerabilities')
|
|
|
5
5
|
|
|
6
6
|
const { contains, intersects, remove } = require('./range-utils')
|
|
7
7
|
|
|
8
|
+
const codeInjectionSensitiveAnalyzer = require('./sensitive-analyzers/code-injection-sensitive-analyzer')
|
|
8
9
|
const commandSensitiveAnalyzer = require('./sensitive-analyzers/command-sensitive-analyzer')
|
|
9
10
|
const hardcodedPasswordAnalyzer = require('./sensitive-analyzers/hardcoded-password-analyzer')
|
|
10
11
|
const headerSensitiveAnalyzer = require('./sensitive-analyzers/header-sensitive-analyzer')
|
|
@@ -23,6 +24,7 @@ class SensitiveHandler {
|
|
|
23
24
|
this._valuePattern = new RegExp(DEFAULT_IAST_REDACTION_VALUE_PATTERN, 'gmi')
|
|
24
25
|
|
|
25
26
|
this._sensitiveAnalyzers = new Map()
|
|
27
|
+
this._sensitiveAnalyzers.set(vulnerabilities.CODE_INJECTION, codeInjectionSensitiveAnalyzer)
|
|
26
28
|
this._sensitiveAnalyzers.set(vulnerabilities.COMMAND_INJECTION, commandSensitiveAnalyzer)
|
|
27
29
|
this._sensitiveAnalyzers.set(vulnerabilities.NOSQL_MONGODB_INJECTION, jsonSensitiveAnalyzer)
|
|
28
30
|
this._sensitiveAnalyzers.set(vulnerabilities.LDAP_INJECTION, ldapSensitiveAnalyzer)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// eslint-disable-next-line max-len
|
|
2
|
-
const DEFAULT_IAST_REDACTION_NAME_PATTERN = '(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)'
|
|
2
|
+
const DEFAULT_IAST_REDACTION_NAME_PATTERN = '(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?|(?:sur|last)name|user(?:name)?|address|e?mail)'
|
|
3
3
|
// eslint-disable-next-line max-len
|
|
4
|
-
const DEFAULT_IAST_REDACTION_VALUE_PATTERN = '(?:bearer\\s+[a-z0-9\\._\\-]+|glpat-[\\w\\-]{20}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\\w=\\-]+\\.ey[I-L][\\w=\\-]+(?:\\.[\\w.+/=\\-]+)?|(?:[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY[\\-]{5}|ssh-rsa\\s*[a-z0-9/\\.+]{100,}))'
|
|
4
|
+
const DEFAULT_IAST_REDACTION_VALUE_PATTERN = '(?:bearer\\s+[a-z0-9\\._\\-]+|glpat-[\\w\\-]{20}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\\w=\\-]+\\.ey[I-L][\\w=\\-]+(?:\\.[\\w.+/=\\-]+)?|(?:[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY[\\-]{5}|ssh-rsa\\s*[a-z0-9/\\.+]{100,})|[\\w\\.-]+@[a-zA-Z\\d\\.-]+\\.[a-zA-Z]{2,})'
|
|
5
5
|
|
|
6
6
|
module.exports = {
|
|
7
7
|
DEFAULT_IAST_REDACTION_NAME_PATTERN,
|
|
@@ -13,7 +13,8 @@ const {
|
|
|
13
13
|
nextBodyParsed,
|
|
14
14
|
nextQueryParsed,
|
|
15
15
|
responseBody,
|
|
16
|
-
responseWriteHead
|
|
16
|
+
responseWriteHead,
|
|
17
|
+
responseSetHeader
|
|
17
18
|
} = require('./channels')
|
|
18
19
|
const waf = require('./waf')
|
|
19
20
|
const addresses = require('./addresses')
|
|
@@ -23,7 +24,7 @@ const apiSecuritySampler = require('./api_security_sampler')
|
|
|
23
24
|
const web = require('../plugins/util/web')
|
|
24
25
|
const { extractIp } = require('../plugins/util/ip_extractor')
|
|
25
26
|
const { HTTP_CLIENT_IP } = require('../../../../ext/tags')
|
|
26
|
-
const { block, setTemplates, getBlockingAction } = require('./blocking')
|
|
27
|
+
const { isBlocked, block, setTemplates, getBlockingAction } = require('./blocking')
|
|
27
28
|
const { passportTrackEvent } = require('./passport')
|
|
28
29
|
const { storage } = require('../../../datadog-core')
|
|
29
30
|
const graphql = require('./graphql')
|
|
@@ -62,6 +63,7 @@ function enable (_config) {
|
|
|
62
63
|
cookieParser.subscribe(onRequestCookieParser)
|
|
63
64
|
responseBody.subscribe(onResponseBody)
|
|
64
65
|
responseWriteHead.subscribe(onResponseWriteHead)
|
|
66
|
+
responseSetHeader.subscribe(onResponseSetHeader)
|
|
65
67
|
|
|
66
68
|
if (_config.appsec.eventTracking.enabled) {
|
|
67
69
|
passportVerify.subscribe(onPassportVerify)
|
|
@@ -223,11 +225,10 @@ function onPassportVerify ({ credentials, user }) {
|
|
|
223
225
|
}
|
|
224
226
|
|
|
225
227
|
const responseAnalyzedSet = new WeakSet()
|
|
226
|
-
const responseBlockedSet = new WeakSet()
|
|
227
228
|
|
|
228
229
|
function onResponseWriteHead ({ req, res, abortController, statusCode, responseHeaders }) {
|
|
229
230
|
// avoid "write after end" error
|
|
230
|
-
if (
|
|
231
|
+
if (isBlocked(res)) {
|
|
231
232
|
abortController?.abort()
|
|
232
233
|
return
|
|
233
234
|
}
|
|
@@ -255,15 +256,18 @@ function onResponseWriteHead ({ req, res, abortController, statusCode, responseH
|
|
|
255
256
|
handleResults(results, req, res, rootSpan, abortController)
|
|
256
257
|
}
|
|
257
258
|
|
|
259
|
+
function onResponseSetHeader ({ res, abortController }) {
|
|
260
|
+
if (isBlocked(res)) {
|
|
261
|
+
abortController?.abort()
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
258
265
|
function handleResults (actions, req, res, rootSpan, abortController) {
|
|
259
266
|
if (!actions || !req || !res || !rootSpan || !abortController) return
|
|
260
267
|
|
|
261
268
|
const blockingAction = getBlockingAction(actions)
|
|
262
269
|
if (blockingAction) {
|
|
263
270
|
block(req, res, rootSpan, abortController, blockingAction)
|
|
264
|
-
if (!abortController.signal || abortController.signal.aborted) {
|
|
265
|
-
responseBlockedSet.add(res)
|
|
266
|
-
}
|
|
267
271
|
}
|
|
268
272
|
}
|
|
269
273
|
|
|
@@ -290,6 +294,7 @@ function disable () {
|
|
|
290
294
|
if (responseBody.hasSubscribers) responseBody.unsubscribe(onResponseBody)
|
|
291
295
|
if (passportVerify.hasSubscribers) passportVerify.unsubscribe(onPassportVerify)
|
|
292
296
|
if (responseWriteHead.hasSubscribers) responseWriteHead.unsubscribe(onResponseWriteHead)
|
|
297
|
+
if (responseSetHeader.hasSubscribers) responseSetHeader.unsubscribe(onResponseSetHeader)
|
|
293
298
|
}
|
|
294
299
|
|
|
295
300
|
module.exports = {
|
|
@@ -3,23 +3,118 @@
|
|
|
3
3
|
const { storage } = require('../../../datadog-core')
|
|
4
4
|
const web = require('./../plugins/util/web')
|
|
5
5
|
const addresses = require('./addresses')
|
|
6
|
-
const { httpClientRequestStart } = require('./channels')
|
|
6
|
+
const { httpClientRequestStart, setUncaughtExceptionCaptureCallbackStart } = require('./channels')
|
|
7
7
|
const { reportStackTrace } = require('./stack_trace')
|
|
8
8
|
const waf = require('./waf')
|
|
9
|
+
const { getBlockingAction, block } = require('./blocking')
|
|
10
|
+
const log = require('../log')
|
|
9
11
|
|
|
10
12
|
const RULE_TYPES = {
|
|
11
13
|
SSRF: 'ssrf'
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
class DatadogRaspAbortError extends Error {
|
|
17
|
+
constructor (req, res, blockingAction) {
|
|
18
|
+
super('DatadogRaspAbortError')
|
|
19
|
+
this.name = 'DatadogRaspAbortError'
|
|
20
|
+
this.req = req
|
|
21
|
+
this.res = res
|
|
22
|
+
this.blockingAction = blockingAction
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let config, abortOnUncaughtException
|
|
27
|
+
|
|
28
|
+
function removeAllListeners (emitter, event) {
|
|
29
|
+
const listeners = emitter.listeners(event)
|
|
30
|
+
emitter.removeAllListeners(event)
|
|
31
|
+
|
|
32
|
+
let cleaned = false
|
|
33
|
+
return function () {
|
|
34
|
+
if (cleaned === true) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
cleaned = true
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < listeners.length; ++i) {
|
|
40
|
+
emitter.on(event, listeners[i])
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function findDatadogRaspAbortError (err, deep = 10) {
|
|
46
|
+
if (err instanceof DatadogRaspAbortError) {
|
|
47
|
+
return err
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (err.cause && deep > 0) {
|
|
51
|
+
return findDatadogRaspAbortError(err.cause, deep - 1)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function handleUncaughtExceptionMonitor (err) {
|
|
56
|
+
const abortError = findDatadogRaspAbortError(err)
|
|
57
|
+
if (!abortError) return
|
|
58
|
+
|
|
59
|
+
const { req, res, blockingAction } = abortError
|
|
60
|
+
block(req, res, web.root(req), null, blockingAction)
|
|
61
|
+
|
|
62
|
+
if (!process.hasUncaughtExceptionCaptureCallback()) {
|
|
63
|
+
const cleanUp = removeAllListeners(process, 'uncaughtException')
|
|
64
|
+
const handler = () => {
|
|
65
|
+
process.removeListener('uncaughtException', handler)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
process.removeListener('uncaughtException', handler)
|
|
70
|
+
cleanUp()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
process.on('uncaughtException', handler)
|
|
74
|
+
} else {
|
|
75
|
+
// uncaughtException event is not executed when hasUncaughtExceptionCaptureCallback is true
|
|
76
|
+
let previousCb
|
|
77
|
+
const cb = ({ currentCallback, abortController }) => {
|
|
78
|
+
setUncaughtExceptionCaptureCallbackStart.unsubscribe(cb)
|
|
79
|
+
if (!currentCallback) {
|
|
80
|
+
abortController.abort()
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
previousCb = currentCallback
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
setUncaughtExceptionCaptureCallbackStart.subscribe(cb)
|
|
88
|
+
|
|
89
|
+
process.setUncaughtExceptionCaptureCallback(null)
|
|
90
|
+
|
|
91
|
+
// For some reason, previous callback was defined before the instrumentation
|
|
92
|
+
// We can not restore it, so we let the app decide
|
|
93
|
+
if (previousCb) {
|
|
94
|
+
process.setUncaughtExceptionCaptureCallback(() => {
|
|
95
|
+
process.setUncaughtExceptionCaptureCallback(null)
|
|
96
|
+
process.setUncaughtExceptionCaptureCallback(previousCb)
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
15
101
|
|
|
16
102
|
function enable (_config) {
|
|
17
103
|
config = _config
|
|
18
104
|
httpClientRequestStart.subscribe(analyzeSsrf)
|
|
105
|
+
|
|
106
|
+
process.on('uncaughtExceptionMonitor', handleUncaughtExceptionMonitor)
|
|
107
|
+
abortOnUncaughtException = process.execArgv?.includes('--abort-on-uncaught-exception')
|
|
108
|
+
|
|
109
|
+
if (abortOnUncaughtException) {
|
|
110
|
+
log.warn('The --abort-on-uncaught-exception flag is enabled. The RASP module will not block operations.')
|
|
111
|
+
}
|
|
19
112
|
}
|
|
20
113
|
|
|
21
114
|
function disable () {
|
|
22
115
|
if (httpClientRequestStart.hasSubscribers) httpClientRequestStart.unsubscribe(analyzeSsrf)
|
|
116
|
+
|
|
117
|
+
process.off('uncaughtExceptionMonitor', handleUncaughtExceptionMonitor)
|
|
23
118
|
}
|
|
24
119
|
|
|
25
120
|
function analyzeSsrf (ctx) {
|
|
@@ -32,17 +127,18 @@ function analyzeSsrf (ctx) {
|
|
|
32
127
|
const persistent = {
|
|
33
128
|
[addresses.HTTP_OUTGOING_URL]: url
|
|
34
129
|
}
|
|
35
|
-
|
|
36
|
-
// block the request if SSRF attempt
|
|
130
|
+
|
|
37
131
|
const result = waf.run({ persistent }, req, RULE_TYPES.SSRF)
|
|
38
|
-
|
|
132
|
+
|
|
133
|
+
const res = store?.res
|
|
134
|
+
handleResult(result, req, res, ctx.abortController)
|
|
39
135
|
}
|
|
40
136
|
|
|
41
137
|
function getGenerateStackTraceAction (actions) {
|
|
42
138
|
return actions?.generate_stack
|
|
43
139
|
}
|
|
44
140
|
|
|
45
|
-
function handleResult (actions, req) {
|
|
141
|
+
function handleResult (actions, req, res, abortController) {
|
|
46
142
|
const generateStackTraceAction = getGenerateStackTraceAction(actions)
|
|
47
143
|
if (generateStackTraceAction && config.appsec.stackTrace.enabled) {
|
|
48
144
|
const rootSpan = web.root(req)
|
|
@@ -53,10 +149,28 @@ function handleResult (actions, req) {
|
|
|
53
149
|
config.appsec.stackTrace.maxStackTraces
|
|
54
150
|
)
|
|
55
151
|
}
|
|
152
|
+
|
|
153
|
+
if (!abortController || abortOnUncaughtException) return
|
|
154
|
+
|
|
155
|
+
const blockingAction = getBlockingAction(actions)
|
|
156
|
+
if (blockingAction) {
|
|
157
|
+
const rootSpan = web.root(req)
|
|
158
|
+
// Should block only in express
|
|
159
|
+
if (rootSpan?.context()._name === 'express.request') {
|
|
160
|
+
const abortError = new DatadogRaspAbortError(req, res, blockingAction)
|
|
161
|
+
abortController.abort(abortError)
|
|
162
|
+
|
|
163
|
+
// TODO Delete this when support for node 16 is removed
|
|
164
|
+
if (!abortController.signal.reason) {
|
|
165
|
+
abortController.signal.reason = abortError
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
56
169
|
}
|
|
57
170
|
|
|
58
171
|
module.exports = {
|
|
59
172
|
enable,
|
|
60
173
|
disable,
|
|
61
|
-
handleResult
|
|
174
|
+
handleResult,
|
|
175
|
+
handleUncaughtExceptionMonitor // exported only for testing purpose
|
|
62
176
|
}
|