Pull CVEs against the current dependency set (osv.dev / GHSA) and classify each as exploitable / theoretical / not-applicable. Use during a security review, before release, or when a new advisory lands on a dependency.
Installs into .claude/skills of the current project.
Are you the author of Cve Triage?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/bakw00ds-cve-triage)
---
name: cve-triage
description: Pull CVEs against the current dependency set (osv.dev / GHSA) and classify each as exploitable / theoretical / not-applicable. Use during a security review, before release, or when a new advisory lands on a dependency.
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.