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 an action and a rationale.

The decision policy (deterministic, defensible)

ConditionAction
not reachable in your deploymentdefer (hygiene; never expedite an unreachable 9.8)
reachable and KEV-listedact-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, otherwisetrack

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.