Files
vulncheck/DATABASE_SCHEMA.md
T
vulncheck 7078f5e024 feat(vsphere): read vCenter + every ESXi host, and detect their CVEs
A hypervisor runs no agent. Wazuh cannot reach it, Intune does not know it,
and Nessus only sees it if someone scoped a credentialed scan at it — so the
machines whose compromise takes every VM on them down were the ones with no
vulnerability coverage at all.

The connector reads the vCenter appliance and every ESXi host in its
inventory over the vSphere SOAP API (pyVmomi): hostname, management IP,
hardware model, product version and BUILD. One PropertyCollector pass, so a
500-host estate is one round trip. Read-only rights are enough.

The build is the point. NVD carries nothing usable — CVE-2026-47876 and
CVE-2026-59310 both sit there with no configuration — and the bounds
cvelistV5 does state are build identifiers, not versions:

    ESX      8.0       lessThan "ESXi80U3k-25595708"
    vCenter  8.0       lessThan "8.0 U3k"

_is_version rejects both, so _ranges_from_affected dropped the entries and
every vSphere CVE was invisible. They are now indexed verbatim and resolved
to build numbers: ESXi bounds carry one inline, vCenter bounds name a release
whose build comes from Broadcom KB 326316 (seeded in full, re-read weekly,
merged never replaced). A bound that resolves to no build produces no
verdict, and neither does a host with no build recorded.

Comparing builds alone is wrong in both directions. CVE-2025-22224 names two
fixes for the 8.0 line at once — U3d (24585383) for 8.0.3 and U2d (24585300)
for 8.0.2 — so the U2d host is patched despite the higher number existing.
And vCenter 8.0 U2f shipped four days AFTER the U3k fix on the older update
line, with a higher build, because Broadcom ships async patches there. So the
decision is scoped to the update line first, then still checked against the
build.

What must never resolve is asserted in the tests: the same advisories file
"VMware Cloud Foundation (vCenter Server)" and "vSphere Foundation" with the
SUITE's version numbers, and matching those would compare a vCenter 8.0.3
appliance against a VCF 5.x range.

EOL comes from endoflife.date (esxi / vcenter), keyed by major line — which
is also the honest granularity, one end-of-support date per line. 7.0 ended
2025-10-02, 6.7/6.5 in 2022. Both slugs are single-release, so an upgrade
retires the old finding. The OS patterns are shared with the CVE pass so a
machine cannot be a hypervisor for its CVEs and something else for its dates.

Checked against all 189 vSphere CVE records currently in cvelistV5: 64 bounds
indexed, none unresolvable, and every current release comes out clean while
each one behind gets its own line's fix named.

Migration 045 adds AssetSource.VCENTER, assets.vmware_uuid (the pin) and
assets.vmware_build.
2026-08-17 11:54:51 +02:00

21 KiB
Raw Blame History

TrueVuln — Database Schema

Stand: Mai 2026, alembic head = 022_backfill_ssvc_only_exploitation_source. PostgreSQL 16. Times in UTC. TimestampMixin adds created_at + updated_at to every audited table unless noted.


