Files
awoooi/scripts/security/iwooos-p0-security-incident-convergence-gate.py

504 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())