Skip to content

Commit cd14cfe

Browse files
IlyasShabiwatson
authored andcommitted
Code injection instrumented metric (#5164)
1 parent a49a500 commit cd14cfe

File tree

5 files changed

+119
-25
lines changed

5 files changed

+119
-25
lines changed

packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js

+19-1
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,32 @@
22

33
const InjectionAnalyzer = require('./injection-analyzer')
44
const { CODE_INJECTION } = require('../vulnerabilities')
5+
const { INSTRUMENTED_SINK } = require('../telemetry/iast-metric')
6+
const { storage } = require('../../../../../datadog-core')
7+
const { getIastContext } = require('../iast-context')
58

69
class CodeInjectionAnalyzer extends InjectionAnalyzer {
710
constructor () {
811
super(CODE_INJECTION)
12+
this.evalInstrumentedInc = false
913
}
1014

1115
onConfigure () {
12-
this.addSub('datadog:eval:call', ({ script }) => this.analyze(script))
16+
this.addSub('datadog:eval:call', ({ script }) => {
17+
if (!this.evalInstrumentedInc) {
18+
const store = storage.getStore()
19+
const iastContext = getIastContext(store)
20+
const tags = INSTRUMENTED_SINK.formatTags(CODE_INJECTION)
21+
22+
for (const tag of tags) {
23+
INSTRUMENTED_SINK.inc(iastContext, tag)
24+
}
25+
26+
this.evalInstrumentedInc = true
27+
}
28+
29+
this.analyze(script)
30+
})
1331
this.addSub('datadog:vm:run-script:start', ({ code }) => this.analyze(code))
1432
this.addSub('datadog:vm:source-text-module:start', ({ code }) => this.analyze(code))
1533
}
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
'use strict'
22

3-
const { createSandbox, FakeAgent, spawnProc } = require('../../../../../integration-tests/helpers')
43
const getPort = require('get-port')
54
const path = require('path')
65
const Axios = require('axios')
6+
const { assert } = require('chai')
7+
const { createSandbox, FakeAgent, spawnProc } = require('../../../../../integration-tests/helpers')
78

89
describe('IAST - code_injection - integration', () => {
9-
let axios, sandbox, cwd, appPort, appFile, agent, proc
10+
let axios, sandbox, cwd, appPort, agent, proc
1011

1112
before(async function () {
1213
this.timeout(process.platform === 'win32' ? 90000 : 30000)
@@ -19,8 +20,6 @@ describe('IAST - code_injection - integration', () => {
1920

2021
appPort = await getPort()
2122
cwd = sandbox.folder
22-
appFile = path.join(cwd, 'resources', 'vm.js')
23-
2423
axios = Axios.create({
2524
baseURL: `http://localhost:${appPort}`
2625
})
@@ -33,16 +32,6 @@ describe('IAST - code_injection - integration', () => {
3332

3433
beforeEach(async () => {
3534
agent = await new FakeAgent().start()
36-
proc = await spawnProc(appFile, {
37-
cwd,
38-
env: {
39-
DD_TRACE_AGENT_PORT: agent.port,
40-
APP_PORT: appPort,
41-
DD_IAST_ENABLED: 'true',
42-
DD_IAST_REQUEST_SAMPLING: '100'
43-
},
44-
execArgv: ['--experimental-vm-modules']
45-
})
4635
})
4736

4837
afterEach(async () => {
@@ -53,24 +42,79 @@ describe('IAST - code_injection - integration', () => {
5342
async function testVulnerabilityRepoting (url) {
5443
await axios.get(url)
5544

56-
return agent.assertMessageReceived(({ headers, payload }) => {
57-
expect(payload[0][0].metrics['_dd.iast.enabled']).to.be.equal(1)
58-
expect(payload[0][0].meta).to.have.property('_dd.iast.json')
45+
let iastTelemetryReceived = false
46+
const checkTelemetry = agent.assertTelemetryReceived(({ headers, payload }) => {
47+
const { namespace, series } = payload.payload
48+
49+
if (namespace === 'iast') {
50+
iastTelemetryReceived = true
51+
52+
const instrumentedSink = series.find(({ metric, tags, type }) => {
53+
return type === 'count' &&
54+
metric === 'instrumented.sink' &&
55+
tags[0] === 'vulnerability_type:code_injection'
56+
})
57+
assert.isNotNull(instrumentedSink)
58+
}
59+
}, 30_000, 'generate-metrics', 2)
60+
61+
const checkMessages = agent.assertMessageReceived(({ headers, payload }) => {
62+
assert.strictEqual(payload[0][0].metrics['_dd.iast.enabled'], 1)
63+
assert.property(payload[0][0].meta, '_dd.iast.json')
5964
const vulnerabilitiesTrace = JSON.parse(payload[0][0].meta['_dd.iast.json'])
60-
expect(vulnerabilitiesTrace).to.not.be.null
65+
assert.isNotNull(vulnerabilitiesTrace)
6166
const vulnerabilities = new Set()
6267

6368
vulnerabilitiesTrace.vulnerabilities.forEach(v => {
6469
vulnerabilities.add(v.type)
6570
})
6671

67-
expect(vulnerabilities.has('CODE_INJECTION')).to.be.true
72+
assert.isTrue(vulnerabilities.has('CODE_INJECTION'))
73+
})
74+
75+
return Promise.all([checkMessages, checkTelemetry]).then(() => {
76+
assert.equal(iastTelemetryReceived, true)
77+
78+
return true
6879
})
6980
}
7081

7182
describe('SourceTextModule', () => {
83+
beforeEach(async () => {
84+
proc = await spawnProc(path.join(cwd, 'resources', 'vm.js'), {
85+
cwd,
86+
env: {
87+
DD_TRACE_AGENT_PORT: agent.port,
88+
APP_PORT: appPort,
89+
DD_IAST_ENABLED: 'true',
90+
DD_IAST_REQUEST_SAMPLING: '100',
91+
DD_TELEMETRY_HEARTBEAT_INTERVAL: 1
92+
},
93+
execArgv: ['--experimental-vm-modules']
94+
})
95+
})
96+
7297
it('should report Code injection vulnerability', async () => {
7398
await testVulnerabilityRepoting('/vm/SourceTextModule?script=export%20const%20result%20%3D%203%3B')
7499
})
75100
})
101+
102+
describe('eval', () => {
103+
beforeEach(async () => {
104+
proc = await spawnProc(path.join(cwd, 'resources', 'eval.js'), {
105+
cwd,
106+
env: {
107+
DD_TRACE_AGENT_PORT: agent.port,
108+
APP_PORT: appPort,
109+
DD_IAST_ENABLED: 'true',
110+
DD_IAST_REQUEST_SAMPLING: '100',
111+
DD_TELEMETRY_HEARTBEAT_INTERVAL: 1
112+
}
113+
})
114+
})
115+
116+
it('should report Code injection vulnerability', async () => {
117+
await testVulnerabilityRepoting('/eval?code=2%2B2')
118+
})
119+
})
76120
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use strict'
2+
3+
module.exports = {
4+
runEval: (code, result) => {
5+
const script = `(${code}, result)`
6+
7+
// eslint-disable-next-line no-eval
8+
return eval(script)
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict'
2+
3+
const tracer = require('dd-trace')
4+
tracer.init({
5+
flushInterval: 1
6+
})
7+
const express = require('express')
8+
9+
const app = express()
10+
const port = process.env.APP_PORT || 3000
11+
12+
app.get('/eval', async (req, res) => {
13+
// eslint-disable-next-line no-eval
14+
require('./eval-methods').runEval(req.query.code, 'test-result')
15+
16+
res.end('OK')
17+
})
18+
19+
app.listen(port, () => {
20+
process.send({ port })
21+
})

packages/dd-trace/test/appsec/rasp/command_injection.integration.spec.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ describe('RASP - command_injection - integration', () => {
6565

6666
let appsecTelemetryReceived = false
6767

68-
const checkMessages = await agent.assertMessageReceived(({ headers, payload }) => {
68+
const checkMessages = agent.assertMessageReceived(({ headers, payload }) => {
6969
assert.property(payload[0][0].meta, '_dd.appsec.json')
7070
assert.include(payload[0][0].meta['_dd.appsec.json'], `"rasp-command_injection-rule-id-${ruleId}"`)
7171
})
7272

73-
const checkTelemetry = await agent.assertTelemetryReceived(({ headers, payload }) => {
73+
const checkTelemetry = agent.assertTelemetryReceived(({ headers, payload }) => {
7474
const namespace = payload.payload.namespace
7575

7676
// Only check telemetry received in appsec namespace and ignore others
@@ -92,10 +92,11 @@ describe('RASP - command_injection - integration', () => {
9292
}
9393
}, 30_000, 'generate-metrics', 2)
9494

95-
const checks = await Promise.all([checkMessages, checkTelemetry])
96-
assert.equal(appsecTelemetryReceived, true)
95+
return Promise.all([checkMessages, checkTelemetry]).then(() => {
96+
assert.equal(appsecTelemetryReceived, true)
9797

98-
return checks
98+
return true
99+
})
99100
}
100101

101102
throw new Error('Request should be blocked')

0 commit comments

Comments
 (0)