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
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
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
215 changes: 33 additions & 182 deletions e2e/scan/eol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,50 +15,27 @@ const execAsync = promisify(exec);
describe('environment', () => {
it('should not be configured to run against the production environment', () => {
notStrictEqual(process.env.GRAPHQL_HOST, 'https://api.nes.herodevs.com');
notStrictEqual(process.env.EOL_REPORT_URL, 'https://eol-report-card.apps.herodevs.io/reports');
notStrictEqual(process.env.EOL_REPORT_URL, 'https://eol-report-card.apps.herodevs.com/reports');
notStrictEqual(config.graphqlHost, 'https://api.nes.herodevs.com');
notStrictEqual(config.eolReportUrl, 'https://eol-report-card.apps.herodevs.io/reports');
notStrictEqual(config.eolReportUrl, 'https://eol-report-card.apps.herodevs.com/reports');
});
});

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');
Comment thread
rlmestre marked this conversation as resolved.
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,29 @@ 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`;
it('correctly identifies Bootstrap as having EOL status and remediation available when using the --json flag', async () => {
const cmd = `scan:eol --purls=${simplePurls} --json`;
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');
const json = JSON.parse(stdout);
const bootstrap = json.components.find((component) => component.purl.startsWith('pkg:npm/bootstrap@'));
strictEqual(bootstrap?.info.status, 'EOL', 'Should match EOL count');
strictEqual(bootstrap?.info.nesAvailable, true, 'Should match remediation count');
});

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 () => {
const cmd = `scan:eol --purls=${angular17Purls}`;
it('correctly identifies Angular 17 as having a EOL date when using --json flag', async () => {
const angular17Purls = path.resolve(__dirname, '../fixtures/npm/angular-17.purls.json');
const cmd = `scan:eol --purls=${angular17Purls} --json`;
const { stdout } = await run(cmd);

// Check for Angular package presence
match(stdout, /pkg:npm\/%40angular\/core@17\.3\.12/, 'Should detect Angular core package');
Comment thread
rlmestre marked this conversation as resolved.

// 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');
const json = JSON.parse(stdout);
const angular17 = json.components.find((component) => component.purl.startsWith('pkg:npm/%40angular/core@'));
// Match EOL count
strictEqual(angular17?.info.status, 'EOL', 'Should match EOL status');
});

describe('web report URL', () => {
Expand All @@ -273,15 +192,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 () => {
Comment thread
rlmestre marked this conversation as resolved.
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');
match(stdout, /View your full EOL report at.*[a-zA-Z0-9-]+/, 'Should show web report text and scan ID');
Comment thread
rlmestre marked this conversation as resolved.
});

it('does not display web report URL when using --json flag', async () => {
Comment thread
rlmestre marked this conversation as resolved.
Expand All @@ -298,7 +209,7 @@ describe('scan:eol e2e', () => {
* Directory scan tests
* Please see CONTRIBUTING.md before adding new tests to this section.
*/
describe('scan:eol e2e directory', () => {
describe('with directory flag', () => {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const simpleDir = path.resolve(__dirname, '../fixtures/npm/simple');
const upToDateDir = path.resolve(__dirname, '../fixtures/npm/up-to-date');
Expand Down Expand Up @@ -326,19 +237,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 +278,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