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
- Investigate before acting. Do not install packages, edit
package.json, or run update until you have walked the full chain and presented options.
- 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.
- 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.
- 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
- Read
$ARGUMENTS from the invocation.
- 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.
- 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
- 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?
- Grep the vulnerable package name in
package.json (dependencies, devDependencies, peerDependencies, and any workspace package.jsons).
- 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.
- If it's not direct → continue to Phase 3.
Phase 3 — Walk the dependency chain
Run the PM-specific "why" command and capture the full output.
Parse it into a flat list of (parent-package@parent-version, declared-range-for-vuln-pkg) tuples. Include every path, not just the first.
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.
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.
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):
- Latest release metadata —
<pm> view <parent> version repository time.modified.
- 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?
- 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):
- 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.
- 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.
- 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.
- 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
- Run the PM-specific command(s) for the chosen option.
- 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.
- Print a before/after table.
- 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>)
- 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:
1---2name: cve-doctor3description: 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.4---56# cve-doctor78You 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.910## Guiding principles11121. **Investigate before acting.** Do not install packages, edit `package.json`, or run `update` until you have walked the full chain and presented options.132. **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.143. **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.154. **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.1617## Phase 0 — Collect input18191. Read `$ARGUMENTS` from the invocation.202. Parse it:21 - **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.22 - **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.23 - **Empty**: ask the user for a Dependabot alert URL or CVE/GHSA ID. Do not proceed without one.243. From the fetched data, extract and display to the user:25 - Package name + ecosystem26 - Severity (CVSS score if available)27 - Short summary28 - `vulnerable_version_range[]` and `first_patched_version` for each range29 - Alert state (if applicable): `open`, `fixed`, `dismissed`, `auto_dismissed`304. **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.3132## Phase 1 — Detect the package manager3334Check the project root for lockfiles (in this priority order when multiple exist — but ask the user if ambiguous):3536| Lockfile | PM |37|---|---|38| `pnpm-lock.yaml` | pnpm |39| `yarn.lock` | yarn (check `package.json` → `packageManager` for classic vs berry) |40| `package-lock.json` | npm |41| `bun.lock` or `bun.lockb` | bun |4243Record the detected PM. All commands below branch on this value — use the matrix at the bottom of this file.4445## Phase 2 — Is this a direct dependency?46471. Grep the vulnerable package name in `package.json` (`dependencies`, `devDependencies`, `peerDependencies`, and any workspace package.jsons).482. If it's a direct dep:49 - Check whether the latest version on the registry (`<pm> view <pkg> version`) is outside all vulnerable ranges.50 - Check whether our declared semver range admits a patched version.51 - 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.52 - 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.533. If it's **not** direct → continue to Phase 3.5455## Phase 3 — Walk the dependency chain56571. Run the PM-specific "why" command and capture the full output.582. Parse it into a flat list of `(parent-package@parent-version, declared-range-for-vuln-pkg)` tuples. Include every path, not just the first.593. 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.604. For each vulnerable installed version, and for each immediate parent:61 - Look up the parent's **latest published version** and its declared range for the vulnerable package: `<pm> view <parent>@latest dependencies.<vuln-pkg>`.62 - Look up the parent's **currently-installed version**'s declared range.63 - Classify the parent:64 - **Auto-resolves on refresh**: the installed parent's declared range already admits a patched version (the lockfile is simply stale).65 - **Fixed in newer parent version**: current parent blocks, but latest parent admits a patch.66 - **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.675. Print a compact summary table:6869 ```70 Vulnerable version | Parent | Status71 <pkg>@<v> | <parent>@<v> | Auto-resolves / Needs parent bump to <v'> / Hard blocker72 ```7374## Phase 4 — Investigate hard-blocking parents7576For each parent in the "hard blocker" category, gather (all read-only):77781. **Latest release metadata** — `<pm> view <parent> version repository time.modified`.792. **Repository signals** — from the `repository` URL, run:80 - `gh api repos/<owner>/<repo>` → capture `archived`, `pushed_at`, `updated_at`, `open_issues_count`.81 - `gh search issues "<CVE-ID>" repo:<owner>/<repo> --state=all` → any existing discussion?82 - `gh search issues "<vuln-pkg>" repo:<owner>/<repo> --state=open` → any open issue mentioning the vulnerable dep?83 - `gh pr list --repo <owner>/<repo> --search "<vuln-pkg>" --state=all` → any open or merged PR bumping it?843. **Classify the blocker** and report to the user:85 - **(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.86 - **(b) Upstream issue exists, no PR** — link the issue and its age. Suggest: +1 the issue, or contribute a PR.87 - **(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.88 - **(d) Unmaintained signals** — report each of the following that applies, verbatim, and let the user judge:89 - Repo `archived: true`90 - `pushed_at` older than 12 months91 - Latest npm release older than 12 months92 - No open-issues response from maintainers in the last 6 months93 Do **not** pronounce a package "abandoned" — state the facts and let the user decide.94 - **(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.9596## Phase 5 — Present ranked options9798Produce a single summary to the user with the applicable options only, in this order (skip any that don't apply to this CVE):991001. **Lockfile refresh** — if Phase 3 found at least one chain that auto-resolves.101 - Command: see matrix.102 - Side effects: lockfile only; no `package.json` change; full Dependabot visibility preserved.1032. **Bump a maintained intermediate parent** — if Phase 4 classified any blocker as (a) or found a newer parent version that admits the patch.104 - Command: bump the declared range of whatever pins that parent (often the user's own `package.json`).105 - Side effects: `package.json` minor diff; may need to review the parent's changelog.1063. **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.1074. **Package-manager override (last resort)** — only if all chains have hard blockers and none of options 1–3 apply or are acceptable.108 - Command: see matrix.109 - **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)"`110 - Do not proceed without an explicit affirmative.111112After presenting, ask: "Which option do you want to apply?" and wait.113114## Phase 6 — Apply the chosen option1151161. Run the PM-specific command(s) for the chosen option.1172. **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.1183. Print a before/after table.1194. Suggest a commit message following conventional commits:120 - Option 1 (lockfile only): `fix(deps): bump <pkg> to patched versions (<CVE-ID>)`121 - Option 2 (parent bump): `fix(deps): bump <parent> to pick up patched <pkg> (<CVE-ID>)`122 - Option 4 (override): `fix(deps): override <pkg> to patched versions (<CVE-ID>)`1235. Do **not** create the branch, commit, or push. Leave that to the user.124125## Package-manager command matrix126127All commands assume the CWD is the project root.128129| Concept | pnpm | npm | yarn (classic) | yarn (berry) | bun |130|---|---|---|---|---|---|131| Why is `<pkg>` installed | `pnpm why <pkg>` | `npm ls <pkg> --all` | `yarn why <pkg>` | `yarn why <pkg>` | `bun pm why <pkg>` |132| 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>` |133| Package metadata | `pnpm view <pkg>` | `npm view <pkg>` | `yarn info <pkg>` | `yarn npm info <pkg>` | `bun pm view <pkg>` (fallback: `npm view <pkg>`) |134| Latest version | `pnpm view <pkg> version` | `npm view <pkg> version` | `yarn info <pkg> version` | `yarn npm info <pkg> --json` | `bun pm view <pkg> version` |135| Override block in `package.json` | `"pnpm": { "overrides": { "<pkg>": "<range>" } }` | `"overrides": { "<pkg>": "<range>" }` | `"resolutions": { "<pkg>": "<range>" }` | `"resolutions": { "<pkg>": "<range>" }` | `"overrides": { "<pkg>": "<range>" }` |136| Apply override | `pnpm install` | `npm install` | `yarn install` | `yarn install` | `bun install` |137138### Override syntax notes139140- **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.141- **npm** supports nested overrides (`"<parent>": { "<pkg>": "<range>" }`). Prefer the flat form unless you need surgical scope.142- **yarn** resolutions support glob patterns (`"**/<pkg>": "<range>"`) — useful for workspace-wide application.143144## Final checklist145146Before reporting success:147148- [ ] Every installed version of the vulnerable package is outside every `vulnerable_version_range`.149- [ ] If an override was used, the override is scoped to the minimum range required (not a global pin).150- [ ] A commit message is suggested but not executed.151- [ ] The Dependabot alert URL is referenced in the suggested commit body for traceability.