@eurekadevsecops/radar 1.2.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eurekadevsecops/radar",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Radar is an open-source orchestrator of security scanners.",
5
5
  "homepage": "https://www.eurekadevsecops.com/radar",
6
6
  "keywords": [
@@ -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 { default: Spinner } = require('tiny-spinner')
8
- const humanize = require('../util/humanize')
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: 'ERRORS', short: 'e', long: 'errors', type: 'string', description: 'severities to treat as errors' },
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
- 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.
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
- Defaults to "SAST", unless you use the SCANNERS option in which case the
50
- default value for CATEGORIES is ignored. To run all scanners across all
51
- categories, use the value "all" for CATEGORIES.
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
- all scanners provided in both options.
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 only scanners from given categories)'.grey,
81
- '$ radar scan -f security ' + '(displays findings as high, medium, and low)'.grey,
82
- '$ radar scan -e warning,note ' + '(treat warnings and notes as errors)'.grey
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
- // Set defaults.
88
- args.TARGET = path.normalize(args.TARGET ?? process.cwd())
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(scanner => scanner.name === name))
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
- // Set scan parameters.
101
- const target = path.resolve(args.TARGET) // target to scan
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 outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'radar-')) // output directory
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
- // Select scanners to use.
107
- const scanners = availableScanners
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((scanner) => scanner.name) })
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
- for (const scanner of scanners) {
142
- let label = scanner.name
143
- const spinner = new Spinner()
144
- if (!args.QUIET) spinner.start(label)
145
-
146
- const t = performance.now()
147
- const interval = setInterval(() => {
148
- const t2 = performance.now()
149
- label = `${scanner.name} [${humanize.duration(t2 - t)}]`
150
- if (!args.QUIET) spinner.update(label)
151
- }, 1000) // 1000 milliseconds = 1 second
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
- try {
154
- let cmd = scanner.cmd
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
- /* eslint-disable no-template-curly-in-string */
157
- cmd = cmd.replaceAll('${target}', target)
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
- const { stdout } = await exec(cmd)
163
- runLog += stdout
166
+ // Analyze scan findings: count findings by severity level.
167
+ const summary = await SARIF.analysis.summarize(results.sarif, target)
164
168
 
165
- if (!args.QUIET) spinner.success(label)
166
- } catch (error) {
167
- isScanCompleted = false
168
- if (!args.QUIET) spinner.error(label)
169
- log(`\n${error}`)
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
- clearInterval(interval)
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
- // Process scan findings.
183
+ // Determine the correct exit code.
178
184
  let exitCode = 0
179
- if (isScanCompleted) {
180
- const consolidated = path.join(outdir, 'scan.sarif')
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 ${consolidated}`)
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 = 0x10
269
- if (!args.QUIET) log('Scan NOT completed!')
270
- if (telemetry.enabled()) telemetry.send(`scans/:scanID/failed`, { scanID })
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(outdir, { recursive: true, force: true })
195
+ fs.rmSync(tmpdir, { recursive: true, force: true })
275
196
 
276
197
  return exitCode
277
198
  }
@@ -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
  }
@@ -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/log`) return `${VDBE_URL}/scans/${params.scanID}/log`
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/findings`) body = { ...body, profileId: process.env.EUREKA_PROFILE }
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
+ }
@@ -0,0 +1,4 @@
1
+ module.exports = (file) => {
2
+ // Load the given SARIF file into a JS object.
3
+ return JSON.parse(fs.readFileSync(file, 'utf8'))
4
+ }
@@ -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: file,
12
+ name: result.locations[0].physicalLocation.artifactLocation.uri,
17
13
  line: result.locations[0].physicalLocation.region.startLine
18
14
  }
19
15
  }
@@ -1,6 +1,15 @@
1
1
  module.exports = {
2
- merge: require('./merge'),
3
- summarize: require('./summarize'),
4
- display_findings: require('./display_findings'),
5
- display_totals: require('./display_totals')
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 (files, outfile) => {
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('./levels')
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('./levels')
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}` + '.')
File without changes