Skip to content

Commit 6b5670c

Browse files
authored
esbuild support for IAST (cjs) (#6467)
1 parent a71c407 commit 6b5670c

15 files changed

Lines changed: 495 additions & 35 deletions

File tree

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
'use strict'
2+
3+
const Axios = require('axios')
4+
const { assert } = require('chai')
5+
const childProcess = require('child_process')
6+
const fs = require('fs')
7+
const path = require('path')
8+
const { promisify } = require('util')
9+
const msgpack = require('@msgpack/msgpack')
10+
11+
const { createSandbox, FakeAgent, spawnProc } = require('../helpers')
12+
13+
const exec = promisify(childProcess.exec)
14+
15+
describe('esbuild support for IAST', () => {
16+
describe('cjs', () => {
17+
let proc, agent, sandbox, axios
18+
let applicationDir, bundledApplicationDir
19+
20+
before(async () => {
21+
sandbox = await createSandbox([])
22+
const cwd = sandbox.folder
23+
applicationDir = path.join(cwd, 'appsec/iast-esbuild')
24+
25+
// Craft node_modules directory to ship native modules
26+
const craftedNodeModulesDir = path.join(applicationDir, 'tmp_node_modules')
27+
fs.mkdirSync(craftedNodeModulesDir)
28+
await exec('npm init -y', { cwd: craftedNodeModulesDir })
29+
await exec('npm install @datadog/native-iast-rewriter @datadog/native-iast-taint-tracking', {
30+
cwd: craftedNodeModulesDir,
31+
timeout: 3e3
32+
})
33+
34+
// Install app deps
35+
await exec('npm install || npm install', {
36+
cwd: applicationDir,
37+
timeout: 6e3
38+
})
39+
40+
// Bundle the application
41+
await exec('npm run build', {
42+
cwd: applicationDir,
43+
timeout: 3e3
44+
})
45+
46+
bundledApplicationDir = path.join(applicationDir, 'build')
47+
48+
// Copy crafted node_modules with native modules
49+
fs.cpSync(path.join(craftedNodeModulesDir, 'node_modules'), bundledApplicationDir, { recursive: true })
50+
})
51+
52+
after(async () => {
53+
await sandbox.remove()
54+
})
55+
56+
function startServer (appFile, iastEnabled) {
57+
beforeEach(async () => {
58+
agent = await new FakeAgent().start()
59+
proc = await spawnProc(path.join(bundledApplicationDir, appFile), {
60+
cwd: applicationDir,
61+
env: {
62+
DD_TRACE_AGENT_PORT: agent.port,
63+
DD_IAST_ENABLED: String(iastEnabled),
64+
DD_IAST_REQUEST_SAMPLING: '100',
65+
}
66+
})
67+
axios = Axios.create({ baseURL: proc.url })
68+
})
69+
70+
afterEach(async () => {
71+
proc.kill()
72+
await agent.stop()
73+
})
74+
}
75+
76+
describe('with IAST enabled', () => {
77+
describe('with sourcemap esbuild option enabled', () => {
78+
startServer('iast-enabled-with-sm.js', true)
79+
80+
it('should detect vulnerability with correct location', async () => {
81+
await axios.get('/iast/cmdi-vulnerable?args=-la')
82+
83+
const expectedVulnerabilityType = 'COMMAND_INJECTION'
84+
const expectedVulnerabilityLocationPath = path.join('iast', 'index.js')
85+
const expectedVulnerabilityLocationLine = 9
86+
87+
await agent.assertMessageReceived(({ payload }) => {
88+
const spans = payload.flatMap(p => p.filter(span => span.name === 'express.request'))
89+
spans.forEach(span => {
90+
assert.property(span.meta, '_dd.iast.json')
91+
const spanIastData = JSON.parse(span.meta['_dd.iast.json'])
92+
assert.strictEqual(spanIastData.vulnerabilities[0].type, expectedVulnerabilityType)
93+
assert.strictEqual(spanIastData.vulnerabilities[0].location.path, expectedVulnerabilityLocationPath)
94+
assert.strictEqual(spanIastData.vulnerabilities[0].location.line, expectedVulnerabilityLocationLine)
95+
96+
const ddStack = msgpack.decode(span.meta_struct['_dd.stack'])
97+
assert.property(ddStack.vulnerability[0], 'frames')
98+
assert.isNotEmpty(ddStack.vulnerability[0].frames)
99+
})
100+
}, null, 1, true)
101+
})
102+
})
103+
104+
describe('with sourcemap esbuild option disabled', () => {
105+
startServer('iast-enabled-with-no-sm.js', true)
106+
107+
it('should detect vulnerability with first callsite location', async () => {
108+
await axios.get('/iast/cmdi-vulnerable?args=-la')
109+
110+
const expectedVulnerabilityType = 'COMMAND_INJECTION'
111+
const expectedVulnerabilityLocationPath = path.join('build', 'iast-enabled-with-no-sm.js')
112+
113+
await agent.assertMessageReceived(({ payload }) => {
114+
const spans = payload.flatMap(p => p.filter(span => span.name === 'express.request'))
115+
spans.forEach(span => {
116+
assert.property(span.meta, '_dd.iast.json')
117+
const spanIastData = JSON.parse(span.meta['_dd.iast.json'])
118+
assert.strictEqual(spanIastData.vulnerabilities[0].type, expectedVulnerabilityType)
119+
assert.strictEqual(spanIastData.vulnerabilities[0].location.path, expectedVulnerabilityLocationPath)
120+
121+
const ddStack = msgpack.decode(span.meta_struct['_dd.stack'])
122+
assert.property(ddStack.vulnerability[0], 'frames')
123+
assert.isNotEmpty(ddStack.vulnerability[0].frames)
124+
})
125+
}, null, 1, true)
126+
})
127+
})
128+
})
129+
130+
describe('with IAST disabled', () => {
131+
startServer('iast-disabled.js', false)
132+
133+
it('should not detect any vulnerability', async () => {
134+
await axios.get('/iast/cmdi-vulnerable?args=-la')
135+
await agent.assertMessageReceived(({ payload }) => {
136+
const spans = payload.flatMap(p => p.filter(span => span.name === 'express.request'))
137+
spans.forEach(span => {
138+
assert.notProperty(span.meta, '_dd.iast.json')
139+
})
140+
}, null, 1, true)
141+
})
142+
})
143+
})
144+
})
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict'
2+
3+
require('dd-trace').init()
4+
5+
const express = require('express')
6+
7+
const iastRouter = require('./iast')
8+
const randomJson = require('./random.json') // eslint-disable-line no-unused-vars
9+
10+
const app = express()
11+
12+
app.use('/iast', iastRouter)
13+
14+
const server = app.listen(0, () => {
15+
process.send?.({ port: server.address().port })
16+
})
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict'
2+
3+
/* eslint-disable no-console */
4+
5+
const esbuild = require('esbuild')
6+
7+
const esbuildCommonConfig = require('./esbuild.common-config')
8+
9+
esbuild.build({
10+
...esbuildCommonConfig,
11+
outfile: 'build/iast-disabled.js',
12+
sourcemap: false
13+
}).catch((err) => {
14+
console.error(err)
15+
process.exit(1)
16+
})
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict'
2+
3+
const ddPlugin = require('dd-trace/esbuild')
4+
5+
module.exports = {
6+
entryPoints: ['app.js'],
7+
bundle: true,
8+
minify: true,
9+
plugins: [ddPlugin],
10+
platform: 'node',
11+
target: ['node18'],
12+
external: [
13+
'@datadog/native-iast-taint-tracking',
14+
'@datadog/native-iast-rewriter',
15+
16+
// required if you encounter graphql errors during the build step
17+
// see https://docs.datadoghq.com/tracing/trace_collection/automatic_instrumentation/dd_libraries/nodejs/#bundling
18+
'graphql/language/visitor',
19+
'graphql/language/printer',
20+
'graphql/utilities'
21+
]
22+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use strict'
2+
3+
/* eslint-disable no-console */
4+
5+
const esbuild = require('esbuild')
6+
7+
const esbuildCommonConfig = require('./esbuild.common-config')
8+
9+
esbuild.build({
10+
...esbuildCommonConfig,
11+
outfile: 'build/iast-enabled-with-sm.js',
12+
sourcemap: true,
13+
}).catch((err) => {
14+
console.error(err)
15+
process.exit(1)
16+
})
17+
18+
esbuild.build({
19+
...esbuildCommonConfig,
20+
outfile: 'build/iast-enabled-with-no-sm.js',
21+
sourcemap: false,
22+
}).catch((err) => {
23+
console.error(err)
24+
process.exit(1)
25+
})
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
'use strict'
2+
3+
const express = require('express')
4+
const { execSync } = require('child_process')
5+
6+
const router = express.Router()
7+
8+
router.get('/cmdi-vulnerable', (req, res) => {
9+
execSync(`ls ${req.query.args}`)
10+
11+
res.end()
12+
})
13+
14+
module.exports = router
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "esbuild-dd-trace-iast",
3+
"private": true,
4+
"version": "1.0.0",
5+
"description": "Basic application to test IAST support on a bundled app with dd-trace via esbuild",
6+
"main": "app.js",
7+
"scripts": {
8+
"build": "DD_IAST_ENABLED=true node ./esbuild.js && DD_IAST_ENABLED=false node ./esbuild-no-iast.js"
9+
},
10+
"keywords": [
11+
"esbuild",
12+
"iast"
13+
],
14+
"author": "Carles Capell <[email protected]>",
15+
"license": "ISC",
16+
"dependencies": {
17+
"esbuild": "^0.25.9",
18+
"express": "^4.21.2"
19+
}
20+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"isRandom": true
3+
}

0 commit comments

Comments
 (0)