Lab 01 — Vulnerability Triage Engine (CVSS / EPSS / KEV)
Frameworks: FIRST CVSS v3.1, FIRST EPSS, CISA KEV, SSVC-style decision. Kind: scoring system. Builds toward: Phase 08 Chapter 13.
The problem (and the threat model)
A scanner produces thousands of findings; the dangerous move is to sort them by CVSS base score and "patch criticals first." CVSS base score measures worst-case severity if exploited — it knows nothing about whether anyone is exploiting the bug, or whether the vulnerable code is even reachable in your deployment. The "attacker" in this lab is misprioritization: the team burns its remediation budget on a 9.8 nobody exploits and that you don't expose, while a KEV-listed, internet-facing, reachable Medium — the one actually being used against your peers — sits in the backlog. The control is a triage engine that scores risk on four axes, not one.
finding ─► CVSS base score (severity) ─┐
─► EPSS probability (likelihood) ─┤
─► CISA KEV (confirmed exploited)─┼─► deterministic decision ─► ordered queue
─► reachability + exposure (you) ─┘ act-now / scheduled / track / defer
What you build (lab.py)
parse_cvss_vector(vector)— parse and validate a CVSS v3.0/3.1 base vector (reject wrong prefix, missing metric, invalid value).roundup(value)— the official CVSS v3.1 Roundup (a float-safe one-decimal ceiling —5.43 → 5.5).cvss_base_score(vector)— the real formula: ISS → Impact (with the Scope rule) → Exploitability → Base. Validated against the canonical vectors (…C:H/I:H/A:H= 9.8; Scope-Changed = 10.0).severity_band(score)— None / Low / Medium / High / Critical.triage(findings)— combine severity with EPSS, KEV, and reachability into a deterministic, highest-risk-first ordered queue, each with anactionand arationale.
The decision policy (deterministic, defensible)
| Condition | Action |
|---|---|
| not reachable in your deployment | defer (hygiene; never expedite an unreachable 9.8) |
| reachable and KEV-listed | act-now (confirmed in-the-wild on a reachable component) |
| reachable, internet-facing, and (base ≥ 9.0 or EPSS ≥ 0.5) | act-now |
| reachable and (base ≥ 7.0 or EPSS ≥ 0.1) | scheduled |
| reachable, otherwise | track |
This is a small SSVC (Stakeholder-Specific Vulnerability Categorization) tree: the framework's point is to make the contextual questions explicit instead of hiding behind a single number.
Attack / failure cases the tests cover
- Severity ≠ risk: a KEV-listed reachable Medium outranks an unreachable Critical 9.8.
- Unreachable always defers even at EPSS 0.99 + KEV (you can't be exploited through code you don't run).
- CVSS formula correctness against canonical FIRST vectors, including the Scope-Changed branch and the Roundup ceiling.
- Vector validation: wrong prefix, missing metric, invalid value →
ValueError. - Determinism: identical input yields identical order across runs (stable tiebreak by CVE).
Run
pip install -r requirements.txt
LAB_MODULE=solution pytest -q # reference (passes)
pytest -q # your implementation after the TODOs
Hardening / extensions
- Add CVSS v4.0 (new metric groups: Attack Requirements, the Threat and Supplemental groups).
- Replace the threshold policy with the full SSVC decision tree (Exploitation / Automatable / Technical Impact / Mission & Well-being) and emit the Act/Attend/Track/Track* decision.
- Pull EPSS and KEV from their published feeds (offline snapshot) and add freshness checks.
- Add reachability evidence (an SCA call-graph result) as a first-class input rather than a bool.
Interview / resume
"Built a vulnerability triage engine implementing the FIRST CVSS v3.1 base-score algorithm and an SSVC-style decision that prioritizes by CVSS × EPSS × KEV × reachability — demonstrating that severity is not risk and that an unreachable 9.8 must wait behind a KEV-listed, reachable Medium."
Limitations: base-metric CVSS only (no Temporal/Environmental re-scoring beyond the reachability gate); EPSS/KEV/reachability are supplied as inputs; the policy thresholds are a defensible default, not an organization-specific risk appetite.