#!/usr/bin/env python3
"""
IwoooS P0 資安事件收斂 Gate。

本工具讀取既有只讀 snapshot，將 Wazuh API / agent 納管、主機入侵、
Public Gateway / Nginx、SSH / firewall、host runtime、monitoring alert、
SOC / Kali 與高價值配置收斂成一張 P0 事件總覽。

它不連線 Wazuh、不 SSH、不讀 live config、不做 scan、不 reload、
不封鎖端口、不重啟服務、不送通知、不建立 SOAR case、不收 secret，
也不把 route 200、Dashboard 可見、agent active 或外部宣稱視為完成。
"""

from __future__ import annotations

import argparse
import json
import re
import subprocess
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any


TAIPEI = timezone(timedelta(hours=8))
SCHEMA_VERSION = "iwooos_p0_security_incident_convergence_gate_v1"

SOURCE_SNAPSHOTS = {
    "wazuh_runtime": "docs/security/wazuh-agent-visibility-runtime-gate.snapshot.json",
    "wazuh_coverage": "docs/security/wazuh-managed-host-coverage-gate.snapshot.json",
    "wazuh_intrusion": "docs/security/wazuh-iwooos-intrusion-readback-plan.snapshot.json",
    "soc_integration": "docs/security/soc-siem-kali-wazuh-integration-control.snapshot.json",
    "external_host": "docs/security/external-host-intrusion-prevention-control.snapshot.json",
    "high_value_config": "docs/security/high-value-config-control-coverage.snapshot.json",
    "public_gateway": "docs/security/public-gateway-preflight-inventory.snapshot.json",
    "public_gateway_post_incident": "docs/security/public-gateway-post-incident-readback-plan.snapshot.json",
    "ssh_network_post_incident": "docs/security/ssh-network-post-incident-readback-plan.snapshot.json",
    "port_firewall": "docs/security/port-firewall-change-evidence-acceptance.snapshot.json",
    "host_service_post_incident": "docs/security/host-service-post-incident-readback-plan.snapshot.json",
    "monitoring_post_incident": "docs/security/monitoring-post-incident-readback-plan.snapshot.json",
}

P0_LANE_DEFINITIONS = [
    {
        "lane_id": "wazuh_dashboard_api_registry",
        "priority": "P0",
        "label": "Wazuh API 與 manager registry 真相",
        "source_keys": ["wazuh_runtime", "wazuh_coverage"],
        "next_gate": "dashboard_api_repair_postcheck_and_manager_registry_owner_evidence",
        "required_evidence": [
            "dashboard_api_connection_ok_ref",
            "dashboard_api_version_ok_ref",
            "manager_registry_agent_counts_ref",
            "per_host_agent_scope_matrix_ref",
        ],
    },
    {
        "lane_id": "host_intrusion_forensics",
        "priority": "P0",
        "label": "主機入侵鑑識與 containment 決策",
        "source_keys": ["wazuh_intrusion", "external_host"],
        "next_gate": "wazuh_event_host_forensic_containment_owner_packet",
        "required_evidence": [
            "wazuh_event_refs",
            "host_auth_process_network_fim_refs",
            "containment_decision_ref",
            "recovery_proof_and_postcheck_ref",
        ],
    },
    {
        "lane_id": "public_gateway_nginx",
        "priority": "P0",
        "label": "Public Gateway / Nginx 變更收斂",
        "source_keys": ["public_gateway", "public_gateway_post_incident", "high_value_config"],
        "next_gate": "owner_live_conf_rendered_diff_nginx_test_route_smoke_packet",
        "required_evidence": [
            "owner_provided_redacted_live_conf_ref",
            "source_to_live_rendered_diff_ref",
            "nginx_test_readback_ref",
            "route_smoke_and_rollback_ref",
        ],
    },
    {
        "lane_id": "ssh_firewall_ports",
        "priority": "P0",
        "label": "SSH / firewall / port / network policy baseline",
        "source_keys": ["ssh_network_post_incident", "port_firewall", "external_host"],
        "next_gate": "before_after_state_actor_impact_rollback_packet",
        "required_evidence": [
            "before_state_ref",
            "after_state_ref",
            "actor_attribution_ref",
            "service_health_impact_and_rollback_ref",
        ],
    },
    {
        "lane_id": "host_runtime_services",
        "priority": "P0",
        "label": "Docker / systemd / process / port binding",
        "source_keys": ["host_service_post_incident", "external_host"],
        "next_gate": "host_runtime_forensic_service_recovery_packet",
        "required_evidence": [
            "docker_daemon_state_ref",
            "systemd_unit_state_ref",
            "process_port_binding_ref",
            "dependency_and_postcheck_ref",
        ],
    },
    {
        "lane_id": "monitoring_alert_receipt",
        "priority": "P0",
        "label": "監控告警可讀性、收件與 no-false-green",
        "source_keys": ["monitoring_post_incident", "soc_integration"],
        "next_gate": "alert_receipt_noise_budget_readable_message_contract",
        "required_evidence": [
            "alert_route_receipt_ref",
            "message_contract_readability_ref",
            "dedupe_noise_budget_ref",
            "post_change_monitoring_window_ref",
        ],
    },
    {
        "lane_id": "soc_kali_wazuh_case",
        "priority": "P0",
        "label": "SOC / Kali / Wazuh 事件 case 化",
        "source_keys": ["soc_integration", "wazuh_intrusion"],
        "next_gate": "incident_case_owner_severity_confidence_chain_of_custody_packet",
        "required_evidence": [
            "incident_case_ref",
            "severity_confidence_mapping_ref",
            "kali_scope_and_finding_envelope_ref",
            "chain_of_custody_ref",
        ],
    },
    {
        "lane_id": "cross_project_freeze_runtime_boundary",
        "priority": "P0",
        "label": "跨專案 freeze、runtime 邊界與防衝突",
        "source_keys": ["external_host", "high_value_config", "soc_integration"],
        "next_gate": "cross_project_sync_runtime_authorization_owner_packet",
        "required_evidence": [
            "cross_project_sync_ref",
            "maintenance_window_or_break_glass_ref",
            "rollback_owner_ref",
            "runtime_authorization_ref",
        ],
    },
]

