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:
- Severity — CVSS base + temporal (intrinsic technical impact).
- Exploitability — EPSS probability, CISA KEV listing, public exploit/PoC, weaponization.
- Exposure — attack vector, internet-facing, network reachability, authentication/preconditions, compensating controls.
- 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)
- On CISA KEV AND reachable/exposed → P0 (drop-everything), SLA ≤ 24-72h.
- EPSS ≥ 0.5 AND internet-facing AND
AV:N → P0/P1.
- CVSS ≥ 9.0 AND internet-facing AND public weaponized exploit → P1.
- 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.
1---2name: vulnerability-triage3description: 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?".4license: MIT5---67# Vulnerability Triage89## Overview1011Vulnerability triage turns a flood of scanner findings or a single CVE into a12**ranked, justified, time-bound remediation decision**. CVSS alone is a poor13prioritizer: only a small fraction of CVEs are ever exploited, and a "Critical"14CVSS on an isolated internal box may matter far less than a "Medium" on an15internet-facing crown-jewel system.1617This skill scores each vulnerability across four dimensions and combines them:18191. **Severity** — CVSS base + temporal (intrinsic technical impact).202. **Exploitability** — EPSS probability, CISA KEV listing, public exploit/PoC, weaponization.213. **Exposure** — attack vector, internet-facing, network reachability, authentication/preconditions, compensating controls.224. **Business impact** — asset criticality, data sensitivity, blast radius, regulatory scope.2324Keywords: CVE, CVSS, EPSS, CISA KEV, exploitability, attack vector, remediation25SLA, patch priority, vulnerability management, risk score, false positive,26compensating control, SSVC, internet-facing, privilege escalation.2728## When to Use2930- Triaging output from Nessus / Qualys / Tenable / Trivy / Grype / Dependabot / Snyk.31- Deciding patch urgency for a newly disclosed CVE.32- Writing a vulnerability risk assessment or exception/risk-acceptance memo.33- Deduplicating and clustering scanner findings by root cause.34- Justifying *why* something is (or is not) urgent to leadership or auditors.3536## Workflow3738Follow these steps in order. For a single CVE, steps 2-5 take minutes; for a39scan, batch through `scripts/triage.py` first, then hand-review the top tier.4041### 1. Normalize and deduplicate the input42- Collapse duplicate findings: same CVE + same package/component on the same host43 is one finding instance; many hosts = one vuln with N instances.44- Group by **root cause** (e.g., one vulnerable `openssl` version → all dependent45 findings). Patching the root usually clears the cluster.46- Drop **confirmed false positives** (see references/false-positives.md) before scoring.4748### 2. Establish severity (CVSS)49- Capture the **CVSS v3.1/v4.0 base score** and the **vector string**, not just the number.50- Read the vector: `AV` (attack vector), `AC` (complexity), `PR` (privileges51 required), `UI` (user interaction), and impact metrics `C/I/A`.52- Apply **temporal** metrics if available (Exploit Code Maturity, Remediation53 Level). A high base with `E:U` (unproven) is less urgent than one with `E:H` (high/weaponized).54- See references/cvss-guide.md for vector decoding and common miscalibrations.5556### 3. Establish exploitability (the single biggest re-ranking signal)57Gather, in priority order:58- **CISA KEV**: Is the CVE on the Known Exploited Vulnerabilities catalog? If yes,59 it is being exploited in the wild now — treat as top priority regardless of CVSS.60- **EPSS**: Exploit Prediction Scoring System probability (0-1) of exploitation in61 the next 30 days. EPSS ≥ 0.5 is high; ≥ 0.1 is elevated.62- **Public exploit / PoC**: Exploit-DB, Metasploit module, GitHub PoC, Nuclei63 template. Weaponized + reliable > theoretical.64- **Ransomware / campaign association**: known use by ransomware crews escalates urgency.6566### 4. Establish exposure (environmental reality)67Answer for *your* environment, not the abstract CVE:68- **Internet-facing?** Reachable from untrusted networks?69- **Network reachability**: Can an attacker actually reach the vulnerable port/service/path?70- **Authentication / preconditions**: Does exploitation need valid creds, local71 access, a specific non-default config, or user interaction?72- **Compensating controls**: WAF rule, network segmentation, EDR, MFA, the73 feature being disabled, the package present but unused/dead code.74- **Is the vulnerable code path actually invoked?** (Reachability analysis for75 dependencies — a vulnerable function never called ≈ not exploitable.)7677### 5. Establish business impact78- **Asset criticality**: crown-jewel / production / internal / dev-test / decommissioning.79- **Data sensitivity**: regulated PII/PHI/PCI, secrets, IP, or low-value data.80- **Blast radius**: lateral movement potential, shared credentials, identity-tier81 (Tier 0 / domain controller / CI-CD / cloud control plane).82- **Regulatory / contractual** clocks (e.g., CISA BOD 22-01 deadlines for federal,83 PCI DSS timelines).8485### 6. Compute the composite priority and SLA86Use the decision framework below (or run `scripts/triage.py`). Map the result to a87**priority tier (P0-P4)** and a **remediation SLA**. Record the *rationale* — triage88is only defensible if the reasoning is written down.8990### 7. Decide the action91For each finding choose exactly one: **Patch now / Patch within SLA / Mitigate92(compensating control) / Accept risk (with expiry + owner) / False positive**.93Risk acceptance always needs an owner, a justification, and an expiry date.9495## Decision Framework9697### Fast-path overrides (apply first, in order)981. **On CISA KEV** AND reachable/exposed → **P0** (drop-everything), SLA ≤ 24-72h.992. **EPSS ≥ 0.5** AND internet-facing AND `AV:N` → **P0/P1**.1003. **CVSS ≥ 9.0** AND internet-facing AND public weaponized exploit → **P1**.1014. **Confirmed not reachable / dead code / not deployed** → downgrade to **P4** or102 mark false-positive — exposure gates everything else.103104### Composite scoring (when no fast-path fires)105Compute a 0-100 priority score (this is what `scripts/triage.py` implements):106107```108priority = 100 * normalize(109 0.30 * severity_norm # CVSS base / 10110 + 0.30 * exploitability_norm # max(EPSS, KEV=1.0, weaponized=0.9, PoC=0.6)111 + 0.25 * exposure_norm # internet/auth/controls-adjusted, 0..1112 + 0.15 * impact_norm # asset+data criticality, 0..1113)114```115116| Priority | Score band | Meaning | Default SLA |117|----------|-----------|---------|-------------|118| P0 | KEV-exposed or ≥ 90 | Active exploitation, exposed, high impact | 24-72 hours |119| P1 | 75-89 | Likely-exploitable, exposed | 7 days |120| P2 | 50-74 | Meaningful risk, moderate exposure | 30 days |121| P3 | 25-49 | Low likelihood or well-contained | 90 days |122| P4 | < 25 | Negligible / accept or backlog | Next maintenance cycle |123124This mirrors the spirit of **SSVC** (Stakeholder-Specific Vulnerability125Categorization): decisions driven by exploitation status, exposure, and impact —126not raw CVSS. See references/scoring-model.md for the full rationale, the SSVC127decision tree, and how to tune weights per organization.128129## Worked Example (condensed)130131CVE-2024-3094 (xz/liblzba backdoor), internal build server, not internet-facing,132sshd not exposed externally, package present:133- Severity: CVSS 10.0.134- Exploitability: KEV-adjacent, supply-chain, but exploit requires specific sshd+systemd path.135- Exposure: SSH only reachable from internal admin VLAN; affected versions present.136- Impact: build server = CI/CD = Tier-0-adjacent, high blast radius.137- Result: KEV/severity high but reachable internally + Tier-0 impact → **P0**,138 patch/downgrade immediately, rotate any secrets handled by the host.139140See examples/triage-walkthrough.md for three fully scored examples, including one141that gets *downgraded* by exposure and one false positive.142143## Best Practices144145- **Exploitability and exposure beat CVSS.** Always pull KEV + EPSS before ranking.146- **Score the instance in your environment**, not the abstract CVE. Reachability changes everything.147- **Cluster by root cause** — fix once, clear many.148- **Write the rationale.** A triage decision without a recorded "why" is not defensible.149- **Risk acceptance is a decision, not a default.** Owner + justification + expiry, always.150- **Re-triage on new signal.** EPSS and KEV change daily; a P3 can become P0 overnight.151- **Track SLA aging**, not just open counts.152153## Common Pitfalls154155- Sorting by CVSS alone and drowning in "Criticals" nobody can exploit.156- Treating a scanner "Critical" as ground truth without confirming the package is157 deployed and the version is actually vulnerable (version-range false positives).158- Ignoring KEV/EPSS because "the CVSS is only Medium" — KEV Mediums are real and exploited.159- Forgetting compensating controls and over-prioritizing already-mitigated issues.160- Counting transitive/dev-only dependencies as production exposure.161- Accepting risk with no expiry, so it silently lives forever.162163## Bundled Files164165- `references/cvss-guide.md` — decode CVSS v3.1/v4.0 vectors, temporal metrics, miscalibrations.166- `references/scoring-model.md` — composite model, SSVC tree, weight tuning, data sources.167- `references/false-positives.md` — confirming/ruling out findings, reachability analysis.168- `references/sla-policy-template.md` — ready-to-adopt SLA + risk-acceptance policy.169- `scripts/triage.py` — stdlib CLI that scores findings from JSON/CSV and emits a ranked report.170- `examples/triage-walkthrough.md` — three fully worked, scored examples.171- `templates/triage-report.md` — fill-in assessment / report template.