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
9 changes: 8 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { chalk } from "./utils/chalk.js";
import { createSpinner } from "./output/spinner.js";
import { buildSuggestedFixCommandPlan } from "./remediation/fix-commands.js";
import { getCliVersion } from "./utils/version-info.js";
import { isLikelyBlockedAdvisoryRequestError } 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";
Expand Down Expand Up @@ -267,7 +268,13 @@ if (parsedArgs) {
}

main().catch((error) => {
console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(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`"));
}
process.exit(1);
});
}
Expand Down
36 changes: 36 additions & 0 deletions src/utils/network.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export function isLikelyBlockedAdvisoryRequestError(message: string): boolean {
if (!message.includes("OSV")) {
return false;
}

const normalized = message.toLowerCase();
const finalCause = normalized.split(":").pop()?.trim() ?? normalized;
const blockedIndicators = [
"access denied",
"blocked",
"body timeout",
"connection refused",
"eai_again",
"econnrefused",
"econnreset",
"enotfound",
"etimedout",
"fetch failed",
"forbidden",
"gateway timeout",
"host unreachable",
"network unavailable",
"proxy",
"socket hang up",
"timed out",
"timeout",
"tunneling socket",
"unable to verify the first certificate",
];

if (blockedIndicators.some(indicator => finalCause.includes(indicator))) {
return true;
}

return /^(401|403|407|408|429|451|502|503|504)\b/.test(finalCause);
}
18 changes: 18 additions & 0 deletions tests/cli-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,24 @@ describe("CLI integration", () => {
expect(loadPackagesMock).not.toHaveBeenCalled();
});

it("prints an offline advisory DB hint when OSV requests appear blocked", async () => {
loadPackagesMock.mockReturnValue(createScanInput({
packages: [{ name: "lodash", version: "4.17.21", ecosystem: "npm", paths: [["project", "lodash"]] }],
}));
scanPackagesMock.mockRejectedValue(
new Error("OSV batch query failed for https://api.osv.dev: fetch failed"),
);

const result = await runIndexModule();
const stderr = stripAnsi(result.stderr.join("\n"));

expect(result.exitCode).toBe(1);
expect(stderr).toContain("Error: OSV batch query failed for https://api.osv.dev: fetch failed");
expect(stderr).toContain("Hint: Outbound access to the OSV API may be blocked or restricted in this environment.");
expect(stderr).toContain("build the advisory DB on a machine with OSV access");
expect(stderr).toContain("cve-lite advisories sync --output /path/to/advisories.db");
});

it("routes verbose mode through the detailed printer pipeline", async () => {
const finding = createFinding({ severity: "medium" });
parseArgsMock.mockReturnValue({
Expand Down
29 changes: 29 additions & 0 deletions tests/network.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { isLikelyBlockedAdvisoryRequestError } from "../src/utils/network.js";

describe("isLikelyBlockedAdvisoryRequestError", () => {
it("returns true for OSV failures that look like blocked or restricted network access", () => {
expect(
isLikelyBlockedAdvisoryRequestError(
"OSV batch query failed for https://api.osv.dev: fetch failed",
),
).toBe(true);

expect(
isLikelyBlockedAdvisoryRequestError(
"OSV batch query failed for https://api.osv.dev: OSV batch query failed: 403 Forbidden",
),
).toBe(true);
});

it("returns false for non-OSV errors", () => {
expect(isLikelyBlockedAdvisoryRequestError("Invalid value for --osv-url: not-a-url")).toBe(false);
});

it("returns false for OSV errors that do not look like blocked network access", () => {
expect(
isLikelyBlockedAdvisoryRequestError(
"OSV vuln fetch failed for OSV-404 via https://api.osv.dev: OSV vuln fetch failed for OSV-404: 404 Not Found",
),
).toBe(false);
});
});
Loading