Skip to content

Commit 40fe24b

Browse files
authored
feat: hint offline advisory DB workflow for blocked OSV requests (#154)
1 parent 1be71e6 commit 40fe24b

4 files changed

Lines changed: 91 additions & 1 deletion

File tree

src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { chalk } from "./utils/chalk.js";
1414
import { createSpinner } from "./output/spinner.js";
1515
import { buildSuggestedFixCommandPlan } from "./remediation/fix-commands.js";
1616
import { getCliVersion } from "./utils/version-info.js";
17+
import { isLikelyBlockedAdvisoryRequestError } from "./utils/network.js";
1718
import type { SuggestedFixCommandPlan, SuggestedFixTarget } from "./remediation/fix-commands.js";
1819
import type { ParsedOptions } from "./types.js";
1920
import type { Finding, SeverityLabel } from "./types.js";
@@ -267,7 +268,13 @@ if (parsedArgs) {
267268
}
268269

269270
main().catch((error) => {
270-
console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
271+
const errorMessage = error instanceof Error ? error.message : String(error);
272+
console.error(chalk.red(`Error: ${errorMessage}`));
273+
if (isLikelyBlockedAdvisoryRequestError(errorMessage)) {
274+
console.error(chalk.yellow("Hint: Outbound access to the OSV API may be blocked or restricted in this environment."));
275+
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`."));
276+
console.error(chalk.gray("Command to build the DB on a network-allowed machine: `cve-lite advisories sync --output /path/to/advisories.db`"));
277+
}
271278
process.exit(1);
272279
});
273280
}

src/utils/network.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
export function isLikelyBlockedAdvisoryRequestError(message: string): boolean {
2+
if (!message.includes("OSV")) {
3+
return false;
4+
}
5+
6+
const normalized = message.toLowerCase();
7+
const finalCause = normalized.split(":").pop()?.trim() ?? normalized;
8+
const blockedIndicators = [
9+
"access denied",
10+
"blocked",
11+
"body timeout",
12+
"connection refused",
13+
"eai_again",
14+
"econnrefused",
15+
"econnreset",
16+
"enotfound",
17+
"etimedout",
18+
"fetch failed",
19+
"forbidden",
20+
"gateway timeout",
21+
"host unreachable",
22+
"network unavailable",
23+
"proxy",
24+
"socket hang up",
25+
"timed out",
26+
"timeout",
27+
"tunneling socket",
28+
"unable to verify the first certificate",
29+
];
30+
31+
if (blockedIndicators.some(indicator => finalCause.includes(indicator))) {
32+
return true;
33+
}
34+
35+
return /^(401|403|407|408|429|451|502|503|504)\b/.test(finalCause);
36+
}

tests/cli-integration.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,24 @@ describe("CLI integration", () => {
322322
expect(loadPackagesMock).not.toHaveBeenCalled();
323323
});
324324

325+
it("prints an offline advisory DB hint when OSV requests appear blocked", async () => {
326+
loadPackagesMock.mockReturnValue(createScanInput({
327+
packages: [{ name: "lodash", version: "4.17.21", ecosystem: "npm", paths: [["project", "lodash"]] }],
328+
}));
329+
scanPackagesMock.mockRejectedValue(
330+
new Error("OSV batch query failed for https://api.osv.dev: fetch failed"),
331+
);
332+
333+
const result = await runIndexModule();
334+
const stderr = stripAnsi(result.stderr.join("\n"));
335+
336+
expect(result.exitCode).toBe(1);
337+
expect(stderr).toContain("Error: OSV batch query failed for https://api.osv.dev: fetch failed");
338+
expect(stderr).toContain("Hint: Outbound access to the OSV API may be blocked or restricted in this environment.");
339+
expect(stderr).toContain("build the advisory DB on a machine with OSV access");
340+
expect(stderr).toContain("cve-lite advisories sync --output /path/to/advisories.db");
341+
});
342+
325343
it("routes verbose mode through the detailed printer pipeline", async () => {
326344
const finding = createFinding({ severity: "medium" });
327345
parseArgsMock.mockReturnValue({

tests/network.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { isLikelyBlockedAdvisoryRequestError } from "../src/utils/network.js";
2+
3+
describe("isLikelyBlockedAdvisoryRequestError", () => {
4+
it("returns true for OSV failures that look like blocked or restricted network access", () => {
5+
expect(
6+
isLikelyBlockedAdvisoryRequestError(
7+
"OSV batch query failed for https://api.osv.dev: fetch failed",
8+
),
9+
).toBe(true);
10+
11+
expect(
12+
isLikelyBlockedAdvisoryRequestError(
13+
"OSV batch query failed for https://api.osv.dev: OSV batch query failed: 403 Forbidden",
14+
),
15+
).toBe(true);
16+
});
17+
18+
it("returns false for non-OSV errors", () => {
19+
expect(isLikelyBlockedAdvisoryRequestError("Invalid value for --osv-url: not-a-url")).toBe(false);
20+
});
21+
22+
it("returns false for OSV errors that do not look like blocked network access", () => {
23+
expect(
24+
isLikelyBlockedAdvisoryRequestError(
25+
"OSV vuln fetch failed for OSV-404 via https://api.osv.dev: OSV vuln fetch failed for OSV-404: 404 Not Found",
26+
),
27+
).toBe(false);
28+
});
29+
});

0 commit comments

Comments
 (0)