#!/usr/bin/env python3 """Generate a fail-closed owner response template for active post-reboot gates. Read-only by design. This script only turns an owner-packet artifact into a placeholder response JSON. It never sends requests, reads secrets, writes credential markers, or modifies host/runtime state. """ from __future__ import annotations import argparse import json import subprocess import sys from datetime import datetime from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[2] OWNER_PACKET_GENERATOR = ( ROOT / "scripts" / "reboot-recovery" / "post-reboot-next-gate-owner-packets.py" ) EXPECTED_OWNER_PACKET_SCHEMA = "awoooi_post_reboot_next_gate_owner_packets_v1" RESPONSE_SCHEMA = "awoooi_post_reboot_next_gate_owner_response_v1" ESCROW_ITEM_IDS = [ "restic_repository_password", "offsite_provider_credentials", "break_glass_admin_credentials", "dns_registrar_recovery", "oauth_ai_provider_recovery", ] EXPECTED_HOST_ALIASES = [ "core-110", "gateway-188", "k3s-control-120", "k3s-control-121", "security-observer-112", "dev-workstation-111", ] def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Create a placeholder owner response JSON for active post-reboot gates.", ) parser.add_argument( "--owner-packet-file", type=Path, help="Use an existing post-reboot owner packet JSON.", ) parser.add_argument( "--dispatch-file", type=Path, help="Generate owner packets from an existing next-gate dispatch file.", ) parser.add_argument( "--summary-file", type=Path, help="Generate owner packets from an existing readiness summary file.", ) parser.add_argument( "--output", type=Path, help="Write JSON to this path instead of stdout.", ) parser.add_argument( "--no-color", action="store_true", help="Pass --no-color when generating owner packets.", ) return parser.parse_args() def load_json(path: Path, label: str) -> dict[str, Any]: try: payload = json.loads(path.read_text(encoding="utf-8")) except FileNotFoundError as exc: raise SystemExit(f"{label}_not_found={path}") from exc except json.JSONDecodeError as exc: raise SystemExit(f"{label}_json_invalid={exc}") from exc if not isinstance(payload, dict): raise SystemExit(f"{label}_json_not_object") return payload def generate_owner_packet(args: argparse.Namespace) -> dict[str, Any]: cmd = [str(OWNER_PACKET_GENERATOR)] if args.dispatch_file: cmd.extend(["--dispatch-file", str(args.dispatch_file)]) if args.summary_file: cmd.extend(["--summary-file", str(args.summary_file)]) if args.no_color: cmd.append("--no-color") completed = subprocess.run( cmd, cwd=ROOT, check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) if completed.returncode != 0: raise SystemExit( "owner_packet_generation_failed " f"rc={completed.returncode}\n{completed.stdout}" ) try: packet = json.loads(completed.stdout) except json.JSONDecodeError as exc: raise SystemExit(f"owner_packet_json_invalid={exc}") from exc if not isinstance(packet, dict): raise SystemExit("owner_packet_json_not_object") return packet def load_owner_packet(args: argparse.Namespace) -> dict[str, Any]: if args.owner_packet_file: return load_json(args.owner_packet_file, label="owner_packet_file") return generate_owner_packet(args) def as_list(value: Any) -> list[Any]: if value is None: return [] if isinstance(value, list): return value return [value] def ensure_owner_packet(packet: dict[str, Any]) -> list[dict[str, Any]]: if packet.get("schema_version") != EXPECTED_OWNER_PACKET_SCHEMA: raise SystemExit(f"owner_packet_schema={packet.get('schema_version')!r}") owner_packets = [item for item in as_list(packet.get("owner_packets")) if isinstance(item, dict)] source = packet.get("source", {}) if isinstance(source, dict): expected = {str(item) for item in as_list(source.get("next_required_gates"))} actual = {str(item.get("packet_id")) for item in owner_packets if item.get("packet_id")} if expected and actual != expected: raise SystemExit(f"owner_packet_gate_mismatch expected={sorted(expected)} actual={sorted(actual)}") return owner_packets def common_response(packet: dict[str, Any]) -> dict[str, Any]: return { "gate_id": str(packet.get("packet_id", "unknown")), "owner_role": "owner_role_here", "owner_team": "owner_team_here", "decision": "pending", "decision_reason": "decision_reason_here", "affected_scope": str(packet.get("title", "post-reboot owner evidence")), "redacted_evidence_refs": [ "redacted_evidence_ref_here", ], "followup_owner": "followup_owner_here", "runtime_action_requested": False, "runtime_action_authorized": False, "host_write_requested": False, "host_write_authorized": False, "secret_value_included": False, "secret_value_collection_allowed": False, } def credential_escrow_response(packet: dict[str, Any]) -> dict[str, Any]: item_ids = [ str(item) for item in as_list(packet.get("required_items")) if str(item) in ESCROW_ITEM_IDS ] or ESCROW_ITEM_IDS response = common_response(packet) response["credential_marker_write_requested"] = False response["credential_marker_write_authorized"] = False response["escrow_items"] = [ { "item_id": item_id, "non_secret_evidence_ref": "non_secret_evidence_ref_here", "recovery_owner": "owner_role_here", "reviewer": "reviewer_here", "last_reviewed_at": "pending", "contains_secret_value": False, } for item_id in item_ids ] return response def wazuh_registry_response(packet: dict[str, Any]) -> dict[str, Any]: response = common_response(packet) response.update( { "wazuh_active_response_requested": False, "wazuh_active_response_authorized": False, "agent_reenroll_requested": False, "wazuh_restart_requested": False, "kali_active_scan_requested": False, "registry_export_ref": "registry_export_ref_here", "registry_time_window": "pending", "expected_host_aliases": EXPECTED_HOST_ALIASES, "manager_registry_count": 0, "dashboard_api_connection_status": "pending", "dashboard_api_version_status": "pending", "reviewer": "reviewer_here", } ) return response def generic_unsupported_response(packet: dict[str, Any]) -> dict[str, Any]: response = common_response(packet) response["gate_specific_evidence_required"] = [ "update_post_reboot_owner_response_preflight_for_this_gate", ] return response def response_for(packet: dict[str, Any]) -> dict[str, Any]: gate_id = str(packet.get("packet_id", "")) if gate_id == "credential_escrow_evidence": return credential_escrow_response(packet) if gate_id == "wazuh_manager_registry_export": return wazuh_registry_response(packet) return generic_unsupported_response(packet) def build_template(packet: dict[str, Any]) -> dict[str, Any]: owner_packets = ensure_owner_packet(packet) gate_ids = [ str(item.get("packet_id")) for item in owner_packets if item.get("packet_id") ] return { "schema_version": RESPONSE_SCHEMA, "generated_at": datetime.now().astimezone().isoformat(timespec="seconds"), "template_status": "placeholder_fail_closed_until_redacted_owner_evidence_is_supplied", "source_owner_packet_schema": packet.get("schema_version", "unknown"), "active_gate_count": len(gate_ids), "active_gates": gate_ids, "responses": [response_for(item) for item in owner_packets], } def main() -> int: args = parse_args() packet = load_owner_packet(args) template = build_template(packet) payload = json.dumps(template, ensure_ascii=False, indent=2, sort_keys=True) if args.output: args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(payload + "\n", encoding="utf-8") else: print(payload) return 0 if __name__ == "__main__": sys.exit(main())