Skip to content

Commit d1d7ccf

Browse files
authored
waf requests telemetry metrics (#5384)
* waf requests telemetry metrics * fix undefined versions * add more tests * fix tags order * update waf metrics integration test * remove duplicate input_truncated on test * fix waf requests telemtry test * fix rate limiter * report waf error metric * fix updateWafRateLimitedMetric reporter test * fix functions order * change function names * add versions to report attack * fix reporting rate limiting metric
1 parent 1a74c9b commit d1d7ccf

File tree

12 files changed

+374
-113
lines changed

12 files changed

+374
-113
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const log = require('../log')
44
const blockedTemplates = require('./blocked_templates')
5+
const { updateBlockFailureMetric } = require('./telemetry')
56

67
const detectedSpecificEndpoints = {}
78

@@ -128,6 +129,7 @@ function block (req, res, rootSpan, abortController, actionParameters = defaultB
128129
rootSpan?.setTag('_dd.appsec.block.failed', 1)
129130
log.error('[ASM] Blocking error', err)
130131

132+
updateBlockFailureMetric(req)
131133
return false
132134
}
133135
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const {
1717
apolloChannel,
1818
apolloServerCoreChannel
1919
} = require('./channels')
20+
const { updateBlockFailureMetric } = require('./telemetry')
2021

2122
const graphqlRequestData = new WeakMap()
2223

@@ -106,8 +107,9 @@ function beforeWriteApolloGraphqlResponse ({ abortController, abortData }) {
106107
abortController?.abort()
107108
} catch (err) {
108109
rootSpan.setTag('_dd.appsec.block.failed', 1)
109-
110110
log.error('[ASM] Blocking error', err)
111+
112+
updateBlockFailureMetric(req)
111113
}
112114
}
113115

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ const {
1010
updateRaspRequestsMetricTags,
1111
incrementWafUpdatesMetric,
1212
incrementWafRequestsMetric,
13-
getRequestMetrics
13+
getRequestMetrics,
14+
updateRateLimitedMetric
1415
} = require('./telemetry')
1516
const zlib = require('zlib')
1617
const { keepTrace } = require('../priority_sampler')
@@ -149,6 +150,8 @@ function reportAttack (attackData) {
149150

150151
if (limiter.isAllowed()) {
151152
keepTrace(rootSpan, ASM)
153+
} else {
154+
updateRateLimitedMetric(req)
152155
}
153156

154157
// TODO: maybe add this to format.js later (to take decision as late as possible)

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
const DD_TELEMETRY_REQUEST_METRICS = Symbol('_dd.appsec.telemetry.request.metrics')
44

55
const tags = {
6+
BLOCK_FAILURE: 'block_failure',
7+
EVENT_RULES_VERSION: 'event_rules_version',
8+
INPUT_TRUNCATED: 'input_truncated',
9+
RATE_LIMITED: 'rate_limited',
610
REQUEST_BLOCKED: 'request_blocked',
711
RULE_TRIGGERED: 'rule_triggered',
12+
WAF_ERROR: 'waf_error',
813
WAF_TIMEOUT: 'waf_timeout',
9-
WAF_VERSION: 'waf_version',
10-
EVENT_RULES_VERSION: 'event_rules_version',
11-
INPUT_TRUNCATED: 'input_truncated'
14+
WAF_VERSION: 'waf_version'
1215
}
1316

1417
function getVersionsTags (wafVersion, rulesVersion) {

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@ function updateWafRequestsMetricTags (metrics, req) {
7474
return trackWafMetrics(store, metrics)
7575
}
7676

77+
function updateRateLimitedMetric (req) {
78+
if (!enabled) return
79+
80+
const store = getStore(req)
81+
trackWafMetrics(store, { rateLimited: true })
82+
}
83+
84+
function updateBlockFailureMetric (req) {
85+
if (!enabled) return
86+
87+
const store = getStore(req)
88+
trackWafMetrics(store, { blockFailed: true })
89+
}
90+
7791
function incrementWafInitMetric (wafVersion, rulesVersion, success) {
7892
if (!enabled) return
7993

@@ -119,6 +133,8 @@ module.exports = {
119133
disable,
120134

121135
updateWafRequestsMetricTags,
136+
updateRateLimitedMetric,
137+
updateBlockFailureMetric,
122138
updateRaspRequestsMetricTags,
123139
incrementWafInitMetric,
124140
incrementWafUpdatesMetric,

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

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,28 @@ function trackWafMetrics (store, metrics) {
5050

5151
const metricTags = getOrCreateMetricTags(store, versionsTags)
5252

53-
const { blockTriggered, ruleTriggered, wafTimeout } = metrics
53+
if (metrics.blockFailed) {
54+
metricTags[tags.BLOCK_FAILURE] = true
55+
}
5456

55-
if (blockTriggered) {
57+
if (metrics.blockTriggered) {
5658
metricTags[tags.REQUEST_BLOCKED] = true
5759
}
5860

59-
if (ruleTriggered) {
61+
if (metrics.rateLimited) {
62+
metricTags[tags.RATE_LIMITED] = true
63+
}
64+
65+
if (metrics.ruleTriggered) {
6066
metricTags[tags.RULE_TRIGGERED] = true
6167
}
6268

63-
if (wafTimeout) {
69+
if (metrics.errorCode) {
70+
metricTags[tags.WAF_ERROR] = true
71+
appsecMetrics.count('waf.error', { ...versionsTags, waf_error: metrics.errorCode }).inc()
72+
}
73+
74+
if (metrics.wafTimeout) {
6475
metricTags[tags.WAF_TIMEOUT] = true
6576
}
6677

@@ -78,10 +89,13 @@ function getOrCreateMetricTags (store, versionsTags) {
7889

7990
if (!metricTags) {
8091
metricTags = {
92+
[tags.BLOCK_FAILURE]: false,
93+
[tags.INPUT_TRUNCATED]: false,
94+
[tags.RATE_LIMITED]: false,
8195
[tags.REQUEST_BLOCKED]: false,
8296
[tags.RULE_TRIGGERED]: false,
97+
[tags.WAF_ERROR]: false,
8398
[tags.WAF_TIMEOUT]: false,
84-
[tags.INPUT_TRUNCATED]: false,
8599

86100
...versionsTags
87101
}

packages/dd-trace/test/appsec/blocking.spec.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe('blocking', () => {
1313
}
1414
}
1515

16-
let log
16+
let log, telemetry
1717
let block, setTemplates
1818
let req, res, rootSpan
1919

@@ -22,9 +22,14 @@ describe('blocking', () => {
2222
warn: sinon.stub()
2323
}
2424

25+
telemetry = {
26+
updateBlockFailureMetric: sinon.stub()
27+
}
28+
2529
const blocking = proxyquire('../src/appsec/blocking', {
2630
'../log': log,
27-
'./blocked_templates': defaultBlockedTemplate
31+
'./blocked_templates': defaultBlockedTemplate,
32+
'./telemetry': telemetry
2833
})
2934

3035
block = blocking.block
@@ -66,6 +71,7 @@ describe('blocking', () => {
6671
expect(rootSpan.setTag).to.have.been.calledOnceWithExactly('_dd.appsec.block.failed', 1)
6772
expect(res.setHeader).to.not.have.been.called
6873
expect(res.constructor.prototype.end).to.not.have.been.called
74+
expect(telemetry.updateBlockFailureMetric).to.be.calledOnceWithExactly(req)
6975
})
7076

7177
it('should send blocking response with html type if present in the headers', () => {
@@ -79,6 +85,7 @@ describe('blocking', () => {
7985
'Content-Length': 12
8086
})
8187
expect(res.constructor.prototype.end).to.have.been.calledOnceWithExactly('htmlBodyéé')
88+
expect(telemetry.updateBlockFailureMetric).to.not.have.been.called
8289
})
8390

8491
it('should send blocking response with json type if present in the headers in priority', () => {
@@ -92,6 +99,7 @@ describe('blocking', () => {
9299
'Content-Length': 8
93100
})
94101
expect(res.constructor.prototype.end).to.have.been.calledOnceWithExactly('jsonBody')
102+
expect(telemetry.updateBlockFailureMetric).to.not.have.been.called
95103
})
96104

97105
it('should send blocking response with json type if neither html or json is present in the headers', () => {
@@ -104,6 +112,7 @@ describe('blocking', () => {
104112
'Content-Length': 8
105113
})
106114
expect(res.constructor.prototype.end).to.have.been.calledOnceWithExactly('jsonBody')
115+
expect(telemetry.updateBlockFailureMetric).to.not.have.been.called
107116
})
108117

109118
it('should send blocking response and call abortController if passed in arguments', () => {
@@ -118,6 +127,7 @@ describe('blocking', () => {
118127
})
119128
expect(res.constructor.prototype.end).to.have.been.calledOnceWithExactly('jsonBody')
120129
expect(abortController.signal.aborted).to.be.true
130+
expect(telemetry.updateBlockFailureMetric).to.not.have.been.called
121131
})
122132

123133
it('should remove all headers before sending blocking response', () => {
@@ -135,6 +145,7 @@ describe('blocking', () => {
135145
'Content-Length': 8
136146
})
137147
expect(res.constructor.prototype.end).to.have.been.calledOnceWithExactly('jsonBody')
148+
expect(telemetry.updateBlockFailureMetric).to.not.have.been.called
138149
})
139150
})
140151

packages/dd-trace/test/appsec/graphql.spec.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ const {
1212
} = require('../../src/appsec/channels')
1313

1414
describe('GraphQL', () => {
15-
let graphql
16-
let blocking
15+
let graphql, blocking, telemetry
1716

1817
beforeEach(() => {
1918
const getBlockingData = sinon.stub()
@@ -29,8 +28,13 @@ describe('GraphQL', () => {
2928
statusCode: 403
3029
})
3130

31+
telemetry = {
32+
updateBlockFailureMetric: sinon.stub()
33+
}
34+
3235
graphql = proxyquire('../../src/appsec/graphql', {
33-
'./blocking': blocking
36+
'./blocking': blocking,
37+
'./telemetry': telemetry
3438
})
3539
})
3640

@@ -234,6 +238,7 @@ describe('GraphQL', () => {
234238
expect(blocking.getBlockingData).to.have.been.calledOnceWithExactly(req, 'graphql', blockParameters)
235239

236240
expect(rootSpan.setTag).to.have.been.calledOnceWithExactly('appsec.blocked', 'true')
241+
expect(telemetry.updateBlockFailureMetric).to.not.have.been.called
237242
})
238243

239244
it('Should catch error when block fails', () => {
@@ -263,6 +268,7 @@ describe('GraphQL', () => {
263268
expect(blocking.getBlockingData).to.have.been.calledOnceWithExactly(req, 'graphql', blockParameters)
264269

265270
expect(rootSpan.setTag).to.have.been.calledOnceWithExactly('_dd.appsec.block.failed', 1)
271+
expect(telemetry.updateBlockFailureMetric).to.be.calledOnceWithExactly(req)
266272
})
267273
})
268274
})

packages/dd-trace/test/appsec/reporter.spec.js

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ describe('reporter', () => {
3838
updateRaspRequestsMetricTags: sinon.stub(),
3939
incrementWafUpdatesMetric: sinon.stub(),
4040
incrementWafRequestsMetric: sinon.stub(),
41+
updateRateLimitedMetric: sinon.stub(),
4142
getRequestMetrics: sinon.stub()
4243
}
4344

@@ -296,6 +297,7 @@ describe('reporter', () => {
296297
'_dd.appsec.json': '{"triggers":[{"rule":{},"rule_matches":[{}]}]}'
297298
})
298299
expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, ASM)
300+
expect(telemetry.updateRateLimitedMetric).to.not.have.been.called
299301
})
300302

301303
it('should add tags to request span', () => {
@@ -310,38 +312,43 @@ describe('reporter', () => {
310312
'network.client.ip': '8.8.8.8'
311313
})
312314
expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, ASM)
315+
expect(telemetry.updateRateLimitedMetric).to.not.have.been.called
313316
})
314317

315318
it('should not add manual.keep when rate limit is reached', (done) => {
316319
const addTags = span.addTags
317-
const params = {}
318320

319-
expect(Reporter.reportAttack('', params)).to.not.be.false
320-
expect(Reporter.reportAttack('', params)).to.not.be.false
321-
expect(Reporter.reportAttack('', params)).to.not.be.false
321+
expect(Reporter.reportAttack('')).to.not.be.false
322+
expect(Reporter.reportAttack('')).to.not.be.false
323+
expect(Reporter.reportAttack('')).to.not.be.false
322324

323325
expect(prioritySampler.setPriority).to.have.callCount(3)
326+
expect(telemetry.updateRateLimitedMetric).to.not.have.been.called
324327

325328
Reporter.setRateLimit(1)
326329

327-
expect(Reporter.reportAttack('', params)).to.not.be.false
330+
expect(Reporter.reportAttack('')).to.not.be.false
328331
expect(addTags.getCall(3).firstArg).to.have.property('appsec.event').that.equals('true')
329332
expect(prioritySampler.setPriority).to.have.callCount(4)
330-
expect(Reporter.reportAttack('', params)).to.not.be.false
333+
expect(telemetry.updateRateLimitedMetric).to.not.have.been.called
334+
335+
expect(Reporter.reportAttack('')).to.not.be.false
331336
expect(addTags.getCall(4).firstArg).to.have.property('appsec.event').that.equals('true')
332337
expect(prioritySampler.setPriority).to.have.callCount(4)
338+
expect(telemetry.updateRateLimitedMetric).to.be.calledOnceWithExactly(req)
333339

334340
setTimeout(() => {
335-
expect(Reporter.reportAttack('', params)).to.not.be.false
341+
expect(Reporter.reportAttack('')).to.not.be.false
336342
expect(prioritySampler.setPriority).to.have.callCount(5)
343+
expect(telemetry.updateRateLimitedMetric).to.be.calledOnceWithExactly(req)
337344
done()
338345
}, 1020)
339346
})
340347

341348
it('should not overwrite origin tag', () => {
342349
span.context()._tags = { '_dd.origin': 'tracer' }
343350

344-
const result = Reporter.reportAttack('[]', {})
351+
const result = Reporter.reportAttack('[]')
345352
expect(result).to.not.be.false
346353
expect(web.root).to.have.been.calledOnceWith(req)
347354

@@ -351,6 +358,7 @@ describe('reporter', () => {
351358
'network.client.ip': '8.8.8.8'
352359
})
353360
expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, ASM)
361+
expect(telemetry.updateRateLimitedMetric).to.not.have.been.called
354362
})
355363

356364
it('should merge attacks json', () => {
@@ -367,6 +375,7 @@ describe('reporter', () => {
367375
'network.client.ip': '8.8.8.8'
368376
})
369377
expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, ASM)
378+
expect(telemetry.updateRateLimitedMetric).to.not.have.been.called
370379
})
371380

372381
it('should call standalone sample', () => {
@@ -384,6 +393,7 @@ describe('reporter', () => {
384393
})
385394

386395
expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, ASM)
396+
expect(telemetry.updateRateLimitedMetric).to.not.have.been.called
387397
})
388398
})
389399

packages/dd-trace/test/appsec/telemetry/rasp.spec.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,15 @@ describe('Appsec Rasp Telemetry metrics', () => {
138138
appsecTelemetry.incrementWafRequestsMetric(req)
139139

140140
expect(count).to.have.been.calledWithExactly('waf.requests', {
141+
block_failure: false,
142+
input_truncated: false,
141143
request_blocked: false,
144+
rate_limited: false,
142145
rule_triggered: false,
146+
waf_error: false,
143147
waf_timeout: false,
144148
waf_version: wafVersion,
145-
event_rules_version: rulesVersion,
146-
input_truncated: false
149+
event_rules_version: rulesVersion
147150
})
148151
})
149152
})

0 commit comments

Comments
 (0)