diff --git a/.agents/workflows/cve-fix.md b/.agents/workflows/cve-fix.md index 2bae872f4..d51f65da4 100644 --- a/.agents/workflows/cve-fix.md +++ b/.agents/workflows/cve-fix.md @@ -182,7 +182,16 @@ If fix requires breaking changes (Go version, K8s major version, incompatible AP - CVE severity (Low/Medium vs High/Critical) - Cost/risk of breaking stable branch dependencies -Add to `.grype.yaml`: +**No fix available** — use `fix-state: not-fixed` so the entry auto-expires when a fix is published: +```yaml +# No fix available for [package]. [context]. +- vulnerability: GHSA-xxxx-xxxx-xxxx + fix-state: not-fixed + package: + name: package.name/path +``` + +**Fix exists but would break the branch** — permanent ignore (no fix-state): ```yaml # Update requires [incompatibility]. [Severity] doesn't justify breaking changes. - vulnerability: GHSA-xxxx-xxxx-xxxx diff --git a/scripts/cve/detect.sh b/scripts/cve/detect.sh index ccc758ae2..0ffa9d53b 100755 --- a/scripts/cve/detect.sh +++ b/scripts/cve/detect.sh @@ -84,14 +84,22 @@ CONTAINER_CMD=$(detect_container_cmd) HAS_LOCAL_GRYPE=false command -v grype &>/dev/null && HAS_LOCAL_GRYPE=true -# Shipyard build image -if [[ "$BRANCH" == "devel" ]]; then - SHIPYARD_TAG="devel" -elif [[ "$BRANCH" =~ ^release- ]]; then - SHIPYARD_TAG="$BRANCH" -else - echo "WARNING: Unknown branch pattern, assuming devel build image" - SHIPYARD_TAG="devel" +# Shipyard build image — read BASE_BRANCH from the repo's Makefile (which +# flows into SHIPYARD_TAG via Makefile.dapper), fall back to branch inference. +SHIPYARD_TAG="" +if [[ -f Makefile ]]; then + SHIPYARD_TAG=$(grep -oP '^\s*BASE_BRANCH\s*\?=\s*\K\S+' Makefile || echo "") +fi + +if [[ -z "$SHIPYARD_TAG" ]]; then + if [[ "$BRANCH" == "devel" ]]; then + SHIPYARD_TAG="devel" + elif [[ "$BRANCH" =~ ^release- ]]; then + SHIPYARD_TAG="$BRANCH" + else + echo "WARNING: Unknown branch pattern, assuming devel build image" + SHIPYARD_TAG="devel" + fi fi SHIPYARD_IMAGE="quay.io/submariner/shipyard-dapper-base:${SHIPYARD_TAG}" @@ -104,11 +112,21 @@ fi ORIGINAL_REF="" FIX_BRANCH="" FETCH_FAILED=false +WORKTREE_DIR="" + +# Detect self-referential fix (fixing the repo that contains these scripts). +# Checkout would overwrite the scripts on disk, so use a worktree instead. +SCRIPT_REPO=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "") +SELF_FIX=false +[[ "$SCRIPT_REPO" == "$REPO" ]] && SELF_FIX=true if [[ "$SETUP_BRANCH" == "true" ]]; then - if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then - echo "ERROR: Working tree has uncommitted changes. Commit or stash first." >&2 - exit 1 + # Uncommitted changes check: skip for self-fix (worktree won't touch working tree) + if [[ "$SELF_FIX" != "true" ]]; then + if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then + echo "ERROR: Working tree has uncommitted changes. Commit or stash first." >&2 + exit 1 + fi fi ORIGINAL_REF=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) @@ -130,11 +148,33 @@ if [[ "$SETUP_BRANCH" == "true" ]]; then done FIX_BRANCH="${FIX_BRANCH}${SUFFIX}" - if ! git checkout -b "$FIX_BRANCH" "origin/$BRANCH" 2>/dev/null; then - echo "ERROR: Could not create fix branch from origin/$BRANCH" >&2 - exit 1 + if [[ "$SELF_FIX" == "true" ]]; then + WORKTREE_DIR=$(mktemp -d) + if ! git worktree add -b "$FIX_BRANCH" "$WORKTREE_DIR" "origin/$BRANCH" 2>/dev/null; then + rm -rf "$WORKTREE_DIR" + echo "ERROR: Could not create worktree from origin/$BRANCH" >&2 + exit 1 + fi + REPO="$WORKTREE_DIR" + # Re-detect build image from target branch Makefile + if [[ -f "$WORKTREE_DIR/Makefile" ]]; then + _TAG=$(grep -oP '^\s*BASE_BRANCH\s*\?=\s*\K\S+' "$WORKTREE_DIR/Makefile" || echo "") + if [[ -n "$_TAG" ]]; then + SHIPYARD_TAG="$_TAG" + SHIPYARD_IMAGE="quay.io/submariner/shipyard-dapper-base:${SHIPYARD_TAG}" + if [[ -n "$CONTAINER_CMD" ]] && [[ -n "$($CONTAINER_CMD image ls -q "$SHIPYARD_IMAGE" 2>/dev/null)" ]]; then + SHIPYARD_GO_VERSION=$($CONTAINER_CMD run --rm "$SHIPYARD_IMAGE" go version 2>/dev/null || echo "unknown") + fi + fi + fi + echo "Fix branch: $FIX_BRANCH (worktree: $WORKTREE_DIR)" + else + if ! git checkout -b "$FIX_BRANCH" "origin/$BRANCH" 2>/dev/null; then + echo "ERROR: Could not create fix branch from origin/$BRANCH" >&2 + exit 1 + fi + echo "Fix branch: $FIX_BRANCH" fi - echo "Fix branch: $FIX_BRANCH" fi # --- Write state file --- @@ -154,6 +194,7 @@ HAS_LOCAL_GRYPE=$HAS_LOCAL_GRYPE SHIPYARD_IMAGE="$SHIPYARD_IMAGE" SHIPYARD_GO_VERSION="$SHIPYARD_GO_VERSION" FETCH_FAILED=$FETCH_FAILED +WORKTREE_DIR="$WORKTREE_DIR" CVE_SCRIPTS="$SCRIPT_DIR" EOF diff --git a/scripts/cve/fix-all.sh b/scripts/cve/fix-all.sh index 08c576dad..a2e88d4dc 100755 --- a/scripts/cve/fix-all.sh +++ b/scripts/cve/fix-all.sh @@ -54,8 +54,14 @@ fi if [[ "$MATCH_COUNT" -eq 0 ]]; then echo "No CVEs found. $(basename "$REPO")/$BRANCH is clean." if [[ -n "$FIX_BRANCH" ]]; then - git checkout "$ORIGINAL_REF" 2>/dev/null - git branch -D "$FIX_BRANCH" 2>/dev/null + if [[ -n "$WORKTREE_DIR" ]]; then + cd / 2>/dev/null || true + git -C "$SCRIPT_DIR" worktree remove --force "$WORKTREE_DIR" 2>/dev/null || true + git -C "$SCRIPT_DIR" branch -D "$FIX_BRANCH" 2>/dev/null || true + else + git checkout "$ORIGINAL_REF" 2>/dev/null + git branch -D "$FIX_BRANCH" 2>/dev/null + fi fi exit 0 fi @@ -79,6 +85,7 @@ CVE_LINES=$(printf '%s\n' "$SCAN_JSON" | jq -r ' { pkg: .artifact.name, fixedIn: .vulnerability.fix.versions[0], + allFixVersions: (.vulnerability.fix.versions | join(",")), cve: .vulnerability.id, severity: .vulnerability.severity, type: (if .artifact.name == "stdlib" then "stdlib" else "package" end) @@ -89,11 +96,12 @@ CVE_LINES=$(printf '%s\n' "$SCAN_JSON" | jq -r ' pkg: .[0].pkg, type: .[0].type, fixedIn: (map(.fixedIn) | sort_by(split(".") | map(tonumber)) | last), + allFixVersions: (if .[0].type == "stdlib" then ([.[].allFixVersions] | unique | join(",")) else "" end), cves: (map(.cve) | unique), severity: .[0].severity }) | .[] | - "\(.type)\t\(.pkg)\t\(.fixedIn)\t\(.cves | join(","))\t\(.severity)" + "\(.type)\t\(.pkg)\t\(.fixedIn)\t\(.cves | join(","))\t\(.severity)\t\(.allFixVersions)" ' 2>/dev/null || echo "") if [[ -z "$CVE_LINES" ]]; then @@ -107,7 +115,7 @@ FIX_SUMMARY="" FIXED_COUNT=0 REVIEW_COUNT=0 -while IFS=$'\t' read -r TYPE PKG FIX_VER CVE_CSV _SEVERITY; do +while IFS=$'\t' read -r TYPE PKG FIX_VER CVE_CSV _SEVERITY ALL_FIX_VERS; do [[ -z "$PKG" ]] && continue # Split CVE_CSV into array @@ -116,12 +124,13 @@ while IFS=$'\t' read -r TYPE PKG FIX_VER CVE_CSV _SEVERITY; do FIX_LOG=$(mktemp) if [[ "$TYPE" == "stdlib" ]]; then STDLIB_EXIT=0 - "$SCRIPT_DIR/fix-stdlib.sh" "$STATE_FILE" "$FIX_VER" "${CVES[@]}" 2>&1 | tee "$FIX_LOG" || STDLIB_EXIT=$? + "$SCRIPT_DIR/fix-stdlib.sh" "$STATE_FILE" "$FIX_VER" "$ALL_FIX_VERS" "${CVES[@]}" 2>&1 | tee "$FIX_LOG" || STDLIB_EXIT=$? if [[ "$STDLIB_EXIT" -eq 0 ]]; then FIX_SUMMARY+="FIXED: stdlib go $FIX_VER for ${CVES[*]}"$'\n' FIXED_COUNT=$((FIXED_COUNT + 1)) else - FIX_SUMMARY+="NEEDS_REVIEW: stdlib — fix-stdlib.sh exited $STDLIB_EXIT"$'\n' + REASON=$(grep "^NEEDS_REVIEW:" "$FIX_LOG" || echo "NEEDS_REVIEW: stdlib — fix-stdlib.sh exited $STDLIB_EXIT") + FIX_SUMMARY+="$REASON"$'\n' REVIEW_COUNT=$((REVIEW_COUNT + 1)) fi else @@ -149,7 +158,7 @@ done <<< "$CVE_LINES" # Verify echo "" echo "Running unit tests..." -if ! make unit; then +if ! env -u SHIPYARD_TAG make unit; then FIX_SUMMARY+="NEEDS_REVIEW: unit tests failed after applying fixes"$'\n' REVIEW_COUNT=$((REVIEW_COUNT + 1)) fi @@ -185,6 +194,9 @@ if [[ "$COMMIT_COUNT" -gt 0 ]]; then FORK_USER=$(git remote get-url "${FORK_REMOTE}" 2>/dev/null | sed -E 's#.*github.com[:/]+([^/]+)/.*#\1#') echo "PR command:" + if [[ -n "${WORKTREE_DIR:-}" ]]; then + echo "cd $WORKTREE_DIR && \\" + fi echo "git push $FORK_REMOTE $CURRENT_BRANCH && \\" echo "gh pr create \\" echo " --title \"Fix CVE${PLURAL} in ${BASE_BRANCH}\" \\" diff --git a/scripts/cve/fix-package.sh b/scripts/cve/fix-package.sh index 10c388bf9..25e29f886 100755 --- a/scripts/cve/fix-package.sh +++ b/scripts/cve/fix-package.sh @@ -36,39 +36,50 @@ while IFS= read -r GOMOD; do GO_VER=$(grep '^go ' "$GOMOD" 2>/dev/null | awk '{print $2}') [[ -n "$GO_VER" ]] && GO_BEFORE+="$GOMOD:$GO_VER " done < <(find_gomods) -# shellcheck disable=SC2046 # word splitting is intentional (multiple file args) -K8S_BEFORE=$(grep -h 'k8s.io/client-go' $(find_gomods) 2>/dev/null | grep -oP 'v0\.\K[0-9]+' | sort -un | tr '\n' ' ') +# Only check root go.mod for K8s version changes (submodule K8s deps don't affect shipped binaries) +K8S_BEFORE=$(grep -h 'k8s.io/client-go' go.mod 2>/dev/null | grep -oP 'v0\.\K[0-9]+' | sort -un | tr '\n' ' ') # Update in all go.mod files that contain this package while IFS= read -r GOMOD; do MODDIR=$(dirname "$GOMOD") if grep -qF "$PACKAGE" "$GOMOD" 2>/dev/null; then - echo "Updating $PACKAGE in $GOMOD..." - go -C "$MODDIR" get "${PACKAGE}@v${VERSION}" && go -C "$MODDIR" mod tidy + INSTALLED=$(grep -F "$PACKAGE" "$GOMOD" | grep -oP 'v\K[0-9]+\.[0-9]+\.[0-9]+' | head -1) + if [[ -n "$INSTALLED" ]] && version_gte "$INSTALLED" "$VERSION"; then + echo "NOTE: $GOMOD already has $PACKAGE v$INSTALLED (>= v$VERSION)" + else + echo "Updating $PACKAGE in $GOMOD..." + go -C "$MODDIR" get "${PACKAGE}@v${VERSION}" && go -C "$MODDIR" mod tidy + fi fi done < <(find_gomods) clean_gomod -# Check for breaking changes (Go or K8s minor version upgrade in any go.mod) +# Check for breaking changes (Go or K8s minor version upgrade) +# Go directive bumps are safe if the build image already satisfies them. +# K8s minor version changes are genuinely breaking (different API versions). +COMPILER_GO=$(echo "$SHIPYARD_GO_VERSION" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+' || echo "") BREAKING="" while IFS= read -r GOMOD; do GO_AFTER=$(grep '^go ' "$GOMOD" 2>/dev/null | awk '{print $2}') [[ -z "$GO_AFTER" ]] && continue - # Find the before version for this go.mod for PAIR in $GO_BEFORE; do if [[ "${PAIR%%:*}" == "$GOMOD" ]]; then GO_WAS="${PAIR#*:}" if [[ "$(echo "$GO_WAS" | cut -d. -f1-2)" != "$(echo "$GO_AFTER" | cut -d. -f1-2)" ]]; then - BREAKING="${BREAKING:+$BREAKING; }$GOMOD: Go $GO_WAS -> $GO_AFTER" + if [[ -n "$COMPILER_GO" ]] && \ + [[ "$(printf '%s\n' "$COMPILER_GO" "$GO_AFTER" | sort -V | tail -1)" == "$COMPILER_GO" ]]; then + echo "NOTE: $GOMOD Go $GO_WAS -> $GO_AFTER (safe: build image has Go $COMPILER_GO)" + else + BREAKING="${BREAKING:+$BREAKING; }$GOMOD: Go $GO_WAS -> $GO_AFTER" + fi fi break fi done done < <(find_gomods) -# shellcheck disable=SC2046 # word splitting is intentional (multiple file args) -K8S_AFTER=$(grep -h 'k8s.io/client-go' $(find_gomods) 2>/dev/null | grep -oP 'v0\.\K[0-9]+' | sort -un | tr '\n' ' ') +K8S_AFTER=$(grep -h 'k8s.io/client-go' go.mod 2>/dev/null | grep -oP 'v0\.\K[0-9]+' | sort -un | tr '\n' ' ') if [[ -n "$K8S_BEFORE" ]] && [[ -n "$K8S_AFTER" ]] && [[ "$K8S_BEFORE" != "$K8S_AFTER" ]]; then BREAKING="${BREAKING:+$BREAKING; }K8s minor versions changed" fi @@ -79,14 +90,17 @@ if [[ -n "$BREAKING" ]]; then exit 2 fi -# Verify fix: check that go.mod has the new version +# Verify fix: check that go.mod has version >= the fix version STILL_VULNERABLE=false while IFS= read -r GOMOD; do - if grep -q "${PACKAGE}.*v${VERSION}" "$GOMOD" 2>/dev/null; then - : # Updated to new version, good - elif grep -qF "$PACKAGE" "$GOMOD" 2>/dev/null; then - echo "WARNING: $GOMOD still has old version of $PACKAGE" - STILL_VULNERABLE=true + if grep -qF "$PACKAGE" "$GOMOD" 2>/dev/null; then + INSTALLED=$(grep -F "$PACKAGE" "$GOMOD" | grep -oP 'v\K[0-9]+\.[0-9]+\.[0-9]+' | head -1) + if [[ -n "$INSTALLED" ]] && version_gte "$INSTALLED" "$VERSION"; then + : # Installed version >= fix version, good + else + echo "WARNING: $GOMOD has $PACKAGE at v${INSTALLED:-unknown} (need >= v$VERSION)" + STILL_VULNERABLE=true + fi fi done < <(find_gomods) @@ -109,25 +123,38 @@ if [[ -n "$GENERATED_FILE" ]] && [[ -n "$DIFF_IGNORE_ARGS" ]]; then fi fi -# Determine if tools-only change (all staged go files under tools/) -TOOLS_ONLY="" -if ! git diff --staged --name-only | grep -qE '^go\.(mod|sum)$' && \ - git diff --staged --name-only | grep -qE '^tools/'; then - TOOLS_ONLY=" in /tools" -fi +# If nothing to commit, the CVE was already fixed by a prior package upgrade +if ! git diff --staged --quiet 2>/dev/null; then + # Determine if tools-only change (all staged go files under tools/) + TOOLS_ONLY="" + if ! git diff --staged --name-only | grep -qE '^go\.(mod|sum)$' && \ + git diff --staged --name-only | grep -qE '^tools/'; then + TOOLS_ONLY=" in /tools" + fi -# Format commit message -ABBREV=$(abbreviate_package "$PACKAGE") -if [[ ${#CVE_IDS[@]} -eq 1 ]]; then - SUBJECT="Bump ${ABBREV} for ${CVE_IDS[0]}${TOOLS_ONLY}" - BODY="Full package: $PACKAGE" + # Format commit message + ABBREV=$(abbreviate_package "$PACKAGE") + if [[ ${#CVE_IDS[@]} -eq 1 ]]; then + SUBJECT="Bump ${ABBREV} for ${CVE_IDS[0]}${TOOLS_ONLY}" + BODY="Full package: $PACKAGE" + else + SUBJECT="Bump ${ABBREV} for CVEs${TOOLS_ONLY}" + FIXES_LINE="Fixes: $(printf '%s, ' "${CVE_IDS[@]}")" + FIXES_LINE="${FIXES_LINE%, }" + if [[ ${#FIXES_LINE} -gt 80 ]]; then + FIXES=$(printf ' %s\n' "${CVE_IDS[@]}") + BODY="Full package: $PACKAGE +Fixes: +$FIXES" + else + BODY="Full package: $PACKAGE +$FIXES_LINE" + fi + fi + + git commit -s -m "$(printf '%s\n\n%s' "$SUBJECT" "$BODY")" else - SUBJECT="Bump ${ABBREV} for CVEs${TOOLS_ONLY}" - FIXES=$(printf '%s, ' "${CVE_IDS[@]}") - BODY="Full package: $PACKAGE -Fixes: ${FIXES%, }" + echo "NOTE: $PACKAGE already at v$VERSION (fixed by prior upgrade)" fi -git commit -s -m "$(printf '%s\n\n%s' "$SUBJECT" "$BODY")" - echo "FIXED: $PACKAGE v$VERSION for ${CVE_IDS[*]}" diff --git a/scripts/cve/fix-stdlib.sh b/scripts/cve/fix-stdlib.sh index d5c515968..4ccf276a9 100755 --- a/scripts/cve/fix-stdlib.sh +++ b/scripts/cve/fix-stdlib.sh @@ -1,6 +1,6 @@ #!/bin/bash # Fix stdlib CVEs by updating the go directive. -# Usage: fix-stdlib.sh STATE_FILE GO_VERSION CVE_ID [CVE_ID...] +# Usage: fix-stdlib.sh STATE_FILE GO_VERSION ALL_FIX_VERSIONS CVE_ID [CVE_ID...] set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -8,9 +8,10 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=lib.sh source "$SCRIPT_DIR/lib.sh" -STATE_FILE="${1:?Usage: fix-stdlib.sh STATE_FILE GO_VERSION CVE_ID [CVE_ID...]}" +STATE_FILE="${1:?Usage: fix-stdlib.sh STATE_FILE GO_VERSION ALL_FIX_VERSIONS CVE_ID [CVE_ID...]}" GO_VERSION="${2:?Missing GO_VERSION}" -shift 2 +ALL_FIX_VERSIONS="${3:-}" +shift 3 CVE_IDS=("$@") [[ ${#CVE_IDS[@]} -gt 0 ]] || { echo "ERROR: At least one CVE_ID required" >&2; exit 1; } @@ -22,14 +23,48 @@ echo "--- Fixing stdlib: go $GO_VERSION for ${CVE_IDS[*]} ---" OLD_GO=$(grep '^go ' go.mod | awk '{print $2}') -# Check for Go minor version upgrade (breaking on stable branches) -OLD_MINOR=$(echo "$OLD_GO" | cut -d. -f1-2) +# Use build image compiler version if known, else fall back to go.mod +COMPILER_GO=$(echo "$SHIPYARD_GO_VERSION" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+' || echo "") +REF_GO="${COMPILER_GO:-$OLD_GO}" +REF_MINOR=$(echo "$REF_GO" | cut -d. -f1-2) + +# Pick fix version matching the compiler's minor stream +if [[ -n "$COMPILER_GO" ]] && [[ -n "$ALL_FIX_VERSIONS" ]]; then + IFS=',' read -ra FIX_VERS <<< "$ALL_FIX_VERSIONS" + for v in "${FIX_VERS[@]}"; do + if [[ "$(echo "$v" | cut -d. -f1-2)" == "$REF_MINOR" ]]; then + GO_VERSION="$v" + break + fi + done +fi + NEW_MINOR=$(echo "$GO_VERSION" | cut -d. -f1-2) -if [[ "$OLD_MINOR" != "$NEW_MINOR" ]]; then - echo "NEEDS_REVIEW: stdlib — would upgrade Go $OLD_GO -> $GO_VERSION (minor version change)" + +# Check if fix is in the compiler's minor stream +if [[ "$REF_MINOR" != "$NEW_MINOR" ]]; then + if [[ -n "$COMPILER_GO" ]]; then + echo "NEEDS_REVIEW: stdlib — no fix in compiler stream $REF_MINOR (build image Go $COMPILER_GO, fix needs $GO_VERSION)" + else + echo "NEEDS_REVIEW: stdlib — would upgrade Go $OLD_GO -> $GO_VERSION (minor version change)" + fi exit 2 fi +# Patch bump in the right stream — but can the build image support it? +if [[ -n "$COMPILER_GO" ]]; then + if [[ "$(printf '%s\n' "$COMPILER_GO" "$GO_VERSION" | sort -V | head -1)" == "$COMPILER_GO" ]] && \ + [[ "$COMPILER_GO" != "$GO_VERSION" ]]; then + echo "NEEDS_REVIEW: stdlib — build image has Go $COMPILER_GO, fix needs $GO_VERSION (patch bump, rebuild shipyard-dapper-base)" + exit 2 + fi +fi + +# Compiler already has the fix version — bump go.mod to match +if [[ -z "${GOTOOLCHAIN:-}" ]]; then + export GOTOOLCHAIN=auto +fi + # Check host Go version is sufficient HOST_GO=$(go version | grep -oP 'go\K[0-9]+\.[0-9]+\.[0-9]+') if [[ -n "$HOST_GO" ]] && [[ "${GOTOOLCHAIN:-}" != *auto* ]] && \ diff --git a/scripts/cve/ignore.sh b/scripts/cve/ignore.sh index 9cbce4d9a..99a87fc5b 100755 --- a/scripts/cve/ignore.sh +++ b/scripts/cve/ignore.sh @@ -1,6 +1,8 @@ #!/bin/bash # Add CVE(s) to the .grype.yaml ignore list and commit. -# Usage: ignore.sh STATE_FILE PACKAGE SEVERITY REASON CVE_ID [CVE_ID...] +# Use --no-fix when ignoring because no fix is available; the entry will +# auto-expire once grype's vulnerability DB shows a fix exists. +# Usage: ignore.sh STATE_FILE PACKAGE SEVERITY REASON [--no-fix] CVE_ID [CVE_ID...] set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -8,11 +10,18 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=lib.sh source "$SCRIPT_DIR/lib.sh" -STATE_FILE="${1:?Usage: ignore.sh STATE_FILE PACKAGE SEVERITY REASON CVE_ID [CVE_ID...]}" +STATE_FILE="${1:?Usage: ignore.sh STATE_FILE PACKAGE SEVERITY REASON [--no-fix] CVE_ID [CVE_ID...]}" PACKAGE="${2:?Missing PACKAGE}" SEVERITY="${3:?Missing SEVERITY}" REASON="${4:?Missing REASON}" shift 4 + +FIX_STATE="" +if [[ "${1:-}" == "--no-fix" ]]; then + FIX_STATE="not-fixed" + shift +fi + CVE_IDS=("$@") [[ ${#CVE_IDS[@]} -gt 0 ]] || { echo "ERROR: At least one CVE_ID required" >&2; exit 1; } @@ -21,7 +30,7 @@ load_state "$STATE_FILE" echo "--- Ignoring: ${CVE_IDS[*]} ($PACKAGE) [$SEVERITY] ---" for CVE_ID in "${CVE_IDS[@]}"; do - insert_grype_ignore .grype.yaml "$CVE_ID" "$PACKAGE" "$REASON" + insert_grype_ignore .grype.yaml "$CVE_ID" "$PACKAGE" "$REASON" "$FIX_STATE" done git add .grype.yaml diff --git a/scripts/cve/lib.sh b/scripts/cve/lib.sh index 528ef819f..2110d0fdb 100644 --- a/scripts/cve/lib.sh +++ b/scripts/cve/lib.sh @@ -87,6 +87,11 @@ run_grype() { return 1 } +# Check if version A >= version B (semver, uses sort -V) +version_gte() { + [[ "$(printf '%s\n' "$1" "$2" | sort -V | head -1)" == "$2" ]] +} + # Abbreviate Go package path for commit messages # github.com/docker/docker -> docker/docker # golang.org/x/net -> x/net @@ -131,12 +136,16 @@ clean_gomod() { # Insert an ignore entry into a .grype.yaml file. # Inserts before exclude: section if present, otherwise appends. -# Usage: insert_grype_ignore FILE CVE_ID PACKAGE REASON +# When FIX_STATE is provided (e.g. "not-fixed"), the entry auto-expires +# once grype's DB shows a fix is available. +# Usage: insert_grype_ignore FILE CVE_ID PACKAGE REASON [FIX_STATE] insert_grype_ignore() { - local file="$1" cve_id="$2" package="$3" reason="$4" + local file="$1" cve_id="$2" package="$3" reason="$4" fix_state="${5:-}" local entry entry=" # $reason - - vulnerability: $cve_id + - vulnerability: $cve_id" + [[ -n "$fix_state" ]] && entry+=$'\n'" fix-state: $fix_state" + entry+=" package: name: $package" diff --git a/scripts/cve/review-prompt.md b/scripts/cve/review-prompt.md index 865a52aa8..006cb9cf3 100644 --- a/scripts/cve/review-prompt.md +++ b/scripts/cve/review-prompt.md @@ -28,10 +28,16 @@ Review ALL CVE outcomes: 2. **Handle unfixed CVEs**: For each CVE that was not fixed: - Try a different approach if possible (different version, drop replace directive) - - If fix would break the branch: add to .grype.yaml ignore list. - **Batch all CVEs for the same package into one ignore.sh call** (it accepts multiple CVE IDs). + - If **no fix is available**: use `--no-fix` so the entry auto-expires when a fix is published. + Run: `bash ${CVE_SCRIPTS}/ignore.sh ${STATE_FILE} PACKAGE SEVERITY "reason" --no-fix CVE_ID [CVE_ID...]` + - If fix requires a **Go or K8s minor version upgrade** on a stable branch: report as **UNRESOLVED**. + Do NOT ignore — this needs team approval. + **Exception**: Go directive bumps that are already committed were validated by the deterministic phase + against the build image (Go ${SHIPYARD_GO_VERSION}). Do NOT roll back or flag these. + - If fix exists but would break API compatibility (not a version upgrade): omit `--no-fix` (permanent ignore). Run: `bash ${CVE_SCRIPTS}/ignore.sh ${STATE_FILE} PACKAGE SEVERITY "reason" CVE_ID [CVE_ID...]` - - Note anything that needs team discussion + - **One ignore.sh call per package.** Combine ALL CVEs regardless of severity. + Use the highest severity for the SEVERITY arg (it is only a log label). 3. **Check for regressions**: Did any fix introduce new CVEs? @@ -41,11 +47,12 @@ ONLY use these scripts. Do NOT run go/git/sed commands directly. - `bash ${CVE_SCRIPTS}/fix-package.sh ${STATE_FILE} PACKAGE VERSION CVE_IDS...` - `bash ${CVE_SCRIPTS}/fix-stdlib.sh ${STATE_FILE} GO_VERSION CVE_IDS...` -- `bash ${CVE_SCRIPTS}/ignore.sh ${STATE_FILE} PACKAGE SEVERITY "reason" CVE_ID [CVE_ID...]` +- `bash ${CVE_SCRIPTS}/ignore.sh ${STATE_FILE} PACKAGE SEVERITY "reason" [--no-fix] CVE_ID [CVE_ID...]` - `bash ${CVE_SCRIPTS}/scan.sh ${STATE_FILE}` If a script exits with NEEDS_REVIEW, do NOT attempt the same fix manually. -Use ignore.sh to document it, or report it as UNRESOLVED. +If the reason indicates a policy decision (version upgrade), report as UNRESOLVED. +Otherwise use ignore.sh. ## Output diff --git a/scripts/cve/scan.sh b/scripts/cve/scan.sh index d095d0f0f..faa65d556 100755 --- a/scripts/cve/scan.sh +++ b/scripts/cve/scan.sh @@ -24,8 +24,11 @@ for arg in "$@"; do done # Build if needed (e.g., submariner repo with UPX compression for stdlib CVE detection) +# Unset SHIPYARD_TAG so the repo's own BASE_BRANCH controls which build image is used. +# Without this, shipyard's exported SHIPYARD_TAG=devel leaks into the child make and +# overrides the repo's ?= assignment, causing builds with the wrong Go version. if [[ "$NEEDS_BUILD_FOR_SCAN" == "true" ]] && [[ "$SKIP_BUILD" != "true" ]]; then - if ! make BUILD_UPX=false build >&2; then + if ! env -u SHIPYARD_TAG make BUILD_UPX=false build >&2; then echo "WARNING: Build failed. Scanning source only (may miss stdlib CVEs in binaries)." >&2 echo "VPN can cause transient Docker DNS failures. Retry, or: sudo systemctl restart docker" >&2 fi diff --git a/scripts/cve/test-lib.sh b/scripts/cve/test-lib.sh index 8f267d84e..f4951a377 100755 --- a/scripts/cve/test-lib.sh +++ b/scripts/cve/test-lib.sh @@ -45,6 +45,10 @@ check "abbrev grpc" test "$(abbreviate_package google.golang.org/grpc)" = "grpc" check "abbrev sigs" test "$(abbreviate_package sigs.k8s.io/controller-runtime)" = "controller-runtime" CVE_LIST=("GHSA-aaaa" "GHSA-bbbb"); JOINED=$(printf '%s, ' "${CVE_LIST[@]}"); JOINED="${JOINED%, }" check "CVE join" test "$JOINED" = "GHSA-aaaa, GHSA-bbbb" +check "gte higher" version_gte 0.45.0 0.44.0 +check "gte equal" version_gte 0.44.0 0.44.0 +check "gte lower" ! version_gte 0.41.0 0.44.0 +check "gte major" version_gte 28.0.0 0.44.0 # === clean_gomod === echo "clean_gomod"