Skip to content

Commit 002eca3

Browse files
committed
report truncation metrics
1 parent 6e11e2a commit 002eca3

File tree

4 files changed

+248
-5
lines changed

4 files changed

+248
-5
lines changed

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

+44
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ const appsecMetrics = telemetryMetrics.manager.namespace('appsec')
77

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

10+
const TRUNCATION_FLAGS = {
11+
LONG_STRING: 1,
12+
LARGE_CONTAINER: 2,
13+
DEEP_CONTAINER: 4
14+
}
15+
1016
function addWafRequestMetrics (store, { duration, durationExt, wafTimeout, errorCode }) {
1117
store[DD_TELEMETRY_REQUEST_METRICS].duration += duration || 0
1218
store[DD_TELEMETRY_REQUEST_METRICS].durationExt += durationExt || 0
@@ -58,6 +64,11 @@ function trackWafMetrics (store, metrics) {
5864
metricTags[tags.WAF_TIMEOUT] = true
5965
}
6066

67+
const truncationReason = getTruncationReason(metrics)
68+
if (truncationReason > 0) {
69+
incrementTruncatedMetrics(metrics, truncationReason)
70+
}
71+
6172
return metricTags
6273
}
6374

@@ -98,6 +109,39 @@ function incrementWafRequests (store) {
98109
}
99110
}
100111

