@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.4",
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"
@@ -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
@@ -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
@@ -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
@@ -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 tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'radar-')) // temporary output directory
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
- const isTelemetryEnabled = telemetry.enabled()
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()) await telemetry.send(`scans/:scanID/failed`, { scanID })
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 (isTelemetryEnabled && scanID) {
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 (isTelemetryEnabled && scanID) {
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 (isTelemetryEnabled && scanID) {
190
+ if (telemetry.enabled && scanID) {
189
191
  await telemetry.send(`scans/:scanID/completed`, { scanID }, summary)
190
192
  }
191
193
 
@@ -1,6 +1,6 @@
1
- const telemetry = require('../telemetry')
1
+ const { Telemetry } = require('../telemetry')
2
2
  module.exports = {
3
3
  toolbox: {
4
- telemetry
4
+ telemetry: new Telemetry()
5
5
  }
6
6
  }
@@ -1,111 +1,125 @@
1
- const package = require('../../package.json')
1
+ const pkg = require('../../package.json')
2
2
  const { DateTime } = require("luxon")
3
+ const { jwtDecode } = require('jwt-decode')
3
4
 
4
- const EWA_URL = process.env.EWA_URL ?? 'https://bff.eurekadevsecops.com'
5
- const VDBE_URL = process.env.VDBE_URL ?? 'https://vulns.eurekadevsecops.com'
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
- const USER_AGENT = `Radar/${package.version} (${package.pkgname}@${package.version}; ${process?.platform}-${process?.arch}; ${process?.release?.name}-${process?.version})`
10
+ constructor() {
11
+ this.enabled = !!this.#EUREKA_AGENT_TOKEN
12
+ this.#EWA_URL = this.#claims(this.#EUREKA_AGENT_TOKEN).aud
13
+ }
8
14
 
9
- const enabled = () => {
10
- if (process.env.EUREKA_AGENT_TOKEN) return true
11
- return false
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
- const receive = async (path, params, token) => {
15
- return fetch(toReceiveURL(path, params), {
16
- method: 'GET',
17
- headers: {
18
- 'Authorization': `Bearer ${token ?? process.env.EUREKA_AGENT_TOKEN}`,
19
- 'User-Agent': USER_AGENT,
20
- 'Accept': 'application/json'
21
- }
22
- }).then(async (res) => {
23
- const responseJson = await res.json();
24
- return responseJson;
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
- const receiveSensitive = async (path, params) => {
29
- return receive(path, params, await token())
30
- }
52
+ async receiveSensitive(path, params) {
53
+ return this.receive(path, params, await this.#token())
54
+ }
31
55
 
32
- const send = async (path, params, body, token) => {
33
- return fetch(toPostURL(path, params), {
34
- method: 'POST',
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
- const sendSensitive = async (path, params, body) => {
51
- return send(path, params, body, await token())
52
- }
60
+ #claims(jwt) {
61
+ let claims = undefined
62
+ try { claims = jwtDecode(jwt) } catch (error) {}
63
+ return claims ?? {}
64
+ }
53
65
 
54
- const token = async () => {
55
- const response = await fetch(`${EWA_URL}/vdbe/token`, {
56
- method: 'POST',
57
- headers: {
58
- 'Authorization': `Bearer ${process.env.EUREKA_AGENT_TOKEN}`,
59
- 'Content-Type': 'application/json',
60
- 'User-Agent': USER_AGENT,
61
- 'Accept': 'application/json'
62
- }
63
- })
64
- if (!response.ok) throw new Error(`Internal Error: Failed to get VDBE auth token from EWA: ${response.statusText}: ${await response.text()}`)
65
- const data = await response.json()
66
- return data.token
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
- const toPostURL = (path, params) => {
70
- if (path === `scans/started`) return `${EWA_URL}/scans/started`
71
- if (path === `scans/:scanID/completed`) return `${EWA_URL}/scans/${params.scanID}/completed`
72
- if (path === `scans/:scanID/failed`) return `${EWA_URL}/scans/${params.scanID}/completed`
73
- if (path === `scans/:scanID/results`) return `${VDBE_URL}/scans/${params.scanID}/results`
74
- throw new Error(`Internal Error: Unknown telemetry event: POST ${path}`)
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
- const toReceiveURL = (path, params) => {
78
- if (path === `scans/:scanID/summary`) return `${VDBE_URL}/scans/${params.scanID}/summary?profileId=${process.env.EUREKA_PROFILE}`
79
- throw new Error(`Internal Error: Unknown telemetry event: GET ${path}`)
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
- const toContentType = (path) => {
83
- if (path === `scans/:scanID/log`) return 'text/plain'
84
- return 'application/json'
85
- }
96
+ #toContentType(path) {
97
+ if (path === `scans/:scanID/log`) return 'text/plain'
98
+ return 'application/json'
99
+ }
86
100
 
87
- const toBody = (path, body) => {
88
- if (path === `scans/started`) body = { ...body, timestamp: DateTime.now().toISO(), profile_id: process.env.EUREKA_PROFILE }
89
- if (path === `scans/:scanID/completed`) body = { ...toFindings(body), timestamp: DateTime.now().toISO(), status: 'success', log: { sizeBytes: 0, warnings: 0, errors: 0, link: 'none' }, params: { id: '' }}
90
- 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: '' }}
91
- if (path === `scans/:scanID/results`) body = { findings: body.findings /* SARIF */, profileId: process.env.EUREKA_PROFILE, log: Buffer.from(body.log, 'utf8').toString('base64') }
92
- return JSON.stringify(body)
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
- const toFindings = (summary) => ({
96
- findings: {
97
- total: summary.errors.length + summary.warnings.length + summary.notes.length,
98
- critical: 0,
99
- high: summary.errors.length,
100
- med: summary.warnings.length,
101
- low: summary.notes.length
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
- enabled,
107
- receive,
108
- receiveSensitive,
109
- send,
110
- sendSensitive
124
+ Telemetry
111
125
  }