| name | cve-doctor |
| description | 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. |
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: