@eurekadevsecops/radar
Version:
Radar is an open-source orchestrator of security scanners.
177 lines (149 loc) • 7.71 kB
JavaScript
const fs = require('node:fs')
const path = require('node:path')
const SARIF = require('../util/sarif')
const {
parseRepositoryValue,
parseRepositoryFromSarif
} = require('../util/repository')
const { DateTime } = require('luxon')
module.exports = {
summary: 'import vulnerabilities',
args: {
INPUT: {
description: 'input SARIF file',
validate: INPUT => {
if (!fs.existsSync(path.normalize(INPUT))) throw new Error(`path doesn't exist: ${INPUT}`)
}
}
},
options: [
{ name: 'ESCALATE', short: 'e', long: 'escalate', type: 'string', description: 'severities to treat as high/error' },
{ name: 'FORMAT', short: 'f', long: 'format', type: 'string', description: 'severity format' },
{ name: 'DISABLE_ANALYTICS', short: 'noa', long: 'disable-analytics', type: 'boolean', description: 'disable analytics for this run' },
{ name: 'QUIET', short: 'q', long: 'quiet', type: 'boolean', description: 'suppress stdout logging' },
{ name: 'REPOSITORY', short: 'r', long: 'repository', type: 'string', description: 'repository in owner[/path]/name format (optional)' }
],
description: `
Imports vulnerabilities from the input SARIF file given by INPUT argument.
The SARIF file must have been produced by scanners supported by Radar CLI.
The repository must have been added to your Eureka organization.
When quiet mode is selected with the QUIET command-line option, most stdout
logs are ommitted except for errors that occur with the importing process.
By default, findings are displayed as high, moderate, and low. This is the
'security' severity format. Findings can also be displayed as errors, warnings,
and notes. This is the 'sarif' severity format.
Exit codes:
0 - Clean and successful import. No errors, warnings, or notes.
1 - Bad command, arguments, or options. Import not completed.
16 - Import aborted due to unexpected error.
`,
examples: [
'$ radar import scan.sarif -r myorg/myproject ' + '(import findings from SARIF file)'.grey,
'$ radar import -f security scan.sarif -r myorg/myproject ' + '(displays findings as high, moderate, and low)'.grey,
'$ radar import -f sarif scan.sarif -r myorg/myproject ' + '(displays findings as error, warning, and note)'.grey,
'$ radar import -e moderate,low scan.sarif -r myorg/myproject ' + '(treat lower severities as high)'.grey,
'$ radar import -f sarif -e warning,note scan.sarif -r myorg/myproject' + '(treat lower severities as errors)'.grey
],
run: async (toolbox, args) => {
const { log, telemetry, analytics } = toolbox
// Set defaults for args and options.
args.FORMAT ??= 'security'
args.DISABLE_ANALYTICS ??= false
// Configure analytics for this run.
analytics.setEnabled(!args.DISABLE_ANALYTICS)
analytics.setLogger(log)
// Normalize and/or rewrite args and options.
args.INPUT = path.resolve(path.normalize(args.INPUT))
// Validate args and options.
if (args.FORMAT !== 'sarif' && args.FORMAT !== 'security') throw new Error('FORMAT must be one of \'sarif\' or \'security\'')
if (args.ESCALATE) args.ESCALATE.split(',').map(severity => {
if (args.FORMAT === 'security' && severity !== 'moderate' && severity !== 'low') throw new Error(`Severity to escalate must be 'moderate' or 'low'`)
if (args.FORMAT === 'sarif' && severity !== 'warning' && severity !== 'note') throw new Error(`Severity to escalate must be 'warning' or 'note'`)
})
const cliRepository = args.REPOSITORY ? parseRepositoryValue(args.REPOSITORY) : null
if (args.REPOSITORY && !cliRepository) throw new Error(`REPOSITORY must be in the format 'owner[/path]/name'`)
// Derive scan parameters.
const escalations = args.ESCALATE?.split(',').map(severity => {
if (severity === 'moderate') return 'warning'
if (severity === 'low') return 'note'
return severity
})
if (!args.QUIET && !telemetry.enabled) {
log(`ERROR: Telemetry not enabled.`)
log(`Terminating with exit code 16. See 'radar help import' for list of possible exit codes.`)
analytics.track(analytics.EVENTS.radar_import_failed, { flags: args, error: 'telemetry_not_enabled' })
return 0x10 // exit code
}
const results = { log: `Import from "${args.INPUT}"` }
results.sarif = JSON.parse(fs.readFileSync(args.INPUT, 'utf8'))
const scanners = []
for (const run of results.sarif.runs) {
const scanner = run.tool.driver?.properties?.scanner_name ?? run.tool.driver.name
scanners.push(scanner)
}
const sarifRepository = parseRepositoryFromSarif(results.sarif)
const resolvedRepository = cliRepository ?? sarifRepository
const importRepoOwner = resolvedRepository?.owner ?? ''
const importRepoPath = resolvedRepository?.path ?? ''
const importRepoName = resolvedRepository?.name ?? ''
const scanMetadata = {
type: 'git',
repo: {
url: {
origin: '',
https: ''
},
source: {
type: '',
domain: ''
},
owner: importRepoOwner,
path: importRepoPath,
name: importRepoName,
abbrevs: 0,
contributors: []
},
commit: {
id: '',
time: Date.now(),
branch: '',
tags: []
}
}
analytics.track(analytics.EVENTS.radar_import_started, { flags: args, scanners, scanners_count: scanners.length })
let scanID
const timestamp = DateTime.now().toISO()
try {
try {
const res = await telemetry.send(`scans/started`, {}, { scanners, metadata: scanMetadata, timestamp })
if (!res.ok) throw new Error(`[${res.status}] ${res.statusText}: ${await res.text()}`)
const data = await res.json()
scanID = data.scan_id
}
catch (error) {
log(`ERROR: ${error.message}${error?.cause?.code === 'ECONNREFUSED' ? ': CONNECTION REFUSED' : ''}`)
log(`Terminating with exit code 16. See 'radar help import' for list of possible exit codes.`)
analytics.track(analytics.EVENTS.radar_import_failed, { flags: args, scanners, scanners_count: scanners.length, error: error.message })
return 0x10 // exit code
}
const res = await telemetry.sendSensitive(`scans/:scanID/started`, { scanID }, { metadata: scanMetadata, timestamp })
if (!res.ok) log(`WARNING: Scan started (stage 2) telemetry upload failed: [${res.status}] ${res.statusText}: ${await res.text()}`)
// Transform scan findings: treat warnings and notes as errors.
if (escalations) results.sarif = SARIF.transforms.escalate(results.sarif, escalations)
await telemetry.sendSensitive(`scans/:scanID/results`, { scanID }, { findings: results.sarif, log: results.log })
const analysis = await telemetry.receiveSensitive(`scans/:scanID/summary`, { scanID })
if (!analysis?.findingsBySeverity) throw new Error(`Failed to retrieve analysis summary for scan '${scanID}'`)
const summary = analysis.findingsBySeverity
await telemetry.send(`scans/:scanID/completed`, { scanID }, { summary })
if (!args.QUIET) {
process.stdout.write('Imported ')
SARIF.visualizations.display_totals(summary, args.FORMAT, log, telemetry.enabled && scanID)
}
analytics.track(analytics.EVENTS.radar_import_completed, { flags: args, scanners, scanners_count: scanners.length, scan_id: scanID, summary })
return 0 // exit code
} catch (error) {
analytics.track(analytics.EVENTS.radar_import_failed, { flags: args, scanners, scanners_count: scanners.length, error: error.message })
throw error
}
}
}