A CVE ingestion and enrichment pipeline that builds a single normalised SQLite database stitched from CVEProject, NVD, CISA KEV, EPSS, and OSV β plus a Streamlit explorer for the result.
Ships as a fully populated SQLite DB on each release so you don't have to wait hours for the initial ingest.
- Full CVE corpus from the upstream CVEProject
cvelistV5zip - NVD bulk merge for CVSS metrics, CPE configurations, vuln status, and references
- CISA Known Exploited Vulnerabilities with
date_added,due_date, ransomware flag, and required action - EPSS exploit probability + percentile, batched
- OSV package mapping per CVE
- Streamlit explorer with ~70 pre-built views over the data
- Resume-safe pipeline state β interruptions pick up where they left off
Current snapshot (May 2026 release):
| count | |
|---|---|
| CVEs | 347,248 |
| Vendors | 21,183 |
| Products | 70,266 |
| KEV entries | 1,586 |
| Tables | 20 |
Deeper references live in docs/:
- Data sources β every upstream feed, endpoint, and licence.
- Pipeline β ingestion β enrichment, watermarks/resume, the cascade+heal model, tri-state discipline.
- Schema β the 20-table layout and query gotchas.
- MCP server β the 7 tools, per-agent setup, troubleshooting.
- Explorer β the Streamlit dashboard, tab by tab.
If you just want to query the data without running the pipeline:
# 1. Clone and pick the extras you need
git clone https://github.com/mez-0/vulnify.git
cd vulnify
uv sync --extra mcp # for agents / MCP server
# or: uv sync --extra explore # for the Streamlit dashboard
# or: uv sync --all-extras # for everything
# 2. Grab the latest release DB
gh release download -R mez-0/vulnify --pattern vulnify.db.zst
zstd -d vulnify.db.zst -o vulnify.db
# 3. Point .env at the DB
cat > .env <<EOF
VULNIFY_SQLITE_PATH=$(pwd)/vulnify.db
VULNIFY_SCHEMA_SQL_PATH=vulnify/db/schema.sql
EOF
# 4a. Use it from an agent (see "MCP server" below for full setup)
uv run vulnify-mcp </dev/null # smoke test β should start then exit on EOF
# 4b. Or browse it in the Streamlit explorer
uv run streamlit run explore/app.py
# 4c. Or just query the SQLite directly
sqlite3 vulnify.db "SELECT cve_id, summary FROM cve WHERE cve_id = 'CVE-2024-3094';"uv sync
cp .env.example .env # edit paths as needed
uv run vulnify-gatherThe first run takes several hours without an NVD API key (the public NVD rate limit is ~5 requests / 30 seconds). With a key it's roughly 10x faster.
Get a free key at https://nvd.nist.gov/developers/request-an-api-key and set
VULNIFY_NVD_API_KEY in .env.
vulnify-gather exposes flags so you can re-run individual enrichment stages
without re-ingesting:
vulnify-gather --skip-ingestion # don't re-download the CVEProject zip
vulnify-gather --skip-nvd # skip NVD bulk merge
vulnify-gather --skip-kev # skip CISA KEV
vulnify-gather --skip-epss # skip EPSS batch
vulnify-gather --skip-osv # skip OSV per-CVE lookups
vulnify-gather --skip-nuclei # skip Nuclei exploit-template ingest
vulnify-gather --skip-exploitdb # skip Exploit-DB artefact ingest
vulnify-gather --skip-metasploit # skip Metasploit module-metadata ingestThe pipeline writes phase watermarks to the pipeline_run table, so re-runs
are idempotent β NVD only fetches changes since the last watermark, KEV
re-syncs in full, EPSS only updates stale scores.
The exploit phases (Nuclei / Exploit-DB / Metasploit) fetch and cache their
corpora under ~/.vulnify/cache and watermark on corpus version (commit SHA /
content hash). They re-parse whenever cvelistV5 ingestion ran this gather β an
ingest cascade-deletes a re-touched CVE's artefacts, so they're healed on the
next run β and otherwise auto-skip an unmoved corpus. An explicit --skip-*
always wins. The tri-state exploit.public_poc / exploit.metasploit summary
bools are materialised from the artefacts: null until the contributing
sources have run, then true/false.
All config lives in .env at the project root (loaded by vulnify.settings).
| Variable | Required | Default | Purpose |
|---|---|---|---|
VULNIFY_SQLITE_PATH |
yes | vulnify.db |
Where the SQLite DB lives. Empty/unset switches the CVEProject ingest to print-only mode. |
VULNIFY_SCHEMA_SQL_PATH |
yes | vulnify/db/schema.sql |
DDL used to bootstrap a new DB. |
VULNIFY_KEV_JSON_PATH |
no | known_exploited_vulnerabilities.json |
Local copy of the CISA KEV catalog. If unset and the default file isn't present, the provider fetches it. |
VULNIFY_NVD_API_KEY |
no | β | NVD API key. Without it you're rate-limited to ~5 req / 30s. |
VULNIFY_CACHE_DIR |
no | ~/.vulnify/cache |
Where the exploit corpora (Nuclei / Exploit-DB / Metasploit) are cached. |
Relative paths are resolved against the project root.
βββββββββββββββββββββ
β gather.py (CLI) β
βββββββββββ¬ββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββ
β providers/cveproject.py β bulk download + parse
ββββββββββββββββ¬βββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββ
β db/sqlite_store.py β upsert into normalised tables
ββββββββββββββββ¬βββββββββββββββ
β
βΌ
βββββββββββββββββββββ΄βββββββββββββββββββββ
β providers/enrichment.py β
β βββ cisa_kev (CISA KEV catalog) β
β βββ nvd (CVSS, CPE, status) β
β βββ epss (probability scores) β
β βββ osv (package mappings) β
β βββ exploit_ingest (Nuclei/EDB/MSF) β
ββββββββββββββββββββββ¬ββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββ΄βββββββββββββββββββββ
βΌ βΌ
βββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββ
β explore/app.py (Streamlit) β β vulnify/mcp.py (FastMCP) β
βββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββ
vulnify/
βββ vulnify/
β βββ gather.py # `vulnify-gather` CLI (ingestion + enrichment)
β βββ mcp.py # `vulnify-mcp` FastMCP server (stdio)
β βββ settings.py # .env loader + path resolvers
β βββ constants.py # Severity, SourceTrust, ExploitMaturity enums
β βββ cvss_severity.py # CVSS-base -> severity bucketing
β βββ http.py # shared httpx client + file/zip download helpers
β βββ models/ # dataclass models (CVE, Vendor, Product, CVSS, β¦)
β βββ providers/
β β βββ cveproject.py # CVEProject bulk parser
β β βββ nvd.py # NVD 2.0 API client + per-CVE merge
β β βββ cisa_kev.py # KEV catalog ingest
β β βββ epss.py # FIRST EPSS batch enrichment
β β βββ osv.py # OSV per-CVE package lookup
β β βββ exploit_ingest.py # Nuclei/Exploit-DB/Metasploit artefact ingest
β β βββ vendor_advisories.py
β β βββ enrichment.py # phase orchestrator
β β βββ enrichment_resume.py
β βββ db/
β βββ schema.sql # 20-table normalised schema + FTS5 index
β βββ sqlite_store.py # connection + upsert API
β βββ readonly.py # shared read-only sqlite3 helper
β βββ cve_upsert.py # CVE/vendor/product writes
β βββ migrate.py # idempotent migrations + FTS5 backfill
β βββ pipeline_state.py # phase watermarks
β βββ sql_conversion.py # model <-> row helpers
βββ explore/
β βββ app.py # Streamlit dashboard
β βββ data.py # overview queries
β βββ data_views.py # per-chart queries
βββ tests/ # pytest suite (no network, no real DB)
20 tables, all normalised, foreign keys enabled. Top-level CVE plus separate tables for the high-cardinality bits (CPE matches, references, CVSS metrics, exploit signals + artefacts, KEV listing, intel) so you can join cleanly.
cve β primary CVE record
vendor β deduped vendors (case/whitespace-normalised)
product β products owned by a vendor
cve_vendor β many-to-many with `role` (assigner, affected, source, β¦)
affected_product β CVE <-> product affected/not-affected with notes
version_range β per affected_product version constraints
cwe / cve_cwe β weakness taxonomy
cvss β every CVSS vector from any source, deduped on (source, version, vector)
cpe_match β NVD configuration leaf matches
exploit β exploit availability + maturity (tri-state summary bools)
exploit_artefact β per-CVE exploit evidence (Nuclei/Exploit-DB/Metasploit)
kev β CISA KEV listing fields per CVE
intel β generic threat-intel rows (kind/value/source)
intel_string_list β backing list values for intel rows
reference β URL references with trust + source
reference_tag β many-to-many for ref tag vocabulary
tag / cve_tag β CVE-level tags
pipeline_run β per-phase watermarks for resume
Full DDL is in vulnify/db/schema.sql; an annotated
walkthrough with query gotchas is in docs/SCHEMA.md.
vulnify.mcp exposes the SQLite database to AI agents over the
Model Context Protocol. It's a FastMCP
server speaking stdio, so agents like Claude Code, Claude Desktop, and
Cursor spawn it as a subprocess on demand.
Full tool reference, per-agent setup, and troubleshooting: docs/MCP.md.
git clone https://github.com/mez-0/vulnify.git
cd vulnify
uv sync --extra mcpTwo options:
Option A β pre-built (recommended). Download the release asset and decompress:
gh release download -R mez-0/vulnify --pattern vulnify.db.zst
zstd -d vulnify.db.zst -o vulnify.dbOption B β build from source. Takes hours; see Building the DB from scratch.
Create .env at the project root with an absolute path:
echo "VULNIFY_SQLITE_PATH=$(pwd)/vulnify.db" > .env
echo "VULNIFY_SCHEMA_SQL_PATH=vulnify/db/schema.sql" >> .envConfirm the server starts and shuts down cleanly. It speaks stdio, so just
sending Ctrl-D (EOF) exits it:
uv run vulnify-mcp </dev/nullIf you used a release DB built before FTS5 shipped, you'll see a one-off ~8 second backfill on stderr:
INFO Backfilling FTS5 index over 347248 CVE rows β one-off, takes ~30s
INFO FTS5 backfill complete in 8.5s
That's it β releases from v2026.05.21.1 onwards ship with the FTS index
pre-built, so this step is silent.
Easiest is the CLI:
claude mcp add vulnify \
--scope user \
-- uv run --directory "$(pwd)" vulnify-mcpOr edit ~/.claude.json directly:
{
"mcpServers": {
"vulnify": {
"command": "uv",
"args": ["run", "--directory", "/absolute/path/to/vulnify", "vulnify-mcp"]
}
}
}Verify it's connected:
claude mcp listEdit ~/Library/Application Support/Claude/claude_desktop_config.json
(macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"vulnify": {
"command": "uv",
"args": ["run", "--directory", "/absolute/path/to/vulnify", "vulnify-mcp"]
}
}
}Restart Claude Desktop after editing.
Edit ~/.cursor/mcp.json (or the project-local .cursor/mcp.json):
{
"mcpServers": {
"vulnify": {
"command": "uv",
"args": ["run", "--directory", "/absolute/path/to/vulnify", "vulnify-mcp"]
}
}
}| Tool | What it does |
|---|---|
get_cve(cve_id) |
Full hydrated CVE record β CVSS, KEV, EPSS, exploits, references, CWEs, affected products, plus an artefacts evidence list. |
search_cves(...) |
Filter by vendor / product / cwe / kev_only / min_cvss / min_epss / year. |
exploits_for(cve_id) |
Concrete exploit artefacts (Nuclei / Exploit-DB / Metasploit) with source, stable id, url, and exact/parsed confidence. |
references_for(cve_id, tag) |
Reference URLs with their tags, optionally filtered to one tag (e.g. patch, exploit). |
list_kev(since, limit) |
Recent CISA KEV entries, newest first. |
database_overview() |
Totals, published-date range, last pipeline-run times. |
search_cves_text(query, limit) |
FTS5 free-text search over title / summary / technical details. AND / OR / NOT / quoted phrases / prefix* all supported. |
All tools return JSON-serialisable dicts. Limits cap at 100 rows.
- "What's CVE-2024-3094?"
- "Show me KEV entries added in the last 60 days about Cisco."
- "Find CVEs about container escape with CVSS β₯ 8."
- "Which products have the most CVEs published in 2025?"
- Tools don't show up in an already-running agent β MCP servers are
attached at session start, not when
claude mcp addruns. Exit your current Claude Code / Cursor session and start a fresh one β don't/resumean old one, that re-reads the old tool list. claude mcp listshowsβ Failed to connectβ first thing to check is the exactArgs:field withclaude mcp get vulnify; a copy-paste truncation that turnsvulnify-mcpintovulnify-mcis a common one. Smoke test the spawned command verbatim in a shell:uv run --directory /path/to/vulnify vulnify-mcp </dev/null.uv: command not foundin the agent's process β agents inherit a minimalPATH. Either use an absolute path touvin the config (command: "/home/you/.cargo/bin/uv") or symlinkuvinto/usr/local/bin.VULNIFY_SQLITE_PATH is unsetat startup β your.envisn't being picked up. The server resolves.envrelative to the vulnify package root; pass an absolute--directoryin the agent config so it lands in the right place.- Tool calls return nothing β confirm the DB has data with
uv run python -c "import sqlite3; print(sqlite3.connect('vulnify.db').execute('SELECT COUNT(*) FROM cve').fetchone())". - Logs disappearing β
vulnify-mcplogs to stderr (so it doesn't corrupt the MCP protocol on stdout). Most agent UIs surface stderr in their MCP logs view; in Claude Code that's/mcpthen pick the server.
explore/app.py is a Streamlit dashboard with roughly 70 pre-built views over
the data: severity distributions, KEV listing lag, EPSS density, CWE
co-occurrence heatmaps, vendor / product pressure, CVSS vector breakdowns,
publication recency, exploit-in-the-wild monthly trends, and more.
uv sync --extra explore
uv run streamlit run explore/app.pyViews are organised into 14 tabs (Overview, Vendors, Trends, Vendor links,
Severity & CWE, Enrichment, Exploit landscape, KEV deep dive, CVSS vector,
Products & CPEs, References, Threat intel, Lifecycle, Pipeline health) and split
across two query modules: explore/data.py (overview / landing) and
explore/data_views.py (the per-chart queries). Full tab-by-tab breakdown and
how to add a view: docs/EXPLORE.md.
uv run pytestTests are hermetic β no network, no real DB. They cover:
- SQLite store upsert + dedup behaviour
- Schema migration + backfill
- NVD bulk merge
- Pipeline state watermarks
- Enrichment resume logic
- Explorer query shapes
- MCP tool surface (structured + FTS5 search)
Each release ships:
vulnify.db.zstβ the full SQLite DB, zstd-compressed. Includes the FTS5 index pre-built fromv2026.05.21.1onwards sovulnify-mcpis ready to serve immediately on first start. Decompress withzstd -d vulnify.db.zst.schema.sqlβ the DDL, in case you only want the structure.
Tag scheme:
vYYYY.MM.DDβ major snapshot, data captured on that date.vYYYY.MM.DD.Nβ code patch against the same data snapshot (e.g.v2026.05.21.1is the2026-05-21data with thevulnify-mcpserver added).
The code version in pyproject.toml follows SemVer independently.
Released under the MIT License.