fix(override): pin exploitation_source on any field change, not just cvss

Tester reported only 4 hits for filter
  exploitation_source='vulnrichment' AND exploitation_status != 'none'
while ssvc_technical_impact='total' returned 4682 and
ssvc_automatable='yes' returned 842 rows. Mismatch by orders of
magnitude.

Root cause: _apply_single_override only set vuln.exploitation_source
inside the CVSS and severity change blocks. SSVC writes
(exploitation_status, ssvc_technical_impact, ssvc_automatable) went
through their own branches without touching the source label. So a
CVE whose Wazuh CVSS happened to already match Vulnrichment got SSVC
fields written but exploitation_source stayed NULL.

Two-part fix:

1. _apply_single_override now sets exploitation_source whenever ANY
   tracked field changed (single guard at the end of the function
   replaces the two redundant assignments inside CVSS/severity blocks
   — they still work because changes['has_changes'] is True there).

2. Migration 022 backfills exploitation_source='vulnrichment' on
   every row that has ANY SSVC field populated but no source yet.
   Idempotent. Existing nvd / cvelistv5 / manual source labels are
   not touched (WHERE exploitation_source IS NULL).

After deploy + alembic upgrade head, the tester's filter will
return the real count (~840 SSVC-marked CVEs from vulnrichment,
not just the 4 with CVSS-diff coincidence).
This commit is contained in:
2026-05-20 14:13:45 +02:00
parent dd16697602
commit f3a5e1e89c
2 changed files with 58 additions and 0 deletions
@@ -0,0 +1,48 @@
"""Backfill exploitation_source for SSVC-only override rows
Revision ID: 022
Revises: 021
Create Date: 2026-05-20 11:00:00.000000
Earlier override-service runs wrote SSVC fields (ssvc_technical_impact,
ssvc_automatable, exploitation_status) without setting
exploitation_source. The pin only happened when CVSS or severity
changed.
Result: operator filter
WHERE exploitation_source='vulnrichment' AND exploitation_status != 'none'
under-reported by orders of magnitude (4 vs ~840 in tester's DB).
Stamp those rows as vulnrichment-sourced so the filter works. Only
touch rows that have at least one Vulnrichment-supplied SSVC field
AND no exploitation_source yet — does not overwrite an existing
source label (e.g. nvd / cvelistv5 / manual).
Idempotent — second run finds no candidates.
"""
from alembic import op
revision = '022'
down_revision = '021'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute("""
UPDATE vulnerabilities
SET exploitation_source = 'vulnrichment'
WHERE exploitation_source IS NULL
AND (
exploitation_status IS NOT NULL
OR ssvc_technical_impact IS NOT NULL
OR ssvc_automatable IS NOT NULL
)
""")
def downgrade() -> None:
# Cannot reliably distinguish backfilled rows from real vulnrichment
# writes, so the downgrade is a no-op.
pass
+10
View File
@@ -884,6 +884,16 @@ class VulnOverrideService:
vuln.ssvc_automatable = verified.ssvc_automatable
changes["has_changes"] = True
# Source pin — set exploitation_source whenever ANY override field
# changed, not just CVSS/severity. Earlier behaviour only pinned the
# source on CVSS/severity diffs, so a CVE whose Wazuh CVSS happened
# to already match Vulnrichment got SSVC fields written but
# exploitation_source stayed NULL. Tester filter
# WHERE exploitation_source='vulnrichment' AND exploitation_status != 'none'
# then under-reported by orders of magnitude (4 vs the real ~840).
if changes["has_changes"] and not dry_run:
vuln.exploitation_source = verified.source or "vulnrichment"
return changes
def correct_all_vulnerabilities(