Skip to content

Commit

Permalink
Report general tags and metrics (#5335)
Browse files Browse the repository at this point in the history
* update native appsec

* report appsec block failed

* report truncation tags

* add error blocking log

* failed graphql blocking tag

* report after block

* add waf timeout span tag

* add rasp timeout span tag

* add waf error tag

* add rasp error tag

* remove raspRule from waf run call

* add waf wrapper tests

* report metrics after blocking

* call runWaf

* rasp timout test

* add waf/rasp errors and timeout

* report metrics after catch on graphql

* return false if waf result is not defined

* graphql blocking action

* graphql report metrics

* check if request data exist first

* user tracking blocking action

* waf and rasp timeout

* fix sql injection tests

* add comment for waf timeout

* keep only waf error test

* fallback to dummy BlockList for cypress 6

* fix linter

* add waf error test

* fix rasp resources path

* fix waf error on windows

* fix waf error on windows path

* remove waf errors test file

* report waf metrics on api security schema extraction

* report metrics inside run waf

* user blocking tests

* add reporter tests

* linter

* add test for block failure

* use settag instead of addtags

* metrics order

* fix linter and test

* remove return null

* add waf context run stubs

* waf error code readability

* fix timers

* report metrics on waf failure

* fix metrics

* remove run waf function

* add unit tests

* add telemetry tests

* test message

* replace total runtime with duration

* reporter test

* test title

* add more tests
  • Loading branch information
IlyasShabi authored Mar 7, 2025
1 parent 0506666 commit 6e11e2a
Show file tree
Hide file tree
Showing 20 changed files with 544 additions and 121 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
},
"dependencies": {
"@datadog/libdatadog": "^0.5.0",
"@datadog/native-appsec": "8.4.0",
"@datadog/native-appsec": "8.5.0",
"@datadog/native-iast-rewriter": "2.8.0",
"@datadog/native-iast-taint-tracking": "3.3.0",
"@datadog/native-metrics": "^3.1.0",
Expand Down
39 changes: 23 additions & 16 deletions packages/dd-trace/src/appsec/blocking.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,29 +100,36 @@ function getBlockingData (req, specificType, actionParameters) {
}

function block (req, res, rootSpan, abortController, actionParameters = defaultBlockingActionParameters) {
if (res.headersSent) {
log.warn('[ASM] Cannot send blocking response when headers have already been sent')
return
}
try {
if (res.headersSent) {
log.warn('[ASM] Cannot send blocking response when headers have already been sent')

const { body, headers, statusCode } = getBlockingData(req, null, actionParameters)
throw new Error('Headers have already been sent')
}

rootSpan.addTags({
'appsec.blocked': 'true'
})
const { body, headers, statusCode } = getBlockingData(req, null, actionParameters)

for (const headerName of res.getHeaderNames()) {
res.removeHeader(headerName)
}
for (const headerName of res.getHeaderNames()) {
res.removeHeader(headerName)
}

res.writeHead(statusCode, headers)
res.writeHead(statusCode, headers)

// this is needed to call the original end method, since express-session replaces it
res.constructor.prototype.end.call(res, body)
// this is needed to call the original end method, since express-session replaces it
res.constructor.prototype.end.call(res, body)

responseBlockedSet.add(res)
rootSpan.setTag('appsec.blocked', 'true')

abortController?.abort()
responseBlockedSet.add(res)
abortController?.abort()

return true
} catch (err) {
rootSpan?.setTag('_dd.appsec.block.failed', 1)
log.error('[ASM] Blocking error', err)

return false
}
}

function getBlockingAction (actions) {
Expand Down
19 changes: 13 additions & 6 deletions packages/dd-trace/src/appsec/graphql.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const {
getBlockingData,
getBlockingAction
} = require('./blocking')
const log = require('../log')
const waf = require('./waf')
const addresses = require('./addresses')
const web = require('../plugins/util/web')
Expand Down Expand Up @@ -94,14 +95,20 @@ function beforeWriteApolloGraphqlResponse ({ abortController, abortData }) {
const rootSpan = web.root(req)
if (!rootSpan) return

const blockingData = getBlockingData(req, specificBlockingTypes.GRAPHQL, requestData.wafAction)
abortData.statusCode = blockingData.statusCode
abortData.headers = blockingData.headers
abortData.message = blockingData.body
try {
const blockingData = getBlockingData(req, specificBlockingTypes.GRAPHQL, requestData.wafAction)
abortData.statusCode = blockingData.statusCode
abortData.headers = blockingData.headers
abortData.message = blockingData.body

rootSpan.setTag('appsec.blocked', 'true')
rootSpan.setTag('appsec.blocked', 'true')

abortController?.abort()
abortController?.abort()
} catch (err) {
rootSpan.setTag('_dd.appsec.block.failed', 1)

log.error('[ASM] Blocking error', err)
}
}

graphqlRequestData.delete(req)
Expand Down
1 change: 0 additions & 1 deletion packages/dd-trace/src/appsec/rasp/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ function handleResult (actions, req, res, abortController, config) {

const blockingAction = getBlockingAction(actions)
if (blockingAction) {
const rootSpan = web.root(req)
// Should block only in express
if (rootSpan?.context()._name === 'express.request') {
const abortError = new DatadogRaspAbortError(req, res, blockingAction)
Expand Down
35 changes: 35 additions & 0 deletions packages/dd-trace/src/appsec/reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,34 @@ function reportWafInit (wafVersion, rulesVersion, diagnosticsRules = {}) {
function reportMetrics (metrics, raspRule) {
const store = storage('legacy').getStore()
const rootSpan = store?.req && web.root(store.req)

if (!rootSpan) return

if (metrics.rulesVersion) {
rootSpan.setTag('_dd.appsec.event_rules.version', metrics.rulesVersion)
}

if (raspRule) {
updateRaspRequestsMetricTags(metrics, store.req, raspRule)
} else {
updateWafRequestsMetricTags(metrics, store.req)
}

reportTruncationMetrics(rootSpan, metrics)
}

function reportTruncationMetrics (rootSpan, metrics) {
if (metrics.maxTruncatedString) {
rootSpan.setTag('_dd.appsec.truncated.string_length', metrics.maxTruncatedString)
}

if (metrics.maxTruncatedContainerSize) {
rootSpan.setTag('_dd.appsec.truncated.container_size', metrics.maxTruncatedContainerSize)
}

if (metrics.maxTruncatedContainerDepth) {
rootSpan.setTag('_dd.appsec.truncated.container_depth', metrics.maxTruncatedContainerDepth)
}
}

function reportAttack (attackData) {
Expand Down Expand Up @@ -189,6 +207,7 @@ function finishRequest (req, res) {
}

const metrics = getRequestMetrics(req)

if (metrics?.duration) {
rootSpan.setTag('_dd.appsec.waf.duration', metrics.duration)
}
Expand All @@ -197,6 +216,14 @@ function finishRequest (req, res) {
rootSpan.setTag('_dd.appsec.waf.duration_ext', metrics.durationExt)
}

if (metrics?.wafErrorCode) {
rootSpan.setTag('_dd.appsec.waf.error', metrics.wafErrorCode)
}

if (metrics?.wafTimeouts) {
rootSpan.setTag('_dd.appsec.waf.timeouts', metrics.wafTimeouts)
}

if (metrics?.raspDuration) {
rootSpan.setTag('_dd.appsec.rasp.duration', metrics.raspDuration)
}
Expand All @@ -205,6 +232,14 @@ function finishRequest (req, res) {
rootSpan.setTag('_dd.appsec.rasp.duration_ext', metrics.raspDurationExt)
}

if (metrics?.raspErrorCode) {
rootSpan.setTag('_dd.appsec.rasp.error', metrics.raspErrorCode)
}

if (metrics?.raspTimeouts) {
rootSpan.setTag('_dd.appsec.rasp.timeout', metrics.raspTimeouts)
}

if (metrics?.raspEvalCount) {
rootSpan.setTag('_dd.appsec.rasp.rule.eval', metrics.raspEvalCount)
}
Expand Down
4 changes: 1 addition & 3 deletions packages/dd-trace/src/appsec/sdk/user_blocking.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,7 @@ function blockRequest (tracer, req, res) {
return false
}

block(req, res, rootSpan)

return true
return block(req, res, rootSpan)
}

module.exports = {
Expand Down
6 changes: 5 additions & 1 deletion packages/dd-trace/src/appsec/telemetry/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ function newStore () {
durationExt: 0,
raspDuration: 0,
raspDurationExt: 0,
raspEvalCount: 0
raspEvalCount: 0,
wafTimeouts: 0,
raspTimeouts: 0,
wafErrorCode: null,
raspErrorCode: null
}
}
}
Expand Down
17 changes: 16 additions & 1 deletion packages/dd-trace/src/appsec/telemetry/rasp.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,25 @@ const { DD_TELEMETRY_REQUEST_METRICS } = require('./common')

const appsecMetrics = telemetryMetrics.manager.namespace('appsec')

function addRaspRequestMetrics (store, { duration, durationExt }) {
function addRaspRequestMetrics (store, { duration, durationExt, wafTimeout, errorCode }) {
store[DD_TELEMETRY_REQUEST_METRICS].raspDuration += duration || 0
store[DD_TELEMETRY_REQUEST_METRICS].raspDurationExt += durationExt || 0
store[DD_TELEMETRY_REQUEST_METRICS].raspEvalCount++

if (wafTimeout) {
store[DD_TELEMETRY_REQUEST_METRICS].raspTimeouts++
}

if (errorCode) {
if (store[DD_TELEMETRY_REQUEST_METRICS].raspErrorCode) {
store[DD_TELEMETRY_REQUEST_METRICS].raspErrorCode = Math.max(
errorCode,
store[DD_TELEMETRY_REQUEST_METRICS].raspErrorCode
)
} else {
store[DD_TELEMETRY_REQUEST_METRICS].raspErrorCode = errorCode
}
}
}

function trackRaspMetrics (metrics, raspRule) {
Expand Down
17 changes: 16 additions & 1 deletion packages/dd-trace/src/appsec/telemetry/waf.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,24 @@ const appsecMetrics = telemetryMetrics.manager.namespace('appsec')

const DD_TELEMETRY_WAF_RESULT_TAGS = Symbol('_dd.appsec.telemetry.waf.result.tags')

function addWafRequestMetrics (store, { duration, durationExt }) {
function addWafRequestMetrics (store, { duration, durationExt, wafTimeout, errorCode }) {
store[DD_TELEMETRY_REQUEST_METRICS].duration += duration || 0
store[DD_TELEMETRY_REQUEST_METRICS].durationExt += durationExt || 0

if (wafTimeout) {
store[DD_TELEMETRY_REQUEST_METRICS].wafTimeouts++
}

if (errorCode) {
if (store[DD_TELEMETRY_REQUEST_METRICS].wafErrorCode) {
store[DD_TELEMETRY_REQUEST_METRICS].wafErrorCode = Math.max(
errorCode,
store[DD_TELEMETRY_REQUEST_METRICS].wafErrorCode
)
} else {
store[DD_TELEMETRY_REQUEST_METRICS].wafErrorCode = errorCode
}
}
}

function trackWafDurations ({ duration, durationExt }, versionsTags) {
Expand Down
56 changes: 43 additions & 13 deletions packages/dd-trace/src/appsec/waf/waf_context_wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ class WAFContextWrapper {
this.wafTimeout = wafTimeout
this.wafVersion = wafVersion
this.rulesVersion = rulesVersion
this.addressesToSkip = new Set()
this.knownAddresses = knownAddresses
this.addressesToSkip = new Set()
this.cachedUserIdActions = new Map()
}

Expand Down Expand Up @@ -77,13 +77,44 @@ class WAFContextWrapper {

if (!payloadHasData) return

const metrics = {
rulesVersion: this.rulesVersion,
wafVersion: this.wafVersion,
wafTimeout: false,
duration: 0,
durationExt: 0,
blockTriggered: false,
ruleTriggered: false,
errorCode: null,
maxTruncatedString: null,
maxTruncatedContainerSize: null,
maxTruncatedContainerDepth: null
}

try {
const start = process.hrtime.bigint()

const result = this.ddwafContext.run(payload, this.wafTimeout)

const end = process.hrtime.bigint()

metrics.durationExt = parseInt(end - start) / 1e3

if (typeof result.errorCode === 'number' && result.errorCode < 0) {
const error = new Error('WAF code error')
error.errorCode = result.errorCode

throw error
}

if (result.metrics) {
const { maxTruncatedString, maxTruncatedContainerSize, maxTruncatedContainerDepth } = result.metrics

if (maxTruncatedString) metrics.maxTruncatedString = maxTruncatedString
if (maxTruncatedContainerSize) metrics.maxTruncatedContainerSize = maxTruncatedContainerSize
if (maxTruncatedContainerDepth) metrics.maxTruncatedContainerDepth = maxTruncatedContainerDepth
}

this.addressesToSkip = newAddressesToSkip

const ruleTriggered = !!result.events?.length
Expand All @@ -96,29 +127,28 @@ class WAFContextWrapper {
this.setUserIdCache(userId, result)
}

Reporter.reportMetrics({
duration: result.totalRuntime / 1e3,
durationExt: parseInt(end - start) / 1e3,
rulesVersion: this.rulesVersion,
ruleTriggered,
blockTriggered,
wafVersion: this.wafVersion,
wafTimeout: result.timeout
}, raspRule)
metrics.duration = result.totalRuntime / 1e3
metrics.blockTriggered = blockTriggered
metrics.ruleTriggered = ruleTriggered
metrics.wafTimeout = result.timeout

if (ruleTriggered) {
Reporter.reportAttack(JSON.stringify(result.events))
}

Reporter.reportDerivatives(result.derivatives)

return result.actions
} catch (err) {
log.error('[ASM] Error while running the AppSec WAF', err)

metrics.errorCode = err.errorCode ?? -127
} finally {
if (wafRunFinished.hasSubscribers) {
wafRunFinished.publish({ payload })
}

return result.actions
} catch (err) {
log.error('[ASM] Error while running the AppSec WAF', err)
Reporter.reportMetrics(metrics, raspRule)
}
}

Expand Down
Loading

0 comments on commit 6e11e2a

Please sign in to comment.