From 3e3bc5dd0a5078af4f3d72ec18df8aa17d837b40 Mon Sep 17 00:00:00 2001 From: joshsiv-mulga Date: Wed, 7 Oct 2026 13:59:06 +1000 Subject: [PATCH] chore: accept known vulnerabilities with an expiry The vulnerability scan failed preflight on a containerd advisory that dev already carries and that is deferred to a separate dependency update. The gate now passes only reachable findings listed with an expiry date, fails on any new one or on an expired acceptance, and warns when an acceptance is no longer needed. --- Makefile | 2 +- govulncheck-accepted.txt | 4 +++ scripts/govulncheck-gate.sh | 50 ++++++++++++++++++++++++++++++ scripts/govulncheck-gate_test.sh | 53 ++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 govulncheck-accepted.txt create mode 100755 scripts/govulncheck-gate.sh create mode 100755 scripts/govulncheck-gate_test.sh 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