FORBIDDEN_TEXT_PATTERNS = [
    re.compile(r"\b(?:10|127|172\.(?:1[6-9]|2\d|3[01])|192\.168)\.\d{1,3}\.\d{1,3}\b"),
    re.compile(r"Authorization\s*:", re.IGNORECASE),
    re.compile(r"Bearer\s+[A-Za-z0-9._-]{10,}", re.IGNORECASE),
    re.compile(r"Basic\s+[A-Za-z0-9+/=]{10,}", re.IGNORECASE),
    re.compile(r"password\s*[:=]\s*['\"][^'\"]+['\"]", re.IGNORECASE),
    re.compile(r"token\s*[:=]\s*['\"][^'\"]+['\"]", re.IGNORECASE),
    re.compile(r"cookie\s*[:=]\s*['\"][^'\"]+['\"]", re.IGNORECASE),
    re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
    re.compile(r"/Users/"),
    re.compile(r"\.codex", re.IGNORECASE),
    re.compile(r"codex_delegation", re.IGNORECASE),
    re.compile(r"In app browser", re.IGNORECASE),
    re.compile(r"My request for Codex", re.IGNORECASE),
    re.compile(r"批准！繼續"),
]

BLOCKED_RUNTIME_ACTIONS = [
    "wazuh_api_live_query",
    "wazuh_active_response",
    "kali_active_scan",
    "kali_execute",
    "ssh_read",
    "ssh_write",
    "host_write",
    "firewall_change",
    "port_close",
    "port_open",
    "nginx_test",
    "nginx_reload",
    "docker_restart",
    "systemctl_restart",
    "argocd_sync",
    "workflow_modification",
    "repo_secret_change",
    "secret_rotation",
    "telegram_send",
    "soar_case_create",
    "auto_block",
    "production_write",
    "force_push",
]


def git_short_sha(root: Path) -> str:
    try:
        result = subprocess.run(
            ["git", "rev-parse", "--short", "HEAD"],
            cwd=root,
            check=True,
            capture_output=True,
            text=True,
        )
        return result.stdout.strip()
    except Exception:
        return "unknown"


def load_json(root: Path, relative_path: str) -> dict[str, Any]:
    path = root / relative_path
    if not path.exists():
        raise SystemExit(f"BLOCKED source_snapshot_missing: {relative_path}")
    return json.loads(path.read_text(encoding="utf-8"))


def nested_get(data: dict[str, Any], key: str, default: Any = 0) -> Any:
    if key in data:
        return data[key]
    summary = data.get("summary", {})
    if isinstance(summary, dict) and key in summary:
        return summary[key]
    return default


