|
1 | 1 | /** |
2 | 2 | * OSV-Scanner wrapper — dependency vulnerability scanner. |
3 | 3 | * |
4 | | - * Invocation: `osv-scanner scan --format=sarif --offline-vulnerabilities |
5 | | - * --recursive .` |
| 4 | + * Invocation: `osv-scanner scan source --format=sarif --recursive .` |
6 | 5 | * |
7 | | - * We enable offline mode by default — OpenCodeHub's v1.0 posture is that |
8 | | - * the vulnerability database is pre-synced by `codehub db-sync`. |
9 | | - * `--offline-vulnerabilities` keeps the tool from reaching osv.dev while |
10 | | - * still allowing local lockfile resolution to touch manifests on disk. |
| 6 | + * Exit-code semantics (osv-scanner v2, per |
| 7 | + * https://google.github.io/osv-scanner/output/ "Return Codes"): |
| 8 | + * |
| 9 | + * 0 — packages found, no vulnerabilities/findings. |
| 10 | + * 1 — packages found, vulnerabilities/findings present (the COMMON |
| 11 | + * non-zero case; NOT an error). |
| 12 | + * 2-126 — reserved for vulnerability/finding-related results. |
| 13 | + * 127 — GENERAL ERROR (e.g. the offline vulnerability DB could not be |
| 14 | + * loaded). Note: the scan may still have walked the filesystem and |
| 15 | + * parsed lockfiles before failing, so partial stderr ("Scanned … |
| 16 | + * uv.lock … found N packages") does NOT mean the scan succeeded. |
| 17 | + * 128 — no packages discovered (scanning format picked up no files). |
| 18 | + * 129-255 — non-result-related errors. |
| 19 | + * |
| 20 | + * The shared `invokeScanner` treats only 0 and 1 as "ran cleanly". For |
| 21 | + * osv-scanner that is too coarse: it would flag exit 1 (vulns found — the |
| 22 | + * whole point of the scan) as a warning, and it would surface exit 127 as |
| 23 | + * a generic "exit code 127" without explaining that the most likely cause |
| 24 | + * is a missing offline database. We interpret osv's codes explicitly here. |
| 25 | + * |
| 26 | + * Offline posture: earlier revisions passed `--offline-vulnerabilities` by |
| 27 | + * default on the assumption the DB was pre-synced by `codehub db-sync`. |
| 28 | + * On a fresh checkout with no synced DB, osv-scanner walks the tree, then |
| 29 | + * fails to load the offline DB and exits 127 — a confusing "it ran but |
| 30 | + * errored" signal. We DROP the default offline flag so the common case |
| 31 | + * (online lookup) works out of the box; operators who need air-gapped |
| 32 | + * scans pass it back via osv-scanner's own env/flags after `db-sync`. |
11 | 33 | */ |
12 | 34 |
|
13 | 35 | import { OSV_SCANNER_SPEC } from "../catalog.js"; |
14 | 36 | import type { ScannerRunContext, ScannerRunResult, ScannerWrapper } from "../spec.js"; |
15 | | -import { DEFAULT_DEPS, invokeScanner, type WrapperDeps } from "./shared.js"; |
| 37 | +import { emptySarifFor } from "../spec.js"; |
| 38 | +import { DEFAULT_DEPS, parseSarifOrEmpty, type WrapperDeps } from "./shared.js"; |
16 | 39 |
|
17 | | -const OSV_ARGS: readonly string[] = [ |
18 | | - "scan", |
19 | | - "--format=sarif", |
20 | | - "--offline-vulnerabilities", |
21 | | - "--recursive", |
22 | | - ".", |
23 | | -]; |
| 40 | +const OSV_ARGS: readonly string[] = ["scan", "source", "--format=sarif", "--recursive", "."]; |
| 41 | + |
| 42 | +/** |
| 43 | + * Map an osv-scanner v2 exit code to an advisory note, or `undefined` when |
| 44 | + * the exit represents a clean / findings-present run that needs no warning. |
| 45 | + */ |
| 46 | +export function osvExitAdvisory(exitCode: number, stderr: string): string | undefined { |
| 47 | + // 0 = no findings; 1-126 = findings present (the normal non-zero outcome). |
| 48 | + if (exitCode >= 0 && exitCode <= 126) return undefined; |
| 49 | + if (exitCode === 127) { |
| 50 | + return ( |
| 51 | + `${OSV_SCANNER_SPEC.id}: general error (exit 127). Common cause: the offline ` + |
| 52 | + `vulnerability DB is missing — run \`codehub db-sync\` then retry, or omit ` + |
| 53 | + `offline mode. stderr: ${truncate(stderr, 160)}` |
| 54 | + ); |
| 55 | + } |
| 56 | + if (exitCode === 128) { |
| 57 | + // No packages discovered. Benign for repos with no lockfiles/manifests. |
| 58 | + return `${OSV_SCANNER_SPEC.id}: no packages discovered (exit 128); nothing to scan.`; |
| 59 | + } |
| 60 | + return `${OSV_SCANNER_SPEC.id}: exit code ${exitCode}; stderr: ${truncate(stderr, 160)}`; |
| 61 | +} |
24 | 62 |
|
25 | 63 | export function createOsvScannerWrapper(deps: WrapperDeps = DEFAULT_DEPS): ScannerWrapper { |
26 | 64 | return { |
27 | 65 | spec: OSV_SCANNER_SPEC, |
28 | | - run: (ctx: ScannerRunContext): Promise<ScannerRunResult> => |
29 | | - invokeScanner(OSV_SCANNER_SPEC, ctx, "osv-scanner", OSV_ARGS, deps), |
| 66 | + run: async (ctx: ScannerRunContext): Promise<ScannerRunResult> => { |
| 67 | + const started = performance.now(); |
| 68 | + const probe = await deps.which("osv-scanner"); |
| 69 | + if (!probe.found) { |
| 70 | + const msg = `${OSV_SCANNER_SPEC.id}: binary 'osv-scanner' not found on PATH (install: ${OSV_SCANNER_SPEC.installCmd}).`; |
| 71 | + ctx.onWarn?.(msg); |
| 72 | + return { |
| 73 | + spec: OSV_SCANNER_SPEC, |
| 74 | + sarif: emptySarifFor(OSV_SCANNER_SPEC), |
| 75 | + skipped: msg, |
| 76 | + durationMs: performance.now() - started, |
| 77 | + }; |
| 78 | + } |
| 79 | + const result = await deps.runBinary("osv-scanner", OSV_ARGS, { |
| 80 | + timeoutMs: ctx.timeoutMs, |
| 81 | + cwd: ctx.projectPath, |
| 82 | + }); |
| 83 | + const sarif = parseSarifOrEmpty(result.stdout, OSV_SCANNER_SPEC, ctx.onWarn); |
| 84 | + const advisory = osvExitAdvisory(result.exitCode, result.stderr); |
| 85 | + if (advisory !== undefined) ctx.onWarn?.(advisory); |
| 86 | + return { |
| 87 | + spec: OSV_SCANNER_SPEC, |
| 88 | + sarif, |
| 89 | + durationMs: performance.now() - started, |
| 90 | + }; |
| 91 | + }, |
30 | 92 | }; |
31 | 93 | } |
| 94 | + |
| 95 | +function truncate(s: string, max: number): string { |
| 96 | + if (s.length <= max) return s.trim(); |
| 97 | + return `${s.slice(0, max).trim()}…`; |
| 98 | +} |
0 commit comments