feat(iwooos): add P0 security incident convergence gate

This commit is contained in:
ogt
2026-06-25 19:25:55 +08:00
parent e11130440b
commit 97affa698a
6 changed files with 1361 additions and 1 deletions

View File

@@ -0,0 +1,503 @@
#!/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())