def collect_string_values(value: Any) -> list[str]:
    if isinstance(value, str):
        return [value]
    if isinstance(value, list):
        values: list[str] = []
        for item in value:
            values.extend(collect_string_values(item))
        return values
    if isinstance(value, dict):
        values: list[str] = []
        for item in value.values():
            values.extend(collect_string_values(item))
        return values
    return []


def validate_no_forbidden_text(report: dict[str, Any]) -> None:
    for text in collect_string_values(report):
        for pattern in FORBIDDEN_TEXT_PATTERNS:
            if pattern.search(text):
                raise SystemExit("BLOCKED iwooos_p0_security_incident_convergence_gate: forbidden sensitive text detected")


def bool_int(value: Any) -> int:
    return 1 if value else 0


def build_lane(definition: dict[str, Any], snapshots: dict[str, dict[str, Any]]) -> dict[str, Any]:
    source_refs = [SOURCE_SNAPSHOTS[key] for key in definition["source_keys"]]
    return {
        "lane_id": definition["lane_id"],
        "priority": definition["priority"],
        "label": definition["label"],
        "status": "blocked_waiting_owner_evidence",
        "source_snapshot_refs": source_refs,
        "next_gate": definition["next_gate"],
        "required_evidence": [
            {"evidence_id": evidence_id, "accepted": False}
            for evidence_id in definition["required_evidence"]
        ],
        "owner_response_received": False,
        "owner_response_accepted": False,
        "redacted_evidence_received": False,
        "redacted_evidence_accepted": False,
        "runtime_authorized": False,
        "action_buttons_allowed": False,
        "not_authorization": True,
        "source_statuses": {
            key: snapshots[key].get("status", "unknown")
            for key in definition["source_keys"]
        },
    }


