dd-trace 5.21.0 → 5.23.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/index.d.ts +20 -8
- package/package.json +11 -5
- package/packages/datadog-instrumentations/src/aerospike.js +1 -1
- package/packages/datadog-instrumentations/src/apollo-server.js +1 -1
- package/packages/datadog-instrumentations/src/aws-sdk.js +4 -4
- package/packages/datadog-instrumentations/src/body-parser.js +4 -4
- package/packages/datadog-instrumentations/src/cassandra-driver.js +2 -2
- package/packages/datadog-instrumentations/src/child_process.js +2 -2
- package/packages/datadog-instrumentations/src/connect.js +4 -4
- package/packages/datadog-instrumentations/src/cookie-parser.js +4 -4
- package/packages/datadog-instrumentations/src/couchbase.js +12 -12
- package/packages/datadog-instrumentations/src/cucumber.js +294 -56
- package/packages/datadog-instrumentations/src/dns.js +10 -10
- package/packages/datadog-instrumentations/src/elasticsearch.js +4 -4
- package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +3 -3
- package/packages/datadog-instrumentations/src/express.js +4 -4
- package/packages/datadog-instrumentations/src/fastify.js +6 -6
- package/packages/datadog-instrumentations/src/fetch.js +1 -1
- package/packages/datadog-instrumentations/src/find-my-way.js +2 -2
- package/packages/datadog-instrumentations/src/fs.js +2 -2
- package/packages/datadog-instrumentations/src/google-cloud-pubsub.js +2 -2
- package/packages/datadog-instrumentations/src/grpc/client.js +4 -6
- package/packages/datadog-instrumentations/src/grpc/server.js +2 -2
- package/packages/datadog-instrumentations/src/hapi.js +10 -13
- package/packages/datadog-instrumentations/src/helpers/register.js +1 -1
- package/packages/datadog-instrumentations/src/http/client.js +3 -3
- package/packages/datadog-instrumentations/src/jest.js +8 -5
- package/packages/datadog-instrumentations/src/kafkajs.js +67 -31
- package/packages/datadog-instrumentations/src/knex.js +2 -2
- package/packages/datadog-instrumentations/src/koa.js +5 -5
- package/packages/datadog-instrumentations/src/ldapjs.js +1 -1
- package/packages/datadog-instrumentations/src/mariadb.js +8 -8
- package/packages/datadog-instrumentations/src/memcached.js +2 -2
- package/packages/datadog-instrumentations/src/microgateway-core.js +7 -5
- package/packages/datadog-instrumentations/src/mocha/common.js +1 -1
- package/packages/datadog-instrumentations/src/mocha/main.js +139 -53
- package/packages/datadog-instrumentations/src/mocha/utils.js +37 -18
- package/packages/datadog-instrumentations/src/mocha/worker.js +29 -1
- package/packages/datadog-instrumentations/src/mocha.js +4 -0
- package/packages/datadog-instrumentations/src/moleculer/server.js +2 -2
- package/packages/datadog-instrumentations/src/mongodb-core.js +7 -7
- package/packages/datadog-instrumentations/src/mongoose.js +5 -6
- package/packages/datadog-instrumentations/src/mysql.js +3 -3
- package/packages/datadog-instrumentations/src/mysql2.js +6 -6
- package/packages/datadog-instrumentations/src/net.js +2 -2
- package/packages/datadog-instrumentations/src/next.js +5 -5
- package/packages/datadog-instrumentations/src/openai.js +62 -71
- package/packages/datadog-instrumentations/src/oracledb.js +8 -8
- package/packages/datadog-instrumentations/src/passport-http.js +1 -1
- package/packages/datadog-instrumentations/src/passport-local.js +1 -1
- package/packages/datadog-instrumentations/src/passport-utils.js +1 -1
- package/packages/datadog-instrumentations/src/pg.js +60 -5
- package/packages/datadog-instrumentations/src/pino.js +4 -4
- package/packages/datadog-instrumentations/src/playwright.js +6 -4
- package/packages/datadog-instrumentations/src/redis.js +2 -2
- package/packages/datadog-instrumentations/src/restify.js +4 -4
- package/packages/datadog-instrumentations/src/rhea.js +4 -4
- package/packages/datadog-instrumentations/src/router.js +5 -5
- package/packages/datadog-instrumentations/src/sharedb.js +2 -2
- package/packages/datadog-instrumentations/src/vitest.js +188 -12
- package/packages/datadog-instrumentations/src/winston.js +2 -3
- package/packages/datadog-plugin-amqplib/src/consumer.js +1 -3
- package/packages/datadog-plugin-aws-sdk/src/base.js +33 -0
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +1 -1
- package/packages/datadog-plugin-aws-sdk/src/services/sns.js +2 -0
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -1
- package/packages/datadog-plugin-cucumber/src/index.js +24 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +39 -10
- package/packages/datadog-plugin-cypress/src/support.js +4 -1
- package/packages/datadog-plugin-hapi/src/index.js +2 -2
- package/packages/datadog-plugin-http/src/client.js +1 -42
- package/packages/datadog-plugin-http2/src/client.js +1 -26
- package/packages/datadog-plugin-jest/src/index.js +18 -1
- package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +20 -0
- package/packages/datadog-plugin-kafkajs/src/consumer.js +1 -2
- package/packages/datadog-plugin-kafkajs/src/index.js +3 -1
- package/packages/datadog-plugin-mocha/src/index.js +18 -0
- package/packages/datadog-plugin-openai/src/index.js +85 -65
- package/packages/datadog-plugin-playwright/src/index.js +9 -0
- package/packages/datadog-plugin-rhea/src/consumer.js +1 -3
- package/packages/datadog-plugin-vitest/src/index.js +68 -3
- package/packages/datadog-shimmer/src/shimmer.js +144 -10
- package/packages/dd-trace/src/appsec/addresses.js +3 -1
- package/packages/dd-trace/src/appsec/blocking.js +23 -17
- package/packages/dd-trace/src/appsec/channels.js +4 -2
- package/packages/dd-trace/src/appsec/graphql.js +3 -1
- package/packages/dd-trace/src/appsec/iast/iast-log.js +2 -1
- package/packages/dd-trace/src/appsec/rasp/index.js +103 -0
- package/packages/dd-trace/src/appsec/rasp/sql_injection.js +86 -0
- package/packages/dd-trace/src/appsec/rasp/ssrf.js +37 -0
- package/packages/dd-trace/src/appsec/rasp/utils.js +63 -0
- package/packages/dd-trace/src/appsec/remote_config/capabilities.js +2 -0
- package/packages/dd-trace/src/appsec/remote_config/index.js +16 -7
- package/packages/dd-trace/src/appsec/remote_config/manager.js +93 -52
- package/packages/dd-trace/src/appsec/rule_manager.js +8 -0
- package/packages/dd-trace/src/appsec/telemetry.js +3 -3
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +33 -14
- package/packages/dd-trace/src/appsec/waf/waf_manager.js +2 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +4 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +15 -1
- package/packages/dd-trace/src/config.js +100 -40
- package/packages/dd-trace/src/constants.js +11 -1
- package/packages/dd-trace/src/data_streams_context.js +3 -0
- package/packages/dd-trace/src/datastreams/fnv.js +23 -0
- package/packages/dd-trace/src/datastreams/pathway.js +12 -5
- package/packages/dd-trace/src/datastreams/processor.js +35 -0
- package/packages/dd-trace/src/datastreams/schemas/schema.js +8 -0
- package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +125 -0
- package/packages/dd-trace/src/datastreams/schemas/schema_sampler.js +29 -0
- package/packages/dd-trace/src/debugger/devtools_client/config.js +24 -0
- package/packages/dd-trace/src/debugger/devtools_client/index.js +57 -0
- package/packages/dd-trace/src/debugger/devtools_client/inspector_promises_polyfill.js +23 -0
- package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +164 -0
- package/packages/dd-trace/src/debugger/devtools_client/send.js +28 -0
- package/packages/dd-trace/src/debugger/devtools_client/session.js +7 -0
- package/packages/dd-trace/src/debugger/devtools_client/state.js +47 -0
- package/packages/dd-trace/src/debugger/devtools_client/status.js +109 -0
- package/packages/dd-trace/src/debugger/index.js +92 -0
- package/packages/dd-trace/src/encode/agentless-ci-visibility.js +29 -2
- package/packages/dd-trace/src/exporters/common/request.js +1 -1
- package/packages/dd-trace/src/lambda/handler.js +1 -0
- package/packages/dd-trace/src/lambda/index.js +12 -1
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +1 -6
- package/packages/dd-trace/src/payload-tagging/config/aws.json +30 -0
- package/packages/dd-trace/src/payload-tagging/config/index.js +30 -0
- package/packages/dd-trace/src/payload-tagging/index.js +93 -0
- package/packages/dd-trace/src/payload-tagging/tagging.js +83 -0
- package/packages/dd-trace/src/plugin_manager.js +11 -10
- package/packages/dd-trace/src/plugins/ci_plugin.js +33 -8
- package/packages/dd-trace/src/plugins/util/env.js +5 -2
- package/packages/dd-trace/src/plugins/util/test.js +24 -4
- package/packages/dd-trace/src/profiler.js +15 -5
- package/packages/dd-trace/src/profiling/config.js +7 -4
- package/packages/dd-trace/src/profiling/exporter_cli.js +13 -1
- package/packages/dd-trace/src/profiling/exporters/agent.js +8 -2
- package/packages/dd-trace/src/profiling/profiler.js +0 -9
- package/packages/dd-trace/src/profiling/profilers/event_plugins/dns.js +13 -0
- package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_lookup.js +16 -0
- package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_lookupservice.js +16 -0
- package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_resolve.js +24 -0
- package/packages/dd-trace/src/profiling/profilers/event_plugins/dns_reverse.js +16 -0
- package/packages/dd-trace/src/profiling/profilers/event_plugins/event.js +48 -0
- package/packages/dd-trace/src/profiling/profilers/event_plugins/net.js +24 -0
- package/packages/dd-trace/src/profiling/profilers/events.js +108 -32
- package/packages/dd-trace/src/profiling/profilers/shared.js +5 -0
- package/packages/dd-trace/src/profiling/profilers/wall.js +9 -3
- package/packages/dd-trace/src/profiling/ssi-heuristics.js +59 -60
- package/packages/dd-trace/src/proxy.js +31 -24
- package/packages/dd-trace/src/span_stats.js +4 -2
- package/packages/dd-trace/src/telemetry/index.js +23 -6
- package/packages/dd-trace/src/telemetry/logs/index.js +20 -0
- package/packages/dd-trace/src/appsec/rasp.js +0 -176
|
@@ -6,10 +6,18 @@ const {
|
|
|
6
6
|
finishAllTraceSpans,
|
|
7
7
|
getTestSuitePath,
|
|
8
8
|
getTestSuiteCommonTags,
|
|
9
|
+
getTestSessionName,
|
|
10
|
+
getIsFaultyEarlyFlakeDetection,
|
|
9
11
|
TEST_SOURCE_FILE,
|
|
10
12
|
TEST_IS_RETRY,
|
|
11
13
|
TEST_CODE_COVERAGE_LINES_PCT,
|
|
12
|
-
TEST_CODE_OWNERS
|
|
14
|
+
TEST_CODE_OWNERS,
|
|
15
|
+
TEST_LEVEL_EVENT_TYPES,
|
|
16
|
+
TEST_SESSION_NAME,
|
|
17
|
+
TEST_SOURCE_START,
|
|
18
|
+
TEST_IS_NEW,
|
|
19
|
+
TEST_EARLY_FLAKE_ENABLED,
|
|
20
|
+
TEST_EARLY_FLAKE_ABORT_REASON
|
|
13
21
|
} = require('../../dd-trace/src/plugins/util/test')
|
|
14
22
|
const { COMPONENT } = require('../../dd-trace/src/constants')
|
|
15
23
|
const {
|
|
@@ -33,7 +41,26 @@ class VitestPlugin extends CiPlugin {
|
|
|
33
41
|
|
|
34
42
|
this.taskToFinishTime = new WeakMap()
|
|
35
43
|
|
|
36
|
-
this.addSub('ci:vitest:test:
|
|
44
|
+
this.addSub('ci:vitest:test:is-new', ({ knownTests, testSuiteAbsolutePath, testName, onDone }) => {
|
|
45
|
+
const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
|
|
46
|
+
const testsForThisTestSuite = knownTests[testSuite] || []
|
|
47
|
+
onDone(!testsForThisTestSuite.includes(testName))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
this.addSub('ci:vitest:is-early-flake-detection-faulty', ({
|
|
51
|
+
knownTests,
|
|
52
|
+
testFilepaths,
|
|
53
|
+
onDone
|
|
54
|
+
}) => {
|
|
55
|
+
const isFaulty = getIsFaultyEarlyFlakeDetection(
|
|
56
|
+
testFilepaths.map(testFilepath => getTestSuitePath(testFilepath, this.repositoryRoot)),
|
|
57
|
+
knownTests,
|
|
58
|
+
this.libraryConfig.earlyFlakeDetectionFaultyThreshold
|
|
59
|
+
)
|
|
60
|
+
onDone(isFaulty)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
this.addSub('ci:vitest:test:start', ({ testName, testSuiteAbsolutePath, isRetry, isNew }) => {
|
|
37
64
|
const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
|
|
38
65
|
const store = storage.getStore()
|
|
39
66
|
|
|
@@ -43,6 +70,9 @@ class VitestPlugin extends CiPlugin {
|
|
|
43
70
|
if (isRetry) {
|
|
44
71
|
extraTags[TEST_IS_RETRY] = 'true'
|
|
45
72
|
}
|
|
73
|
+
if (isNew) {
|
|
74
|
+
extraTags[TEST_IS_NEW] = 'true'
|
|
75
|
+
}
|
|
46
76
|
|
|
47
77
|
const span = this.startTestSpan(
|
|
48
78
|
testName,
|
|
@@ -110,6 +140,7 @@ class VitestPlugin extends CiPlugin {
|
|
|
110
140
|
this.testSuiteSpan,
|
|
111
141
|
{
|
|
112
142
|
[TEST_SOURCE_FILE]: testSuite,
|
|
143
|
+
[TEST_SOURCE_START]: 1, // we can't get the proper start line in vitest
|
|
113
144
|
[TEST_STATUS]: 'skip'
|
|
114
145
|
}
|
|
115
146
|
)
|
|
@@ -120,12 +151,25 @@ class VitestPlugin extends CiPlugin {
|
|
|
120
151
|
})
|
|
121
152
|
|
|
122
153
|
this.addSub('ci:vitest:test-suite:start', ({ testSuiteAbsolutePath, frameworkVersion }) => {
|
|
154
|
+
this.command = process.env.DD_CIVISIBILITY_TEST_COMMAND
|
|
123
155
|
this.frameworkVersion = frameworkVersion
|
|
124
156
|
const testSessionSpanContext = this.tracer.extract('text_map', {
|
|
125
157
|
'x-datadog-trace-id': process.env.DD_CIVISIBILITY_TEST_SESSION_ID,
|
|
126
158
|
'x-datadog-parent-id': process.env.DD_CIVISIBILITY_TEST_MODULE_ID
|
|
127
159
|
})
|
|
128
160
|
|
|
161
|
+
// test suites run in a different process, so they also need to init the metadata dictionary
|
|
162
|
+
const testSessionName = getTestSessionName(this.config, this.command, this.testEnvironmentMetadata)
|
|
163
|
+
const metadataTags = {}
|
|
164
|
+
for (const testLevel of TEST_LEVEL_EVENT_TYPES) {
|
|
165
|
+
metadataTags[testLevel] = {
|
|
166
|
+
[TEST_SESSION_NAME]: testSessionName
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (this.tracer._exporter.setMetadataTags) {
|
|
170
|
+
this.tracer._exporter.setMetadataTags(metadataTags)
|
|
171
|
+
}
|
|
172
|
+
|
|
129
173
|
const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
|
|
130
174
|
const testSuiteMetadata = getTestSuiteCommonTags(
|
|
131
175
|
this.command,
|
|
@@ -133,6 +177,14 @@ class VitestPlugin extends CiPlugin {
|
|
|
133
177
|
testSuite,
|
|
134
178
|
'vitest'
|
|
135
179
|
)
|
|
180
|
+
testSuiteMetadata[TEST_SOURCE_FILE] = testSuite
|
|
181
|
+
testSuiteMetadata[TEST_SOURCE_START] = 1
|
|
182
|
+
|
|
183
|
+
const codeOwners = this.getCodeOwners(testSuiteMetadata)
|
|
184
|
+
if (codeOwners) {
|
|
185
|
+
testSuiteMetadata[TEST_CODE_OWNERS] = codeOwners
|
|
186
|
+
}
|
|
187
|
+
|
|
136
188
|
const testSuiteSpan = this.tracer.startSpan('vitest.test_suite', {
|
|
137
189
|
childOf: testSessionSpanContext,
|
|
138
190
|
tags: {
|
|
@@ -169,7 +221,14 @@ class VitestPlugin extends CiPlugin {
|
|
|
169
221
|
}
|
|
170
222
|
})
|
|
171
223
|
|
|
172
|
-
this.addSub('ci:vitest:session:finish', ({
|
|
224
|
+
this.addSub('ci:vitest:session:finish', ({
|
|
225
|
+
status,
|
|
226
|
+
error,
|
|
227
|
+
testCodeCoverageLinesTotal,
|
|
228
|
+
isEarlyFlakeDetectionEnabled,
|
|
229
|
+
isEarlyFlakeDetectionFaulty,
|
|
230
|
+
onFinish
|
|
231
|
+
}) => {
|
|
173
232
|
this.testSessionSpan.setTag(TEST_STATUS, status)
|
|
174
233
|
this.testModuleSpan.setTag(TEST_STATUS, status)
|
|
175
234
|
if (error) {
|
|
@@ -180,6 +239,12 @@ class VitestPlugin extends CiPlugin {
|
|
|
180
239
|
this.testModuleSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal)
|
|
181
240
|
this.testSessionSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal)
|
|
182
241
|
}
|
|
242
|
+
if (isEarlyFlakeDetectionEnabled) {
|
|
243
|
+
this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ENABLED, 'true')
|
|
244
|
+
}
|
|
245
|
+
if (isEarlyFlakeDetectionFaulty) {
|
|
246
|
+
this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ABORT_REASON, 'faulty')
|
|
247
|
+
}
|
|
183
248
|
this.testModuleSpan.finish()
|
|
184
249
|
this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
|
|
185
250
|
this.testSessionSpan.finish()
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const log = require('../../dd-trace/src/log')
|
|
4
|
+
|
|
3
5
|
// Use a weak map to avoid polluting the wrapped function/method.
|
|
4
6
|
const unwrappers = new WeakMap()
|
|
5
7
|
|
|
@@ -18,9 +20,12 @@ function copyProperties (original, wrapped) {
|
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
function wrapFunction (original, wrapper) {
|
|
24
|
+
if (typeof original === 'function') assertNotClass(original)
|
|
25
|
+
// TODO This needs to be re-done so that this and wrapMethod are distinct.
|
|
26
|
+
const target = { func: original }
|
|
27
|
+
wrapMethod(target, 'func', wrapper, typeof original !== 'function')
|
|
28
|
+
let delegate = target.func
|
|
24
29
|
|
|
25
30
|
const shim = function shim () {
|
|
26
31
|
return delegate.apply(this, arguments)
|
|
@@ -30,17 +35,144 @@ function wrapFn (original, delegate) {
|
|
|
30
35
|
delegate = original
|
|
31
36
|
})
|
|
32
37
|
|
|
33
|
-
copyProperties(original, shim)
|
|
38
|
+
if (typeof original === 'function') copyProperties(original, shim)
|
|
34
39
|
|
|
35
40
|
return shim
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
const wrapFn = function (original, delegate) {
|
|
44
|
+
throw new Error('calling `wrap()` with 2 args is deprecated. Use wrapFunction instead.')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// This is only used in safe mode. It's a simple state machine to track if the
|
|
48
|
+
// original method was called and if it returned. We need this to determine if
|
|
49
|
+
// an error was thrown by the original method, or by us. We'll use one of these
|
|
50
|
+
// per call to a wrapped method.
|
|
51
|
+
class CallState {
|
|
52
|
+
constructor () {
|
|
53
|
+
this.called = false
|
|
54
|
+
this.completed = false
|
|
55
|
+
this.retVal = undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
startCall () {
|
|
59
|
+
this.called = true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
endCall (retVal) {
|
|
63
|
+
this.completed = true
|
|
64
|
+
this.retVal = retVal
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isPromise (obj) {
|
|
69
|
+
return obj && typeof obj === 'object' && typeof obj.then === 'function'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let safeMode = !!process.env.DD_INEJCTION_ENABLED
|
|
73
|
+
function setSafe (value) {
|
|
74
|
+
safeMode = value
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function wrapMethod (target, name, wrapper, noAssert) {
|
|
78
|
+
if (!noAssert) {
|
|
79
|
+
assertMethod(target, name)
|
|
80
|
+
assertFunction(wrapper)
|
|
81
|
+
}
|
|
41
82
|
|
|
42
83
|
const original = target[name]
|
|
43
|
-
|
|
84
|
+
let wrapped
|
|
85
|
+
|
|
86
|
+
if (safeMode && original) {
|
|
87
|
+
// In this mode, we make a best-effort attempt to handle errors that are thrown
|
|
88
|
+
// by us, rather than wrapped code. With such errors, we log them, and then attempt
|
|
89
|
+
// to return the result as if no wrapping was done at all.
|
|
90
|
+
//
|
|
91
|
+
// Caveats:
|
|
92
|
+
// * If the original function is called in a later iteration of the event loop,
|
|
93
|
+
// and we throw _then_, then it won't be caught by this. In practice, we always call
|
|
94
|
+
// the original function synchronously, so this is not a problem.
|
|
95
|
+
// * While async errors are dealt with here, errors in callbacks are not. This
|
|
96
|
+
// is because we don't necessarily know _for sure_ that any function arguments
|
|
97
|
+
// are wrapped by us. We could wrap them all anyway and just make that assumption,
|
|
98
|
+
// or just assume that the last argument is always a callback set by us if it's a
|
|
99
|
+
// function, but those don't seem like things we can rely on. We could add a
|
|
100
|
+
// `shimmer.markCallbackAsWrapped()` function that's a no-op outside safe-mode,
|
|
101
|
+
// but that means modifying every instrumentation. Even then, the complexity of
|
|
102
|
+
// this code increases because then we'd need to effectively do the reverse of
|
|
103
|
+
// what we're doing for synchronous functions. This is a TODO.
|
|
104
|
+
|
|
105
|
+
// We're going to hold on to current callState in this variable in this scope,
|
|
106
|
+
// which is fine because any time we reference it, we're referencing it synchronously.
|
|
107
|
+
// We'll use it in the our wrapper (which, again, is called syncrhonously), and in the
|
|
108
|
+
// errorHandler, which will already have been bound to this callState.
|
|
109
|
+
let currentCallState
|
|
110
|
+
|
|
111
|
+
// Rather than calling the original function directly from the shim wrapper, we wrap
|
|
112
|
+
// it again so that we can track if it was called and if it returned. This is because
|
|
113
|
+
// we need to know if an error was thrown by the original function, or by us.
|
|
114
|
+
// We could do this inside the `wrapper` function defined below, which would simplify
|
|
115
|
+
// managing the callState, but then we'd be calling `wrapper` on each invocation, so
|
|
116
|
+
// instead we do it here, once.
|
|
117
|
+
const innerWrapped = wrapper(function (...args) {
|
|
118
|
+
// We need to stash the callState here because of recursion.
|
|
119
|
+
const callState = currentCallState
|
|
120
|
+
callState.startCall()
|
|
121
|
+
const retVal = original.apply(this, args)
|
|
122
|
+
if (isPromise(retVal)) {
|
|
123
|
+
retVal.then(callState.endCall.bind(callState))
|
|
124
|
+
} else {
|
|
125
|
+
callState.endCall(retVal)
|
|
126
|
+
}
|
|
127
|
+
return retVal
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// This is the crux of what we're doing in safe mode. It handles errors
|
|
131
|
+
// that _we_ cause, by logging them, and transparently providing results
|
|
132
|
+
// as if no wrapping was done at all. That means detecting (via callState)
|
|
133
|
+
// whether the function has already run or not, and if it has, returning
|
|
134
|
+
// the result, and otherwise calling the original function unwrapped.
|
|
135
|
+
const handleError = function (args, callState, e) {
|
|
136
|
+
if (callState.completed) {
|
|
137
|
+
// error was thrown after original function returned/resolved, so
|
|
138
|
+
// it was us. log it.
|
|
139
|
+
log.error(e)
|
|
140
|
+
// original ran and returned something. return it.
|
|
141
|
+
return callState.retVal
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!callState.called) {
|
|
145
|
+
// error was thrown before original function was called, so
|
|
146
|
+
// it was us. log it.
|
|
147
|
+
log.error(e)
|
|
148
|
+
// original never ran. call it unwrapped.
|
|
149
|
+
return original.apply(this, args)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// error was thrown during original function execution, so
|
|
153
|
+
// it was them. throw.
|
|
154
|
+
throw e
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// The wrapped function is the one that will be called by the user.
|
|
158
|
+
// It calls our version of the original function, which manages the
|
|
159
|
+
// callState. That way when we use the errorHandler, it can tell where
|
|
160
|
+
// the error originated.
|
|
161
|
+
wrapped = function (...args) {
|
|
162
|
+
currentCallState = new CallState()
|
|
163
|
+
const errorHandler = handleError.bind(this, args, currentCallState)
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const retVal = innerWrapped.apply(this, args)
|
|
167
|
+
return isPromise(retVal) ? retVal.catch(errorHandler) : retVal
|
|
168
|
+
} catch (e) {
|
|
169
|
+
return errorHandler(e)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// In non-safe mode, we just wrap the original function directly.
|
|
174
|
+
wrapped = wrapper(original)
|
|
175
|
+
}
|
|
44
176
|
const descriptor = Object.getOwnPropertyDescriptor(target, name)
|
|
45
177
|
|
|
46
178
|
const attributes = {
|
|
@@ -48,7 +180,7 @@ function wrapMethod (target, name, wrapper) {
|
|
|
48
180
|
...descriptor
|
|
49
181
|
}
|
|
50
182
|
|
|
51
|
-
copyProperties(original, wrapped)
|
|
183
|
+
if (typeof original === 'function') copyProperties(original, wrapped)
|
|
52
184
|
|
|
53
185
|
if (descriptor) {
|
|
54
186
|
unwrappers.set(wrapped, () => Object.defineProperty(target, name, descriptor))
|
|
@@ -156,7 +288,9 @@ function assertNotClass (target) {
|
|
|
156
288
|
|
|
157
289
|
module.exports = {
|
|
158
290
|
wrap,
|
|
291
|
+
wrapFunction,
|
|
159
292
|
massWrap,
|
|
160
293
|
unwrap,
|
|
161
|
-
massUnwrap
|
|
294
|
+
massUnwrap,
|
|
295
|
+
setSafe
|
|
162
296
|
}
|
|
@@ -9,6 +9,8 @@ let templateHtml = blockedTemplates.html
|
|
|
9
9
|
let templateJson = blockedTemplates.json
|
|
10
10
|
let templateGraphqlJson = blockedTemplates.graphqlJson
|
|
11
11
|
|
|
12
|
+
let defaultBlockingActionParameters
|
|
13
|
+
|
|
12
14
|
const responseBlockedSet = new WeakSet()
|
|
13
15
|
|
|
14
16
|
const specificBlockingTypes = {
|
|
@@ -23,7 +25,7 @@ function addSpecificEndpoint (method, url, type) {
|
|
|
23
25
|
detectedSpecificEndpoints[getSpecificKey(method, url)] = type
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
function getBlockWithRedirectData (
|
|
28
|
+
function getBlockWithRedirectData (actionParameters) {
|
|
27
29
|
let statusCode = actionParameters.status_code
|
|
28
30
|
if (!statusCode || statusCode < 300 || statusCode >= 400) {
|
|
29
31
|
statusCode = 303
|
|
@@ -32,10 +34,6 @@ function getBlockWithRedirectData (rootSpan, actionParameters) {
|
|
|
32
34
|
Location: actionParameters.location
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
rootSpan.addTags({
|
|
36
|
-
'appsec.blocked': 'true'
|
|
37
|
-
})
|
|
38
|
-
|
|
39
37
|
return { headers, statusCode }
|
|
40
38
|
}
|
|
41
39
|
|
|
@@ -49,7 +47,7 @@ function getSpecificBlockingData (type) {
|
|
|
49
47
|
}
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
function getBlockWithContentData (req, specificType,
|
|
50
|
+
function getBlockWithContentData (req, specificType, actionParameters) {
|
|
53
51
|
let type
|
|
54
52
|
let body
|
|
55
53
|
|
|
@@ -90,28 +88,28 @@ function getBlockWithContentData (req, specificType, rootSpan, actionParameters)
|
|
|
90
88
|
'Content-Length': Buffer.byteLength(body)
|
|
91
89
|
}
|
|
92
90
|
|
|
93
|
-
rootSpan.addTags({
|
|
94
|
-
'appsec.blocked': 'true'
|
|
95
|
-
})
|
|
96
|
-
|
|
97
91
|
return { body, statusCode, headers }
|
|
98
92
|
}
|
|
99
93
|
|
|
100
|
-
function getBlockingData (req, specificType,
|
|
94
|
+
function getBlockingData (req, specificType, actionParameters) {
|
|
101
95
|
if (actionParameters?.location) {
|
|
102
|
-
return getBlockWithRedirectData(
|
|
96
|
+
return getBlockWithRedirectData(actionParameters)
|
|
103
97
|
} else {
|
|
104
|
-
return getBlockWithContentData(req, specificType,
|
|
98
|
+
return getBlockWithContentData(req, specificType, actionParameters)
|
|
105
99
|
}
|
|
106
100
|
}
|
|
107
101
|
|
|
108
|
-
function block (req, res, rootSpan, abortController, actionParameters) {
|
|
102
|
+
function block (req, res, rootSpan, abortController, actionParameters = defaultBlockingActionParameters) {
|
|
109
103
|
if (res.headersSent) {
|
|
110
104
|
log.warn('Cannot send blocking response when headers have already been sent')
|
|
111
105
|
return
|
|
112
106
|
}
|
|
113
107
|
|
|
114
|
-
const { body, headers, statusCode } = getBlockingData(req, null,
|
|
108
|
+
const { body, headers, statusCode } = getBlockingData(req, null, actionParameters)
|
|
109
|
+
|
|
110
|
+
rootSpan.addTags({
|
|
111
|
+
'appsec.blocked': 'true'
|
|
112
|
+
})
|
|
115
113
|
|
|
116
114
|
for (const headerName of res.getHeaderNames()) {
|
|
117
115
|
res.removeHeader(headerName)
|
|
@@ -125,7 +123,8 @@ function block (req, res, rootSpan, abortController, actionParameters) {
|
|
|
125
123
|
}
|
|
126
124
|
|
|
127
125
|
function getBlockingAction (actions) {
|
|
128
|
-
|
|
126
|
+
// waf only returns one action, but it prioritizes redirect over block
|
|
127
|
+
return actions?.redirect_request || actions?.block_request
|
|
129
128
|
}
|
|
130
129
|
|
|
131
130
|
function setTemplates (config) {
|
|
@@ -152,6 +151,12 @@ function isBlocked (res) {
|
|
|
152
151
|
return responseBlockedSet.has(res)
|
|
153
152
|
}
|
|
154
153
|
|
|
154
|
+
function setDefaultBlockingActionParameters (actions) {
|
|
155
|
+
const blockAction = actions?.find(action => action.id === 'block')
|
|
156
|
+
|
|
157
|
+
defaultBlockingActionParameters = blockAction?.parameters
|
|
158
|
+
}
|
|
159
|
+
|
|
155
160
|
module.exports = {
|
|
156
161
|
addSpecificEndpoint,
|
|
157
162
|
block,
|
|
@@ -159,5 +164,6 @@ module.exports = {
|
|
|
159
164
|
getBlockingData,
|
|
160
165
|
getBlockingAction,
|
|
161
166
|
setTemplates,
|
|
162
|
-
isBlocked
|
|
167
|
+
isBlocked,
|
|
168
|
+
setDefaultBlockingActionParameters
|
|
163
169
|
}
|
|
@@ -21,6 +21,8 @@ module.exports = {
|
|
|
21
21
|
responseWriteHead: dc.channel('apm:http:server:response:writeHead:start'),
|
|
22
22
|
httpClientRequestStart: dc.channel('apm:http:client:request:start'),
|
|
23
23
|
responseSetHeader: dc.channel('datadog:http:server:response:set-header:start'),
|
|
24
|
-
setUncaughtExceptionCaptureCallbackStart: dc.channel('datadog:process:setUncaughtExceptionCaptureCallback:start')
|
|
25
|
-
|
|
24
|
+
setUncaughtExceptionCaptureCallbackStart: dc.channel('datadog:process:setUncaughtExceptionCaptureCallback:start'),
|
|
25
|
+
pgQueryStart: dc.channel('apm:pg:query:start'),
|
|
26
|
+
pgPoolQueryStart: dc.channel('datadog:pg:pool:query:start'),
|
|
27
|
+
wafRunFinished: dc.channel('datadog:waf:run:finish')
|
|
26
28
|
}
|
|
@@ -94,11 +94,13 @@ function beforeWriteApolloGraphqlResponse ({ abortController, abortData }) {
|
|
|
94
94
|
const rootSpan = web.root(req)
|
|
95
95
|
if (!rootSpan) return
|
|
96
96
|
|
|
97
|
-
const blockingData = getBlockingData(req, specificBlockingTypes.GRAPHQL,
|
|
97
|
+
const blockingData = getBlockingData(req, specificBlockingTypes.GRAPHQL, requestData.wafAction)
|
|
98
98
|
abortData.statusCode = blockingData.statusCode
|
|
99
99
|
abortData.headers = blockingData.headers
|
|
100
100
|
abortData.message = blockingData.body
|
|
101
101
|
|
|
102
|
+
rootSpan.setTag('appsec.blocked', 'true')
|
|
103
|
+
|
|
102
104
|
abortController?.abort()
|
|
103
105
|
}
|
|
104
106
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const web = require('../../plugins/util/web')
|
|
4
|
+
const { setUncaughtExceptionCaptureCallbackStart } = require('../channels')
|
|
5
|
+
const { block } = require('../blocking')
|
|
6
|
+
const ssrf = require('./ssrf')
|
|
7
|
+
const sqli = require('./sql_injection')
|
|
8
|
+
|
|
9
|
+
const { DatadogRaspAbortError } = require('./utils')
|
|
10
|
+
|
|
11
|
+
function removeAllListeners (emitter, event) {
|
|
12
|
+
const listeners = emitter.listeners(event)
|
|
13
|
+
emitter.removeAllListeners(event)
|
|
14
|
+
|
|
15
|
+
let cleaned = false
|
|
16
|
+
return function () {
|
|
17
|
+
if (cleaned === true) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
cleaned = true
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < listeners.length; ++i) {
|
|
23
|
+
emitter.on(event, listeners[i])
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function findDatadogRaspAbortError (err, deep = 10) {
|
|
29
|
+
if (err instanceof DatadogRaspAbortError) {
|
|
30
|
+
return err
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (err.cause && deep > 0) {
|
|
34
|
+
return findDatadogRaspAbortError(err.cause, deep - 1)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function handleUncaughtExceptionMonitor (err) {
|
|
39
|
+
const abortError = findDatadogRaspAbortError(err)
|
|
40
|
+
if (!abortError) return
|
|
41
|
+
|
|
42
|
+
const { req, res, blockingAction } = abortError
|
|
43
|
+
block(req, res, web.root(req), null, blockingAction)
|
|
44
|
+
|
|
45
|
+
if (!process.hasUncaughtExceptionCaptureCallback()) {
|
|
46
|
+
const cleanUp = removeAllListeners(process, 'uncaughtException')
|
|
47
|
+
const handler = () => {
|
|
48
|
+
process.removeListener('uncaughtException', handler)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
process.removeListener('uncaughtException', handler)
|
|
53
|
+
cleanUp()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
process.on('uncaughtException', handler)
|
|
57
|
+
} else {
|
|
58
|
+
// uncaughtException event is not executed when hasUncaughtExceptionCaptureCallback is true
|
|
59
|
+
let previousCb
|
|
60
|
+
const cb = ({ currentCallback, abortController }) => {
|
|
61
|
+
setUncaughtExceptionCaptureCallbackStart.unsubscribe(cb)
|
|
62
|
+
if (!currentCallback) {
|
|
63
|
+
abortController.abort()
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
previousCb = currentCallback
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setUncaughtExceptionCaptureCallbackStart.subscribe(cb)
|
|
71
|
+
|
|
72
|
+
process.setUncaughtExceptionCaptureCallback(null)
|
|
73
|
+
|
|
74
|
+
// For some reason, previous callback was defined before the instrumentation
|
|
75
|
+
// We can not restore it, so we let the app decide
|
|
76
|
+
if (previousCb) {
|
|
77
|
+
process.setUncaughtExceptionCaptureCallback(() => {
|
|
78
|
+
process.setUncaughtExceptionCaptureCallback(null)
|
|
79
|
+
process.setUncaughtExceptionCaptureCallback(previousCb)
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function enable (config) {
|
|
86
|
+
ssrf.enable(config)
|
|
87
|
+
sqli.enable(config)
|
|
88
|
+
|
|
89
|
+
process.on('uncaughtExceptionMonitor', handleUncaughtExceptionMonitor)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function disable () {
|
|
93
|
+
ssrf.disable()
|
|
94
|
+
sqli.disable()
|
|
95
|
+
|
|
96
|
+
process.off('uncaughtExceptionMonitor', handleUncaughtExceptionMonitor)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
enable,
|
|
101
|
+
disable,
|
|
102
|
+
handleUncaughtExceptionMonitor // exported only for testing purpose
|
|
103
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { pgQueryStart, pgPoolQueryStart, wafRunFinished } = require('../channels')
|
|
4
|
+
const { storage } = require('../../../../datadog-core')
|
|
5
|
+
const addresses = require('../addresses')
|
|
6
|
+
const waf = require('../waf')
|
|
7
|
+
const { RULE_TYPES, handleResult } = require('./utils')
|
|
8
|
+
|
|
9
|
+
const DB_SYSTEM_POSTGRES = 'postgresql'
|
|
10
|
+
const reqQueryMap = new WeakMap() // WeakMap<Request, Set<querytext>>
|
|
11
|
+
|
|
12
|
+
let config
|
|
13
|
+
|
|
14
|
+
function enable (_config) {
|
|
15
|
+
config = _config
|
|
16
|
+
|
|
17
|
+
pgQueryStart.subscribe(analyzePgSqlInjection)
|
|
18
|
+
pgPoolQueryStart.subscribe(analyzePgSqlInjection)
|
|
19
|
+
wafRunFinished.subscribe(clearQuerySet)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function disable () {
|
|
23
|
+
if (pgQueryStart.hasSubscribers) pgQueryStart.unsubscribe(analyzePgSqlInjection)
|
|
24
|
+
if (pgPoolQueryStart.hasSubscribers) pgPoolQueryStart.unsubscribe(analyzePgSqlInjection)
|
|
25
|
+
if (wafRunFinished.hasSubscribers) wafRunFinished.unsubscribe(clearQuerySet)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function analyzePgSqlInjection (ctx) {
|
|
29
|
+
const query = ctx.query?.text
|
|
30
|
+
if (!query) return
|
|
31
|
+
|
|
32
|
+
const store = storage.getStore()
|
|
33
|
+
if (!store) return
|
|
34
|
+
|
|
35
|
+
const { req, res } = store
|
|
36
|
+
|
|
37
|
+
if (!req) return
|
|
38
|
+
|
|
39
|
+
let executedQueries = reqQueryMap.get(req)
|
|
40
|
+
if (executedQueries?.has(query)) return
|
|
41
|
+
|
|
42
|
+
// Do not waste time executing same query twice
|
|
43
|
+
// This also will prevent double calls in pg.Pool internal queries
|
|
44
|
+
if (!executedQueries) {
|
|
45
|
+
executedQueries = new Set()
|
|
46
|
+
reqQueryMap.set(req, executedQueries)
|
|
47
|
+
}
|
|
48
|
+
executedQueries.add(query)
|
|
49
|
+
|
|
50
|
+
const persistent = {
|
|
51
|
+
[addresses.DB_STATEMENT]: query,
|
|
52
|
+
[addresses.DB_SYSTEM]: DB_SYSTEM_POSTGRES
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result = waf.run({ persistent }, req, RULE_TYPES.SQL_INJECTION)
|
|
56
|
+
|
|
57
|
+
handleResult(result, req, res, ctx.abortController, config)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function hasInputAddress (payload) {
|
|
61
|
+
return hasAddressesObjectInputAddress(payload.ephemeral) || hasAddressesObjectInputAddress(payload.persistent)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function hasAddressesObjectInputAddress (addressesObject) {
|
|
65
|
+
return addressesObject && Object.keys(addressesObject)
|
|
66
|
+
.some(address => address.startsWith('server.request') || address.startsWith('graphql.server'))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function clearQuerySet ({ payload }) {
|
|
70
|
+
if (!payload) return
|
|
71
|
+
|
|
72
|
+
const store = storage.getStore()
|
|
73
|
+
if (!store) return
|
|
74
|
+
|
|
75
|
+
const { req } = store
|
|
76
|
+
if (!req) return
|
|
77
|
+
|
|
78
|
+
const executedQueries = reqQueryMap.get(req)
|
|
79
|
+
if (!executedQueries) return
|
|
80
|
+
|
|
81
|
+
if (hasInputAddress(payload)) {
|
|
82
|
+
executedQueries.clear()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { enable, disable }
|