@eurekadevsecops/radar 1.3.4 → 1.4.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.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Radar is an open-source orchestrator of security scanners.",
|
|
5
5
|
"homepage": "https://www.eurekadevsecops.com/radar",
|
|
6
6
|
"keywords": [
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"@persistr/clif": "^1.11.0",
|
|
31
31
|
"@persistr/clif-plugin-settings": "^2.3.1",
|
|
32
32
|
"humanize-duration": "^3.33.0",
|
|
33
|
+
"jwt-decode": "^4.0.0",
|
|
33
34
|
"luxon": "^3.7.1",
|
|
34
35
|
"smol-toml": "^1.4.1",
|
|
35
36
|
"tiny-spinner": "^2.0.5"
|
package/scanners/depscan/run.sh
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
# $3 - Path to the output folder where scan results should be stored
|
|
5
5
|
|
|
6
6
|
set -e
|
|
7
|
-
docker run --rm -v $1:/app -v $2:/input -v $3:/output ghcr.io/eurekadevsecops/radar-depscan 2>&1
|
|
7
|
+
docker run --rm -u $(id -u):$(id -g) -v $1:/app -v $2:/input -v $3:/output ghcr.io/eurekadevsecops/radar-depscan 2>&1
|
|
8
8
|
cp $3/depscan/depscan.sarif $3/depscan.sarif
|
package/scanners/gitleaks/run.sh
CHANGED
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
# $3 - Path to the output folder where scan results should be stored
|
|
5
5
|
|
|
6
6
|
set -e
|
|
7
|
-
docker run --rm -v $1:/app -v $2:/input -v $3:/output zricethezav/gitleaks dir --exit-code 0 -f sarif -r /output/gitleaks.sarif /app 2>&1
|
|
7
|
+
docker run --rm -u $(id -u):$(id -g) -v $1:/app -v $2:/input -v $3:/output zricethezav/gitleaks dir --exit-code 0 -f sarif -r /output/gitleaks.sarif /app 2>&1
|
package/scanners/opengrep/run.sh
CHANGED
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
# $3 - Path to the output folder where scan results should be stored
|
|
5
5
|
|
|
6
6
|
set -e
|
|
7
|
-
docker run --rm -v $1:/app -v $2:/input -v $3:/output ghcr.io/eurekadevsecops/radar-opengrep 2>&1
|
|
7
|
+
docker run --rm -u $(id -u):$(id -g) -v $1:/app -v $2:/input -v $3:/output ghcr.io/eurekadevsecops/radar-opengrep 2>&1
|
package/src/commands/scan.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const crypto = require('node:crypto')
|
|
1
2
|
const fs = require('node:fs')
|
|
2
3
|
const path = require('node:path')
|
|
3
4
|
const os = require('node:os')
|
|
@@ -124,7 +125,9 @@ module.exports = {
|
|
|
124
125
|
return severity
|
|
125
126
|
})
|
|
126
127
|
const assets = path.join(__dirname, '..', '..', 'scanners') // scanner assets
|
|
127
|
-
const
|
|
128
|
+
const scansdir = path.join(os.homedir(), '.radar', 'scans')
|
|
129
|
+
const tmpdir = path.join(scansdir, crypto.randomUUID()) // temporary output directory
|
|
130
|
+
fs.mkdirSync(tmpdir, { recursive: true })
|
|
128
131
|
const outfile = args.OUTPUT ? path.resolve(args.OUTPUT) : undefined // output file, if any
|
|
129
132
|
|
|
130
133
|
// Validate scan parameters.
|
|
@@ -133,8 +136,7 @@ module.exports = {
|
|
|
133
136
|
|
|
134
137
|
// Send telemetry: scan started.
|
|
135
138
|
let scanID = undefined
|
|
136
|
-
|
|
137
|
-
if (isTelemetryEnabled) {
|
|
139
|
+
if (telemetry.enabled) {
|
|
138
140
|
// TODO: Should pass scanID to the server; not read it from the server.
|
|
139
141
|
try {
|
|
140
142
|
const res = await telemetry.send(`scans/started`, {}, { scanners: scanners.map((s) => s.name) })
|
|
@@ -157,7 +159,7 @@ module.exports = {
|
|
|
157
159
|
catch (error) {
|
|
158
160
|
log(`\n${error}`)
|
|
159
161
|
if (!args.QUIET) log('Scan NOT completed!')
|
|
160
|
-
if (telemetry.enabled
|
|
162
|
+
if (telemetry.enabled) await telemetry.send(`scans/:scanID/failed`, { scanID })
|
|
161
163
|
fs.rmSync(tmpdir, { recursive: true, force: true }) // Clean up.
|
|
162
164
|
return 0x10 // exit code
|
|
163
165
|
}
|
|
@@ -170,13 +172,13 @@ module.exports = {
|
|
|
170
172
|
if (outfile) fs.writeFileSync(outfile, JSON.stringify(results.sarif))
|
|
171
173
|
|
|
172
174
|
// Send telemetry: scan results.
|
|
173
|
-
if (
|
|
175
|
+
if (telemetry.enabled && scanID) {
|
|
174
176
|
await telemetry.sendSensitive(`scans/:scanID/results`, { scanID }, { findings: results.sarif, log: results.log })
|
|
175
177
|
}
|
|
176
178
|
|
|
177
179
|
// Analyze scan results: group findings by severity level.
|
|
178
180
|
let summary
|
|
179
|
-
if (
|
|
181
|
+
if (telemetry.enabled && scanID) {
|
|
180
182
|
const analysis = await telemetry.receiveSensitive(`scans/:scanID/summary`, { scanID })
|
|
181
183
|
if (!analysis?.summary) throw new Error(`Failed to retrieve analysis summary for scan '${scanID}'`)
|
|
182
184
|
summary = analysis.summary.findingsBySeverity
|
|
@@ -185,7 +187,7 @@ module.exports = {
|
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
// Send telemetry: scan summary.
|
|
188
|
-
if (
|
|
190
|
+
if (telemetry.enabled && scanID) {
|
|
189
191
|
await telemetry.send(`scans/:scanID/completed`, { scanID }, summary)
|
|
190
192
|
}
|
|
191
193
|
|
package/src/plugins/telemetry.js
CHANGED
package/src/telemetry/index.js
CHANGED
|
@@ -1,111 +1,125 @@
|
|
|
1
|
-
const
|
|
1
|
+
const pkg = require('../../package.json')
|
|
2
2
|
const { DateTime } = require("luxon")
|
|
3
|
+
const { jwtDecode } = require('jwt-decode')
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
class Telemetry {
|
|
6
|
+
#EUREKA_AGENT_TOKEN = process.env.EUREKA_AGENT_TOKEN
|
|
7
|
+
#USER_AGENT = `RadarCLI/${pkg.version} (${pkg.pkgname}@${pkg.version}; ${process?.platform}-${process?.arch}; ${process?.release?.name}-${process?.version})`
|
|
8
|
+
#EWA_URL
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
constructor() {
|
|
11
|
+
this.enabled = !!this.#EUREKA_AGENT_TOKEN
|
|
12
|
+
this.#EWA_URL = this.#claims(this.#EUREKA_AGENT_TOKEN).aud
|
|
13
|
+
}
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
async send(path, params, body, token) {
|
|
16
|
+
return fetch(this.#toPostURL(path, params, token), {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: {
|
|
19
|
+
'Authorization': `Bearer ${token ?? this.#EUREKA_AGENT_TOKEN}`,
|
|
20
|
+
'Content-Type': this.#toContentType(path),
|
|
21
|
+
'User-Agent': this.#USER_AGENT,
|
|
22
|
+
'Accept': 'application/json'
|
|
23
|
+
},
|
|
24
|
+
body: this.#toBody(path, body)
|
|
25
|
+
})
|
|
26
|
+
.then(async (res) => {
|
|
27
|
+
//TODO: Display this on stdout only if --debug option is selected on the cmd line.
|
|
28
|
+
//if (!res.ok) console.log(`POST ${this.#toPostURL(path, params, token)} [${res.status}] ${res.statusText}: ${await res.text()}`)
|
|
29
|
+
return res
|
|
30
|
+
})
|
|
31
|
+
}
|
|
13
32
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
async sendSensitive(path, params, body) {
|
|
34
|
+
return this.send(path, params, body, await this.#token())
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async receive(path, params, token) {
|
|
38
|
+
return fetch(this.#toReceiveURL(path, params, token), {
|
|
39
|
+
method: 'GET',
|
|
40
|
+
headers: {
|
|
41
|
+
'Authorization': `Bearer ${token ?? this.#EUREKA_AGENT_TOKEN}`,
|
|
42
|
+
'User-Agent': this.#USER_AGENT,
|
|
43
|
+
'Accept': 'application/json'
|
|
44
|
+
}
|
|
45
|
+
}).then(async (res) => {
|
|
46
|
+
//TODO: Display this on stdout only if --debug option is selected on the cmd line.
|
|
47
|
+
//if (!res.ok) console.log(`GET ${this.#toReceiveURL(path, params, token)} [${res.status}] ${res.statusText}`)
|
|
48
|
+
return await res.json()
|
|
25
49
|
})
|
|
26
50
|
}
|
|
27
51
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
52
|
+
async receiveSensitive(path, params) {
|
|
53
|
+
return this.receive(path, params, await this.#token())
|
|
54
|
+
}
|
|
31
55
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
headers: {
|
|
36
|
-
'Authorization': `Bearer ${token ?? process.env.EUREKA_AGENT_TOKEN}`,
|
|
37
|
-
'Content-Type': toContentType(path),
|
|
38
|
-
'User-Agent': USER_AGENT,
|
|
39
|
-
'Accept': 'application/json'
|
|
40
|
-
},
|
|
41
|
-
body: toBody(path, body)
|
|
42
|
-
})
|
|
43
|
-
.then(async (res) => {
|
|
44
|
-
// TODO: Display this on stdout only if --debug option is selected on the cmd line.
|
|
45
|
-
// if (!res.ok) console.log(`POST ${toURL(path, params)} [${res.status}] ${res.statusText}: ${await res.text()}`)
|
|
46
|
-
return res
|
|
47
|
-
})
|
|
48
|
-
}
|
|
56
|
+
//
|
|
57
|
+
// private
|
|
58
|
+
//
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
60
|
+
#claims(jwt) {
|
|
61
|
+
let claims = undefined
|
|
62
|
+
try { claims = jwtDecode(jwt) } catch (error) {}
|
|
63
|
+
return claims ?? {}
|
|
64
|
+
}
|
|
53
65
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
66
|
+
async #token() {
|
|
67
|
+
const response = await fetch(`${this.#EWA_URL}/vdbe/token`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: {
|
|
70
|
+
'Authorization': `Bearer ${this.#EUREKA_AGENT_TOKEN}`,
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
'User-Agent': this.#USER_AGENT,
|
|
73
|
+
'Accept': 'application/json'
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
if (!response.ok) throw new Error(`Internal Error: Failed to get VDBE auth token from EWA: ${response.statusText}: ${await response.text()}`)
|
|
77
|
+
const data = await response.json()
|
|
78
|
+
return data.token
|
|
79
|
+
}
|
|
68
80
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
81
|
+
#toPostURL(path, params, token) {
|
|
82
|
+
const claims = this.#claims(token ?? this.#EUREKA_AGENT_TOKEN)
|
|
83
|
+
if (path === `scans/started`) return `${claims.aud}/scans/started`
|
|
84
|
+
if (path === `scans/:scanID/completed`) return `${claims.aud}/scans/${params.scanID}/completed`
|
|
85
|
+
if (path === `scans/:scanID/failed`) return `${claims.aud}/scans/${params.scanID}/completed`
|
|
86
|
+
if (path === `scans/:scanID/results`) return `${claims.aud}/scans/${params.scanID}/results`
|
|
87
|
+
throw new Error(`Internal Error: Unknown telemetry event: POST ${path}`)
|
|
88
|
+
}
|
|
76
89
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
90
|
+
#toReceiveURL(path, params, token) {
|
|
91
|
+
const claims = this.#claims(token ?? this.#EUREKA_AGENT_TOKEN)
|
|
92
|
+
if (path === `scans/:scanID/summary`) return `${claims.aud}/scans/${params.scanID}/summary?profileId=${process.env.EUREKA_PROFILE}`
|
|
93
|
+
throw new Error(`Internal Error: Unknown telemetry event: GET ${path}`)
|
|
94
|
+
}
|
|
81
95
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
96
|
+
#toContentType(path) {
|
|
97
|
+
if (path === `scans/:scanID/log`) return 'text/plain'
|
|
98
|
+
return 'application/json'
|
|
99
|
+
}
|
|
86
100
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
101
|
+
#toBody(path, body) {
|
|
102
|
+
if (path === `scans/started`) body = { ...body, timestamp: DateTime.now().toISO(), profile_id: process.env.EUREKA_PROFILE }
|
|
103
|
+
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
|
+
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: '' }}
|
|
105
|
+
if (path === `scans/:scanID/results`) body = { findings: body.findings /* SARIF */, profileId: process.env.EUREKA_PROFILE, log: Buffer.from(body.log, 'utf8').toString('base64') }
|
|
106
|
+
return JSON.stringify(body)
|
|
107
|
+
}
|
|
94
108
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
#toFindings(summary) {
|
|
110
|
+
return {
|
|
111
|
+
findings: {
|
|
112
|
+
total: summary.errors.length + summary.warnings.length + summary.notes.length,
|
|
113
|
+
critical: 0,
|
|
114
|
+
high: summary.errors.length,
|
|
115
|
+
med: summary.warnings.length,
|
|
116
|
+
low: summary.notes.length
|
|
117
|
+
}
|
|
118
|
+
}
|
|
102
119
|
}
|
|
103
|
-
|
|
120
|
+
|
|
121
|
+
}
|
|
104
122
|
|
|
105
123
|
module.exports = {
|
|
106
|
-
|
|
107
|
-
receive,
|
|
108
|
-
receiveSensitive,
|
|
109
|
-
send,
|
|
110
|
-
sendSensitive
|
|
124
|
+
Telemetry
|
|
111
125
|
}
|