Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .envrc.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

export GRAPHQL_HOST='https://api.nes.herodevs.com';
export EOL_REPORT_URL='https://eol-report-card.apps.herodevs.com/reports';
export ANALYTICS_URL='https://eol-api.herodevs.com/track';
34 changes: 34 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"prepack": "oclif manifest && oclif readme",
"pretest": "npm run lint && npm run typecheck",
"readme": "npm run ci:fix && npm run build && npm exec oclif readme",
"test": "globstar -- node --import tsx --test \"test/**/*.test.ts\"",
"test": "globstar -- node --import tsx --test --experimental-test-module-mocks \"test/**/*.test.ts\"",
"test:e2e": "globstar -- node --import tsx --test \"e2e/**/*.test.ts\"",
"typecheck": "tsc --noEmit"
},
Expand All @@ -36,13 +36,15 @@
"herodevs cli"
],
"dependencies": {
"@amplitude/analytics-node": "^1.5.0",
"@apollo/client": "^3.13.8",
"@cyclonedx/cdxgen": "^11.4.3",
"@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.4",
"@oclif/core": "^4.4.0",
"@oclif/plugin-help": "^6.2.29",
"@oclif/plugin-update": "^4.6.45",
"graphql": "^16.11.0",
"node-machine-id": "^1.1.12",
"ora": "^8.2.0",
"packageurl-js": "^2.0.1",
"terminal-link": "^4.0.0",
Expand Down Expand Up @@ -84,7 +86,8 @@
],
"hooks": {
"init": "./dist/hooks/npm-update-notifier.js",
"prerun": "./dist/hooks/prerun.js"
"prerun": "./dist/hooks/prerun.js",
"finally": "./dist/hooks/finally.js"
},
"topicSeparator": " ",
"macos": {
Expand Down
77 changes: 71 additions & 6 deletions src/commands/scan/eol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { Command, Flags } from '@oclif/core';
import ora from 'ora';
import { submitScan } from '../../api/nes.client.ts';
import { config, filenamePrefix } from '../../config/constants.ts';
import { track } from '../../service/analytics.svc.ts';
import { createSbom } from '../../service/cdx.svc.ts';
import { formatScanResults, formatWebReportUrl } from '../../service/display.svc.ts';
import { countComponentsByStatus, formatScanResults, formatWebReportUrl } from '../../service/display.svc.ts';
import { readSbomFromFile, saveReportToFile, saveSbomToFile, validateDirectory } from '../../service/file.svc.ts';
import { getErrorMessage } from '../../service/log.svc.ts';

Expand Down Expand Up @@ -56,23 +57,72 @@ export default class ScanEol extends Command {
public async run(): Promise<EolReport | undefined> {
const { flags } = await this.parse(ScanEol);

track('CLI EOL Scan Started', (context) => ({
command: context.command,
command_flags: context.command_flags,
scan_location: flags.dir,
}));

const sbomStartTime = performance.now();
const sbom = await this.loadSbom();
const sbomEndTime = performance.now();

if (!flags.file) {
track('CLI SBOM Generated', (context) => ({
command: context.command,
command_flags: context.command_flags,
scan_location: flags.dir,
sbom_generation_time: (sbomEndTime - sbomStartTime) / 1000,
}));
}

if (!sbom.components?.length) {
track('CLI EOL Scan Ended, No Components Found', (context) => ({
command: context.command,
command_flags: context.command_flags,
scan_location: flags.dir,
}));
this.log('No components found in scan. Report not generated.');
return;
}

const scanStartTime = performance.now();
const scan = await this.scanSbom(sbom);
const scanEndTime = performance.now();

const componentCounts = countComponentsByStatus(scan);
track('CLI EOL Scan Completed', (context) => ({
command: context.command,
command_flags: context.command_flags,
eol_true_count: componentCounts.EOL,
eol_unknown_count: componentCounts.UNKNOWN,
nes_available_count: componentCounts.NES_AVAILABLE,
number_of_packages: componentCounts.TOTAL,
sbom_created: !flags.file,
scan_location: flags.dir,
scan_load_time: (scanEndTime - scanStartTime) / 1000,
scanned_ecosystems: componentCounts.ECOSYSTEMS,
web_report_link: scan.id ? `${config.eolReportUrl}/${scan.id}` : undefined,
}));

if (flags.save) {
const reportPath = this.saveReport(scan, flags.dir);
this.log(`Report saved to ${reportPath}`);
track('CLI JSON Scan Output Saved', (context) => ({
command: context.command,
command_flags: context.command_flags,
report_output_path: reportPath,
}));
}

if (flags.saveSbom && !flags.file) {
const sbomPath = this.saveSbom(flags.dir, sbom);
this.log(`SBOM saved to ${sbomPath}`);
track('CLI SBOM Output Saved', (context) => ({
command: context.command,
command_flags: context.command_flags,
sbom_output_path: sbomPath,
}));
}

if (!this.jsonEnabled()) {
Expand Down Expand Up @@ -107,23 +157,34 @@ export default class ScanEol extends Command {
return scan;
} catch (error) {
spinner.fail('Scanning failed');
this.error(`Failed to submit scan to NES. ${getErrorMessage(error)}`);
const errorMessage = getErrorMessage(error);
track('CLI EOL Scan Failed', (context) => ({
command: context.command,
command_flags: context.command_flags,
scan_location: context.scan_location,
scan_failure_reason: errorMessage,
}));
this.error(`Failed to submit scan to NES. ${errorMessage}`);
}
}

private saveReport(report: EolReport, dir: string): string {
try {
return saveReportToFile(dir, report);
} catch (error) {
this.error(getErrorMessage(error));
const errorMessage = getErrorMessage(error);
track('CLI Error Encountered', () => ({ error: errorMessage }));
this.error(errorMessage);
}
}

private saveSbom(dir: string, sbom: CdxBom): string {
try {
return saveSbomToFile(dir, sbom);
} catch (error) {
this.error(getErrorMessage(error));
const errorMessage = getErrorMessage(error);
track('CLI Error Encountered', () => ({ error: errorMessage }));
this.error(errorMessage);
}
}

Expand Down Expand Up @@ -154,15 +215,19 @@ export default class ScanEol extends Command {
}
return sbom;
} catch (error) {
this.error(`Failed to scan directory: ${getErrorMessage(error)}`);
const errorMessage = getErrorMessage(error);
track('CLI Error Encountered', () => ({ error: errorMessage }));
this.error(`Failed to scan directory: ${errorMessage}`);
}
}

private getSbomFromFile(filePath: string): CdxBom {
try {
return readSbomFromFile(filePath);
} catch (error) {
this.error(getErrorMessage(error));
const errorMessage = getErrorMessage(error);
track('CLI Error Encountered', () => ({ error: errorMessage }));
this.error(errorMessage);
}
}
}
2 changes: 2 additions & 0 deletions src/config/constants.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
export const EOL_REPORT_URL = 'https://eol-report-card.apps.herodevs.com/reports';
export const GRAPHQL_HOST = 'https://api.nes.herodevs.com';
export const GRAPHQL_PATH = '/graphql';
export const ANALYTICS_URL = 'https://eol-api.herodevs.com/track';

