Compare commits
2
Commits
dc17419824
...
0f372e36a2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f372e36a2 | ||
|
|
0a081faa19 |
+10
-1
@@ -2248,10 +2248,19 @@ Runs inline during the Intune sync — no extra Graph calls beyond the
|
|||||||
device dict and `detectedApps` already fetched.
|
device dict and `detectedApps` already fetched.
|
||||||
|
|
||||||
- **Device EOL/EOS** (`mobile_eol_service.py`) — Apple (iPhone/iPad) fuzzy-
|
- **Device EOL/EOS** (`mobile_eol_service.py`) — Apple (iPhone/iPad) fuzzy-
|
||||||
matches endoflife.date's full release list by marketing name; Samsung
|
matches endoflife.date's full release list by marketing name, except the
|
||||||
|
few names endoflife spells differently (`_APPLE_ALIASES`: first-generation
|
||||||
|
iPhone SE and iPad Air/mini/Pro without "(1st generation)", 12.9" Pro 6th gen, iPhone
|
||||||
|
Air; `tests/test_apple_model_names.py`). Apple announces no end dates, so
|
||||||
|
a current model gets its finding only once endoflife marks it EOL; Samsung
|
||||||
needs a curated SM-code → release-name table (no textual bridge exists
|
needs a curated SM-code → release-name table (no textual bridge exists
|
||||||
between Intune's model code and endoflife's marketing name in either
|
between Intune's model code and endoflife's marketing name in either
|
||||||
dataset) — ~120 models, all verified against the live endoflife API.
|
dataset) — ~120 models, all verified against the live endoflife API.
|
||||||
|
A re-release that keeps the base code gets a suffixed row above it
|
||||||
|
(Tab A9+ 2025 = SM-X216R/SM-X210R; SM-X216B/U and SM-X210 are 2023).
|
||||||
|
A device is one model, so the mobile slugs supersede: a resolved model
|
||||||
|
retires any other model's finding on the asset even when it raises none
|
||||||
|
itself (`tests/test_galaxy_tab_a9plus_2025.py`).
|
||||||
- **OS-level CVEs** — iOS/iPadOS/macOS via NVD-CPE (`app_cve_scanner_service`),
|
- **OS-level CVEs** — iOS/iPadOS/macOS via NVD-CPE (`app_cve_scanner_service`),
|
||||||
with a platform check (CPE `target_sw` token) so e.g. a Firefox-for-iOS
|
with a platform check (CPE `target_sw` token) so e.g. a Firefox-for-iOS
|
||||||
CVE can't match a desktop Firefox install. NVD often has no configuration
|
CVE can't match a desktop Firefox install. NVD often has no configuration
|
||||||
|
|||||||
@@ -938,6 +938,9 @@ _SINGLE_RELEASE_SLUGS = {
|
|||||||
# version, so an 11 -> 12 migration must retire the OS 11 finding instead
|
# version, so an 11 -> 12 migration must retire the OS 11 finding instead
|
||||||
# of leaving it open next to the OS 12 asset it no longer describes.
|
# of leaving it open next to the OS 12 asset it no longer describes.
|
||||||
"igel-os", "igel-ums",
|
"igel-os", "igel-ums",
|
||||||
|
# A phone or tablet IS one model; a finding for any other model on the
|
||||||
|
# same asset came from an earlier, wrong model mapping.
|
||||||
|
"samsung-mobile", "samsung-galaxy-tab", "iphone", "ipad",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,10 @@ logger = logging.getLogger(__name__)
|
|||||||
_PHONE = "samsung-mobile"
|
_PHONE = "samsung-mobile"
|
||||||
_TAB = "samsung-galaxy-tab"
|
_TAB = "samsung-galaxy-tab"
|
||||||
# SM-base-code (4 digits) → (endoflife slug, release `name`). Codes are
|
# SM-base-code (4 digits) → (endoflife slug, release `name`). Codes are
|
||||||
# unique per model, so no prefix collides with another. Slug per row routes
|
# unique per model, except a re-release that keeps the base code and only
|
||||||
# tablets (SM-X/T/P) to samsung-galaxy-tab. Names verified against the live
|
# changes the suffix: its row carries the suffix and sits ABOVE the base row
|
||||||
# endoflife.date API.
|
# (first prefix wins). Slug per row routes tablets (SM-X/T/P) to
|
||||||
|
# samsung-galaxy-tab. Names verified against the live endoflife.date API.
|
||||||
_SAMSUNG: list[Tuple[str, str, str]] = [
|
_SAMSUNG: list[Tuple[str, str, str]] = [
|
||||||
# --- Galaxy S25 / S24 / S23 / S22 (base / + / Ultra / FE) ---
|
# --- Galaxy S25 / S24 / S23 / S22 (base / + / Ultra / FE) ---
|
||||||
("SM-S938", _PHONE, "galaxy-s25-ultra"), ("SM-S936", _PHONE, "galaxy-s25+"), ("SM-S931", _PHONE, "galaxy-s25"), ("SM-S731", _PHONE, "galaxy-s25-fe"),
|
("SM-S938", _PHONE, "galaxy-s25-ultra"), ("SM-S936", _PHONE, "galaxy-s25+"), ("SM-S931", _PHONE, "galaxy-s25"), ("SM-S731", _PHONE, "galaxy-s25-fe"),
|
||||||
@@ -72,6 +73,9 @@ _SAMSUNG: list[Tuple[str, str, str]] = [
|
|||||||
("SM-T866", _TAB, "galaxy-tab-s6"), ("SM-T860", _TAB, "galaxy-tab-s6"), ("SM-P625", _TAB, "galaxy-tab-s6-lite-2024"), ("SM-P620", _TAB, "galaxy-tab-s6-lite-2024"), ("SM-P619", _TAB, "galaxy-tab-s6-lite"), ("SM-P613", _TAB, "galaxy-tab-s6-lite"), ("SM-P615", _TAB, "galaxy-tab-s6-lite-2020"), ("SM-P610", _TAB, "galaxy-tab-s6-lite-2020"),
|
("SM-T866", _TAB, "galaxy-tab-s6"), ("SM-T860", _TAB, "galaxy-tab-s6"), ("SM-P625", _TAB, "galaxy-tab-s6-lite-2024"), ("SM-P620", _TAB, "galaxy-tab-s6-lite-2024"), ("SM-P619", _TAB, "galaxy-tab-s6-lite"), ("SM-P613", _TAB, "galaxy-tab-s6-lite"), ("SM-P615", _TAB, "galaxy-tab-s6-lite-2020"), ("SM-P610", _TAB, "galaxy-tab-s6-lite-2020"),
|
||||||
# --- Galaxy Tab A (budget) ---
|
# --- Galaxy Tab A (budget) ---
|
||||||
("SM-X236", _TAB, "galaxy-tab-a11+"), ("SM-X230", _TAB, "galaxy-tab-a11+"), ("SM-X135", _TAB, "galaxy-tab-a11"), ("SM-X130", _TAB, "galaxy-tab-a11"),
|
("SM-X236", _TAB, "galaxy-tab-a11+"), ("SM-X230", _TAB, "galaxy-tab-a11+"), ("SM-X135", _TAB, "galaxy-tab-a11"), ("SM-X130", _TAB, "galaxy-tab-a11"),
|
||||||
|
# Tab A9+ (2025) re-release is SM-X216R / SM-X210R; the 2023 one is
|
||||||
|
# SM-X216B/U and SM-X210 (EOS 2026, vs 2031 for the 2025 model).
|
||||||
|
("SM-X216R", _TAB, "galaxy-tab-a9+2025"), ("SM-X210R", _TAB, "galaxy-tab-a9+2025"),
|
||||||
("SM-X216", _TAB, "galaxy-tab-a9+"), ("SM-X210", _TAB, "galaxy-tab-a9+"), ("SM-X116", _TAB, "galaxy-tab-a9"), ("SM-X110", _TAB, "galaxy-tab-a9"), ("SM-X205", _TAB, "galaxy-tab-a8"), ("SM-X200", _TAB, "galaxy-tab-a8"),
|
("SM-X216", _TAB, "galaxy-tab-a9+"), ("SM-X210", _TAB, "galaxy-tab-a9+"), ("SM-X116", _TAB, "galaxy-tab-a9"), ("SM-X110", _TAB, "galaxy-tab-a9"), ("SM-X205", _TAB, "galaxy-tab-a8"), ("SM-X200", _TAB, "galaxy-tab-a8"),
|
||||||
("SM-T350", _TAB, "galaxy-tab-a-8.0-2015"), ("SM-T280", _TAB, "galaxy-tab-a-7.0-2016"),
|
("SM-T350", _TAB, "galaxy-tab-a-8.0-2015"), ("SM-T280", _TAB, "galaxy-tab-a-7.0-2016"),
|
||||||
# --- Galaxy Tab Active (rugged tablets) ---
|
# --- Galaxy Tab Active (rugged tablets) ---
|
||||||
@@ -79,6 +83,18 @@ _SAMSUNG: list[Tuple[str, str, str]] = [
|
|||||||
("SM-T395", _TAB, "galaxy-tab-active2"), ("SM-T365", _TAB, "galaxy-tab-active-lte"), ("SM-T360", _TAB, "galaxy-tab-active"),
|
("SM-T395", _TAB, "galaxy-tab-active2"), ("SM-T365", _TAB, "galaxy-tab-active-lte"), ("SM-T360", _TAB, "galaxy-tab-active"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Apple names (normalised) that endoflife.date spells differently → release
|
||||||
|
# `name`. The first iPhone SE, iPad Air, iPad mini and iPad Pro of each size
|
||||||
|
# (pro-1-<size>) have no generation in Apple's name; endoflife adds "(1st
|
||||||
|
# generation)". Exact keys only — no fuzzy fallback.
|
||||||
|
_APPLE_ALIASES: dict[str, str] = {
|
||||||
|
"iphone-se": "se-1", "ipad-air": "air-1", "ipad-mini": "mini-1",
|
||||||
|
"ipad-pro-9-7-inch": "pro-1-9", "ipad-pro-10-5-inch": "pro-1-10",
|
||||||
|
"ipad-pro-11-inch": "pro-1-11", "ipad-pro-12-9-inch": "pro-1-12",
|
||||||
|
"ipad-pro-12-9-inch-6th-generation": "pro-6", # endoflife: "iPad Pro (6th generation)"
|
||||||
|
"iphone-air": "air", # endoflife: "17 Air"
|
||||||
|
}
|
||||||
|
|
||||||
_STALE_CVE_ID = "ANDROID-PATCH-LEVEL-STALE"
|
_STALE_CVE_ID = "ANDROID-PATCH-LEVEL-STALE"
|
||||||
|
|
||||||
|
|
||||||
@@ -88,7 +104,8 @@ def _norm(s: Optional[str]) -> str:
|
|||||||
|
|
||||||
def _resolve_device(manuf: str, model: str, os_name: str):
|
def _resolve_device(manuf: str, model: str, os_name: str):
|
||||||
"""→ (slug, matcher) or None. matcher = {'kind':'name','name':...} for the
|
"""→ (slug, matcher) or None. matcher = {'kind':'name','name':...} for the
|
||||||
Samsung table, or {'kind':'apple','model':...} for Apple fuzzy match."""
|
Samsung table and Apple aliases, or {'kind':'apple','model':...} for Apple
|
||||||
|
fuzzy match."""
|
||||||
m = model or ""
|
m = model or ""
|
||||||
mu = (manuf or "").lower()
|
mu = (manuf or "").lower()
|
||||||
ml = m.lower()
|
ml = m.lower()
|
||||||
@@ -96,6 +113,9 @@ def _resolve_device(manuf: str, model: str, os_name: str):
|
|||||||
|
|
||||||
if "apple" in mu or ml.startswith("ipad") or ml.startswith("iphone") or "ios" in osl:
|
if "apple" in mu or ml.startswith("ipad") or ml.startswith("iphone") or "ios" in osl:
|
||||||
slug = "ipad" if ("ipad" in ml or "ipados" in osl) else "iphone"
|
slug = "ipad" if ("ipad" in ml or "ipados" in osl) else "iphone"
|
||||||
|
alias = _APPLE_ALIASES.get(_norm(m))
|
||||||
|
if alias:
|
||||||
|
return slug, {"kind": "name", "name": alias}
|
||||||
return slug, {"kind": "apple", "model": m}
|
return slug, {"kind": "apple", "model": m}
|
||||||
|
|
||||||
if "samsung" in mu or re.match(r"sm-[a-z]\d", ml):
|
if "samsung" in mu or re.match(r"sm-[a-z]\d", ml):
|
||||||
@@ -282,6 +302,13 @@ def check_device(db: Session, asset, device: dict,
|
|||||||
if row:
|
if row:
|
||||||
row.package_name = f"{vendor} {rel.get('label') or model}".strip()[:255]
|
row.package_name = f"{vendor} {rel.get('label') or model}".strip()[:255]
|
||||||
count += 1
|
count += 1
|
||||||
|
else:
|
||||||
|
# A supported model raises nothing, so the upsert's own
|
||||||
|
# supersede never runs: retire what an earlier mapping
|
||||||
|
# left here (Tab A9+ 2023 finding on the 2025 model).
|
||||||
|
eol_service._supersede_old_eol(
|
||||||
|
db, asset.id, slug,
|
||||||
|
eol_service._pseudo_cve_id(slug, status.release_name or "unknown"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug("mobile-eol device check failed for %s: %s", asset.hostname, e)
|
logger.debug("mobile-eol device check failed for %s: %s", asset.hostname, e)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"""Apple model names that endoflife.date spells differently — run: python tests/test_apple_model_names.py
|
||||||
|
|
||||||
|
Intune reports Apple's marketing name and mobile_eol_service matches it
|
||||||
|
against endoflife.date's release label. Most line up after normalising
|
||||||
|
("iPad Pro (11-inch) (4th generation)" == "iPad Pro 11-inch (4th
|
||||||
|
generation)"), but not these:
|
||||||
|
|
||||||
|
- The first iPhone SE, iPad Air, iPad mini and iPad Pro of each size carry
|
||||||
|
no generation in Apple's name ("iPad Pro (9.7-inch)"); endoflife adds
|
||||||
|
"(1st generation)". Most of them are already EOL (9.7"/12.9" Pro
|
||||||
|
2025-03-31, 10.5" Pro 2026-09-17, SE 2025, Air 2023, mini 2019), so the miss hid
|
||||||
|
real findings.
|
||||||
|
- endoflife files the 12.9-inch 6th generation as "iPad Pro (6th
|
||||||
|
generation)" and the iPhone Air as "17 Air".
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
os.environ.setdefault("JWT_SECRET_KEY", "test-key-test-key-test-key-test-key")
|
||||||
|
|
||||||
|
from app.services import mobile_eol_service # noqa: E402
|
||||||
|
|
||||||
|
# endoflife.date ipad / iphone, fetched 2026-11-03 (only the fields matched on).
|
||||||
|
_IPAD = {"result": {"releases": [
|
||||||
|
{"name": "pro-1-9", "label": "iPad Pro 9.7-inch (1st generation)"},
|
||||||
|
{"name": "pro-1-10", "label": "iPad Pro 10.5-inch (1st generation)"},
|
||||||
|
{"name": "pro-1-11", "label": "iPad Pro 11-inch (1st generation)"},
|
||||||
|
{"name": "pro-1-12", "label": "iPad Pro 12.9-inch (1st generation)"},
|
||||||
|
{"name": "pro-2-12", "label": "iPad Pro 12.9-inch (2nd generation)"},
|
||||||
|
{"name": "pro-4-11", "label": "iPad Pro 11-inch (4th generation)"},
|
||||||
|
{"name": "pro-6", "label": "iPad Pro (6th generation)"},
|
||||||
|
{"name": "9", "label": "iPad (9th generation)"},
|
||||||
|
{"name": "air-1", "label": "iPad Air (1st generation)"},
|
||||||
|
{"name": "air-2", "label": "iPad Air 2"},
|
||||||
|
{"name": "mini-1", "label": "iPad Mini (1st generation)"},
|
||||||
|
]}}
|
||||||
|
_IPHONE = {"result": {"releases": [
|
||||||
|
{"name": "air", "label": "17 Air"},
|
||||||
|
{"name": "17-pro", "label": "17 Pro"},
|
||||||
|
{"name": "se-3", "label": "SE (3rd generation)"},
|
||||||
|
{"name": "se-1", "label": "SE (1st generation)"},
|
||||||
|
]}}
|
||||||
|
|
||||||
|
|
||||||
|
def _match(model):
|
||||||
|
slug, matcher = mobile_eol_service._resolve_device("Apple", model, "iOS")
|
||||||
|
rel = mobile_eol_service._find_release(_IPAD if slug == "ipad" else _IPHONE, matcher)
|
||||||
|
return rel and rel["name"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_apple_names_endoflife_spells_differently():
|
||||||
|
assert _match("iPad Pro (9.7-inch)") == "pro-1-9"
|
||||||
|
assert _match("iPad Pro (10.5-inch)") == "pro-1-10"
|
||||||
|
assert _match("iPad Pro (11-inch)") == "pro-1-11"
|
||||||
|
assert _match("iPad Pro (12.9-inch)") == "pro-1-12"
|
||||||
|
assert _match("iPad Pro (12.9-inch) (6th generation)") == "pro-6"
|
||||||
|
assert _match("iPhone Air") == "air"
|
||||||
|
assert _match("iPad Air") == "air-1"
|
||||||
|
assert _match("iPad mini") == "mini-1"
|
||||||
|
assert _match("iPhone SE") == "se-1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_names_that_already_matched_still_do():
|
||||||
|
assert _match("iPad Pro (12.9-inch) (2nd generation)") == "pro-2-12"
|
||||||
|
assert _match("iPad Pro (11-inch) (4th generation)") == "pro-4-11"
|
||||||
|
assert _match("iPad (9th generation)") == "9"
|
||||||
|
assert _match("iPad Air 2") == "air-2"
|
||||||
|
assert _match("iPhone 17 Pro") == "17-pro"
|
||||||
|
assert _match("iPhone SE (3rd generation)") == "se-3"
|
||||||
|
# An unknown name stays unmatched rather than guessed.
|
||||||
|
assert _match("iPad Pro (13-inch)") is None
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_apple_names_endoflife_spells_differently()
|
||||||
|
test_names_that_already_matched_still_do()
|
||||||
|
print("OK — Apple names endoflife.date spells differently still find their release")
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
"""Galaxy Tab A9+ 5G (2025) is not the 2023 Tab A9+ — run: python tests/test_galaxy_tab_a9plus_2025.py
|
||||||
|
|
||||||
|
Field report 2026-11-03: an SM-X216R showed the Tab A9+ end-of-support
|
||||||
|
finding (active support ended 2026-10-23, security 2027-10-23). SM-X216R is
|
||||||
|
the 2025 re-release (Wi-Fi twin SM-X210R) that endoflife.date tracks as its
|
||||||
|
own release `galaxy-tab-a9+2025`, supported into 2031. The 2023 codes
|
||||||
|
(SM-X216B/U, SM-X210) share the base code, so the plain prefix match took
|
||||||
|
the 2025 tablet for the 2023 one.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
os.environ.setdefault("JWT_SECRET_KEY", "test-key-test-key-test-key-test-key")
|
||||||
|
|
||||||
|
import pytest # noqa: E402
|
||||||
|
from sqlalchemy import create_engine # noqa: E402
|
||||||
|
from sqlalchemy.orm import sessionmaker # noqa: E402
|
||||||
|
|
||||||
|
import app.models # noqa: F401,E402 (registers every mapper)
|
||||||
|
from app.models.asset import Asset # noqa: E402
|
||||||
|
from app.models.base import Base # noqa: E402
|
||||||
|
from app.models.vulnerability import Vulnerability, VulnerabilitySeverity, VulnerabilityStatus # noqa: E402
|
||||||
|
from app.services import app_cve_scanner_service, eol_service, intune_service, mobile_eol_service # noqa: E402
|
||||||
|
|
||||||
|
# endoflife.date samsung-galaxy-tab, fetched 2026-11-03.
|
||||||
|
_TABS = {"result": {"releases": [
|
||||||
|
{"name": "galaxy-tab-a9+2025", "label": "Galaxy Tab A9+(2025)",
|
||||||
|
"eolFrom": None, "eoasFrom": "2031-08-01"},
|
||||||
|
{"name": "galaxy-tab-a9+", "label": "Galaxy Tab A9+",
|
||||||
|
"eolFrom": "2027-10-23", "eoasFrom": "2026-10-23"},
|
||||||
|
]}}
|
||||||
|
|
||||||
|
|
||||||
|
def _release(model):
|
||||||
|
slug, matcher = mobile_eol_service._resolve_device("Samsung", model, "Android")
|
||||||
|
return slug, matcher["name"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_codes_pick_their_generation():
|
||||||
|
assert _release("SM-X216R") == ("samsung-galaxy-tab", "galaxy-tab-a9+2025")
|
||||||
|
assert _release("SM-X210R") == ("samsung-galaxy-tab", "galaxy-tab-a9+2025")
|
||||||
|
assert _release("SM-X216B") == ("samsung-galaxy-tab", "galaxy-tab-a9+")
|
||||||
|
assert _release("SM-X216U") == ("samsung-galaxy-tab", "galaxy-tab-a9+")
|
||||||
|
assert _release("SM-X210") == ("samsung-galaxy-tab", "galaxy-tab-a9+")
|
||||||
|
|
||||||
|
|
||||||
|
def _device(model, dev_id):
|
||||||
|
return {
|
||||||
|
"id": dev_id, "deviceName": f"AndroidEnterprise-{dev_id}", "manufacturer": "Samsung",
|
||||||
|
"model": model, "operatingSystem": "Android", "osVersion": "15",
|
||||||
|
"serialNumber": dev_id.upper(),
|
||||||
|
"lastSyncDateTime": (datetime.now() - timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeClient:
|
||||||
|
def get_managed_devices(self):
|
||||||
|
return [_device("SM-X216R", "tab2025"), _device("SM-X216B", "tab2023")]
|
||||||
|
|
||||||
|
def get_detected_apps(self, device_id):
|
||||||
|
return [{"name": "Microsoft Teams", "version": "1500.0.0"}]
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_sync_clears_the_wrong_finding_and_keeps_the_right_one():
|
||||||
|
with pytest.MonkeyPatch.context() as mp:
|
||||||
|
mp.setattr(intune_service, "_build_client", lambda cfg: _FakeClient())
|
||||||
|
mp.setattr(eol_service, "fetch_product",
|
||||||
|
lambda db, slug: _TABS if slug == "samsung-galaxy-tab" else None)
|
||||||
|
mp.setattr(app_cve_scanner_service, "scan_asset_packages", lambda *a, **kw: 0)
|
||||||
|
_sync_twice()
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_twice():
|
||||||
|
engine = create_engine("sqlite:///:memory:")
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
db = sessionmaker(bind=engine)()
|
||||||
|
|
||||||
|
cfg = {"auto_create_assets": True, "detected_apps": True}
|
||||||
|
intune_service._run_intune_sync_locked(db, cfg)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Pseudo-CVE id of the 2023 release ("+" is stored as "_").
|
||||||
|
a9_2023_id = "EOL-SAMSUNG-GALAXY-TAB-galaxy-tab-a9_"
|
||||||
|
tab2023 = db.query(Asset).filter(Asset.intune_device_id == "tab2023").one()
|
||||||
|
tab2025 = db.query(Asset).filter(Asset.intune_device_id == "tab2025").one()
|
||||||
|
|
||||||
|
old = db.query(Vulnerability).filter_by(asset_id=tab2023.id, cve_id=a9_2023_id).one()
|
||||||
|
assert old.status == VulnerabilityStatus.open
|
||||||
|
|
||||||
|
# What the old mapping left on the 2025 tablet must close on the next sync.
|
||||||
|
wrong = Vulnerability(cve_id=a9_2023_id, asset_id=tab2025.id, status=VulnerabilityStatus.open,
|
||||||
|
severity=VulnerabilitySeverity.medium, cvss_score=5.0,
|
||||||
|
detected_at=datetime.now(), title="Galaxy Tab A9+ EOS",
|
||||||
|
first_detected_by="eol_check", sources='["eol_check"]')
|
||||||
|
db.add(wrong)
|
||||||
|
db.commit()
|
||||||
|
# Without detectedApps (off, empty, or a co-managed device) nothing
|
||||||
|
# reconciles against an app list — the model check must retire it itself.
|
||||||
|
intune_service._run_intune_sync_locked(db, {**cfg, "detected_apps": False})
|
||||||
|
db.commit()
|
||||||
|
db.refresh(wrong)
|
||||||
|
db.refresh(old)
|
||||||
|
|
||||||
|
assert wrong.status == VulnerabilityStatus.patched, "2025 tablet still carries the 2023 EOS finding"
|
||||||
|
assert old.status == VulnerabilityStatus.open
|
||||||
|
open_on_2025 = [v.cve_id for v in db.query(Vulnerability).filter_by(
|
||||||
|
asset_id=tab2025.id, status=VulnerabilityStatus.open)]
|
||||||
|
assert not [c for c in open_on_2025 if c.startswith("EOL-")], open_on_2025
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_model_codes_pick_their_generation()
|
||||||
|
test_sync_clears_the_wrong_finding_and_keeps_the_right_one()
|
||||||
|
print("OK — Galaxy Tab A9+ (2025) is told apart from the 2023 model")
|
||||||
Reference in New Issue
Block a user