diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 4c0f08d506..e5eaa16a89 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -111,6 +111,8 @@ jobs: - name: Check for vulnerabilities (with exclusions) run: | + set -euo pipefail + # Ignored vulnerabilities with justification: # Go stdlib advisories published 2026-06-02, all fixed in go1.26.4 / # go1.25.11 (DoS / log-injection class, no RCE): @@ -123,21 +125,51 @@ jobs: # # GO-2026-5932: golang.org/x/crypto/openpgp is deprecated-by-design # ("unsafe, not maintained, should not be used"). No fixed version - # exists and none is planned. ToolHive does not import openpgp - # directly; it is pulled transitively via sigstore (rekor/sigstore-go) - # for backward-compatible OpenPGP verification. Remove when sigstore - # drops the openpgp dependency. + # exists and none is planned. + # + # ToolHive does not import openpgp. It arrives four levels up: + # pkg/skills/signer -> sigstore-go/pkg/sign -> rekor/pkg/pki + # -> rekor/pkg/pki/pgp -> golang.org/x/crypto/openpgp + # rekor/pkg/pki is a pluggable signature-format registry, so + # importing it links every format, PGP included. Every govulncheck + # trace is package-init reachability, not a call: the code is + # linked but never parses PGP data on any ToolHive path. + # + # REMOVAL TRIGGER: rekor migrated to ProtonMail/go-crypto/openpgp + # in sigstore/rekor#2883 (merged 2026-07-15), which landed after + # v1.5.3 (2026-07-02) and so is not in any release yet. Bump rekor + # past v1.5.3 once a release contains it, then drop this entry. IGNORED_VULNS="GO-2026-5037 GO-2026-5038 GO-2026-5039 GO-2026-5932" # Show the raw output for debugging + # A scan that produced nothing is a failed scan, not a clean one. + # Without this, a crashed or truncated run yields an empty finding + # list indistinguishable from "no vulnerabilities" and the gate + # goes green. + if [ ! -s govulncheck-output.json ]; then + echo "::error::govulncheck produced no output; refusing to report success" + exit 1 + fi + echo "::group::govulncheck raw output" cat govulncheck-output.json echo "::endgroup::" - # Extract vulnerability IDs that have actual findings (called symbols) - # The JSON has "finding" objects with "osv" field only for vulnerabilities - # where vulnerable code paths are actually called - FOUND_VULNS=$(jq -r 'select(.finding != null) | .finding.osv' govulncheck-output.json | sort -u | grep -E '^GO-' || true) + # Extract vulnerability IDs that have actual findings (called symbols). + # The JSON has "finding" objects with an "osv" field only for + # vulnerabilities whose vulnerable code paths are actually called. + # + # jq failures must be fatal. A trailing `|| true` on the pipeline + # would swallow a parse error and hand us an empty list, which is + # how malformed output could pass the gate. + if ! jq -r 'select(.finding != null) | .finding.osv' govulncheck-output.json > found-osv.txt; then + echo "::error::could not parse govulncheck output; treating as a failed scan" + exit 1 + fi + + # grep exiting 1 means "no matches", which is legitimately clean — + # that is the only non-zero status tolerated here. + FOUND_VULNS=$(sort -u found-osv.txt | { grep -E '^GO-' || true; }) if [ -z "$FOUND_VULNS" ]; then echo "✅ No vulnerabilities found"