Files
PawletOS-Build/scripts/check_security_patch.py

129 lines
4.4 KiB
Python

#!/usr/bin/env python3
"""
Reports how far PawletOS's PAWLET_SECURITY_PATCH (vendor/pawlet/config/version.mk)
is behind the latest published AOSP security bulletin. Does not apply anything —
just tells you whether it's time to go read a bulletin and backport.
Usage:
python3 check_security_patch.py [--vendor-tree PATH] [--offline]
"""
import argparse
import re
import sys
from datetime import date, timedelta
from pathlib import Path
try:
import requests
except ImportError:
requests = None
BULLETIN_BASE = "https://source.android.com/docs/security/bulletin"
VERSION_MK_RELATIVE = "config/version.mk"
PATCH_VAR_RE = re.compile(r"PAWLET_SECURITY_PATCH\s*:=\s*(\d{4}-\d{2}-\d{2})")
def find_current_patch_date(vendor_tree: Path) -> date:
version_mk = vendor_tree / VERSION_MK_RELATIVE
if not version_mk.exists():
sys.exit(f"error: {version_mk} not found — pass --vendor-tree pointing at android_vendor_pawlet")
text = version_mk.read_text(encoding="utf-8")
match = PATCH_VAR_RE.search(text)
if not match:
sys.exit(f"error: PAWLET_SECURITY_PATCH not found in {version_mk}")
return date.fromisoformat(match.group(1))
def candidate_bulletin_dates(months_back: int = 15):
"""Yield (label, url, date) for the two possible bulletin days of each of the
last `months_back` months, newest first. AOSP publishes on the 1st and
sometimes a supplemental on the 5th of a month."""
today = date.today()
year, month = today.year, today.month
for _ in range(months_back):
for day in (5, 1):
try:
d = date(year, month, day)
except ValueError:
continue
if d > today:
continue
label = d.isoformat()
url = f"{BULLETIN_BASE}/{label}"
yield label, url, d
month -= 1
if month == 0:
month = 12
year -= 1
def find_latest_published_bulletin(offline: bool):
"""Return (date, url) of the most recent bulletin that actually resolves,
or (None, None) if offline/unreachable — caller falls back to date math."""
if offline or requests is None:
return None, None
for _label, url, d in candidate_bulletin_dates():
try:
resp = requests.head(url, timeout=5, allow_redirects=True)
if resp.status_code == 200:
return d, url
except requests.RequestException:
continue
return None, None
def most_likely_bulletin_date_offline() -> date:
"""No network: assume the most recent 1st-of-month that has passed."""
today = date.today()
candidate = date(today.year, today.month, 1)
if candidate > today:
month = today.month - 1 or 12
year = today.year if today.month > 1 else today.year - 1
candidate = date(year, month, 1)
return candidate
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--vendor-tree",
type=Path,
default=Path(__file__).resolve().parents[2] / "android_vendor_pawlet",
help="Path to the android_vendor_pawlet checkout (default: sibling of PawletOS-Build)",
)
parser.add_argument("--offline", action="store_true", help="Skip the network check, use date math only")
args = parser.parse_args()
current_patch = find_current_patch_date(args.vendor_tree)
print(f"Current PAWLET_SECURITY_PATCH: {current_patch.isoformat()}")
latest_date, latest_url = find_latest_published_bulletin(args.offline)
if latest_date is None:
latest_date = most_likely_bulletin_date_offline()
print(f"(offline/unreachable — estimating latest bulletin as {latest_date.isoformat()}, "
f"double check {BULLETIN_BASE}/{latest_date.isoformat()})")
else:
print(f"Latest published bulletin found: {latest_date.isoformat()} ({latest_url})")
gap_days = (latest_date - current_patch).days
if gap_days <= 0:
print("Up to date — nothing to do.")
sys.exit(0)
months_behind = gap_days // 30
print(f"{gap_days} days behind (~{months_behind} bulletin cycle(s)).")
print(f"Go check: {BULLETIN_BASE}/{latest_date.isoformat()}")
print("Then: backport the listed CVEs, bump PAWLET_SECURITY_PATCH in "
"vendor/pawlet/config/version.mk, bump BOARD_AVB_ROLLBACK_INDEX, and ship an OTA. "
"See docs/security-patch-process.md for the full checklist.")
sys.exit(1)
if __name__ == "__main__":
main()