20 normalised tables plus an FTS5 virtual table. Foreign keys are enabled at the
application layer (PRAGMA foreign_keys = ON) — the top-level cve row is the
parent, and every child table cascades on delete. That cascade is
load-bearing for the pipeline's persistence model (see
PIPELINE.md).
Full DDL: vulnify/db/schema.sql. Additive
migrations and the FTS5 backfill live in vulnify/db/migrate.py.
cve — the primary record, keyed by cve_id. Holds title, cna, summary,
technical_details, published / modified / discovered, vuln_status
(NVD), state, assigner_org_id, source_identifier, and derived
priority_score / confidence. Every other table hangs off this cve_id.
vendor— deduped vendors;UNIQUEonlower(trim(name))so casing and whitespace can't create duplicates.cve_vendor— CVE ↔ vendor many-to-many with arole(assigner/affected/source/ …). PK(cve_id, vendor_id, role).product— products owned by a vendor;UNIQUE (vendor_id, name, component).affected_product— CVE ↔ product with anaffectedflag (1 = affected, 0 = explicitly not) and free-textnotes.version_range— per-affected_productversion constraints:start_including/start_excluding/end_including/end_excluding,fixed_version, and the originalraw_text.
cwe— CWE id, name, description.cve_cwe— CVE ↔ CWE many-to-many.
cvss— every CVSS vector from any source, one row each:version,score,vector,severity, the decomposed metrics (attack vector/complexity, privileges, UI, scope, CIA),exploitability_score,impact_score, andmetric_source/metric_type. Deduped on(source, version, vector). A CVE can carry both a CNA and an NVD vector — queryMAX(score)for the worst case.intel— one row per CVE:epss_score,epss_percentile(from EPSS).intel_string_list— generic backing list values keyed(cve_id, kind, value); where OSV package mappings land.
exploit— one summary row per CVE.maturity, the tri-state boolspublic_poc/metasploit(NULL= not assessed, 0 = none found, 1 = found — see tri-state), and the NVD/KEV-sourced flagsransomware_usage/in_the_wild.exploit_artefact— the evidence layer: many artefacts per CVE (source,stable_id,url,artefact_type,platform,published_date,confidence).confidenceis a provenance vocab (exact/parsed/heuristic), enforced in the ingest layer, not a CHECK constraint.UNIQUE (cve_id, source, stable_id)keeps re-ingest upserts idempotent and servesWHERE cve_id = ?via its leftmost column. Written by direct SQL outside the cvelistV5 upsert graph — so a CVE re-ingest cascade-wipes these and they heal on the next exploit-phase run.kev— CISA KEV listing, one row per CVE with alistedflag (not one row per catalog entry).date_added,due_date,required_action,vulnerability_name, vendor/product labels,short_description. Count listed entries withWHERE listed = 1—COUNT(*)counts every CVE.
cpe_match— NVD configuration leaf matches:criteria(the CPE 2.3 string),match_criteria_id,vulnerableflag.cpe_match_unique(cve_id, criteria, match_criteria_id)is created by the migration after deduping legacy rows.
reference— URL references withsource,title,trust.reference_tag— tags per reference, keyed byreference_id. Nocve_idcolumn — join throughreference:reference JOIN reference_tag ON reference_tag.reference_id = reference.id.tag/cve_tag— CVE-level tag vocabulary and its many-to-many.
pipeline_run— per-phase resume state:phase(PK),last_completed_at,last_watermark(phase-specific — NVD stores an upper-boundlastModified; KEV stores the catalog version; exploit sources store a corpus commit SHA / content hash). See PIPELINE.md.
cve_fts— FTS5 virtual table (external-content, tied tocveby rowid) overtitle/summary/technical_details, tokenisedporter unicode61. Kept in sync by insert/update/delete triggers oncve; backfilled once by the_ensure_fts5_indexmigration on older DBs. Powers the MCPsearch_cves_texttool. (FTS5 also creates internal shadow tables —cve_fts_data,_idx, etc. — which don't count toward the 20.)
- Worst-case CVSS per CVE:
SELECT MAX(score) FROM cvss WHERE cve_id = ?— a CVE may have several vectors from different sources. - KEV count:
WHERE listed = 1, never bareCOUNT(*). - Reference tags: join via
reference.id, not acve_idonreference_tag. - Exploit tri-state: filter
public_poc IS NOT NULLbefore treating the value as boolean, or you'll conflate "not assessed" with "none found". - Enrichment rows (
exploit_artefact,intel,cpe_match,cvss,kev) are cascade-deleted when theircverow is re-ingested — they are rebuilt, not preserved. Don't build long-lived external references to their integer PKs.