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 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/src/appsec/remote_config/capabilities.js b/packages/dd-trace/src/appsec/remote_config/capabilities.js index 05dc96233fd..97965fb1203 100644 --- a/packages/dd-trace/src/appsec/remote_config/capabilities.js +++ b/packages/dd-trace/src/appsec/remote_config/capabilities.js @@ -20,5 +20,8 @@ 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_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/src/appsec/reporter.js b/packages/dd-trace/src/appsec/reporter.js index a58335d9ba7..dd2bde9fb06 100644 --- a/packages/dd-trace/src/appsec/reporter.js +++ b/packages/dd-trace/src/appsec/reporter.js @@ -153,7 +153,11 @@ function reportAttack (attackData) { rootSpan.addTags(newTags) } -function reportSchemas (derivatives) { +function isFingerprintDerivative (derivative) { + return derivative.startsWith('_dd.appsec.fp') +} + +function reportDerivatives (derivatives) { if (!derivatives) return const req = storage.getStore()?.req @@ -162,9 +166,12 @@ function reportSchemas (derivatives) { if (!rootSpan) return const tags = {} - for (const [address, value] of Object.entries(derivatives)) { - const gzippedValue = zlib.gzipSync(JSON.stringify(value)) - tags[address] = gzippedValue.toString('base64') + 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 } rootSpan.addTags(tags) @@ -248,7 +255,7 @@ module.exports = { reportMetrics, reportAttack, reportWafUpdate: incrementWafUpdatesMetric, - reportSchemas, + reportDerivatives, finishRequest, setRateLimit, mapHeaderAndTags diff --git a/packages/dd-trace/src/appsec/sdk/track_event.js b/packages/dd-trace/src/appsec/sdk/track_event.js index 61500e2cfbe..36c40093b19 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/src/appsec/waf/waf_context_wrapper.js b/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js index ed946633174..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,7 +93,7 @@ class WAFContextWrapper { Reporter.reportAttack(JSON.stringify(result.events)) } - Reporter.reportSchemas(result.derivatives) + Reporter.reportDerivatives(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.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..58b54e2c704 --- /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..b51aa57de9c --- /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..013c9cbd3ed --- /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/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) diff --git a/packages/dd-trace/test/appsec/reporter.spec.js b/packages/dd-trace/test/appsec/reporter.spec.js index 6fabf747bcf..0860b2c75ac 100644 --- a/packages/dd-trace/test/appsec/reporter.spec.js +++ b/packages/dd-trace/test/appsec/reporter.spec.js @@ -314,20 +314,24 @@ describe('reporter', () => { }) }) - describe('reportSchemas', () => { + describe('reportDerivatives', () => { 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({}) }) 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, @@ -336,10 +340,14 @@ 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({ + '_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': schemaEncoded, '_dd.appsec.s.req.query': schemaEncoded, '_dd.appsec.s.req.params': schemaEncoded, 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..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', () => { @@ -14,6 +15,7 @@ describe('track_event', () => { let setUserTags let trackUserLoginSuccessEvent, trackUserLoginFailureEvent, trackCustomEvent, trackEvent let sample + let waf beforeEach(() => { log = { @@ -30,6 +32,10 @@ describe('track_event', () => { sample = sinon.stub() + waf = { + run: sinon.spy() + } + const trackEvents = proxyquire('../../../src/appsec/sdk/track_event', { '../../log': log, './utils': { @@ -40,7 +46,8 @@ describe('track_event', () => { }, '../standalone': { sample - } + }, + '../waf': waf }) trackUserLoginSuccessEvent = trackEvents.trackUserLoginSuccessEvent @@ -49,6 +56,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 +117,16 @@ 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: { [LOGIN_SUCCESS]: null } } + ) + }) }) describe('trackUserLoginFailureEvent', () => { @@ -182,6 +203,14 @@ 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: { [LOGIN_FAILURE]: null } } + ) + }) }) describe('trackCustomEvent', () => { diff --git a/packages/dd-trace/test/appsec/waf/index.spec.js b/packages/dd-trace/test/appsec/waf/index.spec.js index 816b3fe89c6..b0c16647872 100644 --- a/packages/dd-trace/test/appsec/waf/index.spec.js +++ b/packages/dd-trace/test/appsec/waf/index.spec.js @@ -48,7 +48,7 @@ describe('WAF Manager', () => { sinon.stub(Reporter, 'reportMetrics') sinon.stub(Reporter, 'reportAttack') sinon.stub(Reporter, 'reportWafUpdate') - sinon.stub(Reporter, 'reportSchemas') + sinon.stub(Reporter, 'reportDerivatives') webContext = {} sinon.stub(web, 'getContext').returns(webContext) @@ -404,7 +404,29 @@ 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', () => { + 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.reportDerivatives, result.derivatives) }) }) }) 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": [