Skip to content
Closed
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
5 changes: 4 additions & 1 deletion bin/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ async function main(isProduction = false) {
dir: new URL('./dev.js', import.meta.url),
});
} catch (error) {
process.exit(1);
if (error instanceof Error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: in what cases is this condition not met? I think all or most thrown errors use Error or extend it (just curious)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In reality I don't know how anything besides an Error could reach this catch block. You're right that our application code always throws Errors.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its always an instance of error. You could add an assert

throw error;
}
throw new Error(String(error));
}
}

Expand Down
9 changes: 4 additions & 5 deletions src/api/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as apollo from '@apollo/client/core/index.js';
import { ApolloError } from '../service/error.svc.ts';

export interface ApolloHelper {
mutate<T, V extends apollo.OperationVariables>(
Expand Down Expand Up @@ -40,8 +39,8 @@ export class ApolloClient implements ApolloHelper {
mutation,
variables,
});
} catch (error: unknown) {
throw new ApolloError('GraphQL mutation failed', error);
} catch (cause: unknown) {
throw new Error('GraphQL mutation failed', { cause });
}
}

Expand All @@ -51,8 +50,8 @@ export class ApolloClient implements ApolloHelper {
query,
variables,
});
} catch (error) {
throw new ApolloError('GraphQL query failed', error);
} catch (cause: unknown) {
throw new Error('GraphQL query failed', { cause });
}
}
}
6 changes: 3 additions & 3 deletions src/api/nes/nes.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ export const batchSubmitPurls = async (

const results = await processBatches(batches, options);
return handleBatchResults(results);
} catch (error) {
debugLogger('Fatal error in batchSubmitPurls: %s', error);
throw new Error(`Failed to process purls: ${error instanceof Error ? error.message : String(error)}`);
} catch (cause: unknown) {
debugLogger('Fatal error in batchSubmitPurls: %s', cause);
throw new Error('Failed to process purls', { cause });
}
};

Expand Down
28 changes: 15 additions & 13 deletions src/commands/report/committers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
groupCommitsByMonth,
parseGitLogOutput,
} from '../../service/committers.svc.ts';
import { getErrorMessage, isErrnoException } from '../../service/error.svc.ts';
import { isErrnoException } from '../../service/error.svc.ts';

export default class Committers extends Command {
static override description = 'Generate report of committers to a git repository';
Expand Down Expand Up @@ -63,8 +63,8 @@ export default class Committers extends Command {
try {
fs.writeFileSync(path.resolve('eol.committers.json'), JSON.stringify(reportData, null, 2));
this.log('Report written to json');
} catch (error) {
this.error(`Failed to save JSON report: ${getErrorMessage(error)}`);
} catch (cause: unknown) {
throw new Error('Failed to save JSON report', { cause });
}
}
return reportData;
Expand All @@ -79,8 +79,8 @@ export default class Committers extends Command {
try {
fs.writeFileSync(path.resolve('eol.committers.csv'), csvOutput);
this.log('Report written to csv');
} catch (error) {
this.error(`Failed to save CSV report: ${getErrorMessage(error)}`);
} catch (cause: unknown) {
throw new Error('Failed to save CSV report', { cause });
}
} else {
this.log(textOutput);
Expand All @@ -92,15 +92,15 @@ export default class Committers extends Command {
try {
fs.writeFileSync(path.resolve('eol.committers.txt'), textOutput);
this.log('Report written to txt');
} catch (error) {
this.error(`Failed to save txt report: ${getErrorMessage(error)}`);
} catch (cause: unknown) {
throw new Error('Failed to save txt report', { cause });
}
} else {
this.log(textOutput);
}
return textOutput;
} catch (error) {
this.error(`Failed to generate report: ${getErrorMessage(error)}`);
} catch (cause: unknown) {
throw new Error('Failed to generate report', { cause });
}
}

