@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eurekadevsecops/radar",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Radar is an open-source orchestrator of security scanners.",
5
5
  "homepage": "https://www.eurekadevsecops.com/radar",
6
6
  "keywords": [
@@ -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
+ }
@@ -1,5 +1,6 @@
1
1
  module.exports = {
2
2
  help: 'display help',
3
+ import: require('./import'),
3
4
  scan: require('./scan'),
4
5
  scanners: require('./scanners')
5
6
  }
@@ -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
  }
@@ -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`).toString().trim()
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.