From shipyard
Fixes CVEs in Submariner Go repositories by updating dependencies with one commit per package and one PR. Supports specified repos/branches with parallel execution capability via sequential calls.
npx claudepluginhub submariner-io/shipyard --plugin shipyard[branch] [repo]This skill is limited to using the following tools:
Fix CVEs in Go dependencies. One commit per package, one PR total.
Generates actionable remediation plans for vulnerable Go codebases, including dependency updates, code refactoring, workarounds, configuration changes, and verification steps. Use after CVE impact analysis.
Fetches CVE details via CIRCL API, scans Ark dependencies for vulnerabilities in Go/Node/Python/Docker, suggests mitigations, and generates security PR templates.
Scans npm, pip, Cargo, Go, Ruby projects for outdated packages, runs CVE audits, summarizes changelogs with Gemini, opens PRs per risk group (patch/minor/major).
Share bugs, ideas, or general feedback.
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:
/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.
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.
# 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
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.
# 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.
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"
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:
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:
rm -rf ./bin will pass (no binaries to scan)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], ...
make clean
Removes build artifacts to avoid false positives in rescan. Network errors: see Common Issues.
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.
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)":
# 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).
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/v3k8s.io/ prefixIf 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.
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:
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
)"
# 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.
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"
When complete, provide a summary including:
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.
| 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 |