129 lines
4.4 KiB
Python
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()
|