fix(sap): require the release to match, not just the patch level
With the real syscollector rows in hand the level-only match turns out to be
a false-positive generator. Every SAP release carries the same patch level
NUMBERS — NVD lists gui_for_windows 7.70:patch_level17 right next to
8.0:patch_level1 — so the tester's host (SAP GUI for Windows 8.00 64bit,
Patch 17) matched every 7.70 CVE that happens to have a patch_level17 entry.
The release is now compared as well, and numerically: Wazuh reports "8.00"
where NVD writes "8.0", so a string compare would have rejected the host's
own release.
The parser is confirmed against the verbatim inventory strings, which differ
per product — Business Client states the level in the version field
("8.00 PL26") while SAP GUI states a compilation there and puts the level in
the NAME ("SAP GUI for Windows 8.00 64bit (Patch 17)"). Both are pinned in
the test, along with "Compilation" not reading as a level despite containing
the letters p-l.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -303,12 +303,23 @@ def _sap_patch_level(name: str, version: str) -> Optional[int]:
|
||||
return None
|
||||
|
||||
|
||||
def _sap_affected(m: dict, pl: Optional[int]) -> bool:
|
||||
"""True if this cpeMatch names exactly the host's patch level."""
|
||||
def _sap_affected(m: dict, installed: str, pl: Optional[int]) -> bool:
|
||||
"""True if this cpeMatch names the host's release AND its patch level.
|
||||
|
||||
BOTH halves are required. The same patch-level number exists under every
|
||||
release — NVD lists gui_for_windows 7.70:patch_level17 next to
|
||||
8.0:patch_level1 — so matching on the level alone puts a 7.70 CVE on an
|
||||
8.00 host. The releases are also spelled differently on the two sides
|
||||
(Wazuh reports "8.00", NVD writes "8.0"), which is why this compares them
|
||||
numerically instead of as strings.
|
||||
"""
|
||||
if pl is None:
|
||||
return False
|
||||
parts = (m.get("criteria") or "").split(":")
|
||||
ver = parts[5] if len(parts) > 5 else "*"
|
||||
upd = parts[6] if len(parts) > 6 else "*"
|
||||
if ver in ("*", "-", "") or _vcmp(installed, ver) != 0:
|
||||
return False
|
||||
hit = _SAP_CPE_PL_RE.match(upd or "")
|
||||
if hit:
|
||||
return int(hit.group(1)) == pl
|
||||
@@ -408,7 +419,7 @@ def _query_nvd_cpe(cpe: str, version: str, sap_pl: Optional[int] = None) -> List
|
||||
continue
|
||||
if not m.get("vulnerable", True):
|
||||
continue
|
||||
ok = (_sap_affected(m, sap_pl) if sap_pl is not None
|
||||
ok = (_sap_affected(m, version, sap_pl) if sap_pl is not None
|
||||
else _in_range(version, m))
|
||||
if ok:
|
||||
matched = True
|
||||
|
||||
@@ -14,11 +14,18 @@ from app.services.app_cve_scanner_service import _sap_affected, _sap_patch_level
|
||||
|
||||
|
||||
def demo():
|
||||
# Inventory spellings seen in the wild (wazuh/wazuh#30334: "PL" in the
|
||||
# version field, "Patch [N]" in the product name).
|
||||
# VERBATIM syscollector rows from the tester's SAP host — the level lives
|
||||
# in the version field for Business Client and in the NAME for SAP GUI,
|
||||
# whose version field states a compilation instead.
|
||||
assert _sap_patch_level("SAP Business Client 8.00", "8.00 PL26") == 26
|
||||
assert _sap_patch_level("SAP GUI for Windows 8.00 64bit (Patch 17)",
|
||||
"8.00 Compilation 1") == 17
|
||||
assert _sap_patch_level("Adobe LiveCycle Designer 11.0 for SAP solutions",
|
||||
"11.0 Patch Level 37") == 37
|
||||
# "Compilation" contains the letters p-l; it must not read as a level.
|
||||
assert _sap_patch_level("SAP GUI for Windows 8.00", "8.00 Compilation 1") is None
|
||||
# Other spellings from wazuh/wazuh#30334.
|
||||
assert _sap_patch_level("SAP GUI for Windows 7.70", "7.70 PL 12") == 12
|
||||
assert _sap_patch_level("SAP GUI for Windows 7.70", "7.70 PL12") == 12
|
||||
assert _sap_patch_level("SAP GUI for Windows 7.70 (Patch 4)", "7.70") == 4
|
||||
assert _sap_patch_level("SAP Business Client 6.0", "6.0 Patch Level 16") == 16
|
||||
# No level stated anywhere → unknown, never a guess.
|
||||
assert _sap_patch_level("SAP Business Client", "6.0") is None
|
||||
@@ -26,19 +33,30 @@ def demo():
|
||||
|
||||
# Real criteria strings from CVE-2023-32113 / CVE-2021-38150.
|
||||
pl4 = {"criteria": "cpe:2.3:a:sap:gui_for_windows:7.70:patch_level4:*:*:*:*:*:*"}
|
||||
pl17 = {"criteria": "cpe:2.3:a:sap:gui_for_windows:7.70:patch_level17:*:*:*:*:*:*"}
|
||||
v8pl1 = {"criteria": "cpe:2.3:a:sap:gui_for_windows:8.0:patch_level1:*:*:*:*:*:*"}
|
||||
pl10 = {"criteria": "cpe:2.3:a:sap:business_client:6.0:patch_level10:*:*:*:*:*:*"}
|
||||
base = {"criteria": "cpe:2.3:a:sap:business_client:6.0:-:*:*:*:*:*:*"}
|
||||
|
||||
assert _sap_affected(pl4, 4)
|
||||
assert not _sap_affected(pl4, 5) # patched past it
|
||||
assert not _sap_affected(pl4, 3) # a DIFFERENT level, not "older"
|
||||
assert _sap_affected(pl10, 10)
|
||||
assert _sap_affected(base, 0) # ':-' is the unpatched base release
|
||||
assert not _sap_affected(base, 1)
|
||||
assert _sap_affected(pl4, "7.70", 4)
|
||||
assert not _sap_affected(pl4, "7.70", 5) # patched past it
|
||||
assert not _sap_affected(pl4, "7.70", 3) # a DIFFERENT level, not "older"
|
||||
assert _sap_affected(pl10, "6.0", 10)
|
||||
assert _sap_affected(base, "6.0", 0) # ':-' is the unpatched base
|
||||
assert not _sap_affected(base, "6.0", 1)
|
||||
|
||||
# The release has to match too. Every release carries the same level
|
||||
# NUMBERS, so level-only matching drops a 7.70 CVE onto an 8.00 host —
|
||||
# exactly the tester's box (SAP GUI 8.00, Patch 17).
|
||||
assert not _sap_affected(pl17, "8.00", 17)
|
||||
assert _sap_affected(pl17, "7.70", 17)
|
||||
# Wazuh says "8.00", NVD says "8.0" — same release, compared numerically.
|
||||
assert _sap_affected(v8pl1, "8.00", 1)
|
||||
assert not _sap_affected(v8pl1, "7.70", 1)
|
||||
|
||||
# The whole point: unknown level matches nothing at all.
|
||||
for m in (pl4, pl10, base):
|
||||
assert not _sap_affected(m, None)
|
||||
for m in (pl4, pl10, base, v8pl1):
|
||||
assert not _sap_affected(m, "7.70", None)
|
||||
|
||||
# A SAP CPE with a WILDCARD update field states no patch level, so it says
|
||||
# nothing about patch state — CVE-2021-38175 lists
|
||||
@@ -46,7 +64,7 @@ def demo():
|
||||
# that as a hit marks every 2.8 build affected forever. Never match it.
|
||||
wild = {"criteria": "cpe:2.3:a:sap:analysis_for_microsoft_office:2.8:*:*:*:*:*:*:*"}
|
||||
for pl in (None, 0, 3, 99):
|
||||
assert not _sap_affected(wild, pl)
|
||||
assert not _sap_affected(wild, "2.8", pl)
|
||||
|
||||
print("sap patch level OK")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user