@eurekadevsecops/radar 1.0.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/CONTRIBUTING.md +61 -0
- package/LICENSE +674 -0
- package/README.md +43 -0
- package/cli.js +15 -0
- package/package.json +36 -0
- package/scanners/depscan/run.sh +8 -0
- package/scanners/depscan/sarif.j2 +90 -0
- package/scanners/gitleaks/run.sh +7 -0
- package/scanners/opengrep/rules.yaml +69031 -0
- package/scanners/opengrep/run.sh +7 -0
- package/scanners/scanners.toml +20 -0
- package/src/commands/index.js +5 -0
- package/src/commands/scan.js +237 -0
- package/src/commands/scanners.js +15 -0
- package/src/index.js +21 -0
- package/src/plugins/scanners.js +12 -0
- package/src/util/humanize.js +23 -0
- package/src/util/sarif/display_findings.js +6 -0
- package/src/util/sarif/display_totals.js +5 -0
- package/src/util/sarif/index.js +6 -0
- package/src/util/sarif/levels.js +32 -0
- package/src/util/sarif/merge.js +52 -0
- package/src/util/sarif/summarize.js +40 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Parameters:
|
|
2
|
+
# $1 - Path to the source code folder that should be scanned
|
|
3
|
+
# $2 - Path to the assets folder
|
|
4
|
+
# $3 - Path to the output folder where scan results should be stored
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
docker run --rm -t -v $1:/app -v $2:/input -v $3:/home/output radar/opengrep:latest /app 2>&1
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[[scanners]]
|
|
2
|
+
name = "depscan"
|
|
3
|
+
title = "OWASP dep-scan"
|
|
4
|
+
description = "Next-generation security and risk audit tool."
|
|
5
|
+
categories = [ "SCA" ]
|
|
6
|
+
cmd = "${assets}/run.sh ${target} ${assets} ${output}"
|
|
7
|
+
|
|
8
|
+
[[scanners]]
|
|
9
|
+
name = "opengrep"
|
|
10
|
+
title = "Opengrep"
|
|
11
|
+
description = "Ultra-fast static analysis tool."
|
|
12
|
+
categories = [ "SAST" ]
|
|
13
|
+
cmd = "${assets}/run.sh ${target} ${assets} ${output}"
|
|
14
|
+
|
|
15
|
+
[[scanners]]
|
|
16
|
+
name = "gitleaks"
|
|
17
|
+
title = "Gitleaks"
|
|
18
|
+
description = "Detect secrets like passwords, API keys, and tokens in source code."
|
|
19
|
+
categories = [ "SAST" ]
|
|
20
|
+
cmd = "${assets}/run.sh ${target} ${assets} ${output}"
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
const util = require('node:util')
|
|
2
|
+
const exec = util.promisify(require('node:child_process').exec)
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
const { performance } = require('node:perf_hooks')
|
|
6
|
+
const os = require('node:os')
|
|
7
|
+
const { default: Spinner } = require('tiny-spinner')
|
|
8
|
+
const humanize = require('../util/humanize')
|
|
9
|
+
const sariftools = require('../util/sarif')
|
|
10
|
+
module.exports = {
|
|
11
|
+
summary: 'scan for vulnerabilities',
|
|
12
|
+
args: {
|
|
13
|
+
TARGET: {
|
|
14
|
+
description: 'target to scan',
|
|
15
|
+
optional: true,
|
|
16
|
+
validate: TARGET => {
|
|
17
|
+
if (!fs.existsSync(path.normalize(TARGET))) throw new Error(`path doesn't exist: ${TARGET}`)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
options: [
|
|
22
|
+
{ name: 'CATEGORIES', short: 'c', long: 'categories', type: 'string', description: 'list of scanner categories' },
|
|
23
|
+
{ name: 'ERRORS', short: 'e', long: 'errors', type: 'string', description: 'severities to treat as errors' },
|
|
24
|
+
{ name: 'FORMAT', short: 'f', long: 'format', type: 'string', description: 'severity format' },
|
|
25
|
+
{ name: 'OUTPUT', short: 'o', long: 'output', type: 'string', description: 'output SARIF file' },
|
|
26
|
+
{ name: 'QUIET', short: 'q', long: 'quiet', type: 'boolean', description: 'suppress stdout logging' },
|
|
27
|
+
{ name: 'SCANNERS', short: 's', long: 'scanners', type: 'string', description: 'list of scanners to use' }
|
|
28
|
+
],
|
|
29
|
+
description: `
|
|
30
|
+
Scans a target for vulnerabilities. Defaults to displaying findings on stdout.
|
|
31
|
+
If TARGET argument is ommitted, it defaults to current working directory.
|
|
32
|
+
|
|
33
|
+
When quiet mode is selected with the QUIET command-line option, most stdout
|
|
34
|
+
logs are ommitted except for errors that occur with the scanning process. To
|
|
35
|
+
suppress SARIF output on stdout, use the OUTPUT option to save findings into
|
|
36
|
+
a file on disk.
|
|
37
|
+
|
|
38
|
+
By default, all scanners are used. If you want to limit your scan to a subset
|
|
39
|
+
of available scanners, use the SCANNERS option. You can provide a specific
|
|
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.
|
|
43
|
+
|
|
44
|
+
If you want to run all scanners of a certain type, such as SAST, SCA, or DAST,
|
|
45
|
+
use the CATEGORIES option. All scanners are classified into categories and
|
|
46
|
+
some may belong to multiple categories. You could run all available SAST
|
|
47
|
+
scanners, for example, by passing in SAST as the value for the CATEGORIES
|
|
48
|
+
option. Values are case-insensitive. Multiple values should be comma-separated.
|
|
49
|
+
Defaults to "SAST".
|
|
50
|
+
|
|
51
|
+
You can specify both SCANNERS and CATEGORIES at the same time. The list of
|
|
52
|
+
available scanners is filtered by the given SCANNERS and CATEGORIES on the
|
|
53
|
+
command line, and any matching scanners are then used for the scan.
|
|
54
|
+
|
|
55
|
+
By default, findings are displayed as high, moderate, and low. This is the
|
|
56
|
+
'security' severity format. Findings can also be displayed as errors, warnings,
|
|
57
|
+
and notes. This is the 'sarif' severity format.
|
|
58
|
+
|
|
59
|
+
Exit codes:
|
|
60
|
+
0 - Clean and successful scan. No errors, warnings, or notes.
|
|
61
|
+
1 - Bad command, arguments, or options. Scan not completed.
|
|
62
|
+
8-15 - Scan completed with errors, warnings, or notes.
|
|
63
|
+
9 - Scan completed with errors (no warnings or notes).
|
|
64
|
+
10 - Scan completed with warnings (no errors or notes).
|
|
65
|
+
11 - Scan completed with errors and warnings (no notes).
|
|
66
|
+
12 - Scan completed with notes (no errors or warnings).
|
|
67
|
+
13 - Scan completed with errors and notes (no warnings).
|
|
68
|
+
14 - Scan completed with warnings and notes (no errors).
|
|
69
|
+
15 - Scan completed with errors, warnings, and notes.
|
|
70
|
+
>= 16 - Scan aborted due to unexpected error.
|
|
71
|
+
`,
|
|
72
|
+
examples: [
|
|
73
|
+
'$ radar scan ' + '(scan current working directory)'.grey,
|
|
74
|
+
'$ radar scan . ' + '(scan current working directory)'.grey,
|
|
75
|
+
'$ radar scan /my/repo/dir ' + '(scan target directory)'.grey,
|
|
76
|
+
'$ radar scan --output=scan.sarif ' + '(save findings in a file)'.grey,
|
|
77
|
+
'$ radar scan -o scan.sarif /my/repo/dir ' + '(short versions of options)'.grey,
|
|
78
|
+
'$ radar scan -s depscan,opengrep ' + '(use only given scanners)'.grey,
|
|
79
|
+
'$ radar scan -c sca,sast ' + '(use only scanners from given categories)'.grey,
|
|
80
|
+
'$ radar scan -f security ' + '(displays findings as high, medium, and low)'.grey,
|
|
81
|
+
'$ radar scan -e warning,note ' + '(treat warnings and notes as errors)'.grey
|
|
82
|
+
],
|
|
83
|
+
run: async (toolbox, args) => {
|
|
84
|
+
const { log, scanners: availableScanners } = toolbox
|
|
85
|
+
|
|
86
|
+
// Set defaults.
|
|
87
|
+
args.TARGET = path.normalize(args.TARGET ?? process.cwd())
|
|
88
|
+
args.FORMAT = args.FORMAT ?? 'security'
|
|
89
|
+
if (args.FORMAT !== 'sarif' && args.FORMAT !== 'security') throw new Error('FORMAT must be one of \'sarif\' or \'security\'')
|
|
90
|
+
args.CATEGORIES = args.CATEGORIES ?? 'sast'
|
|
91
|
+
|
|
92
|
+
// Set scan parameters.
|
|
93
|
+
// const target = args.TARGET ? path.resolve(args.TARGET) : "$PWD" // target to scan
|
|
94
|
+
const target = path.resolve(args.TARGET) // target to scan
|
|
95
|
+
const assets = path.join(__dirname, '..', '..', 'scanners') // scanner assets
|
|
96
|
+
const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'radar-')) // output directory
|
|
97
|
+
const outfile = args.OUTPUT ? path.resolve(args.OUTPUT) : undefined // output file, if any
|
|
98
|
+
|
|
99
|
+
// Select scanners to use.
|
|
100
|
+
const scanners = availableScanners
|
|
101
|
+
|
|
102
|
+
// Filter by scanner names given by the user:
|
|
103
|
+
.filter(scanner => {
|
|
104
|
+
if (!args.SCANNERS) return true
|
|
105
|
+
return args.SCANNERS.split(',').includes(scanner.name)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Filter by scanner categories given by the user:
|
|
109
|
+
.filter(scanner => {
|
|
110
|
+
if (!args.CATEGORIES) return true
|
|
111
|
+
for (const category of args.CATEGORIES.toUpperCase().split(',')) {
|
|
112
|
+
if (scanner.categories.includes(category)) return true
|
|
113
|
+
}
|
|
114
|
+
return false
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// Run scanners.
|
|
118
|
+
let isScanCompleted = true
|
|
119
|
+
for (const scanner of scanners) {
|
|
120
|
+
let label = scanner.name
|
|
121
|
+
const spinner = new Spinner()
|
|
122
|
+
if (!args.QUIET) spinner.start(label)
|
|
123
|
+
|
|
124
|
+
const t = performance.now()
|
|
125
|
+
const interval = setInterval(() => {
|
|
126
|
+
const t2 = performance.now()
|
|
127
|
+
label = `${scanner.name} [${humanize.duration(t2 - t)}]`
|
|
128
|
+
if (!args.QUIET) spinner.update(label)
|
|
129
|
+
}, 1000) // 1000 milliseconds = 1 second
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
let cmd = scanner.cmd
|
|
133
|
+
|
|
134
|
+
/* eslint-disable no-template-curly-in-string */
|
|
135
|
+
cmd = cmd.replaceAll('${target}', target)
|
|
136
|
+
cmd = cmd.replaceAll('${assets}', path.join(assets, scanner.name))
|
|
137
|
+
cmd = cmd.replaceAll('${output}', outdir)
|
|
138
|
+
/* eslint-enable no-template-curly-in-string */
|
|
139
|
+
|
|
140
|
+
/* const { stdout } = */ await exec(cmd)
|
|
141
|
+
if (!args.QUIET) spinner.success(label)
|
|
142
|
+
} catch (error) {
|
|
143
|
+
isScanCompleted = false
|
|
144
|
+
if (!args.QUIET) spinner.error(label)
|
|
145
|
+
log(`\n${error}`)
|
|
146
|
+
if (error.stdout) log(error.stdout)
|
|
147
|
+
if (error.stderr) log(error.stderr)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
clearInterval(interval)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Process scan findings.
|
|
154
|
+
let exitCode = 0
|
|
155
|
+
if (isScanCompleted) {
|
|
156
|
+
const consolidated = path.join(outdir, 'scan.sarif')
|
|
157
|
+
|
|
158
|
+
// Merge all output SARIF files into one.
|
|
159
|
+
const all = []
|
|
160
|
+
for (const scanner of scanners) {
|
|
161
|
+
all.push(path.join(outdir, `${scanner.name}.sarif`))
|
|
162
|
+
}
|
|
163
|
+
await sariftools.merge(all, consolidated)
|
|
164
|
+
|
|
165
|
+
// Display findings on stdout or write them to destination SARIF file.
|
|
166
|
+
let sarif = fs.readFileSync(consolidated, 'utf8')
|
|
167
|
+
|
|
168
|
+
// Convert the SARIF file into a JS object.
|
|
169
|
+
try {
|
|
170
|
+
sarif = JSON.parse(sarif)
|
|
171
|
+
} catch (error) {
|
|
172
|
+
log(`\n${error}`)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Treat warnings and notes as errors.
|
|
176
|
+
if (args.ERRORS) {
|
|
177
|
+
const levels = args.ERRORS.split(',')
|
|
178
|
+
|
|
179
|
+
for (const run of sarif.runs) {
|
|
180
|
+
if (!run.results) continue
|
|
181
|
+
for (const result of run.results) {
|
|
182
|
+
if (levels.includes(result.level)) {
|
|
183
|
+
result.level = 'error'
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const rule of run.tool.driver.rules) {
|
|
188
|
+
if (rule.id === result.ruleId) {
|
|
189
|
+
const level = rule.defaultConfiguration.level
|
|
190
|
+
if (levels.includes(level)) {
|
|
191
|
+
rule.defaultConfiguration.level = 'error'
|
|
192
|
+
}
|
|
193
|
+
break
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Write findings to the destination SARIF file.
|
|
201
|
+
if (outfile) {
|
|
202
|
+
fs.writeFileSync(outfile, JSON.stringify(sarif))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// TODO: Upload SARIF to user's Eureka account.
|
|
206
|
+
|
|
207
|
+
// Count findings by severity level.
|
|
208
|
+
const summary = await sariftools.summarize(sarif, target)
|
|
209
|
+
|
|
210
|
+
// Display summarized findings.
|
|
211
|
+
if (!args.QUIET) {
|
|
212
|
+
log()
|
|
213
|
+
sariftools.display_findings(summary, args.FORMAT, log)
|
|
214
|
+
if (outfile) log(`Findings exported to ${consolidated}`)
|
|
215
|
+
sariftools.display_totals(summary, args.FORMAT, log)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Determine the correct exit code.
|
|
219
|
+
if (!summary.errors.length && !summary.warnings.length && !summary.notes.length) {
|
|
220
|
+
exitCode = 0
|
|
221
|
+
} else {
|
|
222
|
+
exitCode = 0x8
|
|
223
|
+
if (summary.errors.length > 0) exitCode |= 0x1
|
|
224
|
+
if (summary.warnings.length > 0) exitCode |= 0x2
|
|
225
|
+
if (summary.notes.length > 0) exitCode |= 0x4
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
exitCode = 0x10
|
|
229
|
+
if (!args.QUIET) log('Scan NOT completed!')
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Clean up.
|
|
233
|
+
fs.rmSync(outdir, { recursive: true, force: true })
|
|
234
|
+
|
|
235
|
+
return exitCode
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
summary: 'display available scanners',
|
|
3
|
+
description: `
|
|
4
|
+
Displays available scanners.
|
|
5
|
+
`,
|
|
6
|
+
examples: [
|
|
7
|
+
'$ radar scanners'
|
|
8
|
+
],
|
|
9
|
+
run: async (toolbox, args) => {
|
|
10
|
+
const { log, scanners } = toolbox
|
|
11
|
+
for (const scanner of scanners) {
|
|
12
|
+
log(`${scanner.name}: ${scanner.title} [${scanner.categories.join()}] - ${scanner.description}`)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { build } = require('@persistr/clif')
|
|
2
|
+
const pkg = require('../package.json')
|
|
3
|
+
const commands = require('./commands')
|
|
4
|
+
|
|
5
|
+
// Plugins.
|
|
6
|
+
const plugins = {
|
|
7
|
+
settings: require('@persistr/clif-plugin-settings')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
build: (options) => {
|
|
12
|
+
// Configure the Radar CLI.
|
|
13
|
+
return build(pkg.name, 'radar', pkg.repository.url.replace(/\.git$/, ''))
|
|
14
|
+
.plugins(Object.values({ ...plugins, ...options?.plugins }))
|
|
15
|
+
.toolbox(options?.toolbox)
|
|
16
|
+
.version(pkg.version, '-v, --version')
|
|
17
|
+
.description(`${pkg.description}\n${pkg.homepage}`)
|
|
18
|
+
.commands(commands)
|
|
19
|
+
.done()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const fs = require('node:fs')
|
|
2
|
+
const path = require('node:path')
|
|
3
|
+
const TOML = require('smol-toml')
|
|
4
|
+
|
|
5
|
+
const data = fs.readFileSync(path.join(__dirname, '..', '..', 'scanners', 'scanners.toml'), 'utf8')
|
|
6
|
+
const config = TOML.parse(data)
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
toolbox: {
|
|
10
|
+
scanners: config.scanners
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const humanizeDuration = require('humanize-duration')
|
|
2
|
+
|
|
3
|
+
const humanizer = humanizeDuration.humanizer({
|
|
4
|
+
language: 'shortEn',
|
|
5
|
+
languages: {
|
|
6
|
+
shortEn: {
|
|
7
|
+
y: () => 'y',
|
|
8
|
+
mo: () => 'mo',
|
|
9
|
+
w: () => 'w',
|
|
10
|
+
d: () => 'd',
|
|
11
|
+
h: () => 'h',
|
|
12
|
+
m: () => 'm',
|
|
13
|
+
s: () => 's',
|
|
14
|
+
ms: () => 'ms'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
duration: (duration) => {
|
|
21
|
+
return humanizer(duration, { units: ['y', 'mo', 'w', 'd', 'h', 'm', 's'], round: true, spacer: '', delimiter: ' ' })
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
const levels = require('./levels')
|
|
2
|
+
module.exports = async (summary, format, log) => {
|
|
3
|
+
for (const finding of summary.notes) log(`${finding.artifact.name}:${finding.artifact.line}: ` + `${levels[format].single.note}`.bold + `${levels[format].single.suffix}:` + ` ${finding.message}\n`)
|
|
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.message}\n`)
|
|
5
|
+
for (const finding of summary.errors) log(`${finding.artifact.name}:${finding.artifact.line}: ` + `${levels[format].single.error}`.bold.red + `${levels[format].single.suffix}:` + ` ${finding.message}\n`)
|
|
6
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
const levels = require('./levels')
|
|
2
|
+
module.exports = async (summary, format, log) => {
|
|
3
|
+
const total = summary.errors.length + summary.warnings.length + summary.notes.length
|
|
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}` + '.')
|
|
5
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
sarif: {
|
|
3
|
+
single: {
|
|
4
|
+
error: 'error',
|
|
5
|
+
warning: 'warning',
|
|
6
|
+
note: 'note',
|
|
7
|
+
suffix: ''
|
|
8
|
+
},
|
|
9
|
+
total: {
|
|
10
|
+
issue: 'vulnerability',
|
|
11
|
+
issues: 'vulnerabilities',
|
|
12
|
+
error: 'error(s)',
|
|
13
|
+
warning: 'warning(s)',
|
|
14
|
+
note: 'note(s)'
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
security: {
|
|
18
|
+
single: {
|
|
19
|
+
error: 'high',
|
|
20
|
+
warning: 'moderate',
|
|
21
|
+
note: 'low',
|
|
22
|
+
suffix: ' severity'
|
|
23
|
+
},
|
|
24
|
+
total: {
|
|
25
|
+
issue: 'vulnerability',
|
|
26
|
+
issues: 'vulnerabilities',
|
|
27
|
+
error: 'high',
|
|
28
|
+
warning: 'moderate',
|
|
29
|
+
note: 'low'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/*
|
|
2
|
+
const util = require('node:util')
|
|
3
|
+
const exec = util.promisify(require('node:child_process').exec)
|
|
4
|
+
const multitoolPath = require('@microsoft/sarif-multitool')
|
|
5
|
+
const path = require('node:path')
|
|
6
|
+
module.exports = {
|
|
7
|
+
merge: async (outdir) => {
|
|
8
|
+
const cmd = `${multitoolPath} merge ${path.join(outdir, '*.sarif')} --recurse false --output-directory=${outdir} --output-file=scan.sarif`
|
|
9
|
+
const { stdout, stderr } = await exec(cmd)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('node:fs')
|
|
15
|
+
module.exports = async (files, outfile) => {
|
|
16
|
+
const sarif = {
|
|
17
|
+
version: '2.1.0',
|
|
18
|
+
$schema: 'https://json.schemastore.org/sarif-2.1.0.json',
|
|
19
|
+
runs: []
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
const content = fs.readFileSync(file, 'utf8')
|
|
24
|
+
const scan = JSON.parse(content)
|
|
25
|
+
for (const run of scan.runs) {
|
|
26
|
+
const tool = {
|
|
27
|
+
driver: {
|
|
28
|
+
name: run.tool.driver.name,
|
|
29
|
+
semanticVersion: run.tool.driver.semanticVersion,
|
|
30
|
+
informationUri: run.tool.driver.informationUri,
|
|
31
|
+
properties: run.tool.driver.properties,
|
|
32
|
+
rules: []
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const rules = new Map()
|
|
37
|
+
for (const result of run.results) {
|
|
38
|
+
rules.set(result.ruleId, true)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const rule of run.tool.driver.rules) {
|
|
42
|
+
if (rules.has(rule.id)) {
|
|
43
|
+
tool.driver.rules.push(rule)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
sarif.runs.push({ tool, invocations: run.invocations, results: run.results })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fs.writeFileSync(outfile, JSON.stringify(sarif))
|
|
52
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const path = require('node:path')
|
|
2
|
+
module.exports = (sarif, dir) => {
|
|
3
|
+
// Summarize findings by severity level.
|
|
4
|
+
const summary = { errors: [], warnings: [], notes: [] }
|
|
5
|
+
for (const run of sarif.runs) {
|
|
6
|
+
if (!run.results) continue
|
|
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
|
+
const finding = {
|
|
13
|
+
tool: run.tool.driver.name,
|
|
14
|
+
message: result.message.text,
|
|
15
|
+
artifact: {
|
|
16
|
+
name: file,
|
|
17
|
+
line: result.locations[0].physicalLocation.region.startLine
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (result.level === 'error' || result.level === 'warning' || result.level === 'note') {
|
|
22
|
+
finding.level = result.level
|
|
23
|
+
summary[`${finding.level}s`].push(finding)
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const rule of run.tool.driver.rules) {
|
|
28
|
+
if (rule.id === result.ruleId) {
|
|
29
|
+
const level = rule.defaultConfiguration.level
|
|
30
|
+
if (level === 'error' || level === 'warning' || level === 'note') {
|
|
31
|
+
finding.level = level
|
|
32
|
+
summary[`${finding.level}s`].push(finding)
|
|
33
|
+
}
|
|
34
|
+
break
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return summary
|
|
40
|
+
}
|