CVE Triage & Risk Briefing Agent
An agentic Python CLI that takes a software stack (stack.json), autonomously
enriches each package's known CVEs from public threat-intel sources, scores
them by actual exploitability (not just CVSS), and produces a professional
PDF risk briefing ready for a CTO/CISO.
The agent uses an OpenAI model (default gpt-4o) as the orchestrator — it
decides which tools to call, in what order, and when it has enough data to
write the final report. The Python side just runs the tools and feeds
results back.
Data sources
- NIST NVD — authoritative CVE catalogue
- OSV.dev — best coverage for npm, PyPI, Go, Maven, RubyGems
- FIRST EPSS — probability of exploitation in the wild (next 30 days)
- CISA KEV — vulnerabilities confirmed exploited in the wild
- CIRCL CVE / Exploit-DB — public exploit references
Install
pip install cve-triage-agent
Then create a .env file (or export the variables in your shell):
OPENAI_API_KEY=sk-...
NVD_API_KEY= # optional — recommended for higher rate limits
OPENAI_MODEL=gpt-4o # optional — override the default model
Get a free NVD API key at https://nvd.nist.gov/developers/request-an-api-key.
Usage
cve-triage --stack stack.json
# → writes report.pdf
cve-triage --stack stack.json --output briefing.pdf
cve-triage --stack stack.json --format markdown --output briefing.md
cve-triage --stack stack.json --format json --output findings.json
cve-triage --stack stack.json --min-cvss 7.0 # drop low-severity noise
You can also invoke without installing the console script:
python -m cve_triage --stack stack.json
Options
| Flag | Default | Description |
|---|---|---|
--stack |
required | Path to the input stack JSON file |
--output |
report.pdf (or .md / .json to match --format) |
Output path |
--format |
pdf |
pdf, markdown, or json |
--min-cvss |
0.0 |
Drop findings below this CVSS base score |
-v, --verbose |
off | Debug logging in agent_run.log |
--version |
— | Print version and exit |
Input format (stack.json)
[
{ "name": "django", "version": "4.2.3", "ecosystem": "PyPI" },
{ "name": "express", "version": "4.18.2", "ecosystem": "npm" },
{ "name": "openssl", "version": "3.0.7", "ecosystem": "other" }
]
Supported ecosystems: PyPI, npm, Maven, Go, RubyGems, NuGet,
other. Use other for system packages like nginx or openssl that OSV's
ecosystem indices don't cover — NVD will still be queried, plus an
ecosystem-agnostic OSV fallback.
A worked example lives at examples/stack.json.
Risk scoring
The composite risk score (0–100) weights:
- CVSS base severity — 40%
- EPSS exploitation probability — 35%
- CISA KEV listing — +20
- Public exploit available — +10
- Known ransomware association — +5
Tiers: CRITICAL ≥ 75 · HIGH ≥ 50 · MEDIUM ≥ 25 · LOW < 25.
CVSS floor rules guarantee that a CVE with CVSS ≥ 9.5 is always CRITICAL, ≥ 9.0 is always at least CRITICAL, and ≥ 7.0 is always at least HIGH — exploitability signals can promote a tier but never demote one.
A CVE listed in CISA KEV with CVSS 7.5 will outrank a CVSS 9.8 with EPSS 0.001, which is what you want.
Output
A PDF briefing with:
- Title block + scan metadata + tier counts
- Executive summary (150–250 words, plain prose, generated by the LLM after scoring so it can never contradict the tables)
- Packages-scanned table (every input package, even clean ones)
- Critical/High findings table (tier-color coded)
- Per-finding detail blocks with description, why-urgent rationale, remediation, references
- Medium/Low findings table
- Methodology + data-source list
Use --format markdown for a plain-text version (handy for diffing) or
--format json for downstream tooling.
Architecture
cve-triage-agent/
├── cve_triage/
│ ├── cli.py CLI entry point (argparse → run_agent)
│ ├── loop.py Two-pass agent: collect → score → summarise
│ ├── tools.py NVD / OSV / EPSS / KEV / Exploit-DB integrations
│ ├── scorer.py Composite risk score + CVSS floor rules
│ ├── reporter.py Markdown + JSON renderers
│ └── pdf_reporter.py reportlab Platypus PDF renderer
├── examples/
│ └── stack.json
├── pyproject.toml
└── LICENSE
The agent runs a two-pass loop:
- Pass 1 — tool-using loop where the model collects + enriches findings
and emits a single
findings_jsonblock (no prose, no headers). - The Python side scores every finding and applies CVSS floor rules.
- Pass 2 — non-tool call where the model writes the executive summary, anchored to the scored findings (so the prose can never reference tiers that don't exist).
Tool-call traces and HTTP retry warnings land in agent_run.log.
Building and publishing
pip install -e ".[dev]" # editable install with build/twine
python -m build # produces dist/*.whl and dist/*.tar.gz
python -m twine upload dist/* # publish to PyPI
Update author and URL fields in pyproject.toml before your first publish.
License
MIT — see LICENSE.
Metadata
Release files for cve-triage-agent 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| cve_triage_agent-0.1.0.tar.gz | 28.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cve_triage_agent-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 58.2 kB
Release files / cve_triage_agent-0.1.0.tar.gz
| Download URL | cve_triage_agent-0.1.0.tar.gz |
|---|---|
| Size | 28.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
633acfc7f54ae6084a1fd4551ab9900ab7e747397ed8813325494beb880d1405
|
|
BLAKE2b-256 checksum How to use checksums |
d5def16e1926aba7ff56b6616b0baab853a3e7400bdd365e527cfc40293635f0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.4
|
Release files / cve_triage_agent-0.1.0-py3-none-any.whl
| Download URL | cve_triage_agent-0.1.0-py3-none-any.whl |
|---|---|
| Size | 29.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0d6eb04e0e2e80211f29b48af51974eda925196d2b02b6207f8532d978167632
|
|
BLAKE2b-256 checksum How to use checksums |
8dbf8e6b98f64da701823a93b6849c793183136aba554d6de68009d376007ce5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.4
|