Entity Relationship Diagram (ERD)

   ┌─────────┐  M:N (user_groups)  ┌────────┐
   │  User   │◄───────────────────►│ Group  │
   └────┬────┘                     └────┬───┘
        │                               │
        │ 1:N assigned_user_id          │ M:N (asset_groups)
        ▼                               ▼
   ┌──────────────────────────────────────────────┐
   │                    Asset                     │
   │  (hostname, ip, os, wazuh_agent_id,          │
   │   nessus_host_uuid, criticality, policy_id)  │
   └────┬──────────────────┬──────────────────────┘
        │ 1:N              │ 1:N
        ▼                  ▼
   ┌──────────┐    ┌──────────────────┐   ┌───────────┐
   │   Scan   │    │ Vulnerability    │──►│ AIAnalysis│
   │          │    │ (cve, cvss, vpr, │   └───────────┘
   │          │    │  ssvc, sources,  │
   │          │    │  enrichment...)  │
   └──────────┘    └──────────────────┘
                          │ 1:N
                          ▼
                   ┌─────────────────┐
                   │ NotificationLog │
                   └─────────────────┘

   ┌────────────────────┐  1:N  ┌──────────────────┐
   │ ComplianceResult   │──────►│ ComplianceCheck  │
   └────────────────────┘       └──────────────────┘
   ComplianceImpact   (cis_id, benchmark unique) — lookup table
   AssetRiskSnapshot  (asset_id, snapshot_date)  — daily URS history

   ┌────────┐    1:N    ┌──────────┐
   │ Policy │──────────►│  Asset   │   (asset.policy_id)
   └────────┘           └──────────┘

   Standalone tables:
     AuditLog, Setting, ScanSchedule, AIReport

Table Details

users

Column Type Constraints Purpose
id INTEGER PK
username VARCHAR(50) UNIQUE, NOT NULL, INDEX
email VARCHAR(255) UNIQUE, NOT NULL, INDEX
password_hash VARCHAR(255) NULLABLE bcrypt cost-12; NULL for SSO-only users
role ENUM userrole NOT NULL admin / editor / viewer
auth_provider ENUM authprovider NOT NULL local / ldap / oidc / saml
external_id VARCHAR(255) INDEX IdP subject / AD objectGUID
external_groups TEXT NULLABLE JSON array of group names/IDs from IdP
last_provider_sync TIMESTAMP NULLABLE Last successful SSO/LDAP login
totp_secret VARCHAR(255) NULLABLE Fernet-encrypted base32
totp_enabled BOOLEAN DEFAULT FALSE MFA enabled
is_active BOOLEAN DEFAULT TRUE
is_verified BOOLEAN DEFAULT FALSE
failed_login_attempts INTEGER DEFAULT 0 Account lockout counter

user_groups is an M:N association table (user_id, group_id).

groups

Column Type Constraints Purpose
id INTEGER PK
name VARCHAR(100) UNIQUE, NOT NULL
description TEXT NULLABLE
policy_id INTEGER FK policies.id, NULLABLE Group-default SLA policy

assets

Column Type Constraints Purpose
id INTEGER PK
hostname VARCHAR(255) NOT NULL, INDEX FQDN or short
ip_address VARCHAR(45) INDEX IPv4/IPv6
wazuh_agent_id VARCHAR(10) UNIQUE, INDEX Zero-padded ("006")
nessus_host_uuid VARCHAR(64) INDEX Pinned after first Nessus match
intune_device_id VARCHAR(64) INDEX Pinned after first Graph match
defender_machine_id VARCHAR(64) INDEX Defender for Endpoint machine
aad_device_id VARCHAR(64) INDEX Entra device id — shared Intune/Defender anchor
vmware_uuid VARCHAR(64) INDEX ESXi hardware UUID / vCenter instanceUuid (migration 045)
vmware_build VARCHAR(32) NULLABLE VMkernel / vpxd build — VMware states its fixes as builds, so this is what decides a vSphere CVE
operating_system VARCHAR(255) NULLABLE OS family
os_version VARCHAR(100) NULLABLE
kernel_version VARCHAR(100) NULLABLE
criticality VARCHAR(16) NOT NULL DEFAULT 'normal' low / normal / high / critical — URS multiplier
compliance_frameworks TEXT NULLABLE JSON array of mandatory framework slugs
source ENUM assetsource NOT NULL WAZUH / NESSUS / INTUNE / VCENTER / MANUAL
status ENUM assetstatus NOT NULL active / inactive
last_scan TIMESTAMP NULLABLE
last_seen TIMESTAMP NULLABLE
description TEXT NULLABLE
location VARCHAR(255) NULLABLE
owner VARCHAR(255) NULLABLE Free-text owner string
assigned_user_id INTEGER FK users.id, INDEX Default assignee for vulns
policy_id INTEGER FK policies.id, NULLABLE SLA policy override

