---
name: cve-triage
description: Pull CVEs against the current dependency set (osv.dev / GHSA) and classify each as exploitable / theoretical / not-applicable
allowed-tools: Bash Read
argument-hint: "[--source osv|ghsa|both] [--baseline <file>] [--severity-min low|medium|high|critical]"
mode: [audit]
---

# CVE Triage

## Purpose

Query vulnerability databases (osv.dev primary, GHSA secondary)
against the project's locked dependency set, and classify each finding
as **exploitable** (the project uses the vulnerable code path),
**theoretical** (the dep is present but the affected function isn't
called), or **not-applicable** (CVE filed against a different
ecosystem / version / platform). Primary consumer:
`supply-chain-auditor`. Output feeds `sbom-generate --include-vex`.

## Scope

- Reads the lockfile, queries osv.dev's batch API and (optionally)
  GHSA via `gh api`, deduplicates findings.
- For each CVE, attempts to determine reachability — is the
  vulnerable function actually called from the project's code?
  Reachability analysis is best-effort; the skill notes its
  confidence level.
- Emits both a markdown triage report (for humans) and a VEX-shaped
  JSON file at `.claude/cve-triage-latest.json` (for `sbom-generate`).
- Tracks decisions across runs via a baseline so previously-triaged
  CVEs don't re-litigate.

## When to use

- On every dep-update PR, as a CI gate (block on critical /
  exploitable; warn on high).
- Weekly against the current `main` lockfile, to catch CVEs filed
  after the last update.
- Before a release, with the release's locked deps.
- After a public CVE announcement that mentions a dep the project
  uses (run on-demand with `--severity-min critical`).

## When NOT to use

- For zero-day discovery — this skill triages published CVEs only.
  Use `red-team` / fuzzing / source review for novel issues.
- As the only supply-chain check — pair with `license-audit`,
  `sbom-generate`, and dep-pinning.
- For container-image CVEs (OS packages, base images) — use `trivy
  image` / `grype <image>` against the built image; this skill
  scans the source-tree dep set only.

## Automated pass

1. Resolve lockfile + ecosystem (shared logic with `sbom-generate`).

2. Build the osv.dev batch query. osv.dev accepts up to 1000 packages
   per call; chunk if needed:
   ```sh
   # Convert lockfile → osv batch input
   # (one ecosystem-shaped entry per dep)
   case "$lockfile" in
       package-lock.json|pnpm-lock.yaml)
           jq -r '...npm shape...' "$lockfile" > /tmp/osv-query.json ;;
       Cargo.lock)
           # parse TOML → JSON
           ;;
       go.sum) ;;
       poetry.lock) ;;
   esac

   curl -fsS https://api.osv.dev/v1/querybatch \
       -H 'Content-Type: application/json' \
       -d @/tmp/osv-query.json > /tmp/osv-results.json
   ```

3. (Optional) GHSA cross-check via `gh api`:
   ```sh
   if [ "${SOURCE:-both}" != "osv" ] && command -v gh >/dev/null; then
       gh api graphql -f query='...securityAdvisories...' \
           > /tmp/ghsa-results.json
   fi
   ```

4. Merge + dedupe by CVE id (osv records carry GHSA aliases; prefer
   osv as source of truth, fall back to GHSA where osv lacks data).

5. **Reachability analysis** (per finding):
   - Look up the affected function(s) in the advisory metadata
     (osv `affected[].ecosystem_specific.imports` or the GHSA
     `vulnerable_functions` field where present).
   - `grep` the project source for imports / call sites of those
     functions.
   - Classify:
     - **exploitable** — vulnerable function is imported AND
       called from project code OR advisory has no function-level
       data (default-conservative).
     - **theoretical** — vulnerable function is in the dep but
       not imported by the project (transitive-only, unused
       sub-module).
     - **not-applicable** — version range doesn't match (osv
       false-positive on pre-release tags), wrong ecosystem (npm
       advisory matched a python pkg with same name — happens),
       platform-specific to OS/arch the project doesn't ship to.

   Reachability is heuristic: record `reachability_confidence:
   high|medium|low` per finding so reviewers know which to
   re-check by hand.

