dd-trace 4.16.0 → 4.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-3rdparty.csv +1 -0
- package/package.json +4 -3
- package/packages/datadog-instrumentations/src/body-parser.js +2 -1
- package/packages/datadog-instrumentations/src/cucumber.js +24 -4
- package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +45 -0
- package/packages/datadog-instrumentations/src/express.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
- package/packages/datadog-instrumentations/src/jest.js +20 -11
- package/packages/datadog-instrumentations/src/knex.js +62 -1
- package/packages/datadog-instrumentations/src/mocha.js +19 -4
- package/packages/datadog-instrumentations/src/mongodb.js +63 -0
- package/packages/datadog-instrumentations/src/mongoose.js +140 -1
- package/packages/datadog-instrumentations/src/next.js +40 -0
- package/packages/datadog-instrumentations/src/playwright.js +11 -2
- package/packages/datadog-plugin-cucumber/src/index.js +17 -5
- package/packages/datadog-plugin-cypress/src/plugin.js +38 -8
- package/packages/datadog-plugin-jest/src/index.js +19 -4
- package/packages/datadog-plugin-jest/src/util.js +45 -2
- package/packages/datadog-plugin-memcached/src/index.js +10 -5
- package/packages/datadog-plugin-mocha/src/index.js +19 -6
- package/packages/dd-trace/src/appsec/channels.js +3 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +166 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +21 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +3 -3
- package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +1 -2
- package/packages/dd-trace/src/appsec/iast/iast-plugin.js +4 -0
- package/packages/dd-trace/src/appsec/iast/taint-tracking/operations.js +25 -12
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +4 -4
- package/packages/dd-trace/src/appsec/iast/taint-tracking/secure-marks-generator.js +13 -0
- package/packages/dd-trace/src/appsec/iast/taint-tracking/source-types.js +2 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/json-sensitive-analyzer.js +16 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +9 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +13 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +169 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
- package/packages/dd-trace/src/appsec/index.js +31 -13
- package/packages/dd-trace/src/appsec/remote_config/manager.js +11 -3
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +14 -1
- package/packages/dd-trace/src/config.js +8 -0
- package/packages/dd-trace/src/format.js +3 -0
- package/packages/dd-trace/src/plugin_manager.js +3 -1
- package/packages/dd-trace/src/plugins/util/ci.js +17 -0
- package/packages/dd-trace/src/plugins/util/git.js +26 -4
- package/packages/dd-trace/src/plugins/util/test.js +16 -1
- package/packages/dd-trace/src/profiling/config.js +36 -5
- package/packages/dd-trace/src/profiling/profilers/wall.js +7 -1
- package/packages/dd-trace/src/service-naming/extra-services.js +24 -0
- package/packages/dd-trace/src/telemetry/metrics.js +0 -5
|
@@ -18,15 +18,14 @@ let onRemoveTransaction = (transactionId, iastContext) => {}
|
|
|
18
18
|
|
|
19
19
|
function onRemoveTransactionInformationTelemetry (transactionId, iastContext) {
|
|
20
20
|
const metrics = TaintedUtils.getMetrics(transactionId, iastTelemetry.verbosity)
|
|
21
|
-
if (metrics
|
|
21
|
+
if (metrics?.requestCount) {
|
|
22
22
|
REQUEST_TAINTED.add(metrics.requestCount, null, iastContext)
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function removeTransaction (iastContext) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const transactionId = iastContext?.[IAST_TRANSACTION_ID]
|
|
28
|
+
if (transactionId) {
|
|
30
29
|
onRemoveTransaction(transactionId, iastContext)
|
|
31
30
|
|
|
32
31
|
TaintedUtils.removeTransaction(transactionId)
|
|
@@ -36,8 +35,8 @@ function removeTransaction (iastContext) {
|
|
|
36
35
|
|
|
37
36
|
function newTaintedString (iastContext, string, name, type) {
|
|
38
37
|
let result = string
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const transactionId = iastContext?.[IAST_TRANSACTION_ID]
|
|
39
|
+
if (transactionId) {
|
|
41
40
|
result = TaintedUtils.newTaintedString(transactionId, string, name, type)
|
|
42
41
|
} else {
|
|
43
42
|
result = string
|
|
@@ -47,15 +46,17 @@ function newTaintedString (iastContext, string, name, type) {
|
|
|
47
46
|
|
|
48
47
|
function taintObject (iastContext, object, type, keyTainting, keyType) {
|
|
49
48
|
let result = object
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
const transactionId = iastContext?.[IAST_TRANSACTION_ID]
|
|
50
|
+
if (transactionId) {
|
|
52
51
|
const queue = [{ parent: null, property: null, value: object }]
|
|
53
52
|
const visited = new WeakSet()
|
|
53
|
+
|
|
54
54
|
while (queue.length > 0) {
|
|
55
55
|
const { parent, property, value, key } = queue.pop()
|
|
56
56
|
if (value === null) {
|
|
57
57
|
continue
|
|
58
58
|
}
|
|
59
|
+
|
|
59
60
|
try {
|
|
60
61
|
if (typeof value === 'string') {
|
|
61
62
|
const tainted = TaintedUtils.newTaintedString(transactionId, value, property, type)
|
|
@@ -71,11 +72,13 @@ function taintObject (iastContext, object, type, keyTainting, keyType) {
|
|
|
71
72
|
}
|
|
72
73
|
} else if (typeof value === 'object' && !visited.has(value)) {
|
|
73
74
|
visited.add(value)
|
|
75
|
+
|
|
74
76
|
const keys = Object.keys(value)
|
|
75
77
|
for (let i = 0; i < keys.length; i++) {
|
|
76
78
|
const key = keys[i]
|
|
77
79
|
queue.push({ parent: value, property: property ? `${property}.${key}` : key, value: value[key], key })
|
|
78
80
|
}
|
|
81
|
+
|
|
79
82
|
if (parent && keyTainting && key) {
|
|
80
83
|
const taintedProperty = TaintedUtils.newTaintedString(transactionId, key, property, keyType)
|
|
81
84
|
parent[taintedProperty] = value
|
|
@@ -91,8 +94,8 @@ function taintObject (iastContext, object, type, keyTainting, keyType) {
|
|
|
91
94
|
|
|
92
95
|
function isTainted (iastContext, string) {
|
|
93
96
|
let result = false
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
const transactionId = iastContext?.[IAST_TRANSACTION_ID]
|
|
98
|
+
if (transactionId) {
|
|
96
99
|
result = TaintedUtils.isTainted(transactionId, string)
|
|
97
100
|
} else {
|
|
98
101
|
result = false
|
|
@@ -102,8 +105,8 @@ function isTainted (iastContext, string) {
|
|
|
102
105
|
|
|
103
106
|
function getRanges (iastContext, string) {
|
|
104
107
|
let result = []
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
const transactionId = iastContext?.[IAST_TRANSACTION_ID]
|
|
109
|
+
if (transactionId) {
|
|
107
110
|
result = TaintedUtils.getRanges(transactionId, string)
|
|
108
111
|
} else {
|
|
109
112
|
result = []
|
|
@@ -111,6 +114,15 @@ function getRanges (iastContext, string) {
|
|
|
111
114
|
return result
|
|
112
115
|
}
|
|
113
116
|
|
|
117
|
+
function addSecureMark (iastContext, string, mark) {
|
|
118
|
+
const transactionId = iastContext?.[IAST_TRANSACTION_ID]
|
|
119
|
+
if (transactionId) {
|
|
120
|
+
return TaintedUtils.addSecureMarksToTaintedString(transactionId, string, mark)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return string
|
|
124
|
+
}
|
|
125
|
+
|
|
114
126
|
function enableTaintOperations (telemetryVerbosity) {
|
|
115
127
|
if (isInfoAllowed(telemetryVerbosity)) {
|
|
116
128
|
onRemoveTransaction = onRemoveTransactionInformationTelemetry
|
|
@@ -132,6 +144,7 @@ function setMaxTransactions (transactions) {
|
|
|
132
144
|
}
|
|
133
145
|
|
|
134
146
|
module.exports = {
|
|
147
|
+
addSecureMark,
|
|
135
148
|
createTransaction,
|
|
136
149
|
removeTransaction,
|
|
137
150
|
newTaintedString,
|
|
@@ -11,8 +11,8 @@ const {
|
|
|
11
11
|
HTTP_REQUEST_HEADER_VALUE,
|
|
12
12
|
HTTP_REQUEST_HEADER_NAME,
|
|
13
13
|
HTTP_REQUEST_PARAMETER,
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
HTTP_REQUEST_PATH_PARAM,
|
|
15
|
+
HTTP_REQUEST_URI
|
|
16
16
|
} = require('./source-types')
|
|
17
17
|
|
|
18
18
|
class TaintTrackingPlugin extends SourceIastPlugin {
|
|
@@ -93,9 +93,9 @@ class TaintTrackingPlugin extends SourceIastPlugin {
|
|
|
93
93
|
taintUrl (req, iastContext) {
|
|
94
94
|
this.execSource({
|
|
95
95
|
handler: function () {
|
|
96
|
-
req.url = newTaintedString(iastContext, req.url,
|
|
96
|
+
req.url = newTaintedString(iastContext, req.url, HTTP_REQUEST_URI, HTTP_REQUEST_URI)
|
|
97
97
|
},
|
|
98
|
-
tag: [
|
|
98
|
+
tag: [HTTP_REQUEST_URI],
|
|
99
99
|
iastContext
|
|
100
100
|
})
|
|
101
101
|
}
|
|
@@ -8,5 +8,6 @@ module.exports = {
|
|
|
8
8
|
HTTP_REQUEST_HEADER_VALUE: 'http.request.header',
|
|
9
9
|
HTTP_REQUEST_PARAMETER: 'http.request.parameter',
|
|
10
10
|
HTTP_REQUEST_PATH: 'http.request.path',
|
|
11
|
-
HTTP_REQUEST_PATH_PARAM: 'http.request.path.parameter'
|
|
11
|
+
HTTP_REQUEST_PATH_PARAM: 'http.request.path.parameter',
|
|
12
|
+
HTTP_REQUEST_URI: 'http.request.uri'
|
|
12
13
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { stringifyWithRanges } = require('../../utils')
|
|
4
|
+
|
|
5
|
+
class JsonSensitiveAnalyzer {
|
|
6
|
+
extractSensitiveRanges (evidence) {
|
|
7
|
+
// expect object evidence
|
|
8
|
+
const { value, ranges, sensitiveRanges } = stringifyWithRanges(evidence.value, evidence.rangesToApply, true)
|
|
9
|
+
evidence.value = value
|
|
10
|
+
evidence.ranges = ranges
|
|
11
|
+
|
|
12
|
+
return sensitiveRanges
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = JsonSensitiveAnalyzer
|
|
@@ -5,14 +5,12 @@ const vulnerabilities = require('../../vulnerabilities')
|
|
|
5
5
|
const { contains, intersects, remove } = require('./range-utils')
|
|
6
6
|
|
|
7
7
|
const CommandSensitiveAnalyzer = require('./sensitive-analyzers/command-sensitive-analyzer')
|
|
8
|
+
const JsonSensitiveAnalyzer = require('./sensitive-analyzers/json-sensitive-analyzer')
|
|
8
9
|
const LdapSensitiveAnalyzer = require('./sensitive-analyzers/ldap-sensitive-analyzer')
|
|
9
10
|
const SqlSensitiveAnalyzer = require('./sensitive-analyzers/sql-sensitive-analyzer')
|
|
10
11
|
const UrlSensitiveAnalyzer = require('./sensitive-analyzers/url-sensitive-analyzer')
|
|
11
12
|
|
|
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)?)'
|
|
14
|
-
// eslint-disable-next-line max-len
|
|
15
|
-
const DEFAULT_IAST_REDACTION_VALUE_PATTERN = '(?:bearer\\s+[a-z0-9\\._\\-]+|glpat-[\\w\\-]{20}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\\w=\\-]+\\.ey[I-L][\\w=\\-]+(?:\\.[\\w.+/=\\-]+)?|(?:[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY[\\-]{5}|ssh-rsa\\s*[a-z0-9/\\.+]{100,}))'
|
|
13
|
+
const { DEFAULT_IAST_REDACTION_NAME_PATTERN, DEFAULT_IAST_REDACTION_VALUE_PATTERN } = require('./sensitive-regex')
|
|
16
14
|
|
|
17
15
|
const REDACTED_SOURCE_BUFFER = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
18
16
|
|
|
@@ -23,6 +21,7 @@ class SensitiveHandler {
|
|
|
23
21
|
|
|
24
22
|
this._sensitiveAnalyzers = new Map()
|
|
25
23
|
this._sensitiveAnalyzers.set(vulnerabilities.COMMAND_INJECTION, new CommandSensitiveAnalyzer())
|
|
24
|
+
this._sensitiveAnalyzers.set(vulnerabilities.NOSQL_MONGODB_INJECTION, new JsonSensitiveAnalyzer())
|
|
26
25
|
this._sensitiveAnalyzers.set(vulnerabilities.LDAP_INJECTION, new LdapSensitiveAnalyzer())
|
|
27
26
|
this._sensitiveAnalyzers.set(vulnerabilities.SQL_INJECTION, new SqlSensitiveAnalyzer())
|
|
28
27
|
const urlSensitiveAnalyzer = new UrlSensitiveAnalyzer()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// eslint-disable-next-line max-len
|
|
2
|
+
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)?)'
|
|
3
|
+
// eslint-disable-next-line max-len
|
|
4
|
+
const DEFAULT_IAST_REDACTION_VALUE_PATTERN = '(?:bearer\\s+[a-z0-9\\._\\-]+|glpat-[\\w\\-]{20}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\\w=\\-]+\\.ey[I-L][\\w=\\-]+(?:\\.[\\w.+/=\\-]+)?|(?:[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY[\\-]{5}|ssh-rsa\\s*[a-z0-9/\\.+]{100,}))'
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
DEFAULT_IAST_REDACTION_NAME_PATTERN,
|
|
8
|
+
DEFAULT_IAST_REDACTION_VALUE_PATTERN
|
|
9
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const sensitiveHandler = require('./evidence-redaction/sensitive-handler')
|
|
4
|
+
const { stringifyWithRanges } = require('./utils')
|
|
2
5
|
|
|
3
6
|
class VulnerabilityFormatter {
|
|
4
7
|
constructor () {
|
|
@@ -38,6 +41,13 @@ class VulnerabilityFormatter {
|
|
|
38
41
|
getUnredactedValueParts (evidence, sourcesIndexes) {
|
|
39
42
|
const valueParts = []
|
|
40
43
|
let fromIndex = 0
|
|
44
|
+
|
|
45
|
+
if (typeof evidence.value === 'object' && evidence.rangesToApply) {
|
|
46
|
+
const { value, ranges } = stringifyWithRanges(evidence.value, evidence.rangesToApply)
|
|
47
|
+
evidence.value = value
|
|
48
|
+
evidence.ranges = ranges
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
evidence.ranges.forEach((range, rangeIndex) => {
|
|
42
52
|
if (fromIndex < range.start) {
|
|
43
53
|
valueParts.push({ value: evidence.value.substring(fromIndex, range.start) })
|
|
@@ -45,14 +55,16 @@ class VulnerabilityFormatter {
|
|
|
45
55
|
valueParts.push({ value: evidence.value.substring(range.start, range.end), source: sourcesIndexes[rangeIndex] })
|
|
46
56
|
fromIndex = range.end
|
|
47
57
|
})
|
|
58
|
+
|
|
48
59
|
if (fromIndex < evidence.value.length) {
|
|
49
60
|
valueParts.push({ value: evidence.value.substring(fromIndex) })
|
|
50
61
|
}
|
|
62
|
+
|
|
51
63
|
return { valueParts }
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
formatEvidence (type, evidence, sourcesIndexes, sources) {
|
|
55
|
-
if (!evidence.ranges) {
|
|
67
|
+
if (!evidence.ranges && !evidence.rangesToApply) {
|
|
56
68
|
if (typeof evidence.value === 'undefined') {
|
|
57
69
|
return undefined
|
|
58
70
|
} else {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto')
|
|
4
|
+
const { DEFAULT_IAST_REDACTION_VALUE_PATTERN } = require('./evidence-redaction/sensitive-regex')
|
|
5
|
+
|
|
6
|
+
const STRINGIFY_RANGE_KEY = 'DD_' + crypto.randomBytes(20).toString('hex')
|
|
7
|
+
const STRINGIFY_SENSITIVE_KEY = STRINGIFY_RANGE_KEY + 'SENSITIVE'
|
|
8
|
+
const STRINGIFY_SENSITIVE_NOT_STRING_KEY = STRINGIFY_SENSITIVE_KEY + 'NOTSTRING'
|
|
9
|
+
|
|
10
|
+
// eslint-disable-next-line max-len
|
|
11
|
+
const KEYS_REGEX_WITH_SENSITIVE_RANGES = new RegExp(`(?:"(${STRINGIFY_RANGE_KEY}_\\d+_))|(?:"(${STRINGIFY_SENSITIVE_KEY}_\\d+_(\\d+)_))|("${STRINGIFY_SENSITIVE_NOT_STRING_KEY}_\\d+_([\\s0-9.a-zA-Z]*)")`, 'gm')
|
|
12
|
+
const KEYS_REGEX_WITHOUT_SENSITIVE_RANGES = new RegExp(`"(${STRINGIFY_RANGE_KEY}_\\d+_)`, 'gm')
|
|
13
|
+
|
|
14
|
+
const sensitiveValueRegex = new RegExp(DEFAULT_IAST_REDACTION_VALUE_PATTERN, 'gmi')
|
|
15
|
+
|
|
16
|
+
function iterateObject (target, fn, levelKeys = [], depth = 50) {
|
|
17
|
+
Object.keys(target).forEach((key) => {
|
|
18
|
+
const nextLevelKeys = [...levelKeys, key]
|
|
19
|
+
const val = target[key]
|
|
20
|
+
|
|
21
|
+
fn(val, nextLevelKeys, target, key)
|
|
22
|
+
|
|
23
|
+
if (val !== null && typeof val === 'object') {
|
|
24
|
+
iterateObject(val, fn, nextLevelKeys, depth - 1)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function stringifyWithRanges (obj, objRanges, loadSensitiveRanges = false) {
|
|
30
|
+
let value
|
|
31
|
+
const ranges = []
|
|
32
|
+
const sensitiveRanges = []
|
|
33
|
+
objRanges = objRanges || {}
|
|
34
|
+
|
|
35
|
+
if (objRanges || loadSensitiveRanges) {
|
|
36
|
+
const cloneObj = Array.isArray(obj) ? [] : {}
|
|
37
|
+
let counter = 0
|
|
38
|
+
const allRanges = {}
|
|
39
|
+
const sensitiveKeysMapping = {}
|
|
40
|
+
|
|
41
|
+
iterateObject(obj, (val, levelKeys, parent, key) => {
|
|
42
|
+
let currentLevelClone = cloneObj
|
|
43
|
+
for (let i = 0; i < levelKeys.length - 1; i++) {
|
|
44
|
+
let levelKey = levelKeys[i]
|
|
45
|
+
|
|
46
|
+
if (!currentLevelClone[levelKey]) {
|
|
47
|
+
const sensitiveKey = sensitiveKeysMapping[levelKey]
|
|
48
|
+
if (currentLevelClone[sensitiveKey]) {
|
|
49
|
+
levelKey = sensitiveKey
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
currentLevelClone = currentLevelClone[levelKey]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (loadSensitiveRanges) {
|
|
57
|
+
const sensitiveKey = sensitiveKeysMapping[key]
|
|
58
|
+
if (sensitiveKey) {
|
|
59
|
+
key = sensitiveKey
|
|
60
|
+
} else {
|
|
61
|
+
sensitiveValueRegex.lastIndex = 0
|
|
62
|
+
|
|
63
|
+
if (sensitiveValueRegex.test(key)) {
|
|
64
|
+
const current = counter++
|
|
65
|
+
const id = `${STRINGIFY_SENSITIVE_KEY}_${current}_${key.length}_`
|
|
66
|
+
key = `${id}${key}`
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (typeof val === 'string') {
|
|
72
|
+
const ranges = objRanges[levelKeys.join('.')]
|
|
73
|
+
if (ranges) {
|
|
74
|
+
const current = counter++
|
|
75
|
+
const id = `${STRINGIFY_RANGE_KEY}_${current}_`
|
|
76
|
+
|
|
77
|
+
allRanges[id] = ranges
|
|
78
|
+
currentLevelClone[key] = `${id}${val}`
|
|
79
|
+
} else {
|
|
80
|
+
currentLevelClone[key] = val
|
|
81
|
+
}
|
|
82
|
+
if (loadSensitiveRanges) {
|
|
83
|
+
const current = counter++
|
|
84
|
+
const id = `${STRINGIFY_SENSITIVE_KEY}_${current}_${val.length}_`
|
|
85
|
+
|
|
86
|
+
currentLevelClone[key] = `${id}${currentLevelClone[key]}`
|
|
87
|
+
}
|
|
88
|
+
} else if (typeof val !== 'object' || val === null) {
|
|
89
|
+
if (loadSensitiveRanges) {
|
|
90
|
+
const current = counter++
|
|
91
|
+
const id = `${STRINGIFY_SENSITIVE_NOT_STRING_KEY}_${current}_`
|
|
92
|
+
|
|
93
|
+
// this is special, in the final string we should modify "key_value_[null|false|true]..."
|
|
94
|
+
// by null|false|..... ignoring the beginning and ending quotes
|
|
95
|
+
currentLevelClone[key] = id + val
|
|
96
|
+
} else {
|
|
97
|
+
currentLevelClone[key] = val
|
|
98
|
+
}
|
|
99
|
+
} else if (Array.isArray(val)) {
|
|
100
|
+
currentLevelClone[key] = []
|
|
101
|
+
} else {
|
|
102
|
+
currentLevelClone[key] = {}
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
value = JSON.stringify(cloneObj, null, 2)
|
|
107
|
+
|
|
108
|
+
if (counter > 0) {
|
|
109
|
+
let keysRegex
|
|
110
|
+
if (loadSensitiveRanges) {
|
|
111
|
+
keysRegex = KEYS_REGEX_WITH_SENSITIVE_RANGES
|
|
112
|
+
} else {
|
|
113
|
+
keysRegex = KEYS_REGEX_WITHOUT_SENSITIVE_RANGES
|
|
114
|
+
}
|
|
115
|
+
keysRegex.lastIndex = 0
|
|
116
|
+
|
|
117
|
+
let regexRes = keysRegex.exec(value)
|
|
118
|
+
while (regexRes) {
|
|
119
|
+
const offset = regexRes.index + 1 // +1 to increase the " char
|
|
120
|
+
|
|
121
|
+
if (regexRes[1]) {
|
|
122
|
+
// is a range
|
|
123
|
+
const rangesId = regexRes[1]
|
|
124
|
+
value = value.replace(rangesId, '')
|
|
125
|
+
|
|
126
|
+
const updatedRanges = allRanges[rangesId].map(range => {
|
|
127
|
+
return {
|
|
128
|
+
...range,
|
|
129
|
+
start: range.start + offset,
|
|
130
|
+
end: range.end + offset
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
ranges.push(...updatedRanges)
|
|
135
|
+
} else if (regexRes[2]) {
|
|
136
|
+
// is a sensitive string literal
|
|
137
|
+
const sensitiveId = regexRes[2]
|
|
138
|
+
|
|
139
|
+
sensitiveRanges.push({
|
|
140
|
+
start: offset,
|
|
141
|
+
end: offset + parseInt(regexRes[3])
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
value = value.replace(sensitiveId, '')
|
|
145
|
+
} else if (regexRes[4]) {
|
|
146
|
+
// is a sensitive value (number, null, false, ...)
|
|
147
|
+
const sensitiveId = regexRes[4]
|
|
148
|
+
const originalValue = regexRes[5]
|
|
149
|
+
|
|
150
|
+
sensitiveRanges.push({
|
|
151
|
+
start: regexRes.index,
|
|
152
|
+
end: regexRes.index + originalValue.length
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
value = value.replace(sensitiveId, originalValue)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
keysRegex.lastIndex = 0
|
|
159
|
+
regexRes = keysRegex.exec(value)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
value = JSON.stringify(obj, null, 2)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { value, ranges, sensitiveRanges }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = { stringifyWithRanges }
|
|
@@ -5,6 +5,7 @@ module.exports = {
|
|
|
5
5
|
LDAP_INJECTION: 'LDAP_INJECTION',
|
|
6
6
|
NO_HTTPONLY_COOKIE: 'NO_HTTPONLY_COOKIE',
|
|
7
7
|
NO_SAMESITE_COOKIE: 'NO_SAMESITE_COOKIE',
|
|
8
|
+
NOSQL_MONGODB_INJECTION: 'NOSQL_MONGODB_INJECTION',
|
|
8
9
|
PATH_TRAVERSAL: 'PATH_TRAVERSAL',
|
|
9
10
|
SQL_INJECTION: 'SQL_INJECTION',
|
|
10
11
|
SSRF: 'SSRF',
|
|
@@ -10,7 +10,9 @@ const {
|
|
|
10
10
|
incomingHttpRequestStart,
|
|
11
11
|
incomingHttpRequestEnd,
|
|
12
12
|
passportVerify,
|
|
13
|
-
queryParser
|
|
13
|
+
queryParser,
|
|
14
|
+
nextBodyParsed,
|
|
15
|
+
nextQueryParsed
|
|
14
16
|
} = require('./channels')
|
|
15
17
|
const waf = require('./waf')
|
|
16
18
|
const addresses = require('./addresses')
|
|
@@ -30,6 +32,8 @@ function enable (_config) {
|
|
|
30
32
|
if (isEnabled) return
|
|
31
33
|
|
|
32
34
|
try {
|
|
35
|
+
appsecTelemetry.enable(_config.telemetry)
|
|
36
|
+
|
|
33
37
|
setTemplates(_config)
|
|
34
38
|
|
|
35
39
|
RuleManager.applyRules(_config.appsec.rules, _config.appsec)
|
|
@@ -38,11 +42,11 @@ function enable (_config) {
|
|
|
38
42
|
|
|
39
43
|
Reporter.setRateLimit(_config.appsec.rateLimit)
|
|
40
44
|
|
|
41
|
-
appsecTelemetry.enable(_config.telemetry)
|
|
42
|
-
|
|
43
45
|
incomingHttpRequestStart.subscribe(incomingHttpStartTranslator)
|
|
44
46
|
incomingHttpRequestEnd.subscribe(incomingHttpEndTranslator)
|
|
45
47
|
bodyParser.subscribe(onRequestBodyParsed)
|
|
48
|
+
nextBodyParsed.subscribe(onRequestBodyParsed)
|
|
49
|
+
nextQueryParsed.subscribe(onRequestQueryParsed)
|
|
46
50
|
queryParser.subscribe(onRequestQueryParsed)
|
|
47
51
|
cookieParser.subscribe(onRequestCookieParser)
|
|
48
52
|
graphqlFinishExecute.subscribe(onGraphqlFinishExecute)
|
|
@@ -117,6 +121,10 @@ function incomingHttpEndTranslator ({ req, res }) {
|
|
|
117
121
|
payload[addresses.HTTP_INCOMING_COOKIES] = req.cookies
|
|
118
122
|
}
|
|
119
123
|
|
|
124
|
+
if (req.query && typeof req.query === 'object') {
|
|
125
|
+
payload[addresses.HTTP_INCOMING_QUERY] = req.query
|
|
126
|
+
}
|
|
127
|
+
|
|
120
128
|
waf.run(payload, req)
|
|
121
129
|
|
|
122
130
|
waf.disposeContext(req)
|
|
@@ -124,38 +132,48 @@ function incomingHttpEndTranslator ({ req, res }) {
|
|
|
124
132
|
Reporter.finishRequest(req, res)
|
|
125
133
|
}
|
|
126
134
|
|
|
127
|
-
function onRequestBodyParsed ({ req, res, abortController }) {
|
|
135
|
+
function onRequestBodyParsed ({ req, res, body, abortController }) {
|
|
136
|
+
if (body === undefined || body === null) return
|
|
137
|
+
|
|
138
|
+
if (!req) {
|
|
139
|
+
const store = storage.getStore()
|
|
140
|
+
req = store?.req
|
|
141
|
+
}
|
|
142
|
+
|
|
128
143
|
const rootSpan = web.root(req)
|
|
129
144
|
if (!rootSpan) return
|
|
130
145
|
|
|
131
|
-
if (req.body === undefined || req.body === null) return
|
|
132
|
-
|
|
133
146
|
const results = waf.run({
|
|
134
|
-
[addresses.HTTP_INCOMING_BODY]:
|
|
147
|
+
[addresses.HTTP_INCOMING_BODY]: body
|
|
135
148
|
}, req)
|
|
136
149
|
|
|
137
150
|
handleResults(results, req, res, rootSpan, abortController)
|
|
138
151
|
}
|
|
139
152
|
|
|
140
|
-
function onRequestQueryParsed ({ req, res, abortController }) {
|
|
153
|
+
function onRequestQueryParsed ({ req, res, query, abortController }) {
|
|
154
|
+
if (!query || typeof query !== 'object') return
|
|
155
|
+
|
|
156
|
+
if (!req) {
|
|
157
|
+
const store = storage.getStore()
|
|
158
|
+
req = store?.req
|
|
159
|
+
}
|
|
160
|
+
|
|
141
161
|
const rootSpan = web.root(req)
|
|
142
162
|
if (!rootSpan) return
|
|
143
163
|
|
|
144
|
-
if (!req.query || typeof req.query !== 'object') return
|
|
145
|
-
|
|
146
164
|
const results = waf.run({
|
|
147
|
-
[addresses.HTTP_INCOMING_QUERY]:
|
|
165
|
+
[addresses.HTTP_INCOMING_QUERY]: query
|
|
148
166
|
}, req)
|
|
149
167
|
|
|
150
168
|
handleResults(results, req, res, rootSpan, abortController)
|
|
151
169
|
}
|
|
152
170
|
|
|
153
171
|
function onRequestCookieParser ({ req, res, abortController, cookies }) {
|
|
172
|
+
if (!cookies || typeof cookies !== 'object') return
|
|
173
|
+
|
|
154
174
|
const rootSpan = web.root(req)
|
|
155
175
|
if (!rootSpan) return
|
|
156
176
|
|
|
157
|
-
if (!cookies || typeof cookies !== 'object') return
|
|
158
|
-
|
|
159
177
|
const results = waf.run({
|
|
160
178
|
[addresses.HTTP_INCOMING_COOKIES]: cookies
|
|
161
179
|
}, req)
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
const { URL, format } = require('url')
|
|
4
4
|
const uuid = require('crypto-randomuuid')
|
|
5
5
|
const { EventEmitter } = require('events')
|
|
6
|
-
const Scheduler = require('./scheduler')
|
|
7
6
|
const tracerVersion = require('../../../../../package.json').version
|
|
8
7
|
const request = require('../../exporters/common/request')
|
|
9
8
|
const log = require('../../log')
|
|
9
|
+
const { getExtraServices } = require('../../service-naming/extra-services')
|
|
10
10
|
const { UNACKNOWLEDGED, ACKNOWLEDGED, ERROR } = require('./apply_states')
|
|
11
|
+
const Scheduler = require('./scheduler')
|
|
11
12
|
|
|
12
13
|
const clientId = uuid()
|
|
13
14
|
|
|
@@ -57,7 +58,8 @@ class RemoteConfigManager extends EventEmitter {
|
|
|
57
58
|
tracer_version: tracerVersion,
|
|
58
59
|
service: config.service,
|
|
59
60
|
env: config.env,
|
|
60
|
-
app_version: config.version
|
|
61
|
+
app_version: config.version,
|
|
62
|
+
extra_services: []
|
|
61
63
|
},
|
|
62
64
|
capabilities: DEFAULT_CAPABILITY // updated by `updateCapabilities()`
|
|
63
65
|
},
|
|
@@ -113,8 +115,14 @@ class RemoteConfigManager extends EventEmitter {
|
|
|
113
115
|
this.state.client.products = this.eventNames().filter(e => typeof e === 'string')
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
getPayload () {
|
|
119
|
+
this.state.client.client_tracer.extra_services = getExtraServices()
|
|
120
|
+
|
|
121
|
+
return JSON.stringify(this.state)
|
|
122
|
+
}
|
|
123
|
+
|
|
116
124
|
poll (cb) {
|
|
117
|
-
request(
|
|
125
|
+
request(this.getPayload(), this.requestOptions, (err, data, statusCode) => {
|
|
118
126
|
// 404 means RC is disabled, ignore it
|
|
119
127
|
if (statusCode === 404) return cb()
|
|
120
128
|
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const log = require('../../log')
|
|
4
4
|
const Reporter = require('../reporter')
|
|
5
|
+
const addresses = require('../addresses')
|
|
6
|
+
|
|
7
|
+
// TODO: remove once ephemeral addresses are implemented
|
|
8
|
+
const preventDuplicateAddresses = new Set([
|
|
9
|
+
addresses.HTTP_INCOMING_QUERY
|
|
10
|
+
])
|
|
5
11
|
|
|
6
12
|
class WAFContextWrapper {
|
|
7
13
|
constructor (ddwafContext, requiredAddresses, wafTimeout, wafVersion, rulesVersion) {
|
|
@@ -10,16 +16,21 @@ class WAFContextWrapper {
|
|
|
10
16
|
this.wafTimeout = wafTimeout
|
|
11
17
|
this.wafVersion = wafVersion
|
|
12
18
|
this.rulesVersion = rulesVersion
|
|
19
|
+
this.addressesToSkip = new Set()
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
run (params) {
|
|
16
23
|
const inputs = {}
|
|
17
24
|
let someInputAdded = false
|
|
25
|
+
const newAddressesToSkip = new Set(this.addressesToSkip)
|
|
18
26
|
|
|
19
27
|
// TODO: possible optimizaion: only send params that haven't already been sent with same value to this wafContext
|
|
20
28
|
for (const key of Object.keys(params)) {
|
|
21
|
-
if (this.requiredAddresses.has(key)) {
|
|
29
|
+
if (this.requiredAddresses.has(key) && !this.addressesToSkip.has(key)) {
|
|
22
30
|
inputs[key] = params[key]
|
|
31
|
+
if (preventDuplicateAddresses.has(key)) {
|
|
32
|
+
newAddressesToSkip.add(key)
|
|
33
|
+
}
|
|
23
34
|
someInputAdded = true
|
|
24
35
|
}
|
|
25
36
|
}
|
|
@@ -33,6 +44,8 @@ class WAFContextWrapper {
|
|
|
33
44
|
|
|
34
45
|
const end = process.hrtime.bigint()
|
|
35
46
|
|
|
47
|
+
this.addressesToSkip = newAddressesToSkip
|
|
48
|
+
|
|
36
49
|
const ruleTriggered = !!result.events?.length
|
|
37
50
|
const blockTriggered = result.actions?.includes('block')
|
|
38
51
|
|
|
@@ -179,6 +179,11 @@ class Config {
|
|
|
179
179
|
false
|
|
180
180
|
)
|
|
181
181
|
|
|
182
|
+
const DD_TRACE_MEMCACHED_COMMAND_ENABLED = coalesce(
|
|
183
|
+
process.env.DD_TRACE_MEMCACHED_COMMAND_ENABLED,
|
|
184
|
+
false
|
|
185
|
+
)
|
|
186
|
+
|
|
182
187
|
const DD_SERVICE = options.service ||
|
|
183
188
|
process.env.DD_SERVICE ||
|
|
184
189
|
process.env.DD_SERVICE_NAME ||
|
|
@@ -629,6 +634,9 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
|
|
|
629
634
|
|
|
630
635
|
this.openaiSpanCharLimit = DD_OPENAI_SPAN_CHAR_LIMIT
|
|
631
636
|
|
|
637
|
+
// Requires an accompanying DD_APM_OBFUSCATION_MEMCACHED_KEEP_COMMAND=true in the agent
|
|
638
|
+
this.memcachedCommandEnabled = isTrue(DD_TRACE_MEMCACHED_COMMAND_ENABLED)
|
|
639
|
+
|
|
632
640
|
if (this.gitMetadataEnabled) {
|
|
633
641
|
this.repositoryUrl = coalesce(
|
|
634
642
|
process.env.DD_GIT_REPOSITORY_URL,
|
|
@@ -4,6 +4,7 @@ const constants = require('./constants')
|
|
|
4
4
|
const tags = require('../../../ext/tags')
|
|
5
5
|
const id = require('./id')
|
|
6
6
|
const { isError } = require('./util')
|
|
7
|
+
const { registerExtraService } = require('./service-naming/extra-services')
|
|
7
8
|
|
|
8
9
|
const SAMPLING_PRIORITY_KEY = constants.SAMPLING_PRIORITY_KEY
|
|
9
10
|
const SAMPLING_RULE_DECISION = constants.SAMPLING_RULE_DECISION
|
|
@@ -76,6 +77,8 @@ function extractTags (trace, span) {
|
|
|
76
77
|
const tracerService = span.tracer()._service.toLowerCase()
|
|
77
78
|
if (tags['service.name']?.toLowerCase() !== tracerService) {
|
|
78
79
|
span.setTag(BASE_SERVICE, tracerService)
|
|
80
|
+
|
|
81
|
+
registerExtraService(tags['service.name'])
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
for (const tag in tags) {
|
|
@@ -136,7 +136,8 @@ module.exports = class PluginManager {
|
|
|
136
136
|
headerTags,
|
|
137
137
|
dbmPropagationMode,
|
|
138
138
|
dsmEnabled,
|
|
139
|
-
clientIpEnabled
|
|
139
|
+
clientIpEnabled,
|
|
140
|
+
memcachedCommandEnabled
|
|
140
141
|
} = this._tracerConfig
|
|
141
142
|
|
|
142
143
|
const sharedConfig = {}
|
|
@@ -151,6 +152,7 @@ module.exports = class PluginManager {
|
|
|
151
152
|
|
|
152
153
|
sharedConfig.dbmPropagationMode = dbmPropagationMode
|
|
153
154
|
sharedConfig.dsmEnabled = dsmEnabled
|
|
155
|
+
sharedConfig.memcachedCommandEnabled = memcachedCommandEnabled
|
|
154
156
|
|
|
155
157
|
if (serviceMapping && serviceMapping[name]) {
|
|
156
158
|
sharedConfig.service = serviceMapping[name]
|