#!/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())