From 2fca510652b42933471a59b4038dd8ae09cb33c8 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Thu, 12 Sep 2024 16:42:28 +0200 Subject: [PATCH 01/16] Report WAF fingerprints --- packages/dd-trace/src/appsec/reporter.js | 27 ++- .../src/appsec/waf/waf_context_wrapper.js | 1 + .../appsec/attacker-fingerprinting-rules.json | 204 ++++++++++++++++++ .../appsec/attacker-fingerprinting.spec.js | 69 ++++++ .../test/appsec/index.express.plugin.spec.js | 70 ++++++ .../dd-trace/test/appsec/reporter.spec.js | 41 ++++ .../dd-trace/test/appsec/waf/index.spec.js | 23 ++ 7 files changed, 433 insertions(+), 2 deletions(-) create mode 100644 packages/dd-trace/test/appsec/attacker-fingerprinting-rules.json create mode 100644 packages/dd-trace/test/appsec/attacker-fingerprinting.spec.js diff --git a/packages/dd-trace/src/appsec/reporter.js b/packages/dd-trace/src/appsec/reporter.js index a58335d9ba7..dd5830d6395 100644 --- a/packages/dd-trace/src/appsec/reporter.js +++ b/packages/dd-trace/src/appsec/reporter.js @@ -162,9 +162,27 @@ function reportSchemas (derivatives) { if (!rootSpan) return const tags = {} - for (const [address, value] of Object.entries(derivatives)) { + for (const [tag, value] of Object.entries(derivatives)) { + if (isFingerprintDerivative(tag)) continue const gzippedValue = zlib.gzipSync(JSON.stringify(value)) - tags[address] = gzippedValue.toString('base64') + tags[tag] = gzippedValue.toString('base64') + } + + rootSpan.addTags(tags) +} + +function reportFingerprints (derivatives) { + if (!derivatives) return + + const req = storage.getStore()?.req + const rootSpan = web.root(req) + + if (!rootSpan) return + + const tags = {} + for (const [tag, value] of Object.entries(derivatives)) { + if (!isFingerprintDerivative(tag)) continue + tags[tag] = value } rootSpan.addTags(tags) @@ -240,6 +258,10 @@ function setRateLimit (rateLimit) { limiter = new Limiter(rateLimit) } +function isFingerprintDerivative(derivative) { + return derivative.startsWith('_dd.appsec.fp') +} + module.exports = { metricsQueue, filterHeaders, @@ -249,6 +271,7 @@ module.exports = { reportAttack, reportWafUpdate: incrementWafUpdatesMetric, reportSchemas, + reportFingerprints, finishRequest, setRateLimit, mapHeaderAndTags diff --git a/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js b/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js index ed946633174..495179bd1fc 100644 --- a/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +++ b/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js @@ -94,6 +94,7 @@ class WAFContextWrapper { } Reporter.reportSchemas(result.derivatives) + Reporter.reportFingerprints(result.derivatives) if (wafRunFinished.hasSubscribers) { wafRunFinished.publish({ payload }) diff --git a/packages/dd-trace/test/appsec/attacker-fingerprinting-rules.json b/packages/dd-trace/test/appsec/attacker-fingerprinting-rules.json new file mode 100644 index 00000000000..722f9153ce4 --- /dev/null +++ b/packages/dd-trace/test/appsec/attacker-fingerprinting-rules.json @@ -0,0 +1,204 @@ +{ + "version": "2.2", + "metadata": { + "rules_version": "1.5.0" + }, + "rules": [ + { + "id": "tst-000-001-", + "name": "rule to test fingerprint", + "tags": { + "type": "attack_tool", + "category": "attack_attempt", + "confidence": "1" + }, + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.request.query" + } + ], + "list": [ + "testattack" + ] + }, + "operator": "phrase_match" + } + ], + "transformers": [] + } + ], + "processors": [ + { + "id": "http-endpoint-fingerprint", + "generator": "http_endpoint_fingerprint", + "conditions": [ + { + "operator": "exists", + "parameters": { + "inputs": [ + { + "address": "waf.context.event" + }, + { + "address": "server.business_logic.users.login.failure" + }, + { + "address": "server.business_logic.users.login.success" + } + ] + } + } + ], + "parameters": { + "mappings": [ + { + "method": [ + { + "address": "server.request.method" + } + ], + "uri_raw": [ + { + "address": "server.request.uri.raw" + } + ], + "body": [ + { + "address": "server.request.body" + } + ], + "query": [ + { + "address": "server.request.query" + } + ], + "output": "_dd.appsec.fp.http.endpoint" + } + ] + }, + "evaluate": false, + "output": true + }, + { + "id": "http-header-fingerprint", + "generator": "http_header_fingerprint", + "conditions": [ + { + "operator": "exists", + "parameters": { + "inputs": [ + { + "address": "waf.context.event" + }, + { + "address": "server.business_logic.users.login.failure" + }, + { + "address": "server.business_logic.users.login.success" + } + ] + } + } + ], + "parameters": { + "mappings": [ + { + "headers": [ + { + "address": "server.request.headers.no_cookies" + } + ], + "output": "_dd.appsec.fp.http.header" + } + ] + }, + "evaluate": false, + "output": true + }, + { + "id": "http-network-fingerprint", + "generator": "http_network_fingerprint", + "conditions": [ + { + "operator": "exists", + "parameters": { + "inputs": [ + { + "address": "waf.context.event" + }, + { + "address": "server.business_logic.users.login.failure" + }, + { + "address": "server.business_logic.users.login.success" + } + ] + } + } + ], + "parameters": { + "mappings": [ + { + "headers": [ + { + "address": "server.request.headers.no_cookies" + } + ], + "output": "_dd.appsec.fp.http.network" + } + ] + }, + "evaluate": false, + "output": true + }, + { + "id": "session-fingerprint", + "generator": "session_fingerprint", + "conditions": [ + { + "operator": "exists", + "parameters": { + "inputs": [ + { + "address": "waf.context.event" + }, + { + "address": "server.business_logic.users.login.failure" + }, + { + "address": "server.business_logic.users.login.success" + } + ] + } + } + ], + "parameters": { + "mappings": [ + { + "cookies": [ + { + "address": "server.request.cookies" + } + ], + "session_id": [ + { + "address": "usr.session_id" + } + ], + "user_id": [ + { + "address": "usr.id" + } + ], + "output": "_dd.appsec.fp.session" + } + ] + }, + "evaluate": false, + "output": true + } + ] +} diff --git a/packages/dd-trace/test/appsec/attacker-fingerprinting.spec.js b/packages/dd-trace/test/appsec/attacker-fingerprinting.spec.js new file mode 100644 index 00000000000..20eae2a8333 --- /dev/null +++ b/packages/dd-trace/test/appsec/attacker-fingerprinting.spec.js @@ -0,0 +1,69 @@ +'use strict' +// +// describe('Attacker fingerprinting', () => { +// // let config +// // +// // beforeEach(() => { +// // config = { +// // apiSecurity: { +// // enabled: true, +// // requestSampling: 1 +// // } +// // } +// // +// // sinon.stub(Math, 'random').returns(0.3) +// // }) +// // +// // afterEach(sinon.restore) +// +// describe('sampleRequest', () => { +// it('should sample request if enabled and sampling 1', () => { +// apiSecuritySampler.configure(config) +// +// expect(apiSecuritySampler.sampleRequest({})).to.true +// }) +// +// it('should not sample request if enabled and sampling 0', () => { +// config.apiSecurity.requestSampling = 0 +// apiSecuritySampler.configure(config) +// +// expect(apiSecuritySampler.sampleRequest({})).to.false +// }) +// +// it('should sample request if enabled and sampling greater than random', () => { +// config.apiSecurity.requestSampling = 0.5 +// +// apiSecuritySampler.configure(config) +// +// expect(apiSecuritySampler.sampleRequest({})).to.true +// }) +// +// it('should not sample request if enabled and sampling less than random', () => { +// config.apiSecurity.requestSampling = 0.1 +// +// apiSecuritySampler.configure(config) +// +// expect(apiSecuritySampler.sampleRequest()).to.false +// }) +// +// it('should not sample request if incorrect config value', () => { +// config.apiSecurity.requestSampling = NaN +// +// apiSecuritySampler.configure(config) +// +// expect(apiSecuritySampler.sampleRequest()).to.false +// }) +// +// it('should sample request according to the config', () => { +// config.apiSecurity.requestSampling = 1 +// +// apiSecuritySampler.configure(config) +// +// expect(apiSecuritySampler.sampleRequest({})).to.true +// +// apiSecuritySampler.setRequestSampling(0) +// +// expect(apiSecuritySampler.sampleRequest()).to.false +// }) +// }) +// }) diff --git a/packages/dd-trace/test/appsec/index.express.plugin.spec.js b/packages/dd-trace/test/appsec/index.express.plugin.spec.js index e8b0d4a50e4..2e7fced51a2 100644 --- a/packages/dd-trace/test/appsec/index.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/index.express.plugin.spec.js @@ -1,6 +1,7 @@ 'use strict' const axios = require('axios') +const { assert } = require('chai') const path = require('path') const agent = require('../plugins/agent') const appsec = require('../../src/appsec') @@ -186,4 +187,73 @@ withVersions('express', 'express', version => { }) }) }) + + describe('Attacker fingerprinting', () => { + let port, server + + before(() => { + return agent.load(['express', 'http'], { client: false }) + }) + + before((done) => { + const express = require('../../../../versions/express').get() + const bodyParser = require('../../../../versions/body-parser').get() + + const app = express() + app.use(bodyParser.json()) + + app.post('/', (req, res) => { + res.end('DONE') + }) + + server = app.listen(port, () => { + port = server.address().port + done() + }) + }) + + after(() => { + server.close() + return agent.close({ ritmReset: false }) + }) + + beforeEach(() => { + appsec.enable(new Config( + { + appsec: { + enabled: true, + rules: path.join(__dirname, 'attacker-fingerprinting-rules.json') + } + } + )) + }) + + afterEach(() => { + appsec.disable() + }) + + it('should report http fingerprints', async () => { + const res = await axios.post( + `http://localhost:${port}/?key=testattack`, + { + bodyParam: 'bodyValue' + }, + { + headers: { + headerName: 'headerValue', + 'x-real-ip': '255.255.255.255' + } + } + ) + await agent.use((traces) => { + const span = traces[0][0] + assert.property(span.meta, '_dd.appsec.fp.http.header') + assert.equal(span.meta['_dd.appsec.fp.http.header'], 'hdr-0110000110-6431a3e6-5-55682ec1') + assert.property(span.meta, '_dd.appsec.fp.http.network') + assert.equal(span.meta['_dd.appsec.fp.http.network'], 'net-1-0100000000') + assert.property(span.meta, '_dd.appsec.fp.http.endpoint') + assert.equal(span.meta['_dd.appsec.fp.http.endpoint'], 'http-post-8a5edab2-2c70e12b-be31090f') + }) + }) + }) }) diff --git a/packages/dd-trace/test/appsec/reporter.spec.js b/packages/dd-trace/test/appsec/reporter.spec.js index 6fabf747bcf..1700d32aa46 100644 --- a/packages/dd-trace/test/appsec/reporter.spec.js +++ b/packages/dd-trace/test/appsec/reporter.spec.js @@ -328,6 +328,10 @@ describe('reporter', () => { it('should call addTags', () => { const schemaValue = [{ key: [8] }] const derivatives = { + '_dd.appsec.fp.http.endpoint': 'endpoint_fingerprint', + '_dd.appsec.fp.http.header': 'header_fingerprint', + '_dd.appsec.fp.http.network': 'network_fingerprint', + '_dd.appsec.fp.session': 'session_fingerprint', '_dd.appsec.s.req.headers': schemaValue, '_dd.appsec.s.req.query': schemaValue, '_dd.appsec.s.req.params': schemaValue, @@ -350,6 +354,43 @@ describe('reporter', () => { }) }) + describe('reportFingerprints', () => { + it('should not call addTags if parameter is undefined', () => { + Reporter.reportFingerprints(undefined) + sinon.assert.notCalled(span.addTags) + }) + + it('should call addTags with an empty array', () => { + Reporter.reportFingerprints([]) + sinon.assert.calledOnceWithExactly(span.addTags, {}) + }) + + it('should call addTags', () => { + const schemaValue = [{ key: [8] }] + const derivatives = { + '_dd.appsec.fp.http.endpoint': 'endpoint_fingerprint', + '_dd.appsec.fp.http.header': 'header_fingerprint', + '_dd.appsec.fp.http.network': 'network_fingerprint', + '_dd.appsec.fp.session': 'session_fingerprint', + '_dd.appsec.s.req.headers': schemaValue, + '_dd.appsec.s.req.query': schemaValue, + '_dd.appsec.s.req.params': schemaValue, + '_dd.appsec.s.req.cookies': schemaValue, + '_dd.appsec.s.req.body': schemaValue, + 'custom.processor.output': schemaValue + } + + Reporter.reportFingerprints(derivatives) + + sinon.assert.calledOnceWithExactly(span.addTags,{ + '_dd.appsec.fp.http.endpoint': 'endpoint_fingerprint', + '_dd.appsec.fp.http.header': 'header_fingerprint', + '_dd.appsec.fp.http.network': 'network_fingerprint', + '_dd.appsec.fp.session': 'session_fingerprint' + }) + }) + }) + describe('finishRequest', () => { let wafContext diff --git a/packages/dd-trace/test/appsec/waf/index.spec.js b/packages/dd-trace/test/appsec/waf/index.spec.js index 816b3fe89c6..14fc8293bbc 100644 --- a/packages/dd-trace/test/appsec/waf/index.spec.js +++ b/packages/dd-trace/test/appsec/waf/index.spec.js @@ -49,6 +49,7 @@ describe('WAF Manager', () => { sinon.stub(Reporter, 'reportAttack') sinon.stub(Reporter, 'reportWafUpdate') sinon.stub(Reporter, 'reportSchemas') + sinon.stub(Reporter, 'reportFingerprints') webContext = {} sinon.stub(web, 'getContext').returns(webContext) @@ -406,6 +407,28 @@ describe('WAF Manager', () => { wafContextWrapper.run(params) expect(Reporter.reportSchemas).to.be.calledOnceWithExactly(result.derivatives) }) + + it('should report fingerprints when ddwafContext returns fingerprints in results derivatives', () => { + const result = { + totalRuntime: 1, + durationExt: 1, + derivatives: { + '_dd.appsec.s.req.body': [8], + '_dd.appsec.fp.http.endpoint': 'http-post-abcdefgh-12345678-abcdefgh', + '_dd.appsec.fp.http.network': 'net-1-0100000000', + '_dd.appsec.fp.http.headers': 'hdr-0110000110-abcdefgh-5-12345678', + } + } + + ddwafContext.run.returns(result) + + wafContextWrapper.run({ + persistent: { + 'server.request.body': 'foo' + } + }) + sinon.assert.calledOnceWithExactly(Reporter.reportFingerprints, result.derivatives) + }) }) }) }) From bac96ceca133e4adac1b4a54800662cf314500bb Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Fri, 13 Sep 2024 12:07:53 +0200 Subject: [PATCH 02/16] WAF fingerprint RC capabilities --- .../src/appsec/remote_config/capabilities.js | 6 +++- .../src/appsec/remote_config/index.js | 6 ++++ .../test/appsec/remote_config/index.spec.js | 30 +++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/dd-trace/src/appsec/remote_config/capabilities.js b/packages/dd-trace/src/appsec/remote_config/capabilities.js index 05dc96233fd..634112f8d9f 100644 --- a/packages/dd-trace/src/appsec/remote_config/capabilities.js +++ b/packages/dd-trace/src/appsec/remote_config/capabilities.js @@ -20,5 +20,9 @@ module.exports = { ASM_RASP_SQLI: 1n << 21n, ASM_RASP_SSRF: 1n << 23n, ASM_RASP_LFI: 1n << 24n, - APM_TRACING_SAMPLE_RULES: 1n << 29n + APM_TRACING_SAMPLE_RULES: 1n << 29n, + ASM_ENDPOINT_FINGERPRINT: 1n << 32n, + ASM_SESSION_FINGERPRINT: 1n << 33n, + ASM_NETWORK_FINGERPRINT: 1n << 34n, + ASM_HEADER_FINGERPRINT: 1n << 35n } diff --git a/packages/dd-trace/src/appsec/remote_config/index.js b/packages/dd-trace/src/appsec/remote_config/index.js index 28772c60c2e..2b7eea57c82 100644 --- a/packages/dd-trace/src/appsec/remote_config/index.js +++ b/packages/dd-trace/src/appsec/remote_config/index.js @@ -75,6 +75,9 @@ function enableWafUpdate (appsecConfig) { rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_RULES, true) rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, true) rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRUSTED_IPS, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_ENDPOINT_FINGERPRINT, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_NETWORK_FINGERPRINT, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_HEADER_FINGERPRINT, true) if (appsecConfig.rasp?.enabled) { rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SQLI, true) @@ -104,6 +107,9 @@ function disableWafUpdate () { rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_RULES, false) rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, false) rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRUSTED_IPS, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_ENDPOINT_FINGERPRINT, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_NETWORK_FINGERPRINT, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_HEADER_FINGERPRINT, false) rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SQLI, false) rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SSRF, false) diff --git a/packages/dd-trace/test/appsec/remote_config/index.spec.js b/packages/dd-trace/test/appsec/remote_config/index.spec.js index c3da43a17c0..dbd710d6a4e 100644 --- a/packages/dd-trace/test/appsec/remote_config/index.spec.js +++ b/packages/dd-trace/test/appsec/remote_config/index.spec.js @@ -286,6 +286,12 @@ describe('Remote Config index', () => { .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, true) expect(rc.updateCapabilities) .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_TRUSTED_IPS, true) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_ENDPOINT_FINGERPRINT, true) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_NETWORK_FINGERPRINT, true) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_HEADER_FINGERPRINT, true) expect(rc.updateCapabilities) .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_RASP_SSRF, true) expect(rc.updateCapabilities) @@ -322,6 +328,12 @@ describe('Remote Config index', () => { .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, true) expect(rc.updateCapabilities) .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_TRUSTED_IPS, true) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_ENDPOINT_FINGERPRINT, true) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_NETWORK_FINGERPRINT, true) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_HEADER_FINGERPRINT, true) expect(rc.updateCapabilities) .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_RASP_SSRF, true) expect(rc.updateCapabilities) @@ -360,6 +372,12 @@ describe('Remote Config index', () => { .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, true) expect(rc.updateCapabilities) .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_TRUSTED_IPS, true) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_ENDPOINT_FINGERPRINT, true) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_NETWORK_FINGERPRINT, true) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_HEADER_FINGERPRINT, true) expect(rc.updateCapabilities) .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_RASP_SSRF, true) expect(rc.updateCapabilities) @@ -393,6 +411,12 @@ describe('Remote Config index', () => { .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, true) expect(rc.updateCapabilities) .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_TRUSTED_IPS, true) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_ENDPOINT_FINGERPRINT, true) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_NETWORK_FINGERPRINT, true) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_HEADER_FINGERPRINT, true) expect(rc.updateCapabilities) .to.not.have.been.calledWith(RemoteConfigCapabilities.ASM_RASP_SSRF) expect(rc.updateCapabilities) @@ -426,6 +450,12 @@ describe('Remote Config index', () => { .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, false) expect(rc.updateCapabilities) .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_TRUSTED_IPS, false) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_ENDPOINT_FINGERPRINT, false) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_NETWORK_FINGERPRINT, false) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_HEADER_FINGERPRINT, false) expect(rc.updateCapabilities) .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_RASP_SSRF, false) expect(rc.updateCapabilities) From bf97dedcf179c77a7f3a8195daad0946a0c7207e Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Wed, 18 Sep 2024 07:34:28 +0200 Subject: [PATCH 03/16] Linting --- packages/dd-trace/src/appsec/reporter.js | 2 +- packages/dd-trace/test/appsec/index.express.plugin.spec.js | 2 +- packages/dd-trace/test/appsec/reporter.spec.js | 2 +- packages/dd-trace/test/appsec/waf/index.spec.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/dd-trace/src/appsec/reporter.js b/packages/dd-trace/src/appsec/reporter.js index dd5830d6395..0e6e92b4e0a 100644 --- a/packages/dd-trace/src/appsec/reporter.js +++ b/packages/dd-trace/src/appsec/reporter.js @@ -258,7 +258,7 @@ function setRateLimit (rateLimit) { limiter = new Limiter(rateLimit) } -function isFingerprintDerivative(derivative) { +function isFingerprintDerivative (derivative) { return derivative.startsWith('_dd.appsec.fp') } diff --git a/packages/dd-trace/test/appsec/index.express.plugin.spec.js b/packages/dd-trace/test/appsec/index.express.plugin.spec.js index 2e7fced51a2..e86868d9c69 100644 --- a/packages/dd-trace/test/appsec/index.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/index.express.plugin.spec.js @@ -233,7 +233,7 @@ withVersions('express', 'express', version => { }) it('should report http fingerprints', async () => { - const res = await axios.post( + await axios.post( `http://localhost:${port}/?key=testattack`, { bodyParam: 'bodyValue' diff --git a/packages/dd-trace/test/appsec/reporter.spec.js b/packages/dd-trace/test/appsec/reporter.spec.js index 1700d32aa46..0dbcda610bc 100644 --- a/packages/dd-trace/test/appsec/reporter.spec.js +++ b/packages/dd-trace/test/appsec/reporter.spec.js @@ -382,7 +382,7 @@ describe('reporter', () => { Reporter.reportFingerprints(derivatives) - sinon.assert.calledOnceWithExactly(span.addTags,{ + sinon.assert.calledOnceWithExactly(span.addTags, { '_dd.appsec.fp.http.endpoint': 'endpoint_fingerprint', '_dd.appsec.fp.http.header': 'header_fingerprint', '_dd.appsec.fp.http.network': 'network_fingerprint', diff --git a/packages/dd-trace/test/appsec/waf/index.spec.js b/packages/dd-trace/test/appsec/waf/index.spec.js index 14fc8293bbc..c4596b759dc 100644 --- a/packages/dd-trace/test/appsec/waf/index.spec.js +++ b/packages/dd-trace/test/appsec/waf/index.spec.js @@ -416,7 +416,7 @@ describe('WAF Manager', () => { '_dd.appsec.s.req.body': [8], '_dd.appsec.fp.http.endpoint': 'http-post-abcdefgh-12345678-abcdefgh', '_dd.appsec.fp.http.network': 'net-1-0100000000', - '_dd.appsec.fp.http.headers': 'hdr-0110000110-abcdefgh-5-12345678', + '_dd.appsec.fp.http.headers': 'hdr-0110000110-abcdefgh-5-12345678' } } From 3cb33432caf09cbee6a3b5c90ba575f68fe65a30 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Wed, 18 Sep 2024 09:08:15 +0200 Subject: [PATCH 04/16] Remove useless file --- .../appsec/attacker-fingerprinting.spec.js | 69 ------------------- 1 file changed, 69 deletions(-) delete mode 100644 packages/dd-trace/test/appsec/attacker-fingerprinting.spec.js diff --git a/packages/dd-trace/test/appsec/attacker-fingerprinting.spec.js b/packages/dd-trace/test/appsec/attacker-fingerprinting.spec.js deleted file mode 100644 index 20eae2a8333..00000000000 --- a/packages/dd-trace/test/appsec/attacker-fingerprinting.spec.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict' -// -// describe('Attacker fingerprinting', () => { -// // let config -// // -// // beforeEach(() => { -// // config = { -// // apiSecurity: { -// // enabled: true, -// // requestSampling: 1 -// // } -// // } -// // -// // sinon.stub(Math, 'random').returns(0.3) -// // }) -// // -// // afterEach(sinon.restore) -// -// describe('sampleRequest', () => { -// it('should sample request if enabled and sampling 1', () => { -// apiSecuritySampler.configure(config) -// -// expect(apiSecuritySampler.sampleRequest({})).to.true -// }) -// -// it('should not sample request if enabled and sampling 0', () => { -// config.apiSecurity.requestSampling = 0 -// apiSecuritySampler.configure(config) -// -// expect(apiSecuritySampler.sampleRequest({})).to.false -// }) -// -// it('should sample request if enabled and sampling greater than random', () => { -// config.apiSecurity.requestSampling = 0.5 -// -// apiSecuritySampler.configure(config) -// -// expect(apiSecuritySampler.sampleRequest({})).to.true -// }) -// -// it('should not sample request if enabled and sampling less than random', () => { -// config.apiSecurity.requestSampling = 0.1 -// -// apiSecuritySampler.configure(config) -// -// expect(apiSecuritySampler.sampleRequest()).to.false -// }) -// -// it('should not sample request if incorrect config value', () => { -// config.apiSecurity.requestSampling = NaN -// -// apiSecuritySampler.configure(config) -// -// expect(apiSecuritySampler.sampleRequest()).to.false -// }) -// -// it('should sample request according to the config', () => { -// config.apiSecurity.requestSampling = 1 -// -// apiSecuritySampler.configure(config) -// -// expect(apiSecuritySampler.sampleRequest({})).to.true -// -// apiSecuritySampler.setRequestSampling(0) -// -// expect(apiSecuritySampler.sampleRequest()).to.false -// }) -// }) -// }) From 8f69258d735d0abb9af4fa6c8bee9b77113dc75e Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Wed, 18 Sep 2024 09:09:28 +0200 Subject: [PATCH 05/16] Add blank line --- packages/dd-trace/test/appsec/index.express.plugin.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dd-trace/test/appsec/index.express.plugin.spec.js b/packages/dd-trace/test/appsec/index.express.plugin.spec.js index e86868d9c69..92231578129 100644 --- a/packages/dd-trace/test/appsec/index.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/index.express.plugin.spec.js @@ -245,6 +245,7 @@ withVersions('express', 'express', version => { } } ) + await agent.use((traces) => { const span = traces[0][0] assert.property(span.meta, '_dd.appsec.fp.http.header') From 8fa6fa4596fc97b9ffddcb813dc80fd40975ce82 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Wed, 18 Sep 2024 09:09:58 +0200 Subject: [PATCH 06/16] Remove unused capability --- packages/dd-trace/src/appsec/remote_config/capabilities.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dd-trace/src/appsec/remote_config/capabilities.js b/packages/dd-trace/src/appsec/remote_config/capabilities.js index 634112f8d9f..97965fb1203 100644 --- a/packages/dd-trace/src/appsec/remote_config/capabilities.js +++ b/packages/dd-trace/src/appsec/remote_config/capabilities.js @@ -22,7 +22,6 @@ module.exports = { ASM_RASP_LFI: 1n << 24n, APM_TRACING_SAMPLE_RULES: 1n << 29n, ASM_ENDPOINT_FINGERPRINT: 1n << 32n, - ASM_SESSION_FINGERPRINT: 1n << 33n, ASM_NETWORK_FINGERPRINT: 1n << 34n, ASM_HEADER_FINGERPRINT: 1n << 35n } From 22985fe30ff561f0c5742cfd76fcf8db75ae9477 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Tue, 1 Oct 2024 08:36:12 +0200 Subject: [PATCH 07/16] Generate fingerprint on user login events --- .../dd-trace/src/appsec/sdk/track_event.js | 5 + ...cker-fingerprinting.express.plugin.spec.js | 79 +++++++++++++ ...ingerprinting.passport-http.plugin.spec.js | 107 ++++++++++++++++++ ...ngerprinting.passport-local.plugin.spec.js | 105 +++++++++++++++++ .../appsec/attacker-fingerprinting.spec.js | 83 ++++++++++++++ .../test/appsec/index.express.plugin.spec.js | 70 ------------ .../test/appsec/sdk/track_event.spec.js | 24 +++- 7 files changed, 402 insertions(+), 71 deletions(-) create mode 100644 packages/dd-trace/test/appsec/attacker-fingerprinting.express.plugin.spec.js create mode 100644 packages/dd-trace/test/appsec/attacker-fingerprinting.passport-http.plugin.spec.js create mode 100644 packages/dd-trace/test/appsec/attacker-fingerprinting.passport-local.plugin.spec.js create mode 100644 packages/dd-trace/test/appsec/attacker-fingerprinting.spec.js diff --git a/packages/dd-trace/src/appsec/sdk/track_event.js b/packages/dd-trace/src/appsec/sdk/track_event.js index 61500e2cfbe..0d5eca6d121 100644 --- a/packages/dd-trace/src/appsec/sdk/track_event.js +++ b/packages/dd-trace/src/appsec/sdk/track_event.js @@ -5,6 +5,7 @@ const { getRootSpan } = require('./utils') const { MANUAL_KEEP } = require('../../../../../ext/tags') const { setUserTags } = require('./set_user') const standalone = require('../standalone') +const waf = require('../waf') function trackUserLoginSuccessEvent (tracer, user, metadata) { // TODO: better user check here and in _setUser() ? @@ -76,6 +77,10 @@ function trackEvent (eventName, fields, sdkMethodName, rootSpan, mode) { rootSpan.addTags(tags) standalone.sample(rootSpan) + + if (['users.login.success', 'users.login.failure'].includes(eventName)) { + waf.run({ persistent: { [`server.business_logic.${eventName}`]: null }}) + } } module.exports = { diff --git a/packages/dd-trace/test/appsec/attacker-fingerprinting.express.plugin.spec.js b/packages/dd-trace/test/appsec/attacker-fingerprinting.express.plugin.spec.js new file mode 100644 index 00000000000..bc7c918965c --- /dev/null +++ b/packages/dd-trace/test/appsec/attacker-fingerprinting.express.plugin.spec.js @@ -0,0 +1,79 @@ +'use strict' + +const axios = require('axios') +const { assert } = require('chai') +const path = require('path') + +const agent = require('../plugins/agent') +const appsec = require('../../src/appsec') +const Config = require('../../src/config') + +describe('Attacker fingerprinting', () => { + let port, server + + before(() => { + return agent.load(['express', 'http'], { client: false }) + }) + + before((done) => { + const express = require('../../../../versions/express').get() + const bodyParser = require('../../../../versions/body-parser').get() + + const app = express() + app.use(bodyParser.json()) + + app.post('/', (req, res) => { + res.end('DONE') + }) + + server = app.listen(port, () => { + port = server.address().port + done() + }) + }) + + after(() => { + server.close() + return agent.close({ ritmReset: false }) + }) + + beforeEach(() => { + appsec.enable(new Config( + { + appsec: { + enabled: true, + rules: path.join(__dirname, 'attacker-fingerprinting-rules.json') + } + } + )) + }) + + afterEach(() => { + appsec.disable() + }) + + it('should report http fingerprints', async () => { + await axios.post( + `http://localhost:${port}/?key=testattack`, + { + bodyParam: 'bodyValue' + }, + { + headers: { + headerName: 'headerValue', + 'x-real-ip': '255.255.255.255' + } + } + ) + + await agent.use((traces) => { + const span = traces[0][0] + assert.property(span.meta, '_dd.appsec.fp.http.header') + assert.equal(span.meta['_dd.appsec.fp.http.header'], 'hdr-0110000110-6431a3e6-5-55682ec1') + assert.property(span.meta, '_dd.appsec.fp.http.network') + assert.equal(span.meta['_dd.appsec.fp.http.network'], 'net-1-0100000000') + assert.property(span.meta, '_dd.appsec.fp.http.endpoint') + assert.equal(span.meta['_dd.appsec.fp.http.endpoint'], 'http-post-8a5edab2-2c70e12b-be31090f') + }) + }) +}) diff --git a/packages/dd-trace/test/appsec/attacker-fingerprinting.passport-http.plugin.spec.js b/packages/dd-trace/test/appsec/attacker-fingerprinting.passport-http.plugin.spec.js new file mode 100644 index 00000000000..2cf1444119a --- /dev/null +++ b/packages/dd-trace/test/appsec/attacker-fingerprinting.passport-http.plugin.spec.js @@ -0,0 +1,107 @@ +'use strict' + +const Axios = require('axios') +const { assert } = require('chai') + +const agent = require('../plugins/agent') +const appsec = require('../../src/appsec') +const Config = require('../../src/config') + +function assertFingerprintInTraces (traces) { + const span = traces[0][0] + assert.property(span.meta, '_dd.appsec.fp.http.header') + assert.equal(span.meta['_dd.appsec.fp.http.header'], 'hdr-0110000110-6431a3e6-5-e58aa9dd') + assert.property(span.meta, '_dd.appsec.fp.http.network') + assert.equal(span.meta['_dd.appsec.fp.http.network'], 'net-0-0000000000') + assert.property(span.meta, '_dd.appsec.fp.http.endpoint') + assert.equal(span.meta['_dd.appsec.fp.http.endpoint'], 'http-post-7e93fba0--') +} + +withVersions('passport-http', 'passport-http', version => { + describe('Attacker fingerprinting', () => { + let port, server, axios + + before(() => { + return agent.load(['express', 'http'], {client: false}) + }) + + before(() => { + appsec.enable(new Config({ + appsec: true + })) + }) + + before((done) => { + const express = require('../../../../versions/express').get() + const bodyParser = require('../../../../versions/body-parser').get() + const passport = require('../../../../versions/passport').get() + const { BasicStrategy } = require(`../../../../versions/passport-http@${version}`).get() + + const app = express() + app.use(bodyParser.json()) + app.use(passport.initialize()) + + passport.use(new BasicStrategy( + function verify (username, password, done) { + if (username === 'success') { + done(null, { + id: 1234, + username + }) + } else { + done(null, false) + } + } + )); + + app.post('/login', passport.authenticate('basic', { session:false }), function (req, res) { + res.end() + }) + + server = app.listen(port, () => { + port = server.address().port + axios = Axios.create({ + baseURL: `http://localhost:${port}` + }) + done() + }) + }) + + after(() => { + server.close() + return agent.close({ritmReset: false}) + }) + + after(() => { + appsec.disable() + }) + + it('should report http fingerprints on login fail', async () => { + try { + await axios.post( + `http://localhost:${port}/login`, {}, { + auth: { + username: 'fail', + password: '1234' + } + } + ) + } catch (e) {} + + await agent.use(assertFingerprintInTraces) + }) + + it('should report http fingerprints on login successful', async () => { + await axios.post( + `http://localhost:${port}/login`, {}, { + auth: { + username: 'success', + password: '1234' + } + } + ) + + await agent.use(assertFingerprintInTraces) + }) + }) +}) diff --git a/packages/dd-trace/test/appsec/attacker-fingerprinting.passport-local.plugin.spec.js b/packages/dd-trace/test/appsec/attacker-fingerprinting.passport-local.plugin.spec.js new file mode 100644 index 00000000000..a901908c2e2 --- /dev/null +++ b/packages/dd-trace/test/appsec/attacker-fingerprinting.passport-local.plugin.spec.js @@ -0,0 +1,105 @@ +'use strict' + +const Axios = require('axios') +const { assert } = require('chai') + +const agent = require('../plugins/agent') +const appsec = require('../../src/appsec') +const Config = require('../../src/config') + +function assertFingerprintInTraces (traces) { + const span = traces[0][0] + assert.property(span.meta, '_dd.appsec.fp.http.header') + assert.equal(span.meta['_dd.appsec.fp.http.header'], 'hdr-0110000110-6431a3e6-4-c348f529') + assert.property(span.meta, '_dd.appsec.fp.http.network') + assert.equal(span.meta['_dd.appsec.fp.http.network'], 'net-0-0000000000') + assert.property(span.meta, '_dd.appsec.fp.http.endpoint') + assert.equal(span.meta['_dd.appsec.fp.http.endpoint'], 'http-post-7e93fba0--f29f6224') +} + +withVersions('passport-local', 'passport-local', version => { + describe('Attacker fingerprinting', () => { + let port, server, axios + + before(() => { + return agent.load(['express', 'http'], {client: false}) + }) + + before(() => { + appsec.enable(new Config({ + appsec: true + })) + }) + + before((done) => { + const express = require('../../../../versions/express').get() + const bodyParser = require('../../../../versions/body-parser').get() + const passport = require('../../../../versions/passport').get() + const LocalStrategy = require(`../../../../versions/passport-local@${version}`).get() + + const app = express() + app.use(bodyParser.json()) + app.use(passport.initialize()) + + passport.use(new LocalStrategy( + function verify (username, password, done) { + if (username === 'success') { + done(null, { + id: 1234, + username + }) + } else { + done(null, false) + } + } + )); + + app.post('/login', passport.authenticate('local', {session:false}), function (req, res) { + res.end() + }) + + server = app.listen(port, () => { + port = server.address().port + axios = Axios.create({ + baseURL: `http://localhost:${port}` + }) + done() + }) + }) + + after(() => { + server.close() + return agent.close({ritmReset: false}) + }) + + after(() => { + appsec.disable() + }) + + it('should report http fingerprints on login fail', async () => { + try { + await axios.post( + `http://localhost:${port}/login`, + { + username: 'fail', + password: '1234' + } + ) + } catch (e) {} + + await agent.use(assertFingerprintInTraces) + }) + + it('should report http fingerprints on login successful', async () => { + await axios.post( + `http://localhost:${port}/login`, + { + username: 'success', + password: '1234' + } + ) + + await agent.use(assertFingerprintInTraces) + }) + }) +}) diff --git a/packages/dd-trace/test/appsec/attacker-fingerprinting.spec.js b/packages/dd-trace/test/appsec/attacker-fingerprinting.spec.js new file mode 100644 index 00000000000..f645ab3d4ec --- /dev/null +++ b/packages/dd-trace/test/appsec/attacker-fingerprinting.spec.js @@ -0,0 +1,83 @@ +'use strict' + +const axios = require('axios') +const { assert } = require('chai') +const agent = require('../plugins/agent') +const tracer = require('../../../../index') +const appsec = require('../../src/appsec') +const Config = require('../../src/config') + +describe('Attacker fingerprinting', () => { + describe('SDK', () => { + let http + let controller + let appListener + let port + + function listener (req, res) { + if (controller) { + controller(req, res) + } + } + + before(() => { + appsec.enable(new Config({ + enabled: true + })) + }) + + before(async () => { + await agent.load('http') + http = require('http') + }) + + before(done => { + const server = new http.Server(listener) + appListener = server + .listen(port, 'localhost', () => { + port = appListener.address().port + done() + }) + }) + + after(() => { + appListener.close() + appsec.disable() + return agent.close({ ritmReset: false }) + }) + + it('should provide fingerprinting on successful user login track', (done) => { + controller = (req, res) => { + tracer.appsec.trackUserLoginSuccessEvent({ + id: 'test_user_id' + }, { metakey: 'metaValue' }) + res.end() + } + agent.use(traces => { + assert.property(traces[0][0].meta, '_dd.appsec.fp.http.header') + assert.equal(traces[0][0].meta['_dd.appsec.fp.http.header'], 'hdr-0110000010-6431a3e6-3-98425651') + assert.property(traces[0][0].meta, '_dd.appsec.fp.http.network') + assert.equal(traces[0][0].meta['_dd.appsec.fp.http.network'], 'net-0-0000000000') + }) + .then(done) + .catch(done) + axios.get(`http://localhost:${port}/`) + }) + + it('should provide fingerprinting on failed user login track', (done) => { + controller = (req, res) => { + tracer.appsec.trackUserLoginFailureEvent('test_user_id', true, { metakey: 'metaValue' }) + res.end() + } + agent.use(traces => { + assert.property(traces[0][0].meta, '_dd.appsec.fp.http.header') + assert.equal(traces[0][0].meta['_dd.appsec.fp.http.header'], 'hdr-0110000010-6431a3e6-3-98425651') + assert.property(traces[0][0].meta, '_dd.appsec.fp.http.network') + assert.equal(traces[0][0].meta['_dd.appsec.fp.http.network'], 'net-0-0000000000') + }) + .then(done) + .catch(done) + axios.get(`http://localhost:${port}/`) + }) + }) +}) diff --git a/packages/dd-trace/test/appsec/index.express.plugin.spec.js b/packages/dd-trace/test/appsec/index.express.plugin.spec.js index 92231578129..0c38267c648 100644 --- a/packages/dd-trace/test/appsec/index.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/index.express.plugin.spec.js @@ -187,74 +187,4 @@ withVersions('express', 'express', version => { }) }) }) - - describe('Attacker fingerprinting', () => { - let port, server - - before(() => { - return agent.load(['express', 'http'], { client: false }) - }) - - before((done) => { - const express = require('../../../../versions/express').get() - const bodyParser = require('../../../../versions/body-parser').get() - - const app = express() - app.use(bodyParser.json()) - - app.post('/', (req, res) => { - res.end('DONE') - }) - - server = app.listen(port, () => { - port = server.address().port - done() - }) - }) - - after(() => { - server.close() - return agent.close({ ritmReset: false }) - }) - - beforeEach(() => { - appsec.enable(new Config( - { - appsec: { - enabled: true, - rules: path.join(__dirname, 'attacker-fingerprinting-rules.json') - } - } - )) - }) - - afterEach(() => { - appsec.disable() - }) - - it('should report http fingerprints', async () => { - await axios.post( - `http://localhost:${port}/?key=testattack`, - { - bodyParam: 'bodyValue' - }, - { - headers: { - headerName: 'headerValue', - 'x-real-ip': '255.255.255.255' - } - } - ) - - await agent.use((traces) => { - const span = traces[0][0] - assert.property(span.meta, '_dd.appsec.fp.http.header') - assert.equal(span.meta['_dd.appsec.fp.http.header'], 'hdr-0110000110-6431a3e6-5-55682ec1') - assert.property(span.meta, '_dd.appsec.fp.http.network') - assert.equal(span.meta['_dd.appsec.fp.http.network'], 'net-1-0100000000') - assert.property(span.meta, '_dd.appsec.fp.http.endpoint') - assert.equal(span.meta['_dd.appsec.fp.http.endpoint'], 'http-post-8a5edab2-2c70e12b-be31090f') - }) - }) - }) }) diff --git a/packages/dd-trace/test/appsec/sdk/track_event.spec.js b/packages/dd-trace/test/appsec/sdk/track_event.spec.js index acc5db1e905..eee74f553e9 100644 --- a/packages/dd-trace/test/appsec/sdk/track_event.spec.js +++ b/packages/dd-trace/test/appsec/sdk/track_event.spec.js @@ -14,6 +14,7 @@ describe('track_event', () => { let setUserTags let trackUserLoginSuccessEvent, trackUserLoginFailureEvent, trackCustomEvent, trackEvent let sample + let waf beforeEach(() => { log = { @@ -30,6 +31,10 @@ describe('track_event', () => { sample = sinon.stub() + waf = { + run: sinon.spy() + } + const trackEvents = proxyquire('../../../src/appsec/sdk/track_event', { '../../log': log, './utils': { @@ -40,7 +45,8 @@ describe('track_event', () => { }, '../standalone': { sample - } + }, + '../waf': waf }) trackUserLoginSuccessEvent = trackEvents.trackUserLoginSuccessEvent @@ -49,6 +55,10 @@ describe('track_event', () => { trackEvent = trackEvents.trackEvent }) + afterEach(() => { + sinon.restore() + }) + describe('trackUserLoginSuccessEvent', () => { it('should log warning when passed invalid user', () => { trackUserLoginSuccessEvent(tracer, null, { key: 'value' }) @@ -106,6 +116,13 @@ describe('track_event', () => { '_dd.appsec.events.users.login.success.sdk': 'true' }) }) + + it('should call waf run with login success address', () => { + const user = { id: 'user_id' } + + trackUserLoginSuccessEvent(tracer, user) + sinon.assert.calledOnceWithExactly(waf.run, { persistent: { 'server.business_logic.users.login.success': null }}) + }) }) describe('trackUserLoginFailureEvent', () => { @@ -182,6 +199,11 @@ describe('track_event', () => { 'appsec.events.users.login.failure.usr.exists': 'true' }) }) + + it('should call waf run with login failure address', () => { + trackUserLoginFailureEvent(tracer, 'user_id') + sinon.assert.calledOnceWithExactly(waf.run, { persistent: { 'server.business_logic.users.login.failure': null }}) + }) }) describe('trackCustomEvent', () => { From 0b2454eff27687a314bae066c1137770cc11c6e7 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Tue, 1 Oct 2024 16:01:20 +0200 Subject: [PATCH 08/16] Fix linting --- packages/dd-trace/src/appsec/sdk/track_event.js | 2 +- ...ttacker-fingerprinting.passport-http.plugin.spec.js | 8 ++++---- ...tacker-fingerprinting.passport-local.plugin.spec.js | 8 ++++---- .../dd-trace/test/appsec/index.express.plugin.spec.js | 1 - packages/dd-trace/test/appsec/sdk/track_event.spec.js | 10 ++++++++-- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/dd-trace/src/appsec/sdk/track_event.js b/packages/dd-trace/src/appsec/sdk/track_event.js index 0d5eca6d121..36c40093b19 100644 --- a/packages/dd-trace/src/appsec/sdk/track_event.js +++ b/packages/dd-trace/src/appsec/sdk/track_event.js @@ -79,7 +79,7 @@ function trackEvent (eventName, fields, sdkMethodName, rootSpan, mode) { standalone.sample(rootSpan) if (['users.login.success', 'users.login.failure'].includes(eventName)) { - waf.run({ persistent: { [`server.business_logic.${eventName}`]: null }}) + waf.run({ persistent: { [`server.business_logic.${eventName}`]: null } }) } } diff --git a/packages/dd-trace/test/appsec/attacker-fingerprinting.passport-http.plugin.spec.js b/packages/dd-trace/test/appsec/attacker-fingerprinting.passport-http.plugin.spec.js index 2cf1444119a..58b54e2c704 100644 --- a/packages/dd-trace/test/appsec/attacker-fingerprinting.passport-http.plugin.spec.js +++ b/packages/dd-trace/test/appsec/attacker-fingerprinting.passport-http.plugin.spec.js @@ -22,7 +22,7 @@ withVersions('passport-http', 'passport-http', version => { let port, server, axios before(() => { - return agent.load(['express', 'http'], {client: false}) + return agent.load(['express', 'http'], { client: false }) }) before(() => { @@ -52,9 +52,9 @@ withVersions('passport-http', 'passport-http', version => { done(null, false) } } - )); + )) - app.post('/login', passport.authenticate('basic', { session:false }), function (req, res) { + app.post('/login', passport.authenticate('basic', { session: false }), function (req, res) { res.end() }) @@ -69,7 +69,7 @@ withVersions('passport-http', 'passport-http', version => { after(() => { server.close() - return agent.close({ritmReset: false}) + return agent.close({ ritmReset: false }) }) after(() => { diff --git a/packages/dd-trace/test/appsec/attacker-fingerprinting.passport-local.plugin.spec.js b/packages/dd-trace/test/appsec/attacker-fingerprinting.passport-local.plugin.spec.js index a901908c2e2..b51aa57de9c 100644 --- a/packages/dd-trace/test/appsec/attacker-fingerprinting.passport-local.plugin.spec.js +++ b/packages/dd-trace/test/appsec/attacker-fingerprinting.passport-local.plugin.spec.js @@ -22,7 +22,7 @@ withVersions('passport-local', 'passport-local', version => { let port, server, axios before(() => { - return agent.load(['express', 'http'], {client: false}) + return agent.load(['express', 'http'], { client: false }) }) before(() => { @@ -52,9 +52,9 @@ withVersions('passport-local', 'passport-local', version => { done(null, false) } } - )); + )) - app.post('/login', passport.authenticate('local', {session:false}), function (req, res) { + app.post('/login', passport.authenticate('local', { session: false }), function (req, res) { res.end() }) @@ -69,7 +69,7 @@ withVersions('passport-local', 'passport-local', version => { after(() => { server.close() - return agent.close({ritmReset: false}) + return agent.close({ ritmReset: false }) }) after(() => { diff --git a/packages/dd-trace/test/appsec/index.express.plugin.spec.js b/packages/dd-trace/test/appsec/index.express.plugin.spec.js index 0c38267c648..e8b0d4a50e4 100644 --- a/packages/dd-trace/test/appsec/index.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/index.express.plugin.spec.js @@ -1,7 +1,6 @@ 'use strict' const axios = require('axios') -const { assert } = require('chai') const path = require('path') const agent = require('../plugins/agent') const appsec = require('../../src/appsec') diff --git a/packages/dd-trace/test/appsec/sdk/track_event.spec.js b/packages/dd-trace/test/appsec/sdk/track_event.spec.js index eee74f553e9..e9f460eb23b 100644 --- a/packages/dd-trace/test/appsec/sdk/track_event.spec.js +++ b/packages/dd-trace/test/appsec/sdk/track_event.spec.js @@ -121,7 +121,10 @@ describe('track_event', () => { const user = { id: 'user_id' } trackUserLoginSuccessEvent(tracer, user) - sinon.assert.calledOnceWithExactly(waf.run, { persistent: { 'server.business_logic.users.login.success': null }}) + sinon.assert.calledOnceWithExactly( + waf.run, + { persistent: { 'server.business_logic.users.login.success': null } } + ) }) }) @@ -202,7 +205,10 @@ describe('track_event', () => { it('should call waf run with login failure address', () => { trackUserLoginFailureEvent(tracer, 'user_id') - sinon.assert.calledOnceWithExactly(waf.run, { persistent: { 'server.business_logic.users.login.failure': null }}) + sinon.assert.calledOnceWithExactly( + waf.run, + { persistent: { 'server.business_logic.users.login.failure': null } } + ) }) }) From 5de5f0a0e46b684efba9e0191cb67e9fd87574d6 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Tue, 1 Oct 2024 16:01:41 +0200 Subject: [PATCH 09/16] Add passport plugin test to GHA --- .github/workflows/appsec.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/appsec.yml b/.github/workflows/appsec.yml index 19470023010..f41b18f9d53 100644 --- a/.github/workflows/appsec.yml +++ b/.github/workflows/appsec.yml @@ -250,3 +250,17 @@ jobs: - run: yarn test:integration:appsec - uses: ./.github/actions/node/latest - run: yarn test:integration:appsec + + passport: + runs-on: ubuntu-latest + env: + PLUGINS: passport-local|passport-http + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/node/setup + - uses: ./.github/actions/install + - uses: ./.github/actions/node/oldest + - run: yarn test:appsec:plugins:ci + - uses: ./.github/actions/node/latest + - run: yarn test:appsec:plugins:ci + - uses: codecov/codecov-action@v3 From b7b653b1855e17493ccb8ce78f96baa828cc1a7e Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Tue, 1 Oct 2024 16:07:45 +0200 Subject: [PATCH 10/16] Add business logic addressses --- packages/dd-trace/src/appsec/addresses.js | 5 ++++- packages/dd-trace/test/appsec/sdk/track_event.spec.js | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/dd-trace/src/appsec/addresses.js b/packages/dd-trace/src/appsec/addresses.js index f8ce3033d36..40c643012ef 100644 --- a/packages/dd-trace/src/appsec/addresses.js +++ b/packages/dd-trace/src/appsec/addresses.js @@ -26,5 +26,8 @@ module.exports = { FS_OPERATION_PATH: 'server.io.fs.file', DB_STATEMENT: 'server.db.statement', - DB_SYSTEM: 'server.db.system' + DB_SYSTEM: 'server.db.system', + + LOGIN_SUCCESS: 'server.business_logic.users.login.success', + LOGIN_FAILURE: 'server.business_logic.users.login.failure' } diff --git a/packages/dd-trace/test/appsec/sdk/track_event.spec.js b/packages/dd-trace/test/appsec/sdk/track_event.spec.js index e9f460eb23b..e3739488b81 100644 --- a/packages/dd-trace/test/appsec/sdk/track_event.spec.js +++ b/packages/dd-trace/test/appsec/sdk/track_event.spec.js @@ -4,6 +4,7 @@ const proxyquire = require('proxyquire') const agent = require('../../plugins/agent') const axios = require('axios') const tracer = require('../../../../../index') +const { LOGIN_SUCCESS, LOGIN_FAILURE } = require('../../../src/appsec/addresses') describe('track_event', () => { describe('Internal API', () => { @@ -123,7 +124,7 @@ describe('track_event', () => { trackUserLoginSuccessEvent(tracer, user) sinon.assert.calledOnceWithExactly( waf.run, - { persistent: { 'server.business_logic.users.login.success': null } } + { persistent: { [LOGIN_SUCCESS]: null } } ) }) }) @@ -207,7 +208,7 @@ describe('track_event', () => { trackUserLoginFailureEvent(tracer, 'user_id') sinon.assert.calledOnceWithExactly( waf.run, - { persistent: { 'server.business_logic.users.login.failure': null } } + { persistent: { [LOGIN_FAILURE]: null } } ) }) }) From abaacff9ee147e7cf63f54104859b8899028a3c6 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Tue, 1 Oct 2024 16:53:40 +0200 Subject: [PATCH 11/16] Add body-parser dep to passport plugin test --- packages/dd-trace/test/plugins/externals.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/dd-trace/test/plugins/externals.json b/packages/dd-trace/test/plugins/externals.json index e0216047fa4..78373b16daa 100644 --- a/packages/dd-trace/test/plugins/externals.json +++ b/packages/dd-trace/test/plugins/externals.json @@ -341,6 +341,10 @@ { "name": "express", "versions": [">=4.16.2"] + }, + { + "name": "body-parser", + "versions": ["1.20.1"] } ], "pg": [ From a441d2ed5d466a9ec8769b531e2779bbfefada99 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Fri, 4 Oct 2024 08:07:48 +0200 Subject: [PATCH 12/16] Reformat test --- .../test/appsec/attacker-fingerprinting.spec.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/dd-trace/test/appsec/attacker-fingerprinting.spec.js b/packages/dd-trace/test/appsec/attacker-fingerprinting.spec.js index f645ab3d4ec..013c9cbd3ed 100644 --- a/packages/dd-trace/test/appsec/attacker-fingerprinting.spec.js +++ b/packages/dd-trace/test/appsec/attacker-fingerprinting.spec.js @@ -53,14 +53,14 @@ describe('Attacker fingerprinting', () => { }, { metakey: 'metaValue' }) res.end() } + agent.use(traces => { assert.property(traces[0][0].meta, '_dd.appsec.fp.http.header') assert.equal(traces[0][0].meta['_dd.appsec.fp.http.header'], 'hdr-0110000010-6431a3e6-3-98425651') assert.property(traces[0][0].meta, '_dd.appsec.fp.http.network') assert.equal(traces[0][0].meta['_dd.appsec.fp.http.network'], 'net-0-0000000000') - }) - .then(done) - .catch(done) + }).then(done).catch(done) + axios.get(`http://localhost:${port}/`) }) @@ -69,14 +69,14 @@ describe('Attacker fingerprinting', () => { tracer.appsec.trackUserLoginFailureEvent('test_user_id', true, { metakey: 'metaValue' }) res.end() } + agent.use(traces => { assert.property(traces[0][0].meta, '_dd.appsec.fp.http.header') assert.equal(traces[0][0].meta['_dd.appsec.fp.http.header'], 'hdr-0110000010-6431a3e6-3-98425651') assert.property(traces[0][0].meta, '_dd.appsec.fp.http.network') assert.equal(traces[0][0].meta['_dd.appsec.fp.http.network'], 'net-0-0000000000') - }) - .then(done) - .catch(done) + }).then(done).catch(done) + axios.get(`http://localhost:${port}/`) }) }) From 901a848de64f5d951135f94cc4e4f186c6d3a6b5 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Tue, 8 Oct 2024 14:58:16 +0200 Subject: [PATCH 13/16] Refactor report derivatives --- packages/dd-trace/src/appsec/reporter.js | 30 +++++-------------- .../src/appsec/waf/waf_context_wrapper.js | 3 +- .../dd-trace/test/appsec/reporter.spec.js | 17 ++++++----- .../dd-trace/test/appsec/waf/index.spec.js | 7 ++--- 4 files changed, 20 insertions(+), 37 deletions(-) diff --git a/packages/dd-trace/src/appsec/reporter.js b/packages/dd-trace/src/appsec/reporter.js index 0e6e92b4e0a..3880b72367e 100644 --- a/packages/dd-trace/src/appsec/reporter.js +++ b/packages/dd-trace/src/appsec/reporter.js @@ -153,7 +153,7 @@ function reportAttack (attackData) { rootSpan.addTags(newTags) } -function reportSchemas (derivatives) { +function reportDerivatives (derivatives) { if (!derivatives) return const req = storage.getStore()?.req @@ -162,26 +162,11 @@ function reportSchemas (derivatives) { if (!rootSpan) return const tags = {} - for (const [tag, value] of Object.entries(derivatives)) { - if (isFingerprintDerivative(tag)) continue - const gzippedValue = zlib.gzipSync(JSON.stringify(value)) - tags[tag] = gzippedValue.toString('base64') - } - - rootSpan.addTags(tags) -} - -function reportFingerprints (derivatives) { - if (!derivatives) return - - const req = storage.getStore()?.req - const rootSpan = web.root(req) - - if (!rootSpan) return - - const tags = {} - for (const [tag, value] of Object.entries(derivatives)) { - if (!isFingerprintDerivative(tag)) continue + for (let [tag, value] of Object.entries(derivatives)) { + if (!isFingerprintDerivative(tag)) { + const gzippedValue = zlib.gzipSync(JSON.stringify(value)) + value = gzippedValue.toString('base64') + } tags[tag] = value } @@ -270,8 +255,7 @@ module.exports = { reportMetrics, reportAttack, reportWafUpdate: incrementWafUpdatesMetric, - reportSchemas, - reportFingerprints, + reportDerivatives, finishRequest, setRateLimit, mapHeaderAndTags diff --git a/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js b/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js index 495179bd1fc..a2dae737a86 100644 --- a/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +++ b/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js @@ -93,8 +93,7 @@ class WAFContextWrapper { Reporter.reportAttack(JSON.stringify(result.events)) } - Reporter.reportSchemas(result.derivatives) - Reporter.reportFingerprints(result.derivatives) + Reporter.reportDerivatives(result.derivatives) if (wafRunFinished.hasSubscribers) { wafRunFinished.publish({ payload }) diff --git a/packages/dd-trace/test/appsec/reporter.spec.js b/packages/dd-trace/test/appsec/reporter.spec.js index 0dbcda610bc..84a684d93bb 100644 --- a/packages/dd-trace/test/appsec/reporter.spec.js +++ b/packages/dd-trace/test/appsec/reporter.spec.js @@ -316,12 +316,12 @@ describe('reporter', () => { describe('reportSchemas', () => { it('should not call addTags if parameter is undefined', () => { - Reporter.reportSchemas(undefined) + Reporter.reportDerivatives(undefined) expect(span.addTags).not.to.be.called }) it('should call addTags with an empty array', () => { - Reporter.reportSchemas([]) + Reporter.reportDerivatives([]) expect(span.addTags).to.be.calledOnceWithExactly({}) }) @@ -340,10 +340,11 @@ describe('reporter', () => { 'custom.processor.output': schemaValue } - Reporter.reportSchemas(derivatives) + Reporter.reportDerivatives(derivatives) const schemaEncoded = zlib.gzipSync(JSON.stringify(schemaValue)).toString('base64') - expect(span.addTags).to.be.calledOnceWithExactly({ + expect(span.addTags).to.be.calledOnce + expect(span.addTags).to.be.calledWithMatch({ '_dd.appsec.s.req.headers': schemaEncoded, '_dd.appsec.s.req.query': schemaEncoded, '_dd.appsec.s.req.params': schemaEncoded, @@ -356,12 +357,12 @@ describe('reporter', () => { describe('reportFingerprints', () => { it('should not call addTags if parameter is undefined', () => { - Reporter.reportFingerprints(undefined) + Reporter.reportDerivatives(undefined) sinon.assert.notCalled(span.addTags) }) it('should call addTags with an empty array', () => { - Reporter.reportFingerprints([]) + Reporter.reportDerivatives([]) sinon.assert.calledOnceWithExactly(span.addTags, {}) }) @@ -380,9 +381,9 @@ describe('reporter', () => { 'custom.processor.output': schemaValue } - Reporter.reportFingerprints(derivatives) + Reporter.reportDerivatives(derivatives) - sinon.assert.calledOnceWithExactly(span.addTags, { + sinon.assert.calledOnceWithMatch(span.addTags, { '_dd.appsec.fp.http.endpoint': 'endpoint_fingerprint', '_dd.appsec.fp.http.header': 'header_fingerprint', '_dd.appsec.fp.http.network': 'network_fingerprint', diff --git a/packages/dd-trace/test/appsec/waf/index.spec.js b/packages/dd-trace/test/appsec/waf/index.spec.js index c4596b759dc..b0c16647872 100644 --- a/packages/dd-trace/test/appsec/waf/index.spec.js +++ b/packages/dd-trace/test/appsec/waf/index.spec.js @@ -48,8 +48,7 @@ describe('WAF Manager', () => { sinon.stub(Reporter, 'reportMetrics') sinon.stub(Reporter, 'reportAttack') sinon.stub(Reporter, 'reportWafUpdate') - sinon.stub(Reporter, 'reportSchemas') - sinon.stub(Reporter, 'reportFingerprints') + sinon.stub(Reporter, 'reportDerivatives') webContext = {} sinon.stub(web, 'getContext').returns(webContext) @@ -405,7 +404,7 @@ describe('WAF Manager', () => { ddwafContext.run.returns(result) wafContextWrapper.run(params) - expect(Reporter.reportSchemas).to.be.calledOnceWithExactly(result.derivatives) + expect(Reporter.reportDerivatives).to.be.calledOnceWithExactly(result.derivatives) }) it('should report fingerprints when ddwafContext returns fingerprints in results derivatives', () => { @@ -427,7 +426,7 @@ describe('WAF Manager', () => { 'server.request.body': 'foo' } }) - sinon.assert.calledOnceWithExactly(Reporter.reportFingerprints, result.derivatives) + sinon.assert.calledOnceWithExactly(Reporter.reportDerivatives, result.derivatives) }) }) }) From a291490991be78d17414b1411cbb0d3428ccb971 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Tue, 8 Oct 2024 14:59:23 +0200 Subject: [PATCH 14/16] Move method to its right place --- packages/dd-trace/src/appsec/reporter.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/dd-trace/src/appsec/reporter.js b/packages/dd-trace/src/appsec/reporter.js index 3880b72367e..dd2bde9fb06 100644 --- a/packages/dd-trace/src/appsec/reporter.js +++ b/packages/dd-trace/src/appsec/reporter.js @@ -153,6 +153,10 @@ function reportAttack (attackData) { rootSpan.addTags(newTags) } +function isFingerprintDerivative (derivative) { + return derivative.startsWith('_dd.appsec.fp') +} + function reportDerivatives (derivatives) { if (!derivatives) return @@ -243,10 +247,6 @@ function setRateLimit (rateLimit) { limiter = new Limiter(rateLimit) } -function isFingerprintDerivative (derivative) { - return derivative.startsWith('_dd.appsec.fp') -} - module.exports = { metricsQueue, filterHeaders, From 9137f2502f022d438ea0ee4a9c6dfe762ae1cef3 Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Tue, 8 Oct 2024 15:32:12 +0200 Subject: [PATCH 15/16] Unify reportSchemas and reportFingerprint test in one suite --- .../dd-trace/test/appsec/reporter.spec.js | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/dd-trace/test/appsec/reporter.spec.js b/packages/dd-trace/test/appsec/reporter.spec.js index 84a684d93bb..5672bd7b30f 100644 --- a/packages/dd-trace/test/appsec/reporter.spec.js +++ b/packages/dd-trace/test/appsec/reporter.spec.js @@ -314,7 +314,7 @@ describe('reporter', () => { }) }) - describe('reportSchemas', () => { + describe('reportDerivatives', () => { it('should not call addTags if parameter is undefined', () => { Reporter.reportDerivatives(undefined) expect(span.addTags).not.to.be.called @@ -353,18 +353,6 @@ describe('reporter', () => { 'custom.processor.output': schemaEncoded }) }) - }) - - describe('reportFingerprints', () => { - it('should not call addTags if parameter is undefined', () => { - Reporter.reportDerivatives(undefined) - sinon.assert.notCalled(span.addTags) - }) - - it('should call addTags with an empty array', () => { - Reporter.reportDerivatives([]) - sinon.assert.calledOnceWithExactly(span.addTags, {}) - }) it('should call addTags', () => { const schemaValue = [{ key: [8] }] @@ -383,11 +371,18 @@ describe('reporter', () => { Reporter.reportDerivatives(derivatives) - sinon.assert.calledOnceWithMatch(span.addTags, { + const schemaEncoded = zlib.gzipSync(JSON.stringify(schemaValue)).toString('base64') + expect(span.addTags).to.be.calledOnceWithExactly({ '_dd.appsec.fp.http.endpoint': 'endpoint_fingerprint', '_dd.appsec.fp.http.header': 'header_fingerprint', '_dd.appsec.fp.http.network': 'network_fingerprint', - '_dd.appsec.fp.session': 'session_fingerprint' + '_dd.appsec.fp.session': 'session_fingerprint', + '_dd.appsec.s.req.headers': schemaEncoded, + '_dd.appsec.s.req.query': schemaEncoded, + '_dd.appsec.s.req.params': schemaEncoded, + '_dd.appsec.s.req.cookies': schemaEncoded, + '_dd.appsec.s.req.body': schemaEncoded, + 'custom.processor.output': schemaEncoded }) }) }) From ac364fa0f6d70d49aaea06c7af30b09fbdb2420a Mon Sep 17 00:00:00 2001 From: CarlesDD Date: Tue, 8 Oct 2024 15:38:41 +0200 Subject: [PATCH 16/16] Unify reportSchemas and reportFingerprint test in one suite --- .../dd-trace/test/appsec/reporter.spec.js | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/packages/dd-trace/test/appsec/reporter.spec.js b/packages/dd-trace/test/appsec/reporter.spec.js index 5672bd7b30f..0860b2c75ac 100644 --- a/packages/dd-trace/test/appsec/reporter.spec.js +++ b/packages/dd-trace/test/appsec/reporter.spec.js @@ -325,35 +325,6 @@ describe('reporter', () => { expect(span.addTags).to.be.calledOnceWithExactly({}) }) - it('should call addTags', () => { - const schemaValue = [{ key: [8] }] - const derivatives = { - '_dd.appsec.fp.http.endpoint': 'endpoint_fingerprint', - '_dd.appsec.fp.http.header': 'header_fingerprint', - '_dd.appsec.fp.http.network': 'network_fingerprint', - '_dd.appsec.fp.session': 'session_fingerprint', - '_dd.appsec.s.req.headers': schemaValue, - '_dd.appsec.s.req.query': schemaValue, - '_dd.appsec.s.req.params': schemaValue, - '_dd.appsec.s.req.cookies': schemaValue, - '_dd.appsec.s.req.body': schemaValue, - 'custom.processor.output': schemaValue - } - - Reporter.reportDerivatives(derivatives) - - const schemaEncoded = zlib.gzipSync(JSON.stringify(schemaValue)).toString('base64') - expect(span.addTags).to.be.calledOnce - expect(span.addTags).to.be.calledWithMatch({ - '_dd.appsec.s.req.headers': schemaEncoded, - '_dd.appsec.s.req.query': schemaEncoded, - '_dd.appsec.s.req.params': schemaEncoded, - '_dd.appsec.s.req.cookies': schemaEncoded, - '_dd.appsec.s.req.body': schemaEncoded, - 'custom.processor.output': schemaEncoded - }) - }) - it('should call addTags', () => { const schemaValue = [{ key: [8] }] const derivatives = {