Expand Down Expand Up @@ -152,15 +152,17 @@ export default class Committers extends Command {
if (logProcess.error) {
if (isErrnoException(logProcess.error)) {
if (logProcess.error.code === 'ENOENT') {
this.error('Git command not found. Please ensure git is installed and available in your PATH.');
throw new Error('Git command not found. Please ensure git is installed and available in your PATH.', {
cause: logProcess.error,
});
}
this.error(`Git command failed: ${getErrorMessage(logProcess.error)}`);
throw new Error('Git command failed', { cause: logProcess.error });
}
this.error(`Git command failed: ${getErrorMessage(logProcess.error)}`);
throw new Error('Git command failed', { cause: logProcess.error });
}

if (logProcess.status !== 0) {
this.error(`Git command failed with status ${logProcess.status}: ${logProcess.stderr}`);
throw new Error(`Git command failed with status ${logProcess.status}`, { cause: logProcess.stderr });
}

if (!logProcess.stdout) {
Expand Down
25 changes: 11 additions & 14 deletions src/commands/report/purls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { Command, Flags, ux } from '@oclif/core';

import { getErrorMessage, isErrnoException } from '../../service/error.svc.ts';
import { isErrnoException } from '../../service/error.svc.ts';
import { extractPurls, getPurlOutput } from '../../service/purls.svc.ts';
import ScanSbom from '../scan/sbom.ts';

Expand Down Expand Up @@ -62,30 +62,27 @@ export default class ReportPurls extends Command {
fs.writeFileSync(outputPath, purlOutput);

this.log('Purls saved to %s', outputPath);
} catch (error: unknown) {
if (isErrnoException(error)) {
switch (error.code) {
} catch (cause: unknown) {
if (isErrnoException(cause)) {
switch (cause.code) {
case 'EACCES':
this.error('Permission denied: Cannot write to output file');
break;
throw new Error('Permission denied: Cannot write to output file', { cause });
case 'ENOSPC':
this.error('No space left on device');
break;
throw new Error('No space left on device', { cause });
case 'EISDIR':
this.error('Cannot write to output file: Is a directory');
break;
throw new Error('Cannot write to output file: Is a directory', { cause });
default:
this.error(`Failed to save purls: ${getErrorMessage(error)}`);
throw new Error('Failed to save purls', { cause });
}
}
this.error(`Failed to save purls: ${getErrorMessage(error)}`);
throw new Error('Failed to save purls', { cause });
}
}

// Return wrapped object with metadata
return { purls };
} catch (error) {
this.error(`Failed to generate PURLs: ${getErrorMessage(error)}`);
} catch (cause: unknown) {
throw new Error('Failed to generate PURLs', { cause });
}
}
}
32 changes: 15 additions & 17 deletions src/commands/scan/eol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { batchSubmitPurls } from '../../api/nes/nes.client.ts';
import type { ScanResult } from '../../api/types/hd-cli.types.js';
import type { ComponentStatus, InsightsEolScanComponent } from '../../api/types/nes.types.ts';
import type { Sbom } from '../../service/eol/cdx.svc.ts';
import { getErrorMessage, isErrnoException } from '../../service/error.svc.ts';
import { isErrnoException } from '../../service/error.svc.ts';
import { extractPurls, parsePurlsFile } from '../../service/purls.svc.ts';
import { createStatusDisplay, createTableForStatus, groupComponentsByStatus } from '../../ui/eol.ui.ts';
import { INDICATORS, STATUS_COLORS } from '../../ui/shared.ui.ts';
Expand Down Expand Up @@ -90,8 +90,8 @@ export default class ScanEol extends Command {
try {
const purlsFileString = fs.readFileSync(filePath, 'utf8');
return parsePurlsFile(purlsFileString);
} catch (error) {
this.error(`Failed to read purls file. ${getErrorMessage(error)}`);
} catch (cause: unknown) {
throw new Error('Failed to read purls file', { cause });
}
}

