cve-bump
End-to-end workflow for clearing CVEs in kgateway's Go dependencies. The user wants a single pass: discover the alerts → decide the action for each → present a plan → apply the bumps and ignore-list updates → verify locally.
Why this skill exists
CVE patching in kgateway is more mechanical than it looks, but the moving parts are easy to miss:
- The scanner findings live in GitHub code-scanning, not the local tree.
hack/osvtoolis the canonical way to fetch them. - Findings are uploaded per-branch and only the branches allowlisted in
the OSV scan workflow (
.github/workflows/osv-scanner.yaml) get scanned, so a topic branch has to query its base branch. - Some "vulnerabilities" are scanner false positives caused by
pseudo-version comparison quirks (see the istio.io entries in
osv-scanner.toml). These belong in the ignore list, not in go.mod churn. - Indirect deps need either a
go getof the parent that pulls in the fixed version, or an explicitrequireto force MVS to pick the fixed version. Getting this wrong looks like the bump "didn't take."
The skill encodes those decisions so the human only has to approve the plan, not relearn the rules each time.
Protected dependencies
Some dependencies are load-bearing enough that bumping them risks breaking the dataplane, compatibility with managed Kubernetes versions, or the Gateway API conformance contract. Never bump any of these without an explicit, in-conversation yes from the user — even for a critical CVE.
github.com/envoyproxy/*(Envoy andgo-control-plane)k8s.io/*(api, apimachinery, client-go, ...)istio.io/*sigs.k8s.io/gateway-apiand other Gateway API modules
How this gets enforced through the workflow:
- In the plan (phase 4), surface protected-dep rows with the action
needs approval (protected dep)and list them in their own bucket. Do not fold them into a batchgo get. - In the execute step (phase 5), confirm each protected-dep bump
one-by-one before running
go get. If the user says no, leave the dep alone and list the CVE under "deferred — protected dep" in the report. - Never silence a protected-dep CVE via
osv-scanner.tomlto avoid asking. The ignore list is for false positives only.
If a transitive bump (parent dep) would pull a new version of a
protected dep through MVS, that still counts as bumping the
protected dep. Detect it (go list -m -json <module> after the
parent bump) and bring it back to the user.
CVEs with no known fix
OSV-Scanner sometimes surfaces advisories that have no fixed version yet (or were withdrawn upstream). Do not:
- Pin to a fork or a personal patch.
- Add an
osv-scanner.tomlignore entry just to clear the alert. - Invent a "mitigation" not stated in the advisory.
Instead, collect these into a "no fix available" list during phase 4 and surface them to the user verbatim in the report (phase 7): CVE / severity / module / pinned version / advisory URL. They are tracked, not patched.
Trigger fit
Strong fit when the user says any of:
- "patch the CVEs", "fix the CVEs", "clear the OSV alerts"
- "bump X for the CVE", "address GHSA-...", "what CVEs do we have?"
- "scanner is complaining about Y", "osv-scanner findings"
- "update deps for security"
Skip when the user wants a non-security dep bump (no CVE motivation) —
hack/bump_deps.sh and a plain go get are usually right for those.
Tooling
This is a Go shop. The repo does not ship a Python toolchain and many
contributors do not have one installed — pip install pyyaml is not
a viable step.
For every parsing, filtering, or shaping task in this workflow, use:
jqfor JSON. Required byhack/osvtoolalready, so present whenever osvtool runs.yqfor YAML. Also required byhack/osvtool. Useyq -p=yaml -o=jsonto convert the osvtool YAML back into JSON sojqcan do the heavy lifting.go,make, standard shell builtins.
Do not reach for python, python3, node, ruby, or
pip/npm install <anything>. If you find yourself wanting one, write
a jq expression instead — every shape in the osvtool output (alerts,
rule metadata, message text, help tables) is straightforward to handle
in jq.
Concrete starter: to round-trip the saved YAML back to a JSON array of alerts and start filtering:
yq -p=yaml -o=json /tmp/cve-bump/alerts-<branch>-<ts>.yaml \
| jq '[ .[] | {
ghsa: .rule.id,
sev: .rule.security_severity_level,
msg: .most_recent_instance.message.text,
help: .rule.help,
url: .html_url
} ]'
CVE IDs typically appear in rule.full_description or in the message
text alongside the package and version; the help field contains the
GO-XXXX-XXXX advisory table with fixed versions. Extract with jq's
capture / regex helpers or a small sed/grep pipeline, not Python.
Workflow
There are six phases. Do them in order; do not skip the planning phase even if there is only one finding.
1. Show the landscape (cross-branch summary)
Before pulling raw alerts, run the table mode across all supported
branches for both targets so the user sees source and image
findings side by side. With no --branch flags, osvtool scans
exactly the branches allowlisted in the OSV scan workflow
(.github/workflows/osv-scanner.yaml), so this stays correct as LTS
branches are cut or retired:
# Source-target landscape (the planning input for this skill)
./hack/osvtool --table --target source --state open
# Image-target landscape (will be revisited in phase 8)
./hack/osvtool --table --target image --state open
Each call emits a compact markdown table — one row per branch,
columns: branch | C | H | M | L | max age | last scan | code-scanning. Show both to the user as-is; together they fit in
one screen and make image vs. source scope explicit.
Use --state open (not todo) here so the medium/low counts are
visible. The user is about to decide whether to include them; they
should see the numbers first.
Do not use the default YAML summary mode (without --table) for
this — it emits ~60 lines per branch of nested fields the user does
not need. If you ever want the full structured form (e.g. to grep a
specific field), pipe it to a file under /tmp/cve-bump/ instead of
into the chat.
2. Decide severity scope and target branch
Ask the user two short questions, back to back:
Severity scope: just critical/high (
--state todo), or all open severities including medium/low (--state open)? Recommend critical/high since that matches how teams typically prioritize, but the landscape view from phase 1 shows the full severity breakdown so the user can make an informed call (e.g. if there are 30 mediums onv2.2.xthat they want to clear ahead of a release). Just ask in one sentence — no menu.Branch to plan against: the raw pull is single-branch because planning depends on that branch's go.mod state. Run the detection helper for a default:
.claude/skills/cve-bump/scripts/detect_base.shIt prints one line — the supported branch the current HEAD is most likely based on (most-recent merge-base across the branches the OSV scan workflow allowlists, read from
.github/workflows/osv-scanner.yaml), ormainif it cannot decide. Confirm with the user: "Planning against<X>. OK, or pick another?"
If the landscape view in phase 1 showed zero alerts at the chosen severity for the chosen branch, stop here and tell the user — there is nothing to plan.
3. Pull the raw alerts
./hack/osvtool --raw --branch <branch> --target source --state <state>
Where <state> is whatever the user picked in phase 2 (todo or
open).
Notes:
--target sourcefilters this pass to Go-mod findings only. Image findings are addressed separately in phase 8 because their actions are different (Dockerfile / base-image / apk changes) and many of them resolve transitively once the source bumps land. Do not try to plan them in the same table.- The output is YAML. Save it to a scratch path outside the repo so it
is not accidentally committed. Use
/tmp/cve-bump/alerts-<branch>-<UTC-timestamp>.yaml(mkdir -p first). This gives the user a record and lets us re-parse without another GitHub call.
If osvtool errors with an auth message, surface the help text it
prints — it already explains the fix (gh auth refresh --scopes security_events or a fine-grained PAT). Do not paper over it.
4. Plan one action per finding
For each alert, extract:
- CVE ID (
rule.idis typically the GHSA; the CVE lives inrule.full_descriptionormost_recent_instance.message.text). - Severity (
rule.security_severity_level). - Affected Go module + current pinned version.
- Fixed version (the lowest version that resolves the advisory). May be absent — see the no-fix branch below.
- Whether the module is direct or indirect in this repo's go.mod.
- Whether the module is a protected dependency.
Then pick an action per the decision tree below.
Decision tree
flowchart TD
A[Finding] --> N{Is there a fixed version<br/>in the advisory?}
N -- no --> NF[Report only:<br/>add to "no fix available" list]
N -- yes --> P{Is the module a protected dep<br/>envoy / k8s.io/* / istio.io/* / gateway-api?}
P -- yes --> PA[Needs approval:<br/>row in plan with action "needs approval protected dep"<br/>do not include in batch go get]
P -- no --> B{Is the pinned version actually >= fix,<br/>once you account for pseudo-versions?}
B -- yes --> C[False positive → add to osv-scanner.toml]
B -- no --> D{Is the module direct in go.mod?}
D -- yes --> E[go get module@fixed-version]
D -- no --> F{Does a single parent dep<br/>own this transitive?}
F -- yes, and bumping it is reasonable --> G[go get parent@version-that-pulls-fix]
F -- no, or parent bump too risky --> H[Add explicit require<br/>go get module@fixed-version<br/>force MVS to pick the fix]
Detecting pseudo-version false positives: when the affected version
looks like v0.0.0-<14-digit-timestamp>-<sha> and the fix version is a
normal semver like 1.x, OSV-Scanner compares them lexically as
"0.0.0" < "1.x" and fires regardless of whether the pinned commit
already includes the fix. Read the timestamp embedded in the
pseudo-version and compare to the fix release date. If the pinned
commit is newer, it is a false positive — add to osv-scanner.toml
with a reason that names the timestamp, the fix release date, and the
years/months gap. The five istio.io entries already in that file are
the model to copy.
Choosing between parent bump and explicit require: prefer the parent bump when (a) the parent is itself behind by only a minor/patch version, and (b) its changelog does not flag breaking changes for our usage. Otherwise add an explicit require — it is the smallest possible intervention and Go's MVS will honor it.
Output: the plan
Present the plan as up to three fenced tables so each bucket is visually distinct.
1. Actionable bumps — what you intend to go get or ignore.
Columns: CVE | severity | module | from → to | action.
| CVE | sev | module | from | to | action |
| --------------- | --- | --------------------- | -------------- | --------- | --------------------- |
| CVE-2025-1111 | H | golang.org/x/net | v0.20.0 | v0.27.0 | go get (direct) |
| CVE-2025-2222 | M | gopkg.in/foo.v3 | v3.1.0 | v3.2.1 | explicit require |
| CVE-2022-31045 | H | istio.io/istio | v0.0.0-2026... | (n/a) | ignore: false pos |
2. Needs approval — protected deps (omit the table if empty).
Columns: CVE | severity | module | from → to | reason. The user
must say yes per row before any of these move into the actionable
batch.
| CVE | sev | module | from | to | reason |
| --------------- | --- | ----------------------- | -------- | -------- | ----------------------- |
| CVE-2025-9999 | H | k8s.io/apimachinery | v0.31.2 | v0.31.5 | only fix is a k8s.io bump |
3. No fix available (omit the table if empty). Columns:
CVE | severity | module | pinned | advisory. Read-only — no
action proposed. These will be repeated verbatim in the final
report.
| CVE | sev | module | pinned | advisory |
| --------------- | --- | --------------------- | --------- | ----------------------------------------------- |
| CVE-2025-7777 | M | example.com/foo | v1.4.0 | https://github.com/advisories/GHSA-xxxx-yyyy-zz |
Then ask the user to OK the actionable table, decide each protected-dep row, and acknowledge the no-fix list. Wait for explicit approval before touching files.
5. Execute
After approval, in order:
- Protected-dep gate. For any row whose action was
needs approval (protected dep), only proceed if the user explicitly said yes to that row. If unclear, ask again. Do not batch protected deps into step 2 —go getthem one at a time so the user can stop the flow if a transitive surprise shows up. - For each remaining
go getaction:go get <module>@<version>. go mod tidyonce at the end.- Check for surprise protected-dep transitives. After
go mod tidy, diffgo.modfor any envoy / k8s.io / istio.io / gateway-api lines that changed and were not in the approved plan. If any did, stop, surface them to the user, and let them decide (revert or approve). - For each ignore action: append a
[[IgnoredVulns]]entry toosv-scanner.toml. Thereasonmust be specific — name the pseudo-version timestamp, the fix release date, and the gap. Do not write generic reasons like "false positive" or "not applicable". Never add an ignore entry for a no-fix CVE or a protected-dep CVE just to clear noise. - If
go.sumchanged, runmake generate-licensesto refresh the license attribution files. - Run
make fmt-changedto keep the diff clean.
Do not stage anything (git add) and do not commit. The user reviews
the dirty tree themselves.
6. Verify
The user signed off on "build + scanner re-run only", so:
go build -tags e2e ./...— thee2etag is required to compile all source per the repo's CLAUDE.md. If the build fails, the bump is not safe; stop and report.- Local scanner re-run:
make osv-scan. This is the canonical way to run OSV-Scanner locally in kgateway — it shells out to Docker (imageghcr.io/google/osv-scanner-action), so you do not need a localosv-scannerbinary. Do not reach for a standaloneosv-scannerinvocation; the make target handles config lookup, output paths, and reporter conversion in one shot.- Results land at
_output/osv/<safe-branch>/results.json(and a SARIF sibling).<safe-branch>is the current branch with/and.replaced by-. - The
make osv-scanJSON schema is osv-scanner native (results[].packages[].vulnerabilities[].id), which is different from the GitHub code-scanning YAML from phase 3. To diff, extract the set of vuln IDs from each side withjq/yqand compare the sets — don't try to align records field-by-field. Report which CVEs the user expected to clear that still appear, and any new ones the scanner surfaced that were not in the original plan.
- Results land at
- If Docker is not running,
make osv-scanwill fail with a Docker error. Tell the user and stop — do not try to install osv-scanner from source as a workaround.
7. Report
End with the following sections, in order:
- What changed. One line per CVE that got an action: CVE,
module, from → to (or "ignore: "), and the
file(s) touched. The user reads this against
git diff. - Deferred — protected dep. Any protected-dep rows the user said no to (or did not respond to). List CVE / module / proposed version / why deferred so the next maintainer has the context.
- No fix available. Verbatim from the phase-4 table — CVE /
severity / module / pinned / advisory URL. Do not propose
workarounds. Do not add
osv-scanner.tomlentries for these.
Then continue into phase 8.
8. Image-target follow-up
Image findings often resolve transitively once source bumps land (e.g. a Go binary baked into the image gets the fixed dep), but some do not — base-image CVEs and apk packages need Dockerfile or base-image changes.
Refresh the image landscape against the planned branch:
./hack/osvtool --table \
--branch <branch> \
--target image --state open
Then prompt the user: "The source bumps are done. Image findings above remain on the published scan — most should clear once these bumps ship, but base-image / apk CVEs need a Dockerfile change. Want to plan that now, or defer?"
Two important caveats to state plainly:
- The
--tableview reflects the last GitHub-uploaded scan, not the working tree. The source bumps you just made won't be reflected in image scans until CI rebuilds and republishes results. - A local image scan is possible via
make osv-scan OSV_SCAN_IMAGES="<image-ref> ..."(seeosv-scan-latest-main-imagesin the Makefile for the canonical invocation against rolling-main images), but that scans already-built published images, not the just-bumped working tree. Mention this if the user asks why a "fixed" CVE still appears locally.
If the user says "defer", stop here. If they say "go", treat the image flow as a separate planning round — start a new plan with Dockerfile-level actions (do not extend the Go-mod plan with image actions, they live in different files and the decision tree above does not apply).
Edge cases worth knowing
- Pseudo-version on the fix side: rare, but if a fix only exists at
a non-tagged commit,
go get module@<sha>works. Mark this clearly in the plan so the user is not surprised by a pseudo-version in go.mod. - Module renamed/moved: if the advisory's module path no longer
exists upstream, a
replacedirective may be necessary. Treat this as ambiguous and flag it for the user instead of writing the replace silently. - Multiple CVEs on one module: combine into a single
go getto the highest required version. List them as separate rows in the plan table but mark "(combined bump)" in the action column. - Backport branches: when scanning
v2.2.xetc., the supported upgrade window for a given module may be narrower (e.g. cannot move to a new major). Default to the smallest version that resolves the CVE within the same major. - Topic branch with no alerts on the base branch: possible if the branch was just rebased after a recent scan. Tell the user; offer to re-run after CI uploads fresh results.
Files this skill touches
go.mod/go.sum— version bumpsosv-scanner.toml— false-positive ignore entries (root-level; this is the only scanner ignore file in this repo as of writing —.snyk,.grype.yaml,.trivyignoreare not present)- License attribution files, if
make generate-licensesis wired up
Bundled scripts
scripts/detect_base.sh— prints the supported branch that the current HEAD is most likely based on. Used in phase 1.
Things not to do
- Do not commit or stage. The user does that.
- Do not edit go.mod by hand — always go through
go getso go.sum stays consistent. - Do not bump a protected dependency without an explicit, in-conversation yes from the user — not in a batch, not as a transitive pulled in by a parent bump. Surface it for approval and wait.
- Do not invent a workaround for a CVE with no upstream fix.
Do not pin to a fork, do not silence it via
osv-scanner.toml. List it in the no-fix bucket and move on. - Do not stop after the source bumps — phase 8 (image follow-up) is part of the workflow. Prompt the user about remaining image findings every time.
- Do not add an
osv-scanner.tomlentry just to silence noise. The reason field has to defend the entry to a future reader. - Do not run
make generate-allor other heavy codegen targets for a dep bump — they are not needed for non-API changes and they slow the loop down by ~30s for no benefit. - Do not scan an unsupported branch (e.g.
chandler/cveskill) directly viaosvtool— there are no GitHub-uploaded alerts for it. (Note:make osv-scandoes work on any branch because it runs the scanner locally; that is fine for verification.) - Do not reach for Python, Node, or any other parser. Use
jqandyq— see the Tooling section. - Do not invoke
osv-scannerdirectly. Usemake osv-scan— it runs the scanner in Docker with the right config and output paths. - Do not dump the default YAML summary into the chat for the
cross-branch landscape view. Use
./hack/osvtool --table(phase 1) — it's a single small markdown table instead of dozens of lines of nested YAML.