def build_report(root: Path, generated_at: str | None) -> dict[str, Any]:
    report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
    snapshots = {key: load_json(root, path) for key, path in SOURCE_SNAPSHOTS.items()}
    lanes = [build_lane(definition, snapshots) for definition in P0_LANE_DEFINITIONS]

    wazuh_runtime = snapshots["wazuh_runtime"]
    wazuh_coverage = snapshots["wazuh_coverage"]
    wazuh_intrusion = snapshots["wazuh_intrusion"]
    public_gateway = snapshots["public_gateway"]
    public_gateway_post = snapshots["public_gateway_post_incident"]
    ssh_network = snapshots["ssh_network_post_incident"]
    port_firewall = snapshots["port_firewall"]
    host_service = snapshots["host_service_post_incident"]
    monitoring = snapshots["monitoring_post_incident"]
    soc = snapshots["soc_integration"]
    high_value = snapshots["high_value_config"]
    external_host = snapshots["external_host"]

    owner_received_total = sum(
        int(nested_get(snapshot, "owner_response_received_count", 0) or 0)
        for snapshot in snapshots.values()
    )
    owner_accepted_total = sum(
        int(nested_get(snapshot, "owner_response_accepted_count", 0) or 0)
        for snapshot in snapshots.values()
    )
    runtime_gate_total = sum(
        int(nested_get(snapshot, "runtime_gate_count", 0) or 0)
        for snapshot in snapshots.values()
    )
    action_button_total = sum(
        int(nested_get(snapshot, "action_button_count", 0) or 0)
        for snapshot in snapshots.values()
    )

    return {
        "schema_version": SCHEMA_VERSION,
        "generated_at": report_time,
        "git_commit": git_short_sha(root),
        "status": "p0_security_incident_convergence_blocked_waiting_owner_evidence",
        "mode": "snapshot_rollup_only_no_runtime_no_secret_collection",
        "source_snapshot_refs": SOURCE_SNAPSHOTS,
        "summary": {
            "source_snapshot_count": len(SOURCE_SNAPSHOTS),
            "p0_lane_count": len(lanes),
            "blocked_lane_count": len(lanes),
            "source_side_rollup_ready_percent": 100,
            "owner_response_received_count": owner_received_total,
            "owner_response_accepted_count": owner_accepted_total,
            "redacted_evidence_received_count": 0,
            "redacted_evidence_accepted_count": 0,
            "dashboard_api_connection_ok_count": int(nested_get(wazuh_runtime, "dashboard_api_connection_ok_count", 0) or 0),
            "dashboard_api_version_ok_count": int(nested_get(wazuh_runtime, "dashboard_api_version_ok_count", 0) or 0),
            "dashboard_index_pattern_ok_count": int(nested_get(wazuh_runtime, "dashboard_index_pattern_ok_count", 0) or 0),
            "manager_registry_accepted_count": int(nested_get(wazuh_coverage, "manager_registry_accepted_count", 0) or 0),
            "expected_host_scope_count": int(nested_get(wazuh_coverage, "expected_host_scope_count", 0) or 0),
            "direct_agent_active_observed_count": int(nested_get(wazuh_coverage, "direct_agent_active_observed_count", 0) or 0),
            "wazuh_event_ref_received_count": int(nested_get(wazuh_intrusion, "wazuh_event_ref_received_count", 0) or 0),
            "host_forensics_ref_received_count": int(nested_get(wazuh_intrusion, "host_forensics_ref_received_count", 0) or 0),
            "containment_decision_accepted_count": int(nested_get(wazuh_intrusion, "containment_decision_accepted_count", 0) or 0),
            "recovery_proof_accepted_count": int(nested_get(wazuh_intrusion, "recovery_proof_accepted_count", 0) or 0),
            "public_gateway_live_conf_received_count": int(nested_get(public_gateway, "owner_provided_live_conf_received_count", 0) or 0),
            "public_gateway_rendered_diff_ready_count": int(nested_get(public_gateway, "rendered_diff_ready_count", 0) or 0),
            "nginx_test_evidence_count": int(nested_get(public_gateway, "nginx_test_evidence_count", 0) or 0),
            "route_smoke_evidence_count": int(nested_get(public_gateway, "route_smoke_evidence_count", 0) or 0),
            "gateway_post_incident_readback_received_count": int(nested_get(public_gateway_post, "post_incident_readback_received_count", 0) or 0),
            "ssh_network_post_incident_readback_received_count": int(nested_get(ssh_network, "post_incident_readback_received_count", 0) or 0),
            "port_firewall_change_evidence_received_count": int(nested_get(port_firewall, "change_evidence_received_count", 0) or 0),
            "host_service_post_incident_readback_received_count": int(nested_get(host_service, "post_incident_readback_received_count", 0) or 0),
            "monitoring_post_incident_readback_received_count": int(nested_get(monitoring, "post_incident_readback_received_count", 0) or 0),
            "alert_route_accepted_count": int(nested_get(soc, "alert_route_accepted_count", 0) or 0),
            "incident_case_accepted_count": int(nested_get(soc, "incident_case_accepted_count", 0) or 0),
            "high_value_config_category_count": int(nested_get(high_value, "category_count", 0) or 0),
            "high_value_config_average_coverage_percent": int(nested_get(high_value, "average_coverage_percent", 0) or 0),
            "external_host_prevention_candidate_count": int(nested_get(external_host, "control_candidate_count", 0) or 0),
            "external_host_coverage_percent": int(nested_get(external_host, "coverage_percent_after_prevention_control", 0) or 0),
            "wazuh_active_response_authorized_count": 0,
            "kali_active_scan_authorized_count": 0,
            "host_write_authorized_count": 0,
            "firewall_change_authorized_count": 0,
            "nginx_reload_authorized_count": 0,
            "runtime_gate_count": runtime_gate_total,
            "action_button_count": action_button_total,
        },
        "p0_lanes": lanes,
        "blocked_runtime_actions": BLOCKED_RUNTIME_ACTIONS,
        "execution_boundaries": {
            "wazuh_api_live_query_authorized": False,
            "wazuh_active_response_authorized": False,
            "kali_active_scan_authorized": False,
            "kali_execute_authorized": False,
            "ssh_read_authorized": False,
            "ssh_write_authorized": False,
            "host_write_authorized": False,
            "firewall_change_authorized": False,
            "nginx_test_authorized": False,
            "nginx_reload_authorized": False,
            "docker_restart_authorized": False,
            "systemctl_restart_authorized": False,
            "argocd_sync_authorized": False,
            "workflow_modification_authorized": False,
            "repo_secret_change_authorized": False,
            "secret_value_collection_allowed": False,
            "telegram_send_authorized": False,
            "soar_case_create_authorized": False,
            "auto_block_authorized": False,
            "production_write_authorized": False,
            "runtime_execution_authorized": False,
            "action_buttons_allowed": False,
            "not_authorization": True,
        },
        "operator_interpretation": [
            "這張 Gate 是 P0 事件彙總，不是 runtime 修復授權。",
            "Wazuh Dashboard index pattern 綠燈只能當局部訊號；API connection、API version 與 manager registry 仍是硬 Gate。",
            "Nginx、firewall、host runtime、monitoring 與 SOC evidence 必須用脫敏 owner refs 補齊，不能貼 raw log 或工作視窗內容。",
            "所有 containment、active response、scan、reload、restart、secret rotation 與 production write 仍需獨立人工批准。",
        ],
    }


