From 81bafda04a5f505683efb8b1ca81296c9d1ba4b3 Mon Sep 17 00:00:00 2001 From: vulncheck Date: Thu, 8 Oct 2026 13:15:00 +0200 Subject: [PATCH] fix(teams): skip the 1.0.0.0 placeholder, keep Teams Panel/Phone off Teams Field case: an Intune host lists "MSTeams 1.0.0.0" (CN=Microsoft Corporation). It drew CVE-2023-4863, -29328, -29330, CVE-2022-21965 and CVE-2025-53783, plus the Teams classic EOL. 1.0.0.0 names no build. It is the new client's MSIX package (Intune's "MSTeams", Wazuh's WindowsApps\MSTeams_1.0.0.0_...) with no version read; no desktop Teams shipped as 1.0.0.0 and classic was never MSIX. Four parts under 100, so it passed the scheme check and sat below every classic and Android bound. teams_placeholder_version (1.0.0.0, 1.0, 1, 0.0.0.0) is now skipped by the cvelistV5 and NVD scans and by _teams_classic. 0a73eafb had read it as classic; "MSTeams" now counts as the new client for EOL, like "Microsoft Teams". Real classic builds (1.5.00.31168) keep both. CVE-2025-53783 came through teams_panels: the NVD filter matched the vendor:product token as a substring, so microsoft:teams also took teams_panels and teams_phones (1.0.9x device builds), and Teams for Android (1.0.0.2025...) got their CVE and the Panel fix 1.0.97.2025102203. Teams now takes its CPE exactly. Other products keep the prefix match; Adobe relies on it (acrobat_dc). NVD cache prefix v5. --- README.DEV.md | 12 ++++++++++ app/services/app_cve_scanner_service.py | 26 +++++++++++++++++++++- app/services/cvelistv5_scan_service.py | 2 ++ app/services/ms_lifecycle_service.py | 11 ++++++++-- tests/test_eol_teams_classic.py | 18 ++++++++++----- tests/test_teams_classic.py | 29 ++++++++++++++++++++++++- tests/test_wazuh_msix_superseded.py | 2 +- tools/diagnose_scan.py | 2 +- 8 files changed, 91 insertions(+), 11 deletions(-) diff --git a/README.DEV.md b/README.DEV.md index 8679ded..c40b756 100644 --- a/README.DEV.md +++ b/README.DEV.md @@ -1440,6 +1440,18 @@ entry, else the first listed. Windows hosts were told the macOS build is skipped: "Teams for Mac" is not paired, so it held only the desktop's ranges; NVD's macOS entries cover the Mac. +Version 1.0.0.0 (also 1.0, 1, 0.0.0.0) is a placeholder, not a build: +Intune's "MSTeams 1.0.0.0" and Wazuh's WindowsApps\MSTeams_1.0.0.0_… are the +new client's MSIX package with no build read. No desktop Teams shipped as +1.0.0.0, and classic was never MSIX. Read as a build it sat below every +classic and Android bound (CVE-2023-29328/-29330/-4863, CVE-2022-21965) and +drew the classic EOL. Both CVE paths and `_teams_classic` skip it +(`teams_placeholder_version`); "MSTeams" otherwise counts as the new client +for EOL, like "Microsoft Teams". On NVD the Teams lookup takes +`microsoft:teams` exactly: `teams_panels` and `teams_phones` (1.0.9x builds) +gave Teams for Android their CVE-2025-53783 fix. Prefix matching stays for +everything else, Adobe relies on it (`acrobat_dc`). + **Oracle Java SE, JRE and JDK** (key `oracle-java` on cvelistV5; NVD rows `oracle:jdk` and `oracle:jre`, each with its `sun:` twin for the Sun-era records, 6-8 only; a JDK also asks for the JRE's CVEs). Version from the diff --git a/app/services/app_cve_scanner_service.py b/app/services/app_cve_scanner_service.py index 1f4d0c5..0be46d1 100644 --- a/app/services/app_cve_scanner_service.py +++ b/app/services/app_cve_scanner_service.py @@ -1092,6 +1092,21 @@ def _same_version_scheme(installed: str, bound: str) -> bool: return (a[0] >= 100) == (b[0] >= 100) +def teams_placeholder_version(version: str) -> bool: + """A Teams version that names no build: 1.0.0.0, 1.0, 1, 0.0.0.0. + + Intune lists the new client's MSIX package as "MSTeams 1.0.0.0", and + Wazuh reads the same from WindowsApps\\MSTeams_1.0.0.0_…. No Teams desktop + client ever shipped as 1.0.0.0 (classic is 1.N.00.NNNNN, the machine-wide + installer 1.N.0.NNNNN, new Teams 2xxxx.x.x.x). Read as a build, it sits + below every classic and Android bound of the same shape: CVE-2023-29328, + -29330, -4863, CVE-2022-21965 and the Teams classic EOL all landed on it. + The CVE scanners and the EOL check skip it; the version is unknown, not + old. + """ + return bool(re.fullmatch(r"[01](?:\.0+)*", (version or "").strip())) + + def dotnet_sdk_version(v) -> bool: """A modern .NET version whose patch is 100+ is an SDK, not a runtime. @@ -1274,6 +1289,12 @@ def _query_nvd_cpe(cpe: str, version: str, sap_pl: Optional[int] = None, if prod_token not in crit and not ( loadmaster and ":progress:multi-tenant_" in crit): continue # Multi Tenant: its 7.1.35 line, see loadmaster_fix + if prod_token == "microsoft:teams" and ":microsoft:teams:" not in crit: + # teams_panels / teams_phones: own devices, own 1.0.9x + # builds. Teams for Android (1.0.0.2025…) sat below + # them and took their CVEs and fix (CVE-2025-53783). + # Teams only: Adobe needs the prefix (acrobat_dc). + continue if not m.get("vulnerable", True): continue if skip_editions and (crit.split(":") + [""] * 10)[9].lower() \ @@ -1412,9 +1433,10 @@ def _cache_put(db: Session, product_key: str, version: str, cves: List[dict]) -> db.commit() +# v5: Teams Panel / Phone CPEs no longer answer for Teams. # v4: Teams bounds scheme-checked, fixes kept per platform. Also drops answers # cached before every NVD page was read. -CACHE_PREFIX = "v4:" +CACHE_PREFIX = "v5:" def lookup_cves(db: Session, entry: dict, version: str, @@ -1823,6 +1845,8 @@ def scan_asset_packages(db: Session, asset, packages: list, new_ids: Optional[li entry = resolve_product(name) if not entry: continue + if entry["key"] == "cpe:microsoft:teams" and teams_placeholder_version(version): + continue java_upd = None if entry.get("java"): # Asked as the release (1.6.0) and decided by the update; shown diff --git a/app/services/cvelistv5_scan_service.py b/app/services/cvelistv5_scan_service.py index 15ecdf4..5e13a20 100644 --- a/app/services/cvelistv5_scan_service.py +++ b/app/services/cvelistv5_scan_service.py @@ -3817,6 +3817,8 @@ def scan_asset(db: Session, asset, packages: list, index: dict, # at 1.6.00.17554 (its own fix) sat below the desktop's 18681. # NVD carries the macOS entries. continue + if key == "teams" and cpe.teams_placeholder_version(version): + continue # "MSTeams 1.0.0.0" names no build strict = key in _STRICT_SCHEME_KEYS for entry in index[key]: if not _affected(cver, entry.get("start"), entry.get("lt"), diff --git a/app/services/ms_lifecycle_service.py b/app/services/ms_lifecycle_service.py index 59aa57f..229d5c6 100644 --- a/app/services/ms_lifecycle_service.py +++ b/app/services/ms_lifecycle_service.py @@ -34,6 +34,7 @@ import httpx from sqlalchemy.orm import Session from app.models.setting import Setting +from app.services.app_cve_scanner_service import teams_placeholder_version from app.services.eol_service import EOLStatus, EOL_SOON_DAYS, _days_until logger = logging.getLogger(__name__) @@ -130,6 +131,10 @@ def _teams_classic(name: str, version: str) -> bool: every shipping new-Teams build is 2xxxx.x.x.x, never 1.x. Intune detectedApps lists the same package by its AppX name "MSTeams". + 1.0.0.0 is none of them: it is what Intune and Wazuh read off the new + client's MSIX package (WindowsApps\\MSTeams_1.0.0.0_…) when they get no + build. Classic was never an MSIX package; see teams_placeholder_version. + The version test must not widen past an exact "teams" name — "Microsoft Teams Meeting Add-in for Microsoft Office" is a live product at 1.24.x. """ @@ -139,8 +144,10 @@ def _teams_classic(name: str, version: str) -> bool: if "teams classic" in low or "classic teams" in low: return True ver = (version or "").strip() + if teams_placeholder_version(ver): + return False if _normalise(name) in ("teams", "msteams"): - return ver == "1" or ver.startswith("1.") + return ver.startswith("1.") return "machine-wide installer" in low and ver.startswith("1.") @@ -405,7 +412,7 @@ def resolve_ms_lifecycle_eol( # plain "Microsoft Teams", which is exactly the export's listing name for # BOTH releases — the day Microsoft fills a retirement date on the classic # row, a current Teams would read as CRITICAL EOL. - if _normalise(pname) == "teams": + if _normalise(pname) in ("teams", "msteams"): return None # 1) hardcoded exotics (Silverlight, old VC++ redists) diff --git a/tests/test_eol_teams_classic.py b/tests/test_eol_teams_classic.py index 0f4f874..3867c6c 100644 --- a/tests/test_eol_teams_classic.py +++ b/tests/test_eol_teams_classic.py @@ -9,12 +9,16 @@ nothing at all. Fixed date instead, matched on name+version. Three inventory entries are that product: Teams Machine-Wide Installer 1.5.0.31168 Microsoft Teams classic 1.8.00.18356 - Microsoft Teams 1.0.0.0 + Microsoft Teams 1.8.00.18356 The third is what Wazuh syscollector reports on a host where the client was never renamed — same name the CURRENT client uses (26198.304.4946.9672), and so does a machine-wide installer belonging to it. Hence the version, not the name, decides: every shipping new-Teams build is 2xxxx.x.x.x, never 1.x. +1.0.0.0 is not classic. It is what Intune ("MSTeams") and Wazuh +(WindowsApps\\MSTeams_1.0.0.0_…) read off the new client's MSIX package when +they get no build; no Teams desktop client shipped as 1.0.0.0. + The version test is pinned to an exact "Microsoft Teams" name, because "Microsoft Teams Meeting Add-in for Microsoft Office" is a live product sitting at 1.24.x and must stay clean. @@ -45,11 +49,7 @@ def test_classic_is_eol(): for name, version in ( ("Microsoft Teams classic", "1.8.00.18356"), ("Teams Machine-Wide Installer", "1.5.0.31168"), - # Wazuh syscollector, WindowsApps MSTeams_1.0.0.0_x64__8wekyb3d8bbwe - ("Microsoft Teams", "1.0.0.0"), ("Microsoft Teams", "1.8.00.18356"), - # Intune detectedApps lists the same package by its AppX name - ("MSTeams", "1.0.0.0"), ): st = _resolve(name, version) assert st is not None, f"{name}: no EOL finding" @@ -65,6 +65,13 @@ def test_current_teams_is_not_eol(): assert _resolve("Teams Machine-Wide Installer", "26198.304.4946.9672") is None +def test_placeholder_version_is_not_classic(): + """Intune: "MSTeams 1.0.0.0"; Wazuh: WindowsApps\\MSTeams_1.0.0.0_….""" + for name in ("MSTeams", "Microsoft Teams", "Teams", "Teams Machine-Wide Installer"): + for version in ("1.0.0.0", "1.0", "1"): + assert _resolve(name, version) is None, (name, version) + + def test_meeting_addin_is_not_teams(): """Live product at 1.24.x — the 1.x rule must not reach it.""" assert _resolve( @@ -75,5 +82,6 @@ def test_meeting_addin_is_not_teams(): if __name__ == "__main__": test_classic_is_eol() test_current_teams_is_not_eol() + test_placeholder_version_is_not_classic() test_meeting_addin_is_not_teams() print("OK — Teams classic EOL 2024-07-01, current Teams + add-in clean") diff --git a/tests/test_teams_classic.py b/tests/test_teams_classic.py index 66371dc..aab870d 100644 --- a/tests/test_teams_classic.py +++ b/tests/test_teams_classic.py @@ -69,7 +69,13 @@ NVD = {"vulnerabilities": [ _m("*", "1.6.00.26474", edition="desktop")]), ("CVE-2025-53783", [_m("android", "1.0.0.2025102802"), _m("iphone_os", "7.10.1"), _m("macos", "25122.1207.3700.1444"), - _m("*", "25122.1415.3698.6812", edition="desktop")]), + _m("*", "25122.1415.3698.6812", edition="desktop"), + {"vulnerable": True, "versionEndExcluding": "1.0.97.2025102203", + "criteria": "cpe:2.3:a:microsoft:teams_panels:*:*:*:*:*:*:*:*"}, + {"vulnerable": True, "versionEndExcluding": "1.0.94.2025168802", + "criteria": "cpe:2.3:a:microsoft:teams_phones:*:*:*:*:*:*:*:*"}]), + # No platform on the bounded entry: Android / Rooms numbering. + ("CVE-2022-21965", [_m("*", "1.0.94.20000")]), )]} @@ -208,6 +214,27 @@ def demo(): assert mac["CVE-2023-4863"] == "1.6.00.26463", mac assert mac["CVE-2023-29328"] == "1.6.00.17554", mac + # --- placeholder ------------------------------------------------------------ + # Intune: "MSTeams 1.0.0.0" — the new client's MSIX package without a + # build. Four parts and below every classic / Android bound, so it drew + # CVE-2023-29328/-29330 (cvelistV5 + NVD), -4863, CVE-2022-21965 and, + # via teams_panels' 1.0.97.x, CVE-2025-53783. + for version in ("1.0.0.0", "1.0.0", "1", "0.0.0.0"): + for name in ("MSTeams", "Microsoft Teams", "Teams"): + assert _scan5(index, name, version) == [], (name, version) + assert _scan_nvd("Windows 11 Enterprise", version) == [], version + # …while a real classic build still gets them, and the old bound's + # Android numbering (1.0.94.x) is not a placeholder. + assert _scan_nvd("Windows 11 Enterprise", "1.5.00.31168") == _nvd("1.5.00.31168") + assert not cpe.teams_placeholder_version("1.0.94.19999") + + # --- Teams Panel / Phone -------------------------------------------------- + # Their own CPEs (teams_panels, teams_phones), not Teams. Teams for + # Android (1.0.0.2025…) sits below their 1.0.9x bounds and picked them up, + # fix included. It gets the Android entry only. + c = next(c for c in _query("1.0.0.2025102801") if c["cve"] == "CVE-2025-53783") + assert c["fixes"] == {"android": "1.0.0.2025102802"}, c + print("ok classic Teams 1.5.00.31168 gets CVE-2023-29328/-29330 from both paths " "with the desktop fix, CVE-2023-24881 (2.x only) stays off it") diff --git a/tests/test_wazuh_msix_superseded.py b/tests/test_wazuh_msix_superseded.py index 9dbf2c4..d8bea0a 100644 --- a/tests/test_wazuh_msix_superseded.py +++ b/tests/test_wazuh_msix_superseded.py @@ -37,7 +37,7 @@ def test_other_families_and_arp_entries_untouched(): _msix("Microsoft Teams", "MSTeams", "26225.1806.5074.1452"), _msix("Company Portal", "Microsoft.CompanyPortal", "11.2.1.0"), # Same name, classic ARP install, no WindowsApps folder — a different - # product line and must survive (EOL scan flags it). + # product line and must survive. {"name": "Microsoft Teams", "version": "1.0.0.0", "location": ""}, {"name": "Mozilla Firefox", "version": "141.0"}, ] diff --git a/tools/diagnose_scan.py b/tools/diagnose_scan.py index 097ef6e..a259651 100644 --- a/tools/diagnose_scan.py +++ b/tools/diagnose_scan.py @@ -117,7 +117,7 @@ def _index(): return _INDEX_SETTING -@check("NVD result cache prefix", "v4:") +@check("NVD result cache prefix", "v5:") def _cache(): from app.services.app_cve_scanner_service import CACHE_PREFIX return CACHE_PREFIX