Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .agents/workflows/cve-fix.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,16 @@ If fix requires breaking changes (Go version, K8s major version, incompatible AP
- CVE severity (Low/Medium vs High/Critical)
- Cost/risk of breaking stable branch dependencies

Add to `.grype.yaml`:
**No fix available** — use `fix-state: not-fixed` so the entry auto-expires when a fix is published:
```yaml
# No fix available for [package]. [context].
- vulnerability: GHSA-xxxx-xxxx-xxxx
fix-state: not-fixed
package:
name: package.name/path
```

**Fix exists but would break the branch** — permanent ignore (no fix-state):
```yaml
# Update requires [incompatibility]. [Severity] doesn't justify breaking changes.
- vulnerability: GHSA-xxxx-xxxx-xxxx
Expand Down
71 changes: 56 additions & 15 deletions scripts/cve/detect.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,22 @@ CONTAINER_CMD=$(detect_container_cmd)
HAS_LOCAL_GRYPE=false
command -v grype &>/dev/null && HAS_LOCAL_GRYPE=true

# Shipyard build image
if [[ "$BRANCH" == "devel" ]]; then
SHIPYARD_TAG="devel"
elif [[ "$BRANCH" =~ ^release- ]]; then
SHIPYARD_TAG="$BRANCH"
else
echo "WARNING: Unknown branch pattern, assuming devel build image"
SHIPYARD_TAG="devel"
# Shipyard build image — read BASE_BRANCH from the repo's Makefile (which
# flows into SHIPYARD_TAG via Makefile.dapper), fall back to branch inference.
SHIPYARD_TAG=""
if [[ -f Makefile ]]; then
SHIPYARD_TAG=$(grep -oP '^\s*BASE_BRANCH\s*\?=\s*\K\S+' Makefile || echo "")
fi

if [[ -z "$SHIPYARD_TAG" ]]; then
if [[ "$BRANCH" == "devel" ]]; then
SHIPYARD_TAG="devel"
elif [[ "$BRANCH" =~ ^release- ]]; then
SHIPYARD_TAG="$BRANCH"
else
echo "WARNING: Unknown branch pattern, assuming devel build image"
SHIPYARD_TAG="devel"
fi
fi

SHIPYARD_IMAGE="quay.io/submariner/shipyard-dapper-base:${SHIPYARD_TAG}"
Expand All @@ -104,11 +112,21 @@ fi
ORIGINAL_REF=""
FIX_BRANCH=""
FETCH_FAILED=false
WORKTREE_DIR=""

# Detect self-referential fix (fixing the repo that contains these scripts).
# Checkout would overwrite the scripts on disk, so use a worktree instead.
SCRIPT_REPO=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || echo "")
SELF_FIX=false
[[ "$SCRIPT_REPO" == "$REPO" ]] && SELF_FIX=true

if [[ "$SETUP_BRANCH" == "true" ]]; then
if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
echo "ERROR: Working tree has uncommitted changes. Commit or stash first." >&2
exit 1
# Uncommitted changes check: skip for self-fix (worktree won't touch working tree)
if [[ "$SELF_FIX" != "true" ]]; then
if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
echo "ERROR: Working tree has uncommitted changes. Commit or stash first." >&2
exit 1
fi
fi

ORIGINAL_REF=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
Expand All @@ -130,11 +148,33 @@ if [[ "$SETUP_BRANCH" == "true" ]]; then
done
FIX_BRANCH="${FIX_BRANCH}${SUFFIX}"

if ! git checkout -b "$FIX_BRANCH" "origin/$BRANCH" 2>/dev/null; then
echo "ERROR: Could not create fix branch from origin/$BRANCH" >&2
exit 1
if [[ "$SELF_FIX" == "true" ]]; then
WORKTREE_DIR=$(mktemp -d)
if ! git worktree add -b "$FIX_BRANCH" "$WORKTREE_DIR" "origin/$BRANCH" 2>/dev/null; then
rm -rf "$WORKTREE_DIR"
echo "ERROR: Could not create worktree from origin/$BRANCH" >&2
exit 1
fi
REPO="$WORKTREE_DIR"
# Re-detect build image from target branch Makefile
if [[ -f "$WORKTREE_DIR/Makefile" ]]; then
_TAG=$(grep -oP '^\s*BASE_BRANCH\s*\?=\s*\K\S+' "$WORKTREE_DIR/Makefile" || echo "")
if [[ -n "$_TAG" ]]; then
SHIPYARD_TAG="$_TAG"
SHIPYARD_IMAGE="quay.io/submariner/shipyard-dapper-base:${SHIPYARD_TAG}"
if [[ -n "$CONTAINER_CMD" ]] && [[ -n "$($CONTAINER_CMD image ls -q "$SHIPYARD_IMAGE" 2>/dev/null)" ]]; then
SHIPYARD_GO_VERSION=$($CONTAINER_CMD run --rm "$SHIPYARD_IMAGE" go version 2>/dev/null || echo "unknown")
fi
fi
fi
echo "Fix branch: $FIX_BRANCH (worktree: $WORKTREE_DIR)"
else
if ! git checkout -b "$FIX_BRANCH" "origin/$BRANCH" 2>/dev/null; then
echo "ERROR: Could not create fix branch from origin/$BRANCH" >&2
exit 1
fi
echo "Fix branch: $FIX_BRANCH"
fi
echo "Fix branch: $FIX_BRANCH"
fi

# --- Write state file ---
Expand All @@ -154,6 +194,7 @@ HAS_LOCAL_GRYPE=$HAS_LOCAL_GRYPE
SHIPYARD_IMAGE="$SHIPYARD_IMAGE"
SHIPYARD_GO_VERSION="$SHIPYARD_GO_VERSION"
FETCH_FAILED=$FETCH_FAILED
WORKTREE_DIR="$WORKTREE_DIR"
CVE_SCRIPTS="$SCRIPT_DIR"
EOF

Expand Down
26 changes: 19 additions & 7 deletions scripts/cve/fix-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,14 @@ fi
if [[ "$MATCH_COUNT" -eq 0 ]]; then
echo "No CVEs found. $(basename "$REPO")/$BRANCH is clean."
if [[ -n "$FIX_BRANCH" ]]; then
git checkout "$ORIGINAL_REF" 2>/dev/null
git branch -D "$FIX_BRANCH" 2>/dev/null
if [[ -n "$WORKTREE_DIR" ]]; then
cd / 2>/dev/null || true
git -C "$SCRIPT_DIR" worktree remove --force "$WORKTREE_DIR" 2>/dev/null || true
git -C "$SCRIPT_DIR" branch -D "$FIX_BRANCH" 2>/dev/null || true
else
git checkout "$ORIGINAL_REF" 2>/dev/null
git branch -D "$FIX_BRANCH" 2>/dev/null
fi
fi
exit 0
fi
Expand All @@ -79,6 +85,7 @@ CVE_LINES=$(printf '%s\n' "$SCAN_JSON" | jq -r '
{
pkg: .artifact.name,
fixedIn: .vulnerability.fix.versions[0],
allFixVersions: (.vulnerability.fix.versions | join(",")),
cve: .vulnerability.id,
severity: .vulnerability.severity,
type: (if .artifact.name == "stdlib" then "stdlib" else "package" end)
Expand All @@ -89,11 +96,12 @@ CVE_LINES=$(printf '%s\n' "$SCAN_JSON" | jq -r '
pkg: .[0].pkg,
type: .[0].type,
fixedIn: (map(.fixedIn) | sort_by(split(".") | map(tonumber)) | last),
allFixVersions: (if .[0].type == "stdlib" then ([.[].allFixVersions] | unique | join(",")) else "" end),
cves: (map(.cve) | unique),
severity: .[0].severity
}) |
.[] |
"\(.type)\t\(.pkg)\t\(.fixedIn)\t\(.cves | join(","))\t\(.severity)"
"\(.type)\t\(.pkg)\t\(.fixedIn)\t\(.cves | join(","))\t\(.severity)\t\(.allFixVersions)"
' 2>/dev/null || echo "")

if [[ -z "$CVE_LINES" ]]; then
Expand All @@ -107,7 +115,7 @@ FIX_SUMMARY=""
FIXED_COUNT=0
REVIEW_COUNT=0

while IFS=$'\t' read -r TYPE PKG FIX_VER CVE_CSV _SEVERITY; do
while IFS=$'\t' read -r TYPE PKG FIX_VER CVE_CSV _SEVERITY ALL_FIX_VERS; do
[[ -z "$PKG" ]] && continue

# Split CVE_CSV into array
Expand All @@ -116,12 +124,13 @@ while IFS=$'\t' read -r TYPE PKG FIX_VER CVE_CSV _SEVERITY; do
FIX_LOG=$(mktemp)
if [[ "$TYPE" == "stdlib" ]]; then
STDLIB_EXIT=0
"$SCRIPT_DIR/fix-stdlib.sh" "$STATE_FILE" "$FIX_VER" "${CVES[@]}" 2>&1 | tee "$FIX_LOG" || STDLIB_EXIT=$?
"$SCRIPT_DIR/fix-stdlib.sh" "$STATE_FILE" "$FIX_VER" "$ALL_FIX_VERS" "${CVES[@]}" 2>&1 | tee "$FIX_LOG" || STDLIB_EXIT=$?
if [[ "$STDLIB_EXIT" -eq 0 ]]; then
FIX_SUMMARY+="FIXED: stdlib go $FIX_VER for ${CVES[*]}"$'\n'
FIXED_COUNT=$((FIXED_COUNT + 1))
else
FIX_SUMMARY+="NEEDS_REVIEW: stdlib — fix-stdlib.sh exited $STDLIB_EXIT"$'\n'
REASON=$(grep "^NEEDS_REVIEW:" "$FIX_LOG" || echo "NEEDS_REVIEW: stdlib — fix-stdlib.sh exited $STDLIB_EXIT")
FIX_SUMMARY+="$REASON"$'\n'
REVIEW_COUNT=$((REVIEW_COUNT + 1))
fi
else
Expand Down Expand Up @@ -149,7 +158,7 @@ done <<< "$CVE_LINES"
# Verify
echo ""
echo "Running unit tests..."
if ! make unit; then
if ! env -u SHIPYARD_TAG make unit; then
FIX_SUMMARY+="NEEDS_REVIEW: unit tests failed after applying fixes"$'\n'
REVIEW_COUNT=$((REVIEW_COUNT + 1))
fi
Expand Down Expand Up @@ -185,6 +194,9 @@ if [[ "$COMMIT_COUNT" -gt 0 ]]; then
FORK_USER=$(git remote get-url "${FORK_REMOTE}" 2>/dev/null | sed -E 's#.*github.com[:/]+([^/]+)/.*#\1#')

echo "PR command:"
if [[ -n "${WORKTREE_DIR:-}" ]]; then
echo "cd $WORKTREE_DIR && \\"
fi
echo "git push $FORK_REMOTE $CURRENT_BRANCH && \\"
echo "gh pr create \\"
echo " --title \"Fix CVE${PLURAL} in ${BASE_BRANCH}\" \\"
Expand Down
91 changes: 59 additions & 32 deletions scripts/cve/fix-package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,39 +36,50 @@ while IFS= read -r GOMOD; do
GO_VER=$(grep '^go ' "$GOMOD" 2>/dev/null | awk '{print $2}')
[[ -n "$GO_VER" ]] && GO_BEFORE+="$GOMOD:$GO_VER "
done < <(find_gomods)
# shellcheck disable=SC2046 # word splitting is intentional (multiple file args)
K8S_BEFORE=$(grep -h 'k8s.io/client-go' $(find_gomods) 2>/dev/null | grep -oP 'v0\.\K[0-9]+' | sort -un | tr '\n' ' ')
# Only check root go.mod for K8s version changes (submodule K8s deps don't affect shipped binaries)
K8S_BEFORE=$(grep -h 'k8s.io/client-go' go.mod 2>/dev/null | grep -oP 'v0\.\K[0-9]+' | sort -un | tr '\n' ' ')

# Update in all go.mod files that contain this package
while IFS= read -r GOMOD; do
MODDIR=$(dirname "$GOMOD")
if grep -qF "$PACKAGE" "$GOMOD" 2>/dev/null; then
echo "Updating $PACKAGE in $GOMOD..."
go -C "$MODDIR" get "${PACKAGE}@v${VERSION}" && go -C "$MODDIR" mod tidy
INSTALLED=$(grep -F "$PACKAGE" "$GOMOD" | grep -oP 'v\K[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ -n "$INSTALLED" ]] && version_gte "$INSTALLED" "$VERSION"; then
echo "NOTE: $GOMOD already has $PACKAGE v$INSTALLED (>= v$VERSION)"
else
echo "Updating $PACKAGE in $GOMOD..."
go -C "$MODDIR" get "${PACKAGE}@v${VERSION}" && go -C "$MODDIR" mod tidy
fi
fi
done < <(find_gomods)

clean_gomod

# Check for breaking changes (Go or K8s minor version upgrade in any go.mod)
# Check for breaking changes (Go or K8s minor version upgrade)
# Go directive bumps are safe if the build image already satisfies them.
# K8s minor version changes are genuinely breaking (different API versions).
COMPILER_GO=$(echo "$SHIPYARD_GO_VERSION" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+' || echo "")
BREAKING=""
while IFS= read -r GOMOD; do
GO_AFTER=$(grep '^go ' "$GOMOD" 2>/dev/null | awk '{print $2}')
[[ -z "$GO_AFTER" ]] && continue
# Find the before version for this go.mod
for PAIR in $GO_BEFORE; do
if [[ "${PAIR%%:*}" == "$GOMOD" ]]; then
GO_WAS="${PAIR#*:}"
if [[ "$(echo "$GO_WAS" | cut -d. -f1-2)" != "$(echo "$GO_AFTER" | cut -d. -f1-2)" ]]; then
BREAKING="${BREAKING:+$BREAKING; }$GOMOD: Go $GO_WAS -> $GO_AFTER"
if [[ -n "$COMPILER_GO" ]] && \
[[ "$(printf '%s\n' "$COMPILER_GO" "$GO_AFTER" | sort -V | tail -1)" == "$COMPILER_GO" ]]; then
echo "NOTE: $GOMOD Go $GO_WAS -> $GO_AFTER (safe: build image has Go $COMPILER_GO)"
else
BREAKING="${BREAKING:+$BREAKING; }$GOMOD: Go $GO_WAS -> $GO_AFTER"
fi
fi
break
fi
done
done < <(find_gomods)

# shellcheck disable=SC2046 # word splitting is intentional (multiple file args)
K8S_AFTER=$(grep -h 'k8s.io/client-go' $(find_gomods) 2>/dev/null | grep -oP 'v0\.\K[0-9]+' | sort -un | tr '\n' ' ')
K8S_AFTER=$(grep -h 'k8s.io/client-go' go.mod 2>/dev/null | grep -oP 'v0\.\K[0-9]+' | sort -un | tr '\n' ' ')
if [[ -n "$K8S_BEFORE" ]] && [[ -n "$K8S_AFTER" ]] && [[ "$K8S_BEFORE" != "$K8S_AFTER" ]]; then
BREAKING="${BREAKING:+$BREAKING; }K8s minor versions changed"
fi
Expand All @@ -79,14 +90,17 @@ if [[ -n "$BREAKING" ]]; then
exit 2
fi

# Verify fix: check that go.mod has the new version
# Verify fix: check that go.mod has version >= the fix version
STILL_VULNERABLE=false
while IFS= read -r GOMOD; do
if grep -q "${PACKAGE}.*v${VERSION}" "$GOMOD" 2>/dev/null; then
: # Updated to new version, good
elif grep -qF "$PACKAGE" "$GOMOD" 2>/dev/null; then
echo "WARNING: $GOMOD still has old version of $PACKAGE"
STILL_VULNERABLE=true
if grep -qF "$PACKAGE" "$GOMOD" 2>/dev/null; then
INSTALLED=$(grep -F "$PACKAGE" "$GOMOD" | grep -oP 'v\K[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ -n "$INSTALLED" ]] && version_gte "$INSTALLED" "$VERSION"; then
: # Installed version >= fix version, good
else
echo "WARNING: $GOMOD has $PACKAGE at v${INSTALLED:-unknown} (need >= v$VERSION)"
STILL_VULNERABLE=true
fi
fi
done < <(find_gomods)

Expand All @@ -109,25 +123,38 @@ if [[ -n "$GENERATED_FILE" ]] && [[ -n "$DIFF_IGNORE_ARGS" ]]; then
fi
fi

# Determine if tools-only change (all staged go files under tools/)
TOOLS_ONLY=""
if ! git diff --staged --name-only | grep -qE '^go\.(mod|sum)$' && \
git diff --staged --name-only | grep -qE '^tools/'; then
TOOLS_ONLY=" in /tools"
fi
# If nothing to commit, the CVE was already fixed by a prior package upgrade
if ! git diff --staged --quiet 2>/dev/null; then
# Determine if tools-only change (all staged go files under tools/)
TOOLS_ONLY=""
if ! git diff --staged --name-only | grep -qE '^go\.(mod|sum)$' && \
git diff --staged --name-only | grep -qE '^tools/'; then
TOOLS_ONLY=" in /tools"
fi

# Format commit message
ABBREV=$(abbreviate_package "$PACKAGE")
if [[ ${#CVE_IDS[@]} -eq 1 ]]; then
SUBJECT="Bump ${ABBREV} for ${CVE_IDS[0]}${TOOLS_ONLY}"
BODY="Full package: $PACKAGE"
# Format commit message
ABBREV=$(abbreviate_package "$PACKAGE")
if [[ ${#CVE_IDS[@]} -eq 1 ]]; then
SUBJECT="Bump ${ABBREV} for ${CVE_IDS[0]}${TOOLS_ONLY}"
BODY="Full package: $PACKAGE"
else
SUBJECT="Bump ${ABBREV} for CVEs${TOOLS_ONLY}"
FIXES_LINE="Fixes: $(printf '%s, ' "${CVE_IDS[@]}")"
FIXES_LINE="${FIXES_LINE%, }"
if [[ ${#FIXES_LINE} -gt 80 ]]; then
FIXES=$(printf ' %s\n' "${CVE_IDS[@]}")
BODY="Full package: $PACKAGE
Fixes:
$FIXES"
else
BODY="Full package: $PACKAGE
$FIXES_LINE"
fi
fi

git commit -s -m "$(printf '%s\n\n%s' "$SUBJECT" "$BODY")"
else
SUBJECT="Bump ${ABBREV} for CVEs${TOOLS_ONLY}"
FIXES=$(printf '%s, ' "${CVE_IDS[@]}")
BODY="Full package: $PACKAGE
Fixes: ${FIXES%, }"
echo "NOTE: $PACKAGE already at v$VERSION (fixed by prior upgrade)"
fi

git commit -s -m "$(printf '%s\n\n%s' "$SUBJECT" "$BODY")"

echo "FIXED: $PACKAGE v$VERSION for ${CVE_IDS[*]}"
Loading
Loading