---
name: vulnerability-triage
description: Triages and prioritizes security vulnerabilities (CVEs) by combining CVSS base/temporal scores, real-world exploitability (EPSS, KEV, public PoCs), environmental exposure (internet-facing, network reachability, authentication), and business impact (data sensitivity, asset criticality) into a defensible, ranked remediation plan with SLAs. Use this skill when asked to triage CVEs, prioritize a vulnerability scan, assess whether a CVE is exploitable or reachable in a given environment, decide patch urgency, set remediation SLAs, write a vulnerability risk assessment, deduplicate scanner findings, or answer "how bad is CVE-XXXX-YYYYY for us?" / "should we drop everything to patch this?".
license: MIT
---

# Vulnerability Triage

## Overview

Vulnerability triage turns a flood of scanner findings or a single CVE into a
**ranked, justified, time-bound remediation decision**. CVSS alone is a poor
prioritizer: only a small fraction of CVEs are ever exploited, and a "Critical"
CVSS on an isolated internal box may matter far less than a "Medium" on an
internet-facing crown-jewel system.

This skill scores each vulnerability across four dimensions and combines them:

1. **Severity** — CVSS base + temporal (intrinsic technical impact).
2. **Exploitability** — EPSS probability, CISA KEV listing, public exploit/PoC, weaponization.
3. **Exposure** — attack vector, internet-facing, network reachability, authentication/preconditions, compensating controls.
4. **Business impact** — asset criticality, data sensitivity, blast radius, regulatory scope.

Keywords: CVE, CVSS, EPSS, CISA KEV, exploitability, attack vector, remediation
SLA, patch priority, vulnerability management, risk score, false positive,
compensating control, SSVC, internet-facing, privilege escalation.

## When to Use

- Triaging output from Nessus / Qualys / Tenable / Trivy / Grype / Dependabot / Snyk.
- Deciding patch urgency for a newly disclosed CVE.
- Writing a vulnerability risk assessment or exception/risk-acceptance memo.
- Deduplicating and clustering scanner findings by root cause.
- Justifying *why* something is (or is not) urgent to leadership or auditors.

## Workflow

Follow these steps in order. For a single CVE, steps 2-5 take minutes; for a
scan, batch through `scripts/triage.py` first, then hand-review the top tier.

### 1. Normalize and deduplicate the input
- Collapse duplicate findings: same CVE + same package/component on the same host
  is one finding instance; many hosts = one vuln with N instances.
- Group by **root cause** (e.g., one vulnerable `openssl` version → all dependent
  findings). Patching the root usually clears the cluster.
- Drop **confirmed false positives** (see references/false-positives.md) before scoring.

### 2. Establish severity (CVSS)
- Capture the **CVSS v3.1/v4.0 base score** and the **vector string**, not just the number.
- Read the vector: `AV` (attack vector), `AC` (complexity), `PR` (privileges
  required), `UI` (user interaction), and impact metrics `C/I/A`.
- Apply **temporal** metrics if available (Exploit Code Maturity, Remediation
  Level). A high base with `E:U` (unproven) is less urgent than one with `E:H` (high/weaponized).
- See references/cvss-guide.md for vector decoding and common miscalibrations.

### 3. Establish exploitability (the single biggest re-ranking signal)
Gather, in priority order:
- **CISA KEV**: Is the CVE on the Known Exploited Vulnerabilities catalog? If yes,
  it is being exploited in the wild now — treat as top priority regardless of CVSS.
- **EPSS**: Exploit Prediction Scoring System probability (0-1) of exploitation in
  the next 30 days. EPSS ≥ 0.5 is high; ≥ 0.1 is elevated.
- **Public exploit / PoC**: Exploit-DB, Metasploit module, GitHub PoC, Nuclei
  template. Weaponized + reliable > theoretical.
- **Ransomware / campaign association**: known use by ransomware crews escalates urgency.

### 4. Establish exposure (environmental reality)
Answer for *your* environment, not the abstract CVE:
- **Internet-facing?** Reachable from untrusted networks?
- **Network reachability**: Can an attacker actually reach the vulnerable port/service/path?
- **Authentication / preconditions**: Does exploitation need valid creds, local
  access, a specific non-default config, or user interaction?
- **Compensating controls**: WAF rule, network segmentation, EDR, MFA, the
  feature being disabled, the package present but unused/dead code.
- **Is the vulnerable code path actually invoked?** (Reachability analysis for
  dependencies — a vulnerable function never called ≈ not exploitable.)

### 5. Establish business impact
- **Asset criticality**: crown-jewel / production / internal / dev-test / decommissioning.
- **Data sensitivity**: regulated PII/PHI/PCI, secrets, IP, or low-value data.
- **Blast radius**: lateral movement potential, shared credentials, identity-tier
  (Tier 0 / domain controller / CI-CD / cloud control plane).
