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 like0.23auto-expand torelease-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:
/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.
# 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
# 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
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
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."
exit 1
fi
Step 2: Clean Build State
Remove all build artifacts to ensure clean scan:
# 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
# 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:
# 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
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:
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:
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):
go mod graph | grep "$PACKAGE"
[[ "$HAS_TOOLS_GOMOD" == "true" ]] && go -C tools mod graph | grep "$PACKAGE"
Step 5: Update Package
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:
# 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 ./binwill 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:
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
make clean
Removes build artifacts to avoid false positives in rescan. Network errors: see Common Issues.
Step 7: Verify Fix
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
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
# 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:
Bump <abbreviated-package> for <CVE-ID>
Full package: <full-package-path>
Multiple CVEs (same package):
Bump <abbreviated-package> for CVEs
Full package: <full-package-path>
Fixes: <CVE-ID-1>, <CVE-ID-2>
Abbreviations:
github.com/docker/docker→docker/dockergolang.org/x/oauth2→x/oauth2helm.sh/helm/v3→helm/v3- Keep
k8s.io/prefix
If only tools files changed: add "in /tools" to subject.
For stdlib CVE fixes:
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>, <CVE-ID-3>
Example:
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
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:
# Update requires [incompatibility]. [Severity] doesn't justify breaking changes.
- vulnerability: GHSA-xxxx-xxxx-xxxx
package:
name: package.name/path
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
)"
Step 11: Final Verification
# 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
# 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
# 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
Expected: No vulnerabilities, tests pass, clean diff.
Step 12: Create Pull Request
Note: Git push and PR creation require SSH authentication, which may fail if keys are on external devices (YubiKey, hardware tokens).
Extract variables:
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#')
Substitute into template and provide in response (not bash output):
git push <FORK_REMOTE> <CURRENT_BRANCH> && \
gh pr create \
--title "Fix CVE<PLURAL> in <BASE_BRANCH>" \
--body "See commit message<PLURAL> for details." \
--base "<BASE_BRANCH>" \
--head "<FORK_USER>:<CURRENT_BRANCH>" \
--assignee "@me"
Summary (Return Value)
When complete, provide a summary including:
- Repository: Path to repository (if not current directory, omit if ".")
- Branch: Target branch (and fix branch name if created)
- CVEs Fixed: List of CVE IDs with package names (or "None - branch is clean" if no CVEs found)
- CVEs Ignored: List with reasons (if any)
- CVEs Needing Review: Medium/High/Critical that couldn't be fixed (if any)
- Stdlib CVEs: Note if fixed via go.mod update; flag if Shipyard update needed
- PR Command: The substituted command from Step 12 (only if commits were made)
- Status: Success, partial success, or blocked
- 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:
## 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 |
| 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 |
Converted and distributed by TomeVault — claim your Tome and manage your conversions.