@eurekadevsecops/radar 1.4.6 → 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.
@@ -0,0 +1,70 @@
1
+ {
2
+ "version": "2.1.0",
3
+ "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
4
+ "runs": [
5
+ {
6
+ "tool": {
7
+ "driver": {
8
+ "name": "depscan",
9
+ "semanticVersion": "5.5.0",
10
+ "informationUri": "https://github.com/owasp-dep-scan/dep-scan",
11
+ "properties": {
12
+ "protocol_version": "v1.0.0",
13
+ "scanner_name": "depscan",
14
+ "scanner_version": "5.5.0",
15
+ "db": "https://github.com/AppThreat/vulnerability-db",
16
+ "scan_mode": "source"
17
+ },
18
+ "rules": [
19
+ {
20
+ "id": "CVE-2022-33987Y/pkg:npm/got@9.6.0",
21
+ "shortDescription": {
22
+ "text": "Vulnerable pkg: npm/got@9.6.0\nCVE: CVE-2022-33987\nFix: Update to 11.8.5 or later\n\ndepscan:insights: Indirect dependency\ndepscan:prioritized: false\naffectedVersionRange: got@<11.8.5\n"
23
+ },
24
+ "fullDescription": {
25
+ "text": "# Got allows a redirect to a UNIX socket\nThe got package before 11.8.5 and 12.1.0 for Node.js allows a redirect to a UNIX socket.\nUpgrade to version 11.8.5 or later"
26
+ },
27
+ "help": {
28
+ "text": "Update to 11.8.5 or later"
29
+ },
30
+ "helpUri": "https://nvd.nist.gov/vuln/detail/CVE-2022-33987",
31
+ "properties": {
32
+ "tags": [
33
+ "CVE-2022-33987"
34
+ ]
35
+ }
36
+ }
37
+
38
+ ]
39
+ }
40
+ },
41
+ "results": [
42
+ {
43
+ "ruleId": "CVE-2022-33987Y/pkg:npm/got@9.6.0",
44
+ "level": "warning",
45
+ "message": {
46
+ "text": "Vulnerability CVE-2022-33987 in pkg npm/got@9.6.0. Update to 11.8.5 or later"
47
+ },
48
+ "locations": [
49
+ {
50
+ "physicalLocation": {
51
+ "artifactLocation": {
52
+ "uri": "package-lock.json",
53
+ "uriBaseId": "%SRCROOT%"
54
+ },
55
+ "region": {
56
+ "startLine": 1
57
+ }
58
+ },
59
+ "message": {
60
+ "text": "Vulnerability CVE-2022-33987 in pkg npm/got@9.6.0. Update to 11.8.5 or later"
61
+ }
62
+ }
63
+ ]
64
+ }
65
+
66
+
67
+ ]
68
+ }
69
+ ]
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eurekadevsecops/radar",
3
- "version": "1.4.6",
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": [
@@ -29,6 +29,7 @@
29
29
  "dependencies": {
30
30
  "@persistr/clif": "^1.11.0",
31
31
  "@persistr/clif-plugin-settings": "^2.3.1",
32
+ "hosted-git-info": "^9.0.0",
32
33
  "humanize-duration": "^3.33.0",
33
34
  "jwt-decode": "^4.0.0",
34
35
  "luxon": "^3.7.1",
@@ -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
  }
@@ -87,7 +87,7 @@ module.exports = {
87
87
  '$ radar scan -f sarif -e warning,note ' + '(treat lower severities as errors)'.grey
88
88
  ],
89
89
  run: async (toolbox, args) => {
90
- const { log, scanners: availableScanners, categories: availableCategories, telemetry } = toolbox
90
+ const { log, scanners: availableScanners, categories: availableCategories, telemetry, git } = toolbox
91
91
 
92
92
  // Set defaults for args and options.
93
93
  args.TARGET ??= process.cwd()
@@ -149,6 +149,13 @@ module.exports = {
149
149
  }
150
150
  }
151
151
 
152
+ // Send telemetry: git metadata.
153
+ if (telemetry.enabled && scanID) {
154
+ const metadata = git.metadata(target)
155
+ await telemetry.send(`scans/:scanID/metadata`, { scanID }, { metadata })
156
+ await telemetry.sendSensitive(`scans/:scanID/metadata`, { scanID }, { metadata })
157
+ }
158
+
152
159
  // Run scanners.
153
160
  log(`Running ${scanners.length} of ${availableScanners.length} scanners:`)
154
161
  let results = { /* log, sarif */ }
@@ -180,8 +187,8 @@ module.exports = {
180
187
  let summary
181
188
  if (telemetry.enabled && scanID) {
182
189
  const analysis = await telemetry.receiveSensitive(`scans/:scanID/summary`, { scanID })
183
- if (!analysis?.summary) throw new Error(`Failed to retrieve analysis summary for scan '${scanID}'`)
184
- summary = analysis.summary.findingsBySeverity
190
+ if (!analysis?.findingsBySeverity) throw new Error(`Failed to retrieve analysis summary for scan '${scanID}'`)
191
+ summary = analysis.findingsBySeverity
185
192
  } else {
186
193
  summary = await SARIF.analysis.summarize(results.sarif, target)
187
194
  }
package/src/index.js CHANGED
@@ -6,6 +6,7 @@ const path = require('node:path')
6
6
  // Plugins.
7
7
  const plugins = {
8
8
  settings: require('@persistr/clif-plugin-settings'),
9
+ git: require(path.join(__dirname, 'plugins', 'git')),
9
10
  scanners: require(path.join(__dirname, 'plugins', 'scanners')),
10
11
  telemetry: require(path.join(__dirname, 'plugins', 'telemetry'))
11
12
  }
@@ -0,0 +1,6 @@
1
+ const git = require('../util/git')
2
+ module.exports = {
3
+ toolbox: {
4
+ git
5
+ }
6
+ }
@@ -83,6 +83,7 @@ class Telemetry {
83
83
  if (path === `scans/started`) return `${claims.aud}/scans/started`
84
84
  if (path === `scans/:scanID/completed`) return `${claims.aud}/scans/${params.scanID}/completed`
85
85
  if (path === `scans/:scanID/failed`) return `${claims.aud}/scans/${params.scanID}/completed`
86
+ if (path === `scans/:scanID/metadata`) return `${claims.aud}/scans/${params.scanID}/metadata`
86
87
  if (path === `scans/:scanID/results`) return `${claims.aud}/scans/${params.scanID}/results`
87
88
  throw new Error(`Internal Error: Unknown telemetry event: POST ${path}`)
88
89
  }
@@ -102,6 +103,7 @@ class Telemetry {
102
103
  if (path === `scans/started`) body = { ...body, timestamp: DateTime.now().toISO(), profile_id: process.env.EUREKA_PROFILE }
103
104
  if (path === `scans/:scanID/completed`) body = { ...this.#toFindings(body), timestamp: DateTime.now().toISO(), status: 'success', log: { sizeBytes: 0, warnings: 0, errors: 0, link: 'none' }, params: { id: '' }}
104
105
  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: '' }}
106
+ if (path === `scans/:scanID/metadata`) body = { metadata: body.metadata, profileId: process.env.EUREKA_PROFILE }
105
107
  if (path === `scans/:scanID/results`) body = { findings: body.findings /* SARIF */, profileId: process.env.EUREKA_PROFILE, log: Buffer.from(body.log, 'utf8').toString('base64') }
106
108
  return JSON.stringify(body)
107
109
  }
@@ -0,0 +1,89 @@
1
+ const { execSync } = require('node:child_process')
2
+ const hostedGitInfo = require('hosted-git-info')
3
+
4
+ function metadata(folder) {
5
+ try {
6
+ // Determine if we're scanning a valid git repo.
7
+ const isGitRepo = execSync('git rev-parse --is-inside-work-tree', { cwd: folder }).toString().trim()
8
+ if (isGitRepo !== 'true') {
9
+ return { type: 'folder' }
10
+ }
11
+
12
+ // Get the repo name and owner.
13
+ const originUrl = execSync('git config --get remote.origin.url', { cwd: folder }).toString().trim()
14
+ const info = hostedGitInfo.fromUrl(originUrl, { noGitPlus: true })
15
+ const ownerPath = info.user.split('/')
16
+
17
+ // Get the branch name.
18
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: folder }).toString().trim()
19
+
20
+ // Get the commit identifier and timestamp.
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
+
25
+ // Get the tags for the current commit.
26
+ let tags = execSync('git tag --points-at HEAD', { cwd: folder }).toString().trim()
27
+ tags = '["' + tags.split('\n').join('","') + '"]'
28
+ tags = JSON.parse(tags).filter(tag => tag)
29
+
30
+ // Get the list of unique repo contributors (authors and committers).
31
+ const template = '"{\\\"name\\\":\\\"%cn\\\",\\\"email\\\":\\\"%ce\\\"}%n{\\\"name\\\":\\\"%an\\\",\\\"email\\\":\\\"%ae\\\"}"'
32
+ let contributors = execSync(`git log --pretty=${template} | sort -u`, { cwd: folder }).toString().trim()
33
+ contributors = '[' + contributors.split('\n').join(',') + ']'
34
+ contributors = JSON.parse(contributors)
35
+
36
+ const script = `MAX_LENGTH=4;
37
+ git rev-list --abbrev=4 --abbrev-commit --all | \
38
+ ( while read -r line; do
39
+ if [ \${#line} -gt $MAX_LENGTH ]; then
40
+ MAX_LENGTH=\${#line};
41
+ fi
42
+ done && printf %s\\\\n "$MAX_LENGTH"
43
+ )`
44
+ const abbrevs = Number(execSync(script, { cwd: folder }).toString().trim())
45
+
46
+ /*
47
+ // Get the total lines of code in the repo.
48
+ const loc = execSync('git ls-files -z ${1} | xargs -0 cat | wc -l', { cwd: folder }).toString().trim()
49
+ */
50
+
51
+ // Return the repo metadata.
52
+ return {
53
+ type: 'git',
54
+ repo: {
55
+ url: {
56
+ origin: originUrl,
57
+ https: info.https()
58
+ },
59
+ source: {
60
+ type: info.type,
61
+ domain: info.domain
62
+ },
63
+ owner: ownerPath[0],
64
+ path: ownerPath.slice(1).join('/'),
65
+ name: info.project,
66
+ abbrevs,
67
+ contributors
68
+ },
69
+ commit: {
70
+ id: fullCommitId,
71
+ time: commitTime,
72
+ branch,
73
+ tags
74
+ }
75
+ }
76
+ } catch (error) {
77
+ return {
78
+ type: 'error',
79
+ error: {
80
+ code: 'E_GIT_METADATA',
81
+ details: error
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ module.exports = {
88
+ metadata
89
+ }