feat(scan): cover Adobe Acrobat via cvelistV5, and stop Defender flapping

Two findings from the same report.

Adobe had no cvelistV5 entry at all — only the CPE path, which meant current
versions depended entirely on NVD having published a CPE yet. cvelistV5 names
the product "Adobe" / "Acrobat Reader": no DC suffix, and no Reader-vs-Acrobat
split, because Adobe stopped shipping them apart — APSB26-63 covers both and
links the same release notes for either. NVD meanwhile keeps the older _dc
spellings alive in parallel, which is why 2dc60be made the CPE path query both
names. Verified against CVE-2026-47965 / -47911 / -47961: affected up to and
including 26.001.21651, and the bound resolves correctly against that build.
Wazuh only detects ancient Reader builds (wazuh/wazuh#29960), so these two
paths are the entire coverage for current versions.

Separately, Defender findings flapped open and closed within one sync. The
auto-resolve ran per MACHINE, but several Defender machines can map to one
asset — a re-imaged or dual-registered device keeps its old machine entry. The
machine that no longer lists a CVE closed the finding; the one that still
lists it reopened it a minute later; next sync the same again (tester:
CVE-2026-66313, patched 13:41, open 13:42, patched 15:00). The CVE sets are
now unioned per asset and resolved once, after every machine has been asked.

Index key bumped to v14.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 15:53:03 +02:00
co-authored by Claude Opus 5
parent 2dc60becd6
commit 3e6c15cef6
2 changed files with 35 additions and 5 deletions
+12 -1
View File
@@ -42,7 +42,7 @@ logger = logging.getLogger(__name__)
_ZIP_PATH = "/tmp/truevuln-cvelistv5-cache.zip"
_ZIP_URL = "https://github.com/CVEProject/cvelistV5/archive/refs/heads/main.zip"
_ZIP_TTL = 12 * 3600
_INDEX_SETTING = "cvelistv5_product_index_v13" # v13: + exchange-se
_INDEX_SETTING = "cvelistv5_product_index_v14" # v14: + adobe-acrobat
_INDEX_TTL = timedelta(hours=26) # rebuilt nightly; a missed night still serves
# Curated registry: name-regex (installed software) → cvelistV5 (vendor,
@@ -89,6 +89,17 @@ _REGISTRY: List[dict] = [
{"key": "checkmk", "re": r"checkmk agent|check_mk agent|checkmk(?!.*server)",
"pairs": [("checkmk gmbh", "checkmk"), ("checkmk", "checkmk"),
("tribe29", "checkmk"), ("checkmk gmbh", "checkmk agent")]},
# Adobe Acrobat. cvelistV5 names it "Adobe" / "Acrobat Reader" — no DC
# suffix, no separate Reader vs Acrobat product: Adobe stopped shipping
# them apart and one bulletin (APSB26-63) now covers both, linking the same
# release notes for either. NVD still keeps the older _dc spellings alive
# in parallel, which is why the CPE path queries both names.
# Wazuh only detects ancient Reader builds (wazuh/wazuh#29960), so these
# two paths are all the coverage current versions get.
{"key": "adobe-acrobat", "re": r"adobe acrobat|acrobat reader",
"pairs": [("adobe", "acrobat reader"), ("adobe", "acrobat"),
("adobe", "acrobat reader dc"), ("adobe", "acrobat dc"),
("adobe", "adobe acrobat reader"), ("adobe", "adobe acrobat")]},
{"key": "7-zip", "re": r"7-?zip",
"pairs": [("7-zip", "7-zip"), ("igor pavlov", "7-zip")]},
# Require the vendor word: match "Mozilla Firefox" (and "Mozilla Firefox
+23 -4
View File
@@ -218,6 +218,8 @@ def run_defender_sync(db: Session) -> dict:
except Exception as e:
logger.debug("defender software map build failed: %s", e)
# asset id → the union of CVEs every machine behind it reported.
seen_by_asset: dict = {}
for m in machines:
stats["machines"] += 1
asset = _match_asset(db, m)
@@ -236,15 +238,32 @@ def run_defender_sync(db: Session) -> dict:
_upsert_cve(db, asset, v, new_ids,
software=sw.get("label"), vendor=sw.get("vendor"))
stats["cve_rows"] += 1
# Auto-resolve defender-only findings this machine no longer reports.
# Guarded to non-empty responses so a transient/clean read can't
# mass-close (same safety as the Nessus/app-scan backfills).
# Collect, resolve later. Several Defender machines can map to ONE
# asset — a re-imaged or dual-registered device keeps its old
# machine entry — and resolving per machine made them fight: the
# machine that no longer lists the CVE closes the finding, the one
# that still lists it reopens it a minute later, every sync
# (tester: CVE-2026-66313, patched 13:41, open 13:42, patched
# 15:00). A finding may only be closed once EVERY machine behind
# the asset has been asked.
seen_by_asset.setdefault(asset.id, {"asset": asset, "cves": set(),
"any": False})
seen_by_asset[asset.id]["cves"] |= seen_cves
if seen_cves:
stats["resolved"] = stats.get("resolved", 0) + _resolve_stale(db, asset, seen_cves)
seen_by_asset[asset.id]["any"] = True
except Exception as e:
stats["errors"].append(f"machine {m.get('computerDnsName')}: {e}")
db.commit()
# Every machine has been asked, so each asset's CVE union is complete now.
# Guarded to non-empty responses so a transient or clean read can't
# mass-close (same safety as the Nessus and app-scan backfills).
for entry in seen_by_asset.values():
if entry["any"]:
stats["resolved"] = stats.get("resolved", 0) + _resolve_stale(
db, entry["asset"], entry["cves"])
db.commit()
client.close()
stats["new"] = len(new_ids)