Skip to content

feat: summarize scan output to favor web report, removing list and table views #258

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 13, 2025
Merged
Show file tree
Hide file tree
Changes from 5 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
7 changes: 0 additions & 7 deletions .cursorrules
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,6 @@ rule "service-no-inquirer" {
message = "UI interactions should be handled in the UI layer, not services"
}

rule "service-no-cli-table" {
# Prevent cli-table3 usage in service layer
matches = ["src/service/**/*.svc.ts"]
not_contains = ["cli-table3"]
message = "Table formatting should be handled in the UI layer, not services"
}

rule "service-no-apollo" {
# Prevent Apollo Client usage in service layer
matches = ["src/service/**/*.svc.ts"]
Expand Down
2 changes: 1 addition & 1 deletion .envrc.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env bash

export GRAPHQL_HOST='https://api.nes.herodevs.com';
export EOL_REPORT_URL='https://eol-report-card.apps.herodevs.com/reports';
export EOL_REPORT_URL='https://eol-report-card.apps.herodevs.io/reports';
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@ FLAGS
-f, --file=<value> The file path of an existing cyclonedx sbom to scan for EOL
-p, --purls=<value> The file path of a list of purls to scan for EOL
-s, --save Save the generated report as eol.report.json in the scanned directory
-t, --table Display the results in a table

GLOBAL FLAGS
--json Format output as json.
Expand Down
4 changes: 2 additions & 2 deletions bin/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ async function main(isProduction = false) {
strict: false, // Don't validate flags
});

