263 lines
8.6 KiB
Python
Executable File
263 lines
8.6 KiB
Python
Executable File
#!/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())
|