export const config = {
eolReportUrl: process.env.EOL_REPORT_URL || EOL_REPORT_URL,
graphqlHost: process.env.GRAPHQL_HOST || GRAPHQL_HOST,
graphqlPath: process.env.GRAPHQL_PATH || GRAPHQL_PATH,
analyticsUrl: process.env.ANALYTICS_URL || ANALYTICS_URL,
showVulnCount: true,
};

Expand Down
19 changes: 19 additions & 0 deletions src/hooks/finally.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Hook } from '@oclif/core';
import ora from 'ora';
import { track } from '../service/analytics.svc.ts';

const hook: Hook<'finally'> = async (opts) => {
const spinner = ora().start('Cleaning up');
const event = track('CLI Session Ended', (context) => ({
cli_version: context.cli_version,
ended_at: new Date(),
})).promise;

if (!opts.argv.includes('--help')) {
await event;
}

spinner.stop();
};

export default hook;
13 changes: 13 additions & 0 deletions src/hooks/prerun.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import { parseArgs } from 'node:util';
import type { Hook } from '@oclif/core';
import debug from 'debug';
import { initializeAnalytics, track } from '../service/analytics.svc.ts';

const hook: Hook<'prerun'> = async (opts) => {
const args = parseArgs({ allowPositionals: true, strict: false });
initializeAnalytics();
track('CLI Command Submitted', (context) => ({
command: args.positionals.join(' ').trim(),
command_flags: Object.entries(args.values).flat().join(' '),
app_used: context.app_used,
ci_provider: context.ci_provider,
cli_version: context.cli_version,
started_at: context.started_at,
}));

// If JSON flag is enabled, silence debug logging
if (opts.Command.prototype.jsonEnabled()) {
debug.disable();
Expand Down
Loading