// If no arguments at all, default to scan:eol -t
// If no arguments at all, default to scan:eol
if (positionals.length === 0) {
process.argv.splice(2, 0, 'scan:eol', '-t');
process.argv.splice(2, 0, 'scan:eol');
}
// If only flags are provided, set scan:eol as the command for those flags
else if (positionals.length === 1 && positionals[0].startsWith('-')) {
Expand Down
212 changes: 22 additions & 190 deletions e2e/scan/eol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,43 +22,20 @@ describe('environment', () => {
});

describe('default arguments', () => {
it('defaults to scan:eol -t when no arguments are provided', async () => {
it('defaults to scan:eol when no arguments are provided', async () => {
// Run the CLI directly with no arguments
const { stdout } = await execAsync('node bin/run.js');

// Match table header
match(stdout, /┌.*┬.*┬.*┬.*┬.*┐/, 'Should show table top border');
if (config.showVulnCount) {
match(stdout, /│ NAME\s*│ VERSION\s*│ EOL\s*│ DAYS EOL\s*│ TYPE\s*│ # OF VULNS*|/, 'Should show table headers');
} else {
match(stdout, /│ NAME\s*│ VERSION\s*│ EOL\s*│ DAYS EOL\s*│ TYPE\s*|/, 'Should show table headers');
}
match(stdout, /├.*┼.*┼.*┼.*┼.*┤/, 'Should show table header separator');

// Match table content
match(
stdout,
/│ bootstrap\s*│ 3\.1\.1\s*│ 2019-07-24\s*│ \d+\s*│ npm\s*│/,
'Should show bootstrap package in table',
);

// Match table footer
match(stdout, /└.*┴.*┴.*┴.*┴.*┘/, 'Should show table bottom border');
});

it('runs scan:eol -a -t when -a -t is passed in', async () => {
const { stdout, stderr } = await execAsync('node bin/run.js -a -t');

// Verify command executed successfully
match(stdout, /components scanned/, 'Should show components scanned message');
// Match EOL count
match(stdout, /1( .*)End-of-Life \(EOL\)/, 'Should show EOL count');
});

it('runs scan:eol --json when --json is passed in', async () => {
// Run the CLI with --json flag
const { stdout } = await execAsync('node bin/run.js --json');

// Verify JSON output
doesNotMatch(stdout, /Here are the results of the scan:/, 'Should not show results header');
doesNotMatch(stdout, /Scan results:/, 'Should not show results header');
doesNotThrow(() => JSON.parse(stdout), 'Output should be valid JSON');
});

Expand All @@ -80,17 +57,15 @@ describe('default arguments', () => {
match(stdout, /COMMANDS/, 'Should show commands section');
});
});

describe('scan:eol e2e', () => {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const fixturesDir = path.resolve(__dirname, '../fixtures');
const simplePurls = path.resolve(__dirname, '../fixtures/npm/simple.purls.json');
const simpleSbom = path.join(fixturesDir, 'npm/eol.sbom.json');
const transitiveDependenciesSbom = path.join(fixturesDir, 'npm/transitive-dependencies.sbom.json');
const reportPath = path.resolve(fixturesDir, 'eol.report.json');
const upToDatePurls = path.resolve(__dirname, '../fixtures/npm/up-to-date.purls.json');
const extraLargePurlsPath = path.resolve(__dirname, '../fixtures/npm/extra-large.purls.json');
const emptyPurlsPath = path.resolve(__dirname, '../fixtures/npm/empty.purls.json');
const angular17Purls = path.resolve(__dirname, '../fixtures/npm/angular-17.purls.json');

async function run(cmd: string) {
// Ensure fixtures directory exists and is clean
Expand All @@ -114,13 +89,12 @@ describe('scan:eol e2e', () => {
const cmd = `scan:eol --file ${simpleSbom}`;
const { stdout } = await run(cmd);

// Match command output patterns
match(stdout, /Here are the results of the scan:/, 'Should show results header');
match(stdout, /pkg:npm\/bootstrap@3\.1\.1/, 'Should detect bootstrap package');
match(stdout, /EOL Date: 2019-07-24/, 'Should show correct EOL date for bootstrap');
// Match EOL count
match(stdout, /1( .*)End-of-Life \(EOL\)/, 'Should show EOL count');
});

it('generates purls from SBOM for direct and transitive dependencies', async () => {
const transitiveDependenciesSbom = path.join(fixturesDir, 'npm/transitive-dependencies.sbom.json');
const cmd = `report:purls --file ${transitiveDependenciesSbom} --json`;
const { stdout } = await run(cmd);

Expand Down Expand Up @@ -159,11 +133,12 @@ describe('scan:eol e2e', () => {
});

it.skip('scans extra-large.purls.json for EOL components', async () => {
const extraLargePurlsPath = path.resolve(__dirname, '../fixtures/npm/extra-large.purls.json');
const cmd = `scan:eol --purls ${extraLargePurlsPath}`;
const { stdout } = await run(cmd);

// Match command output patterns
match(stdout, /Here are the results of the scan:/, 'Should show results header');
match(stdout, /Scan results:/, 'Should show results header');

// Match specific EOL packages
match(stdout, /pkg:npm\/%40angular\/core@12\.2\.2/, 'Should detect Angular core package');
Expand All @@ -186,85 +161,18 @@ describe('scan:eol e2e', () => {
const { stdout } = await run(cmd);

// Match command output patterns
doesNotMatch(stdout, /Here are the results of the scan:/, 'Should not show results header');
doesNotMatch(stdout, /Scan results:/, 'Should not show results header');
doesNotThrow(() => JSON.parse(stdout));
});

it('displays results in table format when using the -t flag', async () => {
const cmd = `scan:eol --purls=${simplePurls} -t`;
const { stdout } = await run(cmd);

// Match table header
match(stdout, /┌.*┬.*┬.*┬.*┬.*┐/, 'Should show table top border');
if (config.showVulnCount) {
match(stdout, /│ NAME\s*│ VERSION\s*│ EOL\s*│ DAYS EOL\s*│ TYPE\s*│ # OF VULNS*|/, 'Should show table headers');
} else {
match(stdout, /│ NAME\s*│ VERSION\s*│ EOL\s*│ DAYS EOL\s*│ TYPE\s*|/, 'Should show table headers');
}
match(stdout, /├.*┼.*┼.*┼.*┼.*┤/, 'Should show table header separator');

// Match table content
match(
stdout,
/│ bootstrap\s*│ 3\.1\.1\s*│ 2019-07-24\s*│ \d+\s*│ npm\s*│/,
'Should show bootstrap package in table',
);

// Match table footer
match(stdout, /└.*┴.*┴.*┴.*┴.*┘/, 'Should show table bottom border');
});

describe('--all flag', () => {
it('excludes OK packages by default', async () => {
const cmd = `scan:eol --purls=${simplePurls}`;
const { stdout } = await run(cmd);

// Match command output patterns
match(stdout, /Here are the results of the scan:/, 'Should show results header');
doesNotMatch(stdout, /pkg:npm\/vue@3\.5\.13/, 'Should not show vue package');
});

it('shows all packages when --all flag is used', async () => {
const cmd = `scan:eol --purls=${simplePurls} --all`;
const { stdout } = await run(cmd);

// Match command output patterns
match(stdout, /Here are the results of the scan:/, 'Should show results header');
match(stdout, /pkg:npm\/bootstrap@3\.1\.1/, 'Should detect bootstrap package');
match(stdout, /pkg:npm\/vue@3\.5\.13/, 'Should show vue package');
});

it('shows "No EOL" message by default if no components are found', async () => {
const cmd = `scan:eol --purls ${upToDatePurls}`;
const { stdout } = await run(cmd);

// Match command output patterns
doesNotMatch(stdout, /Here are the results of the scan:/, 'Should not show results header');
match(stdout, /No End-of-Life or Supported components found in scan/, 'Should show "No EOL" message');
});

it('shows "No components found" message if no components are found with --all flag', async () => {
const cmd = `scan:eol --purls ${emptyPurlsPath} --all`;
const { stdout } = await run(cmd);

// Match command output patterns
doesNotMatch(stdout, /Here are the results of the scan:/, 'Should not show results header');
match(stdout, /No components found in scan/, 'Should show "No components found" message');
});
});

it('correctly identifies Angular 17 as having a EOL date', async () => {
it('correctly identifies Angular 17 as having a EOL date and remediations available', async () => {
const angular17Purls = path.resolve(__dirname, '../fixtures/npm/angular-17.purls.json');
const cmd = `scan:eol --purls=${angular17Purls}`;
const { stdout } = await run(cmd);

// Check for Angular package presence
match(stdout, /pkg:npm\/%40angular\/core@17\.3\.12/, 'Should detect Angular core package');

// Check for EOL date format
match(stdout, /EOL Date: \d{4}-\d{2}-\d{2}/, 'Should show EOL date');

// Check for the arrow format
match(stdout, /⮑ {2}EOL Date: \d{4}-\d{2}-\d{2}/, 'Should show EOL date with arrow');
// Match EOL count
match(stdout, /1( .*)End-of-Life \(EOL\)/, 'Should show EOL count');
match(stdout, /1( .*)EOL Packages with HeroDevs NES Remediations Available/, 'Should show remediation count');
});

describe('web report URL', () => {
Expand All @@ -273,23 +181,7 @@ describe('scan:eol e2e', () => {
const { stdout } = await run(cmd);

// Match the key text and scan ID pattern
match(stdout, /View your free EOL report at.*[a-zA-Z0-9-]+/, 'Should show web report text and scan ID');
});

it('displays web report URL in table format when using -t flag', async () => {
const cmd = `scan:eol --purls=${simplePurls} -t`;
const { stdout } = await run(cmd);

// Match the key text and scan ID pattern
match(stdout, /View your free EOL report at.*[a-zA-Z0-9-]+/, 'Should show web report text and scan ID');
});

it('does not display web report URL when using --json flag', async () => {
const cmd = `scan:eol --purls=${simplePurls} --json`;
const { stdout } = await run(cmd);

// Verify URL text is not in output
doesNotMatch(stdout, /View your free EOL report/, 'Should not show web report text in JSON output');
match(stdout, /View your full EOL report at.*[a-zA-Z0-9-]+/, 'Should show web report text and scan ID');
});
});
});
Expand Down Expand Up @@ -326,19 +218,16 @@ describe('scan:eol e2e directory', () => {
const cmd = `scan:eol --dir ${simpleDir}`;
const { stdout } = await run(cmd);

// Match command output patterns
match(stdout, /Here are the results of the scan:/, 'Should show results header');
match(stdout, /pkg:npm\/bootstrap@3\.1\.1/, 'Should detect bootstrap package');
match(stdout, /End of Life \(EOL\)/, 'Should show EOL status');
match(stdout, /EOL Date:/, 'Should show EOL date information');
// Match EOL count
match(stdout, /1( .*)End-of-Life \(EOL\)/, 'Should show EOL count');
});

it('displays web report URL when scanning directory', async () => {
const cmd = `scan:eol --dir ${simpleDir}`;
const { stdout } = await run(cmd);

// Match the key text and scan ID pattern
match(stdout, /View your free EOL report at.*[a-zA-Z0-9-]+/, 'Should show web report text and scan ID');
match(stdout, /View your full EOL report at.*[a-zA-Z0-9-]+/, 'Should show web report text and scan ID');
});

it('saves report when --save flag is used', async () => {
Expand Down Expand Up @@ -370,72 +259,15 @@ describe('scan:eol e2e directory', () => {
const cmd = `scan:eol --file ${simpleDir}/sbom.json`;
const { stdout } = await run(cmd);

// Match command output patterns
match(stdout, /Here are the results of the scan:/, 'Should show results header');
match(stdout, /pkg:npm\/bootstrap@3\.1\.1/, 'Should detect bootstrap package');
match(stdout, /EOL Date: 2019-07-24/, 'Should show correct EOL date for bootstrap');
match(stdout, /1( .*)End-of-Life \(EOL\)/, 'Should show EOL count');
});

it('outputs JSON when using the --json flag', async () => {
const cmd = `scan:eol --dir ${simpleDir} --json`;
const { stdout } = await run(cmd);

// Match command output patterns
doesNotMatch(stdout, /Here are the results of the scan:/, 'Should not show results header');
doesNotMatch(stdout, /Scan results:/, 'Should not show results header');
doesNotThrow(() => JSON.parse(stdout));
});

it('displays results in table format when using the -t flag', async () => {
const cmd = `scan:eol --dir ${simpleDir} -t`;
const { stdout } = await run(cmd);

// Match table header
match(stdout, /┌.*┬.*┬.*┬.*┬.*┐/, 'Should show table top border');
match(
stdout,
/│ NAME\s*│ VERSION\s*│ EOL\s*│ DAYS EOL\s*│ TYPE\s*│/, // TODO: add vulns to monorepo api
'Should show table headers',
);
match(stdout, /├.*┼.*┼.*┼.*┼.*┤/, 'Should show table header separator');

// Match table content
match(
stdout,
/│ bootstrap\s*│ 3\.1\.1\s*│ 2019-07-24\s*│ \d+\s*│ npm\s*│/,
'Should show bootstrap package in table',
);

// Match table footer
match(stdout, /└.*┴.*┴.*┴.*┴.*┘/, 'Should show table bottom border');
});

describe('--all flag', () => {
it('excludes OK packages by default', async () => {
const cmd = `scan:eol --dir ${simpleDir}`;
const { stdout } = await run(cmd);

// Match command output patterns
match(stdout, /Here are the results of the scan:/, 'Should show results header');
doesNotMatch(stdout, /pkg:npm\/vue@3\.5\.13/, 'Should not show vue package');
});

it('shows all packages when --all flag is used', async () => {
const cmd = `scan:eol --dir ${simpleDir} --all`;
const { stdout } = await run(cmd);

// Match command output patterns
match(stdout, /Here are the results of the scan:/, 'Should show results header');
match(stdout, /pkg:npm\/bootstrap@3\.1\.1/, 'Should detect bootstrap package');
match(stdout, /pkg:npm\/vue@3\.5\.13/, 'Should show vue package');
});

it('shows "No EOL" message by default if no components are found', async () => {
const cmd = `scan:eol --dir ${upToDateDir}`;
const { stdout } = await run(cmd);

// Match command output patterns
doesNotMatch(stdout, /Here are the results of the scan:/, 'Should not show results header');
match(stdout, /No End-of-Life or Supported components found in scan/, 'Should show "No EOL" message');
});
});
});
Loading
Loading