# Cve Bump

> 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.

- Skill: `kgateway-dev/cve-bump` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add kgateway-dev/cve-bump`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kgateway-dev/cve-bump/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: kgateway-dev (https://skillmd.com/u/kgateway-dev)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/kgateway-dev/cve-bump

---


# 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:

```bash
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:

```bash
# 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:

1. **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.

2. **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:

   ```bash
   .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

```bash
./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](#protected-dependencies).

Then pick an action per the decision tree below.

#### Decision tree

```mermaid
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:

1. **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.
2. For each remaining `go get` action: `go get <module>@<version>`.
3. `go mod tidy` once at the end.
4. **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).
5. 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.
6. If `go.sum` changed, run `make generate-licenses` to refresh the
   license attribution files.
7. 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:

1. `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.
2. 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.
3. 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:

1. **What changed.** One line per CVE that got an action: CVE,
   module, from → to (or "ignore: <reason summary>"), and the
   file(s) touched. The user reads this against `git diff`.
2. **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.
3. **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:

```bash
./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](#protected-dependencies)
  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.

