UNPKG

@eurekadevsecops/radar

Version:

Radar is an open-source orchestrator of security scanners.

187 lines (166 loc) 6.44 kB
const pkg = require('../../package.json') const { DateTime } = require("luxon") const { jwtDecode } = require('jwt-decode') class Telemetry { #EUREKA_AGENT_TOKEN = process.env.EUREKA_AGENT_TOKEN #USER_AGENT = `RadarCLI/${pkg.version} (${pkg.name}@${pkg.version}; ${process?.platform}-${process?.arch}; ${process?.release?.name}-${process?.version})` #EWA_URL #failedScanID // ensure there that scan failure is reported only once constructor() { this.enabled = !!this.#EUREKA_AGENT_TOKEN this.#EWA_URL = this.#claims(this.#EUREKA_AGENT_TOKEN).aud?.replace(/\/$/, '') this.#failedScanID = undefined } async send(path, params, body, token) { let res try { res = await this.#sendRaw(path, params, body, token) } catch (error) { await this.#reportScanFailure(path, params) throw error } if (!res.ok) await this.#reportScanFailure(path, params) return res } async sendSensitive(path, params, body) { try { return this.send(path, params, body, await this.#token()) } catch (error) { await this.#reportScanFailure(path, params) throw error } } async receive(path, params, token) { let res try { res = await this.#receiveRaw(path, params, token) } catch (error) { await this.#reportScanFailure(path, params) throw error } if (!res.ok) await this.#reportScanFailure(path, params) try { return await res.json() } catch (error) { await this.#reportScanFailure(path, params) } } async receiveSensitive(path, params) { try { return this.receive(path, params, await this.#token()) } catch (error) { await this.#reportScanFailure(path, params) throw error } } // // private // #claims(jwt) { let claims = undefined try { claims = jwtDecode(jwt) } catch (error) {} return claims ?? {} } async #token() { const response = await fetch(`${this.#EWA_URL}/vdbe/token`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.#EUREKA_AGENT_TOKEN}`, 'Content-Type': 'application/json', 'User-Agent': this.#USER_AGENT, 'Accept': 'application/json' }, body: JSON.stringify({}) }) if (!response.ok) throw new Error(`Internal Error: Failed to get VDBE auth token from EWA: ${response.statusText}: ${await response.text()}`) const data = await response.json() return data.token } async #sendRaw(path, params, body, token) { return fetch(this.#toPostURL(path, params, token), { method: 'POST', headers: { 'Authorization': `Bearer ${token ?? this.#EUREKA_AGENT_TOKEN}`, 'Content-Type': this.#toContentType(path), 'User-Agent': this.#USER_AGENT, 'Accept': 'application/json' }, body: this.#toBody(path, body) }) .then(async (res) => { //TODO: Display this on stdout only if --debug option is selected on the cmd line. //if (!res.ok) console.log(`POST ${this.#toPostURL(path, params, token)} [${res.status}] ${res.statusText}: ${await res.text()}`) return res }) } async #receiveRaw(path, params, token) { return fetch(this.#toReceiveURL(path, params, token), { method: 'GET', headers: { 'Authorization': `Bearer ${token ?? this.#EUREKA_AGENT_TOKEN}`, 'User-Agent': this.#USER_AGENT, 'Accept': 'application/json' } }).then(async (res) => { //TODO: Display this on stdout only if --debug option is selected on the cmd line. //if (!res.ok) console.log(`GET ${this.#toReceiveURL(path, params, token)} [${res.status}] ${res.statusText}`) return res }) } async #reportScanFailure(path, params) { if (!this.enabled) return const scanID = params?.scanID if (!scanID || path === `scans/:scanID/failed`) return // if scan failure already reported, skip if (this.#failedScanID === scanID) return // mark scan failure as reported this.#failedScanID = scanID try { // we could choose to pass the error and send it somewhere possibly await this.#sendRaw(`scans/:scanID/failed`, { scanID }, {}) } catch (error) { } } #toPostURL(path, params, token) { const claims = this.#claims(token ?? this.#EUREKA_AGENT_TOKEN) const aud = claims.aud.replace(/\/$/, '') if (path === `scans/started`) return `${aud}/scans/started` if (path === `scans/:scanID/started`) return `${aud}/scans/${params.scanID}/started` if (path === `scans/:scanID/completed`) return `${aud}/scans/${params.scanID}/completed` if (path === `scans/:scanID/failed`) return `${aud}/scans/${params.scanID}/failed` if (path === `scans/:scanID/results`) return `${aud}/scans/${params.scanID}/results` throw new Error(`Internal Error: Unknown telemetry event: POST ${path}`) } #toReceiveURL(path, params, token) { const claims = this.#claims(token ?? this.#EUREKA_AGENT_TOKEN) const aud = claims.aud.replace(/\/$/, '') if (path === `scans/:scanID/summary`) return `${aud}/scans/${params.scanID}/summary` throw new Error(`Internal Error: Unknown telemetry event: GET ${path}`) } #toContentType(path) { if (path === `scans/:scanID/log`) return 'text/plain' return 'application/json' } #toBody(path, body) { if (path === `scans/started`) body = { ...body } if (path === `scans/:scanID/started`) body = { ...body } if (path === `scans/:scanID/completed`) body = { ...this.#toFindings(body.summary), timestamp: DateTime.now().toISO(), status: 'success', log: { sizeBytes: 0, warnings: 0, errors: 0, link: 'none' }, params: { id: '' }} 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: '' }} if (path === `scans/:scanID/results`) body = { findings: body.findings /* SARIF */, log: Buffer.from(body.log, 'utf8').toString('base64'), sboms: body.sboms } return JSON.stringify(body) } #toFindings(summary) { return { findings: { total: summary.errors.length + summary.warnings.length + summary.notes.length, critical: 0, high: summary.errors.length, med: summary.warnings.length, low: summary.notes.length } } } } module.exports = { Telemetry }