@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 CHANGED
@@ -2,8 +2,7 @@
2
2
 
3
3
  // require('dotenv').config()
4
4
  const path = require('node:path')
5
- const plugins = { scanners: require(path.join(__dirname, 'src', 'plugins', 'scanners')) }
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.0",
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":[]}}]}]}
@@ -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
- /* const { stdout } = */ await exec(cmd)
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 ${consolidated}`)
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,6 @@
1
+ const telemetry = require('../telemetry')
2
+ module.exports = {
3
+ toolbox: {
4
+ telemetry
5
+ }
6
+ }
@@ -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
+ }
@@ -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
  }