Files
vulncheck/tests/test_android_patched_close.py
T
vulncheck f9e8464431 fix(android): close ASB/SMR findings once the device has the patch
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.
2026-11-04 10:36:33 +01:00

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()