diff --git a/.agents/commit-templates.md b/.agents/commit-templates.md index 9ac127b1a..aa1ffddd0 100644 --- a/.agents/commit-templates.md +++ b/.agents/commit-templates.md @@ -36,6 +36,10 @@ Fixes: , - `github.com/docker/docker` → `docker/docker` - `golang.org/x/oauth2` → `x/oauth2` - `helm.sh/helm/v3` → `helm/v3` +- `go.opentelemetry.io/otel` → `otel` +- `go.opentelemetry.io/otel/exporters/otlp/*/X` → `otel/X` +- `google.golang.org/grpc` → `grpc` +- `sigs.k8s.io/controller-runtime` → `controller-runtime` - Keep `k8s.io/` prefix **Examples:** diff --git a/.agents/workflows/cve-fix.md b/.agents/workflows/cve-fix.md index 0edacac43..f8a35cb83 100644 --- a/.agents/workflows/cve-fix.md +++ b/.agents/workflows/cve-fix.md @@ -1,5 +1,16 @@ #### Fixing CVEs +**Automated:** Use `/cve-fix` in Claude Code or `make cve-fix` from shipyard: + +```bash +/cve-fix release-0.23 ../submariner-operator +make cve-fix REPO=../submariner BRANCH=release-0.23 +``` + +The steps below are the manual process for reference. + +--- + **All commands should be run from the repository root directory.** **Before starting, user must:** diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 8f27f9010..e1799e2a5 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -103,6 +103,9 @@ jobs: - name: Test the compile.sh script run: make script-test SCRIPT_TEST_ARGS="test/scripts/compile/test.sh" + - name: Test the CVE fix scripts + run: make script-test SCRIPT_TEST_ARGS="test/scripts/cve/test.sh" + deployment: name: Deployment needs: images diff --git a/Makefile b/Makefile index d9763d970..10cad39ed 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ LOCAL_COMPONENTS := submariner-metrics-proxy MULTIARCH_IMAGES ?= $(IMAGES) EXTRA_PRELOAD_IMAGES := $(PRELOAD_IMAGES) PLATFORMS ?= linux/amd64,linux/arm64 -NON_DAPPER_GOALS += images multiarch-images +NON_DAPPER_GOALS += images multiarch-images cve-fix cve-clean PLUGIN ?= export LOCAL_COMPONENTS @@ -65,6 +65,13 @@ deploy deploy-latest e2e upgrade-e2e: package/.image.nettest include Makefile.dapper +# CVE fix scripts +cve-fix: + ./scripts/cve/fix-all.sh "$(or $(REPO),.)" "$(or $(BRANCH),$(shell git branch --show-current))" + +cve-clean: + ./scripts/cve/clean.sh + # Make sure linting goals have up-to-date linting image $(LINTING_GOALS): package/.image.shipyard-linting diff --git a/scripts/cve/clean.sh b/scripts/cve/clean.sh new file mode 100755 index 000000000..8a8a4076f --- /dev/null +++ b/scripts/cve/clean.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# Kill orphaned CVE fix processes and containers. +# Safe to run anytime — only targets fix-all.sh, grype, and dapper fix containers. +pkill -f fix-all.sh 2>/dev/null || true +docker ps --format '{{.ID}} {{.Image}}' 2>/dev/null | grep -E 'grype:latest|fix-.*-cves-' | awk '{print $1}' | xargs -r docker kill 2>/dev/null || true diff --git a/scripts/cve/detect.sh b/scripts/cve/detect.sh new file mode 100755 index 000000000..094d56843 --- /dev/null +++ b/scripts/cve/detect.sh @@ -0,0 +1,162 @@ +#!/bin/bash +# Detect repository configuration for CVE fixing and optionally set up fix branch. +# Usage: detect.sh [repo] [branch] [--setup-branch] +# Prints state file path as last line of stdout. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source-path=SCRIPTDIR +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + +# --- Argument parsing (order-independent) --- +REPO="" +BRANCH="" +SETUP_BRANCH=false + +for arg in "$@"; do + case "$arg" in + --setup-branch) SETUP_BRANCH=true ;; + *) + # Expand tilde + arg="${arg/#\~/$HOME}" + # Try sibling directory for bare names (e.g., "subctl" -> "../subctl") + if [[ "$arg" != */* ]] && [[ -d "../$arg" ]]; then + arg="../$arg" + fi + if [[ "$arg" == /* ]] || [[ "$arg" == ./* ]] || [[ "$arg" == ../* ]] || [[ -d "$arg" ]]; then + [[ -n "$REPO" ]] && { echo "ERROR: Multiple repositories specified" >&2; exit 1; } + REPO="$arg" + else + [[ -n "$BRANCH" ]] && { echo "ERROR: Multiple branches specified" >&2; exit 1; } + BRANCH="$arg" + fi + ;; + esac +done + +REPO="${REPO:-.}" + +# --- Validate repo --- +[[ -d "$REPO" ]] || { echo "ERROR: Repository not found: $REPO" >&2; exit 1; } +git -C "$REPO" rev-parse --git-dir &>/dev/null || { echo "ERROR: Not a git repository: $REPO" >&2; exit 1; } + +# --- Resolve branch --- +if [[ -z "$BRANCH" ]]; then + BRANCH=$(git -C "$REPO" branch --show-current 2>/dev/null) + [[ -n "$BRANCH" ]] || { echo "ERROR: Not on a branch (detached HEAD). Specify branch explicitly." >&2; exit 1; } + echo "Using current branch: $BRANCH" +fi + +# Normalize short version (0.23 -> release-0.23) +if [[ "$BRANCH" =~ ^[0-9]+\.[0-9]+$ ]]; then + BRANCH="release-${BRANCH}" + echo "Normalized to branch: $BRANCH" +fi + +# --- Change to repo (absolute path) --- +REPO="$(cd "$REPO" && pwd)" +cd "$REPO" + +REPO_NAME=$(basename "$REPO") +echo "" +echo "=== CVE Fix: $REPO_NAME/$BRANCH ===" + +# --- Detect config --- +HAS_TOOLS_GOMOD=false +test -f tools/go.mod && HAS_TOOLS_GOMOD=true + +GENERATED_FILE="" +DIFF_IGNORE_ARGS="" +if grep -rql "controller-gen.kubebuilder.io/version" --include="*.go" . 2>/dev/null; then + DIFF_IGNORE_ARGS="-Icontroller-gen.kubebuilder.io/version" + GENERATED_FILE=$(grep -rl "controller-gen.kubebuilder.io/version" --include="*.go" . 2>/dev/null | head -1) +elif find . -name "*.pb.go" -type f 2>/dev/null | head -1 | grep -q .; then + DIFF_IGNORE_ARGS="-I^//" + GENERATED_FILE=$(find . -name "*.pb.go" -type f 2>/dev/null | head -1) +fi + +NEEDS_BUILD_FOR_SCAN=false +grep -q '^build:' Makefile 2>/dev/null && NEEDS_BUILD_FOR_SCAN=true + +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" +fi + +SHIPYARD_IMAGE="quay.io/submariner/shipyard-dapper-base:${SHIPYARD_TAG}" +SHIPYARD_GO_VERSION="unknown" +if [[ -n "$CONTAINER_CMD" ]]; then + if $CONTAINER_CMD pull "$SHIPYARD_IMAGE" >/dev/null 2>&1; then + SHIPYARD_GO_VERSION=$($CONTAINER_CMD run --rm "$SHIPYARD_IMAGE" go version 2>/dev/null || echo "unknown") + fi +fi + +# --- Branch setup (optional) --- +ORIGINAL_REF="" +FIX_BRANCH="" +FETCH_FAILED=false + +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 + fi + + ORIGINAL_REF=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) + [[ "$ORIGINAL_REF" == "HEAD" ]] && ORIGINAL_REF=$(git rev-parse HEAD) + + if ! GIT_SSH_COMMAND="ssh -o BatchMode=yes" git fetch 2>/dev/null; then + echo "WARNING: git fetch failed. Continuing with cached remote state." + FETCH_FAILED=true + fi + + DATE=$(date +%Y-%m-%d) + VERSION="${BRANCH//release-/}" + FIX_BRANCH="fix-${VERSION}-cves-${DATE}" + + SUFFIX="" + while git show-ref --verify --quiet "refs/heads/${FIX_BRANCH}${SUFFIX}"; do + if [[ -z "$SUFFIX" ]]; then SUFFIX="-v2"; else NUM=${SUFFIX#-v}; SUFFIX="-v$((NUM+1))"; fi + 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 + fi + echo "Fix branch: $FIX_BRANCH" +fi + +# --- Write state file --- +STATE_FILE=$(state_file_path "$REPO" "$BRANCH") +cat > "$STATE_FILE" </dev/null || true + # Dapper containers run in their own namespace and survive process group kill + if [[ -n "${FIX_BRANCH:-}" ]]; then + docker ps --format '{{.ID}} {{.Image}}' 2>/dev/null | grep ":${FIX_BRANCH}" | awk '{print $1}' | xargs -r docker kill 2>/dev/null || true + fi + git checkout -- . 2>/dev/null || true + rm -f "${STATE_FILE:-}" +} +trap cleanup EXIT +trap 'exit 1' INT TERM + +# --- Detect and Scan --- + +# detect.sh prints state file path as last line (and its own banner) +STATE_FILE=$("$SCRIPT_DIR/detect.sh" "$@" --setup-branch | tee /dev/stderr | tail -1) +# shellcheck source=/dev/null +source "$STATE_FILE" +cd "$REPO" + +echo "" +echo "Cleaning build artifacts..." +make clean >/dev/null 2>&1 || true + +echo "Scanning for CVEs..." +SCAN_JSON=$("$SCRIPT_DIR/scan.sh" "$STATE_FILE" --fresh --json || true) + +if ! MATCH_COUNT=$(printf '%s\n' "$SCAN_JSON" | jq '.matches | length' 2>/dev/null) || [[ -z "$MATCH_COUNT" ]]; then + echo "ERROR: Scan failed or produced invalid output." >&2 + echo "Check scanner availability (grype or docker/podman)." >&2 + exit 1 +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 + fi + exit 0 +fi + +echo "Found $MATCH_COUNT CVE(s) in $(basename "$REPO")/$BRANCH." + +# Show summary table from JSON +printf '%s\n' "$SCAN_JSON" | jq -r ' + ["NAME","INSTALLED","FIXED-IN","VULNERABILITY","SEVERITY"], + (.matches[] | [.artifact.name, .artifact.version, (.vulnerability.fix.versions[0] // ""), .vulnerability.id, .vulnerability.severity]) | + @tsv' 2>/dev/null | column -t || true + +# --- Parse CVEs, group by package, pick highest fix version --- +banner "Fixing CVEs" + +# Extract unique package+fixVersion+cveIDs groups from JSON +# jq outputs: PACKAGE TAB FIXED_IN TAB CVE_ID (one line per match) +CVE_LINES=$(printf '%s\n' "$SCAN_JSON" | jq -r ' + [.matches[] | + select(.vulnerability.fix.versions != null and (.vulnerability.fix.versions | length) > 0) | + { + pkg: .artifact.name, + fixedIn: .vulnerability.fix.versions[0], + cve: .vulnerability.id, + severity: .vulnerability.severity, + type: (if .artifact.name == "stdlib" then "stdlib" else "package" end) + } + ] | + group_by(.pkg) | + map({ + pkg: .[0].pkg, + type: .[0].type, + fixedIn: (map(.fixedIn) | sort_by(split(".") | map(tonumber)) | last), + cves: map(.cve), + severity: .[0].severity + }) | + .[] | + "\(.type)\t\(.pkg)\t\(.fixedIn)\t\(.cves | join(","))\t\(.severity)" +' 2>/dev/null || echo "") + +if [[ -z "$CVE_LINES" ]]; then + echo "WARNING: Could not parse CVE data from JSON. All matches may lack fix versions." + echo "Proceeding to agent review." + CVE_LINES="" +fi + +# Track results +FIX_SUMMARY="" +FIXED_COUNT=0 +REVIEW_COUNT=0 + +while IFS=$'\t' read -r TYPE PKG FIX_VER CVE_CSV _SEVERITY; do + [[ -z "$PKG" ]] && continue + + # Split CVE_CSV into array + IFS=',' read -ra CVES <<< "$CVE_CSV" + + 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=$? + 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' + REVIEW_COUNT=$((REVIEW_COUNT + 1)) + fi + else + EXIT_CODE=0 + "$SCRIPT_DIR/fix-package.sh" "$STATE_FILE" "$PKG" "$FIX_VER" "${CVES[@]}" 2>&1 | tee "$FIX_LOG" || EXIT_CODE=$? + case $EXIT_CODE in + 0) + FIX_SUMMARY+="FIXED: $PKG v$FIX_VER for ${CVES[*]}"$'\n' + FIXED_COUNT=$((FIXED_COUNT + 1)) + ;; + 2|3) + REASON=$(grep "^NEEDS_REVIEW:" "$FIX_LOG" || echo "NEEDS_REVIEW: $PKG — exit code $EXIT_CODE") + FIX_SUMMARY+="$REASON"$'\n' + REVIEW_COUNT=$((REVIEW_COUNT + 1)) + ;; + *) + FIX_SUMMARY+="ERROR: $PKG — fix-package.sh failed with exit code $EXIT_CODE"$'\n' + REVIEW_COUNT=$((REVIEW_COUNT + 1)) + ;; + esac + fi + rm -f "$FIX_LOG" +done <<< "$CVE_LINES" + +# Verify +echo "" +echo "Running unit tests..." +if ! make unit; then + FIX_SUMMARY+="NEEDS_REVIEW: unit tests failed after applying fixes"$'\n' + REVIEW_COUNT=$((REVIEW_COUNT + 1)) +fi +echo "Final scan..." +FINAL_SCAN=$("$SCRIPT_DIR/scan.sh" "$STATE_FILE" 2>&1) || true +printf '%s\n' "$FINAL_SCAN" + +# Agent review (pass scan output to avoid re-scanning) +banner "Agent Review" +"$SCRIPT_DIR/review.sh" "$STATE_FILE" "$FIX_SUMMARY" "$FINAL_SCAN" || true + +# --- Summary --- + +banner "Summary: $(basename "$REPO")/$BRANCH" + +# shellcheck source=/dev/null +source "$STATE_FILE" + +echo "Fixed: $FIXED_COUNT, Needs review: $REVIEW_COUNT" +printf '%s' "$FIX_SUMMARY" + +COMMIT_COUNT=$(git --no-pager log "origin/$BRANCH"..HEAD --oneline 2>/dev/null | wc -l) +if [[ "$COMMIT_COUNT" -gt 0 ]]; then + echo "Commits:" + git --no-pager log "origin/$BRANCH"..HEAD --oneline + + # Generate PR command + echo "" + CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) + BASE_BRANCH=$(echo "$CURRENT_BRANCH" | sed 's/fix-\([0-9.]*\)-.*/release-\1/; s/fix-devel-.*/devel/') + PLURAL=$([[ "$COMMIT_COUNT" -eq 1 ]] && echo "" || echo "s") + FORK_REMOTE=$(git remote -v | awk '!/submariner-io/ && /\(push\)/ { print $1; exit }') + FORK_USER=$(git remote get-url "${FORK_REMOTE}" 2>/dev/null | sed -E 's#.*github.com[:/]+([^/]+)/.*#\1#') + + echo "PR command:" + echo "git push $FORK_REMOTE $CURRENT_BRANCH && \\" + echo "gh pr create \\" + echo " --title \"Fix CVE${PLURAL} in ${BASE_BRANCH}\" \\" + echo " --body \"See commit message${PLURAL} for details.\" \\" + echo " --base \"${BASE_BRANCH}\" \\" + echo " --head \"${FORK_USER}:${CURRENT_BRANCH}\" \\" + echo " --assignee \"@me\"" +fi + +if [[ "$FETCH_FAILED" == "true" ]]; then + echo "" + echo "WARNING: git fetch failed earlier. Branch was created from cached remote state." + echo "Run 'git fetch' and re-run if it fetches new commits." +fi + +# Exit code based on results +if [[ "$REVIEW_COUNT" -gt 0 ]]; then + exit 2 +fi +exit 0 diff --git a/scripts/cve/fix-package.sh b/scripts/cve/fix-package.sh new file mode 100755 index 000000000..dcd926281 --- /dev/null +++ b/scripts/cve/fix-package.sh @@ -0,0 +1,136 @@ +#!/bin/bash +# Fix a single CVE package: update, verify, commit. +# Usage: fix-package.sh STATE_FILE PACKAGE VERSION CVE_ID [CVE_ID...] +# Exit 0: fixed. Exit 2: needs review (breaking change). Exit 3: CVE persists. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source-path=SCRIPTDIR +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + +STATE_FILE="${1:?Usage: fix-package.sh STATE_FILE PACKAGE VERSION CVE_ID [CVE_ID...]}" +PACKAGE="${2:?Missing PACKAGE}" +VERSION="${3:?Missing VERSION}" +shift 3 +CVE_IDS=("$@") +[[ ${#CVE_IDS[@]} -gt 0 ]] || { echo "ERROR: At least one CVE_ID required" >&2; exit 1; } + +load_state "$STATE_FILE" + +trap 'git reset --quiet HEAD -- . 2>/dev/null || true; git checkout -- . 2>/dev/null || true' ERR + +echo "--- Fixing: $PACKAGE -> v$VERSION for ${CVE_IDS[*]} ---" + +# Check for replace directives across all go.mod files (warn, don't block) +while IFS= read -r GOMOD; do + if grep -q "replace.*${PACKAGE}" "$GOMOD" 2>/dev/null; then + echo "WARNING: Replace directive found in $GOMOD for $PACKAGE" + grep "replace.*${PACKAGE}" "$GOMOD" 2>/dev/null || true + fi +done < <(find_gomods) + +# Snapshot go directives before update (for breaking-change detection) +GO_BEFORE="" +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' ' ') + +# Update in all go.mod files that contain this package +while IFS= read -r GOMOD; do + MODDIR=$(dirname "$GOMOD") + if grep -q "$PACKAGE" "$GOMOD" 2>/dev/null; then + if [[ "$MODDIR" == "." ]]; then + go get "${PACKAGE}@v${VERSION}" && go mod tidy + else + 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) +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" + 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' ' ') +if [[ -n "$K8S_BEFORE" ]] && [[ -n "$K8S_AFTER" ]] && [[ "$K8S_BEFORE" != "$K8S_AFTER" ]]; then + BREAKING="${BREAKING:+$BREAKING; }K8s minor versions changed" +fi + +if [[ -n "$BREAKING" ]]; then + echo "NEEDS_REVIEW: $PACKAGE — would upgrade $BREAKING" + git checkout -- . || echo "ERROR: Could not rollback changes" >&2 + exit 2 +fi + +# Verify fix: check that go.mod has the new 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 -q "$PACKAGE" "$GOMOD" 2>/dev/null; then + echo "WARNING: $GOMOD still has old version of $PACKAGE" + STILL_VULNERABLE=true + fi +done < <(find_gomods) + +if [[ "$STILL_VULNERABLE" == "true" ]]; then + echo "NEEDS_REVIEW: $PACKAGE — CVE persists after update to v$VERSION" + git checkout -- . || echo "ERROR: Could not rollback changes" >&2 + exit 3 +fi + +# Stage all changed go.mod/go.sum files (some repos have extra modules like coredns/) +git diff --name-only | grep -E 'go\.(mod|sum)$' | xargs -r git add || true + +# Handle generated files +if [[ -n "$GENERATED_FILE" ]] && [[ -n "$DIFF_IGNORE_ARGS" ]]; then + # shellcheck disable=SC2086 # DIFF_IGNORE_ARGS needs word splitting (-I'pattern') + if git diff $DIFF_IGNORE_ARGS "$GENERATED_FILE" 2>/dev/null | grep -q .; then + git add "$GENERATED_FILE" + else + git checkout "$GENERATED_FILE" 2>/dev/null || true + 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 + +# 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=$(printf '%s, ' "${CVE_IDS[@]}") + BODY="Full package: $PACKAGE +Fixes: ${FIXES%, }" +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 new file mode 100755 index 000000000..02adb09a0 --- /dev/null +++ b/scripts/cve/fix-stdlib.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# Fix stdlib CVEs by updating the go directive. +# Usage: fix-stdlib.sh STATE_FILE GO_VERSION CVE_ID [CVE_ID...] +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source-path=SCRIPTDIR +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + +STATE_FILE="${1:?Usage: fix-stdlib.sh STATE_FILE GO_VERSION CVE_ID [CVE_ID...]}" +GO_VERSION="${2:?Missing GO_VERSION}" +shift 2 +CVE_IDS=("$@") +[[ ${#CVE_IDS[@]} -gt 0 ]] || { echo "ERROR: At least one CVE_ID required" >&2; exit 1; } + +load_state "$STATE_FILE" + +trap 'git reset --quiet HEAD -- . 2>/dev/null || true; git checkout -- . 2>/dev/null || true' ERR + +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) +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)" + exit 2 +fi + +# Update go directive in all go.mod files +while IFS= read -r GOMOD; do + MODDIR=$(dirname "$GOMOD") + if [[ "$MODDIR" == "." ]]; then + go mod edit -go="${GO_VERSION}" + go mod tidy + else + go -C "$MODDIR" mod edit -go="${GO_VERSION}" + go -C "$MODDIR" mod tidy + fi +done < <(find_gomods) + +clean_gomod + +# Check if Shipyard build image has sufficient Go version +if [[ "$SHIPYARD_GO_VERSION" != "unknown" ]]; then + SHIPYARD_GO=$(echo "$SHIPYARD_GO_VERSION" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+' || echo "") + if [[ -n "$SHIPYARD_GO" ]]; then + OLDEST=$(printf '%s\n' "$SHIPYARD_GO" "$GO_VERSION" | sort -V | head -1) + if [[ "$OLDEST" == "$SHIPYARD_GO" ]] && [[ "$SHIPYARD_GO" != "$GO_VERSION" ]]; then + echo "NOTE: Shipyard build image has Go $SHIPYARD_GO but go.mod now requires $GO_VERSION" + echo "CI may fail until Shipyard is updated with a newer Go version." + fi + fi +fi + +# Stage all changed go.mod/go.sum files +git diff --name-only | grep -E 'go\.(mod|sum)$' | xargs -r git add || true + +FIXES=$(printf '%s, ' "${CVE_IDS[@]}") +FIXES="${FIXES%, }" +git commit -s -m "$(cat <&2 + echo "Run detect.sh first." >&2 + return 1 + fi + # shellcheck source=/dev/null + source "$state_file" + cd "$REPO" || return 1 +} + +# Detect container runtime: docker, podman, or empty string +detect_container_cmd() { + if command -v docker &>/dev/null && docker info &>/dev/null; then + echo "docker" + elif command -v podman &>/dev/null && podman info &>/dev/null; then + echo "podman" + else + echo "" + fi +} + +# Run grype scan via container or local install +# Usage: run_grype [--fresh] [--json] +# --fresh: create volume, pull latest image, update DB +# --json: output JSON instead of table +run_grype() { + local fresh=false output_format="table" + for arg in "$@"; do + case "$arg" in + --fresh) fresh=true ;; + --json) output_format="json" ;; + esac + done + + if [[ -n "$CONTAINER_CMD" ]]; then + if [[ "$fresh" == "true" ]]; then + $CONTAINER_CMD volume create grype-db >/dev/null 2>&1 || true + $CONTAINER_CMD run --pull=always --rm \ + -v grype-db:/root/.cache/grype anchore/grype:latest db update >&2 || true + fi + if $CONTAINER_CMD run --rm \ + -v grype-db:/root/.cache/grype \ + -v "$(pwd)":/src \ + anchore/grype:latest /src --config /src/.grype.yaml -o "$output_format"; then + return 0 + fi + echo "WARNING: Container scan failed, trying local grype..." >&2 + fi + + if [[ "$HAS_LOCAL_GRYPE" == "true" ]]; then + if [[ "$fresh" == "true" ]]; then + grype db update >&2 + fi + grype . --config .grype.yaml -o "$output_format" + else + echo "ERROR: No scanner available." >&2 + echo " Install docker/podman or grype locally." >&2 + return 1 + fi +} + +# Abbreviate Go package path for commit messages +# github.com/docker/docker -> docker/docker +# golang.org/x/net -> x/net +# helm.sh/helm/v3 -> helm/v3 +# go.opentelemetry.io/otel -> otel +# go.opentelemetry.io/otel/exporters/otlp/*/X -> otel/X +# google.golang.org/grpc -> grpc +# sigs.k8s.io/controller-runtime -> controller-runtime +# k8s.io/* stays as-is +abbreviate_package() { + local pkg="$1" + case "$pkg" in + github.com/*) echo "${pkg#github.com/}" ;; + golang.org/x/*) echo "x/${pkg#golang.org/x/}" ;; + helm.sh/*) echo "${pkg#helm.sh/}" ;; + go.opentelemetry.io/otel/exporters/otlp/*) + echo "otel/$(basename "$pkg")" ;; + go.opentelemetry.io/*) echo "${pkg#go.opentelemetry.io/}" ;; + google.golang.org/*) echo "${pkg#google.golang.org/}" ;; + sigs.k8s.io/*) echo "${pkg#sigs.k8s.io/}" ;; + *) echo "$pkg" ;; + esac +} + +# Find all go.mod files in the repo (excluding vendor and gitignored dirs) +find_gomods() { + git ls-files --cached --others --exclude-standard '*/go.mod' 'go.mod' 2>/dev/null || \ + find . -name go.mod -not -path '*/vendor/*' +} + +# Remove artifacts added by go mod tidy from all go.mod files: +# - toolchain directive (we pin Go version via Shipyard image, not toolchain) +# - consecutive blank lines (tidy sometimes adds extra whitespace) +clean_gomod() { + local gomod + while IFS= read -r gomod; do + sed -i '/^toolchain/d' "$gomod" + sed -i '/^$/{N;/^\n$/s/\n//;}' "$gomod" + done < <(find_gomods) +} + +# 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 +insert_grype_ignore() { + local file="$1" cve_id="$2" package="$3" reason="$4" + local entry + entry=" # $reason + - vulnerability: $cve_id + package: + name: $package" + + if grep -qn '^exclude:' "$file" 2>/dev/null; then + local exclude_line + exclude_line=$(grep -n '^exclude:' "$file" | head -1 | cut -d: -f1) + { + head -n "$((exclude_line - 1))" "$file" + printf '%s\n' "$entry" + tail -n +"$exclude_line" "$file" + } > "${file}.tmp" + mv "${file}.tmp" "$file" + else + printf '\n%s\n' "$entry" >> "$file" + fi +} + +# Print a section header +banner() { + echo "" + echo "--- $1 ---" +} diff --git a/scripts/cve/locate.sh b/scripts/cve/locate.sh new file mode 100755 index 000000000..40c202680 --- /dev/null +++ b/scripts/cve/locate.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Find a Go package in go.mod files, check for replace directives and dependencies. +# Usage: locate.sh STATE_FILE PACKAGE +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source-path=SCRIPTDIR +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + +STATE_FILE="${1:?Usage: locate.sh STATE_FILE PACKAGE}" +PACKAGE="${2:?Usage: locate.sh STATE_FILE PACKAGE}" +load_state "$STATE_FILE" + +echo "=== Locating: $PACKAGE ===" + +# Find in all go.mod files +FOUND_IN="" +while IFS= read -r GOMOD; do + if grep -q "$PACKAGE" "$GOMOD" 2>/dev/null; then + FOUND_IN="${FOUND_IN:+$FOUND_IN }$GOMOD" + fi +done < <(find_gomods) + +if [[ -z "$FOUND_IN" ]]; then + echo "ERROR: $PACKAGE not found in any go.mod" >&2 + exit 1 +fi +echo "Found in: $FOUND_IN" + +# Check for replace directives across all go.mod files +REPLACE_HITS="" +while IFS= read -r GOMOD; do + HITS=$(grep "replace.*${PACKAGE}" "$GOMOD" 2>/dev/null || true) + [[ -n "$HITS" ]] && REPLACE_HITS+="$GOMOD: $HITS"$'\n' +done < <(find_gomods) +if [[ -n "$REPLACE_HITS" ]]; then + echo "" + echo "REPLACE DIRECTIVES:" + printf '%s' "$REPLACE_HITS" +fi + +# Show dependency graph excerpt for each module containing the package +echo "" +echo "Dependency graph:" +for GOMOD in $FOUND_IN; do + MODDIR=$(dirname "$GOMOD") + if [[ "$MODDIR" == "." ]]; then + go mod graph 2>/dev/null | grep "$PACKAGE" | head -10 || true + else + echo "($MODDIR)" + go -C "$MODDIR" mod graph 2>/dev/null | grep "$PACKAGE" | head -10 || true + fi +done diff --git a/scripts/cve/review-prompt.md b/scripts/cve/review-prompt.md new file mode 100644 index 000000000..5b96bf65a --- /dev/null +++ b/scripts/cve/review-prompt.md @@ -0,0 +1,54 @@ +# CVE Fix Review: ${REPO} ${BRANCH} + +You are reviewing CVE fix results for ${REPO} on ${BRANCH}. +The deterministic phase has already attempted to fix all CVEs. +All evidence has been pre-fetched below. + +## Fix Results + +${FIX_SUMMARY} + +## Current Scan + +${CURRENT_SCAN} + +## Unfixed CVE Details + +${LOCATE_OUTPUT} + +## Build Environment + +Go version in Shipyard build image: ${SHIPYARD_GO_VERSION} + +## Task + +Review ALL CVE outcomes: + +1. **Verify fixes**: Do the committed fixes look correct? Any concerns? + +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. + Run: `bash ${CVE_SCRIPTS}/ignore.sh ${STATE_FILE} PACKAGE CVE_ID SEVERITY "reason"` + - Note anything that needs team discussion + +3. **Check for regressions**: Did any fix introduce new CVEs? + +## Available Actions + +You can run these commands: + +- `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 CVE_ID SEVERITY "reason"` +- `bash ${CVE_SCRIPTS}/scan.sh ${STATE_FILE}` + +## Output + +End with a summary: + +```text +FIXED: N packages (list) +IGNORED: M packages (list with reasons) +UNRESOLVED: P packages (list — need team input) +``` diff --git a/scripts/cve/review.sh b/scripts/cve/review.sh new file mode 100755 index 000000000..884b456b8 --- /dev/null +++ b/scripts/cve/review.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# Review CVE fix results using a Claude subagent. +# Pre-fetches all evidence, builds prompt from template, invokes claude. +# Usage: review.sh STATE_FILE FIX_SUMMARY +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source-path=SCRIPTDIR +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + +STATE_FILE="${1:?Usage: review.sh STATE_FILE FIX_SUMMARY [SCAN_OUTPUT]}" +FIX_SUMMARY="${2:?Missing FIX_SUMMARY}" +CURRENT_SCAN="${3:-}" +load_state "$STATE_FILE" + +PROMPT_TEMPLATE="$SCRIPT_DIR/review-prompt.md" +if [[ ! -f "$PROMPT_TEMPLATE" ]]; then + echo "ERROR: Prompt template not found: $PROMPT_TEMPLATE" >&2 + exit 1 +fi + +# --- Pre-fetch evidence --- + +# Use provided scan output, or run a fresh scan +if [[ -z "$CURRENT_SCAN" ]]; then + echo "Running scan for review evidence..." + # shellcheck disable=SC2119 + CURRENT_SCAN=$(run_grype 2>&1) || CURRENT_SCAN="(scan failed)" +fi + +# Locate info for any NEEDS_REVIEW packages +LOCATE_OUTPUT="" +NEEDS_REVIEW_PKGS=$(echo "$FIX_SUMMARY" | grep "^NEEDS_REVIEW:" | sed 's/NEEDS_REVIEW: \([^ ]*\).*/\1/' || true) +for PKG in $NEEDS_REVIEW_PKGS; do + LOCATE_OUTPUT+="$(bash "$SCRIPT_DIR/locate.sh" "$STATE_FILE" "$PKG" 2>&1 || true)" + LOCATE_OUTPUT+=$'\n\n' +done + +if [[ -z "$LOCATE_OUTPUT" ]]; then + LOCATE_OUTPUT="All CVEs were fixed in the deterministic phase." +fi + +# --- Build prompt --- +export REPO BRANCH FIX_SUMMARY CURRENT_SCAN LOCATE_OUTPUT SHIPYARD_GO_VERSION +export CVE_SCRIPTS="$SCRIPT_DIR" STATE_FILE +PROMPT=$(envsubst < "$PROMPT_TEMPLATE") + +# --- Invoke Claude subagent --- +echo "Invoking Claude for review..." +if ! command -v claude &>/dev/null; then + echo "WARNING: claude CLI not available, skipping agent review." + echo "Deterministic fix results are still valid. Review manually:" + echo "" + echo "$FIX_SUMMARY" + exit 0 +fi + +OUTPUT=$(claude -p "$PROMPT" \ + --print \ + --model sonnet \ + --allowedTools "Bash" \ + --dangerously-skip-permissions \ + 2>&1) || true + +echo "$OUTPUT" + +# Extract summary lines +echo "" +echo "=== Review Summary ===" +echo "$OUTPUT" | grep -E "^(FIXED|IGNORED|UNRESOLVED):" || echo "(no structured summary from agent)" diff --git a/scripts/cve/scan.sh b/scripts/cve/scan.sh new file mode 100755 index 000000000..ac7d3fe6e --- /dev/null +++ b/scripts/cve/scan.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Scan for CVEs using grype. +# Usage: scan.sh STATE_FILE [--fresh] [--json] +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source-path=SCRIPTDIR +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + +STATE_FILE="${1:?Usage: scan.sh STATE_FILE [--fresh] [--json]}" +shift +load_state "$STATE_FILE" + +# Collect flags for run_grype +GRYPE_FLAGS=() +for arg in "$@"; do + case "$arg" in + --fresh|--json) GRYPE_FLAGS+=("$arg") ;; + *) echo "Unknown flag: $arg" >&2; exit 1 ;; + esac +done + +# Build if needed (e.g., submariner repo with UPX compression for stdlib CVE detection) +if [[ "$NEEDS_BUILD_FOR_SCAN" == "true" ]]; then + if ! make BUILD_UPX=false build >&2; then + echo "WARNING: Build failed. Scanning source only (may miss stdlib CVEs in binaries)." >&2 + fi +fi + +run_grype "${GRYPE_FLAGS[@]}" diff --git a/scripts/cve/test-lib.sh b/scripts/cve/test-lib.sh new file mode 100755 index 000000000..8f267d84e --- /dev/null +++ b/scripts/cve/test-lib.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# Unit tests for CVE fix library functions. +# Runs without docker, grype, or network access. +# Uses the real shipyard repo for integration tests. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# shellcheck source-path=SCRIPTDIR +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + +PASS=0 FAIL=0 +check() { + local desc="$1"; shift + local negate=false + [[ "$1" == "!" ]] && { negate=true; shift; } + local rc=0; "$@" 2>/dev/null || rc=$? + if { [[ "$negate" == false ]] && [[ $rc -eq 0 ]]; } || \ + { [[ "$negate" == true ]] && [[ $rc -ne 0 ]]; }; then + PASS=$((PASS + 1)) + else + echo " FAIL: $desc"; FAIL=$((FAIL + 1)) + fi +} + +TMPD=$(mktemp -d) +trap 'rm -rf "$TMPD"' EXIT + +# === Pure functions === +echo "pure functions" +check "state_file_path" bash -c "[[ $(state_file_path /x/shipyard release-0.23) == /tmp/cve-fix-shipyard-release-0-23-*.env ]]" +check "state_file_path devel" bash -c "[[ $(state_file_path /x/admiral devel) == /tmp/cve-fix-admiral-devel-*.env ]]" +check "abbrev github" test "$(abbreviate_package github.com/docker/docker)" = "docker/docker" +check "abbrev x/" test "$(abbreviate_package golang.org/x/net)" = "x/net" +check "abbrev helm" test "$(abbreviate_package helm.sh/helm/v3)" = "helm/v3" +check "abbrev k8s" test "$(abbreviate_package k8s.io/client-go)" = "k8s.io/client-go" +check "abbrev other" test "$(abbreviate_package go.etcd.io/bbolt)" = "go.etcd.io/bbolt" +check "abbrev nested" test "$(abbreviate_package github.com/go-git/go-git/v5)" = "go-git/go-git/v5" +check "abbrev otel" test "$(abbreviate_package go.opentelemetry.io/otel)" = "otel" +check "abbrev otel/sdk" test "$(abbreviate_package go.opentelemetry.io/otel/sdk)" = "otel/sdk" +check "abbrev otel deep" test "$(abbreviate_package go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp)" = "otel/otlptracehttp" +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" + +# === clean_gomod === +echo "clean_gomod" +printf 'module t\ngo 1.25.0\ntoolchain go1.25.1\n' > "$TMPD/go.mod" +(cd "$TMPD" && clean_gomod) +check "toolchain removed" test -z "$(grep toolchain "$TMPD/go.mod")" +check "go kept" grep -q "^go 1.25.0" "$TMPD/go.mod" + +# === load_state === +echo "load_state" +check "missing fails" ! load_state /tmp/nonexistent-cve-test.env +printf 'REPO="%s"\nBRANCH="devel"\n' "$TMPD" > "$TMPD/s.env" +check "loads vars" bash -c "source '$SCRIPT_DIR/lib.sh'; load_state '$TMPD/s.env' && [[ \$BRANCH = devel ]]" + +# === insert_grype_ignore === +echo "insert_grype_ignore" +cp "$REPO_ROOT/.grype.yaml" "$TMPD/g.yaml" +insert_grype_ignore "$TMPD/g.yaml" GHSA-test-1234 example.com/pkg "Test reason" +check "before exclude" test "$(grep -n GHSA-test-1234 "$TMPD/g.yaml" | cut -d: -f1)" -lt "$(grep -n '^exclude:' "$TMPD/g.yaml" | cut -d: -f1)" +check "entry present" grep -q GHSA-test-1234 "$TMPD/g.yaml" +check "original kept" grep -q CVE-2015-5237 "$TMPD/g.yaml" +printf -- '---\nignore:\n - vulnerability: CVE-1\n package:\n name: x\n' > "$TMPD/no-exc.yaml" +insert_grype_ignore "$TMPD/no-exc.yaml" GHSA-append example.com/other "No exclude" +check "appends" grep -q GHSA-append "$TMPD/no-exc.yaml" + +# === detect.sh (real repo) === +echo "detect.sh" +DETECT_OUT=$("$SCRIPT_DIR/detect.sh" "$REPO_ROOT" 0.23 2>&1) || true +check "normalizes 0.23" grep -q "release-0.23" <<< "$DETECT_OUT" +STATE=$(tail -1 <<< "$DETECT_OUT") +check "creates state" test -f "$STATE"; rm -f "$STATE" +check "bad repo fails" ! "$SCRIPT_DIR/detect.sh" /nonexistent devel +check "non-git fails" ! "$SCRIPT_DIR/detect.sh" /tmp devel + +# === locate.sh (real repo, inline state) === +echo "locate.sh" +printf 'REPO="%s"\nBRANCH="devel"\nCVE_SCRIPTS="%s"\n' "$REPO_ROOT" "$SCRIPT_DIR" > "$TMPD/loc.env" +LOCATE_OUT=$(bash "$SCRIPT_DIR/locate.sh" "$TMPD/loc.env" k8s.io/client-go 2>&1) || true +check "finds in go.mod" grep -q "Found in:" <<< "$LOCATE_OUT" +LOCATE_OUT=$(bash "$SCRIPT_DIR/locate.sh" "$TMPD/loc.env" github.com/golangci/golangci-lint 2>&1) || true +check "finds in tools" grep -q "tools" <<< "$LOCATE_OUT" +check "missing fails" ! bash "$SCRIPT_DIR/locate.sh" "$TMPD/loc.env" nonexistent/pkg >/dev/null + +# === Summary === +echo "" +TOTAL=$((PASS + FAIL)) +echo "$PASS/$TOTAL passed" +if [[ "$FAIL" -gt 0 ]]; then echo "$FAIL FAILED"; exit 1; fi diff --git a/skills/cve-fix/SKILL.md b/skills/cve-fix/SKILL.md index 8d3366f1e..c99adb971 100644 --- a/skills/cve-fix/SKILL.md +++ b/skills/cve-fix/SKILL.md @@ -1,695 +1,82 @@ --- name: cve-fix -description: Fix CVEs in Submariner Go repositories. Supports parallel execution across multiple repos and branches. Arguments are optional and order-independent. -version: 1.0.0 +description: Fix CVEs in Submariner Go repositories. Arguments are optional and order-independent. TRIGGER when user asks to fix CVEs, scan for vulnerabilities, or mentions grype/CVE/GHSA. argument-hint: "[branch] [repo]" user-invocable: true -allowed-tools: Bash, Read, Edit, Grep, Glob +allowed-tools: Bash, Read context: fork --- # CVE Fix Workflow -Fix CVEs in Go dependencies. One commit per package, one PR total. - -**Usage:** - -- `/cve-fix` - current repo, current branch -- `/cve-fix 0.23` - current repo, specified branch (short form) -- `/cve-fix ../submariner-operator` - specified repo, current branch -- `/cve-fix release-0.23 ../submariner-operator` - both specified (order doesn't matter) - -**Arguments** (both optional, order-independent): - -- `branch`: Branch name (anything that's not a path). Short versions like `0.23` auto-expand to `release-0.23`. -- `repo`: Path to repository (starts with `/`, `./`, `../`, `~/`, or is existing directory) - -**Working with multiple repositories:** Accepts repository and branch arguments to fix CVEs across different repositories: - -```bash -/cve-fix release-0.23 ../submariner-operator -/cve-fix ../lighthouse release-0.23 -/cve-fix release-0.23 ../admiral -/cve-fix devel ../subctl -``` - -Each invocation is independent (isolated execution context). Multiple invocations run sequentially - one completes before -the next starts. Cannot work on the same repository directory simultaneously. - ---- - -## Step 0: Detect Repository Configuration - -Run detection and display results before proceeding. - -```bash -# Parse arguments: [repo] [branch] in any order -read -r ARG1 ARG2 REST <<<"$ARGUMENTS" - -if [[ -n "$REST" ]]; then - echo "ERROR: Too many arguments. Usage: /cve-fix [repo] [branch]" - exit 1 -fi - -# Classify arguments: path (/, ./, ../, ~/) or existing dir = repo, otherwise = branch -REPO="" -BRANCH="" - -for arg in "$ARG1" "$ARG2"; do - [[ -z "$arg" ]] && continue - - # Expand tilde for home directory paths - arg="${arg/#\~/$HOME}" - - if [[ "$arg" == /* ]] || [[ "$arg" == ./* ]] || [[ "$arg" == ../* ]] || [[ -d "$arg" ]]; then - [[ -n "$REPO" ]] && { echo "ERROR: Multiple repositories specified"; exit 1; } - REPO="$arg" - else - [[ -n "$BRANCH" ]] && { echo "ERROR: Multiple branches specified"; exit 1; } - BRANCH="$arg" - fi -done - -# Apply defaults -REPO="${REPO:-.}" - -# Validate repo exists and is git repo -if [[ ! -d "$REPO" ]]; then - echo "ERROR: Repository not found: $REPO" - exit 1 -fi - -if ! git -C "$REPO" rev-parse --git-dir &>/dev/null; then - echo "ERROR: Not a git repository: $REPO" - exit 1 -fi - -# Get current branch if not specified -if [[ -z "$BRANCH" ]]; then - BRANCH=$(git -C "$REPO" branch --show-current 2>/dev/null) - if [[ -z "$BRANCH" ]]; then - echo "ERROR: Not on a branch (detached HEAD). Specify branch explicitly." - exit 1 - fi - echo "Using current branch: $BRANCH" -fi - -# Normalize short version to full branch name (0.22 → release-0.22) -if [[ "$BRANCH" =~ ^[0-9]+\.[0-9]+$ ]]; then - BRANCH="release-${BRANCH}" - echo "Normalized to branch: $BRANCH" -fi - -# Change to repository directory -if [[ "$REPO" != "." ]]; then - cd "$REPO" || { - echo "ERROR: Cannot change to directory: $REPO" - exit 1 - } - echo "Working in repository: $REPO" -fi - -# tools/go.mod presence -HAS_TOOLS_GOMOD=false -test -f tools/go.mod && HAS_TOOLS_GOMOD=true - -# Generated files with version comments -GENERATED_FILE="" -DIFF_IGNORE_ARGS="" - -if grep -rql "controller-gen.kubebuilder.io/version" --include="*.go" . 2>/dev/null; then - DIFF_IGNORE_ARGS="-Icontroller-gen.kubebuilder.io/version" - GENERATED_FILE=$(grep -rl "controller-gen.kubebuilder.io/version" --include="*.go" . 2>/dev/null | head -1) -elif find . -name "*.pb.go" -type f 2>/dev/null | head -1 | grep -q .; then - DIFF_IGNORE_ARGS="-I^//" - GENERATED_FILE=$(find . -name "*.pb.go" -type f 2>/dev/null | head -1) -fi - -# Pre-scan build requirement (UPX compression) -NEEDS_BUILD_FOR_SCAN=false -grep -q "BUILD_UPX" Makefile 2>/dev/null && NEEDS_BUILD_FOR_SCAN=true - -# Container runtime (docker or podman) -CONTAINER_CMD="" -if command -v docker &>/dev/null && docker info &>/dev/null; then - CONTAINER_CMD="docker" -elif command -v podman &>/dev/null && podman info &>/dev/null; then - CONTAINER_CMD="podman" -fi - -# Local grype -HAS_LOCAL_GRYPE=false -command -v grype &>/dev/null && HAS_LOCAL_GRYPE=true - -# Shipyard build image (legacy artifact name: shipyard-dapper-base) -SHIPYARD_IMAGE="" -SHIPYARD_GO_VERSION="" - -# Detect base branch to determine image tag -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 - -SHIPYARD_IMAGE="quay.io/submariner/shipyard-dapper-base:${SHIPYARD_TAG}" - -# Pull latest image and check Go version -if [[ -n "$CONTAINER_CMD" ]]; then - echo "Checking Shipyard build image: $SHIPYARD_IMAGE" - $CONTAINER_CMD pull "$SHIPYARD_IMAGE" 2>&1 | tail -2 - SHIPYARD_GO_VERSION=$($CONTAINER_CMD run --rm "$SHIPYARD_IMAGE" go version 2>/dev/null || echo "unknown") - echo "Shipyard Go version: $SHIPYARD_GO_VERSION" -fi -``` - -Display configuration: tools/go.mod presence, generated file handling, build requirements, available scanners, Shipyard build image, -and Go version. - ---- - -## Step 1: Branch Setup +Run the command below exactly as written. Do not read, debug, or modify +the CVE scripts. If a bare repo name like `subctl` is passed, convert it +to `../subctl` before running. ```bash -# Save original branch/commit to restore on early exit -ORIGINAL_REF=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) -if [[ "$ORIGINAL_REF" == "HEAD" ]]; then - # Detached HEAD - save the commit hash instead - ORIGINAL_REF=$(git rev-parse HEAD) -fi - -# Track fetch status for summary -FETCH_FAILED=false +#!/bin/bash +set -euo pipefail -if ! git fetch 2>/dev/null; then - echo "WARNING: git fetch failed. Continuing with cached remote state. Run 'git fetch' manually and re-run if it fetches updates." - FETCH_FAILED=true +# Find scripts directory (in shipyard repo) +CVE_SCRIPTS="$(pwd)/scripts/cve" +if [[ ! -d "$CVE_SCRIPTS" ]]; then + CVE_SCRIPTS="$HOME/go/src/submariner-io/shipyard/scripts/cve" fi - -# Create fix branch directly from origin (bypasses local branch state) -DATE=$(date +%Y-%m-%d) -VERSION=$(echo "$BRANCH" | sed 's/release-//') -FIX_BRANCH="fix-${VERSION}-cves-${DATE}" - -# Add suffix if branch exists (-v2, -v3, etc.) -SUFFIX="" -while git show-ref --verify --quiet refs/heads/"${FIX_BRANCH}${SUFFIX}"; do - if [[ -z "$SUFFIX" ]]; then SUFFIX="-v2"; else NUM=${SUFFIX#-v}; SUFFIX="-v$((NUM+1))"; fi -done - -if ! git checkout -b "${FIX_BRANCH}${SUFFIX}" origin/"$BRANCH" 2>/dev/null; then - echo "ERROR: Could not create fix branch from origin/$BRANCH. Branch may not exist remotely." +if [[ ! -d "$CVE_SCRIPTS" ]]; then + echo "ERROR: Cannot find scripts/cve/ directory" + echo "Expected in current directory or ~/go/src/submariner-io/shipyard/" exit 1 fi -``` - ---- - -## Step 2: Clean Build State - -Remove all build artifacts to ensure clean scan: - -```bash -# Remove binary artifacts and build cache -rm -rf ./bin ./dist ./output - -# Run make clean to remove all generated/ignored files -# This runs in Shipyard build container and cleans comprehensively -make clean 2>&1 | grep -v "Error.*ignored" || true - -# Verify critical directories are clean -if [ -d "./bin" ] && [ "$(ls -A ./bin 2>/dev/null)" ]; then - echo "WARNING: ./bin still contains files after cleanup" - ls -la ./bin -fi -``` - -This ensures grype scans only source code and go.mod files, not stale binaries. - ---- - -## Step 3: Scan for CVEs - -```bash -# Build if needed (submariner with UPX compression) -if [[ "$NEEDS_BUILD_FOR_SCAN" == "true" ]]; then - make BUILD_UPX=false build -fi - -# Scan with container runtime or local grype -if [[ -n "$CONTAINER_CMD" ]]; then - $CONTAINER_CMD volume create grype-db && \ - $CONTAINER_CMD run --pull=always --rm -v grype-db:/root/.cache/grype anchore/grype:latest db update && \ - $CONTAINER_CMD run --rm -v grype-db:/root/.cache/grype -v "$(pwd)":/src anchore/grype:latest /src --config /src/.grype.yaml -o table -elif [[ "$HAS_LOCAL_GRYPE" == "true" ]]; then - grype db update && grype . --config .grype.yaml -o table -else - echo "ERROR: No scanner available. Either:" - echo " 1. Install docker or podman (grype runs in container, no local install needed)" - echo " 2. Install grype locally: curl -sSfL https://get.anchore.io/grype | sudo sh -s -- -b /usr/local/bin" -fi -``` - -Ignore warning: `[0000] WARN no explicit name and version provided for directory source, deriving artifact ID from the given -path (which is not ideal)` - -**Check scan results:** - -If output shows "No vulnerabilities found", clean up and exit: - -```bash -# If no CVEs found, delete the fix branch and exit -FIX_BRANCH_FULL=$(git rev-parse --abbrev-ref HEAD) -git checkout "$ORIGINAL_REF" -git branch -D "$FIX_BRANCH_FULL" -echo "No CVEs found in $BRANCH - branch is clean" -exit 0 -``` - -For each CVE found, note **NAME** (package), **FIXED-IN** (version), and **VULNERABILITY** (CVE ID). Same package may -appear multiple times with different versions (e.g., v1.2.3 in tools, v1.3.0 in main); treat each as separate fix. - -**Stdlib CVEs** (NAME=stdlib): Can often be fixed by updating the `go` directive in go.mod to require a minimum Go version with the -fix. If go.mod update doesn't resolve the CVE (because the Shipyard build container has an older Go), then a Fedora version update -in Shipyard is needed. - ---- - -## Step 4: Locate Package - -```bash -PACKAGE="[package-from-scan]" - -# Find in go.mod files -if [[ "$HAS_TOOLS_GOMOD" == "true" ]]; then - grep -Fl "$PACKAGE" go.mod tools/go.mod 2>/dev/null -else - grep -Fl "$PACKAGE" go.mod 2>/dev/null -fi - -# Check for replace directives -grep "replace.*$(basename "$PACKAGE")" go.mod tools/go.mod 2>/dev/null -``` - -**If replace directive found**, check git history to understand why it was added: - -```bash -git log -p --all -G "replace.*$(basename "$PACKAGE")" -- go.mod tools/go.mod | head -50 -``` - -**If obsolete** (no longer needed based on git history), remove it: - -```bash -go mod edit -dropreplace="$PACKAGE" -[[ "$HAS_TOOLS_GOMOD" == "true" ]] && go -C tools mod edit -dropreplace="$PACKAGE" -``` - -**Otherwise** (still needed), the replace directive will be updated to a safe version in Step 5. - -**Check parent-child dependencies** (fix parent first if both have CVEs): - -```bash -go mod graph | grep "$PACKAGE" -[[ "$HAS_TOOLS_GOMOD" == "true" ]] && go -C tools mod graph | grep "$PACKAGE" -``` - ---- - -## Step 5: Update Package - -```bash -PACKAGE="[package]" -VERSION="[fixed-version]" # Use highest if multiple CVEs - -# Update in tools/go.mod if present there -if grep -q "$PACKAGE" tools/go.mod 2>/dev/null; then - go -C tools get "${PACKAGE}@v${VERSION}" && go -C tools mod tidy -fi - -# Update in go.mod if present there -if grep -q "$PACKAGE" go.mod 2>/dev/null; then - go get "${PACKAGE}@v${VERSION}" && go mod tidy -fi - -# Clean up go.mod artifacts (portable sed -i for GNU/BSD) -sed -i.bak '/^toolchain/d' go.mod && rm -f go.mod.bak -[[ "$HAS_TOOLS_GOMOD" == "true" ]] && sed -i.bak '/^toolchain/d' tools/go.mod && rm -f tools/go.mod.bak -sed -i.bak '/^$/{N;/^\n$/s/\n//;}' go.mod && rm -f go.mod.bak -[[ "$HAS_TOOLS_GOMOD" == "true" ]] && sed -i.bak '/^$/{N;/^\n$/s/\n//;}' tools/go.mod && rm -f tools/go.mod.bak - -# Verify changes -git diff $DIFF_IGNORE_ARGS -``` - -Expected: Dependency file updates only. - -**On stable branches:** If go get upgrades Go (1.X→1.Y) or K8s (v0.A→v0.B) minor version: - -- Low CVEs: revert changes, add to ignore list (Step 10), and commit. Note in summary for user awareness. -- Medium/High/Critical CVEs: revert changes, do NOT ignore automatically. Note in summary and flag for team review. - -### Stdlib CVE Variant - -For stdlib CVEs, update the `go` directive instead of a package: - -```bash -# Determine required Go version from CVE scan FIXED-IN column -# Example: "1.24.12" from "*1.24.12, 1.25.6" -GO_VERSION="[version-from-FIXED-IN]" - -# Update go directive in go.mod -go mod edit -go="${GO_VERSION}" -[[ "$HAS_TOOLS_GOMOD" == "true" ]] && go -C tools mod edit -go="${GO_VERSION}" - -# Run go mod tidy to update dependencies -go mod tidy -[[ "$HAS_TOOLS_GOMOD" == "true" ]] && go -C tools mod tidy - -# Clean up toolchain directive and extra blank lines (portable sed -i for GNU/BSD) -sed -i.bak '/^toolchain/d' go.mod && rm -f go.mod.bak -[[ "$HAS_TOOLS_GOMOD" == "true" ]] && sed -i.bak '/^toolchain/d' tools/go.mod && rm -f tools/go.mod.bak -sed -i.bak '/^$/{N;/^\n$/s/\n//;}' go.mod && rm -f go.mod.bak -[[ "$HAS_TOOLS_GOMOD" == "true" ]] && sed -i.bak '/^$/{N;/^\n$/s/\n//;}' tools/go.mod && rm -f tools/go.mod.bak - -# Verify changes -git diff $DIFF_IGNORE_ARGS -``` - -Expected: go.mod shows `go 1.24.0` → `go 1.24.12` (example). - -**If CVE persists after go.mod update**: The Shipyard build container's Go version (from Step 0) is older than required. This means: - -- Local scans after `rm -rf ./bin` will pass (no binaries to scan) -- CI builds will still fail because Shipyard build container compiles with old Go -- A Shipyard Fedora update is needed to bring newer Go to the build image - -For commit message format: - -```text -Bump Go to [version] for stdlib CVEs - -Updates Go requirement from [old-version] to [new-version] to address -stdlib vulnerabilities. - -Fixes: [CVE-ID-1], [CVE-ID-2], ... -``` - ---- - -## Step 6: Clean Build Artifacts - -```bash -make clean -``` - -Removes build artifacts to avoid false positives in rescan. Network errors: see Common Issues. - ---- - -## Step 7: Verify Fix - -```bash -if [[ "$NEEDS_BUILD_FOR_SCAN" == "true" ]]; then - make BUILD_UPX=false build -fi - -if [[ -n "$CONTAINER_CMD" ]]; then - $CONTAINER_CMD run --rm -v grype-db:/root/.cache/grype -v "$(pwd)":/src anchore/grype:latest /src --config /src/.grype.yaml -o table -elif [[ "$HAS_LOCAL_GRYPE" == "true" ]]; then - grype . --config .grype.yaml -o table -fi -``` - -CVE for this package should no longer appear. - -**If CVE persists**: Double-check you used the correct version from Step 3 FIXED-IN column. If version is correct but CVE -persists, recheck Step 4 for replace directives. - ---- - -## Step 8: Verify Build - -```bash -make unit -``` - -This runs unit tests in the Shipyard build container (unless LOCAL_BUILD=1 is set). The Shipyard Go version was displayed in -Step 0. Skip this step during multi-package fixes; run once at end. Build errors may indicate incompatible dependency versions for -this branch. - -**Note**: If tests fail with "go.mod requires go >= X.Y.Z (running go A.B.C)": - -- The error shows the **Shipyard build container's** Go version (A.B.C), not your local Go version -- This is a version requirement mismatch, not an actual test failure -- Compare A.B.C with the Shipyard Go version from Step 0 to confirm -- If Shipyard Go is insufficient (A.B.C < X.Y.Z), Shipyard needs updating with newer Fedora - ---- - -## Step 9: Commit - -```bash -# Stage dependency files -git add go.mod go.sum -[[ "$HAS_TOOLS_GOMOD" == "true" ]] && git add tools/go.mod tools/go.sum - -# Handle generated files (stage only if substantive changes) -if [[ -n "$GENERATED_FILE" ]] && [[ -n "$DIFF_IGNORE_ARGS" ]]; then - if git diff $DIFF_IGNORE_ARGS "$GENERATED_FILE" 2>/dev/null | grep -q .; then - git add "$GENERATED_FILE" - else - git checkout "$GENERATED_FILE" - fi -fi - -git diff --staged --stat -``` - -Expected: go.mod, go.sum (and tools versions if applicable, generated file only if substantive changes). - -### Commit Message Format - -**Single CVE:** - -```text -Bump for - -Full package: -``` - -**Multiple CVEs (same package):** - -```text -Bump for CVEs - -Full package: -Fixes: , -``` -**Abbreviations:** - -- `github.com/docker/docker` → `docker/docker` -- `golang.org/x/oauth2` → `x/oauth2` -- `helm.sh/helm/v3` → `helm/v3` -- Keep `k8s.io/` prefix - -**If only tools files changed:** add "in /tools" to subject. - -**For stdlib CVE fixes:** - -```text -Bump Go to for stdlib CVEs - -Updates Go requirement from to to address -stdlib vulnerabilities. - -Fixes: , , -``` - -Example: - -```text -Bump Go to 1.24.12 for stdlib CVEs - -Updates Go requirement from 1.24.0 to 1.24.12 to address -stdlib vulnerabilities. - -Fixes: CVE-2025-61726, CVE-2025-61727, CVE-2025-61728, CVE-2025-61729, CVE-2025-61730, CVE-2025-61731 -``` - -```bash -git commit -s -m "$(cat <<'EOF' -Bump [abbreviated-package] for [CVE-ID] - -Full package: [full-package-path] -EOF -)" -``` - -**After each commit, rebuild (if NEEDS_BUILD_FOR_SCAN) and rescan** to catch newly introduced CVEs, then repeat Steps 4-9. - ---- - -## Step 10: Ignore Unfixable CVEs - -Skip if no CVEs remain. - -**Always prefer fixing over ignoring.** Even low-severity CVEs appear in user security scanners. Only ignore when fixing -is not possible for this branch. - -**Stdlib CVEs**: Do not ignore. First attempt to fix by updating go.mod (see Step 5 stdlib variant above). If Shipyard's -Go version is too old (check Step 0), flag for Shipyard Fedora update. Note in summary. - -**Autonomous decision criteria:** - -- Attempt fix first -- Low CVEs requiring breaking changes: ignore and commit, note in summary -- Medium/High/Critical requiring breaking changes: flag for review, do NOT ignore - -Add to existing `ignore:` list in `.grype.yaml`: - -```yaml - # Update requires [incompatibility]. [Severity] doesn't justify breaking changes. - - vulnerability: GHSA-xxxx-xxxx-xxxx - package: - name: package.name/path -``` - -```bash -git add .grype.yaml -git commit -s -m "$(cat <<'EOF' -Ignore [package] CVEs incompatible with release-X.Y - -[Package] CVEs require versions with [incompatible dependency] -incompatible with this branch's [current dependency]. -EOF -)" +exec bash "$CVE_SCRIPTS/fix-all.sh" $ARGUMENTS ``` ---- - -## Step 11: Final Verification - -```bash -# Check if any commits were made -COMMIT_COUNT=$(git log origin/"$BRANCH"..HEAD --oneline 2>/dev/null | wc -l) - -if [[ "$COMMIT_COUNT" -eq 0 ]]; then - echo "No commits made - no CVEs were fixed." - FIX_BRANCH_FULL=$(git rev-parse --abbrev-ref HEAD) - git checkout "$ORIGINAL_REF" - git branch -D "$FIX_BRANCH_FULL" - echo "Deleted empty fix branch: $FIX_BRANCH_FULL" - exit 0 -fi - -# Rebuild if needed -if [[ "$NEEDS_BUILD_FOR_SCAN" == "true" ]]; then - make BUILD_UPX=false build -fi +fix-all.sh does everything: detect config, create fix branch, scan for CVEs, +fix each deterministically, run tests, agent-review all results, and print +the PR command. -# Final scan -if [[ -n "$CONTAINER_CMD" ]]; then - $CONTAINER_CMD run --rm -v grype-db:/root/.cache/grype -v "$(pwd)":/src anchore/grype:latest /src --config /src/.grype.yaml -o table -elif [[ "$HAS_LOCAL_GRYPE" == "true" ]]; then - grype . --config .grype.yaml -o table -fi +**Exit code 0**: All CVEs addressed. Review commits and run the printed PR command. -# Only run tests if we have commits to verify -make unit # Runs in Shipyard build container -git log origin/"$BRANCH"..HEAD -git diff $DIFF_IGNORE_ARGS -``` +**Exit code 2**: Some CVEs unresolved after review. -Expected: No vulnerabilities, tests pass, clean diff. +**Exit code 1**: Error. ---- +## Multi-Repo -## Step 12: Create Pull Request +For multiple repos, spawn one agent per repo. Each agent should run +`bash ~/go/src/submariner-io/shipyard/scripts/cve/fix-all.sh REPO BRANCH` +via the Bash tool (not the Skill tool, which times out in subagents). +Report per repo: CVEs found, fixed, ignored, and PR command. On errors +or timeout, clean up orphaned processes with +`bash ~/go/src/submariner-io/shipyard/scripts/cve/clean.sh` before reporting. +Never modify the CVE fix scripts themselves. -**Note:** Git push and PR creation require SSH authentication, which may fail if keys are on external devices (YubiKey, hardware tokens). +## Usage -Extract variables: +- `/cve-fix` - current repo, current branch +- `/cve-fix 0.23` - current repo, specified branch (short form) +- `/cve-fix ../submariner-operator` - specified repo, current branch +- `/cve-fix release-0.23 ../submariner-operator` - both specified (order doesn't matter) -```bash -CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) -BASE_BRANCH=$(echo "$CURRENT_BRANCH" | sed 's/fix-\([0-9.]*\)-.*/release-\1/; s/fix-devel-.*/devel/') -COMMIT_COUNT=$(git log "origin/${BASE_BRANCH}"..HEAD --oneline | wc -l) -PLURAL=$([[ "$COMMIT_COUNT" -eq 1 ]] && echo "" || echo "s") -FORK_REMOTE=$(git remote -v | awk '!/submariner-io/ && /\(push\)/ { print $1; exit }') -FORK_USER=$(git remote get-url "${FORK_REMOTE}" 2>/dev/null | sed -E 's#.*github.com[:/]+([^/]+)/.*#\1#') -``` +Arguments are order-independent. Short versions like `0.23` auto-expand +to `release-0.23`. Repos must be paths. If a bare name like `subctl` is passed, resolve it +to `../subctl` (from any submariner repo) or `~/go/src/submariner-io/subctl`. -Substitute into template and provide in response (not bash output): +**From the command line** (without Claude): ```bash -git push && \ -gh pr create \ - --title "Fix CVE in " \ - --body "See commit message for details." \ - --base "" \ - --head ":" \ - --assignee "@me" +make cve-fix # current repo, current branch +make cve-fix BRANCH=release-0.23 # current repo, specified branch +make cve-fix REPO=../submariner-operator BRANCH=release-0.23 # specified repo and branch ``` ---- - -## Summary (Return Value) - -When complete, provide a summary including: - -1. **Repository**: Path to repository (if not current directory, omit if ".") -2. **Branch**: Target branch (and fix branch name if created) -3. **CVEs Fixed**: List of CVE IDs with package names (or "None - branch is clean" if no CVEs found) -4. **CVEs Ignored**: List with reasons (if any) -5. **CVEs Needing Review**: Medium/High/Critical that couldn't be fixed (if any) -6. **Stdlib CVEs**: Note if fixed via go.mod update; flag if Shipyard update needed -7. **PR Command**: The substituted command from Step 12 (only if commits were made) -8. **Status**: Success, partial success, or blocked -9. **Warnings**: If FETCH_FAILED=true, warn that branch was created from cached remote state and advise running git fetch and - re-running if updates are fetched - -**Note**: Skill exits early in Step 3 if no CVEs found, or in Step 11 if all CVEs were handled without commits. - -Example: - -```text -## CVE Fix Complete: release-0.23 - -**Repository:** ../submariner-operator -**Branch:** fix-0.23-cves-2026-02-09 -**Status:** Success - -### Fixed (3 packages + stdlib) -- CVE-2025-61726, CVE-2025-61729, CVE-2025-61731: stdlib (go.mod updated to 1.24.12) -- GHSA-xxxx-xxxx-xxxx: golang.org/x/net -- GHSA-yyyy-yyyy-yyyy: github.com/docker/docker - -### Requires Shipyard Update -- Stdlib CVEs fixed in go.mod, but Shipyard build image has Go 1.24.11. Recommend updating Shipyard to Fedora 42+ for Go 1.24.12 - to ensure CI passes. - -### PR Command -git push origin fix-0.23-cves-2026-02-09 && gh pr create ... - -### Warnings -⚠️ git fetch failed. Branch created from cached remote state. Run 'git fetch' and re-run if it fetches new commits. -``` - ---- - ## Common Issues | Issue | Solution | | ----- | -------- | -| CVE persists after fix | Verify FIXED-IN version; check for replace directives in Step 4 | +| CVE persists after fix | Verify FIXED-IN version; check for replace directives | | New CVE appears after fix | Dependency downgrade introduced it; fix immediately | | Tests fail | Try different version; check CI logs | -| Large dependency updates (Helm, etc.) | May break old branches; check Go/K8s compatibility | | Container "no route to host" | Run `sudo systemctl restart docker` or `sudo systemctl restart podman` | -| Stdlib CVEs | Update go directive in go.mod. If CVE persists after cleaning, check Shipyard Go version (Step 0). May need Fedora update. | -| Old binaries causing false CVEs | Cleaned automatically in Step 2. For manual cleanup, run `make clean` | -| Git fetch fails | Run `git fetch` before starting to get latest commits. Skill continues with cached state if fetch fails | +| Stdlib CVEs | Fixed via go directive update. Check Shipyard Go version if CI fails | +| Git fetch fails | Run `git fetch` manually before starting | diff --git a/test/scripts/cve/test.sh b/test/scripts/cve/test.sh new file mode 100755 index 000000000..93b70325c --- /dev/null +++ b/test/scripts/cve/test.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Run CVE fix script unit tests inside dapper. +# Uses source tree (not installed scripts) since we're testing changes. +set -e +cd "$(dirname "$0")/../../.." +./scripts/cve/test-lib.sh