def validate(report: dict[str, Any]) -> None:
    if report.get("schema_version") != SCHEMA_VERSION:
        raise SystemExit("BLOCKED schema_version")
    if report.get("status") != "p0_security_incident_convergence_blocked_waiting_owner_evidence":
        raise SystemExit("BLOCKED status")
    summary = report["summary"]
    expected_zero_keys = [
        "owner_response_received_count",
        "owner_response_accepted_count",
        "redacted_evidence_received_count",
        "redacted_evidence_accepted_count",
        "dashboard_api_connection_ok_count",
        "dashboard_api_version_ok_count",
        "manager_registry_accepted_count",
        "wazuh_event_ref_received_count",
        "host_forensics_ref_received_count",
        "containment_decision_accepted_count",
        "public_gateway_live_conf_received_count",
        "public_gateway_rendered_diff_ready_count",
        "nginx_test_evidence_count",
        "route_smoke_evidence_count",
        "gateway_post_incident_readback_received_count",
        "ssh_network_post_incident_readback_received_count",
        "port_firewall_change_evidence_received_count",
        "host_service_post_incident_readback_received_count",
        "monitoring_post_incident_readback_received_count",
        "alert_route_accepted_count",
        "incident_case_accepted_count",
        "wazuh_active_response_authorized_count",
        "kali_active_scan_authorized_count",
        "host_write_authorized_count",
        "firewall_change_authorized_count",
        "nginx_reload_authorized_count",
        "runtime_gate_count",
        "action_button_count",
    ]
    for key in expected_zero_keys:
        if summary.get(key) != 0:
            raise SystemExit(f"BLOCKED summary.{key}: expected 0, got {summary.get(key)!r}")
    if summary.get("source_snapshot_count") != len(SOURCE_SNAPSHOTS):
        raise SystemExit("BLOCKED source_snapshot_count")
    if summary.get("p0_lane_count") != len(P0_LANE_DEFINITIONS):
        raise SystemExit("BLOCKED p0_lane_count")
    if summary.get("blocked_lane_count") != len(P0_LANE_DEFINITIONS):
        raise SystemExit("BLOCKED blocked_lane_count")
    for lane in report.get("p0_lanes", []):
        if lane.get("status") != "blocked_waiting_owner_evidence":
            raise SystemExit(f"BLOCKED lane.status: {lane.get('lane_id')}")
        for field in [
            "owner_response_received",
            "owner_response_accepted",
            "redacted_evidence_received",
            "redacted_evidence_accepted",
            "runtime_authorized",
            "action_buttons_allowed",
        ]:
            if lane.get(field) is not False:
                raise SystemExit(f"BLOCKED lane.{field}: {lane.get('lane_id')}")
    for key, value in report.get("execution_boundaries", {}).items():
        if key == "not_authorization":
            if value is not True:
                raise SystemExit("BLOCKED execution_boundaries.not_authorization")
        elif value is not False:
            raise SystemExit(f"BLOCKED execution_boundaries.{key}")
    validate_no_forbidden_text(report)


def main() -> int:
    parser = argparse.ArgumentParser(description="IwoooS P0 資安事件收斂 Gate")
    parser.add_argument("--root", default=".", help="repo root")
    parser.add_argument("--output", help="寫出 JSON 報告")
    parser.add_argument("--generated-at", help="固定報告時間，供 committed snapshot 使用")
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()

    root = Path(args.root).resolve()
    report = build_report(root, args.generated_at)
    validate(report)
    payload = json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True)

    if args.output:
        output = Path(args.output)
        if not output.is_absolute():
            output = root / output
        output.parent.mkdir(parents=True, exist_ok=True)
        output.write_text(payload + "\n", encoding="utf-8")
    if args.json or not args.output:
        print(payload)

    summary = report["summary"]
    print(
        "IWOOOS_P0_SECURITY_INCIDENT_CONVERGENCE_GATE_OK "
        f"sources={summary['source_snapshot_count']} "
        f"lanes={summary['p0_lane_count']} "
        f"blocked={summary['blocked_lane_count']} "
        f"registry={summary['manager_registry_accepted_count']} "
        f"evidence={summary['redacted_evidence_accepted_count']} "
        f"runtime_gate={summary['runtime_gate_count']}",
        file=sys.stderr,
    )
    return 0


if __name__ == "__main__":
    sys.exit(main())