asset_groups is an M:N association table (asset_id, group_id).

vulnerabilities

Column Type Constraints Purpose
id INTEGER PK
cve_id VARCHAR(50) NOT NULL, INDEX CVE-YYYY-N or NESSUS-PLUGIN-{id}
asset_id INTEGER FK assets.id, NOT NULL, INDEX
cvss_score FLOAT INDEX Latest authoritative score
cvss_vector VARCHAR(100) NULLABLE
severity ENUM vulnseverity NOT NULL critical / high / medium / low / none
status ENUM vulnstatus NOT NULL open / patched / pending_verification / accepted_risk / false_positive / deferred / patch_failed
title TEXT NULLABLE
description TEXT NULLABLE
package_name VARCHAR(255) INDEX Comma-joined merged-packages
package_version VARCHAR(100) NULLABLE
fixed_version VARCHAR(100) NULLABLE From CVE-5 affected[].versions[].lessThan or NVD cpeMatch.versionEndExcluding — drives PATCH AVAILABLE pill
exploitable BOOLEAN DEFAULT FALSE, INDEX Wazuh signal
exploit_available BOOLEAN DEFAULT FALSE Wazuh signal
exploit_maturity VARCHAR(50) NULLABLE poc / functional / high (Nessus)
exploitation_status VARCHAR(20) INDEX SSVC: none / poc / active / widespread (Vulnrichment)
exploitation_source VARCHAR(20) NULLABLE vulnrichment / nvd / cvelistv5 / nessus / manual — source pin
ssvc_technical_impact VARCHAR(16) INDEX SSVC: partial / total
ssvc_automatable VARCHAR(8) INDEX SSVC: yes / no
published_date TIMESTAMP NULLABLE CVE publication date (used in "Newly Published" widget sort)
detected_at TIMESTAMP NOT NULL, INDEX First detection in our system
patched_at TIMESTAMP NULLABLE
references TEXT NULLABLE JSON array of URLs
cwe_id VARCHAR(20) NULLABLE CWE identifier
assigned_user_id INTEGER FK users.id, INDEX
assigned_group_id INTEGER FK groups.id, INDEX
deferred_until TIMESTAMP NULLABLE
defer_reason TEXT NULLABLE
notification_suppressed BOOLEAN DEFAULT FALSE Bell-icon toggle
epss_score FLOAT INDEX 0.01.0
epss_percentile FLOAT NULLABLE 0.01.0
epss_updated_at TIMESTAMP NULLABLE
kev_listed BOOLEAN DEFAULT FALSE, INDEX CISA KEV
kev_date_added TIMESTAMP NULLABLE
kev_ransomware_use BOOLEAN DEFAULT FALSE
kev_short_description TEXT NULLABLE
euvd_listed BOOLEAN DEFAULT FALSE, INDEX ENISA EUVD exploited
euvd_critical BOOLEAN DEFAULT FALSE ENISA-flagged critical
euvd_date_added TIMESTAMP NULLABLE
euvd_id VARCHAR(50) NULLABLE ENISA EUVD-ID
enrichment_sources TEXT NULLABLE JSON array, e.g. ["epss","kev","euvd"]
enrichment_updated_at TIMESTAMP NULLABLE Last enrichment run
sources TEXT NOT NULL DEFAULT '["wazuh"]' JSON array of scanners that detected it (wazuh, nessus, manual)
nessus_plugin_id VARCHAR(20) INDEX Nessus plugin reference
nessus_finding_uuid VARCHAR(64) NULLABLE Per-host-per-plugin stable id
first_detected_by VARCHAR(20) NULLABLE wazuh / nessus / manual
nessus_vpr_score FLOAT INDEX Tenable VPR 010