112+
function incrementTruncatedMetrics (metrics, truncationReason) {
113+
const truncationTags = { truncation_reason: truncationReason }
114+
appsecMetrics.count('waf.input_truncated', truncationTags).inc(1)
115+
116+
if (metrics?.maxTruncatedString) {
117+
appsecMetrics.distribution('waf.truncated_value_size',
118+
{ truncation_reason: TRUNCATION_FLAGS.LONG_STRING })
119+
.track(metrics.maxTruncatedString)
120+
}
121+
122+
if (metrics?.maxTruncatedContainerSize) {
123+
appsecMetrics.distribution('waf.truncated_value_size',
124+
{ truncation_reason: TRUNCATION_FLAGS.LARGE_CONTAINER })
125+
.track(metrics.maxTruncatedContainerSize)
126+
}
127+
128+
if (metrics?.maxTruncatedContainerDepth) {
129+
appsecMetrics.distribution('waf.truncated_value_size',
130+
{ truncation_reason: TRUNCATION_FLAGS.DEEP_CONTAINER })
131+
.track(metrics.maxTruncatedContainerDepth)
132+
}
133+
}
134+
135+
function getTruncationReason ({ maxTruncatedString, maxTruncatedContainerSize, maxTruncatedContainerDepth }) {
136+
let reason = 0
137+
138+
if (maxTruncatedString) reason |= TRUNCATION_FLAGS.LONG_STRING
139+
if (maxTruncatedContainerSize) reason |= TRUNCATION_FLAGS.LARGE_CONTAINER
140+
if (maxTruncatedContainerDepth) reason |= TRUNCATION_FLAGS.DEEP_CONTAINER
141+
142+
return reason
143+
}
144+
101145
module.exports = {
102146
addWafRequestMetrics,
103147
trackWafMetrics,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict'
2+
3+
const tracer = require('dd-trace')
4+
tracer.init({
5+
flushInterval: 1
6+
})
7+
8+
const express = require('express')
9+
const body = require('body-parser')
10+
11+
const app = express()
12+
app.use(body.json())
13+
const port = process.env.APP_PORT || 3000
14+
15+
app.post('/', async (req, res) => {
16+
res.end('OK')
17+
})
18+
19+
app.listen(port, () => {
20+
process.send({ port })
21+
})

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

+57-5
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ describe('Appsec Waf Telemetry metrics', () => {
3030
afterEach(sinon.restore)
3131

3232
describe('if enabled', () => {
33+
const metrics = {
34+
wafVersion,
35+
rulesVersion
36+
}
37+
3338
beforeEach(() => {
3439
appsecTelemetry.enable({
3540
enabled: true,
@@ -38,11 +43,6 @@ describe('Appsec Waf Telemetry metrics', () => {
3843
})
3944

4045
describe('updateWafRequestsMetricTags', () => {
41-
const metrics = {
42-
wafVersion,
43-
rulesVersion
44-
}
45-
4646
it('should skip update if no request is provided', () => {
4747
const result = appsecTelemetry.updateWafRequestsMetricTags(metrics)
4848

@@ -260,6 +260,58 @@ describe('Appsec Waf Telemetry metrics', () => {
260260
expect(count).to.not.have.been.called
261261
})
262262
})
263+
264+
describe('WAF Truncation metrics', () => {
265+
it('should report truncated string metrics', () => {
266+
appsecTelemetry.updateWafRequestsMetricTags({ maxTruncatedString: 5000 }, req)
267+
268+
expect(count).to.have.been.calledWith('waf.input_truncated', { truncation_reason: 1 })
269+
expect(inc).to.have.been.calledWith(1)
270+
271+
expect(distribution).to.have.been.calledWith('waf.truncated_value_size', { truncation_reason: 1 })
272+
expect(track).to.have.been.calledWith(5000)
273+
})
274+
275+
it('should report truncated container size metrics', () => {
276+
appsecTelemetry.updateWafRequestsMetricTags({ maxTruncatedContainerSize: 300 }, req)
277+
278+
expect(count).to.have.been.calledWith('waf.input_truncated', { truncation_reason: 2 })
279+
expect(inc).to.have.been.calledWith(1)
280+
281+
expect(distribution).to.have.been.calledWith('waf.truncated_value_size', { truncation_reason: 2 })
282+
expect(track).to.have.been.calledWith(300)
283+
})
284+
285+
it('should report truncated container depth metrics', () => {
286+
appsecTelemetry.updateWafRequestsMetricTags({ maxTruncatedContainerDepth: 20 }, req)
287+
288+
expect(count).to.have.been.calledWith('waf.input_truncated', { truncation_reason: 4 })
289+
expect(inc).to.have.been.calledWith(1)
290+
291+
expect(distribution).to.have.been.calledWith('waf.truncated_value_size', { truncation_reason: 4 })
292+
expect(track).to.have.been.calledWith(20)
293+
})
294+
295+
it('should combine truncation reasons when multiple truncations occur', () => {
296+
appsecTelemetry.updateWafRequestsMetricTags({
297+
maxTruncatedString: 5000,
298+
maxTruncatedContainerSize: 300,
299+
maxTruncatedContainerDepth: 20
300+
}, req)
301+
302+
expect(count).to.have.been.calledWith('waf.input_truncated', { truncation_reason: 7 })
303+
expect(distribution).to.have.been.calledWith('waf.truncated_value_size', { truncation_reason: 1 })
304+
expect(distribution).to.have.been.calledWith('waf.truncated_value_size', { truncation_reason: 2 })
305+
expect(distribution).to.have.been.calledWith('waf.truncated_value_size', { truncation_reason: 4 })
306+
})
307+
308+
it('should not report truncation metrics when no truncation occurs', () => {
309+
appsecTelemetry.updateWafRequestsMetricTags(metrics, req)
310+
311+
expect(count).to.not.have.been.calledWith('waf.input_truncated')
312+
expect(distribution).to.not.have.been.calledWith('waf.truncated_value_size')
313+
})
314+
})
263315
})
264316

265317
describe('if disabled', () => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
'use strict'
2+
3+
const { createSandbox, FakeAgent, spawnProc } = require('../../../../integration-tests/helpers')
4+
const getPort = require('get-port')
5+
const path = require('path')
6+
const Axios = require('axios')
7+
const { assert } = require('chai')
8+
9+
describe('WAF truncation metrics', () => {
10+
let axios, sandbox, cwd, appPort, appFile, agent, proc
11+
12+
before(async function () {
13+
this.timeout(process.platform === 'win32' ? 90000 : 30000)
14+
15+
sandbox = await createSandbox(
16+
['express'],
17+
false,
18+
[path.join(__dirname, 'resources')]
19+
)
20+
21+
appPort = await getPort()
22+
cwd = sandbox.folder
23+
appFile = path.join(cwd, 'resources', 'index.js')
24+
25+
axios = Axios.create({
26+
baseURL: `http://localhost:${appPort}`
27+
})
28+
})
29+
30+
after(async function () {
31+
this.timeout(60000)
32+
await sandbox.remove()
33+
})
34+
35+
beforeEach(async () => {
36+
agent = await new FakeAgent().start()
37+
proc = await spawnProc(appFile, {
38+
cwd,
39+
env: {
40+
DD_TRACE_AGENT_PORT: agent.port,
41+
APP_PORT: appPort,
42+
DD_APPSEC_ENABLED: 'true',
43+
DD_TELEMETRY_HEARTBEAT_INTERVAL: 1
44+
}
45+
})
46+
})
47+
48+
afterEach(async () => {
49+
proc.kill()
50+
await agent.stop()
51+
})
52+
53+
it('should report tuncation metrics', async () => {
54+
let appsecTelemetryMetricsReceived = false
55+
let appsecTelemetryDistributionsReceived = false
56+
57+
const longValue = 'testattack'.repeat(500)
58+
const largeObject = {}
59+
for (let i = 0; i < 300; ++i) {
60+
largeObject[`key${i}`] = `value${i}`
61+
}
62+
const deepObject = createNestedObject(25, { value: 'a' })
63+
const complexPayload = {
64+
deepObject,
65+
longValue,
66+
largeObject
67+
}
68+
69+
await axios.post('/', { complexPayload })
70+
71+
const checkMessages = agent.assertMessageReceived(({ payload }) => {
72+
assert.strictEqual(payload[0][0].metrics['_dd.appsec.enabled'], 1)
73+
assert.strictEqual(payload[0][0].metrics['_dd.appsec.truncated.container_depth'], 20)
74+
assert.strictEqual(payload[0][0].metrics['_dd.appsec.truncated.container_size'], 300)
75+
assert.strictEqual(payload[0][0].metrics['_dd.appsec.truncated.string_length'], 5000)
76+
})
77+
78+
const checkTelemetryMetrics = agent.assertTelemetryReceived(({ payload }) => {
79+
const namespace = payload.payload.namespace
80+
81+
if (namespace === 'appsec') {
82+
appsecTelemetryMetricsReceived = true
83+
const series = payload.payload.series
84+
const inputTruncated = series.find(s => s.metric === 'waf.input_truncated')
85+
86+
assert.exists(inputTruncated, 'input truncated serie should exist')
87+
assert.strictEqual(inputTruncated.type, 'count')
88+
assert.include(inputTruncated.tags, 'truncation_reason:7')
89+
}
90+
}, 30_000, 'generate-metrics', 2)
91+
92+
const checkTelemetryDistributions = agent.assertTelemetryReceived(({ payload }) => {
93+
const namespace = payload.payload.namespace
94+
95+
if (namespace === 'appsec') {
96+
appsecTelemetryDistributionsReceived = true
97+
const series = payload.payload.series
98+
const wafDuration = series.find(s => s.metric === 'waf.duration')
99+
const wafDurationExt = series.find(s => s.metric === 'waf.duration_ext')
100+
const wafTuncated = series.filter(s => s.metric === 'waf.truncated_value_size')
101+
102+
assert.exists(wafDuration, 'waf duration serie should exist')
103+
assert.exists(wafDurationExt, 'waf duration ext serie should exist')
104+
105+
assert.equal(wafTuncated.length, 3)
106+
assert.include(wafTuncated[0].tags, 'truncation_reason:1')
107+
assert.include(wafTuncated[1].tags, 'truncation_reason:2')
108+
assert.include(wafTuncated[2].tags, 'truncation_reason:4')
109+
}
110+
}, 30_000, 'distributions', 1)
111+
112+
return Promise.all([checkMessages, checkTelemetryMetrics, checkTelemetryDistributions]).then(() => {
113+
assert.equal(appsecTelemetryMetricsReceived, true)
114+
assert.equal(appsecTelemetryDistributionsReceived, true)
115+
116+
return true
117+
})
118+
})
119+
})
120+
121+
const createNestedObject = (n, obj) => {
122+
if (n > 0) {
123+
return { a: createNestedObject(n - 1, obj) }
124+
}
125+
return obj
126+
}

0 commit comments

Comments
 (0)