Skip to content

Commit

Permalink
report truncation metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
IlyasShabi committed Mar 7, 2025
1 parent 6e11e2a commit 1c948a0
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 10 deletions.
52 changes: 47 additions & 5 deletions packages/dd-trace/src/appsec/telemetry/waf.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ const appsecMetrics = telemetryMetrics.manager.namespace('appsec')

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

const TRUNCATION_FLAGS = {
LONG_STRING: 1,
LARGE_CONTAINER: 2,
DEEP_CONTAINER: 4
}

function addWafRequestMetrics (store, { duration, durationExt, wafTimeout, errorCode }) {
store[DD_TELEMETRY_REQUEST_METRICS].duration += duration || 0
store[DD_TELEMETRY_REQUEST_METRICS].durationExt += durationExt || 0
Expand Down Expand Up @@ -44,20 +50,23 @@ function trackWafMetrics (store, metrics) {

const metricTags = getOrCreateMetricTags(store, versionsTags)

const { blockTriggered, ruleTriggered, wafTimeout } = metrics

if (blockTriggered) {
if (metrics.blockTriggered) {
metricTags[tags.REQUEST_BLOCKED] = true
}

if (ruleTriggered) {
if (metrics.ruleTriggered) {
metricTags[tags.RULE_TRIGGERED] = true
}

if (wafTimeout) {
if (metrics.wafTimeout) {
metricTags[tags.WAF_TIMEOUT] = true
}

const truncationReason = getTruncationReason(metrics)
if (truncationReason > 0) {
incrementTruncatedMetrics(metrics, truncationReason)
}

return metricTags
}

Expand Down Expand Up @@ -98,6 +107,39 @@ function incrementWafRequests (store) {
}
}

function incrementTruncatedMetrics (metrics, truncationReason) {
const truncationTags = { truncation_reason: truncationReason }
appsecMetrics.count('waf.input_truncated', truncationTags).inc(1)

if (metrics?.maxTruncatedString) {
appsecMetrics.distribution('waf.truncated_value_size',
{ truncation_reason: TRUNCATION_FLAGS.LONG_STRING })
.track(metrics.maxTruncatedString)
}

if (metrics?.maxTruncatedContainerSize) {
appsecMetrics.distribution('waf.truncated_value_size',
{ truncation_reason: TRUNCATION_FLAGS.LARGE_CONTAINER })
.track(metrics.maxTruncatedContainerSize)
}

if (metrics?.maxTruncatedContainerDepth) {
appsecMetrics.distribution('waf.truncated_value_size',
{ truncation_reason: TRUNCATION_FLAGS.DEEP_CONTAINER })
.track(metrics.maxTruncatedContainerDepth)
}
}

function getTruncationReason ({ maxTruncatedString, maxTruncatedContainerSize, maxTruncatedContainerDepth }) {
let reason = 0

if (maxTruncatedString) reason |= TRUNCATION_FLAGS.LONG_STRING
if (maxTruncatedContainerSize) reason |= TRUNCATION_FLAGS.LARGE_CONTAINER
if (maxTruncatedContainerDepth) reason |= TRUNCATION_FLAGS.DEEP_CONTAINER

return reason
}

module.exports = {
addWafRequestMetrics,
trackWafMetrics,
Expand Down
21 changes: 21 additions & 0 deletions packages/dd-trace/test/appsec/resources/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict'

const tracer = require('dd-trace')
tracer.init({
flushInterval: 1
})

const express = require('express')
const body = require('body-parser')

const app = express()
app.use(body.json())
const port = process.env.APP_PORT || 3000

app.post('/', async (req, res) => {
res.end('OK')
})

app.listen(port, () => {
process.send({ port })
})
62 changes: 57 additions & 5 deletions packages/dd-trace/test/appsec/telemetry/waf.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ describe('Appsec Waf Telemetry metrics', () => {
afterEach(sinon.restore)

describe('if enabled', () => {
const metrics = {
wafVersion,
rulesVersion
}

beforeEach(() => {
appsecTelemetry.enable({
enabled: true,
Expand All @@ -38,11 +43,6 @@ describe('Appsec Waf Telemetry metrics', () => {
})

describe('updateWafRequestsMetricTags', () => {
const metrics = {
wafVersion,
rulesVersion
}

it('should skip update if no request is provided', () => {
const result = appsecTelemetry.updateWafRequestsMetricTags(metrics)

Expand Down Expand Up @@ -260,6 +260,58 @@ describe('Appsec Waf Telemetry metrics', () => {
expect(count).to.not.have.been.called
})
})

describe('WAF Truncation metrics', () => {
it('should report truncated string metrics', () => {
appsecTelemetry.updateWafRequestsMetricTags({ maxTruncatedString: 5000 }, req)

expect(count).to.have.been.calledWith('waf.input_truncated', { truncation_reason: 1 })
expect(inc).to.have.been.calledWith(1)

expect(distribution).to.have.been.calledWith('waf.truncated_value_size', { truncation_reason: 1 })
expect(track).to.have.been.calledWith(5000)
})

it('should report truncated container size metrics', () => {
appsecTelemetry.updateWafRequestsMetricTags({ maxTruncatedContainerSize: 300 }, req)

expect(count).to.have.been.calledWith('waf.input_truncated', { truncation_reason: 2 })
expect(inc).to.have.been.calledWith(1)

expect(distribution).to.have.been.calledWith('waf.truncated_value_size', { truncation_reason: 2 })
expect(track).to.have.been.calledWith(300)
})

it('should report truncated container depth metrics', () => {
appsecTelemetry.updateWafRequestsMetricTags({ maxTruncatedContainerDepth: 20 }, req)

expect(count).to.have.been.calledWith('waf.input_truncated', { truncation_reason: 4 })
expect(inc).to.have.been.calledWith(1)

expect(distribution).to.have.been.calledWith('waf.truncated_value_size', { truncation_reason: 4 })
expect(track).to.have.been.calledWith(20)
})

it('should combine truncation reasons when multiple truncations occur', () => {
appsecTelemetry.updateWafRequestsMetricTags({
maxTruncatedString: 5000,
maxTruncatedContainerSize: 300,
maxTruncatedContainerDepth: 20
}, req)

expect(count).to.have.been.calledWith('waf.input_truncated', { truncation_reason: 7 })
expect(distribution).to.have.been.calledWith('waf.truncated_value_size', { truncation_reason: 1 })
expect(distribution).to.have.been.calledWith('waf.truncated_value_size', { truncation_reason: 2 })
expect(distribution).to.have.been.calledWith('waf.truncated_value_size', { truncation_reason: 4 })
})

it('should not report truncation metrics when no truncation occurs', () => {
appsecTelemetry.updateWafRequestsMetricTags(metrics, req)

expect(count).to.not.have.been.calledWith('waf.input_truncated')
expect(distribution).to.not.have.been.calledWith('waf.truncated_value_size')
})
})
})

describe('if disabled', () => {
Expand Down
126 changes: 126 additions & 0 deletions packages/dd-trace/test/appsec/waf-metrics.integration.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
'use strict'

const { createSandbox, FakeAgent, spawnProc } = require('../../../../integration-tests/helpers')
const getPort = require('get-port')
const path = require('path')
const Axios = require('axios')
const { assert } = require('chai')

describe('WAF truncation metrics', () => {
let axios, sandbox, cwd, appPort, appFile, agent, proc

before(async function () {
this.timeout(process.platform === 'win32' ? 90000 : 30000)

sandbox = await createSandbox(
['express'],
false,
[path.join(__dirname, 'resources')]
)

appPort = await getPort()
cwd = sandbox.folder
appFile = path.join(cwd, 'resources', 'index.js')

axios = Axios.create({
baseURL: `http://localhost:${appPort}`
})
})

after(async function () {
this.timeout(60000)
await sandbox.remove()
})

beforeEach(async () => {
agent = await new FakeAgent().start()
proc = await spawnProc(appFile, {
cwd,
env: {
DD_TRACE_AGENT_PORT: agent.port,
APP_PORT: appPort,
DD_APPSEC_ENABLED: 'true',
DD_TELEMETRY_HEARTBEAT_INTERVAL: 1
}
})
})

afterEach(async () => {
proc.kill()
await agent.stop()
})

it('should report tuncation metrics', async () => {
let appsecTelemetryMetricsReceived = false
let appsecTelemetryDistributionsReceived = false

const longValue = 'testattack'.repeat(500)
const largeObject = {}
for (let i = 0; i < 300; ++i) {
largeObject[`key${i}`] = `value${i}`
}
const deepObject = createNestedObject(25, { value: 'a' })
const complexPayload = {
deepObject,
longValue,
largeObject
}

await axios.post('/', { complexPayload })

const checkMessages = agent.assertMessageReceived(({ payload }) => {
assert.strictEqual(payload[0][0].metrics['_dd.appsec.enabled'], 1)
assert.strictEqual(payload[0][0].metrics['_dd.appsec.truncated.container_depth'], 20)
assert.strictEqual(payload[0][0].metrics['_dd.appsec.truncated.container_size'], 300)
assert.strictEqual(payload[0][0].metrics['_dd.appsec.truncated.string_length'], 5000)
})

const checkTelemetryMetrics = agent.assertTelemetryReceived(({ payload }) => {
const namespace = payload.payload.namespace

if (namespace === 'appsec') {
appsecTelemetryMetricsReceived = true
const series = payload.payload.series
const inputTruncated = series.find(s => s.metric === 'waf.input_truncated')

assert.exists(inputTruncated, 'input truncated serie should exist')
assert.strictEqual(inputTruncated.type, 'count')
assert.include(inputTruncated.tags, 'truncation_reason:7')
}
}, 30_000, 'generate-metrics', 2)

const checkTelemetryDistributions = agent.assertTelemetryReceived(({ payload }) => {
const namespace = payload.payload.namespace

if (namespace === 'appsec') {
appsecTelemetryDistributionsReceived = true
const series = payload.payload.series
const wafDuration = series.find(s => s.metric === 'waf.duration')
const wafDurationExt = series.find(s => s.metric === 'waf.duration_ext')
const wafTuncated = series.filter(s => s.metric === 'waf.truncated_value_size')

assert.exists(wafDuration, 'waf duration serie should exist')
assert.exists(wafDurationExt, 'waf duration ext serie should exist')

assert.equal(wafTuncated.length, 3)
assert.include(wafTuncated[0].tags, 'truncation_reason:1')
assert.include(wafTuncated[1].tags, 'truncation_reason:2')
assert.include(wafTuncated[2].tags, 'truncation_reason:4')
}
}, 30_000, 'distributions', 1)

return Promise.all([checkMessages, checkTelemetryMetrics, checkTelemetryDistributions]).then(() => {
assert.equal(appsecTelemetryMetricsReceived, true)
assert.equal(appsecTelemetryDistributionsReceived, true)

return true
})
})
})

const createNestedObject = (n, obj) => {
if (n > 0) {
return { a: createNestedObject(n - 1, obj) }
}
return obj
}

0 comments on commit 1c948a0

Please sign in to comment.