@eurekadevsecops/radar 1.5.0 → 1.5.1
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/import.js +136 -0
- package/src/commands/index.js +1 -0
- package/src/commands/scan.js +1 -1
- package/src/util/git/index.js +11 -11
package/package.json
CHANGED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const crypto = require('node:crypto')
|
|
2
|
+
const fs = require('node:fs')
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
const os = require('node:os')
|
|
5
|
+
const SARIF = require('../util/sarif')
|
|
6
|
+
const runner = require('../util/runner')
|
|
7
|
+
module.exports = {
|
|
8
|
+
summary: 'import vulnerabilities',
|
|
9
|
+
args: {
|
|
10
|
+
INPUT: {
|
|
11
|
+
description: 'input SARIF file',
|
|
12
|
+
validate: INPUT => {
|
|
13
|
+
if (!fs.existsSync(path.normalize(INPUT))) throw new Error(`path doesn't exist: ${INPUT}`)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
options: [
|
|
18
|
+
{ name: 'ESCALATE', short: 'e', long: 'escalate', type: 'string', description: 'severities to treat as high/error' },
|
|
19
|
+
{ name: 'FORMAT', short: 'f', long: 'format', type: 'string', description: 'severity format' },
|
|
20
|
+
{ name: 'QUIET', short: 'q', long: 'quiet', type: 'boolean', description: 'suppress stdout logging' }
|
|
21
|
+
],
|
|
22
|
+
description: `
|
|
23
|
+
Imports vulnerabilities from the input SARIF file given by INPUT argument.
|
|
24
|
+
The SARIF file must have been produced by scanners supported by Radar CLI.
|
|
25
|
+
|
|
26
|
+
When quiet mode is selected with the QUIET command-line option, most stdout
|
|
27
|
+
logs are ommitted except for errors that occur with the importing process.
|
|
28
|
+
|
|
29
|
+
By default, findings are displayed as high, moderate, and low. This is the
|
|
30
|
+
'security' severity format. Findings can also be displayed as errors, warnings,
|
|
31
|
+
and notes. This is the 'sarif' severity format.
|
|
32
|
+
|
|
33
|
+
Exit codes:
|
|
34
|
+
0 - Clean and successful import. No errors, warnings, or notes.
|
|
35
|
+
1 - Bad command, arguments, or options. Import not completed.
|
|
36
|
+
16 - Import aborted due to unexpected error.
|
|
37
|
+
`,
|
|
38
|
+
examples: [
|
|
39
|
+
'$ radar import scan.sarif ' + '(import findings from SARIF file)'.grey,
|
|
40
|
+
'$ radar import -f security scan.sarif ' + '(displays findings as high, moderate, and low)'.grey,
|
|
41
|
+
'$ radar import -f sarif scan.sarif ' + '(displays findings as error, warning, and note)'.grey,
|
|
42
|
+
'$ radar import -e moderate,low scan.sarif ' + '(treat lower severities as high)'.grey,
|
|
43
|
+
'$ radar import -f sarif -e warning,note scan.sarif ' + '(treat lower severities as errors)'.grey
|
|
44
|
+
],
|
|
45
|
+
run: async (toolbox, args) => {
|
|
46
|
+
const { log, scanners: availableScanners, telemetry } = toolbox
|
|
47
|
+
|
|
48
|
+
// Set defaults for args and options.
|
|
49
|
+
args.FORMAT ??= 'security'
|
|
50
|
+
|
|
51
|
+
// Normalize and/or rewrite args and options.
|
|
52
|
+
args.INPUT = path.resolve(path.normalize(args.INPUT))
|
|
53
|
+
|
|
54
|
+
// Validate args and options.
|
|
55
|
+
if (args.FORMAT !== 'sarif' && args.FORMAT !== 'security') throw new Error('FORMAT must be one of \'sarif\' or \'security\'')
|
|
56
|
+
if (args.ESCALATE) args.ESCALATE.split(',').map(severity => {
|
|
57
|
+
if (args.FORMAT === 'security' && severity !== 'moderate' && severity !== 'low') throw new Error(`Severity to escalate must be 'moderate' or 'low'`)
|
|
58
|
+
if (args.FORMAT === 'sarif' && severity !== 'warning' && severity !== 'note') throw new Error(`Severity to escalate must be 'warning' or 'note'`)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Derive scan parameters.
|
|
62
|
+
const escalations = args.ESCALATE?.split(',').map(severity => {
|
|
63
|
+
if (severity === 'moderate') return 'warning'
|
|
64
|
+
if (severity === 'low') return 'note'
|
|
65
|
+
return severity
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// Check that telemetry is enabled.
|
|
69
|
+
if (!args.QUIET && !telemetry.enabled) {
|
|
70
|
+
log(`ERROR: Telemetry not enabled.`)
|
|
71
|
+
log(`Terminating with exit code 16. See 'radar help import' for list of possible exit codes.`)
|
|
72
|
+
return 0x10 // exit code
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Results include the log and the SARIF findings.
|
|
76
|
+
const results = { log: `Import from "${args.INPUT}"` }
|
|
77
|
+
results.sarif = JSON.parse(fs.readFileSync(args.INPUT, 'utf8'))
|
|
78
|
+
|
|
79
|
+
// Read scanner names from the input SARIF.
|
|
80
|
+
const scanners = []
|
|
81
|
+
for (const run of results.sarif.runs) {
|
|
82
|
+
const scanner = run.tool.driver?.properties?.scanner_name ?? run.tool.driver.name
|
|
83
|
+
scanners.push(scanner)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check for unsupported scanners.
|
|
87
|
+
try {
|
|
88
|
+
const unknownScanners = scanners.filter(name => !availableScanners.find(s => s.name === name))
|
|
89
|
+
if (unknownScanners.length > 1) throw new Error(`Unknown scanners: ${unknownScanners.join(', ')}`)
|
|
90
|
+
else if (unknownScanners.length === 1) throw new Error(`Unknown scanner: ${unknownScanners[0]}`)
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
log(`ERROR: ${error.message}`)
|
|
94
|
+
log(`Terminating with exit code 1. See 'radar help import' for list of possible exit codes.`)
|
|
95
|
+
return 0x1 // exit code
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Send telemetry: scan started.
|
|
99
|
+
let scanID = undefined
|
|
100
|
+
// TODO: Should pass scanID to the server; not read it from the server.
|
|
101
|
+
try {
|
|
102
|
+
const res = await telemetry.send(`scans/started`, {}, { scanners })
|
|
103
|
+
if (!res.ok) throw new Error(`[${res.status}] ${res.statusText}: ${await res.text()}`)
|
|
104
|
+
const data = await res.json()
|
|
105
|
+
scanID = data.scan_id
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
log(`ERROR: ${error.message}${error?.cause?.code === 'ECONNREFUSED' ? ': CONNECTION REFUSED' : ''}`)
|
|
109
|
+
log(`Terminating with exit code 16. See 'radar help import' for list of possible exit codes.`)
|
|
110
|
+
return 0x10 // exit code
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Transform scan findings: treat warnings and notes as errors, and normalize location paths.
|
|
114
|
+
if (escalations) results.sarif = SARIF.transforms.escalate(results.sarif, escalations)
|
|
115
|
+
|
|
116
|
+
// Send telemetry: scan results.
|
|
117
|
+
await telemetry.sendSensitive(`scans/:scanID/results`, { scanID }, { findings: results.sarif, log: results.log })
|
|
118
|
+
|
|
119
|
+
// Analyze scan results: group findings by severity level.
|
|
120
|
+
const analysis = await telemetry.receiveSensitive(`scans/:scanID/summary`, { scanID })
|
|
121
|
+
if (!analysis?.findingsBySeverity) throw new Error(`Failed to retrieve analysis summary for scan '${scanID}'`)
|
|
122
|
+
const summary = analysis.findingsBySeverity
|
|
123
|
+
|
|
124
|
+
// Send telemetry: scan summary.
|
|
125
|
+
await telemetry.send(`scans/:scanID/completed`, { scanID }, summary)
|
|
126
|
+
|
|
127
|
+
// Display summarized findings.
|
|
128
|
+
if (!args.QUIET) {
|
|
129
|
+
process.stdout.write('Imported ')
|
|
130
|
+
SARIF.visualizations.display_totals(summary, args.FORMAT, log, telemetry.enabled && scanID)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Success.
|
|
134
|
+
return 0 // exit code
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/commands/index.js
CHANGED
package/src/commands/scan.js
CHANGED
|
@@ -151,7 +151,7 @@ module.exports = {
|
|
|
151
151
|
|
|
152
152
|
// Send telemetry: git metadata.
|
|
153
153
|
if (telemetry.enabled && scanID) {
|
|
154
|
-
const metadata = git.metadata()
|
|
154
|
+
const metadata = git.metadata(target)
|
|
155
155
|
await telemetry.send(`scans/:scanID/metadata`, { scanID }, { metadata })
|
|
156
156
|
await telemetry.sendSensitive(`scans/:scanID/metadata`, { scanID }, { metadata })
|
|
157
157
|
}
|
package/src/util/git/index.js
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
const { execSync } = require('node:child_process')
|
|
2
2
|
const hostedGitInfo = require('hosted-git-info')
|
|
3
3
|
|
|
4
|
-
function metadata() {
|
|
4
|
+
function metadata(folder) {
|
|
5
5
|
try {
|
|
6
6
|
// Determine if we're scanning a valid git repo.
|
|
7
|
-
const isGitRepo = execSync('git rev-parse --is-inside-work-tree').toString().trim()
|
|
7
|
+
const isGitRepo = execSync('git rev-parse --is-inside-work-tree', { cwd: folder }).toString().trim()
|
|
8
8
|
if (isGitRepo !== 'true') {
|
|
9
9
|
return { type: 'folder' }
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
// Get the repo name and owner.
|
|
13
|
-
const originUrl = execSync('git config --get remote.origin.url').toString().trim()
|
|
13
|
+
const originUrl = execSync('git config --get remote.origin.url', { cwd: folder }).toString().trim()
|
|
14
14
|
const info = hostedGitInfo.fromUrl(originUrl, { noGitPlus: true })
|
|
15
15
|
const ownerPath = info.user.split('/')
|
|
16
16
|
|
|
17
17
|
// Get the branch name.
|
|
18
|
-
const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim()
|
|
18
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: folder }).toString().trim()
|
|
19
19
|
|
|
20
20
|
// Get the commit identifier and timestamp.
|
|
21
|
-
const shortCommitId = execSync('git rev-parse --short HEAD').toString().trim()
|
|
22
|
-
const fullCommitId = execSync('git rev-parse HEAD').toString().trim()
|
|
23
|
-
const commitTime = execSync('git show -s --format=%cI HEAD').toString().trim()
|
|
21
|
+
const shortCommitId = execSync('git rev-parse --short HEAD', { cwd: folder }).toString().trim()
|
|
22
|
+
const fullCommitId = execSync('git rev-parse HEAD', { cwd: folder }).toString().trim()
|
|
23
|
+
const commitTime = execSync('git show -s --format=%cI HEAD', { cwd: folder }).toString().trim()
|
|
24
24
|
|
|
25
25
|
// Get the tags for the current commit.
|
|
26
|
-
let tags = execSync('git tag --points-at HEAD').toString().trim()
|
|
26
|
+
let tags = execSync('git tag --points-at HEAD', { cwd: folder }).toString().trim()
|
|
27
27
|
tags = '["' + tags.split('\n').join('","') + '"]'
|
|
28
28
|
tags = JSON.parse(tags).filter(tag => tag)
|
|
29
29
|
|
|
30
30
|
// Get the list of unique repo contributors (authors and committers).
|
|
31
31
|
const template = '"{\\\"name\\\":\\\"%cn\\\",\\\"email\\\":\\\"%ce\\\"}%n{\\\"name\\\":\\\"%an\\\",\\\"email\\\":\\\"%ae\\\"}"'
|
|
32
|
-
let contributors = execSync(`git log --pretty=${template} | sort -u
|
|
32
|
+
let contributors = execSync(`git log --pretty=${template} | sort -u`, { cwd: folder }).toString().trim()
|
|
33
33
|
contributors = '[' + contributors.split('\n').join(',') + ']'
|
|
34
34
|
contributors = JSON.parse(contributors)
|
|
35
35
|
|
|
@@ -41,11 +41,11 @@ git rev-list --abbrev=4 --abbrev-commit --all | \
|
|
|
41
41
|
fi
|
|
42
42
|
done && printf %s\\\\n "$MAX_LENGTH"
|
|
43
43
|
)`
|
|
44
|
-
const abbrevs = Number(execSync(script).toString().trim())
|
|
44
|
+
const abbrevs = Number(execSync(script, { cwd: folder }).toString().trim())
|
|
45
45
|
|
|
46
46
|
/*
|
|
47
47
|
// Get the total lines of code in the repo.
|
|
48
|
-
const loc = execSync('git ls-files -z ${1} | xargs -0 cat | wc -l').toString().trim()
|
|
48
|
+
const loc = execSync('git ls-files -z ${1} | xargs -0 cat | wc -l', { cwd: folder }).toString().trim()
|
|
49
49
|
*/
|
|
50
50
|
|
|
51
51
|
// Return the repo metadata.
|