Migration 010 + model updates that prep VulnCheck to merge Nessus
findings with existing Wazuh-sourced vulnerabilities on the same
(cve_id, asset_id) row instead of creating parallel duplicates.
Schema (alembic/versions/010_add_nessus_integration.py):
- vulnerabilities.sources JSON list of scanners that detected
this finding, e.g. ["wazuh","nessus"]
- vulnerabilities.nessus_plugin_id Nessus plugin ID for the finding
- vulnerabilities.nessus_finding_uuid stable per-finding identifier
- vulnerabilities.first_detected_by which scanner first reported it
- vulnerabilities.cve_id widened 20 -> 50 chars so non-CVE Nessus
findings can be stored as
NESSUS-PLUGIN-{plugin_id} pseudo-CVEs
- assets.nessus_host_uuid pin Nessus host after first match
- scan_schedules.scanner_type wazuh|nessus, default wazuh for
backwards compat
- Backfill: every existing vuln sources = ["wazuh"],
first_detected_by = wazuh
Model helpers:
- Vulnerability.source_list / cross_confirmed / is_pseudo_cve properties
- Vulnerability.add_source(name) / remove_source(name) (no commit)
- Asset.nessus_host_uuid column
- ScanSchedule.scanner_type column
No behaviour change yet — Phase 2 will add the NessusClient + sync
function that actually populate these fields.
Postgres authprovider enum was created (migration 009) with lowercase
values 'local','ldap','saml','oidc'. By default SQLAlchemy maps enum
columns by member NAME, which for AuthProvider are uppercase
(LOCAL/LDAP/...). On read, SQLAlchemy tried to find a member named
'local' and raised:
LookupError: 'local' is not among the defined enum values.
Possible values: LOCAL, LDAP, SAML, OIDC
Fix: pass values_callable=lambda x: [e.value for e in x] to the SQLEnum
column so SQLAlchemy compares against values, not names. No schema or
data change required.
EUVD (EU Vulnerability Database, ENISA) integration as second
authoritative catalog alongside CISA KEV. EU-Compliance use cases
benefit from a non-US source; CVEs confirmed by both catalogs get
the highest priority via score stacking.
Two ENISA endpoints are merged into one cached map (24h TTL):
- /exploitedvulnerabilities (analogous to CISA KEV)
- /criticalvulnerabilities (ENISA Critical flag)
Priority-Score formula:
- Exploit-Signal now triggered by KEV OR EUVD listing
- Catalog-Bonus stacking: KEV +10, EUVD +10, KEV-ransomware +5,
EU-Critical +3. A CVE in both catalogs adds +20 base.
CPR Score (Cybersecurity Priority Risk = CVSS x EPSS x 10) added
as separate metric next to Priority, per JacquesKruger/EPSS-Server
convention. Calculated on-the-fly, no DB column needed.
New API filters: euvd_only, eu_critical, in_any_catalog,
in_both_catalogs. Setting toggle enrichment_euvd_enabled (default true).
Frontend: new EUVD column (blue badge, EU-CRIT sub-badge), CPR column
with mini-bar, four catalog filter checkboxes. Detail page splits
threat intel into CISA KEV / ENISA EUVD / EPSS sections; breakdown
shows EUVD bonus row and CPR score with both-catalogs hint.
Risk score now pulls from multiple threat intel sources instead of
only AI/CVSS data:
- EPSS (FIRST.org) — probability of exploitation in next 30 days
- CISA KEV — known actively exploited vulnerabilities (with ransomware flag)
- Existing Wazuh exploit flags as fallback
Adds DB columns (epss_score, epss_percentile, kev_listed, kev_*,
enrichment_sources, enrichment_updated_at), an enrichment_service
with cached KEV catalog (24h TTL in settings table) and batched
EPSS lookups, manual + bulk + KEV-refresh endpoints, automatic
enrichment after Wazuh sync, and a daily scheduler job to refresh
scores.
Frontend gets KEV badges, EPSS column with percentile, KEV-only +
EPSS-min filters, a "Refresh Threat Intel" button, and a priority
score breakdown card on the detail page.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>