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
38 changes: 4 additions & 34 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,14 @@ import { createDebugLogger, type DebugLogger } from "./output/debug.js";
import { buildSuggestedFixCommandPlan } from "./remediation/fix-commands.js";
import { scanProjectForPackageUsage } from "./usage/scanner.js";
import { getCliVersion } from "./utils/version-info.js";
import {
blockedAdvisoryRequestHint,
fetchErrorCaCertHint,
isLikelyBlockedAdvisoryRequestError,
isRateLimitError,
isServerError,
isSslCertificateError,
offlineDbSyncHint,
rateLimitAdvisoryRequestHint,
serverAdvisoryRequestHint,
sslCertificateErrorHint,
} from "./utils/network.js";
import { getNetworkErrorHint, offlineDbSyncHint } from "./utils/network.js";
import { formatAdvisoryDbFreshness } from "./utils/time.js";
import { pluralize, nearestCommand } from "./utils/string.js";
import type { ParsedOptions } from "./types.js";
import { EXIT_ERROR } from "./types.js";
import {
formatAdvisorySourceLine,
formatHintLines,
logInfo,
logWarn,
printCacheSummary,
Expand Down Expand Up @@ -765,27 +755,8 @@ if (parsedArgs) {
});
auditLogHandle.close();

if (isSslCertificateError(error)) {
const [hint, ...rest] = sslCertificateErrorHint();
console.error(chalk.yellow(hint));
rest.forEach(line => console.error(chalk.gray(line)));
} else if (isRateLimitError(errorMessage)) {
const [hint, ...rest] = rateLimitAdvisoryRequestHint();
console.error(chalk.yellow(hint));
rest.forEach(line => console.error(chalk.gray(line)));
} else if (isServerError(errorMessage)) {
const [hint, ...rest] = serverAdvisoryRequestHint();
console.error(chalk.yellow(hint));
rest.forEach(line => console.error(chalk.gray(line)));
} else if (isLikelyBlockedAdvisoryRequestError(errorMessage)) {
const [hint, ...rest] = blockedAdvisoryRequestHint();
console.error(chalk.yellow(hint));
rest.forEach(line => console.error(chalk.gray(line)));
} else {
const [hint, ...rest] = fetchErrorCaCertHint();
console.error(chalk.yellow(hint));
rest.forEach(line => console.error(chalk.gray(line)));
}
formatHintLines(getNetworkErrorHint(error, errorMessage))
.forEach(line => console.error(line));
process.exit(1);
});
}
Expand Down Expand Up @@ -863,4 +834,3 @@ async function scanProject(params: {
};
}


10 changes: 9 additions & 1 deletion src/output/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ export function formatAdvisorySourceLine(sourceLabel: string): string {
return `${match[1]} (${chalk.cyan(match[2])})`;
}

export function formatHintLines(lines: string[]): string[] {
if (lines.length === 0) return [];
const [first, ...rest] = lines;
return [
chalk.yellow(first),
...rest.map(line => chalk.gray(line)),
];
}

