diff --git a/README.md b/README.md index 3089d4ea..3a3dde97 100644 --- a/README.md +++ b/README.md @@ -112,12 +112,13 @@ Scan a given SBOM for EOL data ``` USAGE - $ hd scan eol [--json] [-f | -d ] [-s] [--saveSbom] [--saveTrimmedSbom] [--version] + $ hd scan eol [--json] [-f | -d ] [-s] [--saveSbom] [--saveTrimmedSbom] [--hideReportUrl] [--version] FLAGS -d, --dir= [default: ] The directory to scan in order to create a cyclonedx SBOM -f, --file= The file path of an existing cyclonedx SBOM to scan for EOL -s, --save Save the generated report as herodevs.report.json in the scanned directory + --hideReportUrl Hide the generated web report URL for this scan --saveSbom Save the generated SBOM as herodevs.sbom.json in the scanned directory --saveTrimmedSbom Save the trimmed SBOM as herodevs.sbom-trimmed.json in the scanned directory --version Show CLI version. diff --git a/e2e/scan/eol.test.ts b/e2e/scan/eol.test.ts index 046fa61c..502e2a5e 100644 --- a/e2e/scan/eol.test.ts +++ b/e2e/scan/eol.test.ts @@ -326,6 +326,13 @@ describe('scan:eol e2e', () => { const { stdout } = await run(cmd); doesNotMatch(stdout, /View your full EOL report/, 'Should not show web report text in JSON output'); }); + + it('shows save hint when --hideReportUrl flag is used', async () => { + const cmd = `scan:eol --file ${simpleSbom} --hideReportUrl`; + const { stdout } = await run(cmd); + doesNotMatch(stdout, /View your full EOL report/, 'Should not show web report text when hidden'); + match(stdout, /To save your detailed JSON report, use the --save flag/, 'Should show save hint message'); + }); }); describe('privacy and transparency', () => { diff --git a/src/commands/scan/eol.ts b/src/commands/scan/eol.ts index f6530f80..05cde001 100644 --- a/src/commands/scan/eol.ts +++ b/src/commands/scan/eol.ts @@ -9,6 +9,7 @@ import { createSbom } from '../../service/cdx.svc.ts'; import { countComponentsByStatus, formatDataPrivacyLink, + formatReportSaveHint, formatScanResults, formatWebReportUrl, } from '../../service/display.svc.ts'; @@ -68,6 +69,11 @@ export default class ScanEol extends Command { default: false, description: `Save the trimmed SBOM as ${filenamePrefix}.sbom-trimmed.json in the scanned directory`, }), + hideReportUrl: Flags.boolean({ + aliases: ['hide-report-url'], + default: false, + description: 'Hide the generated web report URL for this scan', + }), version: Flags.version(), }; @@ -125,7 +131,8 @@ export default class ScanEol extends Command { sbom_created: !flags.file, scan_load_time: (scanEndTime - scanStartTime) / 1000, scanned_ecosystems: componentCounts.ECOSYSTEMS, - web_report_link: scan.id ? `${config.eolReportUrl}/${scan.id}` : undefined, + web_report_link: !flags.hideReportUrl && scan.id ? `${config.eolReportUrl}/${scan.id}` : undefined, + web_report_hidden: flags.hideReportUrl, })); if (flags.save) { @@ -139,7 +146,7 @@ export default class ScanEol extends Command { } if (!this.jsonEnabled()) { - this.displayResults(scan); + this.displayResults(scan, flags.hideReportUrl); } return scan; @@ -225,17 +232,22 @@ export default class ScanEol extends Command { } } - private displayResults(report: EolReport): void { + private displayResults(report: EolReport, hideReportUrl: boolean): void { const lines = formatScanResults(report); for (const line of lines) { this.log(line); } - if (report.id) { + if (!hideReportUrl && report.id) { const lines = formatWebReportUrl(report.id, config.eolReportUrl); for (const line of lines) { this.log(line); } + } else if (hideReportUrl) { + const lines = formatReportSaveHint(); + for (const line of lines) { + this.log(line); + } } const privacyLines = formatDataPrivacyLink(); diff --git a/src/service/display.svc.ts b/src/service/display.svc.ts index 943461e2..55ff09cc 100644 --- a/src/service/display.svc.ts +++ b/src/service/display.svc.ts @@ -10,6 +10,8 @@ const STATUS_COLORS: Record = { EOL_UPCOMING: 'yellow', }; +const SEPARATOR_WIDTH = 40; + /** * Formats status row text with appropriate color and icon */ @@ -72,7 +74,7 @@ export function formatScanResults(report: EolReport): string[] { return [ ux.colorize('bold', 'Scan results:'), - ux.colorize('bold', '-'.repeat(40)), + ux.colorize('bold', '-'.repeat(SEPARATOR_WIDTH)), ux.colorize('bold', `${report.components.length.toLocaleString()} total packages scanned`), getStatusRowText.EOL(`${EOL.toLocaleString().padEnd(5)} End-of-Life (EOL)`), getStatusRowText.EOL_UPCOMING(`${EOL_UPCOMING.toLocaleString().padEnd(5)} EOL Upcoming`), @@ -93,7 +95,7 @@ export function formatWebReportUrl(id: string, reportCardUrl: string): string[] terminalLink(new URL(reportCardUrl).hostname, `${reportCardUrl}/${id}`, { fallback: (_, url) => url }), ); - return [ux.colorize('bold', '-'.repeat(40)), `🌐 View your full EOL report at: ${url}\n`]; + return [ux.colorize('bold', '-'.repeat(SEPARATOR_WIDTH)), `🌐 View your full EOL report at: ${url}\n`]; } /** @@ -108,3 +110,10 @@ export function formatDataPrivacyLink(): string[] { return [`🔒 ${link}\n`]; } + +/** + * Formats the report save hint for console display when the web report URL is hidden + */ +export function formatReportSaveHint(): string[] { + return [ux.colorize('bold', '-'.repeat(SEPARATOR_WIDTH)), 'To save your detailed JSON report, use the --save flag']; +} diff --git a/test/service/display.svc.test.ts b/test/service/display.svc.test.ts index a65adaa3..90e65de8 100644 --- a/test/service/display.svc.test.ts +++ b/test/service/display.svc.test.ts @@ -4,6 +4,7 @@ import type { EolReport } from '@herodevs/eol-shared'; import { countComponentsByStatus, formatDataPrivacyLink, + formatReportSaveHint, formatScanResults, formatWebReportUrl, } from '../../src/service/display.svc.ts'; @@ -169,4 +170,14 @@ describe('display.svc', () => { assert.ok(lines[0].includes('docs.herodevs.com/eol-ds/data-privacy-and-security')); }); }); + + describe('formatReportSaveHint', () => { + it('should provide a save hint message', () => { + const lines = formatReportSaveHint(); + + assert.strictEqual(lines.length, 2); + assert.ok(lines[0].includes('-'.repeat(40))); + assert.ok(lines[1].includes('--save')); + }); + }); });