- **Regulatory / contractual** clocks (e.g., CISA BOD 22-01 deadlines for federal,
  PCI DSS timelines).

### 6. Compute the composite priority and SLA
Use the decision framework below (or run `scripts/triage.py`). Map the result to a
**priority tier (P0-P4)** and a **remediation SLA**. Record the *rationale* — triage
is only defensible if the reasoning is written down.

### 7. Decide the action
For each finding choose exactly one: **Patch now / Patch within SLA / Mitigate
(compensating control) / Accept risk (with expiry + owner) / False positive**.
Risk acceptance always needs an owner, a justification, and an expiry date.

## Decision Framework

### Fast-path overrides (apply first, in order)
1. **On CISA KEV** AND reachable/exposed → **P0** (drop-everything), SLA ≤ 24-72h.
2. **EPSS ≥ 0.5** AND internet-facing AND `AV:N` → **P0/P1**.
3. **CVSS ≥ 9.0** AND internet-facing AND public weaponized exploit → **P1**.
4. **Confirmed not reachable / dead code / not deployed** → downgrade to **P4** or
   mark false-positive — exposure gates everything else.

### Composite scoring (when no fast-path fires)
Compute a 0-100 priority score (this is what `scripts/triage.py` implements):

```
priority = 100 * normalize(
      0.30 * severity_norm        # CVSS base / 10
    + 0.30 * exploitability_norm  # max(EPSS, KEV=1.0, weaponized=0.9, PoC=0.6)
    + 0.25 * exposure_norm        # internet/auth/controls-adjusted, 0..1
    + 0.15 * impact_norm          # asset+data criticality, 0..1
)
```

| Priority | Score band | Meaning | Default SLA |
|----------|-----------|---------|-------------|
| P0 | KEV-exposed or ≥ 90 | Active exploitation, exposed, high impact | 24-72 hours |
| P1 | 75-89 | Likely-exploitable, exposed | 7 days |
| P2 | 50-74 | Meaningful risk, moderate exposure | 30 days |
| P3 | 25-49 | Low likelihood or well-contained | 90 days |
| P4 | < 25 | Negligible / accept or backlog | Next maintenance cycle |

This mirrors the spirit of **SSVC** (Stakeholder-Specific Vulnerability
Categorization): decisions driven by exploitation status, exposure, and impact —
not raw CVSS. See references/scoring-model.md for the full rationale, the SSVC
decision tree, and how to tune weights per organization.

## Worked Example (condensed)

CVE-2024-3094 (xz/liblzba backdoor), internal build server, not internet-facing,
sshd not exposed externally, package present:
- Severity: CVSS 10.0.
- Exploitability: KEV-adjacent, supply-chain, but exploit requires specific sshd+systemd path.
- Exposure: SSH only reachable from internal admin VLAN; affected versions present.
- Impact: build server = CI/CD = Tier-0-adjacent, high blast radius.
- Result: KEV/severity high but reachable internally + Tier-0 impact → **P0**,
  patch/downgrade immediately, rotate any secrets handled by the host.

See examples/triage-walkthrough.md for three fully scored examples, including one
that gets *downgraded* by exposure and one false positive.

## Best Practices

- **Exploitability and exposure beat CVSS.** Always pull KEV + EPSS before ranking.
- **Score the instance in your environment**, not the abstract CVE. Reachability changes everything.
- **Cluster by root cause** — fix once, clear many.
- **Write the rationale.** A triage decision without a recorded "why" is not defensible.
- **Risk acceptance is a decision, not a default.** Owner + justification + expiry, always.
- **Re-triage on new signal.** EPSS and KEV change daily; a P3 can become P0 overnight.
- **Track SLA aging**, not just open counts.

## Common Pitfalls

- Sorting by CVSS alone and drowning in "Criticals" nobody can exploit.
- Treating a scanner "Critical" as ground truth without confirming the package is
  deployed and the version is actually vulnerable (version-range false positives).
- Ignoring KEV/EPSS because "the CVSS is only Medium" — KEV Mediums are real and exploited.
- Forgetting compensating controls and over-prioritizing already-mitigated issues.
- Counting transitive/dev-only dependencies as production exposure.
- Accepting risk with no expiry, so it silently lives forever.

## Bundled Files

- `references/cvss-guide.md` — decode CVSS v3.1/v4.0 vectors, temporal metrics, miscalibrations.
- `references/scoring-model.md` — composite model, SSVC tree, weight tuning, data sources.
- `references/false-positives.md` — confirming/ruling out findings, reachability analysis.
- `references/sla-policy-template.md` — ready-to-adopt SLA + risk-acceptance policy.
- `scripts/triage.py` — stdlib CLI that scores findings from JSON/CSV and emits a ranked report.
- `examples/triage-walkthrough.md` — three fully worked, scored examples.
- `templates/triage-report.md` — fill-in assessment / report template.
