diff --git a/src/index.ts b/src/index.ts index fc03f54..a4bd7a0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,7 @@ import { createSpinner } from "./output/spinner.js"; import { buildSuggestedFixCommandPlan } from "./remediation/fix-commands.js"; import { scanProjectForPackageUsage } from "./usage/scanner.js"; import { getCliVersion } from "./utils/version-info.js"; -import { isLikelyBlockedAdvisoryRequestError } from "./utils/network.js"; +import { isLikelyBlockedAdvisoryRequestError, isSslCertificateError, sslCertificateErrorHint, blockedAdvisoryRequestHint } from "./utils/network.js"; import type { SuggestedFixCommandPlan, SuggestedFixTarget } from "./remediation/fix-commands.js"; import type { ParsedOptions } from "./types.js"; import type { Finding, SeverityLabel } from "./types.js"; @@ -301,10 +301,14 @@ if (parsedArgs) { main().catch((error) => { const errorMessage = error instanceof Error ? error.message : String(error); console.error(chalk.red(`Error: ${errorMessage}`)); - if (isLikelyBlockedAdvisoryRequestError(errorMessage)) { - console.error(chalk.yellow("Hint: Outbound access to the OSV API may be blocked or restricted in this environment.")); - console.error(chalk.gray("If that is expected, build the advisory DB on a machine with OSV access, then scan here with `--offline` or `--offline-db /path/to/advisories.db`.")); - console.error(chalk.gray("Command to build the DB on a network-allowed machine: `cve-lite advisories sync --output /path/to/advisories.db`")); + if (isSslCertificateError(errorMessage)) { + const [hint, ...rest] = sslCertificateErrorHint(); + 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))); } process.exit(1); }); @@ -358,7 +362,7 @@ async function scanProject(params: { const minSeverity = normalizeSeverity(params.options.minSeverity || "medium"); const tableFindings = params.options.all ? sorted - : sorted.filter(f => severityOrder[f.severity] >= severityOrder[minSeverity]); + : sorted.filter(f => severityOrder[f.severity] >= severityOrder[minSeverity] || f.severity === "unknown"); const suggestedFixCommands = buildSuggestedFixCommandPlan(sorted, params.scanInput, { offline }); return { diff --git a/src/utils/network.ts b/src/utils/network.ts index 278ba4d..1a77c8b 100644 --- a/src/utils/network.ts +++ b/src/utils/network.ts @@ -1,3 +1,36 @@ +export function sslCertificateErrorHint(): string[] { + return [ + "Hint: SSL certificate error — your network may be using a corporate proxy that intercepts HTTPS traffic.", + "Fix 1 (recommended): trust your corporate CA certificate:", + " NODE_EXTRA_CA_CERTS=/path/to/corporate-ca.crt cve-lite .", + "Fix 2 (quick workaround, not recommended for production): disable TLS verification:", + " NODE_TLS_REJECT_UNAUTHORIZED=0 cve-lite .", + ]; +} + +export function blockedAdvisoryRequestHint(): string[] { + return [ + "Hint: Outbound access to the OSV API may be blocked or restricted in this environment.", + "If that is expected, build the advisory DB on a machine with OSV access, then scan here with `--offline` or `--offline-db /path/to/advisories.db`.", + "Command to build the DB on a network-allowed machine: `cve-lite advisories sync --output /path/to/advisories.db`", + ]; +} + +export function isSslCertificateError(message: string): boolean { + const normalized = message.toLowerCase(); + return [ + "self-signed certificate", + "self_signed_cert_in_chain", + "cert_untrusted", + "unable_to_verify_leaf_signature", + "depth_zero_self_signed_cert", + "certificate has expired", + "cert_has_expired", + "unable to verify the first certificate", + "certificate chain", + ].some(indicator => normalized.includes(indicator)); +} + export function isLikelyBlockedAdvisoryRequestError(message: string): boolean { if (!message.includes("OSV")) { return false; diff --git a/tests/network.test.ts b/tests/network.test.ts index 4a672ac..2698419 100644 --- a/tests/network.test.ts +++ b/tests/network.test.ts @@ -1,4 +1,30 @@ -import { isLikelyBlockedAdvisoryRequestError } from "../src/utils/network.js"; +import { isLikelyBlockedAdvisoryRequestError, isSslCertificateError } from "../src/utils/network.js"; + +describe("isSslCertificateError", () => { + it("returns true for self-signed certificate errors", () => { + expect(isSslCertificateError("OSV batch query failed: self-signed certificate in certificate chain")).toBe(true); + expect(isSslCertificateError("fetch failed: self_signed_cert_in_chain")).toBe(true); + expect(isSslCertificateError("SELF_SIGNED_CERT_IN_CHAIN")).toBe(true); + }); + + it("returns true for untrusted certificate errors", () => { + expect(isSslCertificateError("cert_untrusted")).toBe(true); + expect(isSslCertificateError("unable_to_verify_leaf_signature")).toBe(true); + expect(isSslCertificateError("depth_zero_self_signed_cert")).toBe(true); + expect(isSslCertificateError("unable to verify the first certificate")).toBe(true); + }); + + it("returns true for expired certificate errors", () => { + expect(isSslCertificateError("certificate has expired")).toBe(true); + expect(isSslCertificateError("cert_has_expired")).toBe(true); + }); + + it("returns false for unrelated network errors", () => { + expect(isSslCertificateError("ECONNREFUSED")).toBe(false); + expect(isSslCertificateError("fetch failed")).toBe(false); + expect(isSslCertificateError("403 Forbidden")).toBe(false); + }); +}); describe("isLikelyBlockedAdvisoryRequestError", () => { it("returns true for OSV failures that look like blocked or restricted network access", () => {