194 lines
6.8 KiB
Python
194 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Regression guardrails for the offline Phase-0.8R remediation contract."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import copy
|
|
import importlib.util
|
|
from pathlib import Path
|
|
from types import ModuleType
|
|
from typing import Any
|
|
|
|
|
|
def load_validator(root: Path) -> ModuleType:
|
|
path = root / "tools/validate_phase08_remediation.py"
|
|
spec = importlib.util.spec_from_file_location("phase08r_validator", path)
|
|
if spec is None or spec.loader is None:
|
|
raise RuntimeError("could not load Phase-0.8R validator")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def require_invalid(errors: list[str], scenario: str) -> None:
|
|
if not errors:
|
|
raise RuntimeError(f"unsafe mutation passed validation: {scenario}")
|
|
|
|
|
|
def validate_manifest_mutation(
|
|
validator: ModuleType,
|
|
manifest: dict[str, Any],
|
|
denylist: dict[str, Any],
|
|
scenario: str,
|
|
) -> None:
|
|
require_invalid(validator.validate_manifest(manifest, denylist), scenario)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--root", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
root = args.root.resolve()
|
|
validator = load_validator(root)
|
|
|
|
errors = validator.collect_errors(root, require_local_source=False)
|
|
if errors:
|
|
raise RuntimeError("; ".join(errors))
|
|
|
|
manifest = validator.load_json(
|
|
root / "manifests/runtime/phase-0.8-remediation.json"
|
|
)
|
|
denylist = validator.load_json(root / "manifests/artifact-denylist.json")
|
|
doc_contract = validator.extract_json_contract(
|
|
root / "docs/runtime/phase-0.8-remediation.md", "PHASE08R_CONTRACT"
|
|
)
|
|
template = validator.extract_json_contract(
|
|
root / "docs/approvals/phase-0.8-bounded-observation-template.md",
|
|
"PHASE08_BOUNDED_OBSERVATION_TEMPLATE",
|
|
)
|
|
|
|
ready = copy.deepcopy(manifest)
|
|
ready["status"] = "READY"
|
|
validate_manifest_mutation(
|
|
validator, ready, denylist, "general READY status overrides blockers"
|
|
)
|
|
|
|
authorized = copy.deepcopy(manifest)
|
|
authorized["authorization"]["execution_authorized"] = True
|
|
validate_manifest_mutation(
|
|
validator, authorized, denylist, "execution authorization became true"
|
|
)
|
|
|
|
retry = copy.deepcopy(manifest)
|
|
retry["authorization"]["automatic_retry"] = True
|
|
validate_manifest_mutation(
|
|
validator, retry, denylist, "automatic retry became true"
|
|
)
|
|
|
|
current_stock = copy.deepcopy(manifest)
|
|
current_stock["stock_identities"]["classification"] = "current_device_identity"
|
|
current_stock["stock_identities"]["current_device_observed"] = True
|
|
validate_manifest_mutation(
|
|
validator, current_stock, denylist, "reference-only hashes were promoted"
|
|
)
|
|
|
|
backup_ready = copy.deepcopy(manifest)
|
|
backup_ready["payload_manager_backup"]["classification"] = "ready"
|
|
backup_ready["payload_manager_backup"]["on_device_proven"] = True
|
|
backup_ready["payload_manager_backup"]["byte_exact_proven"] = True
|
|
backup_ready["payload_manager_backup"]["result"] = "PASS"
|
|
validate_manifest_mutation(
|
|
validator, backup_ready, denylist, "manager backup hard blocker disappeared"
|
|
)
|
|
|
|
missing_deny = copy.deepcopy(denylist)
|
|
missing_deny["entries"] = []
|
|
validate_manifest_mutation(
|
|
validator, manifest, missing_deny, "permanent denylist entry disappeared"
|
|
)
|
|
|
|
non_options_read_only = copy.deepcopy(manifest)
|
|
for finding in non_options_read_only["side_effect_findings"]:
|
|
if finding["id"] == "get_version":
|
|
finding["writes_server_active_flag"] = False
|
|
finding["strict_read_only_preflight_suitable"] = True
|
|
validate_manifest_mutation(
|
|
validator,
|
|
non_options_read_only,
|
|
denylist,
|
|
"non-OPTIONS request was called strict read-only",
|
|
)
|
|
|
|
autoload_read_only = copy.deepcopy(manifest)
|
|
for finding in autoload_read_only["side_effect_findings"]:
|
|
if finding["id"] == "get_autoload_status":
|
|
finding["writes_autoload_triggered"] = False
|
|
finding["reads_filesystem_or_configuration"] = False
|
|
validate_manifest_mutation(
|
|
validator,
|
|
autoload_read_only,
|
|
denylist,
|
|
"/autoload_status mutations and reads were hidden",
|
|
)
|
|
|
|
options_suitable = copy.deepcopy(manifest)
|
|
for finding in options_suitable["side_effect_findings"]:
|
|
if finding["id"] == "options_any_endpoint":
|
|
finding["strict_read_only_preflight_suitable"] = True
|
|
validate_manifest_mutation(
|
|
validator, options_suitable, denylist, "OPTIONS was promoted to collector"
|
|
)
|
|
|
|
hardware_claim = copy.deepcopy(manifest)
|
|
hardware_claim["claim_boundaries"]["hardware_safety_proven"] = True
|
|
hardware_claim["firmware_runtime_behavior"] = "PROVEN_SAFE"
|
|
validate_manifest_mutation(
|
|
validator, hardware_claim, denylist, "host evidence became hardware proof"
|
|
)
|
|
|
|
retroarch_active = copy.deepcopy(manifest)
|
|
retroarch_active["retroarch"]["active_phase"] = True
|
|
retroarch_active["retroarch"]["work_started"] = True
|
|
validate_manifest_mutation(
|
|
validator, retroarch_active, denylist, "RetroArch became active work"
|
|
)
|
|
|
|
template_authorized = copy.deepcopy(template)
|
|
template_authorized["authorized"] = True
|
|
require_invalid(
|
|
validator.validate_template(template_authorized),
|
|
"bounded-observation template became authorization",
|
|
)
|
|
|
|
template_prefilled = copy.deepcopy(template)
|
|
template_prefilled["required_fields"]["collector_sha256"] = "0" * 64
|
|
require_invalid(
|
|
validator.validate_template(template_prefilled),
|
|
"template invented an artifact hash",
|
|
)
|
|
|
|
merged_approvals = copy.deepcopy(manifest)
|
|
del merged_approvals["authorization"]["lifecycle_authorized"]
|
|
validate_manifest_mutation(
|
|
validator,
|
|
merged_approvals,
|
|
denylist,
|
|
"installation and lifecycle authorization were merged",
|
|
)
|
|
|
|
doc_without_hard_blocker = copy.deepcopy(doc_contract)
|
|
doc_without_hard_blocker["blockers"].remove(
|
|
"payload_manager_backup_not_byte_exact_on_device"
|
|
)
|
|
require_invalid(
|
|
validator.validate_doc_contract(doc_without_hard_blocker, manifest),
|
|
"documentation omitted manager backup hard blocker",
|
|
)
|
|
|
|
if validator.validate_changed_files(root):
|
|
raise RuntimeError("; ".join(validator.validate_changed_files(root)))
|
|
|
|
print(
|
|
"Phase-0.8R regression guardrails passed: immutable hashes, blocked "
|
|
"status, false authorizations, denylist, reference-only stock hashes, "
|
|
"hard backup gate, route effects, template denial, phase separation, "
|
|
"RetroArch deferral, and no target artifact"
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|