dd-trace 4.40.0 → 4.42.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 +1 -0
- package/ext/exporters.d.ts +1 -1
- package/index.d.ts +54 -1
- package/init.js +40 -1
- package/initialize.mjs +8 -5
- package/package.json +24 -20
- package/packages/datadog-core/src/storage/index.js +1 -10
- package/packages/datadog-esbuild/index.js +5 -1
- package/packages/datadog-instrumentations/src/aws-sdk.js +2 -1
- package/packages/datadog-instrumentations/src/cucumber.js +76 -34
- package/packages/datadog-instrumentations/src/helpers/hook.js +8 -3
- package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
- package/packages/datadog-instrumentations/src/helpers/instrument.js +4 -3
- package/packages/datadog-instrumentations/src/helpers/register.js +56 -5
- package/packages/datadog-instrumentations/src/http/server.js +98 -0
- package/packages/datadog-instrumentations/src/mocha/main.js +12 -1
- package/packages/datadog-instrumentations/src/mocha/utils.js +58 -14
- package/packages/datadog-instrumentations/src/mocha/worker.js +1 -0
- package/packages/datadog-instrumentations/src/playwright.js +1 -1
- package/packages/datadog-instrumentations/src/undici.js +18 -0
- package/packages/datadog-instrumentations/src/vitest.js +303 -0
- package/packages/datadog-plugin-aws-sdk/src/base.js +8 -1
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +9 -3
- package/packages/datadog-plugin-aws-sdk/src/services/sns.js +6 -1
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +23 -5
- package/packages/datadog-plugin-child_process/src/index.js +1 -1
- package/packages/datadog-plugin-cucumber/src/index.js +24 -1
- package/packages/datadog-plugin-mocha/src/index.js +25 -4
- package/packages/datadog-plugin-openai/src/index.js +52 -30
- package/packages/datadog-plugin-openai/src/token-estimator.js +20 -0
- package/packages/datadog-plugin-undici/src/index.js +12 -0
- package/packages/datadog-plugin-vitest/src/index.js +156 -0
- package/packages/dd-trace/src/appsec/blocking.js +4 -0
- package/packages/dd-trace/src/appsec/channels.js +1 -0
- package/packages/dd-trace/src/appsec/iast/path-line.js +2 -19
- package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +4 -0
- package/packages/dd-trace/src/appsec/index.js +45 -11
- package/packages/dd-trace/src/appsec/rasp.js +32 -5
- package/packages/dd-trace/src/appsec/recommended.json +208 -3
- package/packages/dd-trace/src/appsec/remote_config/capabilities.js +1 -0
- package/packages/dd-trace/src/appsec/remote_config/index.js +2 -0
- package/packages/dd-trace/src/appsec/reporter.js +64 -20
- package/packages/dd-trace/src/appsec/sdk/track_event.js +3 -0
- package/packages/dd-trace/src/appsec/stack_trace.js +90 -0
- package/packages/dd-trace/src/appsec/standalone.js +130 -0
- package/packages/dd-trace/src/appsec/telemetry.js +33 -1
- package/packages/dd-trace/src/appsec/waf/index.js +2 -2
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +2 -2
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +4 -2
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
- package/packages/dd-trace/src/config.js +110 -40
- package/packages/dd-trace/src/constants.js +3 -1
- package/packages/dd-trace/src/datastreams/processor.js +2 -1
- package/packages/dd-trace/src/exporters/agent/index.js +2 -2
- package/packages/dd-trace/src/format.js +22 -2
- package/packages/dd-trace/src/opentelemetry/span.js +33 -7
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +12 -0
- package/packages/dd-trace/src/opentracing/span.js +42 -1
- package/packages/dd-trace/src/opentracing/tracer.js +2 -2
- package/packages/dd-trace/src/plugins/ci_plugin.js +7 -0
- package/packages/dd-trace/src/plugins/index.js +3 -0
- package/packages/dd-trace/src/plugins/util/test.js +5 -1
- package/packages/dd-trace/src/priority_sampler.js +2 -5
- package/packages/dd-trace/src/profiling/profiler.js +1 -1
- package/packages/dd-trace/src/proxy.js +3 -1
- package/packages/dd-trace/src/rate_limiter.js +2 -2
- package/packages/dd-trace/src/service-naming/schemas/v0/web.js +4 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/web.js +4 -0
- package/packages/dd-trace/src/span_stats.js +4 -3
- package/packages/dd-trace/src/tagger.js +10 -1
- package/packages/dd-trace/src/telemetry/init-telemetry.js +75 -0
- package/packages/dd-trace/src/tracer.js +2 -2
- package/packages/dd-trace/src/util.js +6 -1
- package/packages/datadog-core/src/storage/async_hooks.js +0 -49
|
@@ -7,6 +7,7 @@ const { storage } = require('../../datadog-core')
|
|
|
7
7
|
const services = require('./services')
|
|
8
8
|
const Sampler = require('../../dd-trace/src/sampler')
|
|
9
9
|
const { MEASURED } = require('../../../ext/tags')
|
|
10
|
+
const { estimateTokens } = require('./token-estimator')
|
|
10
11
|
|
|
11
12
|
// String#replaceAll unavailable on Node.js@v14 (dd-trace@<=v3)
|
|
12
13
|
const RE_NEWLINE = /\n/g
|
|
@@ -15,14 +16,17 @@ const RE_TAB = /\t/g
|
|
|
15
16
|
// TODO: In the future we should refactor config.js to make it requirable
|
|
16
17
|
let MAX_TEXT_LEN = 128
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} catch {
|
|
23
|
-
|
|
19
|
+
function safeRequire (path) {
|
|
20
|
+
try {
|
|
21
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
22
|
+
return require(path)
|
|
23
|
+
} catch {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
const encodingForModel = safeRequire('tiktoken')?.encoding_for_model
|
|
29
|
+
|
|
26
30
|
class OpenApiPlugin extends TracingPlugin {
|
|
27
31
|
static get id () { return 'openai' }
|
|
28
32
|
static get operation () { return 'request' }
|
|
@@ -305,6 +309,7 @@ class OpenApiPlugin extends TracingPlugin {
|
|
|
305
309
|
}
|
|
306
310
|
|
|
307
311
|
sendLog (methodName, span, tags, store, error) {
|
|
312
|
+
if (!store) return
|
|
308
313
|
if (!Object.keys(store).length) return
|
|
309
314
|
if (!this.sampler.isSampled()) return
|
|
310
315
|
|
|
@@ -325,9 +330,22 @@ function countPromptTokens (methodName, payload, model) {
|
|
|
325
330
|
const messages = payload.messages
|
|
326
331
|
for (const message of messages) {
|
|
327
332
|
const content = message.content
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
333
|
+
if (typeof content === 'string') {
|
|
334
|
+
const { tokens, estimated } = countTokens(content, model)
|
|
335
|
+
promptTokens += tokens
|
|
336
|
+
promptEstimated = estimated
|
|
337
|
+
} else if (Array.isArray(content)) {
|
|
338
|
+
for (const c of content) {
|
|
339
|
+
if (c.type === 'text') {
|
|
340
|
+
const { tokens, estimated } = countTokens(c.text, model)
|
|
341
|
+
promptTokens += tokens
|
|
342
|
+
promptEstimated = estimated
|
|
343
|
+
}
|
|
344
|
+
// unsupported token computation for image_url
|
|
345
|
+
// as even though URL is a string, its true token count
|
|
346
|
+
// is based on the image itself, something onerous to do client-side
|
|
347
|
+
}
|
|
348
|
+
}
|
|
331
349
|
}
|
|
332
350
|
} else if (methodName === 'completions.create') {
|
|
333
351
|
let prompt = payload.prompt
|
|
@@ -382,25 +400,6 @@ function countTokens (content, model) {
|
|
|
382
400
|
}
|
|
383
401
|
}
|
|
384
402
|
|
|
385
|
-
// If model is unavailable or tiktoken is not imported, then provide a very rough estimate of the number of tokens
|
|
386
|
-
// Approximate using the following assumptions:
|
|
387
|
-
// * English text
|
|
388
|
-
// * 1 token ~= 4 chars
|
|
389
|
-
// * 1 token ~= ¾ words
|
|
390
|
-
function estimateTokens (content) {
|
|
391
|
-
let estimatedTokens = 0
|
|
392
|
-
if (typeof content === 'string') {
|
|
393
|
-
const estimation1 = content.length / 4
|
|
394
|
-
|
|
395
|
-
const matches = content.match(/[\w']+|[.,!?;~@#$%^&*()+/-]/g)
|
|
396
|
-
const estimation2 = matches ? matches.length * 0.75 : 0 // in the case of an empty string
|
|
397
|
-
estimatedTokens = Math.round((1.5 * estimation1 + 0.5 * estimation2) / 2)
|
|
398
|
-
} else if (Array.isArray(content) && typeof content[0] === 'number') {
|
|
399
|
-
estimatedTokens = content.length
|
|
400
|
-
}
|
|
401
|
-
return estimatedTokens
|
|
402
|
-
}
|
|
403
|
-
|
|
404
403
|
function createEditRequestExtraction (tags, payload, store) {
|
|
405
404
|
const instruction = payload.instruction
|
|
406
405
|
tags['openai.request.instruction'] = instruction
|
|
@@ -418,7 +417,7 @@ function createChatCompletionRequestExtraction (tags, payload, store) {
|
|
|
418
417
|
store.messages = payload.messages
|
|
419
418
|
for (let i = 0; i < payload.messages.length; i++) {
|
|
420
419
|
const message = payload.messages[i]
|
|
421
|
-
|
|
420
|
+
tagChatCompletionRequestContent(message.content, i, tags)
|
|
422
421
|
tags[`openai.request.messages.${i}.role`] = message.role
|
|
423
422
|
tags[`openai.request.messages.${i}.name`] = message.name
|
|
424
423
|
tags[`openai.request.messages.${i}.finish_reason`] = message.finish_reason
|
|
@@ -707,7 +706,7 @@ function commonCreateResponseExtraction (tags, body, store, methodName) {
|
|
|
707
706
|
for (let choiceIdx = 0; choiceIdx < body.choices.length; choiceIdx++) {
|
|
708
707
|
const choice = body.choices[choiceIdx]
|
|
709
708
|
|
|
710
|
-
// logprobs can be
|
|
709
|
+
// logprobs can be null and we still want to tag it as 'returned' even when set to 'null'
|
|
711
710
|
const specifiesLogProb = Object.keys(choice).indexOf('logprobs') !== -1
|
|
712
711
|
|
|
713
712
|
tags[`openai.response.choices.${choiceIdx}.finish_reason`] = choice.finish_reason
|
|
@@ -781,6 +780,7 @@ function truncateApiKey (apiKey) {
|
|
|
781
780
|
*/
|
|
782
781
|
function truncateText (text) {
|
|
783
782
|
if (!text) return
|
|
783
|
+
if (typeof text !== 'string' || !text || (typeof text === 'string' && text.length === 0)) return
|
|
784
784
|
|
|
785
785
|
text = text
|
|
786
786
|
.replace(RE_NEWLINE, '\\n')
|
|
@@ -793,6 +793,28 @@ function truncateText (text) {
|
|
|
793
793
|
return text
|
|
794
794
|
}
|
|
795
795
|
|
|
796
|
+
function tagChatCompletionRequestContent (contents, messageIdx, tags) {
|
|
797
|
+
if (typeof contents === 'string') {
|
|
798
|
+
tags[`openai.request.messages.${messageIdx}.content`] = contents
|
|
799
|
+
} else if (Array.isArray(contents)) {
|
|
800
|
+
// content can also be an array of objects
|
|
801
|
+
// which represent text input or image url
|
|
802
|
+
for (const contentIdx in contents) {
|
|
803
|
+
const content = contents[contentIdx]
|
|
804
|
+
const type = content.type
|
|
805
|
+
tags[`openai.request.messages.${messageIdx}.content.${contentIdx}.type`] = content.type
|
|
806
|
+
if (type === 'text') {
|
|
807
|
+
tags[`openai.request.messages.${messageIdx}.content.${contentIdx}.text`] = truncateText(content.text)
|
|
808
|
+
} else if (type === 'image_url') {
|
|
809
|
+
tags[`openai.request.messages.${messageIdx}.content.${contentIdx}.image_url.url`] =
|
|
810
|
+
truncateText(content.image_url.url)
|
|
811
|
+
}
|
|
812
|
+
// unsupported type otherwise, won't be tagged
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
// unsupported type otherwise, won't be tagged
|
|
816
|
+
}
|
|
817
|
+
|
|
796
818
|
// The server almost always responds with JSON
|
|
797
819
|
function coerceResponseBody (body, methodName) {
|
|
798
820
|
switch (methodName) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// If model is unavailable or tiktoken is not imported, then provide a very rough estimate of the number of tokens
|
|
4
|
+
// Approximate using the following assumptions:
|
|
5
|
+
// * English text
|
|
6
|
+
// * 1 token ~= 4 chars
|
|
7
|
+
// * 1 token ~= ¾ words
|
|
8
|
+
module.exports.estimateTokens = function (content) {
|
|
9
|
+
let estimatedTokens = 0
|
|
10
|
+
if (typeof content === 'string') {
|
|
11
|
+
const estimation1 = content.length / 4
|
|
12
|
+
|
|
13
|
+
const matches = content.match(/[\w']+|[.,!?;~@#$%^&*()+/-]/g)
|
|
14
|
+
const estimation2 = matches ? matches.length * 0.75 : 0 // in the case of an empty string
|
|
15
|
+
estimatedTokens = Math.round((1.5 * estimation1 + 0.5 * estimation2) / 2)
|
|
16
|
+
} else if (Array.isArray(content) && typeof content[0] === 'number') {
|
|
17
|
+
estimatedTokens = content.length
|
|
18
|
+
}
|
|
19
|
+
return estimatedTokens
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const FetchPlugin = require('../../datadog-plugin-fetch/src/index.js')
|
|
4
|
+
|
|
5
|
+
class UndiciPlugin extends FetchPlugin {
|
|
6
|
+
static get id () { return 'undici' }
|
|
7
|
+
static get prefix () {
|
|
8
|
+
return 'tracing:apm:undici:fetch'
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = UndiciPlugin
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const CiPlugin = require('../../dd-trace/src/plugins/ci_plugin')
|
|
2
|
+
const { storage } = require('../../datadog-core')
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
TEST_STATUS,
|
|
6
|
+
finishAllTraceSpans,
|
|
7
|
+
getTestSuitePath,
|
|
8
|
+
getTestSuiteCommonTags,
|
|
9
|
+
TEST_SOURCE_FILE
|
|
10
|
+
} = require('../../dd-trace/src/plugins/util/test')
|
|
11
|
+
const { COMPONENT } = require('../../dd-trace/src/constants')
|
|
12
|
+
|
|
13
|
+
// Milliseconds that we subtract from the error test duration
|
|
14
|
+
// so that they do not overlap with the following test
|
|
15
|
+
// This is because there's some loss of resolution.
|
|
16
|
+
const MILLISECONDS_TO_SUBTRACT_FROM_FAILED_TEST_DURATION = 5
|
|
17
|
+
|
|
18
|
+
class VitestPlugin extends CiPlugin {
|
|
19
|
+
static get id () {
|
|
20
|
+
return 'vitest'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
constructor (...args) {
|
|
24
|
+
super(...args)
|
|
25
|
+
|
|
26
|
+
this.taskToFinishTime = new WeakMap()
|
|
27
|
+
|
|
28
|
+
this.addSub('ci:vitest:test:start', ({ testName, testSuiteAbsolutePath }) => {
|
|
29
|
+
const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
|
|
30
|
+
const store = storage.getStore()
|
|
31
|
+
const span = this.startTestSpan(
|
|
32
|
+
testName,
|
|
33
|
+
testSuite,
|
|
34
|
+
this.testSuiteSpan,
|
|
35
|
+
{
|
|
36
|
+
[TEST_SOURCE_FILE]: testSuite
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
this.enter(span, store)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
this.addSub('ci:vitest:test:finish-time', ({ status, task }) => {
|
|
44
|
+
const store = storage.getStore()
|
|
45
|
+
const span = store?.span
|
|
46
|
+
|
|
47
|
+
// we store the finish time to finish at a later hook
|
|
48
|
+
// this is because the test might fail at a `afterEach` hook
|
|
49
|
+
if (span) {
|
|
50
|
+
span.setTag(TEST_STATUS, status)
|
|
51
|
+
this.taskToFinishTime.set(task, span._getTime())
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
this.addSub('ci:vitest:test:pass', ({ task }) => {
|
|
56
|
+
const store = storage.getStore()
|
|
57
|
+
const span = store?.span
|
|
58
|
+
|
|
59
|
+
if (span) {
|
|
60
|
+
span.setTag(TEST_STATUS, 'pass')
|
|
61
|
+
span.finish(this.taskToFinishTime.get(task))
|
|
62
|
+
finishAllTraceSpans(span)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
this.addSub('ci:vitest:test:error', ({ duration, error }) => {
|
|
67
|
+
const store = storage.getStore()
|
|
68
|
+
const span = store?.span
|
|
69
|
+
|
|
70
|
+
if (span) {
|
|
71
|
+
span.setTag(TEST_STATUS, 'fail')
|
|
72
|
+
|
|
73
|
+
if (error) {
|
|
74
|
+
span.setTag('error', error)
|
|
75
|
+
}
|
|
76
|
+
span.finish(span._startTime + duration - MILLISECONDS_TO_SUBTRACT_FROM_FAILED_TEST_DURATION) // milliseconds
|
|
77
|
+
finishAllTraceSpans(span)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
this.addSub('ci:vitest:test:skip', ({ testName, testSuiteAbsolutePath }) => {
|
|
82
|
+
const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
|
|
83
|
+
this.startTestSpan(
|
|
84
|
+
testName,
|
|
85
|
+
testSuite,
|
|
86
|
+
this.testSuiteSpan,
|
|
87
|
+
{
|
|
88
|
+
[TEST_SOURCE_FILE]: testSuite,
|
|
89
|
+
[TEST_STATUS]: 'skip'
|
|
90
|
+
}
|
|
91
|
+
).finish()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
this.addSub('ci:vitest:test-suite:start', (testSuiteAbsolutePath) => {
|
|
95
|
+
const testSessionSpanContext = this.tracer.extract('text_map', {
|
|
96
|
+
'x-datadog-trace-id': process.env.DD_CIVISIBILITY_TEST_SESSION_ID,
|
|
97
|
+
'x-datadog-parent-id': process.env.DD_CIVISIBILITY_TEST_MODULE_ID
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
|
|
101
|
+
const testSuiteMetadata = getTestSuiteCommonTags(
|
|
102
|
+
this.command,
|
|
103
|
+
this.frameworkVersion,
|
|
104
|
+
testSuite,
|
|
105
|
+
'vitest'
|
|
106
|
+
)
|
|
107
|
+
const testSuiteSpan = this.tracer.startSpan('vitest.test_suite', {
|
|
108
|
+
childOf: testSessionSpanContext,
|
|
109
|
+
tags: {
|
|
110
|
+
[COMPONENT]: this.constructor.id,
|
|
111
|
+
...this.testEnvironmentMetadata,
|
|
112
|
+
...testSuiteMetadata
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
const store = storage.getStore()
|
|
116
|
+
this.enter(testSuiteSpan, store)
|
|
117
|
+
this.testSuiteSpan = testSuiteSpan
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
this.addSub('ci:vitest:test-suite:finish', ({ status, onFinish }) => {
|
|
121
|
+
const store = storage.getStore()
|
|
122
|
+
const span = store?.span
|
|
123
|
+
if (span) {
|
|
124
|
+
span.setTag(TEST_STATUS, status)
|
|
125
|
+
span.finish()
|
|
126
|
+
finishAllTraceSpans(span)
|
|
127
|
+
}
|
|
128
|
+
// TODO: too frequent flush - find for method in worker to decrease frequency
|
|
129
|
+
this.tracer._exporter.flush(onFinish)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
this.addSub('ci:vitest:test-suite:error', ({ error }) => {
|
|
133
|
+
const store = storage.getStore()
|
|
134
|
+
const span = store?.span
|
|
135
|
+
if (span && error) {
|
|
136
|
+
span.setTag('error', error)
|
|
137
|
+
span.setTag(TEST_STATUS, 'fail')
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
this.addSub('ci:vitest:session:finish', ({ status, onFinish, error }) => {
|
|
142
|
+
this.testSessionSpan.setTag(TEST_STATUS, status)
|
|
143
|
+
this.testModuleSpan.setTag(TEST_STATUS, status)
|
|
144
|
+
if (error) {
|
|
145
|
+
this.testModuleSpan.setTag('error', error)
|
|
146
|
+
this.testSessionSpan.setTag('error', error)
|
|
147
|
+
}
|
|
148
|
+
this.testModuleSpan.finish()
|
|
149
|
+
this.testSessionSpan.finish()
|
|
150
|
+
finishAllTraceSpans(this.testSessionSpan)
|
|
151
|
+
this.tracer._exporter.flush(onFinish)
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = VitestPlugin
|
|
@@ -111,6 +111,10 @@ function block (req, res, rootSpan, abortController, actionParameters) {
|
|
|
111
111
|
|
|
112
112
|
const { body, headers, statusCode } = getBlockingData(req, null, rootSpan, actionParameters)
|
|
113
113
|
|
|
114
|
+
for (const headerName of res.getHeaderNames()) {
|
|
115
|
+
res.removeHeader(headerName)
|
|
116
|
+
}
|
|
117
|
+
|
|
114
118
|
res.writeHead(statusCode, headers).end(body)
|
|
115
119
|
|
|
116
120
|
abortController?.abort()
|
|
@@ -18,5 +18,6 @@ module.exports = {
|
|
|
18
18
|
nextBodyParsed: dc.channel('apm:next:body-parsed'),
|
|
19
19
|
nextQueryParsed: dc.channel('apm:next:query-parsed'),
|
|
20
20
|
responseBody: dc.channel('datadog:express:response:json:start'),
|
|
21
|
+
responseWriteHead: dc.channel('apm:http:server:response:writeHead:start'),
|
|
21
22
|
httpClientRequestStart: dc.channel('apm:http:client:request:start')
|
|
22
23
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const path = require('path')
|
|
4
4
|
const process = require('process')
|
|
5
5
|
const { calculateDDBasePath } = require('../../util')
|
|
6
|
+
const { getCallSiteList } = require('../stack_trace')
|
|
6
7
|
const pathLine = {
|
|
7
8
|
getFirstNonDDPathAndLine,
|
|
8
9
|
getNodeModulesPaths,
|
|
@@ -24,24 +25,6 @@ const EXCLUDED_PATH_PREFIXES = [
|
|
|
24
25
|
'async_hooks'
|
|
25
26
|
]
|
|
26
27
|
|
|
27
|
-
function getCallSiteInfo () {
|
|
28
|
-
const previousPrepareStackTrace = Error.prepareStackTrace
|
|
29
|
-
const previousStackTraceLimit = Error.stackTraceLimit
|
|
30
|
-
let callsiteList
|
|
31
|
-
Error.stackTraceLimit = 100
|
|
32
|
-
try {
|
|
33
|
-
Error.prepareStackTrace = function (_, callsites) {
|
|
34
|
-
callsiteList = callsites
|
|
35
|
-
}
|
|
36
|
-
const e = new Error()
|
|
37
|
-
e.stack
|
|
38
|
-
} finally {
|
|
39
|
-
Error.prepareStackTrace = previousPrepareStackTrace
|
|
40
|
-
Error.stackTraceLimit = previousStackTraceLimit
|
|
41
|
-
}
|
|
42
|
-
return callsiteList
|
|
43
|
-
}
|
|
44
|
-
|
|
45
28
|
function getFirstNonDDPathAndLineFromCallsites (callsites, externallyExcludedPaths) {
|
|
46
29
|
if (callsites) {
|
|
47
30
|
for (let i = 0; i < callsites.length; i++) {
|
|
@@ -91,7 +74,7 @@ function isExcluded (callsite, externallyExcludedPaths) {
|
|
|
91
74
|
}
|
|
92
75
|
|
|
93
76
|
function getFirstNonDDPathAndLine (externallyExcludedPaths) {
|
|
94
|
-
return getFirstNonDDPathAndLineFromCallsites(
|
|
77
|
+
return getFirstNonDDPathAndLineFromCallsites(getCallSiteList(), externallyExcludedPaths)
|
|
95
78
|
}
|
|
96
79
|
|
|
97
80
|
function getNodeModulesPaths (...paths) {
|
|
@@ -4,6 +4,7 @@ const { MANUAL_KEEP } = require('../../../../../ext/tags')
|
|
|
4
4
|
const LRU = require('lru-cache')
|
|
5
5
|
const vulnerabilitiesFormatter = require('./vulnerabilities-formatter')
|
|
6
6
|
const { IAST_ENABLED_TAG_KEY, IAST_JSON_TAG_KEY } = require('./tags')
|
|
7
|
+
const standalone = require('../standalone')
|
|
7
8
|
|
|
8
9
|
const VULNERABILITIES_KEY = 'vulnerabilities'
|
|
9
10
|
const VULNERABILITY_HASHES_MAX_SIZE = 1000
|
|
@@ -57,6 +58,9 @@ function sendVulnerabilities (vulnerabilities, rootSpan) {
|
|
|
57
58
|
tags[IAST_JSON_TAG_KEY] = JSON.stringify(jsonToSend)
|
|
58
59
|
tags[MANUAL_KEEP] = 'true'
|
|
59
60
|
span.addTags(tags)
|
|
61
|
+
|
|
62
|
+
standalone.sample(span)
|
|
63
|
+
|
|
60
64
|
if (!rootSpan) span.finish()
|
|
61
65
|
}
|
|
62
66
|
}
|
|
@@ -12,7 +12,8 @@ const {
|
|
|
12
12
|
queryParser,
|
|
13
13
|
nextBodyParsed,
|
|
14
14
|
nextQueryParsed,
|
|
15
|
-
responseBody
|
|
15
|
+
responseBody,
|
|
16
|
+
responseWriteHead
|
|
16
17
|
} = require('./channels')
|
|
17
18
|
const waf = require('./waf')
|
|
18
19
|
const addresses = require('./addresses')
|
|
@@ -39,7 +40,7 @@ function enable (_config) {
|
|
|
39
40
|
graphql.enable()
|
|
40
41
|
|
|
41
42
|
if (_config.appsec.rasp.enabled) {
|
|
42
|
-
rasp.enable()
|
|
43
|
+
rasp.enable(_config)
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
setTemplates(_config)
|
|
@@ -60,6 +61,7 @@ function enable (_config) {
|
|
|
60
61
|
queryParser.subscribe(onRequestQueryParsed)
|
|
61
62
|
cookieParser.subscribe(onRequestCookieParser)
|
|
62
63
|
responseBody.subscribe(onResponseBody)
|
|
64
|
+
responseWriteHead.subscribe(onResponseWriteHead)
|
|
63
65
|
|
|
64
66
|
if (_config.appsec.eventTracking.enabled) {
|
|
65
67
|
passportVerify.subscribe(onPassportVerify)
|
|
@@ -110,14 +112,7 @@ function incomingHttpStartTranslator ({ req, res, abortController }) {
|
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
function incomingHttpEndTranslator ({ req, res }) {
|
|
113
|
-
|
|
114
|
-
const responseHeaders = Object.assign({}, res.getHeaders())
|
|
115
|
-
delete responseHeaders['set-cookie']
|
|
116
|
-
|
|
117
|
-
const persistent = {
|
|
118
|
-
[addresses.HTTP_INCOMING_RESPONSE_CODE]: '' + res.statusCode,
|
|
119
|
-
[addresses.HTTP_INCOMING_RESPONSE_HEADERS]: responseHeaders
|
|
120
|
-
}
|
|
115
|
+
const persistent = {}
|
|
121
116
|
|
|
122
117
|
// we need to keep this to support other body parsers
|
|
123
118
|
// TODO: no need to analyze it if it was already done by the body-parser hook
|
|
@@ -139,7 +134,9 @@ function incomingHttpEndTranslator ({ req, res }) {
|
|
|
139
134
|
persistent[addresses.HTTP_INCOMING_QUERY] = req.query
|
|
140
135
|
}
|
|
141
136
|
|
|
142
|
-
|
|
137
|
+
if (Object.keys(persistent).length) {
|
|
138
|
+
waf.run({ persistent }, req)
|
|
139
|
+
}
|
|
143
140
|
|
|
144
141
|
waf.disposeContext(req)
|
|
145
142
|
|
|
@@ -225,12 +222,48 @@ function onPassportVerify ({ credentials, user }) {
|
|
|
225
222
|
passportTrackEvent(credentials, user, rootSpan, config.appsec.eventTracking.mode)
|
|
226
223
|
}
|
|
227
224
|
|
|
225
|
+
const responseAnalyzedSet = new WeakSet()
|
|
226
|
+
const responseBlockedSet = new WeakSet()
|
|
227
|
+
|
|
228
|
+
function onResponseWriteHead ({ req, res, abortController, statusCode, responseHeaders }) {
|
|
229
|
+
// avoid "write after end" error
|
|
230
|
+
if (responseBlockedSet.has(res)) {
|
|
231
|
+
abortController?.abort()
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// avoid double waf call
|
|
236
|
+
if (responseAnalyzedSet.has(res)) {
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const rootSpan = web.root(req)
|
|
241
|
+
if (!rootSpan) return
|
|
242
|
+
|
|
243
|
+
responseHeaders = Object.assign({}, responseHeaders)
|
|
244
|
+
delete responseHeaders['set-cookie']
|
|
245
|
+
|
|
246
|
+
const results = waf.run({
|
|
247
|
+
persistent: {
|
|
248
|
+
[addresses.HTTP_INCOMING_RESPONSE_CODE]: '' + statusCode,
|
|
249
|
+
[addresses.HTTP_INCOMING_RESPONSE_HEADERS]: responseHeaders
|
|
250
|
+
}
|
|
251
|
+
}, req)
|
|
252
|
+
|
|
253
|
+
responseAnalyzedSet.add(res)
|
|
254
|
+
|
|
255
|
+
handleResults(results, req, res, rootSpan, abortController)
|
|
256
|
+
}
|
|
257
|
+
|
|
228
258
|
function handleResults (actions, req, res, rootSpan, abortController) {
|
|
229
259
|
if (!actions || !req || !res || !rootSpan || !abortController) return
|
|
230
260
|
|
|
231
261
|
const blockingAction = getBlockingAction(actions)
|
|
232
262
|
if (blockingAction) {
|
|
233
263
|
block(req, res, rootSpan, abortController, blockingAction)
|
|
264
|
+
if (!abortController.signal || abortController.signal.aborted) {
|
|
265
|
+
responseBlockedSet.add(res)
|
|
266
|
+
}
|
|
234
267
|
}
|
|
235
268
|
}
|
|
236
269
|
|
|
@@ -256,6 +289,7 @@ function disable () {
|
|
|
256
289
|
if (cookieParser.hasSubscribers) cookieParser.unsubscribe(onRequestCookieParser)
|
|
257
290
|
if (responseBody.hasSubscribers) responseBody.unsubscribe(onResponseBody)
|
|
258
291
|
if (passportVerify.hasSubscribers) passportVerify.unsubscribe(onPassportVerify)
|
|
292
|
+
if (responseWriteHead.hasSubscribers) responseWriteHead.unsubscribe(onResponseWriteHead)
|
|
259
293
|
}
|
|
260
294
|
|
|
261
295
|
module.exports = {
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { storage } = require('../../../datadog-core')
|
|
4
|
+
const web = require('./../plugins/util/web')
|
|
4
5
|
const addresses = require('./addresses')
|
|
5
6
|
const { httpClientRequestStart } = require('./channels')
|
|
7
|
+
const { reportStackTrace } = require('./stack_trace')
|
|
6
8
|
const waf = require('./waf')
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
const RULE_TYPES = {
|
|
11
|
+
SSRF: 'ssrf'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let config
|
|
15
|
+
|
|
16
|
+
function enable (_config) {
|
|
17
|
+
config = _config
|
|
9
18
|
httpClientRequestStart.subscribe(analyzeSsrf)
|
|
10
19
|
}
|
|
11
20
|
|
|
@@ -24,12 +33,30 @@ function analyzeSsrf (ctx) {
|
|
|
24
33
|
[addresses.HTTP_OUTGOING_URL]: url
|
|
25
34
|
}
|
|
26
35
|
// TODO: Currently this is only monitoring, we should
|
|
27
|
-
// block the request if SSRF attempt
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
// block the request if SSRF attempt
|
|
37
|
+
const result = waf.run({ persistent }, req, RULE_TYPES.SSRF)
|
|
38
|
+
handleResult(result, req)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getGenerateStackTraceAction (actions) {
|
|
42
|
+
return actions?.generate_stack
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function handleResult (actions, req) {
|
|
46
|
+
const generateStackTraceAction = getGenerateStackTraceAction(actions)
|
|
47
|
+
if (generateStackTraceAction && config.appsec.stackTrace.enabled) {
|
|
48
|
+
const rootSpan = web.root(req)
|
|
49
|
+
reportStackTrace(
|
|
50
|
+
rootSpan,
|
|
51
|
+
generateStackTraceAction.stack_id,
|
|
52
|
+
config.appsec.stackTrace.maxDepth,
|
|
53
|
+
config.appsec.stackTrace.maxStackTraces
|
|
54
|
+
)
|
|
55
|
+
}
|
|
30
56
|
}
|
|
31
57
|
|
|
32
58
|
module.exports = {
|
|
33
59
|
enable,
|
|
34
|
-
disable
|
|
60
|
+
disable,
|
|
61
|
+
handleResult
|
|
35
62
|
}
|