// MAL-* is the OSV prefix for malicious code advisories
function isMaliciousAdvisory(finding: Finding): boolean {
return finding.vulnerabilities.some(v => v.id.startsWith("MAL-"));
Expand Down Expand Up @@ -338,4 +347,3 @@ export function sortFindingsForOutput(findings: Finding[]): Finding[] {
export function countUniqueAdvisories(findings: Finding[]): number {
return new Set(findings.flatMap(f => f.vulnerabilities.map(v => v.id))).size;
}

8 changes: 8 additions & 0 deletions src/utils/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,11 @@ export function isLikelyBlockedAdvisoryRequestError(message: string): boolean {

return /^(401|403|407|408|451)\b/.test(finalCause);
}

export function getNetworkErrorHint(error: unknown, errorMessage: string): string[] {
if (isSslCertificateError(error)) return sslCertificateErrorHint();
if (isRateLimitError(errorMessage)) return rateLimitAdvisoryRequestHint();
if (isServerError(errorMessage)) return serverAdvisoryRequestHint();
if (isLikelyBlockedAdvisoryRequestError(errorMessage)) return blockedAdvisoryRequestHint();
return fetchErrorCaCertHint();
}
1 change: 1 addition & 0 deletions tests/cli-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ jest.unstable_mockModule("../src/advisory/osv-sync.js", () => ({

jest.unstable_mockModule("../src/output/formatters.js", () => ({
formatAdvisorySourceLine: jest.fn<any>((sourceLabel: string) => sourceLabel),
formatHintLines: jest.fn<any>((lines: string[]) => lines),
logInfo: logInfoMock,
logWarn: logWarnMock,
printCacheSummary: printCacheSummaryMock,
Expand Down
46 changes: 45 additions & 1 deletion tests/network.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import { isLikelyBlockedAdvisoryRequestError, isRateLimitError, isServerError, isSslCertificateError, extractErrorMessage } from "../src/utils/network.js";
import {
blockedAdvisoryRequestHint,
extractErrorMessage,
fetchErrorCaCertHint,
getNetworkErrorHint,
isLikelyBlockedAdvisoryRequestError,
isRateLimitError,
isServerError,
isSslCertificateError,
rateLimitAdvisoryRequestHint,
serverAdvisoryRequestHint,
sslCertificateErrorHint,
} from "../src/utils/network.js";

describe("extractErrorMessage", () => {
it("returns the message of a plain Error", () => {
Expand Down Expand Up @@ -131,3 +143,35 @@ describe("isServerError", () => {
expect(isServerError("API failed: 500 Internal Server Error")).toBe(false);
});
});

describe("getNetworkErrorHint", () => {
it("returns the SSL certificate hint for SSL errors", () => {
const err = Object.assign(new Error("fetch failed"), { code: "SELF_SIGNED_CERT_IN_CHAIN" });

expect(getNetworkErrorHint(err, "fetch failed")).toEqual(sslCertificateErrorHint());
});

it("returns the rate-limit hint for OSV 429 errors", () => {
const message = "OSV batch query failed for https://api.osv.dev: OSV batch query failed: 429 Too Many Requests";

expect(getNetworkErrorHint(new Error(message), message)).toEqual(rateLimitAdvisoryRequestHint());
});

it("returns the server-error hint for OSV 5xx errors", () => {
const message = "OSV batch query failed for https://api.osv.dev: OSV batch query failed: 503 Service Unavailable";

expect(getNetworkErrorHint(new Error(message), message)).toEqual(serverAdvisoryRequestHint());
});

it("returns the blocked-advisory hint for likely blocked OSV requests", () => {
const message = "OSV batch query failed for https://api.osv.dev: fetch failed";

expect(getNetworkErrorHint(new Error(message), message)).toEqual(blockedAdvisoryRequestHint());
});

it("returns the CA certificate fallback hint for unrelated errors", () => {
const message = "Unexpected scan failure";

expect(getNetworkErrorHint(new Error(message), message)).toEqual(fetchErrorCaCertHint());
});
});
16 changes: 16 additions & 0 deletions tests/output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
summarizeNextAction,
summarizeRisk,
formatRelLabel,
formatHintLines,
formatFixCommandWithPublishDates,
formatFixVersionPublishDate,
} from "../src/output/formatters.js";
Expand Down Expand Up @@ -159,6 +160,21 @@ describe("output formatters", () => {
}
});

it("formats empty hint lines as empty output", () => {
expect(formatHintLines([])).toEqual([]);
});

it("formats the first hint line before gray detail lines", () => {
const lines = formatHintLines(["Hint: retry later", "Run: cve-lite . --offline"]);

expect(lines.map(line => stripAnsi(line))).toEqual([
"Hint: retry later",
"Run: cve-lite . --offline",
]);
expect(lines[0]).toContain("Hint: retry later");
expect(lines[1]).toContain("Run: cve-lite . --offline");
});

it("getPrimaryParent returns null for paths shorter than 3 nodes", () => {
const shortPath = createFinding({
dependencyPaths: [["project", "lodash"]],
Expand Down