Skip to content

Commit ecc6588

Browse files
authored
Add rasp telemetry metrics (#5458)
* add rasp telemetry missing metrics * add tests for rasp rule match * report rasp rule skipped metric * report rasp rule on nextjs * rasp metrics * report rule match * block * fix windows test * code cleaning * add rule triggered related comment * rule variant tests * fix rasp duration track test
1 parent 120b6b0 commit ecc6588

17 files changed

Lines changed: 607 additions & 63 deletions

packages/dd-trace/src/appsec/rasp/command_injection.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ function analyzeCommandInjection ({ file, fileArgs, shell, abortController }) {
4747
const result = waf.run({ ephemeral }, req, raspRule)
4848

4949
const res = store?.res
50-
handleResult(result, req, res, abortController, config)
50+
handleResult(result, req, res, abortController, config, raspRule)
5151
}
5252

5353
module.exports = {

packages/dd-trace/src/appsec/rasp/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const ssrf = require('./ssrf')
77
const sqli = require('./sql_injection')
88
const lfi = require('./lfi')
99
const cmdi = require('./command_injection')
10+
const { updateRaspRuleMatchMetricTags } = require('../telemetry')
1011

1112
const { DatadogRaspAbortError } = require('./utils')
1213

@@ -84,9 +85,10 @@ function blockOnDatadogRaspAbortError ({ error }) {
8485
const abortError = findDatadogRaspAbortError(error)
8586
if (!abortError) return false
8687

87-
const { req, res, blockingAction } = abortError
88+
const { req, res, blockingAction, raspRule } = abortError
8889
if (!isBlocked(res)) {
89-
block(req, res, web.root(req), null, blockingAction)
90+
const blocked = block(req, res, web.root(req), null, blockingAction)
91+
updateRaspRuleMatchMetricTags(req, raspRule, true, blocked)
9092
}
9193

9294
return true

packages/dd-trace/src/appsec/rasp/lfi.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ function analyzeLfi (ctx) {
6161
const raspRule = { type: RULE_TYPES.LFI }
6262

6363
const result = waf.run({ ephemeral }, req, raspRule)
64-
handleResult(result, req, res, ctx.abortController, config)
64+
handleResult(result, req, res, ctx.abortController, config, raspRule)
6565
})
6666
}
6767

packages/dd-trace/src/appsec/rasp/sql_injection.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ function analyzeSqlInjection (query, dbSystem, abortController) {
7676

7777
const result = waf.run({ ephemeral }, req, raspRule)
7878

79-
handleResult(result, req, res, abortController, config)
79+
handleResult(result, req, res, abortController, config, raspRule)
8080
}
8181

8282
function hasInputAddress (payload) {

packages/dd-trace/src/appsec/rasp/ssrf.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function analyzeSsrf (ctx) {
3434
const result = waf.run({ ephemeral }, req, raspRule)
3535

3636
const res = store?.res
37-
handleResult(result, req, res, ctx.abortController, config)
37+
handleResult(result, req, res, ctx.abortController, config, raspRule)
3838
}
3939

4040
module.exports = { enable, disable }

packages/dd-trace/src/appsec/rasp/utils.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const web = require('../../plugins/util/web')
44
const { getCallsiteFrames, reportStackTrace, canReportStackTrace } = require('../stack_trace')
55
const { getBlockingAction } = require('../blocking')
66
const log = require('../../log')
7+
const { updateRaspRuleMatchMetricTags } = require('../telemetry')
78

89
const abortOnUncaughtException = process.execArgv?.includes('--abort-on-uncaught-exception')
910

@@ -19,16 +20,17 @@ const RULE_TYPES = {
1920
}
2021

2122
class DatadogRaspAbortError extends Error {
22-
constructor (req, res, blockingAction) {
23+
constructor (req, res, blockingAction, raspRule) {
2324
super('DatadogRaspAbortError')
2425
this.name = 'DatadogRaspAbortError'
2526
this.req = req
2627
this.res = res
2728
this.blockingAction = blockingAction
29+
this.raspRule = raspRule
2830
}
2931
}
3032

31-
function handleResult (actions, req, res, abortController, config) {
33+
function handleResult (actions, req, res, abortController, config, raspRule) {
3234
const generateStackTraceAction = actions?.generate_stack
3335

3436
const { enabled, maxDepth, maxStackTraces } = config.appsec.stackTrace
@@ -45,21 +47,24 @@ function handleResult (actions, req, res, abortController, config) {
4547
)
4648
}
4749

48-
if (!abortController || abortOnUncaughtException) return
50+
if (abortController && !abortOnUncaughtException) {
51+
const blockingAction = getBlockingAction(actions)
4952

50-
const blockingAction = getBlockingAction(actions)
51-
if (blockingAction) {
5253
// Should block only in express
53-
if (rootSpan?.context()._name === 'express.request') {
54-
const abortError = new DatadogRaspAbortError(req, res, blockingAction)
54+
if (blockingAction && rootSpan?.context()._name === 'express.request') {
55+
const abortError = new DatadogRaspAbortError(req, res, blockingAction, raspRule)
5556
abortController.abort(abortError)
5657

5758
// TODO Delete this when support for node 16 is removed
5859
if (!abortController.signal.reason) {
5960
abortController.signal.reason = abortError
6061
}
62+
63+
return
6164
}
6265
}
66+
67+
updateRaspRuleMatchMetricTags(req, raspRule, false, false)
6368
}
6469

6570
module.exports = {

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ const web = require('../plugins/util/web')
66
const { ipHeaderList } = require('../plugins/util/ip_extractor')
77
const {
88
incrementWafInitMetric,
9-
updateWafRequestsMetricTags,
10-
updateRaspRequestsMetricTags,
119
incrementWafUpdatesMetric,
1210
incrementWafRequestsMetric,
13-
getRequestMetrics,
14-
updateRateLimitedMetric
11+
updateWafRequestsMetricTags,
12+
updateRaspRequestsMetricTags,
13+
updateRaspRuleSkippedMetricTags,
14+
updateRateLimitedMetric,
15+
getRequestMetrics
1516
} = require('./telemetry')
1617
const zlib = require('zlib')
1718
const { keepTrace } = require('../priority_sampler')
@@ -294,6 +295,7 @@ module.exports = {
294295
reportMetrics,
295296
reportAttack,
296297
reportWafUpdate: incrementWafUpdatesMetric,
298+
reportRaspRuleSkipped: updateRaspRuleSkippedMetricTags,
297299
reportDerivatives,
298300
finishRequest,
299301
setRateLimit,

packages/dd-trace/src/appsec/telemetry/index.js

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
'use strict'
22

33
const { DD_TELEMETRY_REQUEST_METRICS } = require('./common')
4-
const { addRaspRequestMetrics, trackRaspMetrics } = require('./rasp')
54
const { incrementMissingUserId, incrementMissingUserLogin, incrementSdkEvent } = require('./user')
5+
const {
6+
addRaspRequestMetrics,
7+
trackRaspMetrics,
8+
trackRaspRuleMatch,
9+
trackRaspRuleSkipped
10+
} = require('./rasp')
611
const {
712
addWafRequestMetrics,
813
trackWafMetrics,
@@ -34,7 +39,10 @@ function newStore () {
3439
wafTimeouts: 0,
3540
raspTimeouts: 0,
3641
wafErrorCode: null,
37-
raspErrorCode: null
42+
raspErrorCode: null,
43+
wafVersion: null,
44+
rulesVersion: null,
45+
ruleTriggered: null
3846
}
3947
}
4048
}
@@ -58,7 +66,21 @@ function updateRaspRequestsMetricTags (metrics, req, raspRule) {
5866

5967
if (!enabled) return
6068

61-
trackRaspMetrics(metrics, raspRule)
69+
trackRaspMetrics(store, metrics, raspRule)
70+
}
71+
72+
function updateRaspRuleMatchMetricTags (req, raspRule, blockTriggered, blocked) {
73+
if (!enabled || !req) return
74+
75+
const store = getStore(req)
76+
77+
trackRaspRuleMatch(store, raspRule, blockTriggered, blocked)
78+
}
79+
80+
function updateRaspRuleSkippedMetricTags (raspRule, reason) {
81+
if (!enabled) return
82+
83+
trackRaspRuleSkipped(raspRule, reason)
6284
}
6385

6486
function updateWafRequestsMetricTags (metrics, req) {
@@ -142,6 +164,8 @@ module.exports = {
142164
updateRateLimitedMetric,
143165
updateBlockFailureMetric,
144166
updateRaspRequestsMetricTags,
167+
updateRaspRuleMatchMetricTags,
168+
updateRaspRuleSkippedMetricTags,
145169
incrementWafInitMetric,
146170
incrementWafUpdatesMetric,
147171
incrementWafRequestsMetric,

packages/dd-trace/src/appsec/telemetry/rasp.js

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
'use strict'
22

33
const telemetryMetrics = require('../../telemetry/metrics')
4-
const { DD_TELEMETRY_REQUEST_METRICS } = require('./common')
4+
const { DD_TELEMETRY_REQUEST_METRICS, getVersionsTags } = require('./common')
55

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

8+
const BLOCKING_STATUS = {
9+
FAILURE: 'failure',
10+
IRRELEVANT: 'irrelevant',
11+
SUCCESS: 'success'
12+
}
13+
814
function addRaspRequestMetrics (store, { duration, durationExt, wafTimeout, errorCode }) {
915
store[DD_TELEMETRY_REQUEST_METRICS].raspDuration += duration || 0
1016
store[DD_TELEMETRY_REQUEST_METRICS].raspDurationExt += durationExt || 0
@@ -26,25 +32,95 @@ function addRaspRequestMetrics (store, { duration, durationExt, wafTimeout, erro
2632
}
2733
}
2834

29-
function trackRaspMetrics (metrics, raspRule) {
30-
const tags = { rule_type: raspRule.type, waf_version: metrics.wafVersion }
35+
function trackRaspMetrics (store, metrics, raspRule) {
36+
const versionsTags = getVersionsTags(metrics.wafVersion, metrics.rulesVersion)
37+
const tags = { rule_type: raspRule.type, ...versionsTags }
38+
const telemetryMetrics = store[DD_TELEMETRY_REQUEST_METRICS]
3139

3240
if (raspRule.variant) {
3341
tags.rule_variant = raspRule.variant
3442
}
3543

44+
if (metrics.wafVersion) {
45+
telemetryMetrics.wafVersion = metrics.wafVersion
46+
}
47+
48+
if (metrics.rulesVersion) {
49+
telemetryMetrics.rulesVersion = metrics.rulesVersion
50+
}
51+
52+
if (metrics.ruleTriggered) {
53+
telemetryMetrics.ruleTriggered = true
54+
}
55+
3656
appsecMetrics.count('rasp.rule.eval', tags).inc(1)
3757

58+
if (metrics.duration) {
59+
appsecMetrics.distribution('rasp.rule.duration', tags).track(metrics.duration)
60+
61+
const raspDuration = telemetryMetrics.raspDuration
62+
appsecMetrics.distribution('rasp.duration', versionsTags).track(raspDuration)
63+
}
64+
65+
if (metrics.durationExt) {
66+
const raspDurationExt = telemetryMetrics.raspDurationExt
67+
appsecMetrics.distribution('rasp.duration_ext', versionsTags).track(raspDurationExt)
68+
}
69+
70+
if (metrics.errorCode) {
71+
const errorTags = { ...tags, waf_error: metrics.errorCode }
72+
73+
appsecMetrics.count('rasp.error', errorTags).inc(1)
74+
}
75+
3876
if (metrics.wafTimeout) {
3977
appsecMetrics.count('rasp.timeout', tags).inc(1)
4078
}
79+
}
4180

42-
if (metrics.ruleTriggered) {
43-
appsecMetrics.count('rasp.rule.match', tags).inc(1)
81+
function trackRaspRuleMatch (store, raspRule, blockTriggered, blocked) {
82+
const telemetryMetrics = store[DD_TELEMETRY_REQUEST_METRICS]
83+
if (!telemetryMetrics.ruleTriggered) return
84+
85+
const tags = {
86+
waf_version: telemetryMetrics.wafVersion,
87+
event_rules_version: telemetryMetrics.rulesVersion,
88+
rule_type: raspRule.type,
89+
block: getRuleMatchBlockingStatus(blockTriggered, blocked)
90+
}
91+
92+
if (raspRule.variant) {
93+
tags.rule_variant = raspRule.variant
94+
}
95+
96+
appsecMetrics.count('rasp.rule.match', tags).inc(1)
97+
98+
// this is needed to not count it twice for the same match
99+
// but it also means it can only be called once per waf call even if there are multiple rasp match
100+
telemetryMetrics.ruleTriggered = null
101+
}
102+
103+
function trackRaspRuleSkipped (raspRule, reason) {
104+
const tags = { reason, rule_type: raspRule.type }
105+
106+
if (raspRule.variant) {
107+
tags.rule_variant = raspRule.variant
44108
}
109+
110+
appsecMetrics.count('rasp.rule.skipped', tags).inc(1)
111+
}
112+
113+
function getRuleMatchBlockingStatus (blockTriggered, blocked) {
114+
if (!blockTriggered) {
115+
return BLOCKING_STATUS.IRRELEVANT
116+
}
117+
118+
return blocked ? BLOCKING_STATUS.SUCCESS : BLOCKING_STATUS.FAILURE
45119
}
46120

47121
module.exports = {
48122
addRaspRequestMetrics,
49-
trackRaspMetrics
123+
trackRaspMetrics,
124+
trackRaspRuleMatch,
125+
trackRaspRuleSkipped
50126
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ class WAFContextWrapper {
2525
run ({ persistent, ephemeral }, raspRule) {
2626
if (this.ddwafContext.disposed) {
2727
log.warn('[ASM] Calling run on a disposed context')
28+
if (raspRule) {
29+
Reporter.reportRaspRuleSkipped(raspRule, 'after-request')
30+
}
31+
2832
return
2933
}
3034

0 commit comments

Comments
 (0)