feat(app-scan): Veeam Agent for Microsoft Windows on both paths
KB4902 (CVE-2026-58068, CVE-2025-64391) sits at NVD "Awaiting Analysis", so cvelistV5 is the only structured source; the older agent CVEs (CVE-2025-48982, CVE-2024-29853) are NVD's under veeam:veeam_agent_for_windows, the only CPE NVD has for this agent. - New veeam-agent-windows key and CPE, anchored to the agent row. The "Redistributable" beside it is the installer payload Veeam keeps for deploying the agent, not the agent that runs. - "Veeam Agent for ..." reads as a companion of Windows and was cut down to "Veeam", so the name never resolved. A whole_name flag on the entry lets a pattern that spells out the full name skip that rule. - The agent records write "0 .. < 13.1" for "all earlier version 13 and 6 builds"; the 0 floor becomes 6, the oldest line Veeam names. 13.1 is build 13.1.0.544, the first 13.1 build, so the bound needs no build. - CVE-2026-32996 (KB4852) is an agent flaw filed under Backup and Replication and fixed by B&R 13.0.2.29 with no agent build; neither product takes it. - cvelistV5 index bumped to v52.
This commit is contained in:
@@ -95,9 +95,13 @@ _PDFX_CVE_2026_2040 = {
|
||||
def _cpe(name_re: str, vendor_product: str, name_ver: bool = False,
|
||||
also: Optional[List[str]] = None, pkg_ver: bool = False,
|
||||
ver_parts: Optional[int] = None,
|
||||
advisories: Optional[List[dict]] = None) -> tuple:
|
||||
advisories: Optional[List[dict]] = None, whole_name: bool = False) -> tuple:
|
||||
e = {"key": f"cpe:{vendor_product}", "kind": "cpe",
|
||||
"cpe": f"cpe:2.3:a:{vendor_product}"}
|
||||
if whole_name:
|
||||
# The pattern spells out the whole "<x> for <y>" name, so the
|
||||
# companion rule does not apply — see companion_prefix.
|
||||
e["whole_name"] = True
|
||||
if advisories:
|
||||
# Vendor-bulletin rows, read by _advisory_cves. Each decides its own
|
||||
# CVE alone, NVD or not, as on the extension registry.
|
||||
@@ -417,6 +421,9 @@ _REGISTRY: List[tuple] = [
|
||||
# _query_nvd_cpe.
|
||||
_cpe(r"^veeam backup (?:&|and) replication$", "veeam:veeam_backup_\\&_replication"),
|
||||
_cpe(r"^veeam one\b(?!.*\bagent\b)", "veeam:one"),
|
||||
# The agent row only, not its Redistributable (the installer payload).
|
||||
_cpe(r"^veeam agent for (?:microsoft )?windows$", "veeam:veeam_agent_for_windows",
|
||||
whole_name=True),
|
||||
# SAP desktop clients. cvelistV5 carries no CPE for these at all, so NVD is
|
||||
# the only usable source (Wazuh misses them too — wazuh/wazuh#30334). SAP
|
||||
# states the patch level in the CPE's UPDATE field, one entry per level, so
|
||||
@@ -864,7 +871,8 @@ def _platform_ok(tsws, plat: Optional[str]) -> bool:
|
||||
# The product being described always stands FIRST, so only that part is matched
|
||||
# against the registry. Names where the product itself carries the word — a
|
||||
# hypothetical "Microsoft Teams for Desktop" — still resolve, because what
|
||||
# precedes "for" is the real product.
|
||||
# precedes "for" is the real product. "Veeam Agent for Microsoft Windows" is
|
||||
# cut down to "Veeam" by the "agent" word, so its entry is marked whole_name.
|
||||
_COMPANION_RE = re.compile(
|
||||
r"\s+(?:plug-?in|add-?in|add-?on|extension|connector|agent|module|"
|
||||
r"integration|explorer|backup|app)?\s*\bfor\b\s+", re.I)
|
||||
@@ -910,7 +918,7 @@ def resolve_product(name: str) -> Optional[dict]:
|
||||
for rx, entry in _REGISTRY:
|
||||
m = rx.search(n)
|
||||
if m:
|
||||
if head != n and not rx.search(head):
|
||||
if head != n and not rx.search(head) and not entry.get("whole_name"):
|
||||
continue # companion product — see companion_prefix
|
||||
if entry.get("key") == "cpe:mozilla:firefox" and re.search(r"\besr\b", n):
|
||||
# ESR install: the plain mozilla:firefox CPE / cvelistV5 ranges
|
||||
|
||||
@@ -44,7 +44,7 @@ _ZIP_PATH = "/tmp/truevuln-cvelistv5-cache.zip"
|
||||
_ZIP_URL = "https://github.com/CVEProject/cvelistV5/archive/refs/heads/main.zip"
|
||||
_ZIP_TTL = 12 * 3600
|
||||
_ZIP_MIN_BYTES = 100_000_000 # sanity floor; the real ZIP is ~557 MB
|
||||
_INDEX_SETTING = "cvelistv5_product_index_v51" # v51: Veeam B&R, Veeam ONE components
|
||||
_INDEX_SETTING = "cvelistv5_product_index_v52" # v52: Veeam Agent for Windows
|
||||
_INDEX_TTL = timedelta(hours=26) # rebuilt nightly; a missed night still serves
|
||||
|
||||
# Curated registry: name-regex (installed software) → cvelistV5 (vendor,
|
||||
@@ -238,6 +238,18 @@ _REGISTRY: List[dict] = [
|
||||
"pairs": [("veeam", "backup and replication"), ("veeam", "backup & replication"),
|
||||
("veeam", "veeam backup & replication"),
|
||||
("veeam", "veeam backup and replication")]},
|
||||
# Veeam Agent for Microsoft Windows. KB4902 (CVE-2026-58068,
|
||||
# CVE-2025-64391) sits at NVD "Awaiting Analysis". Only the agent row: the
|
||||
# "... Redistributable" beside it is the installer payload Veeam keeps for
|
||||
# deploying the agent, not the agent that runs. "whole_name": the
|
||||
# companion rule would cut the name down to "Veeam", see resolve().
|
||||
# "zero_floor": its records write "0 .. < 13.1" for "all earlier version
|
||||
# 13 and 6 builds"; 6 is the oldest line Veeam names, see veeam_affected.
|
||||
{"key": "veeam-agent-windows", "re": r"^veeam agent for (?:microsoft )?windows$",
|
||||
"whole_name": True, "zero_floor": "6",
|
||||
"pairs": [("veeam", "agent for windows"), ("veeam", "agent for microsoft windows"),
|
||||
("veeam", "veeam agent for windows"),
|
||||
("veeam", "veeam agent for microsoft windows")]},
|
||||
# Wazuh's own components. Both sources are needed here as usual: NVD files
|
||||
# 41 of the 54 under wazuh:wazuh, while the CNA records name the product
|
||||
# per component ("wazuh", "wazuh-kibana-app") and state ranges as
|
||||
@@ -945,7 +957,7 @@ def resolve(name: str) -> Optional[str]:
|
||||
head = cpe.companion_prefix(n)
|
||||
for rx, e in _COMPILED:
|
||||
if rx.search(n):
|
||||
if head != n and not rx.search(head):
|
||||
if head != n and not rx.search(head) and not e.get("whole_name"):
|
||||
continue
|
||||
if e["key"] == "firefox" and re.search(r"\besr\b", n):
|
||||
# ESR install ("Mozilla Firefox 52.2.1 ESR"): the cvelistV5
|
||||
@@ -1173,7 +1185,7 @@ def intel_proset_affected(aff: dict) -> dict:
|
||||
return dict(aff, versions=vers)
|
||||
|
||||
|
||||
def veeam_affected(aff: dict) -> dict:
|
||||
def veeam_affected(aff: dict, zero_floor: Optional[str] = None) -> dict:
|
||||
"""Only the entries of a Veeam record that state a real build range.
|
||||
|
||||
Veeam's KBs name builds ("13.0.2.6723 and all earlier version 13 builds"),
|
||||
@@ -1189,6 +1201,9 @@ def veeam_affected(aff: dict) -> dict:
|
||||
13.1.0.7233, KB4951). The rest is left to NVD, whose analysis names the
|
||||
build. Unaffected entries go too, so the inverse rule in
|
||||
_ranges_from_affected cannot turn one into an open range.
|
||||
zero_floor puts a "0" floor where the KB puts it, instead of dropping the
|
||||
entry: the Agent's KB4902 writes "0 .. < 13.1" for "13.0.4.1341 and all
|
||||
earlier version 13 and 6 builds", and nothing below 6 is named.
|
||||
"""
|
||||
vers = []
|
||||
for v in aff.get("versions") or []:
|
||||
@@ -1196,6 +1211,8 @@ def veeam_affected(aff: dict) -> dict:
|
||||
continue
|
||||
ver, lt, lte = (str(v.get(k) or "").strip()
|
||||
for k in ("version", "lessThan", "lessThanOrEqual"))
|
||||
if ver == "0" and zero_floor:
|
||||
v, ver = dict(v, version=zero_floor), zero_floor
|
||||
if ver in ("", "0", "*", "-") or ver in (lt, lte):
|
||||
continue
|
||||
if lt or len((cpe._vtuple(lte) or ())) >= 4:
|
||||
@@ -1688,8 +1705,8 @@ def build_product_index(db: Session, force_fresh: bool = False) -> dict:
|
||||
aff = aruba_affected(aff)
|
||||
if key == "intel-proset-wireless":
|
||||
aff = intel_proset_affected(aff)
|
||||
if key in ("veeam-br", "veeam-one"):
|
||||
aff = veeam_affected(aff)
|
||||
if key in ("veeam-br", "veeam-one", "veeam-agent-windows"):
|
||||
aff = veeam_affected(aff, _KEY_ENTRY[key].get("zero_floor"))
|
||||
if key == "aruba-switch" or (key == "aruba-os" and any(
|
||||
isinstance(v, dict)
|
||||
and _AOSS_PROSE_RE.search(str(v.get("version") or ""))
|
||||
|
||||
+87
-4
@@ -1,4 +1,4 @@
|
||||
"""Veeam Backup & Replication + Veeam ONE — run: venv/bin/python -m pytest tests/test_veeam.py
|
||||
"""Veeam Backup & Replication, Veeam ONE, Veeam Agent for Windows — run: venv/bin/python -m pytest tests/test_veeam.py
|
||||
|
||||
Field case (Wazuh inventory of one backup server):
|
||||
|
||||
@@ -21,6 +21,22 @@ Reporter Server alike) and is updated by the same P1, so the build decides.
|
||||
The Veeam ONE Agent sits on the backup servers Veeam ONE watches, not on the
|
||||
Veeam ONE server, and stays out.
|
||||
|
||||
Veeam Agent for Microsoft Windows (Checkmk inventory, one host):
|
||||
|
||||
Veeam Agent for Microsoft Windows 13.1.1.700 C:\\Program Files\\Veeam\\Endpoint Backup\\
|
||||
Veeam Agent for Microsoft Windows Redistributable 13.1.1.700 C:\\ProgramData\\Veeam\\Agents\\
|
||||
|
||||
KB4902 (CVE-2026-58068, CVE-2025-64391): "13.0.4.1341 and all earlier version
|
||||
13 and 6 builds", fixed in 13.1 (build 13.1.0.544); the records say "0 .. <
|
||||
13.1", and 13.1.0.544 is the first 13.1 build, so both bounds split the
|
||||
released builds alike. NVD has neither analysed yet. Older ones (CVE-2025-48982,
|
||||
CVE-2024-29853) NVD has under veeam:veeam_agent_for_windows, while their CNA
|
||||
records name no usable range. Only the agent row is the agent: the
|
||||
Redistributable is the installer payload Veeam keeps for deploying it, not the
|
||||
software that runs. CVE-2026-32996 (KB4852) is an agent flaw filed under Backup
|
||||
and Replication and fixed by B&R 13.0.2.29; no agent build is named, so
|
||||
neither product can take it.
|
||||
|
||||
Fixtures are the real cvelistV5 blocks and NVD cpeMatch entries (descriptions
|
||||
shortened), except the one marked as the expected shape of the pending NVD
|
||||
analysis.
|
||||
@@ -44,8 +60,11 @@ ONE_CPE = "cpe:2.3:a:veeam:one:*:*:*:*:*:*:*:*"
|
||||
ONE_NAMES = ("Veeam ONE", "Veeam ONE Client", "Veeam ONE Monitor Server",
|
||||
"Veeam ONE Reporter Server", "Veeam ONE Monitor Client",
|
||||
"Veeam ONE Web Client", "Veeam ONE Web Services")
|
||||
AGENT = "Veeam Agent for Microsoft Windows"
|
||||
AGENT_CPE = "cpe:2.3:a:veeam:veeam_agent_for_windows:*:*:*:*:*:*:*:*"
|
||||
NOT_VEEAM_KEYS = ("Veeam Backup & Replication Console", "Veeam Installer Service",
|
||||
"Veeam Mount Service", "Veeam Backup Transport", "Veeam Agent for Microsoft Windows",
|
||||
"Veeam Mount Service", "Veeam Backup Transport",
|
||||
"Veeam Agent for Microsoft Windows Redistributable", "Veeam Agent for Linux",
|
||||
"Veeam Explorer for Microsoft Exchange", "Veeam Backup Enterprise Manager",
|
||||
"Veeam Backup for Microsoft 365", "Veeam ONE Agent")
|
||||
|
||||
@@ -56,8 +75,10 @@ def test_cvelistv5_products():
|
||||
assert c5._pair_key("Veeam", prod) == "veeam-br", prod
|
||||
for prod in ("ONE", "Veeam ONE"):
|
||||
assert c5._pair_key("Veeam", prod) == "veeam-one", prod
|
||||
for prod in ("Agent for Windows", "Agent for Microsoft Windows"):
|
||||
assert c5._pair_key("Veeam", prod) == "veeam-agent-windows", prod
|
||||
for prod in ("Agent for Linux", "Backup for Microsoft 365", "Veeam Service Provider Console"):
|
||||
assert c5._pair_key("Veeam", prod) not in ("veeam-br", "veeam-one"), prod
|
||||
assert c5._pair_key("Veeam", prod) is None, prod
|
||||
|
||||
|
||||
def test_resolves_names():
|
||||
@@ -67,8 +88,13 @@ def test_resolves_names():
|
||||
for name in ONE_NAMES:
|
||||
assert c5.resolve(name) == "veeam-one", name
|
||||
assert cpe.resolve_product(name)["key"] == "cpe:veeam:one", name
|
||||
# "<x> Agent for <y>" reads as a companion of <y> (cpe.companion_prefix);
|
||||
# this one IS the product.
|
||||
for name in (AGENT, "Veeam Agent for Windows"):
|
||||
assert c5.resolve(name) == "veeam-agent-windows", name
|
||||
assert cpe.resolve_product(name)["key"] == "cpe:veeam:veeam_agent_for_windows", name
|
||||
for name in NOT_VEEAM_KEYS:
|
||||
assert c5.resolve(name) not in ("veeam-br", "veeam-one"), name
|
||||
assert c5.resolve(name) is None, name
|
||||
assert not (cpe.resolve_product(name) or {}).get("key", "").startswith("cpe:veeam:"), name
|
||||
|
||||
|
||||
@@ -116,7 +142,33 @@ R = [
|
||||
("CVE-2026-93026", {"vendor": "Veeam", "product": "Backup and Replication",
|
||||
"versions": [{"version": "12", "status": "affected",
|
||||
"lessThan": "12.3.2.4934", "versionType": "semver"}]}),
|
||||
# Veeam Agent for Windows, verbatim. KB4902: "13.0.4.1341 and all earlier
|
||||
# version 13 and 6 builds", so the 0 floor becomes 6, not "everything".
|
||||
("CVE-2026-58068", {"defaultStatus": "unaffected", "vendor": "Veeam",
|
||||
"product": "Agent for Windows",
|
||||
"versions": [{"version": "0", "status": "affected",
|
||||
"lessThan": "13.1", "versionType": "semver"}]}),
|
||||
("CVE-2025-64391", {"defaultStatus": "unaffected", "vendor": "Veeam",
|
||||
"product": "Agent for Windows",
|
||||
"versions": [{"version": "0", "status": "affected",
|
||||
"lessThan": "13.1", "versionType": "semver"}]}),
|
||||
# KB4771: 6.0 .. 6.3.2.1205, fixed in 6.3.2.1302; the record says 4.7.2.
|
||||
("CVE-2025-48982", {"defaultStatus": "unaffected", "vendor": "Veeam",
|
||||
"product": "Agent for Microsoft Windows",
|
||||
"versions": [{"version": "4.7.2", "status": "affected",
|
||||
"lessThanOrEqual": "4.7.2", "versionType": "semver"}]}),
|
||||
# KB4581: fixed in 6.1.2.134; the record's range is empty.
|
||||
("CVE-2024-29853", {"defaultStatus": "unaffected", "vendor": "Veeam",
|
||||
"product": "Agent for Windows",
|
||||
"versions": [{"version": " 6.1.2.134", "status": "affected",
|
||||
"lessThan": " 6.1.2.134", "versionType": "semver"}]}),
|
||||
# KB4852: an agent flaw filed under B&R, see the module docstring.
|
||||
("CVE-2026-32996", {"defaultStatus": "unaffected", "vendor": "Veeam",
|
||||
"product": "Backup and Replication",
|
||||
"versions": [{"version": "13", "status": "affected",
|
||||
"lessThanOrEqual": "13.0.1", "versionType": "semver"}]}),
|
||||
]
|
||||
KB4902 = ["CVE-2025-64391", "CVE-2026-58068"]
|
||||
KB4948 = ["CVE-2026-108140", "CVE-2026-108141", "CVE-2026-108142"]
|
||||
|
||||
|
||||
@@ -209,6 +261,26 @@ def test_cvelistv5_one_bounds():
|
||||
assert hits("Veeam ONE Agent", "13.1.0.7233") == []
|
||||
|
||||
|
||||
AGENT_FIELD = [{"name": AGENT, "version": "13.1.1.700"},
|
||||
{"name": AGENT + " Redistributable", "version": "13.1.1.700"}] * 2
|
||||
|
||||
|
||||
def test_cvelistv5_agent():
|
||||
index = _build_index()
|
||||
assert sorted(e["cve"] for e in index["veeam-agent-windows"]) == KB4902
|
||||
assert _scan_c5(index, AGENT_FIELD) == []
|
||||
old = [dict(p, version="13.0.4.1341") for p in AGENT_FIELD]
|
||||
assert _scan_c5(index, old) == [(AGENT, c, "13.1") for c in KB4902] # once per host
|
||||
hits = lambda v: [c for _, c, _ in _scan_c5(index, [{"name": AGENT, "version": v}])]
|
||||
assert hits("13.0.4.1341") == KB4902 # last affected build in KB4902
|
||||
assert hits("6.3.2.1302") == KB4902 # "all earlier version 13 and 6 builds"
|
||||
assert hits("13.1.0.544") == [] # the fix
|
||||
assert hits("13.1.1.700") == []
|
||||
assert hits("6.0.0.959") == KB4902
|
||||
assert hits("5.0.3.4708") == [] # below every line KB4902 names
|
||||
assert hits("4.7.2.0") == [] # nor CVE-2025-48982: its record's range is no range
|
||||
|
||||
|
||||
# ---- NVD path -----------------------------------------------------------------
|
||||
def _cfg(crit, start, end):
|
||||
return [{"nodes": [{"cpeMatch": [{"vulnerable": True, "criteria": crit,
|
||||
@@ -228,6 +300,12 @@ NVD = [
|
||||
"versionEndExcluding": "13.1.2.57"}]}]}]}},
|
||||
{"cve": {"id": "CVE-2024-42024", "descriptions": [{"lang": "en", "value": "RCE"}],
|
||||
"configurations": _cfg(ONE_CPE, "12.0.0.2498", "12.2.0.4093")}},
|
||||
{"cve": {"id": "CVE-2025-48982", "descriptions": [{"lang": "en", "value": "LPE"}],
|
||||
"configurations": _cfg(AGENT_CPE, "6.0.0.959", "6.3.2.1302")}},
|
||||
{"cve": {"id": "CVE-2024-29853", "descriptions": [{"lang": "en", "value": "LPE"}],
|
||||
"configurations": [{"nodes": [{"cpeMatch": [
|
||||
{"vulnerable": True, "criteria": AGENT_CPE,
|
||||
"versionEndExcluding": "6.1.2.134"}]}]}]}},
|
||||
]
|
||||
|
||||
|
||||
@@ -281,3 +359,8 @@ def test_nvd(monkeypatch):
|
||||
assert _scan_nvd(monkeypatch, "Veeam ONE Reporter Server", "12.1.0.3208") == [
|
||||
("CVE-2024-42024", "12.2.0.4093")]
|
||||
assert _scan_nvd(monkeypatch, "Veeam Backup & Replication Console", "13.1.0.411") == []
|
||||
assert _scan_nvd(monkeypatch, AGENT, "6.1.0.349") == [
|
||||
("CVE-2024-29853", "6.1.2.134"), ("CVE-2025-48982", "6.3.2.1302")]
|
||||
assert _scan_nvd(monkeypatch, AGENT, "6.3.2.1302") == []
|
||||
assert _scan_nvd(monkeypatch, AGENT, "13.1.1.700") == []
|
||||
assert _scan_nvd(monkeypatch, AGENT + " Redistributable", "6.1.0.349") == []
|
||||
|
||||
Reference in New Issue
Block a user