feat(govulncheck): reachability-aware Go vulnerability scanner#250
feat(govulncheck): reachability-aware Go vulnerability scanner#250BGebken wants to merge 1 commit into
Conversation
Presence-based scanners (grype/trivy/osv) flag every known vuln in every dependency in a Go module's graph, regardless of whether the vulnerable symbol is ever called — the false-positive class that prompts no-op fix PRs against upstreams whose maintainers point out "the code isn't used." govulncheck builds the call graph from source and reports a vuln only when the affected symbol is actually reachable. This adds it as a first-class SDK scanner plus a composite action so the "verify with govulncheck" step is automated in the pipeline. Scanner (argus/scanners/govulncheck.py): - category=sca, languages=[go]; runs `govulncheck -json ./...`. - Parses govulncheck's concatenated-JSON message stream (not one doc), correlates finding↔osv by id, collapses per-level findings per vuln. - Two tiers: reachable (real OSV severity, gates) and imported-but-not- called (INFO, metadata.reachable=false, never gates). Reachable findings carry the reconstructed call stack + vulnerable symbol in metadata. - Go advisories often lack CVSS → reachable findings may be UNKNOWN severity (documented; pair with osv for CVSS gating). Plumbing: - run_subprocess_scan gains an optional cwd= (govulncheck resolves ./... against the working dir); scan() sets cwd=path. Container path uses the image's WORKDIR /workspace since the engine sets no -w. Backward-compat: cwd defaults to None (unchanged for every other scanner). - Pinned CUSTOM_IMAGES entry (placeholder digest until first publish) + docker/Dockerfile.govulncheck (golang base, govulncheck, WORKDIR /workspace, writable Go caches under /tmp since /workspace is ro). - Composite action sets up Go + installs govulncheck (no official image), then runs via the SDK like every other scanner. Tests: 21 cases (reachability tiers, severity mapping, stream parsing, malformed-stream→parse_failed, build_args/cwd wiring); module at 99%. Docs/AICaC: docs/scanners.md Dependency Scanners section, action README, .ai/architecture.yaml, and a .ai/errors.yaml entry mapping the Go false-positive symptom to the govulncheck remedy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
E2E Test Coverage Report
Summary
❌ Actions Missing E2E Tests
|
🚀 Release Preview📦 Version UpdateCurrent: 📋 Changelog1.5.0 (2026-06-09)Features
🔍 Version Reference Coverage✅ Version refs found: 352 across 117 files All covered by release-it config. ✅ Actions that would be performed
This preview is generated by running |
🔒 Argus Container Security ScanBranch: 📊 Combined Findings Summary
Scanned: 5 containers | Build Failures: 0 📦 Container Breakdown
🔍 Detailed Findings by Container🚨 cli - 108 vulnerabilities (45 unique)Image: Combined (Deduplicated)
🔷 Trivy Scanner (108 findings, 43 unique)
...and 58 more ⚓ Grype Scanner (0 findings, 0 unique)✅ No vulnerabilities detected by Grype ✅ scanner-bandit - 0 vulnerabilities (0 unique)Image: Combined (Deduplicated)
🔷 Trivy Scanner (0 findings, 0 unique)✅ No vulnerabilities detected by Trivy ⚓ Grype Scanner (0 findings, 0 unique)✅ No vulnerabilities detected by Grype ✅ scanner-mumps - 0 vulnerabilities (0 unique)Image: Combined (Deduplicated)
🔷 Trivy Scanner (0 findings, 0 unique)✅ No vulnerabilities detected by Trivy ⚓ Grype Scanner (0 findings, 0 unique)✅ No vulnerabilities detected by Grype 🚨 scanner-opengrep - 106 vulnerabilities (49 unique)Image: Combined (Deduplicated)
🔷 Trivy Scanner (106 findings, 48 unique)
...and 56 more ⚓ Grype Scanner (0 findings, 0 unique)✅ No vulnerabilities detected by Grype
|
| 🚨 Critical | 🟡 Medium | 🔵 Low | Total | Unique | |
|---|---|---|---|---|---|
| 0 | 12 | 6 | 0 | 18 | 18 |
🔷 Trivy Scanner (18 findings, 18 unique)
| CVE | Severity | Package | Version | Fixed |
|---|---|---|---|---|
| CVE-2026-32280 | stdlib | v1.26.1 | 1.25.9, 1.26.2 | |
| CVE-2026-32281 | stdlib | v1.26.1 | 1.25.9, 1.26.2 | |
| CVE-2026-32283 | stdlib | v1.26.1 | 1.25.9, 1.26.2 | |
| CVE-2026-33810 | stdlib | v1.26.1 | 1.26.2 | |
| CVE-2026-33811 | stdlib | v1.26.1 | 1.25.10, 1.26.3 | |
| CVE-2026-33814 | stdlib | v1.26.1 | 1.25.10, 1.26.3 | |
| CVE-2026-39820 | stdlib | v1.26.1 | 1.25.10, 1.26.3 | |
| CVE-2026-39823 | stdlib | v1.26.1 | 1.25.10, 1.26.3 | |
| CVE-2026-39825 | stdlib | v1.26.1 | 1.25.10, 1.26.3 | |
| CVE-2026-39836 | stdlib | v1.26.1 | 1.25.10, 1.26.3 | |
| CVE-2026-42499 | stdlib | v1.26.1 | 1.25.10, 1.26.3 | |
| CVE-2026-42504 | stdlib | v1.26.1 | 1.25.11, 1.26.4 | |
| CVE-2026-27145 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.11, 1.26.4 |
| CVE-2026-32282 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.9, 1.26.2 |
| CVE-2026-32288 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.9, 1.26.2 |
| CVE-2026-32289 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.9, 1.26.2 |
| CVE-2026-39826 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.10, 1.26.3 |
| CVE-2026-42507 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.11, 1.26.4 |
⚓ Grype Scanner (0 findings, 0 unique)
✅ No vulnerabilities detected by Grype
Generated by Argus
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
Drafted this up - not sure if it makes sense though, per discussion thread |
Why
A Go project (OPA) scan surfaced CVEs that turned out to be false positives — the vulnerable code paths weren't reachable. The upstream maintainer's guidance: "in future, please consider using govulncheck to verify."
Every vuln scanner Argus runs against Go today is presence-based — grype/trivy (container) and osv (lockfile) flag a CVE whenever the vulnerable package is in the dependency graph, regardless of whether the vulnerable symbol is ever called.
gosecis Go SAST (code patterns), not dependency-CVE reachability. So Argus had no way to filter the "imported but never called" false-positive class.govulncheckis Go's official scanner and is reachability-aware: it builds the program call graph from source and reports a vulnerability only when the affected symbol is actually reachable. This PR adds it as a first-class scanner so that verification is automated in the pipeline rather than a manual afterthought.What
SDK scanner —
argus/scanners/govulncheck.py(category=sca,languages=[go]):govulncheck -json ./...and parses govulncheck's concatenated-JSON message stream (it's not a single document), correlating eachfindingwith itsosvrecord and collapsing per-level findings into one Finding per vuln.fail_on_severity. Carries the reconstructed call stack (metadata.call_stack) +metadata.vulnerable_symbol.INFO,metadata.reachable: false, titled[imported, not called]. Visible for audit, never gates — this is the false-positive class, surfaced transparently instead of as an actionable CVE.Composite action —
.github/actions/scanner-govulncheck/sets up Go, installs govulncheck (no official binary/image exists upstream — it ships viago install), and runs through the SDK like every other scanner.Image — pinned
CUSTOM_IMAGESentry +docker/Dockerfile.govulncheck(golang base + pinned govulncheck,WORKDIR /workspace, Go caches redirected to/tmpsince/workspacemounts read-only). Per the agreed scope: code-only — the release pipeline builds/publishes the image, so the digest is a placeholder for now (zero sha256) and the comment says so; until first publish the scanner runs from a locally-installedgovulncheck(the SDK prefers the local binary).Plumbing —
run_subprocess_scangains an optionalcwd=(govulncheck resolves./...against the working directory).scan()setscwd=path; the container path gets the same anchor from the image'sWORKDIR /workspace(the engine sets no-w). Backward-compatible:cwddefaults toNone, unchanged for every other scanner.Design notes
GO-YYYY-NNNN) frequently carry no CVSS, so a reachable finding may beUNKNOWNseverity. That's a Go vuln-DB limitation, not a parse bug — documented, with the recommendation to pair govulncheck withosvwhen you need CVSS-derived gating. govulncheck's value here is the reachability signal.-oflag — it streams to stdout. Both the SDK template (run_subprocess_scan) and the engine's container path already capture stdout into the results file, and parsing is exit-code-independent (govulncheck exits 3 when it finds vulns), so both execution paths work without special-casing.report_unreachabletoggle, butparse_resultsreceives only the output path in both the local and container paths (the engine calls it directly, bypassingscan()), so the flag couldn't work for container runs. Dropped it — unreachable findings are always emitted asINFO(transparent, never gating) rather than shipping a knob that works in one path and silently not the other.Tests
21 cases in
argus/tests/scanners/test_govulncheck.py(reachability tiers, severity mapping incl. UNKNOWN, streamed-JSON parsing, malformed-stream →parse_failed, empty-stream →[],build_argsrelative-pattern guard,cwdwiring, registry registration). New module at 99% coverage. Fixtures are realistic govulncheck-jsonstreams.The architecture-sync CI check passes. The only red in the container/linter test files is a pre-existing Windows path-separator bug (
test_happy_path_extracts_files,test_workspace_is_resolved_to_absolute) confirmed to fail on cleanmainand untouched by this change.Docs / AICaC
docs/scanners.md— new Dependency Scanners → govulncheck (Go) section (tiers table, severity note, relationship to gosec/osv)..github/actions/scanner-govulncheck/README.md— action usage..ai/architecture.yaml— scanner entry..ai/errors.yaml— maps the "Go CVE false positive / code isn't used" symptom to the govulncheck remedy.CLAUDE.md— Dependencies row.Follow-up (out of scope here)
The custom image must be built + published and its digest pinned (release pipeline / Renovate), replacing the placeholder, before container-backed runs work without a local govulncheck.
🤖 Generated with Claude Code