dd-trace 4.7.0 → 4.8.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/README.md +2 -2
- package/package.json +3 -3
- package/packages/datadog-core/src/storage/async_resource.js +4 -0
- package/packages/datadog-instrumentations/src/couchbase.js +4 -4
- package/packages/datadog-instrumentations/src/jest.js +6 -4
- package/packages/datadog-plugin-graphql/src/execute.js +6 -4
- package/packages/datadog-plugin-jest/src/index.js +8 -3
- package/packages/datadog-plugin-openai/src/index.js +39 -16
- package/packages/datadog-plugin-openai/src/services.js +13 -9
- package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +3 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +45 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/index.js +3 -3
- package/packages/dd-trace/src/appsec/iast/analyzers/missing-header-analyzer.js +66 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +25 -8
- package/packages/dd-trace/src/appsec/iast/analyzers/xcontenttype-header-missing-analyzer.js +19 -0
- package/packages/dd-trace/src/appsec/iast/index.js +5 -2
- package/packages/dd-trace/src/appsec/iast/taint-tracking/index.js +4 -2
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +17 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/source-types.js +1 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +5 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +3 -1
- package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +1 -1
- package/packages/dd-trace/src/config.js +26 -10
- package/packages/dd-trace/src/external-logger/src/index.js +9 -1
- package/packages/dd-trace/src/external-logger/test/index.spec.js +1 -1
- package/packages/dd-trace/src/format.js +1 -1
- package/packages/dd-trace/src/lambda/handler.js +8 -1
- package/packages/dd-trace/src/opentelemetry/span.js +3 -1
- package/packages/dd-trace/src/opentracing/span_context.js +2 -1
- package/packages/dd-trace/src/plugins/util/ci.js +2 -1
- package/packages/dd-trace/src/plugins/util/web.js +1 -0
- package/packages/dd-trace/src/profiling/config.js +8 -5
- package/packages/dd-trace/src/profiling/exporters/agent.js +4 -1
- package/packages/dd-trace/src/profiling/profiler.js +1 -1
- package/packages/dd-trace/src/profiling/profilers/wall.js +144 -4
- package/packages/dd-trace/src/service-naming/index.js +2 -2
- package/packages/dd-trace/src/service-naming/schemas/v0/graphql.js +12 -0
- package/packages/dd-trace/src/service-naming/schemas/v0/index.js +2 -1
- package/packages/dd-trace/src/service-naming/schemas/v1/graphql.js +12 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/index.js +2 -1
- package/packages/dd-trace/src/span_processor.js +0 -4
- package/packages/dd-trace/src/span_sampler.js +1 -1
- package/packages/dd-trace/src/telemetry/dependencies.js +24 -12
- package/packages/dd-trace/src/telemetry/metrics.js +11 -1
- package/scripts/install_plugin_modules.js +1 -0
- package/scripts/version.js +0 -66
package/README.md
CHANGED
|
@@ -143,8 +143,8 @@ $ yarn leak:plugins
|
|
|
143
143
|
|
|
144
144
|
### Linting
|
|
145
145
|
|
|
146
|
-
We use [ESLint](https://eslint.org) to make sure that new code
|
|
147
|
-
|
|
146
|
+
We use [ESLint](https://eslint.org) to make sure that new code
|
|
147
|
+
conforms to our coding standards.
|
|
148
148
|
|
|
149
149
|
To run the linter, use:
|
|
150
150
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dd-trace",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.0",
|
|
4
4
|
"description": "Datadog APM tracing client for JavaScript",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"typings": "index.d.ts",
|
|
@@ -68,9 +68,9 @@
|
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@datadog/native-appsec": "^3.2.0",
|
|
70
70
|
"@datadog/native-iast-rewriter": "2.0.1",
|
|
71
|
-
"@datadog/native-iast-taint-tracking": "
|
|
71
|
+
"@datadog/native-iast-taint-tracking": "1.5.0",
|
|
72
72
|
"@datadog/native-metrics": "^2.0.0",
|
|
73
|
-
"@datadog/pprof": "3.
|
|
73
|
+
"@datadog/pprof": "3.1.0",
|
|
74
74
|
"@datadog/sketches-js": "^2.1.0",
|
|
75
75
|
"@opentelemetry/api": "^1.0.0",
|
|
76
76
|
"@opentelemetry/core": "^1.14.0",
|
|
@@ -5,6 +5,7 @@ const { channel } = require('../../../diagnostics_channel')
|
|
|
5
5
|
|
|
6
6
|
const beforeCh = channel('dd-trace:storage:before')
|
|
7
7
|
const afterCh = channel('dd-trace:storage:after')
|
|
8
|
+
const enterCh = channel('dd-trace:storage:enter')
|
|
8
9
|
|
|
9
10
|
let PrivateSymbol = Symbol
|
|
10
11
|
function makePrivateSymbol () {
|
|
@@ -52,6 +53,7 @@ class AsyncResourceStorage {
|
|
|
52
53
|
const resource = this._executionAsyncResource()
|
|
53
54
|
|
|
54
55
|
resource[this._ddResourceStore] = store
|
|
56
|
+
enterCh.publish()
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
run (store, callback, ...args) {
|
|
@@ -61,11 +63,13 @@ class AsyncResourceStorage {
|
|
|
61
63
|
const oldStore = resource[this._ddResourceStore]
|
|
62
64
|
|
|
63
65
|
resource[this._ddResourceStore] = store
|
|
66
|
+
enterCh.publish()
|
|
64
67
|
|
|
65
68
|
try {
|
|
66
69
|
return callback(...args)
|
|
67
70
|
} finally {
|
|
68
71
|
resource[this._ddResourceStore] = oldStore
|
|
72
|
+
enterCh.publish()
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
|
|
@@ -160,7 +160,7 @@ function wrapV3Query (query) {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
// semver >=2 <3
|
|
163
|
-
addHook({ name: 'couchbase', file: 'lib/bucket.js', versions: ['^2.6.
|
|
163
|
+
addHook({ name: 'couchbase', file: 'lib/bucket.js', versions: ['^2.6.12'] }, Bucket => {
|
|
164
164
|
const startCh = channel('apm:couchbase:query:start')
|
|
165
165
|
const finishCh = channel('apm:couchbase:query:finish')
|
|
166
166
|
const errorCh = channel('apm:couchbase:query:error')
|
|
@@ -208,7 +208,7 @@ addHook({ name: 'couchbase', file: 'lib/bucket.js', versions: ['^2.6.5'] }, Buck
|
|
|
208
208
|
return Bucket
|
|
209
209
|
})
|
|
210
210
|
|
|
211
|
-
addHook({ name: 'couchbase', file: 'lib/cluster.js', versions: ['^2.6.
|
|
211
|
+
addHook({ name: 'couchbase', file: 'lib/cluster.js', versions: ['^2.6.12'] }, Cluster => {
|
|
212
212
|
Cluster.prototype._maybeInvoke = wrapMaybeInvoke(Cluster.prototype._maybeInvoke)
|
|
213
213
|
Cluster.prototype.query = wrapQuery(Cluster.prototype.query)
|
|
214
214
|
|
|
@@ -217,7 +217,7 @@ addHook({ name: 'couchbase', file: 'lib/cluster.js', versions: ['^2.6.5'] }, Clu
|
|
|
217
217
|
|
|
218
218
|
// semver >=3 <3.2.0
|
|
219
219
|
|
|
220
|
-
addHook({ name: 'couchbase', file: 'lib/collection.js', versions: ['
|
|
220
|
+
addHook({ name: 'couchbase', file: 'lib/collection.js', versions: ['^3.0.7', '^3.1.3'] }, Collection => {
|
|
221
221
|
wrapAllNames(['upsert', 'insert', 'replace'], name => {
|
|
222
222
|
shimmer.wrap(Collection.prototype, name, wrapWithName(name))
|
|
223
223
|
})
|
|
@@ -225,7 +225,7 @@ addHook({ name: 'couchbase', file: 'lib/collection.js', versions: ['>=3.0.0 <3.2
|
|
|
225
225
|
return Collection
|
|
226
226
|
})
|
|
227
227
|
|
|
228
|
-
addHook({ name: 'couchbase', file: 'lib/cluster.js', versions: ['
|
|
228
|
+
addHook({ name: 'couchbase', file: 'lib/cluster.js', versions: ['^3.0.7', '^3.1.3'] }, Cluster => {
|
|
229
229
|
shimmer.wrap(Cluster.prototype, 'query', wrapV3Query)
|
|
230
230
|
return Cluster
|
|
231
231
|
})
|
|
@@ -129,8 +129,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
129
129
|
suite: this.testSuite,
|
|
130
130
|
runner: 'jest-circus',
|
|
131
131
|
testParameters,
|
|
132
|
-
frameworkVersion: jestVersion
|
|
133
|
-
testStartLine: getTestLineStart(event.test.asyncError, this.testSuite)
|
|
132
|
+
frameworkVersion: jestVersion
|
|
134
133
|
})
|
|
135
134
|
originalTestFns.set(event.test, event.test.fn)
|
|
136
135
|
event.test.fn = asyncResource.bind(event.test.fn)
|
|
@@ -145,7 +144,10 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
145
144
|
const formattedError = formatJestError(event.test.errors[0])
|
|
146
145
|
testErrCh.publish(formattedError)
|
|
147
146
|
}
|
|
148
|
-
testRunFinishCh.publish(
|
|
147
|
+
testRunFinishCh.publish({
|
|
148
|
+
status,
|
|
149
|
+
testStartLine: getTestLineStart(event.test.asyncError, this.testSuite)
|
|
150
|
+
})
|
|
149
151
|
// restore in case it is retried
|
|
150
152
|
event.test.fn = originalTestFns.get(event.test)
|
|
151
153
|
})
|
|
@@ -471,7 +473,7 @@ function jasmineAsyncInstallWraper (jasmineAsyncInstallExport, jestVersion) {
|
|
|
471
473
|
const formattedError = formatJestError(spec.result.failedExpectations[0].error)
|
|
472
474
|
testErrCh.publish(formattedError)
|
|
473
475
|
}
|
|
474
|
-
testRunFinishCh.publish(specStatusToTestStatus[spec.result.status])
|
|
476
|
+
testRunFinishCh.publish({ status: specStatusToTestStatus[spec.result.status] })
|
|
475
477
|
onComplete.apply(this, arguments)
|
|
476
478
|
})
|
|
477
479
|
arguments[0] = callback
|
|
@@ -7,6 +7,8 @@ let tools
|
|
|
7
7
|
class GraphQLExecutePlugin extends TracingPlugin {
|
|
8
8
|
static get id () { return 'graphql' }
|
|
9
9
|
static get operation () { return 'execute' }
|
|
10
|
+
static get type () { return 'graphql' }
|
|
11
|
+
static get kind () { return 'server' }
|
|
10
12
|
|
|
11
13
|
start ({ operation, args, docSource }) {
|
|
12
14
|
const type = operation && operation.operation
|
|
@@ -14,11 +16,11 @@ class GraphQLExecutePlugin extends TracingPlugin {
|
|
|
14
16
|
const document = args.document
|
|
15
17
|
const source = this.config.source && document && docSource
|
|
16
18
|
|
|
17
|
-
const span = this.startSpan(
|
|
18
|
-
service: this.config.service,
|
|
19
|
+
const span = this.startSpan(this.operationName(), {
|
|
20
|
+
service: this.config.service || this.serviceName(),
|
|
19
21
|
resource: getSignature(document, name, type, this.config.signature),
|
|
20
|
-
kind:
|
|
21
|
-
type:
|
|
22
|
+
kind: this.constructor.kind,
|
|
23
|
+
type: this.constructor.type,
|
|
22
24
|
meta: {
|
|
23
25
|
'graphql.operation.type': type,
|
|
24
26
|
'graphql.operation.name': name,
|
|
@@ -166,9 +166,12 @@ class JestPlugin extends CiPlugin {
|
|
|
166
166
|
this.enter(span, store)
|
|
167
167
|
})
|
|
168
168
|
|
|
169
|
-
this.addSub('ci:jest:test:finish', (status) => {
|
|
169
|
+
this.addSub('ci:jest:test:finish', ({ status, testStartLine }) => {
|
|
170
170
|
const span = storage.getStore().span
|
|
171
171
|
span.setTag(TEST_STATUS, status)
|
|
172
|
+
if (testStartLine) {
|
|
173
|
+
span.setTag(TEST_SOURCE_START, testStartLine)
|
|
174
|
+
}
|
|
172
175
|
span.finish()
|
|
173
176
|
finishAllTraceSpans(span)
|
|
174
177
|
})
|
|
@@ -197,8 +200,10 @@ class JestPlugin extends CiPlugin {
|
|
|
197
200
|
const extraTags = {
|
|
198
201
|
[JEST_TEST_RUNNER]: runner,
|
|
199
202
|
[TEST_PARAMETERS]: testParameters,
|
|
200
|
-
[TEST_FRAMEWORK_VERSION]: frameworkVersion
|
|
201
|
-
|
|
203
|
+
[TEST_FRAMEWORK_VERSION]: frameworkVersion
|
|
204
|
+
}
|
|
205
|
+
if (testStartLine) {
|
|
206
|
+
extraTags[TEST_SOURCE_START] = testStartLine
|
|
202
207
|
}
|
|
203
208
|
|
|
204
209
|
return super.startTestSpan(name, suite, this.testSuiteSpan, extraTags)
|
|
@@ -8,6 +8,10 @@ const services = require('./services')
|
|
|
8
8
|
const Sampler = require('../../dd-trace/src/sampler')
|
|
9
9
|
const { MEASURED } = require('../../../ext/tags')
|
|
10
10
|
|
|
11
|
+
// String#replaceAll unavailable on Node.js@v14 (dd-trace@<=v3)
|
|
12
|
+
const RE_NEWLINE = /\n/g
|
|
13
|
+
const RE_TAB = /\t/g
|
|
14
|
+
|
|
11
15
|
// TODO: In the future we should refactor config.js to make it requirable
|
|
12
16
|
let MAX_TEXT_LEN = 128
|
|
13
17
|
|
|
@@ -26,7 +30,9 @@ class OpenApiPlugin extends TracingPlugin {
|
|
|
26
30
|
this.sampler = new Sampler(0.1) // default 10% log sampling
|
|
27
31
|
|
|
28
32
|
// hoist the max length env var to avoid making all of these functions a class method
|
|
29
|
-
|
|
33
|
+
if (this._tracerConfig) {
|
|
34
|
+
MAX_TEXT_LEN = this._tracerConfig.openaiSpanCharLimit
|
|
35
|
+
}
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
configure (config) {
|
|
@@ -83,11 +89,11 @@ class OpenApiPlugin extends TracingPlugin {
|
|
|
83
89
|
store.prompt = prompt
|
|
84
90
|
if (typeof prompt === 'string' || (Array.isArray(prompt) && typeof prompt[0] === 'number')) {
|
|
85
91
|
// This is a single prompt, either String or [Number]
|
|
86
|
-
tags[`openai.request.prompt`] = normalizeStringOrTokenArray(prompt)
|
|
92
|
+
tags[`openai.request.prompt`] = normalizeStringOrTokenArray(prompt, true)
|
|
87
93
|
} else if (Array.isArray(prompt)) {
|
|
88
94
|
// This is multiple prompts, either [String] or [[Number]]
|
|
89
95
|
for (let i = 0; i < prompt.length; i++) {
|
|
90
|
-
tags[`openai.request.prompt.${i}`] = normalizeStringOrTokenArray(prompt[i])
|
|
96
|
+
tags[`openai.request.prompt.${i}`] = normalizeStringOrTokenArray(prompt[i], true)
|
|
91
97
|
}
|
|
92
98
|
}
|
|
93
99
|
}
|
|
@@ -363,6 +369,8 @@ function retrieveModelResponseExtraction (tags, body) {
|
|
|
363
369
|
tags['openai.response.parent'] = body.parent
|
|
364
370
|
tags['openai.response.root'] = body.root
|
|
365
371
|
|
|
372
|
+
if (!body.permission) return
|
|
373
|
+
|
|
366
374
|
tags['openai.response.permission.id'] = body.permission[0].id
|
|
367
375
|
tags['openai.response.permission.created'] = body.permission[0].created
|
|
368
376
|
tags['openai.response.permission.allow_create_engine'] = body.permission[0].allow_create_engine
|
|
@@ -382,10 +390,14 @@ function commonLookupFineTuneRequestExtraction (tags, body) {
|
|
|
382
390
|
}
|
|
383
391
|
|
|
384
392
|
function listModelsResponseExtraction (tags, body) {
|
|
393
|
+
if (!body.data) return
|
|
394
|
+
|
|
385
395
|
tags['openai.response.count'] = body.data.length
|
|
386
396
|
}
|
|
387
397
|
|
|
388
398
|
function commonImageResponseExtraction (tags, body) {
|
|
399
|
+
if (!body.data) return
|
|
400
|
+
|
|
389
401
|
tags['openai.response.images_count'] = body.data.length
|
|
390
402
|
|
|
391
403
|
for (let i = 0; i < body.data.length; i++) {
|
|
@@ -400,7 +412,7 @@ function createAudioResponseExtraction (tags, body) {
|
|
|
400
412
|
tags['openai.response.text'] = body.text
|
|
401
413
|
tags['openai.response.language'] = body.language
|
|
402
414
|
tags['openai.response.duration'] = body.duration
|
|
403
|
-
tags['openai.response.segments_count'] = body.segments
|
|
415
|
+
tags['openai.response.segments_count'] = defensiveArrayLength(body.segments)
|
|
404
416
|
}
|
|
405
417
|
|
|
406
418
|
function createFineTuneRequestExtraction (tags, body) {
|
|
@@ -417,21 +429,24 @@ function createFineTuneRequestExtraction (tags, body) {
|
|
|
417
429
|
}
|
|
418
430
|
|
|
419
431
|
function commonFineTuneResponseExtraction (tags, body) {
|
|
420
|
-
tags['openai.response.events_count'] = body.events
|
|
432
|
+
tags['openai.response.events_count'] = defensiveArrayLength(body.events)
|
|
421
433
|
tags['openai.response.fine_tuned_model'] = body.fine_tuned_model
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
tags['openai.response.
|
|
434
|
+
if (body.hyperparams) {
|
|
435
|
+
tags['openai.response.hyperparams.n_epochs'] = body.hyperparams.n_epochs
|
|
436
|
+
tags['openai.response.hyperparams.batch_size'] = body.hyperparams.batch_size
|
|
437
|
+
tags['openai.response.hyperparams.prompt_loss_weight'] = body.hyperparams.prompt_loss_weight
|
|
438
|
+
tags['openai.response.hyperparams.learning_rate_multiplier'] = body.hyperparams.learning_rate_multiplier
|
|
439
|
+
}
|
|
440
|
+
tags['openai.response.training_files_count'] = defensiveArrayLength(body.training_files)
|
|
441
|
+
tags['openai.response.result_files_count'] = defensiveArrayLength(body.result_files)
|
|
442
|
+
tags['openai.response.validation_files_count'] = defensiveArrayLength(body.validation_files)
|
|
429
443
|
tags['openai.response.updated_at'] = body.updated_at
|
|
430
444
|
tags['openai.response.status'] = body.status
|
|
431
445
|
}
|
|
432
446
|
|
|
433
447
|
// the OpenAI package appears to stream the content download then provide it all as a singular string
|
|
434
448
|
function downloadFileResponseExtraction (tags, body) {
|
|
449
|
+
if (!body.file) return
|
|
435
450
|
tags['openai.response.total_bytes'] = body.file.length
|
|
436
451
|
}
|
|
437
452
|
|
|
@@ -472,6 +487,8 @@ function createRetrieveFileResponseExtraction (tags, body) {
|
|
|
472
487
|
function createEmbeddingResponseExtraction (tags, body) {
|
|
473
488
|
usageExtraction(tags, body)
|
|
474
489
|
|
|
490
|
+
if (!body.data) return
|
|
491
|
+
|
|
475
492
|
tags['openai.response.embeddings_count'] = body.data.length
|
|
476
493
|
for (let i = 0; i < body.data.length; i++) {
|
|
477
494
|
tags[`openai.response.embedding.${i}.embedding_length`] = body.data[i].embedding.length
|
|
@@ -479,6 +496,7 @@ function createEmbeddingResponseExtraction (tags, body) {
|
|
|
479
496
|
}
|
|
480
497
|
|
|
481
498
|
function commonListCountResponseExtraction (tags, body) {
|
|
499
|
+
if (!body.data) return
|
|
482
500
|
tags['openai.response.count'] = body.data.length
|
|
483
501
|
}
|
|
484
502
|
|
|
@@ -486,6 +504,9 @@ function commonListCountResponseExtraction (tags, body) {
|
|
|
486
504
|
function createModerationResponseExtraction (tags, body) {
|
|
487
505
|
tags['openai.response.id'] = body.id
|
|
488
506
|
// tags[`openai.response.model`] = body.model // redundant, already extracted globally
|
|
507
|
+
|
|
508
|
+
if (!body.results) return
|
|
509
|
+
|
|
489
510
|
tags['openai.response.flagged'] = body.results[0].flagged
|
|
490
511
|
|
|
491
512
|
for (const [category, match] of Object.entries(body.results[0].categories)) {
|
|
@@ -501,6 +522,8 @@ function createModerationResponseExtraction (tags, body) {
|
|
|
501
522
|
function commonCreateResponseExtraction (tags, body, store) {
|
|
502
523
|
usageExtraction(tags, body)
|
|
503
524
|
|
|
525
|
+
if (!body.choices) return
|
|
526
|
+
|
|
504
527
|
tags['openai.response.choices_count'] = body.choices.length
|
|
505
528
|
|
|
506
529
|
store.choices = body.choices
|
|
@@ -530,7 +553,7 @@ function usageExtraction (tags, body) {
|
|
|
530
553
|
}
|
|
531
554
|
|
|
532
555
|
function truncateApiKey (apiKey) {
|
|
533
|
-
return `sk-...${apiKey.substr(apiKey.length - 4)}`
|
|
556
|
+
return apiKey && `sk-...${apiKey.substr(apiKey.length - 4)}`
|
|
534
557
|
}
|
|
535
558
|
|
|
536
559
|
/**
|
|
@@ -540,8 +563,8 @@ function truncateText (text) {
|
|
|
540
563
|
if (!text) return
|
|
541
564
|
|
|
542
565
|
text = text
|
|
543
|
-
.
|
|
544
|
-
.
|
|
566
|
+
.replace(RE_NEWLINE, '\\n')
|
|
567
|
+
.replace(RE_TAB, '\\t')
|
|
545
568
|
|
|
546
569
|
if (text.length > MAX_TEXT_LEN) {
|
|
547
570
|
return text.substring(0, MAX_TEXT_LEN) + '...'
|
|
@@ -671,7 +694,7 @@ function normalizeRequestPayload (methodName, args) {
|
|
|
671
694
|
* "foo" -> "foo"
|
|
672
695
|
* [1,2,3] -> "[1, 2, 3]"
|
|
673
696
|
*/
|
|
674
|
-
function normalizeStringOrTokenArray (input, truncate
|
|
697
|
+
function normalizeStringOrTokenArray (input, truncate) {
|
|
675
698
|
const normalized = Array.isArray(input)
|
|
676
699
|
? `[${input.join(', ')}]` // "[1, 2, 999]"
|
|
677
700
|
: input // "foo"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { DogStatsDClient, NoopDogStatsDClient } = require('../../dd-trace/src/dogstatsd')
|
|
4
|
-
const ExternalLogger = require('../../dd-trace/src/external-logger/src')
|
|
4
|
+
const { ExternalLogger, NoopExternalLogger } = require('../../dd-trace/src/external-logger/src')
|
|
5
5
|
|
|
6
6
|
const FLUSH_INTERVAL = 10 * 1000
|
|
7
7
|
|
|
@@ -10,7 +10,7 @@ let logger = null
|
|
|
10
10
|
let interval = null
|
|
11
11
|
|
|
12
12
|
module.exports.init = function (tracerConfig) {
|
|
13
|
-
if (tracerConfig.dogstatsd) {
|
|
13
|
+
if (tracerConfig && tracerConfig.dogstatsd) {
|
|
14
14
|
metrics = new DogStatsDClient({
|
|
15
15
|
host: tracerConfig.dogstatsd.hostname,
|
|
16
16
|
port: tracerConfig.dogstatsd.port,
|
|
@@ -24,13 +24,17 @@ module.exports.init = function (tracerConfig) {
|
|
|
24
24
|
metrics = new NoopDogStatsDClient()
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
if (tracerConfig && tracerConfig.apiKey) {
|
|
28
|
+
logger = new ExternalLogger({
|
|
29
|
+
ddsource: 'openai',
|
|
30
|
+
hostname: tracerConfig.hostname,
|
|
31
|
+
service: tracerConfig.service,
|
|
32
|
+
apiKey: tracerConfig.apiKey,
|
|
33
|
+
interval: FLUSH_INTERVAL
|
|
34
|
+
})
|
|
35
|
+
} else {
|
|
36
|
+
logger = new NoopExternalLogger()
|
|
37
|
+
}
|
|
34
38
|
|
|
35
39
|
interval = setInterval(() => {
|
|
36
40
|
metrics.flush()
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
4
|
'COMMAND_INJECTION_ANALYZER': require('./command-injection-analyzer'),
|
|
5
|
+
'HSTS_HEADER_MISSING_ANALYZER': require('./hsts-header-missing-analyzer'),
|
|
5
6
|
'INSECURE_COOKIE_ANALYZER': require('./insecure-cookie-analyzer'),
|
|
6
7
|
'LDAP_ANALYZER': require('./ldap-injection-analyzer'),
|
|
7
8
|
'NO_HTTPONLY_COOKIE_ANALYZER': require('./no-httponly-cookie-analyzer'),
|
|
@@ -11,5 +12,6 @@ module.exports = {
|
|
|
11
12
|
'SSRF': require('./ssrf-analyzer'),
|
|
12
13
|
'UNVALIDATED_REDIRECT_ANALYZER': require('./unvalidated-redirect-analyzer'),
|
|
13
14
|
'WEAK_CIPHER_ANALYZER': require('./weak-cipher-analyzer'),
|
|
14
|
-
'WEAK_HASH_ANALYZER': require('./weak-hash-analyzer')
|
|
15
|
+
'WEAK_HASH_ANALYZER': require('./weak-hash-analyzer'),
|
|
16
|
+
'XCONTENTTYPE_HEADER_MISSING_ANALYZER': require('./xcontenttype-header-missing-analyzer')
|
|
15
17
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { HSTS_HEADER_MISSING } = require('../vulnerabilities')
|
|
4
|
+
const { MissingHeaderAnalyzer } = require('./missing-header-analyzer')
|
|
5
|
+
|
|
6
|
+
const HSTS_HEADER_NAME = 'Strict-Transport-Security'
|
|
7
|
+
const HEADER_VALID_PREFIX = 'max-age'
|
|
8
|
+
class HstsHeaderMissingAnalyzer extends MissingHeaderAnalyzer {
|
|
9
|
+
constructor () {
|
|
10
|
+
super(HSTS_HEADER_MISSING, HSTS_HEADER_NAME)
|
|
11
|
+
}
|
|
12
|
+
_isVulnerableFromRequestAndResponse (req, res) {
|
|
13
|
+
const headerToCheck = res.getHeader(HSTS_HEADER_NAME)
|
|
14
|
+
return !this._isHeaderValid(headerToCheck) && this._isHttpsProtocol(req)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_isHeaderValid (headerValue) {
|
|
18
|
+
if (!headerValue) {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
headerValue = headerValue.trim()
|
|
22
|
+
|
|
23
|
+
if (!headerValue.startsWith(HEADER_VALID_PREFIX)) {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const semicolonIndex = headerValue.indexOf(';')
|
|
28
|
+
let timestampString
|
|
29
|
+
if (semicolonIndex > -1) {
|
|
30
|
+
timestampString = headerValue.substring(HEADER_VALID_PREFIX.length + 1, semicolonIndex)
|
|
31
|
+
} else {
|
|
32
|
+
timestampString = headerValue.substring(HEADER_VALID_PREFIX.length + 1)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const timestamp = parseInt(timestampString)
|
|
36
|
+
// eslint-disable-next-line eqeqeq
|
|
37
|
+
return timestamp == timestampString && timestamp > 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_isHttpsProtocol (req) {
|
|
41
|
+
return req.protocol === 'https' || req.headers['x-forwarded-proto'] === 'https'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = new HstsHeaderMissingAnalyzer()
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
const analyzers = require('./analyzers')
|
|
4
4
|
const setCookiesHeaderInterceptor = require('./set-cookies-header-interceptor')
|
|
5
5
|
|
|
6
|
-
function enableAllAnalyzers () {
|
|
7
|
-
setCookiesHeaderInterceptor.configure(true)
|
|
6
|
+
function enableAllAnalyzers (tracerConfig) {
|
|
7
|
+
setCookiesHeaderInterceptor.configure({ enabled: true, tracerConfig })
|
|
8
8
|
for (const analyzer in analyzers) {
|
|
9
|
-
analyzers[analyzer].configure(true)
|
|
9
|
+
analyzers[analyzer].configure({ enabled: true, tracerConfig })
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Analyzer = require('./vulnerability-analyzer')
|
|
4
|
+
|
|
5
|
+
const SC_MOVED_PERMANENTLY = 301
|
|
6
|
+
const SC_MOVED_TEMPORARILY = 302
|
|
7
|
+
const SC_NOT_MODIFIED = 304
|
|
8
|
+
const SC_TEMPORARY_REDIRECT = 307
|
|
9
|
+
const SC_NOT_FOUND = 404
|
|
10
|
+
const SC_GONE = 410
|
|
11
|
+
const SC_INTERNAL_SERVER_ERROR = 500
|
|
12
|
+
|
|
13
|
+
const IGNORED_RESPONSE_STATUS_LIST = [SC_MOVED_PERMANENTLY, SC_MOVED_TEMPORARILY, SC_NOT_MODIFIED,
|
|
14
|
+
SC_TEMPORARY_REDIRECT, SC_NOT_FOUND, SC_GONE, SC_INTERNAL_SERVER_ERROR]
|
|
15
|
+
const HTML_CONTENT_TYPES = ['text/html', 'application/xhtml+xml']
|
|
16
|
+
|
|
17
|
+
class MissingHeaderAnalyzer extends Analyzer {
|
|
18
|
+
constructor (type, headerName) {
|
|
19
|
+
super(type)
|
|
20
|
+
|
|
21
|
+
this.headerName = headerName
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
onConfigure () {
|
|
25
|
+
this.addSub({
|
|
26
|
+
channelName: 'datadog:iast:response-end',
|
|
27
|
+
moduleName: 'http'
|
|
28
|
+
}, (data) => this.analyze(data))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_getLocation () {
|
|
32
|
+
return undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_checkOCE (context) {
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_createHashSource (type, evidence, location) {
|
|
40
|
+
return `${type}:${this.config.tracerConfig.service}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_getEvidence ({ res }) {
|
|
44
|
+
return { value: res.getHeader(this.headerName) }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_isVulnerable ({ req, res }, context) {
|
|
48
|
+
if (!IGNORED_RESPONSE_STATUS_LIST.includes(res.statusCode) && this._isResponseHtml(res)) {
|
|
49
|
+
return this._isVulnerableFromRequestAndResponse(req, res)
|
|
50
|
+
}
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_isVulnerableFromRequestAndResponse (req, res) {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_isResponseHtml (res) {
|
|
59
|
+
const contentType = res.getHeader('content-type')
|
|
60
|
+
return contentType && HTML_CONTENT_TYPES.some(htmlContentType => {
|
|
61
|
+
return htmlContentType === contentType || contentType.startsWith(htmlContentType + ';')
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { MissingHeaderAnalyzer }
|
|
@@ -4,7 +4,11 @@ const InjectionAnalyzer = require('./injection-analyzer')
|
|
|
4
4
|
const { UNVALIDATED_REDIRECT } = require('../vulnerabilities')
|
|
5
5
|
const { getNodeModulesPaths } = require('../path-line')
|
|
6
6
|
const { getRanges } = require('../taint-tracking/operations')
|
|
7
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
HTTP_REQUEST_HEADER_VALUE,
|
|
9
|
+
HTTP_REQUEST_PATH,
|
|
10
|
+
HTTP_REQUEST_PATH_PARAM
|
|
11
|
+
} = require('../taint-tracking/source-types')
|
|
8
12
|
|
|
9
13
|
const EXCLUDED_PATHS = getNodeModulesPaths('express/lib/response.js')
|
|
10
14
|
|
|
@@ -17,9 +21,6 @@ class UnvalidatedRedirectAnalyzer extends InjectionAnalyzer {
|
|
|
17
21
|
this.addSub('datadog:http:server:response:set-header:finish', ({ name, value }) => this.analyze(name, value))
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
// TODO: In case the location header value is tainted, this analyzer should check the ranges of the tainted.
|
|
21
|
-
// And do not report a vulnerability if source of the ranges (range.iinfo.type) are exclusively url or path params
|
|
22
|
-
// to avoid false positives.
|
|
23
24
|
analyze (name, value) {
|
|
24
25
|
if (!this.isLocationHeader(name) || typeof value !== 'string') return
|
|
25
26
|
|
|
@@ -34,12 +35,28 @@ class UnvalidatedRedirectAnalyzer extends InjectionAnalyzer {
|
|
|
34
35
|
if (!value) return false
|
|
35
36
|
|
|
36
37
|
const ranges = getRanges(iastContext, value)
|
|
37
|
-
return ranges && ranges.length > 0 && !this.
|
|
38
|
+
return ranges && ranges.length > 0 && !this._areSafeRanges(ranges)
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// Do not report vulnerability if ranges sources are exclusively url,
|
|
42
|
+
// path params or referer header to avoid false positives.
|
|
43
|
+
_areSafeRanges (ranges) {
|
|
44
|
+
return ranges && ranges.every(
|
|
45
|
+
range => this._isPathParam(range) || this._isUrl(range) || this._isRefererHeader(range)
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_isRefererHeader (range) {
|
|
50
|
+
return range.iinfo.type === HTTP_REQUEST_HEADER_VALUE &&
|
|
51
|
+
range.iinfo.parameterName && range.iinfo.parameterName.toLowerCase() === 'referer'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_isPathParam (range) {
|
|
55
|
+
return range.iinfo.type === HTTP_REQUEST_PATH_PARAM
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_isUrl (range) {
|
|
59
|
+
return range.iinfo.type === HTTP_REQUEST_PATH
|
|
43
60
|
}
|
|
44
61
|
|
|
45
62
|
_getExcludedPaths () {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { XCONTENTTYPE_HEADER_MISSING } = require('../vulnerabilities')
|
|
4
|
+
const { MissingHeaderAnalyzer } = require('./missing-header-analyzer')
|
|
5
|
+
|
|
6
|
+
const XCONTENTTYPEOPTIONS_HEADER_NAME = 'X-Content-Type-Options'
|
|
7
|
+
|
|
8
|
+
class XcontenttypeHeaderMissingAnalyzer extends MissingHeaderAnalyzer {
|
|
9
|
+
constructor () {
|
|
10
|
+
super(XCONTENTTYPE_HEADER_MISSING, XCONTENTTYPEOPTIONS_HEADER_NAME)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
_isVulnerableFromRequestAndResponse (req, res) {
|
|
14
|
+
const headerToCheck = res.getHeader(XCONTENTTYPEOPTIONS_HEADER_NAME)
|
|
15
|
+
return !headerToCheck || headerToCheck.trim().toLowerCase() !== 'nosniff'
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = new XcontenttypeHeaderMissingAnalyzer()
|
|
@@ -19,10 +19,11 @@ const iastTelemetry = require('./telemetry')
|
|
|
19
19
|
// order of the callbacks can be enforce
|
|
20
20
|
const requestStart = dc.channel('dd-trace:incomingHttpRequestStart')
|
|
21
21
|
const requestClose = dc.channel('dd-trace:incomingHttpRequestEnd')
|
|
22
|
+
const iastResponseEnd = dc.channel('datadog:iast:response-end')
|
|
22
23
|
|
|
23
24
|
function enable (config, _tracer) {
|
|
24
25
|
iastTelemetry.configure(config, config.iast && config.iast.telemetryVerbosity)
|
|
25
|
-
enableAllAnalyzers()
|
|
26
|
+
enableAllAnalyzers(config)
|
|
26
27
|
enableTaintTracking(config.iast, iastTelemetry.verbosity)
|
|
27
28
|
requestStart.subscribe(onIncomingHttpRequestStart)
|
|
28
29
|
requestClose.subscribe(onIncomingHttpRequestEnd)
|
|
@@ -54,7 +55,7 @@ function onIncomingHttpRequestStart (data) {
|
|
|
54
55
|
createTransaction(rootSpan.context().toSpanId(), iastContext)
|
|
55
56
|
overheadController.initializeRequestContext(iastContext)
|
|
56
57
|
iastTelemetry.onRequestStart(iastContext)
|
|
57
|
-
taintTrackingPlugin.
|
|
58
|
+
taintTrackingPlugin.taintRequest(data.req, iastContext)
|
|
58
59
|
}
|
|
59
60
|
if (rootSpan.addTags) {
|
|
60
61
|
rootSpan.addTags({
|
|
@@ -72,6 +73,8 @@ function onIncomingHttpRequestEnd (data) {
|
|
|
72
73
|
const topContext = web.getContext(data.req)
|
|
73
74
|
const iastContext = iastContextFunctions.getIastContext(store, topContext)
|
|
74
75
|
if (iastContext && iastContext.rootSpan) {
|
|
76
|
+
iastResponseEnd.publish(data)
|
|
77
|
+
|
|
75
78
|
const vulnerabilities = iastContext.vulnerabilities
|
|
76
79
|
const rootSpan = iastContext.rootSpan
|
|
77
80
|
vulnerabilityReporter.sendVulnerabilities(vulnerabilities, rootSpan)
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { enableRewriter, disableRewriter } = require('./rewriter')
|
|
4
|
-
const {
|
|
4
|
+
const {
|
|
5
|
+
createTransaction,
|
|
5
6
|
removeTransaction,
|
|
6
7
|
setMaxTransactions,
|
|
7
8
|
enableTaintOperations,
|
|
8
|
-
disableTaintOperations
|
|
9
|
+
disableTaintOperations
|
|
10
|
+
} = require('./operations')
|
|
9
11
|
|
|
10
12
|
const taintTrackingPlugin = require('./plugin')
|
|
11
13
|
|