Files
awoooi/scripts/reboot-recovery/post-reboot-declaration-guard.py
ogt 6afa3e4f35
Some checks failed
Code Review / ai-code-review (push) Successful in 19s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
ops(reboot): classify stock eod freshness window
2026-06-26 18:24:42 +08:00

366 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
"""Guard post-reboot recovery declarations against false green claims.
Read-only by design. It consumes post-reboot-readiness-summary.sh output, or
runs that summary when no file is provided. It never repairs services, writes
markers, sends owner requests, or changes 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]
SUMMARY_SCRIPT = ROOT / "scripts" / "reboot-recovery" / "post-reboot-readiness-summary.sh"
SCHEMA_VERSION = "awoooi_post_reboot_declaration_guard_v1"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Validate what may be declared after reboot recovery.",
)
parser.add_argument(
"--summary-file",
type=Path,
help="Read an existing post-reboot readiness summary key/value file.",
)
parser.add_argument(
"--proposed",
action="append",
default=[],
help="Optional declaration to validate. Repeatable.",
)
parser.add_argument(
"--json",
action="store_true",
help="Print JSON instead of a compact status line.",
)
parser.add_argument(
"--output",
type=Path,
help="Write JSON payload to this path.",
)
parser.add_argument(
"--no-color",
action="store_true",
help="Pass --no-color when running the readiness summary.",
)
return parser.parse_args()
def run_summary(no_color: bool) -> str:
cmd = [str(SUMMARY_SCRIPT)]
if 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 not in (0, 2):
raise SystemExit(
"POST_REBOOT_DECLARATION_GUARD_FAILED "
f"summary_rc={completed.returncode}\n{completed.stdout}"
)
return completed.stdout
def load_summary(args: argparse.Namespace) -> str:
if args.summary_file:
return args.summary_file.read_text(encoding="utf-8")
return run_summary(no_color=args.no_color)
def parse_summary(text: str) -> dict[str, str]:
values: dict[str, str] = {}
for raw_line in text.splitlines():
line = raw_line.strip()
if not line or "=" not in line:
continue
key, value = line.split("=", 1)
values[key.strip()] = value.strip()
return values
def truthy(value: str | None) -> bool:
return value in {"1", "true", "True", "yes", "YES"}
def nonzero(value: str | None) -> bool:
if value is None or value in {"", "unknown"}:
return False
try:
return int(value) != 0
except ValueError:
return False
def split_csv(value: str | None) -> list[str]:
if not value or value == "none":
return []
return [item.strip() for item in value.split(",") if item.strip()]
def build_payload(summary: dict[str, str], proposed: list[str]) -> dict[str, Any]:
allowed: list[str] = []
forbidden: list[dict[str, str]] = []
service_green = truthy(summary.get("SERVICE_GREEN"))
product_data_green = truthy(summary.get("PRODUCT_DATA_GREEN"))
backup_core_green = truthy(summary.get("BACKUP_CORE_GREEN"))
dr_escrow_blocked = truthy(summary.get("DR_ESCROW_BLOCKED")) or nonzero(
summary.get("ESCROW_MISSING_COUNT")
)
host_188_hygiene_blocked = truthy(summary.get("HOST_188_HYGIENE_BLOCKED"))
wazuh_registry_accepted = truthy(summary.get("WAZUH_MANAGER_REGISTRY_ACCEPTED"))
runtime_authorized = truthy(summary.get("RUNTIME_ACTION_AUTHORIZED"))
next_required_gates = split_csv(summary.get("NEXT_REQUIRED_GATES"))
overall_declaration = summary.get("OVERALL_DECLARATION", "unknown")
if service_green:
allowed.append("SERVICE_RECOVERY_GREEN")
else:
forbidden.append(
{
"declaration": "SERVICE_RECOVERY_GREEN",
"reason": "service_green_not_1",
}
)
if product_data_green:
allowed.append("PRODUCT_DATA_GREEN")
else:
forbidden.append(
{
"declaration": "PRODUCT_DATA_GREEN",
"reason": "product_data_green_not_1",
}
)
if backup_core_green:
allowed.append("BACKUP_CORE_GREEN")
else:
forbidden.append(
{
"declaration": "BACKUP_CORE_GREEN",
"reason": "backup_core_green_not_1",
}
)
if overall_declaration != "unknown":
allowed.append(overall_declaration)
if (
service_green
and product_data_green
and backup_core_green
and not dr_escrow_blocked
and not host_188_hygiene_blocked
and wazuh_registry_accepted
and not runtime_authorized
):
allowed.append("FULL_STACK_GREEN")
else:
full_stack_reasons: list[str] = []
if not service_green:
full_stack_reasons.append("service_green_not_1")
if not product_data_green:
full_stack_reasons.append("product_data_green_not_1")
if not backup_core_green:
full_stack_reasons.append("backup_core_green_not_1")
if dr_escrow_blocked:
full_stack_reasons.append(
f"escrow_missing_count:{summary.get('ESCROW_MISSING_COUNT', 'unknown')}"
)
if host_188_hygiene_blocked:
full_stack_reasons.append("host_188_hygiene_blocked:1")
if not wazuh_registry_accepted:
full_stack_reasons.append("wazuh_manager_registry_accepted:0")
if runtime_authorized:
full_stack_reasons.append("runtime_action_authorized:1")
forbidden.append(
{
"declaration": "FULL_STACK_GREEN",
"reason": ",".join(full_stack_reasons) or "unknown",
}
)
if dr_escrow_blocked:
forbidden.append(
{
"declaration": "DR_COMPLETE",
"reason": f"escrow_missing_count:{summary.get('ESCROW_MISSING_COUNT', 'unknown')}",
}
)
else:
allowed.append("DR_COMPLETE")
if host_188_hygiene_blocked:
forbidden.append(
{
"declaration": "HOST_188_FULLY_GREEN",
"reason": "host_188_hygiene_blocked:1",
}
)
else:
allowed.append("HOST_188_FULLY_GREEN")
if not wazuh_registry_accepted:
forbidden.append(
{
"declaration": "WAZUH_REGISTRY_RECOVERED",
"reason": "wazuh_manager_registry_accepted:0",
}
)
else:
allowed.append("WAZUH_REGISTRY_RECOVERED")
if not runtime_authorized:
forbidden.append(
{
"declaration": "RUNTIME_ACTION_AUTHORIZED",
"reason": "runtime_action_authorized:0",
}
)
else:
allowed.append("RUNTIME_ACTION_AUTHORIZED")
allowed_set = set(allowed)
forbidden_map = {item["declaration"]: item["reason"] for item in forbidden}
rejected_proposed = [
{"declaration": item, "reason": forbidden_map[item]}
for item in proposed
if item in forbidden_map
]
accepted_proposed = [item for item in proposed if item in allowed_set]
unknown_proposed = [
item
for item in proposed
if item not in allowed_set and item not in forbidden_map
]
if not service_green:
status = "blocked_service_recovery"
elif not product_data_green:
status = "blocked_product_data_recovery"
else:
status = "allowed_with_boundary_blockers"
if rejected_proposed:
status = "blocked_false_green_proposal"
return {
"schema_version": SCHEMA_VERSION,
"generated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
"source": {
"summary_script": str(SUMMARY_SCRIPT.relative_to(ROOT)),
"artifact_dir": summary.get("ARTIFACT_DIR", "unknown"),
"overall_declaration": overall_declaration,
},
"status": status,
"allowed_declarations": sorted(set(allowed)),
"forbidden_declarations": forbidden,
"next_required_gates": next_required_gates,
"counts": {
"allowed_count": len(set(allowed)),
"forbidden_count": len(forbidden),
"next_required_gate_count": len(next_required_gates),
"proposed_count": len(proposed),
"accepted_proposed_count": len(accepted_proposed),
"rejected_proposed_count": len(rejected_proposed),
"unknown_proposed_count": len(unknown_proposed),
},
"proposed": {
"accepted": accepted_proposed,
"rejected": rejected_proposed,
"unknown": unknown_proposed,
},
"evidence": {
"service_green": summary.get("SERVICE_GREEN", "unknown"),
"product_data_green": summary.get("PRODUCT_DATA_GREEN", "unknown"),
"stock_freshness_status": summary.get("STOCK_FRESHNESS_STATUS", "unknown"),
"stock_latest_trading_date": summary.get(
"STOCK_LATEST_TRADING_DATE",
"unknown",
),
"stock_blockers": summary.get("STOCK_BLOCKERS", "unknown"),
"stock_eod_window_pending": summary.get(
"STOCK_EOD_WINDOW_PENDING",
"unknown",
),
"stock_eod_classification": summary.get(
"STOCK_EOD_CLASSIFICATION",
"unknown",
),
"stock_eod_next_action": summary.get("STOCK_EOD_NEXT_ACTION", "unknown"),
"backup_core_green": summary.get("BACKUP_CORE_GREEN", "unknown"),
"escrow_missing_count": summary.get("ESCROW_MISSING_COUNT", "unknown"),
"host_188_hygiene_blocked": summary.get("HOST_188_HYGIENE_BLOCKED", "unknown"),
"wazuh_manager_registry_accepted": summary.get(
"WAZUH_MANAGER_REGISTRY_ACCEPTED",
"unknown",
),
"wazuh_route_code": summary.get("WAZUH_ROUTE_CODE", "unknown"),
"wazuh_transport_count": summary.get("WAZUH_TRANSPORT_COUNT", "unknown"),
"wazuh_coverage_scope": summary.get("WAZUH_COVERAGE_SCOPE", "unknown"),
"wazuh_direct_active": summary.get("WAZUH_DIRECT_ACTIVE", "unknown"),
"wazuh_no_transport": summary.get("WAZUH_NO_TRANSPORT", "unknown"),
"wazuh_ssh_blocked": summary.get("WAZUH_SSH_BLOCKED", "unknown"),
"wazuh_dashboard_api_connection": summary.get(
"WAZUH_DASHBOARD_API_CONNECTION",
"unknown",
),
"wazuh_dashboard_index_ok": summary.get("WAZUH_DASHBOARD_INDEX_OK", "unknown"),
"runtime_action_authorized": summary.get("RUNTIME_ACTION_AUTHORIZED", "unknown"),
},
"runtime_write_performed": False,
}
def main() -> int:
args = parse_args()
summary = parse_summary(load_summary(args))
payload = build_payload(summary, args.proposed)
serialized = json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True)
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(serialized + "\n", encoding="utf-8")
if args.json:
print(serialized)
else:
prefix = "POST_REBOOT_DECLARATION_GUARD_OK"
if payload["proposed"]["rejected"]:
prefix = "POST_REBOOT_DECLARATION_GUARD_REJECTED"
elif payload["status"] == "blocked_service_recovery":
prefix = "POST_REBOOT_DECLARATION_GUARD_BLOCKED"
print(
f"{prefix} "
f"status={payload['status']} "
f"allowed={payload['counts']['allowed_count']} "
f"forbidden={payload['counts']['forbidden_count']} "
f"next_gates={payload['counts']['next_required_gate_count']} "
f"rejected_proposed={payload['counts']['rejected_proposed_count']}"
)
if payload["proposed"]["rejected"]:
return 2
if payload["status"] == "blocked_service_recovery":
return 2
return 0
if __name__ == "__main__":
sys.exit(main())