fix(scan): a companion product is not the product it names
"Veeam Explorer for Microsoft Teams" 13.3.2.3 collected four Teams CVEs whose bounds are 25122.x — Veeam's own software, matched purely because its name contains "Microsoft Teams". Checking the rest of the registry against the same shape found four more: Chrome findings on "Citrix Workspace app for Google Chrome", Firefox findings on "Kaspersky Plugin for Mozilla Firefox", and so on. Backup tools, plugins and connectors are all named this way. The product being described stands FIRST, so a match that lands only in the "…for <product>" tail is dropped. Matching itself still runs on the full name, which matters: the Firefox entry rejects "Firefox for iOS" itself (WebKit, not Gecko, different CVEs), and trimming the name before matching hid that from it — caught by test_mobile_browsers. Teams is anchored to the start of the name as well, so a product that merely mentions it later cannot match even without a "for". Both scanner paths share the rule, so they cannot disagree about the same install.
This commit is contained in:
@@ -181,7 +181,7 @@ _REGISTRY: List[tuple] = [
|
||||
# Only Teams itself — NOT the Office add-in, the VDI/Citrix plugin, or the
|
||||
# machine-wide installer (separate products with their own versioning that
|
||||
# would false-positive against Teams-app CVE ranges).
|
||||
_cpe(r"microsoft teams(?!.*(machine-wide|add-in|plugin|vdi|citrix))", "microsoft:teams"),
|
||||
_cpe(r"^microsoft teams(?!.*(machine-wide|add-in|plugin|vdi|citrix))", "microsoft:teams"),
|
||||
# Exchange Server Subscription Edition — a full blind spot in Wazuh
|
||||
# (wazuh/wazuh#36200). ANCHORED and exact on purpose: the same host also
|
||||
# carries "Microsoft Exchange Server" and a dozen "… Language Pack - X"
|
||||
@@ -300,13 +300,44 @@ def _platform_ok(tsws, plat: Optional[str]) -> bool:
|
||||
return bool(plat) and plat in tsws
|
||||
|
||||
|
||||
# "<something> for <product>" names a companion, not the product. Backup tools,
|
||||
# plugins and connectors are all built this way: "Veeam Explorer for Microsoft
|
||||
# Teams" is Veeam's software, and its 13.3.2.3 has nothing to do with Teams'
|
||||
# 25122.x — the tester saw four Teams CVEs land on it. The same shape produced
|
||||
# Chrome findings on "Citrix Workspace app for Google Chrome" and Firefox
|
||||
# findings on "Kaspersky Plugin for Mozilla Firefox".
|
||||
#
|
||||
# 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.
|
||||
_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)
|
||||
|
||||
|
||||
def companion_prefix(name: str) -> str:
|
||||
"""The product part of a "<x> for <y>" name, or the name unchanged."""
|
||||
parts = _COMPANION_RE.split(name or "", maxsplit=1)
|
||||
head = (parts[0] or "").strip()
|
||||
return head if len(parts) > 1 and head else (name or "")
|
||||
|
||||
|
||||
def resolve_product(name: str) -> Optional[dict]:
|
||||
n = (name or "").strip().lower()
|
||||
if not n:
|
||||
return None
|
||||
# Matching runs on the FULL name so every exclusion in the registry still
|
||||
# sees what it was written against — "Firefox for iOS" is rejected by the
|
||||
# Firefox entry itself (WebKit, different CVEs), and trimming the name
|
||||
# first would have hidden that from it. The prefix only decides WHERE the
|
||||
# match landed: in the product, or in the "…for <product>" tail.
|
||||
head = companion_prefix(n)
|
||||
for rx, entry in _REGISTRY:
|
||||
m = rx.search(n)
|
||||
if m:
|
||||
if head != n and not rx.search(head):
|
||||
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
|
||||
# describe the release train, not ESR (NVD tracks ESR as its
|
||||
|
||||
@@ -245,7 +245,7 @@ _REGISTRY: List[dict] = [
|
||||
# products are dropped so a Mac-only Teams CVE can't land on a Windows host
|
||||
# (the per-scan platform filter is the general backstop, but not every CNA
|
||||
# fills the platforms field, so scoping the pairs is belt-and-suspenders).
|
||||
{"key": "teams", "re": r"microsoft teams(?!.*(machine-wide|add-in|plugin|vdi|citrix))",
|
||||
{"key": "teams", "re": r"^microsoft teams(?!.*(machine-wide|add-in|plugin|vdi|citrix))",
|
||||
"pairs": [("microsoft", "microsoft teams for desktop"),
|
||||
("microsoft", "microsoft teams for windows"),
|
||||
("microsoft", "microsoft teams"), ("microsoft", "teams")]},
|
||||
@@ -328,8 +328,13 @@ def resolve(name: str) -> Optional[str]:
|
||||
n = (name or "").strip().lower()
|
||||
if not n:
|
||||
return None
|
||||
# Full name for matching (exclusions depend on it), prefix only to tell a
|
||||
# companion product apart — see cpe.companion_prefix / resolve_product.
|
||||
head = cpe.companion_prefix(n)
|
||||
for rx, e in _COMPILED:
|
||||
if rx.search(n):
|
||||
if head != n and not rx.search(head):
|
||||
continue
|
||||
if e["key"] == "firefox" and re.search(r"\besr\b", n):
|
||||
# ESR install ("Mozilla Firefox 52.2.1 ESR"): the cvelistV5
|
||||
# Firefox ranges describe the RELEASE train (fresh records list
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
""""<x> for <y>" is a companion product, not the product itself.
|
||||
|
||||
"Veeam Explorer for Microsoft Teams" 13.3.2.3 collected four Teams CVEs whose
|
||||
bounds are 25122.x — Veeam's software, matched because its name contains
|
||||
"Microsoft Teams". Backup tools, plugins and connectors are all named this way,
|
||||
and the same shape put Chrome findings on "Citrix Workspace app for Google
|
||||
Chrome" and Firefox findings on "Kaspersky Plugin for Mozilla Firefox".
|
||||
|
||||
The product being described always comes FIRST, so only that part is matched.
|
||||
"""
|
||||
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 ( # noqa: E402
|
||||
companion_prefix, resolve_product)
|
||||
from app.services import cvelistv5_scan_service as c5 # noqa: E402
|
||||
|
||||
|
||||
def demo():
|
||||
# The split itself.
|
||||
assert companion_prefix("Veeam Explorer for Microsoft Teams") == "Veeam"
|
||||
assert companion_prefix("Citrix Workspace app for Google Chrome") == "Citrix Workspace"
|
||||
assert companion_prefix("Microsoft Teams") == "Microsoft Teams"
|
||||
# No "for" → untouched, including names with punctuation and editions.
|
||||
for name in ("Microsoft Visual C++ 2013 Redistributable (x86) - 12.0.21005",
|
||||
"7-Zip 19.00 (x64 edition)", "Adobe Acrobat (64-bit)"):
|
||||
assert companion_prefix(name) == name, name
|
||||
|
||||
# Companions must not resolve to the product they name.
|
||||
for name in ("Veeam Explorer for Microsoft Teams",
|
||||
"Veeam Backup for Microsoft 365",
|
||||
"Citrix Workspace app for Google Chrome",
|
||||
"Kaspersky Plugin for Mozilla Firefox",
|
||||
"Nagios Plugin for Node.js"):
|
||||
assert resolve_product(name) is None, name
|
||||
assert c5.resolve(name) is None, name
|
||||
|
||||
# The real products still resolve on both paths.
|
||||
for name, cpe_key, c5_key in (
|
||||
("Microsoft Teams", "cpe:microsoft:teams", "teams"),
|
||||
("Google Chrome", "cpe:google:chrome", "chrome"),
|
||||
("Mozilla Firefox", "cpe:mozilla:firefox", "firefox"),
|
||||
("Node.js", "cpe:nodejs:node.js", "nodejs"),
|
||||
("7-Zip 19.00 (x64 edition)", "cpe:7-zip:7-zip", "7-zip")):
|
||||
assert (resolve_product(name) or {}).get("key") == cpe_key, name
|
||||
assert c5.resolve(name) == c5_key, name
|
||||
|
||||
# Teams is also anchored now: the name has to START with it, so a product
|
||||
# that merely mentions Teams later cannot match even without a "for".
|
||||
assert resolve_product("Some Vendor Microsoft Teams Toolkit") is None
|
||||
|
||||
print("ok companion products no longer inherit the product's CVEs")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo()
|
||||
Reference in New Issue
Block a user