Watch
1
0
Fork
You've already forked RedFlag
0
RedFlag/scripts/govulncheck-gate.py
Fimeg e2dab2845a supply-chain: gate our own deps, ship the verdict signed
dep-scan.sh gates go/npm/cargo on push and bakes an attested posture into the
release — embedded in the server, signed into the manifest. Reasoning and the
two Moby exceptions are in SECURITY.md.

(posture-builder runs rustup; bookworm's cargo is too old for cargo-audit.)
2026-06-14 11:43:23 -04:00

136 lines
4.9 KiB
Python
Executable file

#!/usr/bin/env python3
"""
govulncheck reachability gate.
govulncheck has no native ignore mechanism. RedFlag links github.com/docker/docker
as a client and inherits two daemon-side Moby CVEs that have no fixed version, so a
bare `govulncheck ./...` can never go green. This gate reads govulncheck JSON, keeps
only the *reachable* (called) vulns, subtracts a documented allowlist, and fails the
build on anything left over.
govulncheck -format=json ./... | govulncheck-gate.py --allow ../.govulncheck-allow
Exit codes:
0 no un-allowlisted reachable vulns
1 one or more reachable vulns not in the allowlist -> CI fails
2 usage / parse error
A vuln is "reachable" when govulncheck emits a finding whose trace has a frame with
a populated `function` (symbol-level), per govulncheck's own semantics. Findings that
stop at module/package level (imported but not called) are reported as advisory only.
"""
import argparse
import json
import re
import sys
GO_ID = re.compile(r"^GO-\d{4}-\d+$")
def load_allow(path):
"""Return {id: reason} from the allowlist file (blank/`#` lines ignored)."""
allow = {}
if not path:
return allow
with open(path, encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split(None, 1)
ident = parts[0]
if not GO_ID.match(ident):
print(f"[WARN] [dep-scan] [govulncheck-gate] allowlist line ignored "
f"(not a GO-id): {line}", file=sys.stderr)
continue
allow[ident] = parts[1].strip() if len(parts) > 1 else "(no reason given)"
return allow
def parse_findings(text):
"""
govulncheck -format=json emits a stream of concatenated JSON objects.
Return {osv_id: reachable_bool} merged across all findings for that id.
"""
reachable = {}
osv_titles = {}
decoder = json.JSONDecoder()
idx, n = 0, len(text)
while idx < n:
while idx < n and text[idx] in " \t\r\n":
idx += 1
if idx >= n:
break
obj, end = decoder.raw_decode(text, idx)
idx = end
if "osv" in obj:
osv = obj["osv"]
osv_titles[osv.get("id", "")] = (osv.get("summary") or "").strip()
if "finding" in obj:
f = obj["finding"]
osv_id = f.get("osv")
if not osv_id:
continue
frames = f.get("trace") or []
called = any(fr.get("function") for fr in frames)
reachable[osv_id] = reachable.get(osv_id, False) or called
return reachable, osv_titles
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--allow", default=None, help="path to .govulncheck-allow")
ap.add_argument("--label", default="", help="module label for log lines")
ap.add_argument("--summary-out", default=None,
help="write a JSON verdict {accepted,blocked,exceptions} here "
"for posture assembly")
args = ap.parse_args()
raw = sys.stdin.read()
if not raw.strip():
print("[ERROR] [dep-scan] [govulncheck-gate] no JSON on stdin", file=sys.stderr)
return 2
try:
reachable, titles = parse_findings(raw)
except (json.JSONDecodeError, ValueError) as exc:
print(f"[ERROR] [dep-scan] [govulncheck-gate] bad govulncheck JSON: {exc}",
file=sys.stderr)
return 2
allow = load_allow(args.allow)
label = f"[{args.label}] " if args.label else ""
called = sorted(i for i, r in reachable.items() if r)
blocked = [i for i in called if i not in allow]
accepted = [i for i in called if i in allow]
stale = [i for i in allow if i not in called]
for i in accepted:
print(f"[INFO] [dep-scan] [govulncheck-gate] {label}accepted {i}: "
f"{titles.get(i, '')} -- {allow[i]}")
for i in stale:
print(f"[WARN] [dep-scan] [govulncheck-gate] {label}stale exception {i} "
f"no longer reachable -- remove it from .govulncheck-allow")
for i in blocked:
print(f"[ERROR] [dep-scan] [govulncheck-gate] {label}reachable & NOT "
f"allowlisted: {i}: {titles.get(i, '')}")
if args.summary_out:
with open(args.summary_out, "w", encoding="utf-8") as fh:
json.dump({
"accepted": len(accepted),
"blocked": len(blocked),
"exceptions": [{"id": i, "reason": allow[i]} for i in accepted],
}, fh)
if blocked:
print(f"[ERROR] [dep-scan] [govulncheck-gate] {label}{len(blocked)} "
f"un-accepted reachable vulnerability(ies) -- build blocked")
return 1
print(f"[INFO] [dep-scan] [govulncheck-gate] {label}clean "
f"({len(accepted)} accepted, 0 blocked)")
return 0
if __name__ == "__main__":
sys.exit(main())