dd-trace 4.2.0 → 4.3.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/index.d.ts +7 -0
- package/package.json +5 -5
- package/packages/datadog-instrumentations/src/cookie.js +21 -0
- package/packages/datadog-instrumentations/src/fetch.js +48 -0
- package/packages/datadog-instrumentations/src/grpc/server.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/register.js +10 -0
- package/packages/datadog-instrumentations/src/otel-sdk-trace.js +18 -0
- package/packages/datadog-plugin-fetch/src/index.js +36 -0
- package/packages/datadog-plugin-http/src/client.js +24 -8
- package/packages/datadog-plugin-mysql/src/index.js +2 -11
- package/packages/datadog-plugin-tedious/src/index.js +2 -2
- package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +3 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +52 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/insecure-cookie-analyzer.js +3 -22
- package/packages/dd-trace/src/appsec/iast/analyzers/no-httponly-cookie-analyzer.js +12 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/no-samesite-cookie-analyzer.js +12 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/set-cookies-header-interceptor.js +7 -3
- package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +3 -3
- package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +48 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +3 -3
- package/packages/dd-trace/src/appsec/iast/index.js +9 -2
- package/packages/dd-trace/src/appsec/iast/path-line.js +13 -0
- package/packages/dd-trace/src/appsec/iast/tags.js +6 -0
- package/packages/dd-trace/src/appsec/iast/taint-tracking/index.js +2 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/operations.js +13 -4
- package/packages/dd-trace/src/appsec/iast/taint-tracking/origin-types.js +5 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +24 -4
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +3 -0
- package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +7 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +4 -3
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +5 -2
- package/packages/dd-trace/src/config.js +13 -0
- package/packages/dd-trace/src/external-logger/src/index.js +126 -0
- package/packages/dd-trace/src/external-logger/test/index.spec.js +147 -0
- package/packages/dd-trace/src/lambda/handler.js +3 -15
- package/packages/dd-trace/src/noop/proxy.js +4 -0
- package/packages/dd-trace/src/opentelemetry/context_manager.js +1 -1
- package/packages/dd-trace/src/plugin_manager.js +10 -7
- package/packages/dd-trace/src/plugins/database.js +7 -3
- package/packages/dd-trace/src/plugins/plugin.js +3 -1
- package/packages/dd-trace/src/plugins/util/exec.js +2 -2
- package/packages/dd-trace/src/plugins/util/git.js +51 -24
- package/packages/dd-trace/src/profiling/config.js +2 -0
- package/packages/dd-trace/src/profiling/profiler.js +13 -4
- package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +24 -1
- package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +18 -1
- package/packages/dd-trace/src/util.js +1 -1
package/index.d.ts
CHANGED
|
@@ -749,6 +749,7 @@ interface Plugins {
|
|
|
749
749
|
"elasticsearch": plugins.elasticsearch;
|
|
750
750
|
"express": plugins.express;
|
|
751
751
|
"fastify": plugins.fastify;
|
|
752
|
+
"fetch": plugins.fetch;
|
|
752
753
|
"generic-pool": plugins.generic_pool;
|
|
753
754
|
"google-cloud-pubsub": plugins.google_cloud_pubsub;
|
|
754
755
|
"graphql": plugins.graphql;
|
|
@@ -1092,6 +1093,12 @@ declare namespace plugins {
|
|
|
1092
1093
|
*/
|
|
1093
1094
|
interface fastify extends HttpServer {}
|
|
1094
1095
|
|
|
1096
|
+
/**
|
|
1097
|
+
* This plugin automatically instruments the
|
|
1098
|
+
* [fetch](https://nodejs.org/api/globals.html#fetch) global.
|
|
1099
|
+
*/
|
|
1100
|
+
interface fetch extends HttpClient {}
|
|
1101
|
+
|
|
1095
1102
|
/**
|
|
1096
1103
|
* This plugin patches the [generic-pool](https://github.com/coopernurse/node-pool)
|
|
1097
1104
|
* module to bind the callbacks the the caller context.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dd-trace",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "Datadog APM tracing client for JavaScript",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"typings": "index.d.ts",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"test:appsec:ci": "nyc --no-clean --include \"packages/dd-trace/src/appsec/**/*.js\" --exclude \"packages/dd-trace/test/appsec/**/*.plugin.spec.js\" -- npm run test:appsec",
|
|
20
20
|
"test:appsec:plugins": "mocha --colors --exit -r \"packages/dd-trace/test/setup/mocha.js\" \"packages/dd-trace/test/appsec/**/*.@($(echo $PLUGINS)).plugin.spec.js\"",
|
|
21
21
|
"test:appsec:plugins:ci": "yarn services && nyc --no-clean --include \"packages/dd-trace/test/appsec/**/*.@($(echo $PLUGINS)).plugin.spec.js\" -- npm run test:appsec:plugins",
|
|
22
|
-
"test:trace:core": "tap packages/dd-trace/test/*.spec.js \"packages/dd-trace/test/{ci-visibility,encode,exporters,opentelemetry,opentracing,plugins,telemetry}/**/*.spec.js\"",
|
|
22
|
+
"test:trace:core": "tap packages/dd-trace/test/*.spec.js \"packages/dd-trace/test/{ci-visibility,config,encode,exporters,opentelemetry,opentracing,plugins,telemetry}/**/*.spec.js\"",
|
|
23
23
|
"test:trace:core:ci": "npm run test:trace:core -- --coverage --nyc-arg=--include=\"packages/dd-trace/src/**/*.js\"",
|
|
24
24
|
"test:instrumentations": "mocha --colors -r 'packages/dd-trace/test/setup/mocha.js' 'packages/datadog-instrumentations/test/**/*.spec.js'",
|
|
25
25
|
"test:instrumentations:ci": "nyc --no-clean --include 'packages/datadog-instrumentations/src/**/*.js' -- npm run test:instrumentations",
|
|
@@ -68,12 +68,12 @@
|
|
|
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": "^1.
|
|
71
|
+
"@datadog/native-iast-taint-tracking": "^1.5.0",
|
|
72
72
|
"@datadog/native-metrics": "^2.0.0",
|
|
73
|
-
"@datadog/pprof": "
|
|
73
|
+
"@datadog/pprof": "2.2.3",
|
|
74
74
|
"@datadog/sketches-js": "^2.1.0",
|
|
75
75
|
"@opentelemetry/api": "^1.0.0",
|
|
76
|
-
"@opentelemetry/core": "
|
|
76
|
+
"@opentelemetry/core": "^1.14.0",
|
|
77
77
|
"crypto-randomuuid": "^1.0.0",
|
|
78
78
|
"diagnostics_channel": "^1.1.0",
|
|
79
79
|
"ignore": "^5.2.0",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const shimmer = require('../../datadog-shimmer')
|
|
4
|
+
const { channel, addHook } = require('./helpers/instrument')
|
|
5
|
+
|
|
6
|
+
const cookieParseCh = channel('datadog:cookie:parse:finish')
|
|
7
|
+
|
|
8
|
+
function wrapParse (originalParse) {
|
|
9
|
+
return function () {
|
|
10
|
+
const cookies = originalParse.apply(this, arguments)
|
|
11
|
+
if (cookieParseCh.hasSubscribers && cookies) {
|
|
12
|
+
cookieParseCh.publish({ cookies })
|
|
13
|
+
}
|
|
14
|
+
return cookies
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
addHook({ name: 'cookie', versions: ['>=0.4'] }, cookie => {
|
|
19
|
+
shimmer.wrap(cookie, 'parse', wrapParse)
|
|
20
|
+
return cookie
|
|
21
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const shimmer = require('../../datadog-shimmer')
|
|
4
|
+
const { channel } = require('./helpers/instrument')
|
|
5
|
+
|
|
6
|
+
const startChannel = channel('apm:fetch:request:start')
|
|
7
|
+
const finishChannel = channel('apm:fetch:request:finish')
|
|
8
|
+
const errorChannel = channel('apm:fetch:request:error')
|
|
9
|
+
|
|
10
|
+
function wrapFetch (fetch, Request) {
|
|
11
|
+
if (typeof fetch !== 'function') return fetch
|
|
12
|
+
|
|
13
|
+
return function (input, init) {
|
|
14
|
+
if (!startChannel.hasSubscribers) return fetch.apply(this, arguments)
|
|
15
|
+
|
|
16
|
+
const req = new Request(input, init)
|
|
17
|
+
const headers = req.headers
|
|
18
|
+
const message = { req, headers }
|
|
19
|
+
|
|
20
|
+
startChannel.publish(message)
|
|
21
|
+
|
|
22
|
+
// Request object is read-only so we need new objects to change headers.
|
|
23
|
+
arguments[0] = message.req
|
|
24
|
+
arguments[1] = { headers: message.headers }
|
|
25
|
+
|
|
26
|
+
return fetch.apply(this, arguments)
|
|
27
|
+
.then(
|
|
28
|
+
res => {
|
|
29
|
+
finishChannel.publish({ req, res })
|
|
30
|
+
|
|
31
|
+
return res
|
|
32
|
+
},
|
|
33
|
+
err => {
|
|
34
|
+
if (err.name !== 'AbortError') {
|
|
35
|
+
errorChannel.publish(err)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
finishChannel.publish({ req })
|
|
39
|
+
|
|
40
|
+
throw err
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (globalThis.fetch) {
|
|
47
|
+
globalThis.fetch = shimmer.wrap(fetch, wrapFetch(fetch, globalThis.Request))
|
|
48
|
+
}
|
|
@@ -109,7 +109,7 @@ function wrapStream (call, requestResource, onCancel) {
|
|
|
109
109
|
function wrapCallback (callback, call, requestResource, parentResource, onCancel) {
|
|
110
110
|
return function (err, value, trailer, flags) {
|
|
111
111
|
requestResource.runInAsyncScope(() => {
|
|
112
|
-
if (err
|
|
112
|
+
if (err) {
|
|
113
113
|
errorChannel.publish(err)
|
|
114
114
|
finishChannel.publish(err)
|
|
115
115
|
} else {
|
|
@@ -14,6 +14,7 @@ module.exports = {
|
|
|
14
14
|
'@koa/router': () => require('../koa'),
|
|
15
15
|
'@node-redis/client': () => require('../redis'),
|
|
16
16
|
'@opensearch-project/opensearch': () => require('../opensearch'),
|
|
17
|
+
'@opentelemetry/sdk-trace-node': () => require('../otel-sdk-trace'),
|
|
17
18
|
'@redis/client': () => require('../redis'),
|
|
18
19
|
'amqp10': () => require('../amqp10'),
|
|
19
20
|
'amqplib': () => require('../amqplib'),
|
|
@@ -25,6 +26,7 @@ module.exports = {
|
|
|
25
26
|
'child_process': () => require('../child-process'),
|
|
26
27
|
'node:child_process': () => require('../child-process'),
|
|
27
28
|
'connect': () => require('../connect'),
|
|
29
|
+
'cookie': () => require('../cookie'),
|
|
28
30
|
'couchbase': () => require('../couchbase'),
|
|
29
31
|
'crypto': () => require('../crypto'),
|
|
30
32
|
'cypress': () => require('../cypress'),
|
|
@@ -7,16 +7,26 @@ const Hook = require('./hook')
|
|
|
7
7
|
const requirePackageJson = require('../../../dd-trace/src/require-package-json')
|
|
8
8
|
const log = require('../../../dd-trace/src/log')
|
|
9
9
|
|
|
10
|
+
const { DD_TRACE_DISABLED_INSTRUMENTATIONS = '' } = process.env
|
|
11
|
+
|
|
10
12
|
const hooks = require('./hooks')
|
|
11
13
|
const instrumentations = require('./instrumentations')
|
|
12
14
|
const names = Object.keys(hooks)
|
|
13
15
|
const pathSepExpr = new RegExp(`\\${path.sep}`, 'g')
|
|
16
|
+
const disabledInstrumentations = new Set(
|
|
17
|
+
DD_TRACE_DISABLED_INSTRUMENTATIONS ? DD_TRACE_DISABLED_INSTRUMENTATIONS.split(',') : []
|
|
18
|
+
)
|
|
14
19
|
|
|
15
20
|
const loadChannel = channel('dd-trace:instrumentation:load')
|
|
16
21
|
|
|
22
|
+
// Globals
|
|
23
|
+
require('../fetch')
|
|
24
|
+
|
|
17
25
|
// TODO: make this more efficient
|
|
18
26
|
|
|
19
27
|
for (const packageName of names) {
|
|
28
|
+
if (disabledInstrumentations.has(packageName)) continue
|
|
29
|
+
|
|
20
30
|
Hook([packageName], (moduleExports, moduleName, moduleBaseDir, moduleVersion) => {
|
|
21
31
|
moduleName = moduleName.replace(pathSepExpr, '/')
|
|
22
32
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { addHook } = require('./helpers/instrument')
|
|
4
|
+
const shimmer = require('../../datadog-shimmer')
|
|
5
|
+
const tracer = require('../../dd-trace')
|
|
6
|
+
|
|
7
|
+
if (process.env.DD_TRACE_OTEL_ENABLED) {
|
|
8
|
+
addHook({
|
|
9
|
+
name: '@opentelemetry/sdk-trace-node',
|
|
10
|
+
file: 'build/src/NodeTracerProvider.js',
|
|
11
|
+
versions: ['*']
|
|
12
|
+
}, (mod) => {
|
|
13
|
+
shimmer.wrap(mod, 'NodeTracerProvider', () => {
|
|
14
|
+
return tracer.TracerProvider
|
|
15
|
+
})
|
|
16
|
+
return mod
|
|
17
|
+
})
|
|
18
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const HttpClientPlugin = require('../../datadog-plugin-http/src/client')
|
|
4
|
+
const { HTTP_HEADERS } = require('../../../ext/formats')
|
|
5
|
+
|
|
6
|
+
class FetchPlugin extends HttpClientPlugin {
|
|
7
|
+
static get id () { return 'fetch' }
|
|
8
|
+
|
|
9
|
+
addTraceSub (eventName, handler) {
|
|
10
|
+
this.addSub(`apm:${this.constructor.id}:${this.operation}:${eventName}`, handler)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
start (message) {
|
|
14
|
+
const req = message.req
|
|
15
|
+
const options = new URL(req.url)
|
|
16
|
+
const headers = options.headers = Object.fromEntries(req.headers.entries())
|
|
17
|
+
|
|
18
|
+
const args = { options }
|
|
19
|
+
|
|
20
|
+
super.start({ args })
|
|
21
|
+
|
|
22
|
+
message.req = new globalThis.Request(req, { headers })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_inject (span, headers) {
|
|
26
|
+
const carrier = {}
|
|
27
|
+
|
|
28
|
+
this.tracer.inject(span, HTTP_HEADERS, carrier)
|
|
29
|
+
|
|
30
|
+
for (const name in carrier) {
|
|
31
|
+
headers.append(name, carrier[name])
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = FetchPlugin
|
|
@@ -24,15 +24,17 @@ class HttpClientPlugin extends ClientPlugin {
|
|
|
24
24
|
this.addSub(`apm:${this.constructor.id}:client:${this.operation}:${eventName}`, handler)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
start ({ args, http }) {
|
|
27
|
+
start ({ args, http = {} }) {
|
|
28
28
|
const store = storage.getStore()
|
|
29
29
|
const options = args.options
|
|
30
|
-
const agent = options.agent || options._defaultAgent || http.globalAgent
|
|
30
|
+
const agent = options.agent || options._defaultAgent || http.globalAgent || {}
|
|
31
31
|
const protocol = options.protocol || agent.protocol || 'http:'
|
|
32
32
|
const hostname = options.hostname || options.host || 'localhost'
|
|
33
33
|
const host = options.port ? `${hostname}:${options.port}` : hostname
|
|
34
|
-
const
|
|
34
|
+
const pathname = options.path || options.pathname
|
|
35
|
+
const path = pathname ? pathname.split(/[?#]/)[0] : '/'
|
|
35
36
|
const uri = `${protocol}//${host}${path}`
|
|
37
|
+
|
|
36
38
|
const allowed = this.config.filter(uri)
|
|
37
39
|
|
|
38
40
|
const method = (options.method || 'GET').toUpperCase()
|
|
@@ -71,9 +73,11 @@ class HttpClientPlugin extends ClientPlugin {
|
|
|
71
73
|
finish ({ req, res }) {
|
|
72
74
|
const span = storage.getStore().span
|
|
73
75
|
if (res) {
|
|
74
|
-
|
|
76
|
+
const status = res.status || res.statusCode
|
|
77
|
+
|
|
78
|
+
span.setTag(HTTP_STATUS_CODE, status)
|
|
75
79
|
|
|
76
|
-
if (!this.config.validateStatus(
|
|
80
|
+
if (!this.config.validateStatus(status)) {
|
|
77
81
|
span.setTag('error', 1)
|
|
78
82
|
}
|
|
79
83
|
|
|
@@ -106,8 +110,14 @@ class HttpClientPlugin extends ClientPlugin {
|
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
function addResponseHeaders (res, span, config) {
|
|
113
|
+
if (!res.headers) return
|
|
114
|
+
|
|
115
|
+
const headers = typeof res.headers.entries === 'function'
|
|
116
|
+
? Object.fromEntries(res.headers.entries())
|
|
117
|
+
: res.headers
|
|
118
|
+
|
|
109
119
|
config.headers.forEach(key => {
|
|
110
|
-
const value =
|
|
120
|
+
const value = headers[key]
|
|
111
121
|
|
|
112
122
|
if (value) {
|
|
113
123
|
span.setTag(`${HTTP_RESPONSE_HEADERS}.${key}`, value)
|
|
@@ -116,8 +126,12 @@ function addResponseHeaders (res, span, config) {
|
|
|
116
126
|
}
|
|
117
127
|
|
|
118
128
|
function addRequestHeaders (req, span, config) {
|
|
129
|
+
const headers = req.headers && typeof req.headers.entries === 'function'
|
|
130
|
+
? Object.fromEntries(req.headers.entries())
|
|
131
|
+
: req.headers || req.getHeaders()
|
|
132
|
+
|
|
119
133
|
config.headers.forEach(key => {
|
|
120
|
-
const value =
|
|
134
|
+
const value = headers[key]
|
|
121
135
|
|
|
122
136
|
if (value) {
|
|
123
137
|
span.setTag(`${HTTP_REQUEST_HEADERS}.${key}`, Array.isArray(value) ? value.toString() : value)
|
|
@@ -193,7 +207,9 @@ function hasAmazonSignature (options) {
|
|
|
193
207
|
}
|
|
194
208
|
}
|
|
195
209
|
|
|
196
|
-
|
|
210
|
+
const search = options.search || options.path
|
|
211
|
+
|
|
212
|
+
return search && search.toLowerCase().indexOf('x-amz-signature=') !== -1
|
|
197
213
|
}
|
|
198
214
|
|
|
199
215
|
function getServiceName (tracer, config, options) {
|
|
@@ -8,9 +8,8 @@ class MySQLPlugin extends DatabasePlugin {
|
|
|
8
8
|
static get system () { return 'mysql' }
|
|
9
9
|
|
|
10
10
|
start (payload) {
|
|
11
|
-
const service =
|
|
12
|
-
|
|
13
|
-
this.startSpan(`${this.system}.query`, {
|
|
11
|
+
const service = this.serviceName(this.config, payload.conf, this.system)
|
|
12
|
+
this.startSpan(this.operationName(), {
|
|
14
13
|
service,
|
|
15
14
|
resource: payload.sql,
|
|
16
15
|
type: 'sql',
|
|
@@ -27,12 +26,4 @@ class MySQLPlugin extends DatabasePlugin {
|
|
|
27
26
|
}
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
function getServiceName (config, dbConfig) {
|
|
31
|
-
if (typeof config.service === 'function') {
|
|
32
|
-
return config.service(dbConfig)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return config.service
|
|
36
|
-
}
|
|
37
|
-
|
|
38
29
|
module.exports = MySQLPlugin
|
|
@@ -9,8 +9,8 @@ class TediousPlugin extends DatabasePlugin {
|
|
|
9
9
|
static get system () { return 'mssql' }
|
|
10
10
|
|
|
11
11
|
start ({ queryOrProcedure, connectionConfig }) {
|
|
12
|
-
this.startSpan(
|
|
13
|
-
service: this.config.
|
|
12
|
+
this.startSpan(this.operationName(), {
|
|
13
|
+
service: this.serviceName(this.config, this.system),
|
|
14
14
|
resource: queryOrProcedure,
|
|
15
15
|
type: 'sql',
|
|
16
16
|
kind: 'client',
|
|
@@ -4,9 +4,12 @@ module.exports = {
|
|
|
4
4
|
'COMMAND_INJECTION_ANALYZER': require('./command-injection-analyzer'),
|
|
5
5
|
'INSECURE_COOKIE_ANALYZER': require('./insecure-cookie-analyzer'),
|
|
6
6
|
'LDAP_ANALYZER': require('./ldap-injection-analyzer'),
|
|
7
|
+
'NO_HTTPONLY_COOKIE_ANALYZER': require('./no-httponly-cookie-analyzer'),
|
|
8
|
+
'NO_SAMESITE_COOKIE_ANALYZER': require('./no-samesite-cookie-analyzer'),
|
|
7
9
|
'PATH_TRAVERSAL_ANALYZER': require('./path-traversal-analyzer'),
|
|
8
10
|
'SQL_INJECTION_ANALYZER': require('./sql-injection-analyzer'),
|
|
9
11
|
'SSRF': require('./ssrf-analyzer'),
|
|
12
|
+
'UNVALIDATED_REDIRECT_ANALYZER': require('./unvalidated-redirect-analyzer'),
|
|
10
13
|
'WEAK_CIPHER_ANALYZER': require('./weak-cipher-analyzer'),
|
|
11
14
|
'WEAK_HASH_ANALYZER': require('./weak-hash-analyzer')
|
|
12
15
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Analyzer = require('./vulnerability-analyzer')
|
|
4
|
+
const { getNodeModulesPaths } = require('../path-line')
|
|
5
|
+
|
|
6
|
+
const EXCLUDED_PATHS = getNodeModulesPaths('express/lib/response.js')
|
|
7
|
+
|
|
8
|
+
class CookieAnalyzer extends Analyzer {
|
|
9
|
+
constructor (type, propertyToBeSafe) {
|
|
10
|
+
super(type)
|
|
11
|
+
this.propertyToBeSafe = propertyToBeSafe.toLowerCase()
|
|
12
|
+
this.addSub('datadog:iast:set-cookie', (cookieInfo) => this.analyze(cookieInfo))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
_isVulnerable ({ cookieProperties, cookieValue }) {
|
|
16
|
+
return cookieValue && !(cookieProperties && cookieProperties
|
|
17
|
+
.map(x => x.toLowerCase().trim()).includes(this.propertyToBeSafe))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_getEvidence ({ cookieName }) {
|
|
21
|
+
return { value: cookieName }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_createHashSource (type, evidence, location) {
|
|
25
|
+
return `${type}:${evidence.value}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_getExcludedPaths () {
|
|
29
|
+
return EXCLUDED_PATHS
|
|
30
|
+
}
|
|
31
|
+
_checkOCE (context, value) {
|
|
32
|
+
if (value && value.location) {
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
35
|
+
return super._checkOCE(context, value)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_getLocation (value) {
|
|
39
|
+
if (!value) {
|
|
40
|
+
return super._getLocation()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (value.location) {
|
|
44
|
+
return value.location
|
|
45
|
+
}
|
|
46
|
+
const location = super._getLocation(value)
|
|
47
|
+
value.location = location
|
|
48
|
+
return location
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = CookieAnalyzer
|
|
@@ -1,30 +1,11 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const Analyzer = require('./vulnerability-analyzer')
|
|
4
3
|
const { INSECURE_COOKIE } = require('../vulnerabilities')
|
|
4
|
+
const CookieAnalyzer = require('./cookie-analyzer')
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class InsecureCookieAnalyzer extends Analyzer {
|
|
6
|
+
class InsecureCookieAnalyzer extends CookieAnalyzer {
|
|
9
7
|
constructor () {
|
|
10
|
-
super(INSECURE_COOKIE)
|
|
11
|
-
this.addSub('datadog:iast:set-cookie', (cookieInfo) => this.analyze(cookieInfo))
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
_isVulnerable ({ cookieProperties, cookieValue }) {
|
|
15
|
-
return cookieValue && !(cookieProperties && cookieProperties.map(x => x.toLowerCase().trim()).includes('secure'))
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
_getEvidence ({ cookieName }) {
|
|
19
|
-
return { value: cookieName }
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
_createHashSource (type, evidence, location) {
|
|
23
|
-
return `${type}:${evidence.value}`
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
_getExcludedPaths () {
|
|
27
|
-
return EXCLUDED_PATHS
|
|
8
|
+
super(INSECURE_COOKIE, 'secure')
|
|
28
9
|
}
|
|
29
10
|
}
|
|
30
11
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { NO_HTTPONLY_COOKIE } = require('../vulnerabilities')
|
|
4
|
+
const CookieAnalyzer = require('./cookie-analyzer')
|
|
5
|
+
|
|
6
|
+
class NoHttponlyCookieAnalyzer extends CookieAnalyzer {
|
|
7
|
+
constructor () {
|
|
8
|
+
super(NO_HTTPONLY_COOKIE, 'HttpOnly')
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = new NoHttponlyCookieAnalyzer()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { NO_SAMESITE_COOKIE } = require('../vulnerabilities')
|
|
4
|
+
const CookieAnalyzer = require('./cookie-analyzer')
|
|
5
|
+
|
|
6
|
+
class NoSamesiteCookieAnalyzer extends CookieAnalyzer {
|
|
7
|
+
constructor () {
|
|
8
|
+
super(NO_SAMESITE_COOKIE, 'SameSite=strict')
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = new NoSamesiteCookieAnalyzer()
|
|
@@ -14,24 +14,28 @@ class SetCookiesHeaderInterceptor extends Plugin {
|
|
|
14
14
|
allCookies = [value]
|
|
15
15
|
}
|
|
16
16
|
const alreadyCheckedCookies = this._getAlreadyCheckedCookiesInResponse(res)
|
|
17
|
+
|
|
18
|
+
let location
|
|
17
19
|
allCookies.forEach(cookieString => {
|
|
18
20
|
if (!alreadyCheckedCookies.includes(cookieString)) {
|
|
19
21
|
alreadyCheckedCookies.push(cookieString)
|
|
20
|
-
|
|
22
|
+
const parsedCookie = this._parseCookie(cookieString, location)
|
|
23
|
+
setCookieChannel.publish(parsedCookie)
|
|
24
|
+
location = parsedCookie.location
|
|
21
25
|
}
|
|
22
26
|
})
|
|
23
27
|
}
|
|
24
28
|
})
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
_parseCookie (cookieString) {
|
|
31
|
+
_parseCookie (cookieString, location) {
|
|
28
32
|
const cookieParts = cookieString.split(';')
|
|
29
33
|
const nameValueParts = cookieParts[0].split('=')
|
|
30
34
|
const cookieName = nameValueParts[0]
|
|
31
35
|
const cookieValue = nameValueParts.slice(1).join('=')
|
|
32
36
|
const cookieProperties = cookieParts.slice(1).map(part => part.trim())
|
|
33
37
|
|
|
34
|
-
return { cookieName, cookieValue, cookieProperties, cookieString }
|
|
38
|
+
return { cookieName, cookieValue, cookieProperties, cookieString, location }
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
_getAlreadyCheckedCookiesInResponse (res) {
|
|
@@ -6,9 +6,9 @@ const { getRanges } = require('../taint-tracking/operations')
|
|
|
6
6
|
const { storage } = require('../../../../../datadog-core')
|
|
7
7
|
const { getIastContext } = require('../iast-context')
|
|
8
8
|
const { addVulnerability } = require('../vulnerability-reporter')
|
|
9
|
+
const { getNodeModulesPaths } = require('../path-line')
|
|
9
10
|
|
|
10
|
-
const EXCLUDED_PATHS =
|
|
11
|
-
'node_modules\\sequelize']
|
|
11
|
+
const EXCLUDED_PATHS = getNodeModulesPaths('mysql2', 'sequelize')
|
|
12
12
|
|
|
13
13
|
class SqlInjectionAnalyzer extends InjectionAnalyzer {
|
|
14
14
|
constructor () {
|
|
@@ -28,7 +28,7 @@ class SqlInjectionAnalyzer extends InjectionAnalyzer {
|
|
|
28
28
|
|
|
29
29
|
this.addSub('datadog:sequelize:query:finish', () => {
|
|
30
30
|
const store = storage.getStore()
|
|
31
|
-
if (store.sequelizeParentStore) {
|
|
31
|
+
if (store && store.sequelizeParentStore) {
|
|
32
32
|
storage.enterWith(store.sequelizeParentStore)
|
|
33
33
|
}
|
|
34
34
|
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const InjectionAnalyzer = require('./injection-analyzer')
|
|
4
|
+
const { UNVALIDATED_REDIRECT } = require('../vulnerabilities')
|
|
5
|
+
const { getNodeModulesPaths } = require('../path-line')
|
|
6
|
+
const { getRanges } = require('../taint-tracking/operations')
|
|
7
|
+
const { HTTP_REQUEST_HEADER_VALUE } = require('../taint-tracking/origin-types')
|
|
8
|
+
|
|
9
|
+
const EXCLUDED_PATHS = getNodeModulesPaths('express/lib/response.js')
|
|
10
|
+
|
|
11
|
+
class UnvalidatedRedirectAnalyzer extends InjectionAnalyzer {
|
|
12
|
+
constructor () {
|
|
13
|
+
super(UNVALIDATED_REDIRECT)
|
|
14
|
+
|
|
15
|
+
this.addSub('datadog:http:server:response:set-header:finish', ({ name, value }) => this.analyze(name, value))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// TODO: In case the location header value is tainted, this analyzer should check the ranges of the tainted.
|
|
19
|
+
// And do not report a vulnerability if source of the ranges (range.iinfo.type) are exclusively url or path params
|
|
20
|
+
// to avoid false positives.
|
|
21
|
+
analyze (name, value) {
|
|
22
|
+
if (!this.isLocationHeader(name) || typeof value !== 'string') return
|
|
23
|
+
|
|
24
|
+
super.analyze(value)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
isLocationHeader (name) {
|
|
28
|
+
return name && name.trim().toLowerCase() === 'location'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_isVulnerable (value, iastContext) {
|
|
32
|
+
if (!value) return false
|
|
33
|
+
|
|
34
|
+
const ranges = getRanges(iastContext, value)
|
|
35
|
+
return ranges && ranges.length > 0 && !this._isRefererHeader(ranges)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_isRefererHeader (ranges) {
|
|
39
|
+
return ranges && ranges.every(range => range.iinfo.type === HTTP_REQUEST_HEADER_VALUE &&
|
|
40
|
+
range.iinfo.parameterName && range.iinfo.parameterName.toLowerCase() === 'referer')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_getExcludedPaths () {
|
|
44
|
+
return EXCLUDED_PATHS
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = new UnvalidatedRedirectAnalyzer()
|
|
@@ -38,7 +38,7 @@ class Analyzer extends Plugin {
|
|
|
38
38
|
|
|
39
39
|
_report (value, context) {
|
|
40
40
|
const evidence = this._getEvidence(value, context)
|
|
41
|
-
const location = this._getLocation()
|
|
41
|
+
const location = this._getLocation(value)
|
|
42
42
|
if (!this._isExcluded(location)) {
|
|
43
43
|
const spanId = context && context.rootSpan && context.rootSpan.context().toSpanId()
|
|
44
44
|
const vulnerability = this._createVulnerability(this._type, evidence, spanId, location)
|
|
@@ -47,7 +47,7 @@ class Analyzer extends Plugin {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
_reportIfVulnerable (value, context) {
|
|
50
|
-
if (this._isVulnerable(value, context) && this._checkOCE(context)) {
|
|
50
|
+
if (this._isVulnerable(value, context) && this._checkOCE(context, value)) {
|
|
51
51
|
this._report(value, context)
|
|
52
52
|
return true
|
|
53
53
|
}
|
|
@@ -78,7 +78,7 @@ class Analyzer extends Plugin {
|
|
|
78
78
|
for (let i = 0; i < values.length; i++) {
|
|
79
79
|
const value = values[i]
|
|
80
80
|
if (this._isVulnerable(value, iastContext)) {
|
|
81
|
-
if (this._checkOCE(iastContext)) {
|
|
81
|
+
if (this._checkOCE(iastContext, value)) {
|
|
82
82
|
this._report(value, iastContext)
|
|
83
83
|
}
|
|
84
84
|
break
|
|
@@ -5,10 +5,16 @@ const { storage } = require('../../../../datadog-core')
|
|
|
5
5
|
const overheadController = require('./overhead-controller')
|
|
6
6
|
const dc = require('../../../../diagnostics_channel')
|
|
7
7
|
const iastContextFunctions = require('./iast-context')
|
|
8
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
enableTaintTracking,
|
|
10
|
+
disableTaintTracking,
|
|
11
|
+
createTransaction,
|
|
12
|
+
removeTransaction,
|
|
13
|
+
taintTrackingPlugin
|
|
14
|
+
} = require('./taint-tracking')
|
|
15
|
+
const { IAST_ENABLED_TAG_KEY } = require('./tags')
|
|
9
16
|
|
|
10
17
|
const telemetryLogs = require('./telemetry/logs')
|
|
11
|
-
const IAST_ENABLED_TAG_KEY = '_dd.iast.enabled'
|
|
12
18
|
|
|
13
19
|
// TODO Change to `apm:http:server:request:[start|close]` when the subscription
|
|
14
20
|
// order of the callbacks can be enforce
|
|
@@ -48,6 +54,7 @@ function onIncomingHttpRequestStart (data) {
|
|
|
48
54
|
const iastContext = iastContextFunctions.saveIastContext(store, topContext, { rootSpan, req: data.req })
|
|
49
55
|
createTransaction(rootSpan.context().toSpanId(), iastContext)
|
|
50
56
|
overheadController.initializeRequestContext(iastContext)
|
|
57
|
+
taintTrackingPlugin.taintHeaders(data.req.headers, iastContext)
|
|
51
58
|
}
|
|
52
59
|
if (rootSpan.addTags) {
|
|
53
60
|
rootSpan.addTags({
|
|
@@ -5,6 +5,7 @@ const process = require('process')
|
|
|
5
5
|
const { calculateDDBasePath } = require('../../util')
|
|
6
6
|
const pathLine = {
|
|
7
7
|
getFirstNonDDPathAndLine,
|
|
8
|
+
getNodeModulesPaths,
|
|
8
9
|
getFirstNonDDPathAndLineFromCallsites, // Exported only for test purposes
|
|
9
10
|
calculateDDBasePath, // Exported only for test purposes
|
|
10
11
|
ddBasePath: calculateDDBasePath(__dirname) // Only for test purposes
|
|
@@ -83,4 +84,16 @@ function isExcluded (callsite, externallyExcludedPaths) {
|
|
|
83
84
|
function getFirstNonDDPathAndLine (externallyExcludedPaths) {
|
|
84
85
|
return getFirstNonDDPathAndLineFromCallsites(getCallSiteInfo(), externallyExcludedPaths)
|
|
85
86
|
}
|
|
87
|
+
|
|
88
|
+
function getNodeModulesPaths (...paths) {
|
|
89
|
+
const nodeModulesPaths = []
|
|
90
|
+
|
|
91
|
+
paths.forEach(p => {
|
|
92
|
+
const pathParts = p.split('/')
|
|
93
|
+
nodeModulesPaths.push(path.join('node_modules', ...pathParts))
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
return nodeModulesPaths
|
|
97
|
+
}
|
|
98
|
+
|
|
86
99
|
module.exports = pathLine
|