diff --git a/packages/dd-trace/src/appsec/blocking.js b/packages/dd-trace/src/appsec/blocking.js index 5b3dd290c00..ab9bafc3ae2 100644 --- a/packages/dd-trace/src/appsec/blocking.js +++ b/packages/dd-trace/src/appsec/blocking.js @@ -2,6 +2,7 @@ const log = require('../log') const blockedTemplates = require('./blocked_templates') +const { updateBlockFailureMetric } = require('./telemetry') const detectedSpecificEndpoints = {} @@ -128,6 +129,7 @@ function block (req, res, rootSpan, abortController, actionParameters = defaultB rootSpan?.setTag('_dd.appsec.block.failed', 1) log.error('[ASM] Blocking error', err) + updateBlockFailureMetric(req) return false } } diff --git a/packages/dd-trace/src/appsec/graphql.js b/packages/dd-trace/src/appsec/graphql.js index 2ff265e6282..4799bab6ce9 100644 --- a/packages/dd-trace/src/appsec/graphql.js +++ b/packages/dd-trace/src/appsec/graphql.js @@ -17,6 +17,7 @@ const { apolloChannel, apolloServerCoreChannel } = require('./channels') +const { updateBlockFailureMetric } = require('./telemetry') const graphqlRequestData = new WeakMap() @@ -106,8 +107,9 @@ function beforeWriteApolloGraphqlResponse ({ abortController, abortData }) { abortController?.abort() } catch (err) { rootSpan.setTag('_dd.appsec.block.failed', 1) - log.error('[ASM] Blocking error', err) + + updateBlockFailureMetric(req) } } diff --git a/packages/dd-trace/src/appsec/reporter.js b/packages/dd-trace/src/appsec/reporter.js index c5f3bdce56c..ba7ceaa7b0c 100644 --- a/packages/dd-trace/src/appsec/reporter.js +++ b/packages/dd-trace/src/appsec/reporter.js @@ -10,7 +10,8 @@ const { updateRaspRequestsMetricTags, incrementWafUpdatesMetric, incrementWafRequestsMetric, - getRequestMetrics + getRequestMetrics, + updateRateLimitedMetric } = require('./telemetry') const zlib = require('zlib') const { keepTrace } = require('../priority_sampler') @@ -149,6 +150,8 @@ function reportAttack (attackData) { if (limiter.isAllowed()) { keepTrace(rootSpan, ASM) + } else { + updateRateLimitedMetric(req) } // TODO: maybe add this to format.js later (to take decision as late as possible) diff --git a/packages/dd-trace/src/appsec/telemetry/common.js b/packages/dd-trace/src/appsec/telemetry/common.js index a8ed471bd10..d91cd9988b7 100644 --- a/packages/dd-trace/src/appsec/telemetry/common.js +++ b/packages/dd-trace/src/appsec/telemetry/common.js @@ -3,12 +3,15 @@ const DD_TELEMETRY_REQUEST_METRICS = Symbol('_dd.appsec.telemetry.request.metrics') const tags = { + BLOCK_FAILURE: 'block_failure', + EVENT_RULES_VERSION: 'event_rules_version', + INPUT_TRUNCATED: 'input_truncated', + RATE_LIMITED: 'rate_limited', REQUEST_BLOCKED: 'request_blocked', RULE_TRIGGERED: 'rule_triggered', + WAF_ERROR: 'waf_error', WAF_TIMEOUT: 'waf_timeout', - WAF_VERSION: 'waf_version', - EVENT_RULES_VERSION: 'event_rules_version', - INPUT_TRUNCATED: 'input_truncated' + WAF_VERSION: 'waf_version' } function getVersionsTags (wafVersion, rulesVersion) { diff --git a/packages/dd-trace/src/appsec/telemetry/index.js b/packages/dd-trace/src/appsec/telemetry/index.js index 0593efd97b9..18a3e7ca0ec 100644 --- a/packages/dd-trace/src/appsec/telemetry/index.js +++ b/packages/dd-trace/src/appsec/telemetry/index.js @@ -74,6 +74,20 @@ function updateWafRequestsMetricTags (metrics, req) { return trackWafMetrics(store, metrics) } +function updateRateLimitedMetric (req) { + if (!enabled) return + + const store = getStore(req) + trackWafMetrics(store, { rateLimited: true }) +} + +function updateBlockFailureMetric (req) { + if (!enabled) return + + const store = getStore(req) + trackWafMetrics(store, { blockFailed: true }) +} + function incrementWafInitMetric (wafVersion, rulesVersion, success) { if (!enabled) return @@ -119,6 +133,8 @@ module.exports = { disable, updateWafRequestsMetricTags, + updateRateLimitedMetric, + updateBlockFailureMetric, updateRaspRequestsMetricTags, incrementWafInitMetric, incrementWafUpdatesMetric, diff --git a/packages/dd-trace/src/appsec/telemetry/waf.js b/packages/dd-trace/src/appsec/telemetry/waf.js index bf995741ce2..5571c9cfa90 100644 --- a/packages/dd-trace/src/appsec/telemetry/waf.js +++ b/packages/dd-trace/src/appsec/telemetry/waf.js @@ -50,17 +50,28 @@ function trackWafMetrics (store, metrics) { const metricTags = getOrCreateMetricTags(store, versionsTags) - const { blockTriggered, ruleTriggered, wafTimeout } = metrics + if (metrics.blockFailed) { + metricTags[tags.BLOCK_FAILURE] = true + } - if (blockTriggered) { + if (metrics.blockTriggered) { metricTags[tags.REQUEST_BLOCKED] = true } - if (ruleTriggered) { + if (metrics.rateLimited) { + metricTags[tags.RATE_LIMITED] = true + } + + if (metrics.ruleTriggered) { metricTags[tags.RULE_TRIGGERED] = true } - if (wafTimeout) { + if (metrics.errorCode) { + metricTags[tags.WAF_ERROR] = true + appsecMetrics.count('waf.error', { ...versionsTags, waf_error: metrics.errorCode }).inc() + } + + if (metrics.wafTimeout) { metricTags[tags.WAF_TIMEOUT] = true } @@ -78,10 +89,13 @@ function getOrCreateMetricTags (store, versionsTags) { if (!metricTags) { metricTags = { + [tags.BLOCK_FAILURE]: false, + [tags.INPUT_TRUNCATED]: false, + [tags.RATE_LIMITED]: false, [tags.REQUEST_BLOCKED]: false, [tags.RULE_TRIGGERED]: false, + [tags.WAF_ERROR]: false, [tags.WAF_TIMEOUT]: false, - [tags.INPUT_TRUNCATED]: false, ...versionsTags } diff --git a/packages/dd-trace/test/appsec/blocking.spec.js b/packages/dd-trace/test/appsec/blocking.spec.js index 692ef58b6ef..a90d30dc9af 100644 --- a/packages/dd-trace/test/appsec/blocking.spec.js +++ b/packages/dd-trace/test/appsec/blocking.spec.js @@ -13,7 +13,7 @@ describe('blocking', () => { } } - let log + let log, telemetry let block, setTemplates let req, res, rootSpan @@ -22,9 +22,14 @@ describe('blocking', () => { warn: sinon.stub() } + telemetry = { + updateBlockFailureMetric: sinon.stub() + } + const blocking = proxyquire('../src/appsec/blocking', { '../log': log, - './blocked_templates': defaultBlockedTemplate + './blocked_templates': defaultBlockedTemplate, + './telemetry': telemetry }) block = blocking.block @@ -66,6 +71,7 @@ describe('blocking', () => { expect(rootSpan.setTag).to.have.been.calledOnceWithExactly('_dd.appsec.block.failed', 1) expect(res.setHeader).to.not.have.been.called expect(res.constructor.prototype.end).to.not.have.been.called + expect(telemetry.updateBlockFailureMetric).to.be.calledOnceWithExactly(req) }) it('should send blocking response with html type if present in the headers', () => { @@ -79,6 +85,7 @@ describe('blocking', () => { 'Content-Length': 12 }) expect(res.constructor.prototype.end).to.have.been.calledOnceWithExactly('htmlBodyéé') + expect(telemetry.updateBlockFailureMetric).to.not.have.been.called }) it('should send blocking response with json type if present in the headers in priority', () => { @@ -92,6 +99,7 @@ describe('blocking', () => { 'Content-Length': 8 }) expect(res.constructor.prototype.end).to.have.been.calledOnceWithExactly('jsonBody') + expect(telemetry.updateBlockFailureMetric).to.not.have.been.called }) it('should send blocking response with json type if neither html or json is present in the headers', () => { @@ -104,6 +112,7 @@ describe('blocking', () => { 'Content-Length': 8 }) expect(res.constructor.prototype.end).to.have.been.calledOnceWithExactly('jsonBody') + expect(telemetry.updateBlockFailureMetric).to.not.have.been.called }) it('should send blocking response and call abortController if passed in arguments', () => { @@ -118,6 +127,7 @@ describe('blocking', () => { }) expect(res.constructor.prototype.end).to.have.been.calledOnceWithExactly('jsonBody') expect(abortController.signal.aborted).to.be.true + expect(telemetry.updateBlockFailureMetric).to.not.have.been.called }) it('should remove all headers before sending blocking response', () => { @@ -135,6 +145,7 @@ describe('blocking', () => { 'Content-Length': 8 }) expect(res.constructor.prototype.end).to.have.been.calledOnceWithExactly('jsonBody') + expect(telemetry.updateBlockFailureMetric).to.not.have.been.called }) }) diff --git a/packages/dd-trace/test/appsec/graphql.spec.js b/packages/dd-trace/test/appsec/graphql.spec.js index ab030e3082b..9b6d6380e9e 100644 --- a/packages/dd-trace/test/appsec/graphql.spec.js +++ b/packages/dd-trace/test/appsec/graphql.spec.js @@ -12,8 +12,7 @@ const { } = require('../../src/appsec/channels') describe('GraphQL', () => { - let graphql - let blocking + let graphql, blocking, telemetry beforeEach(() => { const getBlockingData = sinon.stub() @@ -29,8 +28,13 @@ describe('GraphQL', () => { statusCode: 403 }) + telemetry = { + updateBlockFailureMetric: sinon.stub() + } + graphql = proxyquire('../../src/appsec/graphql', { - './blocking': blocking + './blocking': blocking, + './telemetry': telemetry }) }) @@ -234,6 +238,7 @@ describe('GraphQL', () => { expect(blocking.getBlockingData).to.have.been.calledOnceWithExactly(req, 'graphql', blockParameters) expect(rootSpan.setTag).to.have.been.calledOnceWithExactly('appsec.blocked', 'true') + expect(telemetry.updateBlockFailureMetric).to.not.have.been.called }) it('Should catch error when block fails', () => { @@ -263,6 +268,7 @@ describe('GraphQL', () => { expect(blocking.getBlockingData).to.have.been.calledOnceWithExactly(req, 'graphql', blockParameters) expect(rootSpan.setTag).to.have.been.calledOnceWithExactly('_dd.appsec.block.failed', 1) + expect(telemetry.updateBlockFailureMetric).to.be.calledOnceWithExactly(req) }) }) }) diff --git a/packages/dd-trace/test/appsec/reporter.spec.js b/packages/dd-trace/test/appsec/reporter.spec.js index 887f620b948..c5575c0baae 100644 --- a/packages/dd-trace/test/appsec/reporter.spec.js +++ b/packages/dd-trace/test/appsec/reporter.spec.js @@ -38,6 +38,7 @@ describe('reporter', () => { updateRaspRequestsMetricTags: sinon.stub(), incrementWafUpdatesMetric: sinon.stub(), incrementWafRequestsMetric: sinon.stub(), + updateRateLimitedMetric: sinon.stub(), getRequestMetrics: sinon.stub() } @@ -296,6 +297,7 @@ describe('reporter', () => { '_dd.appsec.json': '{"triggers":[{"rule":{},"rule_matches":[{}]}]}' }) expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, ASM) + expect(telemetry.updateRateLimitedMetric).to.not.have.been.called }) it('should add tags to request span', () => { @@ -310,30 +312,35 @@ describe('reporter', () => { 'network.client.ip': '8.8.8.8' }) expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, ASM) + expect(telemetry.updateRateLimitedMetric).to.not.have.been.called }) it('should not add manual.keep when rate limit is reached', (done) => { const addTags = span.addTags - const params = {} - expect(Reporter.reportAttack('', params)).to.not.be.false - expect(Reporter.reportAttack('', params)).to.not.be.false - expect(Reporter.reportAttack('', params)).to.not.be.false + expect(Reporter.reportAttack('')).to.not.be.false + expect(Reporter.reportAttack('')).to.not.be.false + expect(Reporter.reportAttack('')).to.not.be.false expect(prioritySampler.setPriority).to.have.callCount(3) + expect(telemetry.updateRateLimitedMetric).to.not.have.been.called Reporter.setRateLimit(1) - expect(Reporter.reportAttack('', params)).to.not.be.false + expect(Reporter.reportAttack('')).to.not.be.false expect(addTags.getCall(3).firstArg).to.have.property('appsec.event').that.equals('true') expect(prioritySampler.setPriority).to.have.callCount(4) - expect(Reporter.reportAttack('', params)).to.not.be.false + expect(telemetry.updateRateLimitedMetric).to.not.have.been.called + + expect(Reporter.reportAttack('')).to.not.be.false expect(addTags.getCall(4).firstArg).to.have.property('appsec.event').that.equals('true') expect(prioritySampler.setPriority).to.have.callCount(4) + expect(telemetry.updateRateLimitedMetric).to.be.calledOnceWithExactly(req) setTimeout(() => { - expect(Reporter.reportAttack('', params)).to.not.be.false + expect(Reporter.reportAttack('')).to.not.be.false expect(prioritySampler.setPriority).to.have.callCount(5) + expect(telemetry.updateRateLimitedMetric).to.be.calledOnceWithExactly(req) done() }, 1020) }) @@ -341,7 +348,7 @@ describe('reporter', () => { it('should not overwrite origin tag', () => { span.context()._tags = { '_dd.origin': 'tracer' } - const result = Reporter.reportAttack('[]', {}) + const result = Reporter.reportAttack('[]') expect(result).to.not.be.false expect(web.root).to.have.been.calledOnceWith(req) @@ -351,6 +358,7 @@ describe('reporter', () => { 'network.client.ip': '8.8.8.8' }) expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, ASM) + expect(telemetry.updateRateLimitedMetric).to.not.have.been.called }) it('should merge attacks json', () => { @@ -367,6 +375,7 @@ describe('reporter', () => { 'network.client.ip': '8.8.8.8' }) expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, ASM) + expect(telemetry.updateRateLimitedMetric).to.not.have.been.called }) it('should call standalone sample', () => { @@ -384,6 +393,7 @@ describe('reporter', () => { }) expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, ASM) + expect(telemetry.updateRateLimitedMetric).to.not.have.been.called }) }) diff --git a/packages/dd-trace/test/appsec/telemetry/rasp.spec.js b/packages/dd-trace/test/appsec/telemetry/rasp.spec.js index c36ffd274c0..fcd20b78607 100644 --- a/packages/dd-trace/test/appsec/telemetry/rasp.spec.js +++ b/packages/dd-trace/test/appsec/telemetry/rasp.spec.js @@ -138,12 +138,15 @@ describe('Appsec Rasp Telemetry metrics', () => { appsecTelemetry.incrementWafRequestsMetric(req) expect(count).to.have.been.calledWithExactly('waf.requests', { + block_failure: false, + input_truncated: false, request_blocked: false, + rate_limited: false, rule_triggered: false, + waf_error: false, waf_timeout: false, waf_version: wafVersion, - event_rules_version: rulesVersion, - input_truncated: false + event_rules_version: rulesVersion }) }) }) diff --git a/packages/dd-trace/test/appsec/telemetry/waf.spec.js b/packages/dd-trace/test/appsec/telemetry/waf.spec.js index eff86ddabb6..d25f80ab112 100644 --- a/packages/dd-trace/test/appsec/telemetry/waf.spec.js +++ b/packages/dd-trace/test/appsec/telemetry/waf.spec.js @@ -53,12 +53,15 @@ describe('Appsec Waf Telemetry metrics', () => { const result = appsecTelemetry.updateWafRequestsMetricTags(metrics, req) expect(result).to.be.deep.eq({ - waf_version: wafVersion, + block_failure: false, event_rules_version: rulesVersion, + input_truncated: false, + rate_limited: false, request_blocked: false, rule_triggered: false, + waf_error: false, waf_timeout: false, - input_truncated: false + waf_version: wafVersion }) }) @@ -67,17 +70,22 @@ describe('Appsec Waf Telemetry metrics', () => { blockTriggered: true, ruleTriggered: true, wafTimeout: true, + rateLimited: true, + errorCode: -1, maxTruncatedString: 5000, ...metrics }, req) expect(result).to.be.deep.eq({ - waf_version: wafVersion, + block_failure: false, event_rules_version: rulesVersion, + input_truncated: true, + rate_limited: true, request_blocked: true, rule_triggered: true, + waf_error: true, waf_timeout: true, - input_truncated: true + waf_version: wafVersion }) }) @@ -86,18 +94,22 @@ describe('Appsec Waf Telemetry metrics', () => { const result2 = appsecTelemetry.updateWafRequestsMetricTags({ ruleTriggered: true, + rateLimited: true, ...metrics }, req) expect(result).to.be.eq(result2) expect(result).to.be.deep.eq({ - waf_version: wafVersion, + block_failure: false, event_rules_version: rulesVersion, + input_truncated: false, + rate_limited: true, request_blocked: false, rule_triggered: true, + waf_error: false, waf_timeout: false, - input_truncated: false + waf_version: wafVersion }) }) @@ -106,6 +118,7 @@ describe('Appsec Waf Telemetry metrics', () => { blockTriggered: true, ruleTriggered: true, wafTimeout: true, + rateLimited: true, maxTruncatedContainerSize: 300, ...metrics }, req) @@ -115,18 +128,22 @@ describe('Appsec Waf Telemetry metrics', () => { blockTriggered: false, ruleTriggered: false, wafTimeout: false, + rateLimited: false, ...metrics }, req2) expect(result).to.be.not.eq(result2) expect(result).to.be.deep.eq({ - waf_version: wafVersion, + block_failure: false, event_rules_version: rulesVersion, + input_truncated: true, + rate_limited: true, request_blocked: true, rule_triggered: true, + waf_error: false, waf_timeout: true, - input_truncated: true + waf_version: wafVersion }) }) @@ -175,8 +192,19 @@ describe('Appsec Waf Telemetry metrics', () => { }) it('should keep the maximum wafErrorCode', () => { - appsecTelemetry.updateWafRequestsMetricTags({ errorCode: -1 }, req) - appsecTelemetry.updateWafRequestsMetricTags({ errorCode: -3 }, req) + appsecTelemetry.updateWafRequestsMetricTags({ wafVersion, rulesVersion, errorCode: -1 }, req) + expect(count).to.have.been.calledWithExactly('waf.error', { + waf_version: wafVersion, + event_rules_version: rulesVersion, + waf_error: -1 + }) + + appsecTelemetry.updateWafRequestsMetricTags({ wafVersion, rulesVersion, errorCode: -3 }, req) + expect(count).to.have.been.calledWithExactly('waf.error', { + waf_version: wafVersion, + event_rules_version: rulesVersion, + waf_error: -3 + }) const { wafErrorCode } = appsecTelemetry.getRequestMetrics(req) expect(wafErrorCode).to.equal(-1) @@ -280,22 +308,30 @@ describe('Appsec Waf Telemetry metrics', () => { describe('incWafRequestsMetric', () => { it('should increment waf.requests metric', () => { appsecTelemetry.updateWafRequestsMetricTags({ - blockTriggered: false, - ruleTriggered: false, + blockTriggered: true, + blockFailed: true, + ruleTriggered: true, wafTimeout: true, + errorCode: -3, + rateLimited: true, + maxTruncatedString: 5000, wafVersion, rulesVersion }, req) appsecTelemetry.incrementWafRequestsMetric(req) - expect(count).to.have.been.calledOnceWithExactly('waf.requests', { - request_blocked: false, - rule_triggered: false, + expect(count).to.have.been.calledWithExactly('waf.input_truncated', { truncation_reason: 1 }) + expect(count).to.have.been.calledWithExactly('waf.requests', { + request_blocked: true, + block_failure: true, + rule_triggered: true, waf_timeout: true, + waf_error: true, + rate_limited: true, + input_truncated: true, waf_version: wafVersion, - event_rules_version: rulesVersion, - input_truncated: false + event_rules_version: rulesVersion }) }) @@ -306,6 +342,22 @@ describe('Appsec Waf Telemetry metrics', () => { }) }) + describe('updateRateLimitedMetric', () => { + it('should set rate_limited to true on the request tags', () => { + appsecTelemetry.updateRateLimitedMetric(req, metrics) + const result = appsecTelemetry.updateWafRequestsMetricTags({ wafVersion, rulesVersion }, req) + expect(result.rate_limited).to.be.true + }) + }) + + describe('updateBlockFailureMetric', () => { + it('should set block_failure to true on the request tags', () => { + appsecTelemetry.updateBlockFailureMetric(req) + const result = appsecTelemetry.updateWafRequestsMetricTags({ wafVersion, rulesVersion }, req) + expect(result.block_failure).to.be.true + }) + }) + describe('WAF Truncation metrics', () => { it('should report truncated string metrics', () => { const result = appsecTelemetry.updateWafRequestsMetricTags({ maxTruncatedString: 5000 }, req) @@ -389,6 +441,18 @@ describe('Appsec Waf Telemetry metrics', () => { expect(inc).to.not.have.been.called }) + it('should not set rate_limited if telemetry is disabled', () => { + appsecTelemetry.updateRateLimitedMetric(req, { wafVersion, rulesVersion }) + const result = appsecTelemetry.updateWafRequestsMetricTags({ wafVersion, rulesVersion }, req) + expect(result).to.be.undefined + }) + + it('should not set block_failure if telemetry is disabled', () => { + appsecTelemetry.updateBlockFailureMetric(req) + const result = appsecTelemetry.updateWafRequestsMetricTags({ wafVersion, rulesVersion }, req) + expect(result).to.be.undefined + }) + describe('updateWafRequestMetricTags', () => { it('should sum waf.duration and waf.durationExt request metrics', () => { appsecTelemetry.enable({ diff --git a/packages/dd-trace/test/appsec/waf-metrics.integration.spec.js b/packages/dd-trace/test/appsec/waf-metrics.integration.spec.js index 322ce7a2fb2..268d3c972b7 100644 --- a/packages/dd-trace/test/appsec/waf-metrics.integration.spec.js +++ b/packages/dd-trace/test/appsec/waf-metrics.integration.spec.js @@ -6,8 +6,8 @@ const path = require('path') const Axios = require('axios') const { assert } = require('chai') -describe('WAF truncation metrics', () => { - let axios, sandbox, cwd, appPort, appFile, agent, proc +describe('WAF Metrics', () => { + let axios, sandbox, cwd, appPort, appFile before(async function () { this.timeout(process.platform === 'win32' ? 90000 : 30000) @@ -32,96 +32,223 @@ describe('WAF truncation metrics', () => { 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 + describe('WAF error metrics', () => { + let agent, proc + + 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, + DD_APPSEC_WAF_TIMEOUT: 0.1 + } + }) + }) + + afterEach(async () => { + proc.kill() + await agent.stop() + }) + + it('should report waf error metrics', async () => { + let appsecTelemetryMetricsReceived = false + + const body = { + name: 'hey' } + + await axios.post('/', body) + + const checkMessages = agent.assertMessageReceived(({ payload }) => { + assert.strictEqual(payload[0][0].metrics['_dd.appsec.enabled'], 1) + assert.strictEqual(payload[0][0].metrics['_dd.appsec.waf.error'], -127) + }) + + const checkTelemetryMetrics = agent.assertTelemetryReceived(({ payload }) => { + const namespace = payload.payload.namespace + + if (namespace === 'appsec') { + appsecTelemetryMetricsReceived = true + const series = payload.payload.series + const wafRequests = series.find(s => s.metric === 'waf.requests') + + assert.exists(wafRequests, 'Waf requests serie should exist') + assert.strictEqual(wafRequests.type, 'count') + assert.include(wafRequests.tags, 'waf_error:true') + assert.include(wafRequests.tags, 'rate_limited:false') + + const wafError = series.find(s => s.metric === 'waf.error') + assert.exists(wafError, 'Waf error serie should exist') + assert.strictEqual(wafError.type, 'count') + assert.include(wafError.tags, 'waf_error:-127') + } + }, 30_000, 'generate-metrics', 2) + + return Promise.all([checkMessages, checkTelemetryMetrics]).then(() => { + assert.equal(appsecTelemetryMetricsReceived, true) + + return true + }) }) }) - afterEach(async () => { - proc.kill() - await agent.stop() + describe('WAF timeout metrics', () => { + let agent, proc + + 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, + DD_APPSEC_WAF_TIMEOUT: 1 + } + }) + }) + + afterEach(async () => { + proc.kill() + await agent.stop() + }) + + it('should report waf timeout metrics', async () => { + let appsecTelemetryMetricsReceived = false + + const complexPayload = createComplexPayload() + await axios.post('/', { complexPayload }) + + const checkMessages = agent.assertMessageReceived(({ payload }) => { + assert.strictEqual(payload[0][0].metrics['_dd.appsec.enabled'], 1) + assert.isTrue(payload[0][0].metrics['_dd.appsec.waf.timeouts'] > 0) + }) + + const checkTelemetryMetrics = agent.assertTelemetryReceived(({ payload }) => { + const namespace = payload.payload.namespace + + if (namespace === 'appsec') { + appsecTelemetryMetricsReceived = true + const series = payload.payload.series + const wafRequests = series.find(s => s.metric === 'waf.requests') + + assert.exists(wafRequests, 'Waf requests serie should exist') + assert.strictEqual(wafRequests.type, 'count') + assert.include(wafRequests.tags, 'waf_timeout:true') + } + }, 30_000, 'generate-metrics', 2) + + return Promise.all([checkMessages, checkTelemetryMetrics]).then(() => { + assert.equal(appsecTelemetryMetricsReceived, true) + + return true + }) + }) }) - 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) + describe('WAF truncation metrics', () => { + let agent, proc + + 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 + } + }) }) - const checkTelemetryMetrics = agent.assertTelemetryReceived(({ payload }) => { - const namespace = payload.payload.namespace + afterEach(async () => { + proc.kill() + await agent.stop() + }) - if (namespace === 'appsec') { - appsecTelemetryMetricsReceived = true - const series = payload.payload.series - const inputTruncated = series.find(s => s.metric === 'waf.input_truncated') + it('should report truncation metrics', async () => { + let appsecTelemetryMetricsReceived = false + let appsecTelemetryDistributionsReceived = false - assert.exists(inputTruncated, 'input truncated serie should exist') - assert.strictEqual(inputTruncated.type, 'count') - assert.include(inputTruncated.tags, 'truncation_reason:7') + const complexPayload = createComplexPayload() + await axios.post('/', { complexPayload }) - const wafRequests = series.find(s => s.metric === 'waf.requests') - assert.exists(wafRequests, 'waf requests serie should exist') - assert.include(wafRequests.tags, 'input_truncated:true') - } - }, 30_000, 'generate-metrics', 2) + 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 checkTelemetryDistributions = agent.assertTelemetryReceived(({ payload }) => { - const namespace = payload.payload.namespace + const checkTelemetryMetrics = 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') + if (namespace === 'appsec') { + appsecTelemetryMetricsReceived = true + const series = payload.payload.series + const inputTruncated = series.find(s => s.metric === 'waf.input_truncated') - assert.exists(wafDuration, 'waf duration serie should exist') - assert.exists(wafDurationExt, 'waf duration ext serie should exist') + assert.exists(inputTruncated, 'input truncated serie should exist') + assert.strictEqual(inputTruncated.type, 'count') + assert.include(inputTruncated.tags, 'truncation_reason:7') - 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) + const wafRequests = series.find(s => s.metric === 'waf.requests') + assert.exists(wafRequests, 'waf requests serie should exist') + assert.include(wafRequests.tags, 'input_truncated:true') + } + }, 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 Promise.all([checkMessages, checkTelemetryMetrics, checkTelemetryDistributions]).then(() => { + assert.equal(appsecTelemetryMetricsReceived, true) + assert.equal(appsecTelemetryDistributionsReceived, true) - return true + return true + }) }) }) }) +const createComplexPayload = () => { + 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' }) + + return { + deepObject, + longValue, + largeObject + } +} + const createNestedObject = (n, obj) => { if (n > 0) { return { a: createNestedObject(n - 1, obj) }