Files
vulncheck/tests/test_sap_patch_level.py
T
vulncheckandClaude Opus 5 d1badf70ed 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>
2026-07-29 14:57:17 +02:00

74 lines
3.5 KiB
Python

"""SAP patch-level matching — run: python tests/test_sap_patch_level.py
SAP enumerates one CPE per patch level instead of stating a range, so
"affected" is set membership. The dangerous failure is silent: if the level
can't be read, the base release (7.70) matches every CVE ever filed against
it. Unknown level must therefore report NOTHING.
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.services.app_cve_scanner_service import _sap_affected, _sap_patch_level
def demo():
# 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 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
assert _sap_patch_level("", "") is None
# 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, "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, 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
# `analysis_for_microsoft_office:2.8:*` with no range at all, and treating
# 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, "2.8", pl)
print("sap patch level OK")
if __name__ == "__main__":
demo()