Computed-but-not-persisted on the response model: priority_score, cpr_score, cross_confirmed (true if sources length ≥ 2).

Unique pair (effective): (cve_id, asset_id) — enforced application-side; the sync paths use savepoint-protected inserts to handle race-induced duplicates.

scans

Column Type Constraints Purpose
id INTEGER PK
asset_id INTEGER FK assets.id, NOT NULL, INDEX
scan_type ENUM scantype NOT NULL WAZUH / NESSUS (uppercase post-019)
status ENUM scanstatus NOT NULL PENDING / RUNNING / COMPLETED / FAILED
started_at TIMESTAMP NULLABLE
completed_at TIMESTAMP NULLABLE
vulnerabilities_found INTEGER DEFAULT 0
vulnerabilities_new INTEGER DEFAULT 0
vulnerabilities_resolved INTEGER DEFAULT 0
error_message TEXT NULLABLE

API-layer display map (_display_scan_type() in app/routers/scans.py) collapses legacy values FULL / SYSCOLLECTOR / MANUAL → wazuh so the UI shows the human-friendly source string.

scan_schedules

Column Type Constraints Purpose
id INTEGER PK
name VARCHAR(200) NOT NULL
interval ENUM scheduleinterval NOT NULL HOURLY / DAILY / WEEKLY / CRON
cron_expression VARCHAR(100) NULLABLE e.g. 0 2 * * *
scanner_type VARCHAR(20) NOT NULL DEFAULT 'wazuh' wazuh / nessus
enabled BOOLEAN NOT NULL DEFAULT TRUE
last_run TIMESTAMP NULLABLE
next_run TIMESTAMP NULLABLE Re-computed every 60 s by scheduler_sync
created_by INTEGER NULLABLE users.id

policies

Column Type Constraints Purpose
id INTEGER PK
name VARCHAR(255) NOT NULL
description TEXT NULLABLE
category VARCHAR(100) NOT NULL
status ENUM policystatus DEFAULT active active / warning / disabled — DISABLED suppresses SLA mails for assets pinned to this policy
critical_sla_days INTEGER DEFAULT 2
high_sla_days INTEGER DEFAULT 7
medium_sla_days INTEGER DEFAULT 30
low_sla_days INTEGER DEFAULT 90

notification_logs

Column Type Constraints Purpose
id INTEGER PK
vulnerability_id INTEGER FK, INDEX
asset_id INTEGER FK, INDEX
user_id INTEGER FK, INDEX Recipient user
notification_type ENUM notificationtype NOT NULL, INDEX SLA_BREACH / ASSIGNMENT / NEW_VULNERABILITY / KEV_ALERT / MANUAL
sent_at TIMESTAMP NOT NULL, INDEX
subject VARCHAR(500) NULLABLE
recipient_email VARCHAR(255) NULLABLE
status ENUM notificationstatus NOT NULL SENT / FAILED
message_body TEXT NULLABLE
error_message TEXT NULLABLE

The hourly SLA job applies a per-vuln 24h throttle by querying this table.

audit_logs

Column Type Constraints Purpose
id INTEGER PK
user_id INTEGER FK users.id, INDEX Nullable for system events
event_type ENUM auditeventtype NOT NULL, INDEX Login success/fail, role change, MFA reset, password change, delete, etc.
event_description VARCHAR(500) NOT NULL
ip_address VARCHAR(45) NULLABLE
user_agent VARCHAR(500) NULLABLE
resource_type VARCHAR(50) NULLABLE vulnerability / asset / user
resource_id VARCHAR(50) NULLABLE
old_value TEXT NULLABLE JSON diff
new_value TEXT NULLABLE JSON diff
timestamp TIMESTAMP NOT NULL, INDEX

settings

Key-value store. Stable keys:

Key Value Purpose
smtp_config JSON host, port, user, pass (Fernet-encrypted), tls flag
notification_mode digest | single New-vuln + SLA mail mode
notification_min_severity critical | high | ... Filter for new-vuln digest
sla_breach_enabled true | false Master toggle for the hourly SLA job
enrichment_epss_enabled true | false
enrichment_kev_enabled true | false
enrichment_euvd_enabled true | false
nessus_config JSON base_url, access_key, secret_key, verify_ssl, default_scan_ids, auto_create_assets
ldap_config JSON server, base DN, search filter
ldap_bind_password_encrypted string Fernet-encrypted
oidc_config JSON issuer, client_id, client_secret, scopes
saml_config JSON sp + idp settings
role_mappings JSON [{group_pattern, role}]
email_template_* HTML/Jinja-light Override default mail templates
kev_cache_json JSON CISA KEV catalog cache (24h TTL)
euvd_cache_json JSON ENISA EUVD catalog cache (24h TTL)
kev_alert_enabled true | false Master toggle for the hourly KEV alert mail (default on)
kev_alert_sources cisa,euvd Which KEV sources feed the alert; empty = all
kev_alert_recipients string Comma/semicolon list; empty = notification_default_recipients, else active admins
kev_alert_state JSON {cve: {at, n}} — what was already mailed, so a run is idempotent

compliance_results

Column Type Notes
id, asset_id (FK)
policy_id VARCHAR(64), INDEX Wazuh SCA policy id
policy_name, policy_description
total_checks, pass_count, fail_count, not_applicable_count INTEGER
score FLOAT Plain pass-%
weighted_score FLOAT, INDEX impact-weighted score (sum_impact_passed / sum_impact)
end_scan, last_synced TIMESTAMP

compliance_checks

Column Type Notes
id, result_id (FK CASCADE)
check_id VARCHAR(64) Wazuh internal id
title, description, rationale, remediation TEXT
result VARCHAR(20), INDEX passed / failed / not applicable
severity VARCHAR(20) Wazuh-side severity

compliance_impacts

Column Type Notes
id, cis_id (INDEX), benchmark (INDEX) UNIQUE (cis_id, benchmark)
level VARCHAR(16) L1 / L2 / NG
title VARCHAR(500)
impact INTEGER DEFAULT 50 0-100

Loaded via POST /compliance/impacts/import (multipart CSV). Unique constraint makes repeat uploads upsert cleanly.

asset_risk_snapshots

Column Type Notes
id, asset_id (FK CASCADE)
snapshot_date TIMESTAMP, INDEX one row per asset per day
avs FLOAT Asset Vulnerability Score 0-100
ass FLOAT Asset Security Score 0-100
urs FLOAT Unified Risk Score 0-100
severity VARCHAR(16) none / low / medium / high / critical
criticality_factor FLOAT snapshot of the multiplier at compute time

Pruned > 90 days by the 04:00 UTC nightly job.

ai_analyses

Column Type Notes
id, vulnerability_id (FK CASCADE)
analysis_text TEXT LLM output
threat_level VARCHAR(20) low/medium/high/critical
exploits_found, workarounds, remediation_steps, threat_intel_sources TEXT (JSON arrays)
model_version VARCHAR(50)
confidence_score VARCHAR(20)
analysis_timestamp TIMESTAMP
cache_expires_at TIMESTAMP TTL for forced refresh

ai_reports

Column Type Notes
id, created_at, created_by_id (FK users.id)
global_strategy TEXT LLM-produced strategy summary
recommendations TEXT JSON array

Constraints and Foreign Keys

  • vulnerabilities.asset_idassets.id (NO CASCADE — vulns survive asset reassignment)
  • compliance_results.asset_idassets.id (ON DELETE CASCADE)
  • compliance_checks.result_idcompliance_results.id (ON DELETE CASCADE)
  • asset_risk_snapshots.asset_idassets.id (ON DELETE CASCADE)
  • notification_logs.{vulnerability,asset,user}_id → NULLABLE — log preserved if entity deleted
  • audit_logs.user_id → NULLABLE for the same reason
  • compliance_impacts UNIQUE(cis_id, benchmark)

