diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index ac2ec746..afea77bd 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -19802,6 +19802,67 @@ } } }, + "p0SecurityIncidentConvergence": { + "eyebrow": "P0 資安事件收斂 Gate", + "title": "把 Wazuh、主機入侵、Nginx、端口與告警紅點收成同一張處置表", + "subtitle": "這張卡彙總 12 個既有只讀 snapshot,固定 8 條 P0 事件線:Wazuh API / 登錄、主機鑑識、公開入口、SSH / firewall、主機 runtime、告警收件、SOC 事件單與跨專案 freeze;目前只顯示缺口與 0/false 邊界,不連 Wazuh、不掃描、不 reload、不封鎖、不重啟、不送通知。", + "checkLabel": "優先", + "stateLabel": "狀態", + "boundaryTitle": "P0 事件收斂邊界", + "boundaryIntro": "以下鍵值固定:P0 事件收斂不是修復完成,也不是 runtime 授權;Dashboard index pattern 綠燈、route 200、agent active、CD success、UI 可見或外部宣稱都不能單獨當資安完成。", + "summary": { + "sources": { + "label": "來源", + "detail": "12 個只讀 snapshot 已被收成同一張 P0 事件總覽。" + }, + "lanes": { + "label": "P0 線", + "detail": "8 條事件線全部仍等待 owner 脫敏證據。" + }, + "accepted": { + "label": "已接受", + "detail": "owner response、事件證據與 registry acceptance 仍為 0%。" + }, + "runtimeGate": { + "label": "執行期", + "detail": "active response、scan、reload、firewall、host write 與 action button 都是 0。" + } + }, + "items": { + "wazuhApi": { + "title": "Wazuh API / registry 是第一硬 Gate", + "body": "API connection 與 API version 仍未通過;index pattern 綠燈不能替代 manager registry 與逐主機納管證據。" + }, + "hostForensics": { + "title": "主機入侵不能只靠宣稱", + "body": "需要 Wazuh event、auth、process、network、FIM、package、persistence、containment 與 recovery proof refs。" + }, + "gatewayNginx": { + "title": "Nginx 入口先收 live diff", + "body": "需要 owner 提供脫敏 live conf、rendered diff、nginx test readback、route smoke、維護窗口與 rollback。" + }, + "sshFirewall": { + "title": "端口與防火牆要 before / after", + "body": "SSH、firewall、WireGuard、NodePort 與 NetworkPolicy 需 actor、影響、服務健康、回滾與 postcheck。" + }, + "hostRuntime": { + "title": "Docker 與 systemd 要收 runtime 證據", + "body": "需要 daemon、unit、process、port binding、dependency、recovery proof 與 postcheck refs,避免只靠 route 回 200 誤判。" + }, + "alertReceipt": { + "title": "告警要能收件與行動", + "body": "告警需有白話摘要、嚴重度、信心、asset alias、receipt、dedupe、noise budget 與 owner gate。" + }, + "socCase": { + "title": "SOC / Kali / Wazuh 要 case 化", + "body": "事件要有 case id、severity、confidence、Kali scope envelope、chain of custody 與可追蹤處置 owner。" + }, + "runtimeBoundary": { + "title": "跨專案與 runtime 動作不得自動執行", + "body": "需要跨專案同步、維護窗口或 break-glass、rollback owner;Wazuh active response、Kali scan、reload、firewall、host write、Telegram 實發與 SOAR 都需獨立批准。" + } + } + }, "securityAssetControlLedger": { "eyebrow": "P0-A 資安資產控制總帳", "title": "把主機、入口、版本來源、監控、Wazuh、Kali 與供應鏈收成一張總帳", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index ac2ec746..afea77bd 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -19802,6 +19802,67 @@ } } }, + "p0SecurityIncidentConvergence": { + "eyebrow": "P0 資安事件收斂 Gate", + "title": "把 Wazuh、主機入侵、Nginx、端口與告警紅點收成同一張處置表", + "subtitle": "這張卡彙總 12 個既有只讀 snapshot,固定 8 條 P0 事件線:Wazuh API / 登錄、主機鑑識、公開入口、SSH / firewall、主機 runtime、告警收件、SOC 事件單與跨專案 freeze;目前只顯示缺口與 0/false 邊界,不連 Wazuh、不掃描、不 reload、不封鎖、不重啟、不送通知。", + "checkLabel": "優先", + "stateLabel": "狀態", + "boundaryTitle": "P0 事件收斂邊界", + "boundaryIntro": "以下鍵值固定:P0 事件收斂不是修復完成,也不是 runtime 授權;Dashboard index pattern 綠燈、route 200、agent active、CD success、UI 可見或外部宣稱都不能單獨當資安完成。", + "summary": { + "sources": { + "label": "來源", + "detail": "12 個只讀 snapshot 已被收成同一張 P0 事件總覽。" + }, + "lanes": { + "label": "P0 線", + "detail": "8 條事件線全部仍等待 owner 脫敏證據。" + }, + "accepted": { + "label": "已接受", + "detail": "owner response、事件證據與 registry acceptance 仍為 0%。" + }, + "runtimeGate": { + "label": "執行期", + "detail": "active response、scan、reload、firewall、host write 與 action button 都是 0。" + } + }, + "items": { + "wazuhApi": { + "title": "Wazuh API / registry 是第一硬 Gate", + "body": "API connection 與 API version 仍未通過;index pattern 綠燈不能替代 manager registry 與逐主機納管證據。" + }, + "hostForensics": { + "title": "主機入侵不能只靠宣稱", + "body": "需要 Wazuh event、auth、process、network、FIM、package、persistence、containment 與 recovery proof refs。" + }, + "gatewayNginx": { + "title": "Nginx 入口先收 live diff", + "body": "需要 owner 提供脫敏 live conf、rendered diff、nginx test readback、route smoke、維護窗口與 rollback。" + }, + "sshFirewall": { + "title": "端口與防火牆要 before / after", + "body": "SSH、firewall、WireGuard、NodePort 與 NetworkPolicy 需 actor、影響、服務健康、回滾與 postcheck。" + }, + "hostRuntime": { + "title": "Docker 與 systemd 要收 runtime 證據", + "body": "需要 daemon、unit、process、port binding、dependency、recovery proof 與 postcheck refs,避免只靠 route 回 200 誤判。" + }, + "alertReceipt": { + "title": "告警要能收件與行動", + "body": "告警需有白話摘要、嚴重度、信心、asset alias、receipt、dedupe、noise budget 與 owner gate。" + }, + "socCase": { + "title": "SOC / Kali / Wazuh 要 case 化", + "body": "事件要有 case id、severity、confidence、Kali scope envelope、chain of custody 與可追蹤處置 owner。" + }, + "runtimeBoundary": { + "title": "跨專案與 runtime 動作不得自動執行", + "body": "需要跨專案同步、維護窗口或 break-glass、rollback owner;Wazuh active response、Kali scan、reload、firewall、host write、Telegram 實發與 SOAR 都需獨立批准。" + } + } + }, "securityAssetControlLedger": { "eyebrow": "P0-A 資安資產控制總帳", "title": "把主機、入口、版本來源、監控、Wazuh、Kali 與供應鏈收成一張總帳", diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index 592644e6..21bb06d6 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -351,6 +351,14 @@ type ExternalHostIntrusionPreventionControlItem = { tone: 'steady' | 'warn' | 'locked' } +type P0SecurityIncidentConvergenceItem = { + key: string + check: string + state: string + icon: typeof ShieldCheck + tone: 'steady' | 'warn' | 'locked' +} + type HighValueConfigOwnerPacketItem = { key: string gate: string @@ -2544,6 +2552,77 @@ const socSiemKaliWazuhIntegrationBoundaries = [ 'not_authorization=true', ] as const +const p0SecurityIncidentConvergenceSummary = [ + { key: 'sources', value: '12', icon: FileText, tone: 'steady' }, + { key: 'lanes', value: '8', icon: AlertTriangle, tone: 'warn' }, + { key: 'accepted', value: '0%', icon: Lock, tone: 'locked' }, + { key: 'runtimeGate', value: '0', icon: Lock, tone: 'locked' }, +] as const + +const p0SecurityIncidentConvergenceItems: P0SecurityIncidentConvergenceItem[] = [ + { key: 'wazuhApi', check: 'P0-1', state: 'API / 登錄 0', icon: Radar, tone: 'locked' }, + { key: 'hostForensics', check: 'P0-2', state: '事件 ref 0', icon: Server, tone: 'warn' }, + { key: 'gatewayNginx', check: 'P0-3', state: '環境差異 0', icon: Route, tone: 'warn' }, + { key: 'sshFirewall', check: 'P0-4', state: '前後狀態 0', icon: Network, tone: 'warn' }, + { key: 'hostRuntime', check: 'P0-5', state: '服務證據 0', icon: Activity, tone: 'warn' }, + { key: 'alertReceipt', check: 'P0-6', state: '收件 0', icon: Bell, tone: 'warn' }, + { key: 'socCase', check: 'P0-7', state: '事件單 0', icon: ClipboardCheck, tone: 'warn' }, + { key: 'runtimeBoundary', check: 'P0-8', state: '不得自動', icon: Lock, tone: 'locked' }, +] as const + +const p0SecurityIncidentConvergenceBoundaries = [ + 'iwooos_p0_security_incident_convergence_gate_visible=true', + 'iwooos_p0_security_incident_convergence_source_snapshot_count=12', + 'iwooos_p0_security_incident_convergence_p0_lane_count=8', + 'iwooos_p0_security_incident_convergence_blocked_lane_count=8', + 'iwooos_p0_security_incident_convergence_source_side_rollup_ready_percent=100', + 'iwooos_p0_security_incident_convergence_owner_response_received_count=0', + 'iwooos_p0_security_incident_convergence_owner_response_accepted_count=0', + 'iwooos_p0_security_incident_convergence_redacted_evidence_received_count=0', + 'iwooos_p0_security_incident_convergence_redacted_evidence_accepted_count=0', + 'iwooos_p0_security_incident_convergence_dashboard_api_connection_ok_count=0', + 'iwooos_p0_security_incident_convergence_dashboard_api_version_ok_count=0', + 'iwooos_p0_security_incident_convergence_dashboard_index_pattern_ok_count=3', + 'iwooos_p0_security_incident_convergence_manager_registry_accepted_count=0', + 'iwooos_p0_security_incident_convergence_expected_host_scope_count=6', + 'iwooos_p0_security_incident_convergence_direct_agent_active_observed_count=2', + 'iwooos_p0_security_incident_convergence_wazuh_event_ref_received_count=0', + 'iwooos_p0_security_incident_convergence_host_forensics_ref_received_count=0', + 'iwooos_p0_security_incident_convergence_containment_decision_accepted_count=0', + 'iwooos_p0_security_incident_convergence_public_gateway_live_conf_received_count=0', + 'iwooos_p0_security_incident_convergence_nginx_test_evidence_count=0', + 'iwooos_p0_security_incident_convergence_route_smoke_evidence_count=0', + 'iwooos_p0_security_incident_convergence_port_firewall_change_evidence_received_count=0', + 'iwooos_p0_security_incident_convergence_monitoring_post_incident_readback_received_count=0', + 'iwooos_p0_security_incident_convergence_alert_route_accepted_count=0', + 'iwooos_p0_security_incident_convergence_incident_case_accepted_count=0', + 'iwooos_p0_security_incident_convergence_high_value_config_category_count=14', + 'iwooos_p0_security_incident_convergence_high_value_config_average_coverage_percent=73', + 'iwooos_p0_security_incident_convergence_external_host_coverage_percent=74', + 'iwooos_p0_security_incident_convergence_wazuh_active_response_authorized_count=0', + 'iwooos_p0_security_incident_convergence_kali_active_scan_authorized_count=0', + 'iwooos_p0_security_incident_convergence_host_write_authorized_count=0', + 'iwooos_p0_security_incident_convergence_firewall_change_authorized_count=0', + 'iwooos_p0_security_incident_convergence_nginx_reload_authorized_count=0', + 'iwooos_p0_security_incident_convergence_runtime_gate_count=0', + 'iwooos_p0_security_incident_convergence_action_button_count=0', + 'wazuh_api_live_query_authorized=false', + 'wazuh_active_response_authorized=false', + 'kali_active_scan_authorized=false', + 'kali_execute_authorized=false', + 'ssh_write_authorized=false', + 'host_write_authorized=false', + 'firewall_change_authorized=false', + 'nginx_reload_authorized=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', +] as const + const securityAssetControlLedgerSummary = [ { key: 'assetGroups', value: '16', icon: ShieldCheck, tone: 'steady' }, { key: 'p0Groups', value: '14', icon: AlertTriangle, tone: 'warn' }, @@ -2661,7 +2740,7 @@ const externalHostIntrusionPreventionControlBoundaries = [ 'external_host_intrusion_prevention_control_action_button_count=0', 'docker_compose_systemd_host_config_coverage_percent=68', 'ssh_firewall_network_access_coverage_percent=70', - 'monitoring_alerting_observability_coverage_percent=78', + 'monitoring_alerting_observability_coverage_percent=74', 'host_write_authorized=false', 'ssh_write_authorized=false', 'firewall_change_authorized=false', @@ -8923,6 +9002,137 @@ function IwoooSSocSiemKaliWazuhIntegrationBoard() { ) } +function IwoooSP0SecurityIncidentConvergenceBoard() { + const t = useTranslations('iwooos.p0SecurityIncidentConvergence') + const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const } + + return ( + + + + + + + {t('eyebrow')} + + {t('title')} + + {t('subtitle')} + + + + + {p0SecurityIncidentConvergenceSummary.map(item => { + const Icon = item.icon + return ( + + + {t(`summary.${item.key}.label` as never)} + + + + {item.value} + + + {t(`summary.${item.key}.detail` as never)} + + + ) + })} + + + + + {p0SecurityIncidentConvergenceItems.map(item => { + const Icon = item.icon + return ( + + + + {t('checkLabel')} {item.check} + + + + + + {t(`items.${item.key}.title` as never)} + + + {t('stateLabel')}:{item.state} + + + + {t(`items.${item.key}.body` as never)} + + + ) + })} + + + + + {t('boundaryTitle')} + + + {t('boundaryIntro')} + + + {p0SecurityIncidentConvergenceBoundaries.map(item => ( + + {item} + + ))} + + + + + ) +} + function IwoooSSecurityAssetControlLedgerBoard() { const t = useTranslations('iwooos.securityAssetControlLedger') const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const } @@ -21722,6 +21932,7 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) { + diff --git a/docs/security/IWOOOS-P0-SECURITY-INCIDENT-CONVERGENCE-GATE.md b/docs/security/IWOOOS-P0-SECURITY-INCIDENT-CONVERGENCE-GATE.md new file mode 100644 index 00000000..6799e42b --- /dev/null +++ b/docs/security/IWOOOS-P0-SECURITY-INCIDENT-CONVERGENCE-GATE.md @@ -0,0 +1,77 @@ +# IwoooS P0 資安事件收斂 Gate + +| 項目 | 狀態 | +| --- | --- | +| 工具 | `scripts/security/iwooos-p0-security-incident-convergence-gate.py` | +| Snapshot | `docs/security/iwooos-p0-security-incident-convergence-gate.snapshot.json` | +| Schema | `iwooos_p0_security_incident_convergence_gate_v1` | +| 狀態 | `p0_security_incident_convergence_blocked_waiting_owner_evidence` | +| Runtime gate | `0` | +| Action button | `0` | + +## 目的 + +這個 Gate 把目前最容易造成即時資安或服務風險的紅點收成同一張只讀總覽: + +1. Wazuh API 與 manager registry 真相。 +2. 主機入侵鑑識與 containment 決策。 +3. Public Gateway / Nginx 變更收斂。 +4. SSH / firewall / port / network policy baseline。 +5. Docker / systemd / process / port binding。 +6. 監控告警可讀性、收件與 no-false-green。 +7. SOC / Kali / Wazuh 事件 case 化。 +8. 跨專案 freeze、runtime 邊界與防衝突。 + +它只讀取既有 snapshot,不連線 Wazuh、不 SSH、不讀 live config、不做 scan、不 reload、不封鎖端口、不重啟服務、不送通知、不建立 SOAR case、不收 secret。 + +## 目前讀回 + +| 指標 | 讀回 | +| --- | ---: | +| source snapshot | `12` | +| P0 lane | `8` | +| blocked lane | `8` | +| source-side rollup ready | `100%` | +| owner response received / accepted | `0 / 0` | +| redacted evidence received / accepted | `0 / 0` | +| Wazuh dashboard API connection ok | `0` | +| Wazuh dashboard API version ok | `0` | +| Wazuh manager registry accepted | `0` | +| Wazuh event ref received | `0` | +| host forensics ref received | `0` | +| Public Gateway live conf / rendered diff / nginx test / route smoke | `0 / 0 / 0 / 0` | +| monitoring post-incident readback received | `0` | +| incident case accepted | `0` | +| runtime gate / action button | `0 / 0` | + +## 不可誤判 + +- Wazuh Dashboard index pattern 綠燈只能當局部訊號;API connection、API version 與 manager registry 仍是硬 Gate。 +- route 200、Dashboard 可見、agent active、CD success、UI 可見或外部 Agent 宣稱,都不能單獨當資安完成。 +- Nginx、firewall、host runtime、monitoring 與 SOC evidence 必須用脫敏 owner refs 補齊。 +- 不得貼 raw log、raw Wazuh payload、工作視窗對話、內網 IP、個人 namespace、secret、token、private key 或未脫敏截圖。 + +## 下一步優先序 + +| 優先 | 工作 | 驗收前維持 | +| --- | --- | --- | +| P0-1 | Wazuh Dashboard API 修復 postcheck 與 manager registry owner evidence | `manager_registry_accepted_count=0` | +| P0-2 | Wazuh event、host forensic、containment decision 與 recovery proof refs | `wazuh_event_ref_received_count=0` | +| P0-3 | Nginx owner live conf、rendered diff、nginx test readback、route smoke 與 rollback refs | `nginx_reload_authorized_count=0` | +| P0-4 | SSH / firewall / port before-after state、actor、impact 與 rollback refs | `firewall_change_authorized_count=0` | +| P0-5 | Docker / systemd / process / port binding 與 dependency postcheck refs | `host_write_authorized_count=0` | +| P0-6 | 告警訊息合約、receipt、dedupe、noise budget 與 post-change monitoring refs | `alert_route_accepted_count=0` | +| P0-7 | SOC case id、severity / confidence、Kali scope envelope 與 chain of custody refs | `incident_case_accepted_count=0` | +| P0-8 | 跨專案同步、維護窗口或 break-glass、rollback owner 與 runtime authorization refs | `runtime_gate_count=0` | + +## 驗證指令 + +```bash +python3 scripts/security/iwooos-p0-security-incident-convergence-gate.py --root . +python3 scripts/security/security-mirror-progress-guard.py --root . +python3 scripts/security/iwooos-frontend-display-redaction-guard.py --root . +``` + +## 邊界 + +這個 Gate 不是批准、不是修復完成、不是 active response、不是 Kali active scan、不是 Nginx reload、不是 firewall change、不是 host write、不是 secret rotation,也不是 production write。所有 runtime 動作仍需獨立 owner、維護窗口、rollback、postcheck 與人工批准。 diff --git a/docs/security/iwooos-p0-security-incident-convergence-gate.snapshot.json b/docs/security/iwooos-p0-security-incident-convergence-gate.snapshot.json new file mode 100644 index 00000000..12349dc1 --- /dev/null +++ b/docs/security/iwooos-p0-security-incident-convergence-gate.snapshot.json @@ -0,0 +1,447 @@ +{ + "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" + ], + "execution_boundaries": { + "action_buttons_allowed": false, + "argocd_sync_authorized": false, + "auto_block_authorized": false, + "docker_restart_authorized": false, + "firewall_change_authorized": false, + "host_write_authorized": false, + "kali_active_scan_authorized": false, + "kali_execute_authorized": false, + "nginx_reload_authorized": false, + "nginx_test_authorized": false, + "not_authorization": true, + "production_write_authorized": false, + "repo_secret_change_authorized": false, + "runtime_execution_authorized": false, + "secret_value_collection_allowed": false, + "soar_case_create_authorized": false, + "ssh_read_authorized": false, + "ssh_write_authorized": false, + "systemctl_restart_authorized": false, + "telegram_send_authorized": false, + "wazuh_active_response_authorized": false, + "wazuh_api_live_query_authorized": false, + "workflow_modification_authorized": false + }, + "generated_at": "2026-06-25T19:23:16+08:00", + "git_commit": "e1113044", + "mode": "snapshot_rollup_only_no_runtime_no_secret_collection", + "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 仍需獨立人工批准。" + ], + "p0_lanes": [ + { + "action_buttons_allowed": false, + "label": "Wazuh API 與 manager registry 真相", + "lane_id": "wazuh_dashboard_api_registry", + "next_gate": "dashboard_api_repair_postcheck_and_manager_registry_owner_evidence", + "not_authorization": true, + "owner_response_accepted": false, + "owner_response_received": false, + "priority": "P0", + "redacted_evidence_accepted": false, + "redacted_evidence_received": false, + "required_evidence": [ + { + "accepted": false, + "evidence_id": "dashboard_api_connection_ok_ref" + }, + { + "accepted": false, + "evidence_id": "dashboard_api_version_ok_ref" + }, + { + "accepted": false, + "evidence_id": "manager_registry_agent_counts_ref" + }, + { + "accepted": false, + "evidence_id": "per_host_agent_scope_matrix_ref" + } + ], + "runtime_authorized": false, + "source_snapshot_refs": [ + "docs/security/wazuh-agent-visibility-runtime-gate.snapshot.json", + "docs/security/wazuh-managed-host-coverage-gate.snapshot.json" + ], + "source_statuses": { + "wazuh_coverage": "blocked_waiting_full_host_registry_readback", + "wazuh_runtime": "blocked_waiting_manager_agent_registry_readback" + }, + "status": "blocked_waiting_owner_evidence" + }, + { + "action_buttons_allowed": false, + "label": "主機入侵鑑識與 containment 決策", + "lane_id": "host_intrusion_forensics", + "next_gate": "wazuh_event_host_forensic_containment_owner_packet", + "not_authorization": true, + "owner_response_accepted": false, + "owner_response_received": false, + "priority": "P0", + "redacted_evidence_accepted": false, + "redacted_evidence_received": false, + "required_evidence": [ + { + "accepted": false, + "evidence_id": "wazuh_event_refs" + }, + { + "accepted": false, + "evidence_id": "host_auth_process_network_fim_refs" + }, + { + "accepted": false, + "evidence_id": "containment_decision_ref" + }, + { + "accepted": false, + "evidence_id": "recovery_proof_and_postcheck_ref" + } + ], + "runtime_authorized": false, + "source_snapshot_refs": [ + "docs/security/wazuh-iwooos-intrusion-readback-plan.snapshot.json", + "docs/security/external-host-intrusion-prevention-control.snapshot.json" + ], + "source_statuses": { + "external_host": "external_host_intrusion_prevention_control_ready_no_runtime_action", + "wazuh_intrusion": "wazuh_intrusion_readback_plan_ready_no_runtime_action" + }, + "status": "blocked_waiting_owner_evidence" + }, + { + "action_buttons_allowed": false, + "label": "Public Gateway / Nginx 變更收斂", + "lane_id": "public_gateway_nginx", + "next_gate": "owner_live_conf_rendered_diff_nginx_test_route_smoke_packet", + "not_authorization": true, + "owner_response_accepted": false, + "owner_response_received": false, + "priority": "P0", + "redacted_evidence_accepted": false, + "redacted_evidence_received": false, + "required_evidence": [ + { + "accepted": false, + "evidence_id": "owner_provided_redacted_live_conf_ref" + }, + { + "accepted": false, + "evidence_id": "source_to_live_rendered_diff_ref" + }, + { + "accepted": false, + "evidence_id": "nginx_test_readback_ref" + }, + { + "accepted": false, + "evidence_id": "route_smoke_and_rollback_ref" + } + ], + "runtime_authorized": false, + "source_snapshot_refs": [ + "docs/security/public-gateway-preflight-inventory.snapshot.json", + "docs/security/public-gateway-post-incident-readback-plan.snapshot.json", + "docs/security/high-value-config-control-coverage.snapshot.json" + ], + "source_statuses": { + "high_value_config": "coverage_matrix_ready", + "public_gateway": "repo_only_preflight_contract_ready", + "public_gateway_post_incident": "post_incident_readback_plan_ready_no_runtime_action" + }, + "status": "blocked_waiting_owner_evidence" + }, + { + "action_buttons_allowed": false, + "label": "SSH / firewall / port / network policy baseline", + "lane_id": "ssh_firewall_ports", + "next_gate": "before_after_state_actor_impact_rollback_packet", + "not_authorization": true, + "owner_response_accepted": false, + "owner_response_received": false, + "priority": "P0", + "redacted_evidence_accepted": false, + "redacted_evidence_received": false, + "required_evidence": [ + { + "accepted": false, + "evidence_id": "before_state_ref" + }, + { + "accepted": false, + "evidence_id": "after_state_ref" + }, + { + "accepted": false, + "evidence_id": "actor_attribution_ref" + }, + { + "accepted": false, + "evidence_id": "service_health_impact_and_rollback_ref" + } + ], + "runtime_authorized": false, + "source_snapshot_refs": [ + "docs/security/ssh-network-post-incident-readback-plan.snapshot.json", + "docs/security/port-firewall-change-evidence-acceptance.snapshot.json", + "docs/security/external-host-intrusion-prevention-control.snapshot.json" + ], + "source_statuses": { + "external_host": "external_host_intrusion_prevention_control_ready_no_runtime_action", + "port_firewall": "change_evidence_acceptance_ready_no_runtime_action", + "ssh_network_post_incident": "post_incident_readback_plan_ready_no_runtime_action" + }, + "status": "blocked_waiting_owner_evidence" + }, + { + "action_buttons_allowed": false, + "label": "Docker / systemd / process / port binding", + "lane_id": "host_runtime_services", + "next_gate": "host_runtime_forensic_service_recovery_packet", + "not_authorization": true, + "owner_response_accepted": false, + "owner_response_received": false, + "priority": "P0", + "redacted_evidence_accepted": false, + "redacted_evidence_received": false, + "required_evidence": [ + { + "accepted": false, + "evidence_id": "docker_daemon_state_ref" + }, + { + "accepted": false, + "evidence_id": "systemd_unit_state_ref" + }, + { + "accepted": false, + "evidence_id": "process_port_binding_ref" + }, + { + "accepted": false, + "evidence_id": "dependency_and_postcheck_ref" + } + ], + "runtime_authorized": false, + "source_snapshot_refs": [ + "docs/security/host-service-post-incident-readback-plan.snapshot.json", + "docs/security/external-host-intrusion-prevention-control.snapshot.json" + ], + "source_statuses": { + "external_host": "external_host_intrusion_prevention_control_ready_no_runtime_action", + "host_service_post_incident": "post_incident_readback_plan_ready_no_runtime_action" + }, + "status": "blocked_waiting_owner_evidence" + }, + { + "action_buttons_allowed": false, + "label": "監控告警可讀性、收件與 no-false-green", + "lane_id": "monitoring_alert_receipt", + "next_gate": "alert_receipt_noise_budget_readable_message_contract", + "not_authorization": true, + "owner_response_accepted": false, + "owner_response_received": false, + "priority": "P0", + "redacted_evidence_accepted": false, + "redacted_evidence_received": false, + "required_evidence": [ + { + "accepted": false, + "evidence_id": "alert_route_receipt_ref" + }, + { + "accepted": false, + "evidence_id": "message_contract_readability_ref" + }, + { + "accepted": false, + "evidence_id": "dedupe_noise_budget_ref" + }, + { + "accepted": false, + "evidence_id": "post_change_monitoring_window_ref" + } + ], + "runtime_authorized": false, + "source_snapshot_refs": [ + "docs/security/monitoring-post-incident-readback-plan.snapshot.json", + "docs/security/soc-siem-kali-wazuh-integration-control.snapshot.json" + ], + "source_statuses": { + "monitoring_post_incident": "post_incident_readback_plan_ready_no_runtime_action", + "soc_integration": "soc_siem_kali_wazuh_integration_control_ready_no_runtime_action" + }, + "status": "blocked_waiting_owner_evidence" + }, + { + "action_buttons_allowed": false, + "label": "SOC / Kali / Wazuh 事件 case 化", + "lane_id": "soc_kali_wazuh_case", + "next_gate": "incident_case_owner_severity_confidence_chain_of_custody_packet", + "not_authorization": true, + "owner_response_accepted": false, + "owner_response_received": false, + "priority": "P0", + "redacted_evidence_accepted": false, + "redacted_evidence_received": false, + "required_evidence": [ + { + "accepted": false, + "evidence_id": "incident_case_ref" + }, + { + "accepted": false, + "evidence_id": "severity_confidence_mapping_ref" + }, + { + "accepted": false, + "evidence_id": "kali_scope_and_finding_envelope_ref" + }, + { + "accepted": false, + "evidence_id": "chain_of_custody_ref" + } + ], + "runtime_authorized": false, + "source_snapshot_refs": [ + "docs/security/soc-siem-kali-wazuh-integration-control.snapshot.json", + "docs/security/wazuh-iwooos-intrusion-readback-plan.snapshot.json" + ], + "source_statuses": { + "soc_integration": "soc_siem_kali_wazuh_integration_control_ready_no_runtime_action", + "wazuh_intrusion": "wazuh_intrusion_readback_plan_ready_no_runtime_action" + }, + "status": "blocked_waiting_owner_evidence" + }, + { + "action_buttons_allowed": false, + "label": "跨專案 freeze、runtime 邊界與防衝突", + "lane_id": "cross_project_freeze_runtime_boundary", + "next_gate": "cross_project_sync_runtime_authorization_owner_packet", + "not_authorization": true, + "owner_response_accepted": false, + "owner_response_received": false, + "priority": "P0", + "redacted_evidence_accepted": false, + "redacted_evidence_received": false, + "required_evidence": [ + { + "accepted": false, + "evidence_id": "cross_project_sync_ref" + }, + { + "accepted": false, + "evidence_id": "maintenance_window_or_break_glass_ref" + }, + { + "accepted": false, + "evidence_id": "rollback_owner_ref" + }, + { + "accepted": false, + "evidence_id": "runtime_authorization_ref" + } + ], + "runtime_authorized": false, + "source_snapshot_refs": [ + "docs/security/external-host-intrusion-prevention-control.snapshot.json", + "docs/security/high-value-config-control-coverage.snapshot.json", + "docs/security/soc-siem-kali-wazuh-integration-control.snapshot.json" + ], + "source_statuses": { + "external_host": "external_host_intrusion_prevention_control_ready_no_runtime_action", + "high_value_config": "coverage_matrix_ready", + "soc_integration": "soc_siem_kali_wazuh_integration_control_ready_no_runtime_action" + }, + "status": "blocked_waiting_owner_evidence" + } + ], + "schema_version": "iwooos_p0_security_incident_convergence_gate_v1", + "source_snapshot_refs": { + "external_host": "docs/security/external-host-intrusion-prevention-control.snapshot.json", + "high_value_config": "docs/security/high-value-config-control-coverage.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", + "port_firewall": "docs/security/port-firewall-change-evidence-acceptance.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", + "soc_integration": "docs/security/soc-siem-kali-wazuh-integration-control.snapshot.json", + "ssh_network_post_incident": "docs/security/ssh-network-post-incident-readback-plan.snapshot.json", + "wazuh_coverage": "docs/security/wazuh-managed-host-coverage-gate.snapshot.json", + "wazuh_intrusion": "docs/security/wazuh-iwooos-intrusion-readback-plan.snapshot.json", + "wazuh_runtime": "docs/security/wazuh-agent-visibility-runtime-gate.snapshot.json" + }, + "status": "p0_security_incident_convergence_blocked_waiting_owner_evidence", + "summary": { + "action_button_count": 0, + "alert_route_accepted_count": 0, + "blocked_lane_count": 8, + "containment_decision_accepted_count": 0, + "dashboard_api_connection_ok_count": 0, + "dashboard_api_version_ok_count": 0, + "dashboard_index_pattern_ok_count": 3, + "direct_agent_active_observed_count": 2, + "expected_host_scope_count": 6, + "external_host_coverage_percent": 74, + "external_host_prevention_candidate_count": 14, + "firewall_change_authorized_count": 0, + "gateway_post_incident_readback_received_count": 0, + "high_value_config_average_coverage_percent": 73, + "high_value_config_category_count": 14, + "host_forensics_ref_received_count": 0, + "host_service_post_incident_readback_received_count": 0, + "host_write_authorized_count": 0, + "incident_case_accepted_count": 0, + "kali_active_scan_authorized_count": 0, + "manager_registry_accepted_count": 0, + "monitoring_post_incident_readback_received_count": 0, + "nginx_reload_authorized_count": 0, + "nginx_test_evidence_count": 0, + "owner_response_accepted_count": 0, + "owner_response_received_count": 0, + "p0_lane_count": 8, + "port_firewall_change_evidence_received_count": 0, + "public_gateway_live_conf_received_count": 0, + "public_gateway_rendered_diff_ready_count": 0, + "recovery_proof_accepted_count": 0, + "redacted_evidence_accepted_count": 0, + "redacted_evidence_received_count": 0, + "route_smoke_evidence_count": 0, + "runtime_gate_count": 0, + "source_side_rollup_ready_percent": 100, + "source_snapshot_count": 12, + "ssh_network_post_incident_readback_received_count": 0, + "wazuh_active_response_authorized_count": 0, + "wazuh_event_ref_received_count": 0 + } +} diff --git a/scripts/security/iwooos-p0-security-incident-convergence-gate.py b/scripts/security/iwooos-p0-security-incident-convergence-gate.py new file mode 100644 index 00000000..2565568a --- /dev/null +++ b/scripts/security/iwooos-p0-security-incident-convergence-gate.py @@ -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())
+ {t('subtitle')} +
+ {t(`summary.${item.key}.detail` as never)} +
+ {t(`items.${item.key}.body` as never)} +
+ {t('boundaryIntro')} +
+ {item} +