#!/usr/bin/env python3 """Validate restricted milestone 19 account-security acceptance evidence.""" from __future__ import annotations import argparse import datetime as dt import json from pathlib import Path import re from urllib.parse import urlparse SCENARIOS={ "authenticator-enrollment","totp-window-replay","recovery-codes","administrator-recovery", "smtp-delivery-revocation","role-boundaries","legacy-staff","preferences","restart-recovery", "monitoring","audit-review","secret-free-interfaces", } def require(value:bool,message:str)->None: if not value: raise ValueError(message) def timestamp(value:object,field:str)->dt.datetime: require(isinstance(value,str) and value.endswith("Z"),f"{field} must be a UTC timestamp ending in Z.") try:return dt.datetime.fromisoformat(value.removesuffix("Z")+"+00:00") except ValueError as error:raise ValueError(f"{field} is not a valid timestamp.") from error def person(value:object,field:str)->str: result=str(value or "").strip();require(2<=len(result)<=120 and "@" not in result,f"{field} requires a name without an email address.");return result def refs(value:object,field:str)->None: require(isinstance(value,list) and 1<=len(value)<=12 and all(isinstance(item,str) and 3<=len(item)<=200 and "@" not in item and "http" not in item.casefold() for item in value),f"{field} requires safe opaque evidence references.") def validate(record:object)->None: require(isinstance(record,dict),"Acceptance record must be a JSON object.") require(record.get("schemaVersion")==1,"Unsupported account-security acceptance schema.") require(record.get("system")=="guestops-account-security","system must be guestops-account-security.") require(record.get("milestone")==19,"milestone must be 19.") require(re.fullmatch(r"[0-9a-f]{40}",str(record.get("releaseCommit",""))) is not None,"releaseCommit must be a full lowercase Git SHA.") require(re.fullmatch(r"[0-9a-f]{64}",str(record.get("releaseRecordSha256",""))) is not None,"releaseRecordSha256 must be a SHA-256 digest.") origin=urlparse(str(record.get("environment","")));require(origin.scheme=="https" and origin.hostname and origin.path in ("","/") and not origin.query and not origin.fragment and origin.username is None,"environment must be an HTTPS origin without credentials, path, query or fragment.") operator=person(record.get("operator"),"operator");reviewer=person(record.get("reviewedBy"),"reviewedBy");security=person(record.get("securityReviewedBy"),"securityReviewedBy") require(len({operator.casefold(),reviewer.casefold(),security.casefold()})==3,"operator, reviewer, and security reviewer must be different people.") started=timestamp(record.get("startedAt"),"startedAt");ended=timestamp(record.get("endedAt"),"endedAt");reviewed=timestamp(record.get("reviewedAt"),"reviewedAt");security_at=timestamp(record.get("securityReviewedAt"),"securityReviewedAt") require(started<=ended<=reviewed and ended<=security_at,"Acceptance timestamps are out of order.") refs(record.get("securityReviewEvidence"),"securityReviewEvidence") controls=record.get("productionControls");require(isinstance(controls,dict),"productionControls is required.") require(set(controls)=={"smtpEnabled","requireMfa","enabledAfterSecurityReview"},"productionControls has unexpected fields.") require(all(isinstance(controls[key],bool) for key in controls),"productionControls values must be booleans.") require(controls["enabledAfterSecurityReview"],"Production SMTP and MFA enforcement must be enabled only after independent security review.") scenarios=record.get("scenarios");require(isinstance(scenarios,list),"scenarios must be a list.") ids=[item.get("id") for item in scenarios if isinstance(item,dict)];require(len(ids)==len(scenarios)==len(set(ids)) and set(ids)==SCENARIOS,"Acceptance record requires the exact milestone 19 scenario set.") for item in scenarios: require(item.get("status")=="pass",f"Scenario {item['id']} has not passed.");refs(item.get("evidence"),f"Scenario {item['id']}") findings=record.get("findings");require(isinstance(findings,list),"findings must be a list.") for finding in findings: require(isinstance(finding,dict) and finding.get("status") in ("resolved","accepted"),"Every finding must be resolved or explicitly accepted.") require(3<=len(str(finding.get("id","")))<=80,"Every finding requires an opaque ID.") person(finding.get("owner"),f"Finding {finding.get('id')} owner");refs(finding.get("evidence"),f"Finding {finding.get('id')}") def main()->None: parser=argparse.ArgumentParser(description=__doc__);parser.add_argument("record",type=Path);args=parser.parse_args();validate(json.loads(args.record.read_text(encoding="utf-8"))) print("Milestone 19 account-security acceptance record is structurally complete. This validates the record, not its restricted evidence.") if __name__=="__main__": try:main() except (OSError,ValueError,json.JSONDecodeError) as error: print(f"Account-security acceptance record rejected: {error}",file=__import__("sys").stderr);raise SystemExit(1)