PostgreSQL enum types created per ENUM column: userrole, authprovider, assetsource, assetstatus, vulnseverity, vulnstatus, scantype, scanstatus, scheduleinterval, policystatus, notificationtype, notificationstatus, auditeventtype.

⚠️ Enums on Postgres: SQLAlchemy defaults to mapping by NAME, but several enums are stored by VALUE (AuthProvider, ScanType since 019 unification). These columns declare SQLEnum(..., values_callable=lambda x: [e.value for e in x]) to keep the postgres type and python enum in sync.


Migration History

Rev Title Notes
001 initial_schema Users, assets, vulns, scans, policies
002 combined_updates Status / severity refinements
003 add_user_deleted_audit_event AuditEventType extension
004 add_ai_reports ai_reports table
005 add_asset_groups_and_policies M:N + policy_id
006 lowercase_enum_values Normalises legacy mixed-case enums
007 add_enrichment_fields EPSS / KEV
008 add_euvd_fields EUVD + critical flag
009 multi_provider_auth LDAP / OIDC / SAML columns, role mappings
010 add_nessus_integration sources, nessus_plugin_id, first_detected_by, widens cve_id to 50
011 add_nessus_vpr_score Tenable VPR column
012 add_exploitation_status SSVC exploitation_status
013 add_ssvc_technical_impact_automatable SSVC technical_impact + automatable
014 add_nessus_scan_type Adds nessus enum value to scantype
015 fix_nessus_scan_type_uppercase Postgres enum uppercase NESSUS
016 add_compliance_tables compliance_results + compliance_checks
017 add_urs_schema compliance_impacts, asset_risk_snapshots, assets.criticality, assets.compliance_frameworks, compliance_results.weighted_score
018 heal_compliance_sequences self-heals compliance_results.id / compliance_checks.id sequences
019 unify_scan_type_wazuh Adds enum value wazuh to scantype (DDL-only)
020 backfill_wazuh_scan_type no-op — replaced by _display_scan_type() API-layer mapping (see commit 06f1615)
021 drop_manual_scan_type_rows Removes orphaned scan rows with legacy MANUAL type
022 backfill_ssvc_only_exploitation_source Pins exploitation_source='vulnrichment' on rows that have SSVC but no source pin

Migration commands

# Apply all pending
docker compose exec backend alembic upgrade head

# Show current head
docker compose exec backend alembic current

# Step back one
docker compose exec backend alembic downgrade -1

# Generate new (autogenerate from models)
docker compose exec backend alembic revision --autogenerate -m "add foo column"

Performance Optimisations

  • B-tree indexes on filter-heavy columns (cve_id, severity, status, kev_listed, euvd_listed, epss_score, exploitation_status, ssvc_*, nessus_vpr_score, package_name, detected_at).
  • Composite-effective queries on (asset_id, status) use the asset_id + status indexes plus PostgreSQL's bitmap-AND.
  • JSON columns (sources, enrichment_sources, references) keep the row narrow; LIKE queries on them are used sparingly (e.g. sources LIKE '%"wazuh"%').
  • asset_risk_snapshots pruning at 90 days keeps the trend-arrow query bounded.
  • kev_cache_json / euvd_cache_json in settings table cache external feeds 24 h, avoiding per-vuln HTTP fetches during enrichment.

Backup Strategy

# Nightly logical dump
docker compose exec postgres pg_dump -U vulnmanager vulnmanager \
    | gzip > /backup/vulnmanager-$(date +%F).sql.gz

# Point-in-Time Recovery (PITR): enable WAL archiving in postgresql.conf
#   wal_level = replica
#   archive_mode = on
#   archive_command = 'cp %p /backup/wal/%f'

Restoring a single table is straightforward via pg_restore -t from the dump. Full DR test recommended quarterly.