--- 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 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///security/dependabot/`: run `gh api repos///dependabot/alerts/` to fetch the alert JSON. - **CVE ID** (`CVE-YYYY-NNNN`) or **GHSA ID** (`GHSA-*`): run `gh api /advisories/` 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 (` view 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+:` 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: ` view @latest dependencies.`. - 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 @ | @ | Auto-resolves / Needs parent bump to / Hard blocker ``` ## Phase 4 — Investigate hard-blocking parents For each parent in the "hard blocker" category, gather (all read-only): 1. **Latest release metadata** — ` view version repository time.modified`. 2. **Repository signals** — from the `repository` URL, run: - `gh api repos//` → capture `archived`, `pushed_at`, `updated_at`, `open_issues_count`. - `gh search issues "" repo:/ --state=all` → any existing discussion? - `gh search issues "" repo:/ --state=open` → any open issue mentioning the vulnerable dep? - `gh pr list --repo / --search "" --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: . 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 to patched versions ()` - Option 2 (parent bump): `fix(deps): bump to pick up patched ()` - Option 4 (override): `fix(deps): override to patched versions ()` 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 `` installed | `pnpm why ` | `npm ls --all` | `yarn why ` | `yarn why ` | `bun pm why ` | | Update `` within ranges | `pnpm update --depth Infinity ` (add `-r` for workspaces) | `npm update ` | `yarn upgrade ` | `yarn up ` | `bun update ` | | Package metadata | `pnpm view ` | `npm view ` | `yarn info ` | `yarn npm info ` | `bun pm view ` (fallback: `npm view `) | | Latest version | `pnpm view version` | `npm view version` | `yarn info version` | `yarn npm info --json` | `bun pm view version` | | Override block in `package.json` | `"pnpm": { "overrides": { "": "" } }` | `"overrides": { "": "" }` | `"resolutions": { "": "" }` | `"resolutions": { "": "" }` | `"overrides": { "": "" }` | | Apply override | `pnpm install` | `npm install` | `yarn install` | `yarn install` | `bun install` | ### Override syntax notes - **pnpm** supports per-major overrides via `"@": ""` (e.g. `"lodash@4": ">=4.17.21"`). Use this when multiple majors are installed and only some are vulnerable. - **npm** supports nested overrides (`"": { "": "" }`). Prefer the flat form unless you need surgical scope. - **yarn** resolutions support glob patterns (`"**/": ""`) — 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.