A Bayesian framework that converts the global exploitation probabilities of an exploit likelihood model (ELM) such as EPSS into a defender's own daily exploit hazard rate, aggregated from individual vulnerability instances up to the whole organization.
This repository is the research artifact for the paper Modeling Local Exploit Hazard — A Bayesian Framework for Quantifying Exploit Risk and Operational Efficiency (arXiv:2607.24618): standalone notebooks that implement the model end to end, the supporting modules that hold the paper's equations, and the scripts that build the public datasets the analysis runs on — including the Weibull calibration pipeline that fits the hazard shape parameter from observed exploitation timing.
The paper is the source of truth for the model. Read it alongside the code: every module and notebook cites the section or equation it implements.
Given my set of exposed vulnerabilities, how many exploit events should I expect in a given timeframe, and how can I efficiently reduce that number?
The model measures the exploit-prevention effectiveness of deployed controls as a probability distribution (seeded from a subject-matter-expert opinion pool, updated by Beta-Binomial inference), applies it to ELM scores by attack-vector alignment, and converts the result to a hazard rate using standard survival-analysis techniques. Because hazards are additive under independence, per-vulnerability rates sum up to host, network, business unit, and organization. Candidate remediation actions are then ranked by the hazard they remove.
| Level | Output | Paper |
|---|---|---|
| Control effectiveness | Beta posterior per control (SME pool → telemetry update) | §3.1 |
| Applied likelihood | EL_ic = EL_i (1 - CE), aligned by CVSS attack vector |
§3.2 |
| Host aggregation | EL_g = 1 - ∏(1 - EL_ic) |
§3.4 |
| Hazard | h = -ln(1 - EL_g) / t_ELM; exponential or KEV-calibrated Weibull |
§3.5 |
| Scaling | H_agg = Σ h_i to any grouping |
§3.6 |
| Remediation | actions ranked by ΔH_agg removed |
§3.7 |
.
├── LICENSE
└── code/
├── requirements.txt # all Python dependencies
├── notebooks/
│ ├── exploit_hazard_end_to_end.ipynb # full pipeline, Level 1 → remediation
│ ├── sensitivity_analysis.ipynb # sensitivity analysis on real EPSS/CVE data
│ └── REQUIREMENTS.md # environment, run guide, scope
└── scripts/
├── hazard_model.py # paper equations as pure functions
├── synthetic_org.py # reproducible ~3,000-host synthetic inventory
├── sensitivity_toolkit.py # real-data organization, layered controls, SA methods
├── download_kev.py # fetch the CISA KEV catalog
├── enrich_cve_bundle.py # add NVD dates + KEV timing → time-to-exploit
├── calibrate_weibull.py # fit the Weibull shape parameter k from TTE
├── build_real_cve_bundle.py # assemble the real-CVE bundle (EPSS + CVSS + KEV)
├── test_hazard_model.py # tests for the paper's equations
├── test_sensitivity_toolkit.py # tests for the sensitivity toolkit
└── README.md # script details
No data ships with this repository. The scripts in code/scripts/ build it
from public sources (FIRST/EPSS, the CVE List, CISA KEV, NVD) into a gitignored
code/data/ directory. See Data below.
From the repository root:
python -m venv .venv
source .venv/bin/activate
pip install -r code/requirements.txtPython 3.9 or newer.
code/notebooks/exploit_hazard_end_to_end.ipynb
runs the full pipeline on a seeded synthetic organization. It needs no data, no
network, no API keys — every input is either a literal in the notebook or drawn
from a seeded RNG. The notebook locates the repository itself, so it runs from any
working directory:
jupyter notebook code/notebooks/exploit_hazard_end_to_end.ipynbor execute headless:
jupyter nbconvert --to notebook --execute --inplace \
code/notebooks/exploit_hazard_end_to_end.ipynbRuntime is well under a minute. Figures render inline — the notebooks write nothing to disk. See code/notebooks/REQUIREMENTS.md for the full run guide and scope.
code/notebooks/sensitivity_analysis.ipynb
runs a thorough sensitivity analysis of the model — one-at-a-time tornado,
elasticities, layered-control ablation, Monte-Carlo uncertainty propagation, an
exact per-instance hazard decomposition, variance-based global sensitivity
(Sobol + PRCC with bootstrap CIs), and decision-robustness of the remediation
ranking.
It is grounded in real data: real EPSS scores, real CVEs, real CVSS attack vectors, and real KEV membership, mapped onto a synthetic device inventory with multiple layered controls composed per host. A real vulnerability inventory is proprietary; the synthetic one stands in for it.
This notebook requires the CVE bundle, which you build first (see Data):
python code/scripts/build_real_cve_bundle.pypython -m pytest code/scripts/Covers the hazard equations (exponential/Weibull agreement at k = 1, scale
anchoring, aggregation round-trips, the calibrated shape constant) and the
sensitivity toolkit. Tests that need the CVE bundle skip cleanly when it has not
been built.
Nothing in code/data/ is committed — the directory is gitignored and built
on demand from public sources. Two independent pipelines produce it, and both
require network access.
Keeping the data out of the repository means the artifact stays small and every
input is traceable to its upstream source rather than to a snapshot of unclear
provenance. The cost is that you build it yourself. Reproducibility is preserved
by pinning the snapshot date: --date 2026-07-23 is the snapshot the paper uses,
and the EPSS archive is immutable per day, so that flag reproduces the paper's
inputs exactly.
python code/scripts/build_real_cve_bundle.pyAssembles real EPSS scores (FIRST/EPSS daily archive), real CVSS attack vectors
and real KEV membership (CVE List CVEProject/cvelistV5, including the CISA-ADP
type: kev metric) into code/data/real_cve_bundle.csv. Nothing in the bundle is
fabricated or imputed.
To reproduce the paper's inputs exactly:
python code/scripts/build_real_cve_bundle.py --date 2026-07-23 --extra 5500All sources are fetched from GitHub, not the NVD API, so there is no API key and
no rate-limit wait. Expect a few minutes for the paper-sized bundle; --extra
and --cap trade size for speed.
The shape parameter k is fitted from CISA KEV timing — the interval between a
vulnerability's disclosure and its first confirmed exploitation. The paper's
calibrated value, k ≈ 0.605, is a constant in
code/scripts/hazard_model.py, so the notebooks
do not need this pipeline; run it only to recalibrate:
python code/scripts/download_kev.py # fetch the KEV catalog
python code/scripts/enrich_cve_bundle.py # compute time-to-exploit per CVE
python code/scripts/calibrate_weibull.py # fit k, write weibull_calibration.jsonSee code/scripts/README.md for details on both pipelines.
Every stochastic step draws from a seeded numpy.random.Generator
(SEED = 20260723). Re-running reproduces the same organization, the same pooled
statistics, and the same figures. Changing the seed changes the synthetic
organization but must not change any qualitative conclusion — if it does, that is
a finding worth reporting, not noise to reroll.
The notebooks assert rather than narrate: each stage checks its own output, so a broken run stops instead of quietly producing a plausible-looking figure.
MIT — see LICENSE. This covers everything in this repository.
The paper is not distributed here. It lives on arXiv under CC BY-SA, which governs reuse of the paper itself. That share-alike condition does not reach this code — the MIT-licensed implementation can be reused without triggering it.
Stephen Shaffer · Laura Voicu