@eurekadevsecops/radar 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/scan.js +97 -176
- package/src/plugins/scanners.js +3 -1
- package/src/telemetry/index.js +13 -5
- package/src/util/runner.js +75 -0
- package/src/util/sarif/analysis/load.js +4 -0
- package/src/util/sarif/{summarize.js → analysis/summarize.js} +1 -5
- package/src/util/sarif/index.js +13 -4
- package/src/util/sarif/transforms/escalate.js +22 -0
- package/src/util/sarif/{merge.js → transforms/merge.js} +1 -1
- package/src/util/sarif/transforms/normalize.js +20 -0
- package/src/util/sarif/{display_findings.js → visualizations/display_findings.js} +1 -1
- package/src/util/sarif/{display_totals.js → visualizations/display_totals.js} +1 -1
- package/scan.sarif +0 -1
- /package/src/util/{sarif → localization}/levels.js +0 -0
package/package.json
CHANGED
package/src/commands/scan.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
const util = require('node:util')
|
|
2
|
-
const exec = util.promisify(require('node:child_process').exec)
|
|
3
1
|
const fs = require('node:fs')
|
|
4
2
|
const path = require('node:path')
|
|
5
|
-
const { performance } = require('node:perf_hooks')
|
|
6
3
|
const os = require('node:os')
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const sariftools = require('../util/sarif')
|
|
4
|
+
const SARIF = require('../util/sarif')
|
|
5
|
+
const runner = require('../util/runner')
|
|
10
6
|
module.exports = {
|
|
11
7
|
summary: 'scan for vulnerabilities',
|
|
12
8
|
args: {
|
|
@@ -20,7 +16,7 @@ module.exports = {
|
|
|
20
16
|
},
|
|
21
17
|
options: [
|
|
22
18
|
{ name: 'CATEGORIES', short: 'c', long: 'categories', type: 'string', description: 'list of scanner categories' },
|
|
23
|
-
{ name: '
|
|
19
|
+
{ name: 'ESCALATE', short: 'e', long: 'escalate', type: 'string', description: 'severities to treat as high/error' },
|
|
24
20
|
{ name: 'FORMAT', short: 'f', long: 'format', type: 'string', description: 'severity format' },
|
|
25
21
|
{ name: 'OUTPUT', short: 'o', long: 'output', type: 'string', description: 'output SARIF file' },
|
|
26
22
|
{ name: 'QUIET', short: 'q', long: 'quiet', type: 'boolean', description: 'suppress stdout logging' },
|
|
@@ -35,23 +31,27 @@ module.exports = {
|
|
|
35
31
|
suppress SARIF output on stdout, use the OUTPUT option to save findings into
|
|
36
32
|
a file on disk.
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
list of scanners, comma-separated, that will be used to run the scan. Scanner
|
|
41
|
-
names passed into the SCANNERS option should match the scanner names returned
|
|
42
|
-
by the "scanners" command.
|
|
34
|
+
Select which scanners to use with the SCANNERS and CATEGORIES options. If
|
|
35
|
+
neither option is specified, all scanners are run.
|
|
43
36
|
|
|
44
37
|
If you want to run all scanners of a certain type, such as SAST, SCA, or DAST,
|
|
45
38
|
use the CATEGORIES option. All scanners are classified into categories and
|
|
46
39
|
some may belong to multiple categories. You could run all available SAST
|
|
47
40
|
scanners, for example, by passing in SAST as the value for the CATEGORIES
|
|
48
41
|
option. Values are case-insensitive. Multiple values should be comma-separated.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
To select all categories, use the value "all" for CATEGORIES. Defaults to 'all'.
|
|
43
|
+
|
|
44
|
+
If you want to limit your scan to a subset of available scanners, provide a
|
|
45
|
+
specific list of scanners, comma-separated, in the SCANNERS option. Scanner
|
|
46
|
+
names passed into the SCANNERS option should match the scanner names returned
|
|
47
|
+
by the "scanners" command. To select all scanners across selected categories,
|
|
48
|
+
use the value 'all' for SCANNERS. Defaults to 'all'.
|
|
52
49
|
|
|
53
50
|
You can specify both SCANNERS and CATEGORIES at the same time. This will run
|
|
54
|
-
|
|
51
|
+
only those scanners that match both options. For example, if you specify the
|
|
52
|
+
SCA category and 'all' for SCANNERS then all scanners in the SCA category will
|
|
53
|
+
run. If you specify 'SAST' and 'opengrep,depscan' then only opengrep will run
|
|
54
|
+
(because depscan is an SCA scanner, not a SAST one).
|
|
55
55
|
|
|
56
56
|
By default, findings are displayed as high, moderate, and low. This is the
|
|
57
57
|
'security' severity format. Findings can also be displayed as errors, warnings,
|
|
@@ -77,201 +77,122 @@ module.exports = {
|
|
|
77
77
|
'$ radar scan --output=scan.sarif ' + '(save findings in a file)'.grey,
|
|
78
78
|
'$ radar scan -o scan.sarif /my/repo/dir ' + '(short versions of options)'.grey,
|
|
79
79
|
'$ radar scan -s depscan,opengrep ' + '(use only given scanners)'.grey,
|
|
80
|
-
'$ radar scan -c sca,sast ' + '(use
|
|
81
|
-
'$ radar scan -
|
|
82
|
-
'$ radar scan -
|
|
80
|
+
'$ radar scan -c sca,sast ' + '(use all scanners from given categories)'.grey,
|
|
81
|
+
'$ radar scan -c sca,sast -s all ' + '(use all scanners from given categories)'.grey,
|
|
82
|
+
'$ radar scan -c sast -s opengrep ' + '(use only the opengrep scanner)'.grey,
|
|
83
|
+
'$ radar scan -f security ' + '(displays findings as high, moderate, and low)'.grey,
|
|
84
|
+
'$ radar scan -f sarif ' + '(displays findings as error, warning, and note)'.grey,
|
|
85
|
+
'$ radar scan -e moderate,low ' + '(treat lower severities as high)'.grey,
|
|
86
|
+
'$ radar scan -f sarif -e warning,note ' + '(treat lower severities as errors)'.grey
|
|
83
87
|
],
|
|
84
88
|
run: async (toolbox, args) => {
|
|
85
|
-
const { log, scanners: availableScanners, telemetry } = toolbox
|
|
89
|
+
const { log, scanners: availableScanners, categories: availableCategories, telemetry } = toolbox
|
|
90
|
+
|
|
91
|
+
// Set defaults for args and options.
|
|
92
|
+
args.TARGET ??= process.cwd()
|
|
93
|
+
args.FORMAT ??= 'security'
|
|
94
|
+
args.CATEGORIES ??= 'all'
|
|
95
|
+
args.SCANNERS ??= 'all'
|
|
96
|
+
|
|
97
|
+
// Normalize and/or rewrite args and options.
|
|
98
|
+
args.TARGET = path.resolve(path.normalize(args.TARGET))
|
|
99
|
+
if (args.CATEGORIES.split(',').includes('all')) args.CATEGORIES = availableCategories.join(',')
|
|
100
|
+
if (args.SCANNERS.split(',').includes('all')) args.SCANNERS = availableScanners.map(s => s.name).join(',')
|
|
86
101
|
|
|
87
|
-
//
|
|
88
|
-
args.TARGET
|
|
89
|
-
args.FORMAT = args.FORMAT ?? 'security'
|
|
102
|
+
// Validate args and options.
|
|
103
|
+
if (!fs.existsSync(args.TARGET)) throw new Error(`Path not found: ${args.TARGET}`)
|
|
90
104
|
if (args.FORMAT !== 'sarif' && args.FORMAT !== 'security') throw new Error('FORMAT must be one of \'sarif\' or \'security\'')
|
|
91
|
-
if (args.CATEGORIES && !['all', 'sca', 'sast', 'dast'].includes(args.CATEGORIES.toLowerCase())) throw new Error(`CATEGORIES must be one of 'all', 'SCA', 'SAST', or 'DAST'`)
|
|
92
|
-
if (!args.SCANNERS && !args.CATEGORIES) args.CATEGORIES = 'sast'
|
|
93
|
-
if (!args.SCANNERS && (args.CATEGORIES === 'all')) args.CATEGORIES = ''
|
|
94
105
|
if (args.SCANNERS) {
|
|
95
|
-
const unknownScanners = args.SCANNERS.split(',').filter(name => !availableScanners.find(
|
|
106
|
+
const unknownScanners = args.SCANNERS.split(',').filter(name => !availableScanners.find(s => s.name === name))
|
|
96
107
|
if (unknownScanners.length > 1) throw new Error(`Unknown scanners: ${unknownScanners.join(', ')}`)
|
|
97
108
|
else if (unknownScanners.length === 1) throw new Error(`Unknown scanner: ${unknownScanners[0]}`)
|
|
98
109
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
if (args.ESCALATE) args.ESCALATE.split(',').map(severity => {
|
|
111
|
+
if (args.FORMAT === 'security' && severity !== 'moderate' && severity !== 'low') throw new Error(`Severity to escalate must be 'moderate' or 'low'`)
|
|
112
|
+
if (args.FORMAT === 'sarif' && severity !== 'warning' && severity !== 'note') throw new Error(`Severity to escalate must be 'warning' or 'note'`)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// Derive scan parameters.
|
|
116
|
+
const target = args.TARGET // target to scan
|
|
117
|
+
const categories = args.CATEGORIES.toUpperCase().split(',').filter(c => availableCategories.includes(c))
|
|
118
|
+
const scanners = availableScanners
|
|
119
|
+
.filter(s => args.SCANNERS.split(',').includes(s.name))
|
|
120
|
+
.filter(s => categories.filter(c => s.categories.includes(c)).length > 0)
|
|
121
|
+
const escalations = args.ESCALATE?.split(',').map(severity => {
|
|
122
|
+
if (severity === 'moderate') return 'warning'
|
|
123
|
+
if (severity === 'low') return 'note'
|
|
124
|
+
return severity
|
|
125
|
+
})
|
|
102
126
|
const assets = path.join(__dirname, '..', '..', 'scanners') // scanner assets
|
|
103
|
-
const
|
|
127
|
+
const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'radar-')) // temporary output directory
|
|
104
128
|
const outfile = args.OUTPUT ? path.resolve(args.OUTPUT) : undefined // output file, if any
|
|
105
129
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// Filter by scanner names given by the user:
|
|
110
|
-
.filter(scanner => {
|
|
111
|
-
if (!args.SCANNERS) return true
|
|
112
|
-
return args.SCANNERS.split(',').includes(scanner.name)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
// Filter by scanner categories given by the user:
|
|
116
|
-
.filter(scanner => {
|
|
117
|
-
if (!args.CATEGORIES) return true
|
|
118
|
-
for (const category of args.CATEGORIES.toUpperCase().split(',')) {
|
|
119
|
-
if (scanner.categories.includes(category)) return true
|
|
120
|
-
}
|
|
121
|
-
return false
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
// At least one scanner must be selected in order to have a successful scan.
|
|
125
|
-
if (scanners.length === 0) throw new Error('No available scanners selected.')
|
|
130
|
+
// Validate scan parameters.
|
|
131
|
+
if (!categories.length) throw new Error(`CATEGORIES must be one or more of '${availableCategories.join("', '")}', or 'all'`)
|
|
132
|
+
if (!scanners.length) throw new Error('No available scanners selected.')
|
|
126
133
|
|
|
127
134
|
// Send telemetry: scan started.
|
|
128
135
|
let scanID = undefined
|
|
129
136
|
const isTelemetryEnabled = telemetry.enabled()
|
|
130
137
|
if (isTelemetryEnabled) {
|
|
131
138
|
// TODO: Should pass scanID to the server; not read it from the server.
|
|
132
|
-
const response = await telemetry.send(`scans/started`, {}, { scanners: scanners.map((
|
|
139
|
+
const response = await telemetry.send(`scans/started`, {}, { scanners: scanners.map((s) => s.name) })
|
|
133
140
|
const data = await response.json()
|
|
134
141
|
scanID = data.scan_id
|
|
135
142
|
}
|
|
136
143
|
|
|
137
144
|
// Run scanners.
|
|
138
|
-
let isScanCompleted = true
|
|
139
|
-
let runLog = ''
|
|
140
145
|
log(`Running ${scanners.length} of ${availableScanners.length} scanners:`)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
146
|
+
let results = { /* log, sarif */ }
|
|
147
|
+
try {
|
|
148
|
+
// This will run all scanners and return the combined stdout log and SARIF object.
|
|
149
|
+
results = await runner.run({ scanners, target, assets, outdir: tmpdir, quiet: args.QUIET, log })
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
log(`\n${error}`)
|
|
153
|
+
if (!args.QUIET) log('Scan NOT completed!')
|
|
154
|
+
if (telemetry.enabled()) telemetry.send(`scans/:scanID/failed`, { scanID })
|
|
155
|
+
fs.rmSync(tmpdir, { recursive: true, force: true }) // Clean up.
|
|
156
|
+
return 0x10 // exit code
|
|
157
|
+
}
|
|
152
158
|
|
|
153
|
-
|
|
154
|
-
|
|
159
|
+
// Transform scan findings: treat warnings and notes as errors, and normalize location paths.
|
|
160
|
+
if (escalations) results.sarif = SARIF.transforms.escalate(results.sarif, escalations)
|
|
161
|
+
SARIF.transforms.normalize(results.sarif, target)
|
|
155
162
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
cmd = cmd.replaceAll('${assets}', path.join(assets, scanner.name))
|
|
159
|
-
cmd = cmd.replaceAll('${output}', outdir)
|
|
160
|
-
/* eslint-enable no-template-curly-in-string */
|
|
163
|
+
// Write findings to the destination SARIF file.
|
|
164
|
+
if (outfile) fs.writeFileSync(outfile, JSON.stringify(results.sarif))
|
|
161
165
|
|
|
162
|
-
|
|
163
|
-
|
|
166
|
+
// Analyze scan findings: count findings by severity level.
|
|
167
|
+
const summary = await SARIF.analysis.summarize(results.sarif, target)
|
|
164
168
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (error.stdout) log(error.stdout)
|
|
171
|
-
if (error.stderr) log(error.stderr)
|
|
172
|
-
}
|
|
169
|
+
// Send telemetry.
|
|
170
|
+
if (isTelemetryEnabled && scanID) {
|
|
171
|
+
telemetry.send(`scans/:scanID/completed`, { scanID }, summary)
|
|
172
|
+
telemetry.sendSensitive(`scans/:scanID/results`, { scanID }, { findings: results.sarif, log: results.log })
|
|
173
|
+
}
|
|
173
174
|
|
|
174
|
-
|
|
175
|
+
// Display summarized findings.
|
|
176
|
+
if (!args.QUIET) {
|
|
177
|
+
log()
|
|
178
|
+
SARIF.visualizations.display_findings(summary, args.FORMAT, log)
|
|
179
|
+
if (outfile) log(`Findings exported to ${outfile}`)
|
|
180
|
+
SARIF.visualizations.display_totals(summary, args.FORMAT, log)
|
|
175
181
|
}
|
|
176
182
|
|
|
177
|
-
//
|
|
183
|
+
// Determine the correct exit code.
|
|
178
184
|
let exitCode = 0
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// Merge all output SARIF files into one.
|
|
183
|
-
const all = []
|
|
184
|
-
for (const scanner of scanners) {
|
|
185
|
-
all.push(path.join(outdir, `${scanner.name}.sarif`))
|
|
186
|
-
}
|
|
187
|
-
await sariftools.merge(all, consolidated)
|
|
188
|
-
|
|
189
|
-
// Display findings on stdout or write them to destination SARIF file.
|
|
190
|
-
let sarif = fs.readFileSync(consolidated, 'utf8')
|
|
191
|
-
|
|
192
|
-
// Convert the SARIF file into a JS object.
|
|
193
|
-
try {
|
|
194
|
-
sarif = JSON.parse(sarif)
|
|
195
|
-
} catch (error) {
|
|
196
|
-
log(`\n${error}`)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Treat warnings and notes as errors.
|
|
200
|
-
if (args.ERRORS) {
|
|
201
|
-
const levels = args.ERRORS.split(',')
|
|
202
|
-
|
|
203
|
-
for (const run of sarif.runs) {
|
|
204
|
-
if (!run.results) continue
|
|
205
|
-
for (const result of run.results) {
|
|
206
|
-
if (levels.includes(result.level)) {
|
|
207
|
-
result.level = 'error'
|
|
208
|
-
continue
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
for (const rule of run.tool.driver.rules) {
|
|
212
|
-
if (rule.id === result.ruleId) {
|
|
213
|
-
const level = rule.defaultConfiguration.level
|
|
214
|
-
if (levels.includes(level)) {
|
|
215
|
-
rule.defaultConfiguration.level = 'error'
|
|
216
|
-
}
|
|
217
|
-
break
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Write findings to the destination SARIF file.
|
|
225
|
-
if (outfile) {
|
|
226
|
-
fs.writeFileSync(outfile, JSON.stringify(sarif))
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Count findings by severity level.
|
|
230
|
-
const summary = await sariftools.summarize(sarif, target)
|
|
231
|
-
|
|
232
|
-
// Send telemetry.
|
|
233
|
-
if (isTelemetryEnabled && scanID) {
|
|
234
|
-
// Scan completed.
|
|
235
|
-
telemetry.send(`scans/:scanID/completed`, { scanID }, {
|
|
236
|
-
findings: {
|
|
237
|
-
total: summary.errors.length + summary.warnings.length + summary.notes.length,
|
|
238
|
-
critical: 0,
|
|
239
|
-
high: summary.errors.length,
|
|
240
|
-
med: summary.warnings.length,
|
|
241
|
-
low: summary.notes.length
|
|
242
|
-
}
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
// Send sensitive telemetry: scan log and scan findings.
|
|
246
|
-
telemetry.sendSensitive(`scans/:scanID/log`, { scanID }, runLog)
|
|
247
|
-
telemetry.sendSensitive(`scans/:scanID/findings`, { scanID }, { findings: sarif })
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Display summarized findings.
|
|
251
|
-
if (!args.QUIET) {
|
|
252
|
-
log()
|
|
253
|
-
sariftools.display_findings(summary, args.FORMAT, log)
|
|
254
|
-
if (outfile) log(`Findings exported to ${outfile}`)
|
|
255
|
-
sariftools.display_totals(summary, args.FORMAT, log)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Determine the correct exit code.
|
|
259
|
-
if (!summary.errors.length && !summary.warnings.length && !summary.notes.length) {
|
|
260
|
-
exitCode = 0
|
|
261
|
-
} else {
|
|
262
|
-
exitCode = 0x8
|
|
263
|
-
if (summary.errors.length > 0) exitCode |= 0x1
|
|
264
|
-
if (summary.warnings.length > 0) exitCode |= 0x2
|
|
265
|
-
if (summary.notes.length > 0) exitCode |= 0x4
|
|
266
|
-
}
|
|
185
|
+
if (!summary.errors.length && !summary.warnings.length && !summary.notes.length) {
|
|
186
|
+
exitCode = 0
|
|
267
187
|
} else {
|
|
268
|
-
exitCode =
|
|
269
|
-
if (
|
|
270
|
-
if (
|
|
188
|
+
exitCode = 0x8
|
|
189
|
+
if (summary.errors.length > 0) exitCode |= 0x1
|
|
190
|
+
if (summary.warnings.length > 0) exitCode |= 0x2
|
|
191
|
+
if (summary.notes.length > 0) exitCode |= 0x4
|
|
271
192
|
}
|
|
272
193
|
|
|
273
194
|
// Clean up.
|
|
274
|
-
fs.rmSync(
|
|
195
|
+
fs.rmSync(tmpdir, { recursive: true, force: true })
|
|
275
196
|
|
|
276
197
|
return exitCode
|
|
277
198
|
}
|
package/src/plugins/scanners.js
CHANGED
|
@@ -4,9 +4,11 @@ const TOML = require('smol-toml')
|
|
|
4
4
|
|
|
5
5
|
const data = fs.readFileSync(path.join(__dirname, '..', '..', 'scanners', 'scanners.toml'), 'utf8')
|
|
6
6
|
const config = TOML.parse(data)
|
|
7
|
+
const categories = Array.from(new Set(config.scanners.flatMap(scanner => scanner.categories)))
|
|
7
8
|
|
|
8
9
|
module.exports = {
|
|
9
10
|
toolbox: {
|
|
10
|
-
scanners: config.scanners
|
|
11
|
+
scanners: config.scanners,
|
|
12
|
+
categories
|
|
11
13
|
}
|
|
12
14
|
}
|
package/src/telemetry/index.js
CHANGED
|
@@ -47,8 +47,7 @@ const toURL = (path, params) => {
|
|
|
47
47
|
if (path === `scans/started`) return `${EWA_URL}/scans/started`
|
|
48
48
|
if (path === `scans/:scanID/completed`) return `${EWA_URL}/scans/${params.scanID}/completed`
|
|
49
49
|
if (path === `scans/:scanID/failed`) return `${EWA_URL}/scans/${params.scanID}/completed`
|
|
50
|
-
if (path === `scans/:scanID/
|
|
51
|
-
if (path === `scans/:scanID/findings`) return `${VDBE_URL}/scans/${params.scanID}/findings`
|
|
50
|
+
if (path === `scans/:scanID/results`) return `${VDBE_URL}/scans/${params.scanID}/results`
|
|
52
51
|
throw new Error(`Internal Error: Unknown telemetry event: ${path}`)
|
|
53
52
|
}
|
|
54
53
|
|
|
@@ -58,14 +57,23 @@ const toContentType = (path) => {
|
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
const toBody = (path, body) => {
|
|
61
|
-
if (path === `scans/:scanID/log`) return body
|
|
62
60
|
if (path === `scans/started`) body = { ...body, timestamp: DateTime.now().toISO(), profile_id: process.env.EUREKA_PROFILE }
|
|
63
|
-
if (path === `scans/:scanID/completed`) body = { ...body, timestamp: DateTime.now().toISO(), status: 'success', log: { sizeBytes: 0, warnings: 0, errors: 0, link: 'none' }, params: { id: '' }}
|
|
61
|
+
if (path === `scans/:scanID/completed`) body = { ...toFindings(body), timestamp: DateTime.now().toISO(), status: 'success', log: { sizeBytes: 0, warnings: 0, errors: 0, link: 'none' }, params: { id: '' }}
|
|
64
62
|
if (path === `scans/:scanID/failed`) body = { ...body, timestamp: DateTime.now().toISO(), status: 'failure', findings: { total: 0, critical: 0, high: 0, med: 0, low: 0 }, log: { sizeBytes: 0, warnings: 0, errors: 0, link: 'none' }, params: { id: '' }}
|
|
65
|
-
if (path === `scans/:scanID/
|
|
63
|
+
if (path === `scans/:scanID/results`) body = { findings: body.findings /* SARIF */, profileId: process.env.EUREKA_PROFILE, log: Buffer.from(body.log, 'utf8').toString('base64') }
|
|
66
64
|
return JSON.stringify(body)
|
|
67
65
|
}
|
|
68
66
|
|
|
67
|
+
const toFindings = (summary) => ({
|
|
68
|
+
findings: {
|
|
69
|
+
total: summary.errors.length + summary.warnings.length + summary.notes.length,
|
|
70
|
+
critical: 0,
|
|
71
|
+
high: summary.errors.length,
|
|
72
|
+
med: summary.warnings.length,
|
|
73
|
+
low: summary.notes.length
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
69
77
|
module.exports = {
|
|
70
78
|
enabled,
|
|
71
79
|
send,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const fs = require('node:fs')
|
|
2
|
+
const util = require('node:util')
|
|
3
|
+
const exec = util.promisify(require('node:child_process').exec)
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
const { performance } = require('node:perf_hooks')
|
|
6
|
+
const { default: Spinner } = require('tiny-spinner')
|
|
7
|
+
const humanize = require('./humanize')
|
|
8
|
+
const SARIF = require('./sarif')
|
|
9
|
+
|
|
10
|
+
const runAll = async ({ scanners, target, assets, outdir, quiet, log }) => {
|
|
11
|
+
// Results will include the stdout log and the final combined SARIF object.
|
|
12
|
+
const results = { log: '' }
|
|
13
|
+
|
|
14
|
+
// Run all scanners. This will produce one output SARIF file per scanner.
|
|
15
|
+
for (const scanner of scanners) {
|
|
16
|
+
results.log += await runScanner({ scanner, target, assets, outdir, quiet, log, display: {
|
|
17
|
+
begin: () => scanner.name,
|
|
18
|
+
progress: (label, duration) => `${scanner.name} [${humanize.duration(duration)}]`,
|
|
19
|
+
success: (label) => label,
|
|
20
|
+
error: (label) => label
|
|
21
|
+
}})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Merge all output SARIF files into one and load it into a JS object.
|
|
25
|
+
const consolidated = path.join(outdir, 'scan.sarif')
|
|
26
|
+
await SARIF.transforms.merge(consolidated, scanners.map(s => path.join(outdir, `${s.name}.sarif`)))
|
|
27
|
+
results.sarif = JSON.parse(fs.readFileSync(consolidated, 'utf8'))
|
|
28
|
+
|
|
29
|
+
return results
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const runScanner = async ({ scanner, target, assets, outdir, quiet, log, display }) => {
|
|
33
|
+
let label = display.begin()
|
|
34
|
+
const spinner = new Spinner()
|
|
35
|
+
if (!quiet) spinner.start(label)
|
|
36
|
+
|
|
37
|
+
const t = performance.now()
|
|
38
|
+
const interval = setInterval(() => {
|
|
39
|
+
const t2 = performance.now()
|
|
40
|
+
label = display.progress(label, t2 - t)
|
|
41
|
+
if (!quiet) spinner.update(label)
|
|
42
|
+
}, 1000) // 1000 milliseconds = 1 second
|
|
43
|
+
|
|
44
|
+
let runLog = ''
|
|
45
|
+
try {
|
|
46
|
+
let cmd = scanner.cmd
|
|
47
|
+
|
|
48
|
+
/* eslint-disable no-template-curly-in-string */
|
|
49
|
+
cmd = cmd.replaceAll('${target}', target)
|
|
50
|
+
cmd = cmd.replaceAll('${assets}', path.join(assets, scanner.name))
|
|
51
|
+
cmd = cmd.replaceAll('${output}', outdir)
|
|
52
|
+
/* eslint-enable no-template-curly-in-string */
|
|
53
|
+
|
|
54
|
+
const { stdout } = await exec(cmd)
|
|
55
|
+
runLog = stdout
|
|
56
|
+
|
|
57
|
+
if (!quiet) spinner.success(display.success(label))
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (!quiet) spinner.error(display.error(label))
|
|
60
|
+
|
|
61
|
+
let message = `${error}`
|
|
62
|
+
if (error.stdout) message += error.stdout
|
|
63
|
+
if (error.stderr) message += error.stderr
|
|
64
|
+
throw new Error(message)
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
clearInterval(interval)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return runLog
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
run: runAll
|
|
75
|
+
}
|
|
@@ -5,15 +5,11 @@ module.exports = (sarif, dir) => {
|
|
|
5
5
|
for (const run of sarif.runs) {
|
|
6
6
|
if (!run.results) continue
|
|
7
7
|
for (const result of run.results) {
|
|
8
|
-
let file = path.relative('/app', result.locations[0].physicalLocation.artifactLocation.uri)
|
|
9
|
-
file = path.join(dir, file)
|
|
10
|
-
file = path.relative(process.cwd(), file)
|
|
11
|
-
|
|
12
8
|
const finding = {
|
|
13
9
|
tool: run.tool.driver.name,
|
|
14
10
|
message: result.message.text,
|
|
15
11
|
artifact: {
|
|
16
|
-
name:
|
|
12
|
+
name: result.locations[0].physicalLocation.artifactLocation.uri,
|
|
17
13
|
line: result.locations[0].physicalLocation.region.startLine
|
|
18
14
|
}
|
|
19
15
|
}
|
package/src/util/sarif/index.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
module.exports = {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
transforms: {
|
|
3
|
+
escalate: require('./transforms/escalate'),
|
|
4
|
+
merge: require('./transforms/merge'),
|
|
5
|
+
normalize: require('./transforms/normalize')
|
|
6
|
+
},
|
|
7
|
+
analysis: {
|
|
8
|
+
load: require('./analysis/load'),
|
|
9
|
+
summarize: require('./analysis/summarize')
|
|
10
|
+
},
|
|
11
|
+
visualizations: {
|
|
12
|
+
display_findings: require('./visualizations/display_findings'),
|
|
13
|
+
display_totals: require('./visualizations/display_totals')
|
|
14
|
+
}
|
|
6
15
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module.exports = (sarif, escalations) => {
|
|
2
|
+
// Treat warnings and notes as errors.
|
|
3
|
+
for (const run of sarif.runs) {
|
|
4
|
+
if (!run.results) continue
|
|
5
|
+
for (const result of run.results) {
|
|
6
|
+
if (escalations.includes(result.level)) {
|
|
7
|
+
result.level = 'error'
|
|
8
|
+
continue
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
for (const rule of run.tool.driver.rules) {
|
|
12
|
+
if (rule.id === result.ruleId) {
|
|
13
|
+
if (escalations.includes(rule.defaultConfiguration?.level)) {
|
|
14
|
+
rule.defaultConfiguration.level = 'error'
|
|
15
|
+
}
|
|
16
|
+
break
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return sarif
|
|
22
|
+
}
|
|
@@ -13,7 +13,7 @@ module.exports = {
|
|
|
13
13
|
|
|
14
14
|
const fs = require('node:fs')
|
|
15
15
|
const path = require('node:path')
|
|
16
|
-
module.exports = async (
|
|
16
|
+
module.exports = async (outfile, files) => {
|
|
17
17
|
const sarif = {
|
|
18
18
|
version: '2.1.0',
|
|
19
19
|
$schema: 'https://json.schemastore.org/sarif-2.1.0.json',
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const path = require('node:path')
|
|
2
|
+
module.exports = (sarif, dir) => {
|
|
3
|
+
// Normalize findings.
|
|
4
|
+
for (const run of sarif.runs) {
|
|
5
|
+
if (!run.results) continue
|
|
6
|
+
for (const result of run.results) {
|
|
7
|
+
|
|
8
|
+
// Find paths in the finding description and make them relative to the scan directory.
|
|
9
|
+
if (result?.message?.text) result.message.text = result.message.text.replace('/app/', '')
|
|
10
|
+
|
|
11
|
+
// Make all physical locations for the result relative to the scan directory.
|
|
12
|
+
for (const location of result.locations) {
|
|
13
|
+
if (!location.physicalLocation?.artifactLocation?.uri?.startsWith('/app')) continue
|
|
14
|
+
let file = path.relative('/app', location.physicalLocation.artifactLocation.uri)
|
|
15
|
+
location.physicalLocation.artifactLocation.uri = file
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const levels = require('
|
|
1
|
+
const levels = require('../../localization/levels')
|
|
2
2
|
module.exports = async (summary, format, log) => {
|
|
3
3
|
for (const finding of summary.notes) log(`${finding.artifact.name}:${finding.artifact.line}: ` + `${levels[format].single.note}`.bold + `${levels[format].single.suffix}:` + ` ${finding.tool}:` + ` ${finding.message}\n`)
|
|
4
4
|
for (const finding of summary.warnings) log(`${finding.artifact.name}:${finding.artifact.line}: ` + `${levels[format].single.warning}`.bold.yellow + `${levels[format].single.suffix}:` + ` ${finding.tool}:` + ` ${finding.message}\n`)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const levels = require('
|
|
1
|
+
const levels = require('../../localization/levels')
|
|
2
2
|
module.exports = async (summary, format, log) => {
|
|
3
3
|
const total = summary.errors.length + summary.warnings.length + summary.notes.length
|
|
4
4
|
log(`${total} ${total === 1 ? levels[format].total.issue : levels[format].total.issues}: ${summary.errors.length} ` + `${levels[format].total.error}`.red.bold + `, ${summary.warnings.length} ` + `${levels[format].total.warning}`.yellow.bold + `, ${summary.notes.length} ` + `${levels[format].total.note}` + '.')
|
package/scan.sarif
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":"2.1.0","$schema":"https://json.schemastore.org/sarif-2.1.0.json","runs":[{"tool":{"driver":{"name":"gitleaks","semanticVersion":"v8.0.0","informationUri":"https://github.com/gitleaks/gitleaks","properties":{"officialName":"gitleaks"},"rules":[{"id":"generic-api-key","shortDescription":{"text":"Detected a Generic API Key, potentially exposing access to various services and sensitive operations."}},{"id":"private-key","shortDescription":{"text":"Identified a Private Key, which may compromise cryptographic security and sensitive data encryption."}}]}},"results":[{"message":{"text":"generic-api-key has detected secret for file /app/app/services/key_service.ts."},"ruleId":"generic-api-key","locations":[{"physicalLocation":{"artifactLocation":{"uri":"/app/app/services/key_service.ts"},"region":{"startLine":10,"startColumn":44,"endLine":10,"endColumn":75,"snippet":{"text":"private_pkcs8.pem"}}}}],"properties":{"tags":[]}},{"message":{"text":"private-key has detected secret for file /app/keys/vdbe.private.pem."},"ruleId":"private-key","locations":[{"physicalLocation":{"artifactLocation":{"uri":"/app/keys/vdbe.private.pem"},"region":{"startLine":1,"startColumn":1,"endLine":28,"endColumn":26,"snippet":{"text":"-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDT5JLxkpJH2pkv\ntTmZgEduEHGVO8sv4xLpcBCKgSpVh9apBZnJJgDOl77m7zv2VDycjTQLq4dMk/CT\nBWt50kx+e/jHCP8f64UxbDPMxgDgrYj6YaAoho9coJeu6nG+6h2PNFXM941CrA9V\nddEc9K9piYXSLlr2bHBKeLeuH87Qpmx3bEr8jAXdA51zss2ZsUw5qRH6oWMcqWhC\n5CevbWmFXdoo2W8XfGAwpV2D251uUt6oS4qwU5VT/byR65lewxq3YqPvRpQa5plo\n3aTVypyaqy4l5T2jfLiywXpAuhVPzso4EeDx2Jtfvhh03Is59DFQkQwuXS9caqyY\nDtndupYZAgMBAAECggEADU+dn4laoSLtXp7e2HLDifmEeSCBeiekp39/uoO0uhzH\ncmTErWsyv8eumWlL9gSCrj78lwMWg8wDX+kGQGfioEt/bFl3VXUBMAKhGmsR4Qtl\nwHzjh8g0N1hrTvSxYpHoe3eJMFAY0qhmajL1iQEiB9o4yuRYmIRlZXhB6bFb16WP\nflFcGS/KKxXleco8aEZIu2+X3hXwS2/lDD1sIQua65qXhHZMpB4TfgR2B5r87P9Y\nv5Ilb53nDMWbWDCA8ZifG2fjl29X4bujlJy/fu6Y5UVZB5EIQymKDnnWvemZUFrw\nRtjrJJ9mhM8ON2bYI2LHTMhBixpa3OM3jLQc2FeGRQKBgQDzbv6M4zp8foDU+Ji9\nYlhe0wt6t1EMJQrTgS0h0gbm9ekVLXHDm/hMAUQss6YorXY4Iih/e9HEvNf1byx9\nsfVlkNHrlsNklMmG/jGhzs7xpMOgyq8uzzuY9o/gIFzT3LiuTnk23poGNtDAwNVV\nkaxWOiDtpKFiiFbhRW/pos01PwKBgQDe1MPmW7wnwKD34ee/GaTZpQZ4wH8jdTa1\nVM5xOQKGVyZ5zTc5ao7cR0KYN22ycCI/PwIWWaufDWAYuunRfJP1sWqKf4ENQd5P\nxh66uv3qxETOJ4jk2D8O8fysHLyQu8KvS5TvH/UzNiLRHVv9vfyywmWHOpVP5C1i\nvSiC4yqmpwKBgEEtZ7Q7Jq6shDwBb4vNaBHDeeBacrXIuTRV8sqKXFS8ZLLJ3xrb\niMh40lMRqpxbjTqMUsGHWmvNkBjjskrZOfX+p2XnkNs+RxMAvjMvlxL15XcIrYzf\n6XoUEgOVRqVnBH+O/T9mrGCbjpr9RmFJxpWzrJtUJ+2kyXY5TDSG5WCrAoGAaaFc\nmCemYwXKiJdbP1jNr6quDbHa0xkubPkdv8hxrPNFNvoUErCztjJFnFiyNKM5aNfa\ninPJimVRx4dbbcXrcc2/npXgvEMcOp7FVGluEsslfsB5AVqNUe1ehMw+izGmkWh3\n2n9Awh0Ili6fvAJC9w52CIu52hxlc2gN+zXqswMCgYEA7WfWRh+Rz/3RPlsDBuIu\nu78M6hZ5Uj2SneTtYhXOahMuSGQY4vVd8Z23PBudLVNjNvw05532cVJQvDLGrPeg\nMgig/vMURbQIN5TXVtL7vgyi4QXQgCdHET8YEJrPleyttv/lOJv3ZBUfzKtb9/2s\nK47YjsOczOFh0NbeYPXDZrY=\n-----END PRIVATE KEY-----"}}}}],"properties":{"tags":[]}}]}]}
|
|
File without changes
|