Expand All @@ -100,14 +100,14 @@ export default class ScanEol extends Command {
let purls: string[];

try {
purls = await extractPurls(sbom);
} catch (error) {
this.error(`Failed to extract purls from sbom. ${getErrorMessage(error)}`);
purls = extractPurls(sbom);
} catch (cause: unknown) {
throw new Error('Failed to extract purls from sbom', { cause });
}
try {
scan = await batchSubmitPurls(purls);
} catch (error) {
this.error(`Failed to submit scan to NES from sbom. ${getErrorMessage(error)}`);
} catch (cause: unknown) {
throw new Error('Failed to submit scan to NES from sbom', { cause });
}

if (scan.components.size === 0) {
Expand All @@ -130,19 +130,17 @@ export default class ScanEol extends Command {
try {
fs.writeFileSync(reportPath, JSON.stringify({ components }, null, 2));
this.log('Report saved to eol.report.json');
} catch (error) {
if (!isErrnoException(error)) {
this.error(`Failed to save report: ${getErrorMessage(error)}`);
} catch (cause: unknown) {
if (!isErrnoException(cause)) {
throw new Error('Failed to save report', { cause });
}
switch (error.code) {
switch (cause.code) {
case 'EACCES':
this.error('Permission denied. Unable to save report to eol.report.json');
break;
throw new Error('Permission denied. Unable to save report to eol.report.json', { cause });
case 'ENOSPC':
this.error('No space left on device. Unable to save report to eol.report.json');
break;
throw new Error('No space left on device. Unable to save report to eol.report.json', { cause });
default:
this.error(`Failed to save report: ${getErrorMessage(error)}`);
throw new Error('Failed to save report', { cause });
}
}
}
Expand Down
26 changes: 12 additions & 14 deletions src/commands/scan/sbom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { join, resolve } from 'node:path';
import { Command, Flags, ux } from '@oclif/core';
import type { Sbom } from '../../service/eol/cdx.svc.ts';
import { createSbom, validateIsCycloneDxSbom } from '../../service/eol/eol.svc.ts';
import { getErrorMessage } from '../../service/error.svc.ts';

export default class ScanSbom extends Command {
static override description = 'Scan a SBOM for purls';
Expand Down Expand Up @@ -69,7 +68,7 @@ export default class ScanSbom extends Command {

// Validate that exactly one of --file or --dir is provided
if (file && dir) {
this.error('Cannot specify both --file and --dir flags. Please use one or the other.');
throw new Error('Cannot specify both --file and --dir flags. Please use one or the other.');
}
let sbom: Sbom;
const path = dir || process.cwd();
Expand All @@ -93,23 +92,22 @@ export default class ScanSbom extends Command {
const dir = resolve(_dirFlag);
try {
if (!fs.existsSync(dir)) {
this.error(`Directory not found: ${dir}`);
throw new Error(`Directory not found: ${dir}`);
}
const stats = fs.statSync(dir);
if (!stats.isDirectory()) {
this.error(`Path is not a directory: ${dir}`);
}

ux.action.start(`Scanning ${dir}`);

const options = this.getScanOptions();
const sbom = await createSbom(dir, options);
if (!sbom) {
this.error(`SBOM failed to generate for dir: ${dir}`);
throw new Error(`SBOM failed to generate: ${dir}`);
}
return sbom;
} catch (error) {
this.error(`Failed to scan directory: ${getErrorMessage(error)}`);
} catch (cause: unknown) {
throw new Error('Failed to scan directory', { cause });
}
}

Expand All @@ -130,16 +128,16 @@ export default class ScanSbom extends Command {
});

workerProcess.unref();
} catch (error) {
this.error(`Failed to start background scan: ${getErrorMessage(error)}`);
} catch (cause: unknown) {
throw new Error('Failed to start background scan', { cause });
}
}

private _getSbomFromFile(_fileFlag: string): Sbom {
const file = resolve(_fileFlag);
try {
if (!fs.existsSync(file)) {
this.error(`SBOM file not found: ${file}`);
throw new Error(`SBOM file not found: ${file}`);
}

ux.action.start(`Loading sbom from ${file}`);
Expand All @@ -151,8 +149,8 @@ export default class ScanSbom extends Command {
const sbom = JSON.parse(fileContent) as Sbom;
validateIsCycloneDxSbom(sbom);
return sbom;
} catch (error) {
this.error(`Failed to read SBOM file: ${getErrorMessage(error)}`);
} catch (cause: unknown) {
throw new Error('Failed to read SBOM file', { cause });
}
}

Expand All @@ -163,8 +161,8 @@ export default class ScanSbom extends Command {
if (!this.jsonEnabled()) {
this.log(`SBOM saved to ${outputPath}`);
}
} catch (error) {
this.error(`Failed to save SBOM: ${getErrorMessage(error)}`);
} catch (cause: unknown) {
throw new Error('Failed to save SBOM', { cause });
}
}
}
25 changes: 0 additions & 25 deletions src/service/error.svc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,3 @@ export const isError = (error: unknown): error is Error => {
export const isErrnoException = (error: unknown): error is NodeJS.ErrnoException => {
return isError(error) && 'code' in error;
};

export const isApolloError = (error: unknown): error is ApolloError => {
return error instanceof ApolloError;
};

export const getErrorMessage = (error: unknown): string => {
if (isError(error)) {
return error.message;
}
return 'Unknown error';
};

export class ApolloError extends Error {
public readonly originalError?: unknown;

constructor(message: string, original?: unknown) {
if (isError(original)) {
super(`${message}: ${original.message}`);
} else {
super(`${message}: ${String(original)}`);
}
this.name = 'ApolloError';
this.originalError = original;
}
}
10 changes: 8 additions & 2 deletions src/service/nes/nes.svc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ export const SbomScanner =
(client: NesApolloClient) =>
async (purls: string[], options: ScanInputOptions): Promise<InsightsEolScanResult> => {
const { type, page, totalPages, scanId } = options;
const input: InsightsEolScanInput = { components: purls, type, page, totalPages, scanId };
const input: InsightsEolScanInput = {
components: purls,
type,
page,
totalPages,
scanId,
};

const res = await client.mutate<ScanResponse, { input: InsightsEolScanInput }>(M_SCAN.gql, { input });

Expand All @@ -36,7 +42,7 @@ export const SbomScanner =
debugLogger('failed scan %o', scan || {});
debugLogger('scan failed');

throw new Error('Failed to provide scan: ');
throw new Error(`Scan failed: ${scan?.message}`);
}

return scan;
Expand Down
4 changes: 2 additions & 2 deletions src/ui/date.ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function parseMomentToSimpleDate(momentDate: string | Date | number | nul
throw new Error('Invalid date');
}
return dateObj.toISOString().split('T')[0];
} catch {
throw new Error('Invalid date');
} catch (cause: unknown) {
throw new Error('Invalid date', { cause });
}
}