Skip to content

Commit a073145

Browse files
authored
feat(aap): Extended request data collection via rules (#6275)
1 parent c95eb35 commit a073145

File tree

11 files changed

+572
-81
lines changed

11 files changed

+572
-81
lines changed

index.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,8 @@ declare namespace tracer {
786786

787787
/** Whether to enable request body collection on RASP event
788788
* @default false
789+
*
790+
* @deprecated Use UI and Remote Configuration to enable extended data collection
789791
*/
790792
bodyCollection?: boolean
791793
},
@@ -810,20 +812,28 @@ declare namespace tracer {
810812
},
811813
/**
812814
* Configuration for extended headers collection tied to security events
815+
*
816+
* @deprecated Use UI and Remote Configuration to enable extended data collection
813817
*/
814818
extendedHeadersCollection?: {
815819
/** Whether to enable extended headers collection
816820
* @default false
821+
*
822+
* @deprecated Use UI and Remote Configuration to enable extended data collection
817823
*/
818824
enabled: boolean,
819825

820826
/** Whether to redact collected headers
821827
* @default true
828+
*
829+
* @deprecated Use UI and Remote Configuration to enable extended data collection
822830
*/
823831
redaction: boolean,
824832

825833
/** Specifies the maximum number of headers collected.
826834
* @default 50
835+
*
836+
* @deprecated Use UI and Remote Configuration to enable extended data collection
827837
*/
828838
maxHeaders: number,
829839
}

packages/dd-trace/src/appsec/reporter.js

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,20 @@ const config = {
3838

3939
const metricsQueue = new Map()
4040

41+
const extendedDataCollectionRequest = new WeakMap()
42+
4143
// following header lists are ordered in the same way the spec orders them, it doesn't matter but it's easier to compare
4244
const contentHeaderList = [
4345
'content-length',
44-
'content-type',
4546
'content-encoding',
4647
'content-language'
4748
]
4849

50+
const responseHeaderList = [
51+
...contentHeaderList,
52+
'content-type'
53+
]
54+
4955
const identificationHeaders = [
5056
'x-amzn-trace-id',
5157
'cloudfront-viewer-ja3-fingerprint',
@@ -75,15 +81,27 @@ const requestHeadersList = [
7581
...identificationHeaders
7682
]
7783

84+
const redactedHeadersList = [
85+
'authorization',
86+
'proxy-authorization',
87+
'www-authenticate',
88+
'proxy-authenticate',
89+
'authentication-info',
90+
'proxy-authentication-info',
91+
'cookie',
92+
'set-cookie'
93+
]
94+
7895
// these request headers are always collected - it breaks the expected spec orders
7996
const REQUEST_HEADERS_MAP = mapHeaderAndTags(requestHeadersList, REQUEST_HEADER_TAG_PREFIX)
8097

8198
const EVENT_HEADERS_MAP = mapHeaderAndTags(eventHeadersList, REQUEST_HEADER_TAG_PREFIX)
8299

83-
const RESPONSE_HEADERS_MAP = mapHeaderAndTags(contentHeaderList, RESPONSE_HEADER_TAG_PREFIX)
100+
const RESPONSE_HEADERS_MAP = mapHeaderAndTags(responseHeaderList, RESPONSE_HEADER_TAG_PREFIX)
84101

85102
const NON_EXTENDED_REQUEST_HEADERS = new Set([...requestHeadersList, ...eventHeadersList])
86-
const NON_EXTENDED_RESPONSE_HEADERS = new Set(contentHeaderList)
103+
const NON_EXTENDED_RESPONSE_HEADERS = new Set(responseHeaderList)
104+
const REDACTED_HEADERS = new Set(redactedHeadersList)
87105

88106
function init (_config) {
89107
config.headersExtendedCollectionEnabled = _config.extendedHeadersCollection.enabled
@@ -132,15 +150,17 @@ function filterExtendedHeaders (headers, excludedHeaderNames, tagPrefix, limit =
132150
for (const [headerName, headerValue] of Object.entries(headers)) {
133151
if (counter >= limit) break
134152
if (!excludedHeaderNames.has(headerName)) {
135-
result[getHeaderTag(tagPrefix, headerName)] = String(headerValue)
153+
result[getHeaderTag(tagPrefix, headerName)] = REDACTED_HEADERS.has(headerName)
154+
? '<redacted>'
155+
: String(headerValue)
136156
counter++
137157
}
138158
}
139159

140160
return result
141161
}
142162

143-
function getCollectedHeaders (req, res, shouldCollectEventHeaders, storedResponseHeaders = {}) {
163+
function getCollectedHeaders (req, res, shouldCollectEventHeaders, storedResponseHeaders = {}, extendedDataCollection) {
144164
// Mandatory
145165
const mandatoryCollectedHeaders = filterHeaders(req.headers, REQUEST_HEADERS_MAP)
146166

@@ -154,7 +174,8 @@ function getCollectedHeaders (req, res, shouldCollectEventHeaders, storedRespons
154174
const requestEventCollectedHeaders = filterHeaders(req.headers, EVENT_HEADERS_MAP)
155175
const responseEventCollectedHeaders = filterHeaders(responseHeaders, RESPONSE_HEADERS_MAP)
156176

157-
if (!config.headersExtendedCollectionEnabled || config.headersRedaction) {
177+
// TODO headersExtendedCollectionEnabled and headersRedaction properties are deprecated to delete in a major
178+
if ((!config.headersExtendedCollectionEnabled || config.headersRedaction) && !extendedDataCollection) {
158179
// Standard collection
159180
return Object.assign(
160181
mandatoryCollectedHeaders,
@@ -163,12 +184,15 @@ function getCollectedHeaders (req, res, shouldCollectEventHeaders, storedRespons
163184
)
164185
}
165186

187+
// TODO config.maxHeadersCollected is deprecated to delete in a major
188+
const maxHeadersCollected = extendedDataCollection?.max_collected_headers ?? config.maxHeadersCollected
189+
166190
// Extended collection
167-
const requestExtendedHeadersAvailableCount =
168-
config.maxHeadersCollected -
169-
Object.keys(mandatoryCollectedHeaders).length -
191+
const collectedHeadersCount = Object.keys(mandatoryCollectedHeaders).length +
170192
Object.keys(requestEventCollectedHeaders).length
171193

194+
const requestExtendedHeadersAvailableCount = maxHeadersCollected - collectedHeadersCount
195+
172196
const requestEventExtendedCollectedHeaders =
173197
filterExtendedHeaders(
174198
req.headers,
@@ -178,7 +202,7 @@ function getCollectedHeaders (req, res, shouldCollectEventHeaders, storedRespons
178202
)
179203

180204
const responseExtendedHeadersAvailableCount =
181-
config.maxHeadersCollected -
205+
maxHeadersCollected -
182206
Object.keys(responseEventCollectedHeaders).length
183207

184208
const responseEventExtendedCollectedHeaders =
@@ -199,15 +223,15 @@ function getCollectedHeaders (req, res, shouldCollectEventHeaders, storedRespons
199223

200224
// Check discarded headers
201225
const requestHeadersCount = Object.keys(req.headers).length
202-
if (requestHeadersCount > config.maxHeadersCollected) {
226+
if (requestHeadersCount > maxHeadersCollected) {
203227
headersTags['_dd.appsec.request.header_collection.discarded'] =
204-
requestHeadersCount - config.maxHeadersCollected
228+
requestHeadersCount - maxHeadersCollected
205229
}
206230

207231
const responseHeadersCount = Object.keys(responseHeaders).length
208-
if (responseHeadersCount > config.maxHeadersCollected) {
232+
if (responseHeadersCount > maxHeadersCollected) {
209233
headersTags['_dd.appsec.response.header_collection.discarded'] =
210-
responseHeadersCount - config.maxHeadersCollected
234+
responseHeadersCount - maxHeadersCollected
211235
}
212236

213237
return headersTags
@@ -307,7 +331,7 @@ function reportTruncationMetrics (rootSpan, metrics) {
307331
}
308332
}
309333

310-
function reportAttack (attackData) {
334+
function reportAttack ({ events: attackData, actions }) {
311335
const store = storage('legacy').getStore()
312336
const req = store?.req
313337
const rootSpan = web.root(req)
@@ -338,8 +362,14 @@ function reportAttack (attackData) {
338362

339363
rootSpan.addTags(newTags)
340364

365+
// TODO this should be deleted in a major
341366
if (config.raspBodyCollection && isRaspAttack(attackData)) {
342-
reportRequestBody(rootSpan, req.body)
367+
reportRequestBody(rootSpan, req.body, true)
368+
}
369+
370+
const extendedDataCollection = actions?.extended_data_collection
371+
if (extendedDataCollection) {
372+
extendedDataCollectionRequest.set(req, extendedDataCollection)
343373
}
344374
}
345375

@@ -398,18 +428,29 @@ function truncateRequestBody (target, depth = 0) {
398428
}
399429
}
400430

401-
function reportRequestBody (rootSpan, requestBody) {
402-
if (!requestBody) return
431+
function reportRequestBody (rootSpan, requestBody, comesFromRaspAction = false) {
432+
if (!requestBody || Object.keys(requestBody).length === 0) return
403433

404434
if (!rootSpan.meta_struct) {
405435
rootSpan.meta_struct = {}
406436
}
407437

408-
if (!rootSpan.meta_struct['http.request.body']) {
438+
if (rootSpan.meta_struct['http.request.body']) {
439+
// If the rasp.exceed metric exists, set also the same for the new tag
440+
const currentTags = rootSpan.context()._tags
441+
const sizeExceedTagValue = currentTags['_dd.appsec.rasp.request_body_size.exceeded']
442+
443+
if (sizeExceedTagValue) {
444+
rootSpan.setTag('_dd.appsec.request_body_size.exceeded', sizeExceedTagValue)
445+
}
446+
} else {
409447
const { truncated, value } = truncateRequestBody(requestBody)
410448
rootSpan.meta_struct['http.request.body'] = value
411449
if (truncated) {
412-
rootSpan.setTag('_dd.appsec.rasp.request_body_size.exceeded', 'true')
450+
const sizeExceedTagKey = comesFromRaspAction
451+
? '_dd.appsec.rasp.request_body_size.exceeded' // TODO old metric to delete in a major
452+
: '_dd.appsec.request_body_size.exceeded'
453+
rootSpan.setTag(sizeExceedTagKey, 'true')
413454
}
414455
}
415456
}
@@ -496,7 +537,15 @@ function finishRequest (req, res, storedResponseHeaders) {
496537

497538
const tags = rootSpan.context()._tags
498539

499-
const newTags = getCollectedHeaders(req, res, shouldCollectEventHeaders(tags), storedResponseHeaders)
540+
const extendedDataCollection = extendedDataCollectionRequest.get(req)
541+
const newTags = getCollectedHeaders(
542+
req, res, shouldCollectEventHeaders(tags), storedResponseHeaders, extendedDataCollection
543+
)
544+
545+
if (extendedDataCollection) {
546+
// TODO add support for fastify, req.body is not available in fastify
547+
reportRequestBody(rootSpan, req.body)
548+
}
500549

501550
if (tags['appsec.event'] === 'true' && typeof req.route?.path === 'string') {
502551
newTags['http.endpoint'] = req.route.path

packages/dd-trace/src/appsec/waf/waf_context_wrapper.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ class WAFContextWrapper {
141141
metrics.wafTimeout = result.timeout
142142

143143
if (ruleTriggered) {
144-
Reporter.reportAttack(result.events)
144+
Reporter.reportAttack(result)
145145
}
146146

147147
Reporter.reportAttributes(result.attributes)

packages/dd-trace/src/config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,7 @@ class Config {
668668
this._envUnprocessed['appsec.blockedTemplateJson'] = DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON
669669
this._setBoolean(env, 'appsec.enabled', DD_APPSEC_ENABLED)
670670
this._setString(env, 'appsec.eventTracking.mode', DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE)
671+
// TODO appsec.extendedHeadersCollection are deprecated, to delete in a major
671672
this._setBoolean(env, 'appsec.extendedHeadersCollection.enabled', DD_APPSEC_COLLECT_ALL_HEADERS)
672673
this._setBoolean(
673674
env,
@@ -679,6 +680,7 @@ class Config {
679680
this._setString(env, 'appsec.obfuscatorKeyRegex', DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP)
680681
this._setString(env, 'appsec.obfuscatorValueRegex', DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP)
681682
this._setBoolean(env, 'appsec.rasp.enabled', DD_APPSEC_RASP_ENABLED)
683+
// TODO Deprecated, to delete in a major
682684
this._setBoolean(env, 'appsec.rasp.bodyCollection', DD_APPSEC_RASP_COLLECT_REQUEST_BODY)
683685
env['appsec.rateLimit'] = maybeInt(DD_APPSEC_TRACE_RATE_LIMIT)
684686
this._envUnprocessed['appsec.rateLimit'] = DD_APPSEC_TRACE_RATE_LIMIT

packages/dd-trace/src/config_defaults.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,14 @@ module.exports = {
3636
'appsec.blockedTemplateJson': undefined,
3737
'appsec.enabled': undefined,
3838
'appsec.eventTracking.mode': 'identification',
39+
// TODO appsec.extendedHeadersCollection is deprecated, to delete in a major
3940
'appsec.extendedHeadersCollection.enabled': false,
4041
'appsec.extendedHeadersCollection.redaction': true,
4142
'appsec.extendedHeadersCollection.maxHeaders': 50,
4243
'appsec.obfuscatorKeyRegex': defaultWafObfuscatorKeyRegex,
4344
'appsec.obfuscatorValueRegex': defaultWafObfuscatorValueRegex,
4445
'appsec.rasp.enabled': true,
46+
// TODO Deprecated, to delete in a major
4547
'appsec.rasp.bodyCollection': false,
4648
'appsec.rateLimit': 100,
4749
'appsec.rules': undefined,

packages/dd-trace/src/remote_config/capabilities.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ module.exports = {
3131
ASM_RASP_CMDI: 1n << 37n,
3232
ASM_DD_MULTICONFIG: 1n << 42n,
3333
ASM_TRACE_TAGGING_RULES: 1n << 43n,
34+
ASM_EXTENDED_DATA_COLLECTION: 1n << 44n,
3435
/*
3536
DO NOT ADD ARBITRARY CAPABILITIES IN YOUR CODE
3637
UNLESS THEY ARE ALREADY DEFINED IN THE BACKEND SOURCE OF TRUTH

packages/dd-trace/src/remote_config/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ function enableWafUpdate (appsecConfig) {
9696
rc.updateCapabilities(RemoteConfigCapabilities.ASM_HEADER_FINGERPRINT, true)
9797
rc.updateCapabilities(RemoteConfigCapabilities.ASM_DD_MULTICONFIG, true)
9898
rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRACE_TAGGING_RULES, true)
99+
rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXTENDED_DATA_COLLECTION, true)
99100

100101
if (appsecConfig.rasp?.enabled) {
101102
rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SQLI, true)
@@ -134,6 +135,7 @@ function disableWafUpdate () {
134135
rc.updateCapabilities(RemoteConfigCapabilities.ASM_HEADER_FINGERPRINT, false)
135136
rc.updateCapabilities(RemoteConfigCapabilities.ASM_DD_MULTICONFIG, false)
136137
rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRACE_TAGGING_RULES, false)
138+
rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXTENDED_DATA_COLLECTION, false)
137139

138140
rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SQLI, false)
139141
rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SSRF, false)

0 commit comments

Comments
 (0)