diff --git a/integration-tests/appsec/index.spec.js b/integration-tests/appsec/index.spec.js index c4eb1886e4f..43c700fae3a 100644 --- a/integration-tests/appsec/index.spec.js +++ b/integration-tests/appsec/index.spec.js @@ -13,207 +13,249 @@ describe('RASP', () => { stdioHandler && stdioHandler(data) } - const confs = [ - // { abortOnUncaughtException: true }, - { abortOnUncaughtException: false }] - // execArgv --abort-on-uncaught-exception - - confs.forEach((conf) => { - describe(`abortOnUncaughtException: ${conf.abortOnUncaughtException}`, () => { - before(async () => { - sandbox = await createSandbox(['express', 'axios']) - appPort = await getPort() - cwd = sandbox.folder - appFile = path.join(cwd, 'appsec/rasp/index.js') - axios = Axios.create({ - baseURL: `http://localhost:${appPort}` - }) - }) - - beforeEach(async () => { - let execArgv = process.execArgv - if (conf.abortOnUncaughtException) { - execArgv = ['--abort-on-uncaught-exception', ...execArgv] + before(async () => { + sandbox = await createSandbox(['express', 'axios']) + appPort = await getPort() + cwd = sandbox.folder + appFile = path.join(cwd, 'appsec/rasp/index.js') + axios = Axios.create({ + baseURL: `http://localhost:${appPort}` + }) + }) + + after(async () => { + await sandbox.remove() + }) + + function startServer (abortOnUncaughtException) { + beforeEach(async () => { + let execArgv = process.execArgv + if (abortOnUncaughtException) { + execArgv = ['--abort-on-uncaught-exception', ...execArgv] + } + agent = await new FakeAgent().start() + proc = await spawnProc(appFile, { + cwd, + execArgv, + env: { + DD_TRACE_AGENT_PORT: agent.port, + APP_PORT: appPort, + DD_APPSEC_ENABLED: true, + DD_APPSEC_RASP_ENABLED: true, + DD_APPSEC_RULES: path.join(cwd, 'appsec/rasp/rasp_rules.json') } - agent = await new FakeAgent().start() - proc = await spawnProc(appFile, { - cwd, - execArgv, - env: { - AGENT_PORT: agent.port, - APP_PORT: appPort, - DD_APPSEC_ENABLED: true, - DD_APPSEC_RASP_ENABLED: true, - DD_APPSEC_RULES: path.join(cwd, 'appsec/rasp/rasp_rules.json') + }, stdOutputHandler, stdOutputHandler) + }) + + afterEach(async () => { + proc.kill() + await agent.stop() + }) + } + + async function assertRaspDetected () { + await agent.assertMessageReceived(({ headers, payload }) => { + assert.property(payload[0][0].meta, '_dd.appsec.json') + assert.include(payload[0][0].meta['_dd.appsec.json'], '"test-rule-id-2"') + }) + } + + describe('--abort-on-uncaught-exception is not configured', () => { + startServer(false) + + async function testNotCrashedAfterBlocking (path) { + let hasOutput = false + stdioHandler = () => { + hasOutput = true + } + + try { + await axios.get(`${path}?host=ifconfig.pro`) + + assert.fail('Request should have failed') + } catch (e) { + if (!e.response) { + throw e + } + + assert.strictEqual(e.response.status, 403) + await assertRaspDetected() + } + + return new Promise((resolve, reject) => { + setTimeout(() => { + if (hasOutput) { + reject(new Error('Unexpected output in stdout/stderr after blocking request')) + } else { + resolve() } - }, stdOutputHandler, stdOutputHandler) + }, 50) }) + } - afterEach(async () => { - proc.kill() - await agent.stop() - }) + describe('ssrf', () => { + it('should crash when error is not an AbortError', async () => { + let hasOutput = false + stdioHandler = () => { + hasOutput = true + } + + try { + await axios.get('/crash') - after(async () => { - await sandbox.remove() + assert.fail('Request should have failed') + } catch (e) { + return new Promise((resolve, reject) => { + setTimeout(() => { + if (hasOutput) { + resolve() + } else { + reject(new Error('Output expected after crash')) + } + }, 50) + }) + } }) - async function testNotCrashedAfterBlocking (path) { + it('should not crash when customer has his own setUncaughtExceptionCaptureCallback', async () => { let hasOutput = false stdioHandler = () => { hasOutput = true } try { - await axios.get(`${path}?host=ifconfig.pro`) + await axios.get('/crash-and-recovery-A') assert.fail('Request should have failed') } catch (e) { - if (!e.response) { - throw e - } + return new Promise((resolve, reject) => { + setTimeout(() => { + if (hasOutput) { + reject(new Error('Unexpected output in stdout/stderr after blocking request')) + } else { + resolve() + } + }, 50) + }) + } + }) - assert.strictEqual(e.response.status, 403) + it('should not crash when customer has his own uncaughtException', async () => { + let hasOutput = false + stdioHandler = () => { + hasOutput = true } - return new Promise((resolve, reject) => { - setTimeout(() => { - if (hasOutput) { - reject(new Error('Unexpected output in stdout/stderr after blocking request')) - } else { - resolve() - } - }, 50) - }) - } + try { + await axios.get('/crash-and-recovery-B') - describe('ssrf', () => { - it('should crash when error is not an AbortError', async () => { - let hasOutput = false - stdioHandler = () => { - hasOutput = true - } + assert.fail('Request should have failed') + } catch (e) { + // console.log(e) + return new Promise((resolve, reject) => { + setTimeout(() => { + if (hasOutput) { + reject(new Error('Unexpected output in stdout/stderr after blocking request')) + } else { + resolve() + } + }, 50) + }) + } + }) - try { - await axios.get('/crash') - - assert.fail('Request should have failed') - } catch (e) { - return new Promise((resolve, reject) => { - setTimeout(() => { - if (hasOutput) { - resolve() - } else { - reject(new Error('Output expected after crash')) - } - }, 50) - }) - } - }) + it('should block manually', async () => { + let response + const raspDetectedPromise = assertRaspDetected() + try { + response = await axios.get('/ssrf/http/manual-blocking?host=ifconfig.pro') - it('should not crash when customer has his own setUncaughtExceptionCaptureCallback', async () => { - let hasOutput = false - stdioHandler = () => { - hasOutput = true + assert.fail('Request should have failed') + } catch (e) { + if (!e.response) { + throw e } + response = e.response + } + assert.strictEqual(response.status, 403) + await raspDetectedPromise + }) - try { - await axios.get('/crash-and-recovery-A') - - assert.fail('Request should have failed') - } catch (e) { - return new Promise((resolve, reject) => { - setTimeout(() => { - if (hasOutput) { - reject(new Error('Unexpected output in stdout/stderr after blocking request')) - } else { - resolve() - } - }, 50) - }) + it('should block when error is unhandled', async () => { + try { + await axios.get('/ssrf/http/unhandled-error?host=ifconfig.pro') + + assert.fail('Request should have failed') + } catch (e) { + if (!e.response) { + throw e } - }) - - // this test is not valid when abortOnUncaughtException is true - if (!conf.abortOnUncaughtException) { - it('should not crash when customer has his own uncaughtException', async () => { - let hasOutput = false - stdioHandler = () => { - hasOutput = true - } - - try { - await axios.get('/crash-and-recovery-B') - - assert.fail('Request should have failed') - } catch (e) { - // console.log(e) - return new Promise((resolve, reject) => { - setTimeout(() => { - if (hasOutput) { - reject(new Error('Unexpected output in stdout/stderr after blocking request')) - } else { - resolve() - } - }, 50) - }) - } - }) + + assert.strictEqual(e.response.status, 403) + await assertRaspDetected() } + }) - it('should block when error is unhandled', async () => { - try { - await axios.get('/ssrf/http/unhandled-error?host=ifconfig.pro') + it('should not crash when app send data after blocking', () => { + return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-A') + }) - assert.fail('Request should have failed') - } catch (e) { - if (!e.response) { - throw e - } + it('should not crash when app stream data after blocking', () => { + return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-B') + }) - assert.strictEqual(e.response.status, 403) - } - }) + it('should not crash when setHeader, writeHead or end after blocking', () => { + return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-C') + }) - it('should not crash when app send data after blocking', () => { - return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-A') - }) + it('should not crash when appendHeader, flushHeaders, removeHeader after blocking', () => { + return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-D') + }) - it('should not crash when app stream data after blocking', () => { - return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-B') - }) + it('should not crash when writeContinue after blocking', () => { + return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-E') + }) - it('should not crash when setHeader, writeHead or end after blocking', () => { - return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-C') - }) + it('should not crash when writeProcessing after blocking', () => { + return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-F') + }) + + it('should not crash when writeEarlyHints after blocking', () => { + return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-G') + }) - it('should not crash when appendHeader, flushHeaders, removeHeader after blocking', () => { - return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-D') - }) + it('should not crash when res.json after blocking', () => { + return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-H') + }) - it('should not crash when writeContinue after blocking', () => { - return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-E') - }) + it('should not crash when is blocked using axios', () => { + return testNotCrashedAfterBlocking('/ssrf/http/unhandled-axios') + }) - it('should not crash when writeProcessing after blocking', () => { - return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-F') - }) + it('should not crash when is blocked with unhandled rejection', () => { + return testNotCrashedAfterBlocking('/ssrf/http/unhandled-promise') + }) + }) + }) - it('should not crash when writeEarlyHints after blocking', () => { - return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-G') - }) + describe('--abort-on-uncaught-exception is configured', () => { + startServer(true) - it('should not crash when res.json after blocking', () => { - return testNotCrashedAfterBlocking('/ssrf/http/unhandled-async-write-H') - }) + describe('ssrf', () => { + it('should not block', async () => { + let response - it('should not crash when is blocked using axios', () => { - return testNotCrashedAfterBlocking('/ssrf/http/unhandled-axios') - }) + try { + response = await axios.get('/ssrf/http/manual-blocking?host=ifconfig.pro') + } catch (e) { + if (!e.response) { + throw e + } + response = e.response + } - it('should not crash when is blocked with unhandled rejection', () => { - return testNotCrashedAfterBlocking('/ssrf/http/unhandled-promise') - }) + assert.notEqual(response.status, 403) // 200 or 500 expected + await assertRaspDetected() }) }) }) diff --git a/integration-tests/appsec/rasp/index.js b/integration-tests/appsec/rasp/index.js index abd159670ca..d4f3410f8fd 100644 --- a/integration-tests/appsec/rasp/index.js +++ b/integration-tests/appsec/rasp/index.js @@ -1,6 +1,8 @@ 'use strict' - -require('dd-trace').init() +const tracer = require('dd-trace') +tracer.init({ + flushInterval: 0 +}) const path = require('path') const fs = require('fs') @@ -11,10 +13,6 @@ const axios = require('axios') const app = express() const port = process.env.APP_PORT || 3000 -app.get('/ping', (req, res) => { - res.end('pong') -}) - function makeOutgoingRequestAndCbAfterTimeout (req, res, cb) { let finished = false setTimeout(() => { @@ -75,6 +73,22 @@ app.get('/crash-and-recovery-B', (req, res) => { }) }) +app.get('/ssrf/http/manual-blocking', (req, res) => { + const clientRequest = http.get(`https://${req.query.host}`, () => { + res.send('end') + }) + + clientRequest.on('error', (err) => { + if (err.name === 'AbortError') { + res.writeHead(403) + res.end('aborted') + } else { + res.writeHead(500) + res.end('error') + } + }) +}) + app.get('/ssrf/http/unhandled-error', (req, res) => { makeOutgoingRequestAndCbAfterTimeout(req, res) }) diff --git a/packages/dd-trace/src/exporters/common/writer.js b/packages/dd-trace/src/exporters/common/writer.js index 57f08accd32..8dbe21ca4d1 100644 --- a/packages/dd-trace/src/exporters/common/writer.js +++ b/packages/dd-trace/src/exporters/common/writer.js @@ -17,7 +17,6 @@ class Writer { done() } else if (count > 0) { const payload = this._encoder.makePayload() - this._sendPayload(payload, count, done) } else { done()