dd-trace 3.21.0 → 3.22.1
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/package.json +3 -2
- package/packages/datadog-esbuild/index.js +13 -1
- package/packages/datadog-instrumentations/src/cucumber.js +13 -0
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/http/client.js +2 -1
- package/packages/datadog-instrumentations/src/http/server.js +14 -0
- package/packages/datadog-instrumentations/src/http2/client.js +4 -0
- package/packages/datadog-instrumentations/src/pg.js +14 -11
- package/packages/datadog-instrumentations/src/playwright.js +1 -1
- package/packages/datadog-instrumentations/src/sequelize.js +51 -0
- package/packages/datadog-plugin-amqp10/src/consumer.js +1 -3
- package/packages/datadog-plugin-amqp10/src/producer.js +1 -3
- package/packages/datadog-plugin-amqplib/src/client.js +4 -3
- package/packages/datadog-plugin-amqplib/src/consumer.js +1 -3
- package/packages/datadog-plugin-amqplib/src/producer.js +1 -3
- package/packages/datadog-plugin-cucumber/src/index.js +2 -2
- package/packages/datadog-plugin-cypress/src/plugin.js +150 -30
- package/packages/datadog-plugin-cypress/src/support.js +6 -3
- package/packages/datadog-plugin-google-cloud-pubsub/src/client.js +4 -3
- package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +1 -3
- package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +1 -3
- package/packages/datadog-plugin-http/src/client.js +70 -68
- package/packages/datadog-plugin-http2/src/client.js +50 -47
- package/packages/datadog-plugin-jest/src/index.js +5 -4
- package/packages/datadog-plugin-kafkajs/src/consumer.js +1 -4
- package/packages/datadog-plugin-kafkajs/src/producer.js +1 -3
- package/packages/datadog-plugin-memcached/src/index.js +2 -3
- package/packages/datadog-plugin-mocha/src/index.js +4 -2
- package/packages/datadog-plugin-pg/src/index.js +1 -1
- package/packages/datadog-plugin-redis/src/index.js +2 -13
- package/packages/datadog-plugin-rhea/src/consumer.js +1 -3
- package/packages/datadog-plugin-rhea/src/producer.js +1 -5
- package/packages/datadog-plugin-router/src/index.js +12 -1
- package/packages/dd-trace/src/appsec/blocked_templates.js +2 -101
- package/packages/dd-trace/src/appsec/blocking.js +60 -11
- package/packages/dd-trace/src/appsec/channels.js +3 -2
- package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +7 -5
- package/packages/dd-trace/src/appsec/iast/analyzers/index.js +3 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/insecure-cookie-analyzer.js +31 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/path-traversal-analyzer.js +4 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/set-cookies-header-interceptor.js +47 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +30 -5
- package/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js +26 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +35 -3
- package/packages/dd-trace/src/appsec/iast/path-line.js +14 -7
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +19 -4
- package/packages/dd-trace/src/appsec/iast/telemetry/logs.js +1 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +25 -2
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +49 -0
- 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 +7 -5
- package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +0 -33
- package/packages/dd-trace/src/appsec/recommended.json +45 -46
- package/packages/dd-trace/src/appsec/remote_config/capabilities.js +3 -1
- package/packages/dd-trace/src/appsec/remote_config/index.js +4 -0
- package/packages/dd-trace/src/appsec/rule_manager.js +49 -6
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +2 -7
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +1 -6
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +10 -4
- package/packages/dd-trace/src/config.js +36 -5
- package/packages/dd-trace/src/encode/coverage-ci-visibility.js +11 -3
- package/packages/dd-trace/src/exporters/common/util.js +9 -0
- package/packages/dd-trace/src/exporters/common/writer.js +3 -2
- package/packages/dd-trace/src/plugin_manager.js +2 -0
- package/packages/dd-trace/src/plugins/cache.js +7 -0
- package/packages/dd-trace/src/plugins/ci_plugin.js +2 -0
- package/packages/dd-trace/src/plugins/client.js +3 -2
- package/packages/dd-trace/src/plugins/consumer.js +14 -2
- package/packages/dd-trace/src/plugins/database.js +2 -2
- package/packages/dd-trace/src/plugins/inbound.js +7 -0
- package/packages/dd-trace/src/plugins/{outgoing.js → outbound.js} +2 -2
- package/packages/dd-trace/src/plugins/producer.js +19 -2
- package/packages/dd-trace/src/plugins/server.js +2 -2
- package/packages/dd-trace/src/plugins/storage.js +2 -0
- package/packages/dd-trace/src/plugins/tracing.js +11 -0
- package/packages/dd-trace/src/plugins/util/ci.js +1 -1
- package/packages/dd-trace/src/profiling/config.js +4 -2
- package/packages/dd-trace/src/service-naming/index.js +30 -0
- package/packages/dd-trace/src/service-naming/schemas/definition.js +24 -0
- package/packages/dd-trace/src/service-naming/schemas/index.js +6 -0
- package/packages/dd-trace/src/service-naming/schemas/util.js +5 -0
- package/packages/dd-trace/src/service-naming/schemas/v0/index.js +5 -0
- package/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +64 -0
- package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +33 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/index.js +5 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/messaging.js +52 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +21 -0
- package/packages/dd-trace/src/telemetry/index.js +5 -6
- package/packages/dd-trace/src/telemetry/send-data.js +17 -5
- package/packages/dd-trace/src/plugins/incoming.js +0 -7
|
@@ -5,32 +5,62 @@ const blockedTemplates = require('./blocked_templates')
|
|
|
5
5
|
|
|
6
6
|
let templateHtml = blockedTemplates.html
|
|
7
7
|
let templateJson = blockedTemplates.json
|
|
8
|
+
let blockingConfiguration
|
|
8
9
|
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
function blockWithRedirect (res, rootSpan, abortController) {
|
|
11
|
+
rootSpan.addTags({
|
|
12
|
+
'appsec.blocked': 'true'
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
let statusCode = blockingConfiguration.parameters.status_code
|
|
16
|
+
if (!statusCode || statusCode < 300 || statusCode >= 400) {
|
|
17
|
+
statusCode = 303
|
|
13
18
|
}
|
|
14
19
|
|
|
20
|
+
res.writeHead(statusCode, {
|
|
21
|
+
'Location': blockingConfiguration.parameters.location
|
|
22
|
+
}).end()
|
|
23
|
+
|
|
24
|
+
if (abortController) {
|
|
25
|
+
abortController.abort()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function blockWithContent (req, res, rootSpan, abortController) {
|
|
15
30
|
let type
|
|
16
31
|
let body
|
|
17
32
|
|
|
18
33
|
// parse the Accept header, ex: Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8
|
|
19
34
|
const accept = req.headers.accept && req.headers.accept.split(',').map((str) => str.split(';', 1)[0].trim())
|
|
20
35
|
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
36
|
+
if (!blockingConfiguration || blockingConfiguration.parameters.type === 'auto') {
|
|
37
|
+
if (accept && accept.includes('text/html') && !accept.includes('application/json')) {
|
|
38
|
+
type = 'text/html; charset=utf-8'
|
|
39
|
+
body = templateHtml
|
|
40
|
+
} else {
|
|
41
|
+
type = 'application/json'
|
|
42
|
+
body = templateJson
|
|
43
|
+
}
|
|
24
44
|
} else {
|
|
25
|
-
type
|
|
26
|
-
|
|
45
|
+
if (blockingConfiguration.parameters.type === 'html') {
|
|
46
|
+
type = 'text/html; charset=utf-8'
|
|
47
|
+
body = templateHtml
|
|
48
|
+
} else {
|
|
49
|
+
type = 'application/json'
|
|
50
|
+
body = templateJson
|
|
51
|
+
}
|
|
27
52
|
}
|
|
28
53
|
|
|
29
54
|
rootSpan.addTags({
|
|
30
55
|
'appsec.blocked': 'true'
|
|
31
56
|
})
|
|
32
57
|
|
|
33
|
-
|
|
58
|
+
if (blockingConfiguration && blockingConfiguration.type === 'block_request' &&
|
|
59
|
+
blockingConfiguration.parameters.status_code) {
|
|
60
|
+
res.statusCode = blockingConfiguration.parameters.status_code
|
|
61
|
+
} else {
|
|
62
|
+
res.statusCode = 403
|
|
63
|
+
}
|
|
34
64
|
res.setHeader('Content-Type', type)
|
|
35
65
|
res.setHeader('Content-Length', Buffer.byteLength(body))
|
|
36
66
|
res.end(body)
|
|
@@ -40,6 +70,20 @@ function block (req, res, rootSpan, abortController) {
|
|
|
40
70
|
}
|
|
41
71
|
}
|
|
42
72
|
|
|
73
|
+
function block (req, res, rootSpan, abortController) {
|
|
74
|
+
if (res.headersSent) {
|
|
75
|
+
log.warn('Cannot send blocking response when headers have already been sent')
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (blockingConfiguration && blockingConfiguration.type === 'redirect_request' &&
|
|
80
|
+
blockingConfiguration.parameters.location) {
|
|
81
|
+
blockWithRedirect(res, rootSpan, abortController)
|
|
82
|
+
} else {
|
|
83
|
+
blockWithContent(req, res, rootSpan, abortController)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
43
87
|
function setTemplates (config) {
|
|
44
88
|
if (config.appsec.blockedTemplateHtml) {
|
|
45
89
|
templateHtml = config.appsec.blockedTemplateHtml
|
|
@@ -49,7 +93,12 @@ function setTemplates (config) {
|
|
|
49
93
|
}
|
|
50
94
|
}
|
|
51
95
|
|
|
96
|
+
function updateBlockingConfiguration (newBlockingConfiguration) {
|
|
97
|
+
blockingConfiguration = newBlockingConfiguration
|
|
98
|
+
}
|
|
99
|
+
|
|
52
100
|
module.exports = {
|
|
53
101
|
block,
|
|
54
|
-
setTemplates
|
|
102
|
+
setTemplates,
|
|
103
|
+
updateBlockingConfiguration
|
|
55
104
|
}
|
|
@@ -4,8 +4,9 @@ const dc = require('../../../diagnostics_channel')
|
|
|
4
4
|
|
|
5
5
|
// TODO: use TBD naming convention
|
|
6
6
|
module.exports = {
|
|
7
|
+
bodyParser: dc.channel('datadog:body-parser:read:finish'),
|
|
7
8
|
incomingHttpRequestStart: dc.channel('dd-trace:incomingHttpRequestStart'),
|
|
8
9
|
incomingHttpRequestEnd: dc.channel('dd-trace:incomingHttpRequestEnd'),
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
queryParser: dc.channel('datadog:query:read:finish'),
|
|
11
|
+
setCookieChannel: dc.channel('datadog:iast:set-cookie')
|
|
11
12
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
|
-
'WEAK_CIPHER_ANALYZER': require('./weak-cipher-analyzer'),
|
|
5
|
-
'WEAK_HASH_ANALYZER': require('./weak-hash-analyzer'),
|
|
6
|
-
'SQL_INJECTION_ANALYZER': require('./sql-injection-analyzer'),
|
|
7
|
-
'PATH_TRAVERSAL_ANALYZER': require('./path-traversal-analyzer'),
|
|
8
4
|
'COMMAND_INJECTION_ANALYZER': require('./command-injection-analyzer'),
|
|
9
|
-
'
|
|
5
|
+
'INSECURE_COOKIE_ANALYZER': require('./insecure-cookie-analyzer'),
|
|
6
|
+
'LDAP_ANALYZER': require('./ldap-injection-analyzer'),
|
|
7
|
+
'PATH_TRAVERSAL_ANALYZER': require('./path-traversal-analyzer'),
|
|
8
|
+
'SQL_INJECTION_ANALYZER': require('./sql-injection-analyzer'),
|
|
9
|
+
'SSRF': require('./ssrf-analyzer'),
|
|
10
|
+
'WEAK_CIPHER_ANALYZER': require('./weak-cipher-analyzer'),
|
|
11
|
+
'WEAK_HASH_ANALYZER': require('./weak-hash-analyzer')
|
|
10
12
|
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const analyzers = require('./analyzers')
|
|
4
|
+
const setCookiesHeaderInterceptor = require('./set-cookies-header-interceptor')
|
|
4
5
|
|
|
5
6
|
function enableAllAnalyzers () {
|
|
7
|
+
setCookiesHeaderInterceptor.configure(true)
|
|
6
8
|
for (const analyzer in analyzers) {
|
|
7
9
|
analyzers[analyzer].configure(true)
|
|
8
10
|
}
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
function disableAllAnalyzers () {
|
|
14
|
+
setCookiesHeaderInterceptor.configure(false)
|
|
12
15
|
for (const analyzer in analyzers) {
|
|
13
16
|
analyzers[analyzer].configure(false)
|
|
14
17
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Analyzer = require('./vulnerability-analyzer')
|
|
4
|
+
const { INSECURE_COOKIE } = require('../vulnerabilities')
|
|
5
|
+
|
|
6
|
+
const EXCLUDED_PATHS = ['node_modules/express/lib/response.js', 'node_modules\\express\\lib\\response.js']
|
|
7
|
+
|
|
8
|
+
class InsecureCookieAnalyzer extends Analyzer {
|
|
9
|
+
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
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = new InsecureCookieAnalyzer()
|
|
@@ -6,10 +6,14 @@ const { storage } = require('../../../../../datadog-core')
|
|
|
6
6
|
const InjectionAnalyzer = require('./injection-analyzer')
|
|
7
7
|
const { PATH_TRAVERSAL } = require('../vulnerabilities')
|
|
8
8
|
|
|
9
|
+
const ignoredOperations = ['dir.close', 'close']
|
|
10
|
+
|
|
9
11
|
class PathTraversalAnalyzer extends InjectionAnalyzer {
|
|
10
12
|
constructor () {
|
|
11
13
|
super(PATH_TRAVERSAL)
|
|
12
14
|
this.addSub('apm:fs:operation:start', obj => {
|
|
15
|
+
if (ignoredOperations.includes(obj.operation)) return
|
|
16
|
+
|
|
13
17
|
const pathArguments = []
|
|
14
18
|
if (obj.dest) {
|
|
15
19
|
pathArguments.push(obj.dest)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Plugin = require('../../../plugins/plugin')
|
|
4
|
+
const { setCookieChannel } = require('../../channels')
|
|
5
|
+
|
|
6
|
+
class SetCookiesHeaderInterceptor extends Plugin {
|
|
7
|
+
constructor () {
|
|
8
|
+
super()
|
|
9
|
+
this.cookiesInRequest = new WeakMap()
|
|
10
|
+
this.addSub('datadog:http:server:response:set-header:finish', ({ name, value, res }) => {
|
|
11
|
+
if (name.toLowerCase() === 'set-cookie') {
|
|
12
|
+
let allCookies = value
|
|
13
|
+
if (typeof value === 'string') {
|
|
14
|
+
allCookies = [value]
|
|
15
|
+
}
|
|
16
|
+
const alreadyCheckedCookies = this._getAlreadyCheckedCookiesInResponse(res)
|
|
17
|
+
allCookies.forEach(cookieString => {
|
|
18
|
+
if (!alreadyCheckedCookies.includes(cookieString)) {
|
|
19
|
+
alreadyCheckedCookies.push(cookieString)
|
|
20
|
+
setCookieChannel.publish(this._parseCookie(cookieString))
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_parseCookie (cookieString) {
|
|
28
|
+
const cookieParts = cookieString.split(';')
|
|
29
|
+
const nameValueParts = cookieParts[0].split('=')
|
|
30
|
+
const cookieName = nameValueParts[0]
|
|
31
|
+
const cookieValue = nameValueParts.slice(1).join('=')
|
|
32
|
+
const cookieProperties = cookieParts.slice(1).map(part => part.trim())
|
|
33
|
+
|
|
34
|
+
return { cookieName, cookieValue, cookieProperties, cookieString }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_getAlreadyCheckedCookiesInResponse (res) {
|
|
38
|
+
let alreadyCheckedCookies = this.cookiesInRequest.get(res)
|
|
39
|
+
if (!alreadyCheckedCookies) {
|
|
40
|
+
alreadyCheckedCookies = []
|
|
41
|
+
this.cookiesInRequest.set(res, alreadyCheckedCookies)
|
|
42
|
+
}
|
|
43
|
+
return alreadyCheckedCookies
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = new SetCookiesHeaderInterceptor()
|
|
@@ -5,7 +5,10 @@ const { SQL_INJECTION } = require('../vulnerabilities')
|
|
|
5
5
|
const { getRanges } = require('../taint-tracking/operations')
|
|
6
6
|
const { storage } = require('../../../../../datadog-core')
|
|
7
7
|
const { getIastContext } = require('../iast-context')
|
|
8
|
-
const {
|
|
8
|
+
const { addVulnerability } = require('../vulnerability-reporter')
|
|
9
|
+
|
|
10
|
+
const EXCLUDED_PATHS = ['node_modules/mysql2', 'node_modules/sequelize', 'node_modules\\mysql2',
|
|
11
|
+
'node_modules\\sequelize']
|
|
9
12
|
|
|
10
13
|
class SqlInjectionAnalyzer extends InjectionAnalyzer {
|
|
11
14
|
constructor () {
|
|
@@ -13,6 +16,22 @@ class SqlInjectionAnalyzer extends InjectionAnalyzer {
|
|
|
13
16
|
this.addSub('apm:mysql:query:start', ({ sql }) => this.analyze(sql, 'MYSQL'))
|
|
14
17
|
this.addSub('apm:mysql2:query:start', ({ sql }) => this.analyze(sql, 'MYSQL'))
|
|
15
18
|
this.addSub('apm:pg:query:start', ({ query }) => this.analyze(query.text, 'POSTGRES'))
|
|
19
|
+
|
|
20
|
+
this.addSub('datadog:sequelize:query:start', ({ sql, dialect }) => {
|
|
21
|
+
const parentStore = storage.getStore()
|
|
22
|
+
if (parentStore) {
|
|
23
|
+
this.analyze(sql, dialect.toUpperCase())
|
|
24
|
+
|
|
25
|
+
storage.enterWith({ ...parentStore, sqlAnalyzed: true, sequelizeParentStore: parentStore })
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
this.addSub('datadog:sequelize:query:finish', () => {
|
|
30
|
+
const store = storage.getStore()
|
|
31
|
+
if (store.sequelizeParentStore) {
|
|
32
|
+
storage.enterWith(store.sequelizeParentStore)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
16
35
|
}
|
|
17
36
|
|
|
18
37
|
_getEvidence (value, iastContext, dialect) {
|
|
@@ -22,9 +41,12 @@ class SqlInjectionAnalyzer extends InjectionAnalyzer {
|
|
|
22
41
|
|
|
23
42
|
analyze (value, dialect) {
|
|
24
43
|
const store = storage.getStore()
|
|
25
|
-
|
|
26
|
-
if (store &&
|
|
27
|
-
|
|
44
|
+
|
|
45
|
+
if (!(store && store.sqlAnalyzed)) {
|
|
46
|
+
const iastContext = getIastContext(store)
|
|
47
|
+
if (store && !iastContext) return
|
|
48
|
+
this._reportIfVulnerable(value, iastContext, dialect)
|
|
49
|
+
}
|
|
28
50
|
}
|
|
29
51
|
|
|
30
52
|
_reportIfVulnerable (value, context, dialect) {
|
|
@@ -40,10 +62,13 @@ class SqlInjectionAnalyzer extends InjectionAnalyzer {
|
|
|
40
62
|
const location = this._getLocation()
|
|
41
63
|
if (!this._isExcluded(location)) {
|
|
42
64
|
const spanId = context && context.rootSpan && context.rootSpan.context().toSpanId()
|
|
43
|
-
const vulnerability =
|
|
65
|
+
const vulnerability = this._createVulnerability(this._type, evidence, spanId, location)
|
|
44
66
|
addVulnerability(context, vulnerability)
|
|
45
67
|
}
|
|
46
68
|
}
|
|
69
|
+
_getExcludedPaths () {
|
|
70
|
+
return EXCLUDED_PATHS
|
|
71
|
+
}
|
|
47
72
|
}
|
|
48
73
|
|
|
49
74
|
module.exports = new SqlInjectionAnalyzer()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const InjectionAnalyzer = require('./injection-analyzer')
|
|
4
|
+
const { SSRF } = require('../vulnerabilities')
|
|
5
|
+
|
|
6
|
+
class SSRFAnalyzer extends InjectionAnalyzer {
|
|
7
|
+
constructor () {
|
|
8
|
+
super(SSRF)
|
|
9
|
+
|
|
10
|
+
this.addSub('apm:http:client:request:start', ({ args }) => {
|
|
11
|
+
if (typeof args.originalUrl === 'string') {
|
|
12
|
+
this.analyze(args.originalUrl)
|
|
13
|
+
} else if (args.options && args.options.host) {
|
|
14
|
+
this.analyze(args.options.host)
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
this.addSub('apm:http2:client:connect:start', ({ authority }) => {
|
|
19
|
+
if (authority && typeof authority === 'string') {
|
|
20
|
+
this.analyze(authority)
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = new SSRFAnalyzer()
|
|
@@ -4,7 +4,7 @@ const Plugin = require('../../../../src/plugins/plugin')
|
|
|
4
4
|
const { storage } = require('../../../../../datadog-core')
|
|
5
5
|
const iastLog = require('../iast-log')
|
|
6
6
|
const { getFirstNonDDPathAndLine } = require('../path-line')
|
|
7
|
-
const {
|
|
7
|
+
const { addVulnerability } = require('../vulnerability-reporter')
|
|
8
8
|
const { getIastContext } = require('../iast-context')
|
|
9
9
|
const overheadController = require('../overhead-controller')
|
|
10
10
|
|
|
@@ -41,7 +41,7 @@ class Analyzer extends Plugin {
|
|
|
41
41
|
const location = this._getLocation()
|
|
42
42
|
if (!this._isExcluded(location)) {
|
|
43
43
|
const spanId = context && context.rootSpan && context.rootSpan.context().toSpanId()
|
|
44
|
-
const vulnerability =
|
|
44
|
+
const vulnerability = this._createVulnerability(this._type, evidence, spanId, location)
|
|
45
45
|
addVulnerability(context, vulnerability)
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -59,9 +59,11 @@ class Analyzer extends Plugin {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
_getLocation () {
|
|
62
|
-
return getFirstNonDDPathAndLine()
|
|
62
|
+
return getFirstNonDDPathAndLine(this._getExcludedPaths())
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
_getExcludedPaths () {}
|
|
66
|
+
|
|
65
67
|
analyze (value) {
|
|
66
68
|
const store = storage.getStore()
|
|
67
69
|
const iastContext = getIastContext(store)
|
|
@@ -87,6 +89,36 @@ class Analyzer extends Plugin {
|
|
|
87
89
|
_checkOCE (context) {
|
|
88
90
|
return overheadController.hasQuota(overheadController.OPERATIONS.REPORT_VULNERABILITY, context)
|
|
89
91
|
}
|
|
92
|
+
|
|
93
|
+
_createVulnerability (type, evidence, spanId, location) {
|
|
94
|
+
if (type && evidence) {
|
|
95
|
+
const _spanId = spanId || 0
|
|
96
|
+
return {
|
|
97
|
+
type,
|
|
98
|
+
evidence,
|
|
99
|
+
location: {
|
|
100
|
+
spanId: _spanId,
|
|
101
|
+
...location
|
|
102
|
+
},
|
|
103
|
+
hash: this._createHash(this._createHashSource(type, evidence, location))
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_createHashSource (type, evidence, location) {
|
|
110
|
+
return location ? `${type}:${location.path}:${location.line}` : type
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_createHash (hashSource) {
|
|
114
|
+
let hash = 0
|
|
115
|
+
let offset = 0
|
|
116
|
+
const size = hashSource.length
|
|
117
|
+
for (let i = 0; i < size; i++) {
|
|
118
|
+
hash = ((hash << 5) - hash) + hashSource.charCodeAt(offset++)
|
|
119
|
+
}
|
|
120
|
+
return hash
|
|
121
|
+
}
|
|
90
122
|
}
|
|
91
123
|
|
|
92
124
|
module.exports = Analyzer
|
|
@@ -37,12 +37,12 @@ function getCallSiteInfo () {
|
|
|
37
37
|
return callsiteList
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function getFirstNonDDPathAndLineFromCallsites (callsites) {
|
|
40
|
+
function getFirstNonDDPathAndLineFromCallsites (callsites, externallyExcludedPaths) {
|
|
41
41
|
if (callsites) {
|
|
42
42
|
for (let i = 0; i < callsites.length; i++) {
|
|
43
43
|
const callsite = callsites[i]
|
|
44
44
|
const filepath = callsite.getFileName()
|
|
45
|
-
if (!isExcluded(callsite) && filepath.indexOf(pathLine.ddBasePath) === -1) {
|
|
45
|
+
if (!isExcluded(callsite, externallyExcludedPaths) && filepath.indexOf(pathLine.ddBasePath) === -1) {
|
|
46
46
|
return {
|
|
47
47
|
path: path.relative(process.cwd(), filepath),
|
|
48
48
|
line: callsite.getLineNumber(),
|
|
@@ -54,26 +54,33 @@ function getFirstNonDDPathAndLineFromCallsites (callsites) {
|
|
|
54
54
|
return null
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function isExcluded (callsite) {
|
|
57
|
+
function isExcluded (callsite, externallyExcludedPaths) {
|
|
58
58
|
if (callsite.isNative()) return true
|
|
59
59
|
const filename = callsite.getFileName()
|
|
60
60
|
if (!filename) {
|
|
61
61
|
return true
|
|
62
62
|
}
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
let excludedPaths = EXCLUDED_PATHS
|
|
64
|
+
if (externallyExcludedPaths) {
|
|
65
|
+
excludedPaths = [...excludedPaths, ...externallyExcludedPaths]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < excludedPaths.length; i++) {
|
|
69
|
+
if (filename.indexOf(excludedPaths[i]) > -1) {
|
|
65
70
|
return true
|
|
66
71
|
}
|
|
67
72
|
}
|
|
73
|
+
|
|
68
74
|
for (let i = 0; i < EXCLUDED_PATH_PREFIXES.length; i++) {
|
|
69
75
|
if (filename.indexOf(EXCLUDED_PATH_PREFIXES[i]) === 0) {
|
|
70
76
|
return true
|
|
71
77
|
}
|
|
72
78
|
}
|
|
79
|
+
|
|
73
80
|
return false
|
|
74
81
|
}
|
|
75
82
|
|
|
76
|
-
function getFirstNonDDPathAndLine () {
|
|
77
|
-
return getFirstNonDDPathAndLineFromCallsites(getCallSiteInfo())
|
|
83
|
+
function getFirstNonDDPathAndLine (externallyExcludedPaths) {
|
|
84
|
+
return getFirstNonDDPathAndLineFromCallsites(getCallSiteInfo(), externallyExcludedPaths)
|
|
78
85
|
}
|
|
79
86
|
module.exports = pathLine
|
|
@@ -12,17 +12,32 @@ class TaintTrackingPlugin extends Plugin {
|
|
|
12
12
|
this._type = 'taint-tracking'
|
|
13
13
|
this.addSub(
|
|
14
14
|
'datadog:body-parser:read:finish',
|
|
15
|
-
({ req }) =>
|
|
15
|
+
({ req }) => {
|
|
16
|
+
const iastContext = getIastContext(storage.getStore())
|
|
17
|
+
if (iastContext && iastContext['body'] !== req.body) {
|
|
18
|
+
this._taintTrackingHandler(HTTP_REQUEST_BODY, req, 'body', iastContext)
|
|
19
|
+
iastContext['body'] = req.body
|
|
20
|
+
}
|
|
21
|
+
}
|
|
16
22
|
)
|
|
17
23
|
this.addSub(
|
|
18
24
|
'datadog:qs:parse:finish',
|
|
19
25
|
({ qs }) => this._taintTrackingHandler(HTTP_REQUEST_PARAMETER, qs))
|
|
26
|
+
|
|
27
|
+
this.addSub('apm:express:middleware:next', ({ req }) => {
|
|
28
|
+
if (req && req.body && typeof req.body === 'object') {
|
|
29
|
+
const iastContext = getIastContext(storage.getStore())
|
|
30
|
+
if (iastContext && iastContext['body'] !== req.body) {
|
|
31
|
+
this._taintTrackingHandler(HTTP_REQUEST_BODY, req, 'body', iastContext)
|
|
32
|
+
iastContext['body'] = req.body
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
})
|
|
20
36
|
}
|
|
21
37
|
|
|
22
|
-
_taintTrackingHandler (type, target, property) {
|
|
23
|
-
const iastContext = getIastContext(storage.getStore())
|
|
38
|
+
_taintTrackingHandler (type, target, property, iastContext = getIastContext(storage.getStore())) {
|
|
24
39
|
if (!property) {
|
|
25
|
-
|
|
40
|
+
taintObject(iastContext, target, type)
|
|
26
41
|
} else {
|
|
27
42
|
target[property] = taintObject(iastContext, target[property], type)
|
|
28
43
|
}
|
|
@@ -28,7 +28,7 @@ function sendLogs () {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function isLevelEnabled (level) {
|
|
31
|
-
return isLogCollectionEnabled(config) &&
|
|
31
|
+
return isLogCollectionEnabled(config) && level !== 'DEBUG'
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function isLogCollectionEnabled (config) {
|
|
@@ -21,10 +21,20 @@ const NUMERIC_LITERAL =
|
|
|
21
21
|
INTEGER_NUMBER + EXPONENT
|
|
22
22
|
].join('|')
|
|
23
23
|
})`
|
|
24
|
+
const ORACLE_ESCAPED_LITERAL = 'q\'<.*?>\'|q\'\\(.*?\\)\'|q\'\\{.*?\\}\'|q\'\\[.*?\\]\'|q\'(?<ESCAPE>.).*?\\k<ESCAPE>\''
|
|
24
25
|
|
|
25
26
|
class SqlSensitiveAnalyzer {
|
|
26
27
|
constructor () {
|
|
27
28
|
this._patterns = {
|
|
29
|
+
ANSI: new RegExp( // Default
|
|
30
|
+
[
|
|
31
|
+
NUMERIC_LITERAL,
|
|
32
|
+
STRING_LITERAL,
|
|
33
|
+
LINE_COMMENT,
|
|
34
|
+
BLOCK_COMMENT
|
|
35
|
+
].join('|'),
|
|
36
|
+
'gmi'
|
|
37
|
+
),
|
|
28
38
|
MYSQL: new RegExp(
|
|
29
39
|
[
|
|
30
40
|
NUMERIC_LITERAL,
|
|
@@ -43,13 +53,26 @@ class SqlSensitiveAnalyzer {
|
|
|
43
53
|
BLOCK_COMMENT
|
|
44
54
|
].join('|'),
|
|
45
55
|
'gmi'
|
|
46
|
-
)
|
|
56
|
+
),
|
|
57
|
+
ORACLE: new RegExp([
|
|
58
|
+
NUMERIC_LITERAL,
|
|
59
|
+
ORACLE_ESCAPED_LITERAL,
|
|
60
|
+
STRING_LITERAL,
|
|
61
|
+
LINE_COMMENT,
|
|
62
|
+
BLOCK_COMMENT
|
|
63
|
+
].join('|'),
|
|
64
|
+
'gmi')
|
|
47
65
|
}
|
|
66
|
+
this._patterns.SQLITE = this._patterns.MYSQL
|
|
67
|
+
this._patterns.MARIADB = this._patterns.MYSQL
|
|
48
68
|
}
|
|
49
69
|
|
|
50
70
|
extractSensitiveRanges (evidence) {
|
|
51
71
|
try {
|
|
52
|
-
|
|
72
|
+
let pattern = this._patterns[evidence.dialect]
|
|
73
|
+
if (!pattern) {
|
|
74
|
+
pattern = this._patterns['ANSI']
|
|
75
|
+
}
|
|
53
76
|
pattern.lastIndex = 0
|
|
54
77
|
const tokens = []
|
|
55
78
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const iastLog = require('../../../iast-log')
|
|
4
|
+
|
|
5
|
+
const AUTHORITY = '^(?:[^:]+:)?//([^@]+)@'
|
|
6
|
+
const QUERY_FRAGMENT = '[?#&]([^=&;]+)=([^?#&]+)'
|
|
7
|
+
|
|
8
|
+
class UrlSensitiveAnalyzer {
|
|
9
|
+
constructor () {
|
|
10
|
+
this._pattern = new RegExp([AUTHORITY, QUERY_FRAGMENT].join('|'), 'gmi')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
extractSensitiveRanges (evidence) {
|
|
14
|
+
try {
|
|
15
|
+
const pattern = this._pattern
|
|
16
|
+
|
|
17
|
+
const ranges = []
|
|
18
|
+
let regexResult = pattern.exec(evidence.value)
|
|
19
|
+
|
|
20
|
+
while (regexResult != null) {
|
|
21
|
+
if (typeof regexResult[1] === 'string') {
|
|
22
|
+
// AUTHORITY regex match always ends by group + @
|
|
23
|
+
// it means that the match last chars - 1 are always the group
|
|
24
|
+
const end = regexResult.index + (regexResult[0].length - 1)
|
|
25
|
+
const start = end - regexResult[1].length
|
|
26
|
+
ranges.push({ start, end })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof regexResult[3] === 'string') {
|
|
30
|
+
// QUERY_FRAGMENT regex always ends with the group
|
|
31
|
+
// it means that the match last chars are always the group
|
|
32
|
+
const end = regexResult.index + regexResult[0].length
|
|
33
|
+
const start = end - regexResult[3].length
|
|
34
|
+
ranges.push({ start, end })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
regexResult = pattern.exec(evidence.value)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return ranges
|
|
41
|
+
} catch (e) {
|
|
42
|
+
iastLog.debug(e)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return []
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = UrlSensitiveAnalyzer
|
|
@@ -7,6 +7,7 @@ const { contains, intersects, remove } = require('./range-utils')
|
|
|
7
7
|
const CommandSensitiveAnalyzer = require('./sensitive-analyzers/command-sensitive-analyzer')
|
|
8
8
|
const LdapSensitiveAnalyzer = require('./sensitive-analyzers/ldap-sensitive-analyzer')
|
|
9
9
|
const SqlSensitiveAnalyzer = require('./sensitive-analyzers/sql-sensitive-analyzer')
|
|
10
|
+
const UrlSensitiveAnalyzer = require('./sensitive-analyzers/url-sensitive-analyzer')
|
|
10
11
|
|
|
11
12
|
// eslint-disable-next-line max-len
|
|
12
13
|
const DEFAULT_IAST_REDACTION_NAME_PATTERN = '(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)'
|
|
@@ -22,6 +23,7 @@ class SensitiveHandler {
|
|
|
22
23
|
this._sensitiveAnalyzers.set(vulnerabilities.COMMAND_INJECTION, new CommandSensitiveAnalyzer())
|
|
23
24
|
this._sensitiveAnalyzers.set(vulnerabilities.LDAP_INJECTION, new LdapSensitiveAnalyzer())
|
|
24
25
|
this._sensitiveAnalyzers.set(vulnerabilities.SQL_INJECTION, new SqlSensitiveAnalyzer())
|
|
26
|
+
this._sensitiveAnalyzers.set(vulnerabilities.SSRF, new UrlSensitiveAnalyzer())
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
isSensibleName (name) {
|
|
@@ -102,7 +104,7 @@ class SensitiveHandler {
|
|
|
102
104
|
if (entry.start === i) {
|
|
103
105
|
nextSensitive = entry
|
|
104
106
|
} else {
|
|
105
|
-
sensitive.
|
|
107
|
+
sensitive.unshift(entry)
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
110
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
module.exports = {
|
|
2
|
-
WEAK_HASH: 'WEAK_HASH',
|
|
3
|
-
WEAK_CIPHER: 'WEAK_CIPHER',
|
|
4
|
-
SQL_INJECTION: 'SQL_INJECTION',
|
|
5
|
-
PATH_TRAVERSAL: 'PATH_TRAVERSAL',
|
|
6
2
|
COMMAND_INJECTION: 'COMMAND_INJECTION',
|
|
7
|
-
|
|
3
|
+
INSECURE_COOKIE: 'INSECURE_COOKIE',
|
|
4
|
+
LDAP_INJECTION: 'LDAP_INJECTION',
|
|
5
|
+
PATH_TRAVERSAL: 'PATH_TRAVERSAL',
|
|
6
|
+
SQL_INJECTION: 'SQL_INJECTION',
|
|
7
|
+
SSRF: 'SSRF',
|
|
8
|
+
WEAK_CIPHER: 'WEAK_CIPHER',
|
|
9
|
+
WEAK_HASH: 'WEAK_HASH'
|
|
8
10
|
}
|