# Cve Doctor

> Triage a CVE / Dependabot alert in a JS/TS project and recommend the least-invasive fix. Walks the dependency chain, identifies the parent that blocks the patch, flags unmaintained packages, and only suggests a package-manager override as a last resort with explicit user confirmation. TRIGGER when the user asks to "fix a CVE", references a Dependabot alert URL (github.com/*/security/dependabot/*), mentions a CVE-YYYY-NNNN or GHSA-* identifier, or asks how to resolve a vulnerable transitive dependency.

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

---


# cve-doctor

You are helping the user triage a single CVE / security advisory in a JavaScript or TypeScript project. The goal is to resolve it with the **least-invasive change possible**, preserving the project's semver intent and Dependabot/Renovate visibility. A package-manager override is the absolute last resort.

## Guiding principles

1. **Investigate before acting.** Do not install packages, edit `package.json`, or run `update` until you have walked the full chain and presented options.
2. **User decides at every branch.** When there are multiple viable fix paths, list them ranked least → most invasive and wait for the user to pick. Never silently pick the "easiest" option for them.
3. **Overrides are a last resort.** A `pnpm.overrides` / `npm.overrides` / `yarn.resolutions` entry hides the problem from future automated scans and creates a drift-debt. Only suggest it after every upstream path has been exhausted, and only with explicit "yes, do it" from the user.
4. **Unmaintained upstream is information, not a verdict.** If a blocking parent looks abandoned, report the specific signals (archived flag, last commit date, last release date) and let the user decide whether to fork, switch packages, or override.

## Phase 0 — Collect input

1. Read `$ARGUMENTS` from the invocation.
2. Parse it:
   - **Dependabot alert URL** of the form `https://github.com/<owner>/<repo>/security/dependabot/<n>`: run `gh api repos/<owner>/<repo>/dependabot/alerts/<n>` to fetch the alert JSON.
   - **CVE ID** (`CVE-YYYY-NNNN`) or **GHSA ID** (`GHSA-*`): run `gh api /advisories/<id>` to fetch the advisory. If the advisory has multiple `vulnerabilities[]`, ask the user which package+ecosystem applies in this project.
   - **Empty**: ask the user for a Dependabot alert URL or CVE/GHSA ID. Do not proceed without one.
3. From the fetched data, extract and display to the user:
   - Package name + ecosystem
   - Severity (CVSS score if available)
   - Short summary
   - `vulnerable_version_range[]` and `first_patched_version` for each range
   - Alert state (if applicable): `open`, `fixed`, `dismissed`, `auto_dismissed`
4. **Short-circuit:** if the alert state is `fixed` or `auto_dismissed`, report that the alert is already resolved and stop. Do not continue to Phase 1.

## Phase 1 — Detect the package manager

Check the project root for lockfiles (in this priority order when multiple exist — but ask the user if ambiguous):

| Lockfile | PM |
|---|---|
| `pnpm-lock.yaml` | pnpm |
| `yarn.lock` | yarn (check `package.json` → `packageManager` for classic vs berry) |
| `package-lock.json` | npm |
| `bun.lock` or `bun.lockb` | bun |

Record the detected PM. All commands below branch on this value — use the matrix at the bottom of this file.

## Phase 2 — Is this a direct dependency?

1. Grep the vulnerable package name in `package.json` (`dependencies`, `devDependencies`, `peerDependencies`, and any workspace package.jsons).
2. If it's a direct dep:
   - Check whether the latest version on the registry (`<pm> view <pkg> version`) is outside all vulnerable ranges.
   - Check whether our declared semver range admits a patched version.
   - If yes to both → **this is a one-shot fix**. Propose the single targeted update command (see matrix) and ask: "Want me to run this?" Then stop here.
   - If no (declared range pins to an unpatched major) → also ask the user whether they want to bump the major, and outline the risks. Stop here; it's not a transitive-chain problem.
3. If it's **not** direct → continue to Phase 3.

## Phase 3 — Walk the dependency chain

1. Run the PM-specific "why" command and capture the full output.
2. Parse it into a flat list of `(parent-package@parent-version, declared-range-for-vuln-pkg)` tuples. Include every path, not just the first.
3. Identify every **distinct installed version** of the vulnerable package in the lockfile (`grep` the lockfile for `^\s+<pkg>:` or equivalent). For each version, check it against every `vulnerable_version_range` from Phase 0.
4. For each vulnerable installed version, and for each immediate parent:
   - Look up the parent's **latest published version** and its declared range for the vulnerable package: `<pm> view <parent>@latest dependencies.<vuln-pkg>`.
   - Look up the parent's **currently-installed version**'s declared range.
   - Classify the parent:
     - **Auto-resolves on refresh**: the installed parent's declared range already admits a patched version (the lockfile is simply stale).
     - **Fixed in newer parent version**: current parent blocks, but latest parent admits a patch.
     - **Blocks even at latest**: current AND latest parent versions both pin to a vulnerable range. This is a **hard blocker** — continue to Phase 4 for this parent.
5. Print a compact summary table:

   ```
   Vulnerable version  | Parent                  | Status
   <pkg>@<v>           | <parent>@<v>            | Auto-resolves / Needs parent bump to <v'> / Hard blocker
   ```

## Phase 4 — Investigate hard-blocking parents

For each parent in the "hard blocker" category, gather (all read-only):

1. **Latest release metadata** — `<pm> view <parent> version repository time.modified`.
2. **Repository signals** — from the `repository` URL, run:
   - `gh api repos/<owner>/<repo>` → capture `archived`, `pushed_at`, `updated_at`, `open_issues_count`.
   - `gh search issues "<CVE-ID>" repo:<owner>/<repo> --state=all` → any existing discussion?
   - `gh search issues "<vuln-pkg>" repo:<owner>/<repo> --state=open` → any open issue mentioning the vulnerable dep?
   - `gh pr list --repo <owner>/<repo> --search "<vuln-pkg>" --state=all` → any open or merged PR bumping it?
3. **Classify the blocker** and report to the user:
   - **(a) Upstream PR exists** — link the PR and its state. Suggest: wait for merge, or (if urgent) pin to a git-URL fork of the merged branch.
   - **(b) Upstream issue exists, no PR** — link the issue and its age. Suggest: +1 the issue, or contribute a PR.
   - **(c) No discussion upstream, repo actively maintained** — propose opening an issue. Pre-fill a suggested title/body citing the CVE ID, vulnerable range, and patched version.
   - **(d) Unmaintained signals** — report each of the following that applies, verbatim, and let the user judge:
     - Repo `archived: true`
     - `pushed_at` older than 12 months
     - Latest npm release older than 12 months
     - No open-issues response from maintainers in the last 6 months
     Do **not** pronounce a package "abandoned" — state the facts and let the user decide.
   - **(d-alt) Known alternative package exists** — if the blocker is in a narrow category (e.g. `request` → `undici`/`node-fetch`, `node-sass` → `sass`), mention the alternative as context but do not recommend migrating without explicit user interest.

## Phase 5 — Present ranked options

Produce a single summary to the user with the applicable options only, in this order (skip any that don't apply to this CVE):

1. **Lockfile refresh** — if Phase 3 found at least one chain that auto-resolves.
   - Command: see matrix.
   - Side effects: lockfile only; no `package.json` change; full Dependabot visibility preserved.
2. **Bump a maintained intermediate parent** — if Phase 4 classified any blocker as (a) or found a newer parent version that admits the patch.
   - Command: bump the declared range of whatever pins that parent (often the user's own `package.json`).
   - Side effects: `package.json` minor diff; may need to review the parent's changelog.
3. **Switch to an alternative package** — only if the blocker is (d) + a well-known drop-in exists. Phrase as a significant codebase change; do not propose command-level automation for this.
4. **Package-manager override (last resort)** — only if all chains have hard blockers and none of options 1–3 apply or are acceptable.
   - Command: see matrix.
   - **Ask explicitly**: `"This will add an override that hides the issue from Dependabot/Renovate on future scans. Upstream blocker: <link>. Do you want to proceed? (yes/no)"`
   - Do not proceed without an explicit affirmative.

After presenting, ask: "Which option do you want to apply?" and wait.

## Phase 6 — Apply the chosen option

1. Run the PM-specific command(s) for the chosen option.
2. **Verify the fix**: re-grep the lockfile for every installed version of the vulnerable package, and assert each is outside every `vulnerable_version_range` from Phase 0.
3. Print a before/after table.
4. Suggest a commit message following conventional commits:
   - Option 1 (lockfile only): `fix(deps): bump <pkg> to patched versions (<CVE-ID>)`
   - Option 2 (parent bump): `fix(deps): bump <parent> to pick up patched <pkg> (<CVE-ID>)`
   - Option 4 (override): `fix(deps): override <pkg> to patched versions (<CVE-ID>)`
5. Do **not** create the branch, commit, or push. Leave that to the user.

## Package-manager command matrix

All commands assume the CWD is the project root.

| Concept | pnpm | npm | yarn (classic) | yarn (berry) | bun |
|---|---|---|---|---|---|
| Why is `<pkg>` installed | `pnpm why <pkg>` | `npm ls <pkg> --all` | `yarn why <pkg>` | `yarn why <pkg>` | `bun pm why <pkg>` |
| Update `<pkg>` within ranges | `pnpm update --depth Infinity <pkg>` (add `-r` for workspaces) | `npm update <pkg>` | `yarn upgrade <pkg>` | `yarn up <pkg>` | `bun update <pkg>` |
| Package metadata | `pnpm view <pkg>` | `npm view <pkg>` | `yarn info <pkg>` | `yarn npm info <pkg>` | `bun pm view <pkg>` (fallback: `npm view <pkg>`) |
| Latest version | `pnpm view <pkg> version` | `npm view <pkg> version` | `yarn info <pkg> version` | `yarn npm info <pkg> --json` | `bun pm view <pkg> version` |
| Override block in `package.json` | `"pnpm": { "overrides": { "<pkg>": "<range>" } }` | `"overrides": { "<pkg>": "<range>" }` | `"resolutions": { "<pkg>": "<range>" }` | `"resolutions": { "<pkg>": "<range>" }` | `"overrides": { "<pkg>": "<range>" }` |
| Apply override | `pnpm install` | `npm install` | `yarn install` | `yarn install` | `bun install` |

### Override syntax notes

- **pnpm** supports per-major overrides via `"<pkg>@<major>": "<range>"` (e.g. `"lodash@4": ">=4.17.21"`). Use this when multiple majors are installed and only some are vulnerable.
- **npm** supports nested overrides (`"<parent>": { "<pkg>": "<range>" }`). Prefer the flat form unless you need surgical scope.
- **yarn** resolutions support glob patterns (`"**/<pkg>": "<range>"`) — useful for workspace-wide application.

## Final checklist

Before reporting success:

- [ ] Every installed version of the vulnerable package is outside every `vulnerable_version_range`.
- [ ] If an override was used, the override is scoped to the minimum range required (not a global pin).
- [ ] A commit message is suggested but not executed.
- [ ] The Dependabot alert URL is referenced in the suggested commit body for traceability.

