diff --git a/Makefile b/Makefile index f313994cb..3f31188d3 100644 --- a/Makefile +++ b/Makefile @@ -412,7 +412,7 @@ fix: govulncheck: @echo "Running govulncheck..." - $(_Q)scripts/run-gate.sh govulncheck go tool govulncheck ./... + $(_Q)scripts/run-gate.sh govulncheck scripts/govulncheck-gate.sh @echo " govulncheck ok" # NilAway — advisory nil-panic analysis. Not in preflight due to false positives diff --git a/govulncheck-accepted.txt b/govulncheck-accepted.txt new file mode 100644 index 000000000..680ab6fa0 --- /dev/null +++ b/govulncheck-accepted.txt @@ -0,0 +1,4 @@ +# Reachable vulnerabilities accepted for a bounded time, read by scripts/govulncheck-gate.sh. +# Format: +# The gate fails once the expiry passes: fix the dependency or renew deliberately. +GO-2026-6597 2026-11-07 containerd v2.4.0 image-pull DoS, fixed in v2.4.1; inherited from dev and deferred to a dependency update outside the package-boundary refactor. diff --git a/scripts/govulncheck-gate.sh b/scripts/govulncheck-gate.sh new file mode 100755 index 000000000..a6dec4f0f --- /dev/null +++ b/scripts/govulncheck-gate.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Fails on any reachable vulnerability that is not listed in +# govulncheck-accepted.txt, and on any acceptance whose expiry has passed. +# Each line is " "; '#' starts a comment. +# GOVULNCHECK_JSON substitutes a saved -format json report, for tests. +set -euo pipefail + +accepted_file=${GOVULNCHECK_ACCEPTED:-govulncheck-accepted.txt} +today=${GOVULNCHECK_TODAY:-$(date -u +%F)} + +if [ -n "${GOVULNCHECK_JSON:-}" ]; then + report=$(cat "$GOVULNCHECK_JSON") +else + report=$(go tool govulncheck -format json ./...) +fi + +# A finding whose first trace frame names a function is reachable from our +# code; text mode reports only these, so module- and package-level ones pass. +mapfile -t called < <(printf '%s\n' "$report" | jq -rs \ + '[.[] | select(.finding) | .finding | select(.trace[0].function) | .osv] | unique | .[]') + +declare -A expiry reason +if [ -f "$accepted_file" ]; then + while read -r id until why; do + [[ -z "$id" || "$id" == \#* ]] && continue + expiry[$id]=$until + reason[$id]=$why + done <"$accepted_file" +fi + +fail=0 +for id in "${called[@]}"; do + if [ -z "${expiry[$id]:-}" ]; then + echo " $id is reachable and not accepted; run 'go tool govulncheck ./...' for the trace" + fail=1 + elif [[ "$today" > "${expiry[$id]}" ]]; then + echo " $id acceptance expired on ${expiry[$id]}: ${reason[$id]}" + fail=1 + else + echo " $id accepted until ${expiry[$id]}: ${reason[$id]}" + fi +done + +for id in "${!expiry[@]}"; do + if ! printf '%s\n' "${called[@]}" | grep -qx "$id"; then + echo " $id is accepted but no longer reported; remove it from $accepted_file" + fi +done + +exit "$fail" diff --git a/scripts/govulncheck-gate_test.sh b/scripts/govulncheck-gate_test.sh new file mode 100755 index 000000000..c69672744 --- /dev/null +++ b/scripts/govulncheck-gate_test.sh @@ -0,0 +1,53 @@ +#!/bin/sh +# Tests the accepted-vulnerability gate against saved reports, so the logic is +# checked without running the scanner. +set -u + +here=$(cd "$(dirname "$0")" && pwd) +gate="$here/govulncheck-gate.sh" +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +cat >"$tmp/accepted.txt" <<'EOF' +# comment +GO-2026-6597 2026-11-07 accepted for the test +EOF +cat >"$tmp/reachable.json" <<'EOF' +{"config":{}} +{"finding":{"osv":"GO-2026-6597","trace":[{"module":"m","package":"p","function":"Pull"}]}} +{"finding":{"osv":"GO-2026-0001","trace":[{"module":"m"}]}} +EOF +cat >"$tmp/new.json" <<'EOF' +{"finding":{"osv":"GO-2026-6597","trace":[{"module":"m","package":"p","function":"Pull"}]}} +{"finding":{"osv":"GO-2026-7777","trace":[{"module":"m","package":"p","function":"F"}]}} +EOF +echo '{"config":{}}' >"$tmp/clean.json" + +fails=0 +check() { + want=$1 name=$2 report=$3 today=$4 accepted=$5 expect=$6 + out=$(GOVULNCHECK_JSON="$tmp/$report" GOVULNCHECK_TODAY=$today GOVULNCHECK_ACCEPTED=$accepted bash "$gate" 2>&1) + rc=$? + if [ "$rc" != "$want" ] || ! printf '%s' "$out" | grep -q "$expect"; then + echo "FAIL [$name] rc=$rc want=$want output: $out" + fails=$((fails + 1)) + else + echo "ok [$name]" + fi +} + +check 0 "accepted reachable finding passes" reachable.json 2026-10-07 "$tmp/accepted.txt" "GO-2026-6597 accepted until 2026-11-07" +check 0 "expiry day itself still passes" reachable.json 2026-11-07 "$tmp/accepted.txt" "accepted until" +check 1 "expired acceptance fails" reachable.json 2026-11-08 "$tmp/accepted.txt" "acceptance expired on 2026-11-07" +check 1 "new reachable finding fails" new.json 2026-10-07 "$tmp/accepted.txt" "GO-2026-7777 is reachable and not accepted" +out=$(GOVULNCHECK_JSON="$tmp/reachable.json" GOVULNCHECK_TODAY=2026-10-07 GOVULNCHECK_ACCEPTED="$tmp/accepted.txt" bash "$gate" 2>&1) +if printf '%s' "$out" | grep -q "GO-2026-0001"; then + echo "FAIL [module-level finding is not reachable] output: $out" + fails=$((fails + 1)) +else + echo "ok [module-level finding is not reachable]" +fi +check 0 "fixed finding warns that acceptance is stale" clean.json 2026-10-07 "$tmp/accepted.txt" "no longer reported" +check 1 "missing acceptance file fails reachable finding" reachable.json 2026-10-07 "$tmp/none" "not accepted" + +[ "$fails" -eq 0 ] || exit 1