| name | cve-bump |
| description | Patch CVEs surfaced by the kgateway OSV scanner by bumping the affected Go module dependencies (direct, indirect via require, or replace) and curating false-positive entries in osv-scanner.toml. Use this skill whenever the user wants to fix CVEs, address OSV-Scanner alerts, bump deps for security reasons, triage code-scanning findings, "see what CVEs we have", or do anything CVE/security-bump-flavored in this repo — even if they don't name the skill explicitly. |
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/osvtool is 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 get of the parent that pulls in the
fixed version, or an explicit require to 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 and go-control-plane)
k8s.io/* (api, apimachinery, client-go, ...)
istio.io/*
sigs.k8s.io/gateway-api and 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 batch go 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.toml to 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.toml ignore 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:
jq for JSON. Required by hack/osvtool already, so present
whenever osvtool runs.
yq for YAML. Also required by hack/osvtool. Use
yq -p=yaml -o=json to convert the osvtool YAML back into JSON so
jq can 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:
./hack/osvtool --table --target source --state open
./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 on
v2.2.x that 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.sh
It 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), or main if 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 source filters 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.id is typically the GHSA; the CVE lives in
rule.full_description or most_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 get them one at a time so the
user can stop the flow if a transitive surprise shows up.
- For each remaining
go get action: go get <module>@<version>.
go mod tidy once at the end.
- Check for surprise protected-dep transitives. After
go mod tidy, diff go.mod for 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 to
osv-scanner.toml. The reason must 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.sum changed, run make generate-licenses to refresh the
license attribution files.
- Run
make fmt-changed to 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 ./... — the e2e tag 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 (image ghcr.io/google/osv-scanner-action), so you do not
need a local osv-scanner binary. Do not reach for a
standalone osv-scanner invocation; 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-scan JSON 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 with jq/yq and 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.
- If Docker is not running,
make osv-scan will 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.toml entries 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
--table view 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> ..." (see
osv-scan-latest-main-images in 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
replace directive 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 get to
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.x etc., 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 bumps
osv-scanner.toml — false-positive ignore entries (root-level; this
is the only scanner ignore file in this repo as of writing — .snyk,
.grype.yaml, .trivyignore are not present)
- License attribution files, if
make generate-licenses is 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 get so
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.toml entry just to silence noise.
The reason field has to defend the entry to a future reader.
- Do not run
make generate-all or 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 via osvtool — there are no GitHub-uploaded alerts for it.
(Note: make osv-scan does 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
jq
and yq — see the Tooling section.
- Do not invoke
osv-scanner directly. Use make 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.