@eurekadevsecops/radar 1.1.0 → 1.2.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/cli.js +1 -2
- package/package.json +2 -1
- package/scan.sarif +1 -0
- package/src/commands/scan.js +35 -5
- package/src/index.js +4 -1
- package/src/plugins/telemetry.js +6 -0
- package/src/telemetry/README.md +52 -0
- package/src/telemetry/index.js +73 -0
- package/src/util/sarif/merge.js +15 -0
package/cli.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// require('dotenv').config()
|
|
4
4
|
const path = require('node:path')
|
|
5
|
-
const
|
|
6
|
-
const cli = require(path.join(__dirname, 'src')).build({ plugins })
|
|
5
|
+
const cli = require(path.join(__dirname, 'src')).build()
|
|
7
6
|
|
|
8
7
|
// Check for updates (not in browsers).
|
|
9
8
|
cli.checkForUpdates()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eurekadevsecops/radar",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
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
|
+
"luxon": "^3.7.1",
|
|
33
34
|
"smol-toml": "^1.4.1",
|
|
34
35
|
"tiny-spinner": "^2.0.5"
|
|
35
36
|
},
|
package/scan.sarif
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"2.1.0","$schema":"https://json.schemastore.org/sarif-2.1.0.json","runs":[{"tool":{"driver":{"name":"gitleaks","semanticVersion":"v8.0.0","informationUri":"https://github.com/gitleaks/gitleaks","properties":{"officialName":"gitleaks"},"rules":[{"id":"generic-api-key","shortDescription":{"text":"Detected a Generic API Key, potentially exposing access to various services and sensitive operations."}},{"id":"private-key","shortDescription":{"text":"Identified a Private Key, which may compromise cryptographic security and sensitive data encryption."}}]}},"results":[{"message":{"text":"generic-api-key has detected secret for file /app/app/services/key_service.ts."},"ruleId":"generic-api-key","locations":[{"physicalLocation":{"artifactLocation":{"uri":"/app/app/services/key_service.ts"},"region":{"startLine":10,"startColumn":44,"endLine":10,"endColumn":75,"snippet":{"text":"private_pkcs8.pem"}}}}],"properties":{"tags":[]}},{"message":{"text":"private-key has detected secret for file /app/keys/vdbe.private.pem."},"ruleId":"private-key","locations":[{"physicalLocation":{"artifactLocation":{"uri":"/app/keys/vdbe.private.pem"},"region":{"startLine":1,"startColumn":1,"endLine":28,"endColumn":26,"snippet":{"text":"-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDT5JLxkpJH2pkv\ntTmZgEduEHGVO8sv4xLpcBCKgSpVh9apBZnJJgDOl77m7zv2VDycjTQLq4dMk/CT\nBWt50kx+e/jHCP8f64UxbDPMxgDgrYj6YaAoho9coJeu6nG+6h2PNFXM941CrA9V\nddEc9K9piYXSLlr2bHBKeLeuH87Qpmx3bEr8jAXdA51zss2ZsUw5qRH6oWMcqWhC\n5CevbWmFXdoo2W8XfGAwpV2D251uUt6oS4qwU5VT/byR65lewxq3YqPvRpQa5plo\n3aTVypyaqy4l5T2jfLiywXpAuhVPzso4EeDx2Jtfvhh03Is59DFQkQwuXS9caqyY\nDtndupYZAgMBAAECggEADU+dn4laoSLtXp7e2HLDifmEeSCBeiekp39/uoO0uhzH\ncmTErWsyv8eumWlL9gSCrj78lwMWg8wDX+kGQGfioEt/bFl3VXUBMAKhGmsR4Qtl\nwHzjh8g0N1hrTvSxYpHoe3eJMFAY0qhmajL1iQEiB9o4yuRYmIRlZXhB6bFb16WP\nflFcGS/KKxXleco8aEZIu2+X3hXwS2/lDD1sIQua65qXhHZMpB4TfgR2B5r87P9Y\nv5Ilb53nDMWbWDCA8ZifG2fjl29X4bujlJy/fu6Y5UVZB5EIQymKDnnWvemZUFrw\nRtjrJJ9mhM8ON2bYI2LHTMhBixpa3OM3jLQc2FeGRQKBgQDzbv6M4zp8foDU+Ji9\nYlhe0wt6t1EMJQrTgS0h0gbm9ekVLXHDm/hMAUQss6YorXY4Iih/e9HEvNf1byx9\nsfVlkNHrlsNklMmG/jGhzs7xpMOgyq8uzzuY9o/gIFzT3LiuTnk23poGNtDAwNVV\nkaxWOiDtpKFiiFbhRW/pos01PwKBgQDe1MPmW7wnwKD34ee/GaTZpQZ4wH8jdTa1\nVM5xOQKGVyZ5zTc5ao7cR0KYN22ycCI/PwIWWaufDWAYuunRfJP1sWqKf4ENQd5P\nxh66uv3qxETOJ4jk2D8O8fysHLyQu8KvS5TvH/UzNiLRHVv9vfyywmWHOpVP5C1i\nvSiC4yqmpwKBgEEtZ7Q7Jq6shDwBb4vNaBHDeeBacrXIuTRV8sqKXFS8ZLLJ3xrb\niMh40lMRqpxbjTqMUsGHWmvNkBjjskrZOfX+p2XnkNs+RxMAvjMvlxL15XcIrYzf\n6XoUEgOVRqVnBH+O/T9mrGCbjpr9RmFJxpWzrJtUJ+2kyXY5TDSG5WCrAoGAaaFc\nmCemYwXKiJdbP1jNr6quDbHa0xkubPkdv8hxrPNFNvoUErCztjJFnFiyNKM5aNfa\ninPJimVRx4dbbcXrcc2/npXgvEMcOp7FVGluEsslfsB5AVqNUe1ehMw+izGmkWh3\n2n9Awh0Ili6fvAJC9w52CIu52hxlc2gN+zXqswMCgYEA7WfWRh+Rz/3RPlsDBuIu\nu78M6hZ5Uj2SneTtYhXOahMuSGQY4vVd8Z23PBudLVNjNvw05532cVJQvDLGrPeg\nMgig/vMURbQIN5TXVtL7vgyi4QXQgCdHET8YEJrPleyttv/lOJv3ZBUfzKtb9/2s\nK47YjsOczOFh0NbeYPXDZrY=\n-----END PRIVATE KEY-----"}}}}],"properties":{"tags":[]}}]}]}
|
package/src/commands/scan.js
CHANGED
|
@@ -82,7 +82,7 @@ module.exports = {
|
|
|
82
82
|
'$ radar scan -e warning,note ' + '(treat warnings and notes as errors)'.grey
|
|
83
83
|
],
|
|
84
84
|
run: async (toolbox, args) => {
|
|
85
|
-
const { log, scanners: availableScanners } = toolbox
|
|
85
|
+
const { log, scanners: availableScanners, telemetry } = toolbox
|
|
86
86
|
|
|
87
87
|
// Set defaults.
|
|
88
88
|
args.TARGET = path.normalize(args.TARGET ?? process.cwd())
|
|
@@ -124,8 +124,19 @@ module.exports = {
|
|
|
124
124
|
// At least one scanner must be selected in order to have a successful scan.
|
|
125
125
|
if (scanners.length === 0) throw new Error('No available scanners selected.')
|
|
126
126
|
|
|
127
|
+
// Send telemetry: scan started.
|
|
128
|
+
let scanID = undefined
|
|
129
|
+
const isTelemetryEnabled = telemetry.enabled()
|
|
130
|
+
if (isTelemetryEnabled) {
|
|
131
|
+
// TODO: Should pass scanID to the server; not read it from the server.
|
|
132
|
+
const response = await telemetry.send(`scans/started`, {}, { scanners: scanners.map((scanner) => scanner.name) })
|
|
133
|
+
const data = await response.json()
|
|
134
|
+
scanID = data.scan_id
|
|
135
|
+
}
|
|
136
|
+
|
|
127
137
|
// Run scanners.
|
|
128
138
|
let isScanCompleted = true
|
|
139
|
+
let runLog = ''
|
|
129
140
|
log(`Running ${scanners.length} of ${availableScanners.length} scanners:`)
|
|
130
141
|
for (const scanner of scanners) {
|
|
131
142
|
let label = scanner.name
|
|
@@ -148,7 +159,9 @@ module.exports = {
|
|
|
148
159
|
cmd = cmd.replaceAll('${output}', outdir)
|
|
149
160
|
/* eslint-enable no-template-curly-in-string */
|
|
150
161
|
|
|
151
|
-
|
|
162
|
+
const { stdout } = await exec(cmd)
|
|
163
|
+
runLog += stdout
|
|
164
|
+
|
|
152
165
|
if (!args.QUIET) spinner.success(label)
|
|
153
166
|
} catch (error) {
|
|
154
167
|
isScanCompleted = false
|
|
@@ -213,16 +226,32 @@ module.exports = {
|
|
|
213
226
|
fs.writeFileSync(outfile, JSON.stringify(sarif))
|
|
214
227
|
}
|
|
215
228
|
|
|
216
|
-
// TODO: Upload SARIF to user's Eureka account.
|
|
217
|
-
|
|
218
229
|
// Count findings by severity level.
|
|
219
230
|
const summary = await sariftools.summarize(sarif, target)
|
|
220
231
|
|
|
232
|
+
// Send telemetry.
|
|
233
|
+
if (isTelemetryEnabled && scanID) {
|
|
234
|
+
// Scan completed.
|
|
235
|
+
telemetry.send(`scans/:scanID/completed`, { scanID }, {
|
|
236
|
+
findings: {
|
|
237
|
+
total: summary.errors.length + summary.warnings.length + summary.notes.length,
|
|
238
|
+
critical: 0,
|
|
239
|
+
high: summary.errors.length,
|
|
240
|
+
med: summary.warnings.length,
|
|
241
|
+
low: summary.notes.length
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
// Send sensitive telemetry: scan log and scan findings.
|
|
246
|
+
telemetry.sendSensitive(`scans/:scanID/log`, { scanID }, runLog)
|
|
247
|
+
telemetry.sendSensitive(`scans/:scanID/findings`, { scanID }, { findings: sarif })
|
|
248
|
+
}
|
|
249
|
+
|
|
221
250
|
// Display summarized findings.
|
|
222
251
|
if (!args.QUIET) {
|
|
223
252
|
log()
|
|
224
253
|
sariftools.display_findings(summary, args.FORMAT, log)
|
|
225
|
-
if (outfile) log(`Findings exported to ${
|
|
254
|
+
if (outfile) log(`Findings exported to ${outfile}`)
|
|
226
255
|
sariftools.display_totals(summary, args.FORMAT, log)
|
|
227
256
|
}
|
|
228
257
|
|
|
@@ -238,6 +267,7 @@ module.exports = {
|
|
|
238
267
|
} else {
|
|
239
268
|
exitCode = 0x10
|
|
240
269
|
if (!args.QUIET) log('Scan NOT completed!')
|
|
270
|
+
if (telemetry.enabled()) telemetry.send(`scans/:scanID/failed`, { scanID })
|
|
241
271
|
}
|
|
242
272
|
|
|
243
273
|
// Clean up.
|
package/src/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
const { build } = require('@persistr/clif')
|
|
2
2
|
const pkg = require('../package.json')
|
|
3
3
|
const commands = require('./commands')
|
|
4
|
+
const path = require('node:path')
|
|
4
5
|
|
|
5
6
|
// Plugins.
|
|
6
7
|
const plugins = {
|
|
7
|
-
settings: require('@persistr/clif-plugin-settings')
|
|
8
|
+
settings: require('@persistr/clif-plugin-settings'),
|
|
9
|
+
scanners: require(path.join(__dirname, 'plugins', 'scanners')),
|
|
10
|
+
telemetry: require(path.join(__dirname, 'plugins', 'telemetry'))
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
module.exports = {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# TELEMETRY
|
|
2
|
+
|
|
3
|
+
## How to use this module to send telemetry
|
|
4
|
+
|
|
5
|
+
### Step 1: Import the module:
|
|
6
|
+
```js
|
|
7
|
+
const telemetry = require('./telemetry')
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
### Step 2: Call telemetry.send to send telemetry. Choose one of several telemetry events available.
|
|
11
|
+
|
|
12
|
+
All available telemetry events:
|
|
13
|
+
```js
|
|
14
|
+
telemetry.send(`scans/:scanID/started`, { scanID }, { scanners })
|
|
15
|
+
telemetry.send(`scans/:scanID/completed`, { scanID }, { status, findings, log })
|
|
16
|
+
telemetry.sendSensitive(`scans/:scanID/log`, { scanID }, log)
|
|
17
|
+
telemetry.sendSensitive(`scans/:scanID/findings`, { scanID }, sarif)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
To send a telemetry event indicating that a scan has started:
|
|
21
|
+
```js
|
|
22
|
+
telemetry.send(`scans/:scanID/started`, { scanID }, { scanners })
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
To send a telemetry event indicating that a scan has completed:
|
|
26
|
+
```js
|
|
27
|
+
telemetry.send(`scans/:scanID/completed`, { scanID }, { status, findings, log })
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
To send a telemetry event with the scan console log:
|
|
31
|
+
```js
|
|
32
|
+
telemetry.sendSensitive(`scans/:scanID/log`, { scanID }, log)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
To send a telemetry event with the scan vulnerability findings:
|
|
36
|
+
```js
|
|
37
|
+
telemetry.sendSensitive(`scans/:scanID/findings`, { scanID }, sarif)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Step 3: (optional) You can await on telemetry.send to read the fetch response.
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
const response = await telemetry.send(`scans/:scanID/started`, { scanID }, { scanners })
|
|
44
|
+
console.log(response.statusCode)
|
|
45
|
+
console.log(await response.json())
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Sensitive data in telemtry events
|
|
49
|
+
|
|
50
|
+
NOTE: Telemetry events that contain vulnerability data are sent to the Vulnerability
|
|
51
|
+
Data Backend (VDBE) which can be hosted by customers directly on their own infrastructure.
|
|
52
|
+
These telemetry events must be sent using the `sendSensitive` function.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const package = require('../../package.json')
|
|
2
|
+
const { DateTime } = require("luxon")
|
|
3
|
+
|
|
4
|
+
const EWA_URL = process.env.EWA_URL ?? 'https://app.eurekadevsecops.com'
|
|
5
|
+
const VDBE_URL = process.env.VDBE_URL ?? 'https://vulns.eurekadevsecops.com'
|
|
6
|
+
|
|
7
|
+
const USER_AGENT = `Radar/${package.version} (${package.pkgname}@${package.version}; ${process?.platform}-${process?.arch}; ${process?.release?.name}-${process?.version})`
|
|
8
|
+
|
|
9
|
+
const enabled = () => {
|
|
10
|
+
if (process.env.EUREKA_AGENT_TOKEN) return true
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const send = async (path, params, body, token) => {
|
|
15
|
+
return fetch(toURL(path, params), {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: {
|
|
18
|
+
'Authorization': `Bearer ${token ?? process.env.EUREKA_AGENT_TOKEN}`,
|
|
19
|
+
'Content-Type': toContentType(path),
|
|
20
|
+
'User-Agent': USER_AGENT,
|
|
21
|
+
'Accept': 'application/json'
|
|
22
|
+
},
|
|
23
|
+
body: toBody(path, body)
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const sendSensitive = async (path, params, body) => {
|
|
28
|
+
return send(path, params, body, await token())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const token = async () => {
|
|
32
|
+
const response = await fetch(`${EWA_URL}/vdbe/token`, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: {
|
|
35
|
+
'Authorization': `Bearer ${process.env.EUREKA_AGENT_TOKEN}`,
|
|
36
|
+
'Content-Type': 'application/json',
|
|
37
|
+
'User-Agent': USER_AGENT,
|
|
38
|
+
'Accept': 'application/json'
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
if (!response.ok) throw new Error(`Internal Error: Failed to get VDBE auth token from EWA: ${response.statusText}: ${await response.text()}`)
|
|
42
|
+
const data = await response.json()
|
|
43
|
+
return data.token
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const toURL = (path, params) => {
|
|
47
|
+
if (path === `scans/started`) return `${EWA_URL}/scans/started`
|
|
48
|
+
if (path === `scans/:scanID/completed`) return `${EWA_URL}/scans/${params.scanID}/completed`
|
|
49
|
+
if (path === `scans/:scanID/failed`) return `${EWA_URL}/scans/${params.scanID}/completed`
|
|
50
|
+
if (path === `scans/:scanID/log`) return `${VDBE_URL}/scans/${params.scanID}/log`
|
|
51
|
+
if (path === `scans/:scanID/findings`) return `${VDBE_URL}/scans/${params.scanID}/findings`
|
|
52
|
+
throw new Error(`Internal Error: Unknown telemetry event: ${path}`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const toContentType = (path) => {
|
|
56
|
+
if (path === `scans/:scanID/log`) return 'text/plain'
|
|
57
|
+
return 'application/json'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const toBody = (path, body) => {
|
|
61
|
+
if (path === `scans/:scanID/log`) return body
|
|
62
|
+
if (path === `scans/started`) body = { ...body, timestamp: DateTime.now().toISO(), profile_id: process.env.EUREKA_PROFILE }
|
|
63
|
+
if (path === `scans/:scanID/completed`) body = { ...body, timestamp: DateTime.now().toISO(), status: 'success', log: { sizeBytes: 0, warnings: 0, errors: 0, link: 'none' }, params: { id: '' }}
|
|
64
|
+
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: '' }}
|
|
65
|
+
if (path === `scans/:scanID/findings`) body = { ...body, profileId: process.env.EUREKA_PROFILE }
|
|
66
|
+
return JSON.stringify(body)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
enabled,
|
|
71
|
+
send,
|
|
72
|
+
sendSensitive
|
|
73
|
+
}
|
package/src/util/sarif/merge.js
CHANGED
|
@@ -51,5 +51,20 @@ module.exports = async (files, outfile) => {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// Clean up the SARIF object, to conform to the SARIF schema.
|
|
55
|
+
for (const run of sarif.runs) {
|
|
56
|
+
for (const result of run.results) {
|
|
57
|
+
const partialFingerprints = result.partialFingerprints
|
|
58
|
+
if (partialFingerprints) {
|
|
59
|
+
if (!partialFingerprints.commitSha) delete partialFingerprints.commitSha
|
|
60
|
+
if (!partialFingerprints.email) delete partialFingerprints.email
|
|
61
|
+
if (!partialFingerprints.author) delete partialFingerprints.author
|
|
62
|
+
if (!partialFingerprints.date) delete partialFingerprints.date
|
|
63
|
+
if (!partialFingerprints.commitMessage) delete partialFingerprints.commitMessage
|
|
64
|
+
if (Object.keys(partialFingerprints).length === 0) delete result.partialFingerprints
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
fs.writeFileSync(outfile, JSON.stringify(sarif))
|
|
55
70
|
}
|