6. Diff against baseline (`.claude/cve-triage-baseline.json`).
   Previously-triaged CVEs with explicit decisions
   (`accepted`, `mitigated-via-config`, `false-positive`) carry
   forward unless the affected version range changed.

7. Compose markdown report:
   - Summary: counts by severity × classification.
   - Critical / exploitable: full detail, reproduction or
     advisory link, suggested upgrade target.
   - High / exploitable: full detail.
   - Theoretical / not-applicable: collapsed list with reason
     codes.
   - Newly-introduced findings since baseline (with the dep
     version that introduced them).
   - Resolved findings since baseline (with the upgrade that
     fixed them).

8. Emit VEX JSON at `.claude/cve-triage-latest.json` in CycloneDX
   VEX 1.6 shape:
   ```json
   {
     "vulnerabilities": [
       {
         "id": "CVE-2026-NNNNN",
         "ratings": [...],
         "affects": [{"ref": "pkg:npm/foo@1.2.3"}],
         "analysis": {
           "state": "exploitable|in_triage|not_affected|resolved",
           "justification": "code_not_present|code_not_reachable|...",
           "detail": "..."
         }
       }
     ]
   }
   ```

9. Exit code:
   - 0 if no exploitable critical/high.
   - 1 if exploitable critical/high present.
   - 2 if any finding has `reachability_confidence: low` AND no
     baseline decision (forces a human triage pass).

## Manual pass

For an immediate read on the current state:

```sh
osv-scanner --lockfile=package-lock.json
osv-scanner --lockfile=poetry.lock
osv-scanner --lockfile=Cargo.lock
osv-scanner --recursive .                 # multi-ecosystem repos
```

Or to investigate one CVE:

```sh
curl -fsS https://api.osv.dev/v1/vulns/CVE-2026-NNNNN | jq
gh api graphql -f query='{ securityAdvisory(ghsaId: "GHSA-xxxx-xxxx-xxxx") { ... } }'
```

## Known gotchas

- **CVE != exploitable.** A naive count of CVEs against deps will
  scare anyone with a large dep tree. The reachability classification
  is the value-add — surface "exploitable" prominently and
  "theoretical" quietly.
- **Reachability is heuristic.** Dynamic dispatch, reflection, and
  callbacks defeat static call-graph analysis. A `low`-confidence
  reachability classification is an invitation to manual review,
  not a verdict.
- **CVE database lag.** osv.dev typically lags GHSA by hours; GHSA
  lags CVE.org by days; CVE.org lags vendor disclosure by weeks.
  For zero-days that haven't reached osv yet, the skill misses
  them. Pair with vendor mailing-list monitoring.
- **Disputed CVEs.** Some CVEs are disputed by the upstream
  maintainer (filed by a security researcher, contested as
  not-a-bug). Honor the upstream's position by default; flag the
  dispute in the report so the operator can decide.
- **Version range gotchas.** Pre-release semver tags
  (`1.0.0-rc.1`) compare oddly. osv's matcher is correct; ad-hoc
  string comparison isn't. Always defer to the database's
  range-evaluation, don't reimplement.
- **Internal forks.** A dep forked into the monorepo with a custom
  patch may carry the original purl but not the vulnerability.
  Add to `.claude/cve-triage-overrides.json` with the patch
  reference.
- **VEX justifications are constrained.** CycloneDX VEX requires
  one of: `code_not_present`, `code_not_reachable`,
  `requires_configuration`, `requires_dependency`,
  `requires_environment`, `protected_by_compiler`,
  `protected_at_runtime`, `protected_at_perimeter`,
  `protected_by_mitigating_control`. Map the skill's classification
  to the closest justification; "theoretical" → `code_not_reachable`.

## References

- `lib/agents/supply-chain-auditor.md` — primary consumer.
- `lib/skills/sbom-generate/SKILL.md` — consumes VEX output.
- `lib/skills/license-audit/SKILL.md` — sibling supply-chain skill.
- osv.dev API: https://google.github.io/osv.dev/api/
- GHSA via `gh api`: https://docs.github.com/en/graphql
- CycloneDX VEX: https://cyclonedx.org/capabilities/vex/
- `.claude/cve-triage-baseline.json` — carry-forward decisions.
- `.claude/cve-triage-overrides.json` — internal-fork / known-FP list.
