-
Notifications
You must be signed in to change notification settings - Fork 397
feat(aap): In App WAF support for lambda #7783
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4bf2cba
dd4d188
04d4032
4ab0342
4cd1280
7f58984
c38806d
0dc7da4
64c21ed
590897a
1606181
fafde73
cd79f96
732b6ff
67c81cb
03d0a02
a8d1734
fdf489c
22d6848
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| 'use strict' | ||
|
|
||
| const { HTTP_CLIENT_IP } = require('../../../../ext/tags') | ||
|
|
||
| const log = require('../log') | ||
| const addresses = require('./addresses') | ||
| const Reporter = require('./reporter') | ||
| const waf = require('./waf') | ||
|
|
||
| // Tracks spans for which start-invocation has been processed, so that | ||
| // end-invocation can gate correctly | ||
| const activeInvocations = new WeakSet() | ||
|
|
||
| /** | ||
| * Maps pre-extracted HTTP data from the Lambda event to WAF addresses, | ||
| * runs the WAF, and reports results on the span. | ||
| * | ||
| * @param {{ span: object, headers: Record<string, string>, method: string, path: string, | ||
| * query: Record<string, string | string[]> | undefined, body: string | object | undefined, | ||
| * isBase64Encoded: boolean, clientIp: string | undefined, | ||
| * pathParams: Record<string, string> | undefined, | ||
| * cookies: Record<string, string> | undefined, | ||
| * route: string | undefined }} data | ||
| */ | ||
| function onLambdaStartInvocation (data) { | ||
| try { | ||
| const { span, headers, method, path, query, body, clientIp, pathParams, cookies } = data | ||
|
|
||
| if (!span) { | ||
| log.warn('[ASM] No span provided in Lambda start invocation') | ||
| return | ||
| } | ||
|
|
||
| activeInvocations.add(span) | ||
|
|
||
| span.setTag('_dd.appsec.enabled', 1) | ||
|
|
||
| const persistent = {} | ||
|
|
||
| if (path) { | ||
| persistent[addresses.HTTP_INCOMING_URL] = path | ||
|
uurien marked this conversation as resolved.
|
||
| } | ||
|
|
||
| if (method) { | ||
| persistent[addresses.HTTP_INCOMING_METHOD] = method | ||
| } | ||
|
|
||
| if (headers) { | ||
| // Cookie header is already stripped by the Lambda layer's event-data-extractor | ||
| persistent[addresses.HTTP_INCOMING_HEADERS] = headers | ||
| } | ||
|
simon-id marked this conversation as resolved.
|
||
|
|
||
| if (clientIp) { | ||
| span.setTag(HTTP_CLIENT_IP, clientIp) | ||
| persistent[addresses.HTTP_CLIENT_IP] = clientIp | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. The two separate conditionals are now merged into one. |
||
| } | ||
|
|
||
| if (query) { | ||
| persistent[addresses.HTTP_INCOMING_QUERY] = query | ||
| } | ||
|
|
||
| if (body !== undefined && body !== null) { | ||
| persistent[addresses.HTTP_INCOMING_BODY] = body | ||
| } | ||
|
|
||
| if (pathParams) { | ||
| persistent[addresses.HTTP_INCOMING_PARAMS] = pathParams | ||
| } | ||
|
|
||
| if (cookies) { | ||
| persistent[addresses.HTTP_INCOMING_COOKIES] = cookies | ||
| } | ||
|
|
||
| waf.run({ persistent }, span, undefined, span) | ||
| } catch (err) { | ||
| log.error('[ASM] Error in Lambda start-invocation handler', err) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Maps response data to WAF addresses, runs a final WAF pass, | ||
| * disposes the WAF context, and finishes the request report. | ||
| * | ||
| * @param {{ span: object, statusCode: string | undefined, | ||
| * responseHeaders: Record<string, string> | undefined }} data | ||
| */ | ||
| function onLambdaEndInvocation (data) { | ||
| try { | ||
| const { span, statusCode, responseHeaders } = data | ||
|
|
||
| if (!span) { | ||
| log.warn('[ASM] No span provided in Lambda end invocation') | ||
| return | ||
| } | ||
|
|
||
| if (!activeInvocations.has(span)) { | ||
| return | ||
| } | ||
|
|
||
| activeInvocations.delete(span) | ||
|
|
||
| let hasPersistentData = false | ||
| const persistent = {} | ||
|
|
||
| if (statusCode) { | ||
| persistent[addresses.HTTP_INCOMING_RESPONSE_CODE] = String(statusCode) | ||
| hasPersistentData = true | ||
| } | ||
|
|
||
| if (responseHeaders) { | ||
| const filteredHeaders = { ...responseHeaders } | ||
| delete filteredHeaders['set-cookie'] | ||
| persistent[addresses.HTTP_INCOMING_RESPONSE_HEADERS] = filteredHeaders | ||
| hasPersistentData = true | ||
| } | ||
|
|
||
| if (hasPersistentData) { | ||
| waf.run({ persistent }, span, undefined, span) | ||
| } | ||
|
|
||
| waf.disposeContext(span) | ||
|
|
||
| Reporter.finishRequest(span, null, {}, undefined, span) | ||
| } catch (err) { | ||
| log.error('[ASM] Error in Lambda end-invocation handler', err) | ||
| } | ||
| } | ||
|
|
||
| module.exports = { | ||
| onLambdaStartInvocation, | ||
| onLambdaEndInvocation, | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -318,13 +318,21 @@ function reportWafConfigUpdate (product, rcConfigId, diagnostics, wafVersion) { | |
| } | ||
| } | ||
|
|
||
| function reportMetrics (metrics, raspRule, req) { | ||
| /** | ||
| * @param {object} metrics - WAF run metrics | ||
| * @param {string} [raspRule] - RASP rule identifier | ||
| * @param {object} [req] - Request key (plain object for lambda) | ||
| * @param {object} [rootSpan] - Root span (required for lambda) | ||
| */ | ||
| function reportMetrics (metrics, raspRule, req, rootSpan) { | ||
| if (!req) { | ||
| req = getActiveRequest() | ||
| } | ||
| const rootSpan = req && web.root(req) | ||
| if (!rootSpan) { | ||
| rootSpan = req && web.root(req) | ||
| } | ||
|
|
||
| if (!rootSpan) return | ||
| if (!req || !rootSpan) return | ||
|
|
||
| if (metrics.rulesVersion) { | ||
| rootSpan.setTag('_dd.appsec.event_rules.version', metrics.rulesVersion) | ||
|
|
@@ -353,13 +361,26 @@ function reportTruncationMetrics (rootSpan, metrics) { | |
| } | ||
| } | ||
|
|
||
| function reportAttack ({ events: attackData, actions }, req) { | ||
| // NOTE: `req` in the WAF execution path may be any object, not necessarily | ||
| // an HTTP IncomingMessage. In Lambda, it is a plain context key ({}) with no | ||
| // HTTP properties. Always guard HTTP-specific property access | ||
| // See tests in lambda.spec.js for enforcement. | ||
|
|
||
| /** | ||
| * @param {{ events: Array, actions: object }} result - WAF result with attack data | ||
| * @param {object} [req] - Request key. May be an HTTP IncomingMessage or a plain object (Lambda) | ||
| * @param {object} [rootSpan] - Root span (required for lambda) | ||
| */ | ||
| function reportAttack ({ events: attackData, actions }, req, rootSpan) { | ||
| if (!req) { | ||
| req = getActiveRequest() | ||
| } | ||
|
Comment on lines
+374
to
377
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i'm worried that in this function, if we don't pass the empty object fake "req", it will try to get it from the store, which would make this req variable here undefined, and crash later down the function because it's trying to access props in req (req.socket, req.body, etc). The safety mecanism that was in place before was the rootSpan check. If req was missing, then rootSpan would also be missing, and thus the Can you prove to me that req can never be empty here ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, your concern is legit: decoupling rootSpan from req breaks the implicit safety mechanism. But in the lambda path, req is never I've added a test to check the WAF path with a strict req object that throws on any unaudited property access, making it fail if someone adds |
||
|
|
||
| const rootSpan = web.root(req) | ||
| if (!rootSpan) return | ||
| if (!rootSpan) { | ||
| rootSpan = web.root(req) | ||
| } | ||
|
|
||
| if (!req || !rootSpan) return | ||
|
|
||
| const spanContext = rootSpan.context() | ||
|
|
||
|
|
@@ -494,14 +515,21 @@ function isSchemaAttribute (attribute) { | |
| return attribute.startsWith('_dd.appsec.s.') | ||
| } | ||
|
|
||
| function reportAttributes (attributes, req) { | ||
| /** | ||
| * @param {object} [attributes] - WAF result attributes | ||
| * @param {object} [req] - Request key (plain object for lambda) | ||
| * @param {object} [rootSpan] - Root span (required for lambda) | ||
| */ | ||
| function reportAttributes (attributes, req, rootSpan) { | ||
| if (!attributes) return | ||
|
|
||
| if (!req) { | ||
| req = getActiveRequest() | ||
| } | ||
|
|
||
| const rootSpan = web.root(req) | ||
| if (!rootSpan) { | ||
| rootSpan = web.root(req) | ||
| } | ||
|
|
||
| if (!rootSpan) return | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ditto
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There is also an existing test that calls |
||
|
|
||
|
|
@@ -517,8 +545,17 @@ function reportAttributes (attributes, req) { | |
| rootSpan.addTags(tags) | ||
| } | ||
|
|
||
| function finishRequest (req, res, storedResponseHeaders, requestBody) { | ||
| const rootSpan = web.root(req) | ||
| /** | ||
| * @param {object} [req] - Request key (null for Lambda) | ||
| * @param {object} [res] - Response object (null for lambda) | ||
| * @param {object} storedResponseHeaders | ||
| * @param {object} [requestBody] | ||
| * @param {object} [rootSpan] - Root span (required for lambda) | ||
| */ | ||
| function finishRequest (req, res, storedResponseHeaders, requestBody, rootSpan) { | ||
| if (!rootSpan) { | ||
| rootSpan = web.root(req) | ||
| } | ||
| if (!rootSpan) return | ||
|
|
||
| if (metricsQueue.size) { | ||
|
|
@@ -569,6 +606,8 @@ function finishRequest (req, res, storedResponseHeaders, requestBody) { | |
| rootSpan.setTag('_dd.appsec.rasp.rule.eval', metrics.raspEvalCount) | ||
| } | ||
|
|
||
| if (!req) return | ||
|
|
||
| incrementWafRequestsMetric(req) | ||
|
|
||
| const tags = rootSpan.context().getTags() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's helpful to attach the error here for later investigations.