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:
@@ -42,7 +42,7 @@ logger = logging.getLogger(__name__)
|
|||||||
_ZIP_PATH = "/tmp/truevuln-cvelistv5-cache.zip"
|
_ZIP_PATH = "/tmp/truevuln-cvelistv5-cache.zip"
|
||||||
_ZIP_URL = "https://github.com/CVEProject/cvelistV5/archive/refs/heads/main.zip"
|
_ZIP_URL = "https://github.com/CVEProject/cvelistV5/archive/refs/heads/main.zip"
|
||||||
_ZIP_TTL = 12 * 3600
|
_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
|
_INDEX_TTL = timedelta(hours=26) # rebuilt nightly; a missed night still serves
|
||||||
|
|
||||||
# Curated registry: name-regex (installed software) → cvelistV5 (vendor,
|
# 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)",
|
{"key": "checkmk", "re": r"checkmk agent|check_mk agent|checkmk(?!.*server)",
|
||||||
"pairs": [("checkmk gmbh", "checkmk"), ("checkmk", "checkmk"),
|
"pairs": [("checkmk gmbh", "checkmk"), ("checkmk", "checkmk"),
|
||||||
("tribe29", "checkmk"), ("checkmk gmbh", "checkmk agent")]},
|
("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",
|
{"key": "7-zip", "re": r"7-?zip",
|
||||||
"pairs": [("7-zip", "7-zip"), ("igor pavlov", "7-zip")]},
|
"pairs": [("7-zip", "7-zip"), ("igor pavlov", "7-zip")]},
|
||||||
# Require the vendor word: match "Mozilla Firefox" (and "Mozilla Firefox
|
# Require the vendor word: match "Mozilla Firefox" (and "Mozilla Firefox
|
||||||
|
|||||||
@@ -218,6 +218,8 @@ def run_defender_sync(db: Session) -> dict:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug("defender software map build failed: %s", 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:
|
for m in machines:
|
||||||
stats["machines"] += 1
|
stats["machines"] += 1
|
||||||
asset = _match_asset(db, m)
|
asset = _match_asset(db, m)
|
||||||
@@ -236,15 +238,32 @@ def run_defender_sync(db: Session) -> dict:
|
|||||||
_upsert_cve(db, asset, v, new_ids,
|
_upsert_cve(db, asset, v, new_ids,
|
||||||
software=sw.get("label"), vendor=sw.get("vendor"))
|
software=sw.get("label"), vendor=sw.get("vendor"))
|
||||||
stats["cve_rows"] += 1
|
stats["cve_rows"] += 1
|
||||||
# Auto-resolve defender-only findings this machine no longer reports.
|
# Collect, resolve later. Several Defender machines can map to ONE
|
||||||
# Guarded to non-empty responses so a transient/clean read can't
|
# asset — a re-imaged or dual-registered device keeps its old
|
||||||
# mass-close (same safety as the Nessus/app-scan backfills).
|
# 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:
|
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:
|
except Exception as e:
|
||||||
stats["errors"].append(f"machine {m.get('computerDnsName')}: {e}")
|
stats["errors"].append(f"machine {m.get('computerDnsName')}: {e}")
|
||||||
db.commit()
|
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()
|
client.close()
|
||||||
stats["new"] = len(new_ids)
|
stats["new"] = len(new_ids)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user