The Android bulletin pass only ever raised findings. A device that installed the October patch kept every October CVE open forever, since no scanner looks again at a month the device is no longer behind on. check_android_cves now retracts android-asb / samsung-smr from findings whose month (read from the package name, "Android (SMR 2026-10)") is at or below the device's patch level, and closes them when no other scanner still reports the CVE, with an audit entry naming the patch level. A CVE that a month still behind lists again stays open, and so does a row whose month cannot be read. The gate is the month, not "not seen this run": the fetched months are capped at 12 and a failed fetch returns nothing, and both would close CVEs the device still lacks.
90 lines
3.7 KiB
Python
90 lines
3.7 KiB
Python
"""Bulletin CVEs close once the device reaches their month — run: python tests/test_android_patched_close.py
|
|
|
|
Found 2026-11-04: the ASB/SMR pass only ever raised findings. A device that
|
|
installed the October patch kept every October CVE open forever, since no
|
|
scanner looks at a month the device is no longer behind on.
|
|
|
|
Pinned here: a finding from a month at or below the device's patch level loses
|
|
our source and closes when no other scanner reports it; a month still behind,
|
|
a CVE that a newer month lists again, and a row whose month we cannot read are
|
|
left alone.
|
|
"""
|
|
import os
|
|
import sys
|
|
from datetime import datetime
|
|
|
|
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 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.base import Base # noqa: E402
|
|
from app.models.asset import Asset, AssetSource, AssetStatus # noqa: E402
|
|
from app.models.vulnerability import ( # noqa: E402
|
|
Vulnerability,
|
|
VulnerabilitySeverity,
|
|
VulnerabilityStatus,
|
|
)
|
|
from app.services import android_cve_service, samsung_smr_service # noqa: E402
|
|
|
|
|
|
def _vuln(asset, cve, sources, package):
|
|
return Vulnerability(
|
|
cve_id=cve, asset_id=asset.id, severity=VulnerabilitySeverity.high,
|
|
status=VulnerabilityStatus.open, title=cve, detected_at=datetime.now(),
|
|
sources=sources, first_detected_by="test", package_name=package,
|
|
)
|
|
|
|
|
|
def demo():
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(engine)
|
|
db = sessionmaker(bind=engine)()
|
|
asset = Asset(hostname="PHONE-1", operating_system="Android", os_version="16",
|
|
source=AssetSource.INTUNE, status=AssetStatus.ACTIVE)
|
|
db.add(asset)
|
|
db.flush()
|
|
db.add_all([
|
|
_vuln(asset, "CVE-2026-30001", '["samsung-smr"]', "Android (SMR 2026-09)"),
|
|
_vuln(asset, "CVE-2026-30002", '["samsung-smr"]', "Android (SMR 2026-10)"),
|
|
# Another scanner still reports it: stays open, only our claim goes.
|
|
_vuln(asset, "CVE-2026-30003", '["android-asb", "app-scan"]', "Android (ASB 2026-08)"),
|
|
# Every month after the patch level still owes it.
|
|
_vuln(asset, "CVE-2026-30004", '["samsung-smr"]', "Android (SMR 2026-11)"),
|
|
# Raised in September, listed again by a month still behind.
|
|
_vuln(asset, "CVE-2026-30005", '["samsung-smr"]', "Android (SMR 2026-09)"),
|
|
# Month unknown (another scanner named the package first).
|
|
_vuln(asset, "CVE-2026-30006", '["samsung-smr"]', "android-kernel"),
|
|
])
|
|
db.commit()
|
|
|
|
owed = [("CVE-2026-30004", "critical"), ("CVE-2026-30005", "high")]
|
|
real = samsung_smr_service.get_smr_month
|
|
samsung_smr_service.get_smr_month = lambda db, y, m: owed
|
|
try:
|
|
android_cve_service.check_android_cves(db, asset, "2026-10-01", manufacturer="samsung")
|
|
finally:
|
|
samsung_smr_service.get_smr_month = real
|
|
db.commit()
|
|
|
|
by_cve = {v.cve_id: v for v in db.query(Vulnerability).all()}
|
|
assert by_cve["CVE-2026-30001"].status == VulnerabilityStatus.patched
|
|
assert by_cve["CVE-2026-30001"].source_list == []
|
|
assert by_cve["CVE-2026-30002"].status == VulnerabilityStatus.patched
|
|
assert by_cve["CVE-2026-30003"].status == VulnerabilityStatus.open
|
|
assert by_cve["CVE-2026-30003"].source_list == ["app-scan"]
|
|
for cve in ("CVE-2026-30004", "CVE-2026-30005", "CVE-2026-30006"):
|
|
assert by_cve[cve].status == VulnerabilityStatus.open, cve
|
|
assert by_cve[cve].patched_at is None, cve
|
|
print("OK")
|
|
|
|
|
|
def test_android_patched_close():
|
|
demo()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
demo()
|