From ffeab51bc1077f6cab5367e08b45c958ee27b747 Mon Sep 17 00:00:00 2001 From: ogt Date: Thu, 25 Jun 2026 14:17:12 +0800 Subject: [PATCH] feat(iwooos): add Wazuh registry export preflight --- apps/web/messages/en.json | 18 +++- apps/web/messages/zh-TW.json | 18 +++- apps/web/src/app/[locale]/iwooos/page.tsx | 25 +++-- ...ENT-VISIBILITY-OWNER-EVIDENCE-PREFLIGHT.md | 55 ++++++++++- .../WAZUH-MANAGED-HOST-COVERAGE-GATE.md | 13 ++- ...ity-owner-evidence-preflight.snapshot.json | 53 ++++++++++- .../security-mirror-progress-guard.py | 8 ++ ...ent-visibility-owner-evidence-preflight.py | 92 +++++++++++++++++++ 8 files changed, 259 insertions(+), 23 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 2f97935a..61e70931 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -19130,15 +19130,19 @@ "checkLabel": "檢核", "stateLabel": "狀態", "boundaryTitle": "Owner evidence 收件邊界", - "boundaryIntro": "以下鍵值固定:收件格式已準備好,但已收件、已接受與執行閘門仍為 0;任何原始紀錄、未脫敏截圖、內網位址、代理原名或機密都必須拒收或隔離。", + "boundaryIntro": "以下鍵值固定:收件格式已準備好,且新增 6 個公開節點別名與逐主機匯出矩陣要求;registry export、已收件、已接受與執行閘門仍為 0。任何原始紀錄、未脫敏截圖、內網位址、代理原名或機密都必須拒收或隔離。", "summary": { "fields": { "label": "必要欄位", - "detail": "負責人、決策、影響範圍、代理計數、時間窗與後檢計畫。" + "detail": "負責人、決策、收集方式、代理計數、逐主機矩陣與後檢計畫。" + }, + "aliases": { + "label": "節點別名", + "detail": "用 6 個公開別名覆蓋應納管範圍,不顯示主機原名。" }, "checks": { "label": "審查檢查", - "detail": "確認計數、時間窗、健康參照、脫敏與回滾責任。" + "detail": "確認計數、別名覆蓋、矩陣欄位、脫敏與回滾責任。" }, "received": { "label": "已收件", @@ -19150,10 +19154,18 @@ } }, "items": { + "scopeAliases": { + "title": "應納管範圍改用公開別名", + "body": "所有主機只用公開節點別名交叉檢查;前台不得顯示內網位址、主機原名或代理原名。" + }, "registryCounts": { "title": "代理計數必須可交叉檢查", "body": "需要總數、在線、離線、從未連線;總數不得小於各分類加總。" }, + "perHostMatrix": { + "title": "逐主機矩陣必須補齊", + "body": "每列需包含節點別名、範圍角色、registry presence、狀態桶、最後連線狀態、群組 ref、agent ref、缺口原因與證據 ref。" + }, "timeWindow": { "title": "時間窗要覆蓋事故觀察區間", "body": "需要清單收集時間與最後連線起訖時間;不能只用單張畫面判定恢復。" diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 2f97935a..61e70931 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -19130,15 +19130,19 @@ "checkLabel": "檢核", "stateLabel": "狀態", "boundaryTitle": "Owner evidence 收件邊界", - "boundaryIntro": "以下鍵值固定:收件格式已準備好,但已收件、已接受與執行閘門仍為 0;任何原始紀錄、未脫敏截圖、內網位址、代理原名或機密都必須拒收或隔離。", + "boundaryIntro": "以下鍵值固定:收件格式已準備好,且新增 6 個公開節點別名與逐主機匯出矩陣要求;registry export、已收件、已接受與執行閘門仍為 0。任何原始紀錄、未脫敏截圖、內網位址、代理原名或機密都必須拒收或隔離。", "summary": { "fields": { "label": "必要欄位", - "detail": "負責人、決策、影響範圍、代理計數、時間窗與後檢計畫。" + "detail": "負責人、決策、收集方式、代理計數、逐主機矩陣與後檢計畫。" + }, + "aliases": { + "label": "節點別名", + "detail": "用 6 個公開別名覆蓋應納管範圍,不顯示主機原名。" }, "checks": { "label": "審查檢查", - "detail": "確認計數、時間窗、健康參照、脫敏與回滾責任。" + "detail": "確認計數、別名覆蓋、矩陣欄位、脫敏與回滾責任。" }, "received": { "label": "已收件", @@ -19150,10 +19154,18 @@ } }, "items": { + "scopeAliases": { + "title": "應納管範圍改用公開別名", + "body": "所有主機只用公開節點別名交叉檢查;前台不得顯示內網位址、主機原名或代理原名。" + }, "registryCounts": { "title": "代理計數必須可交叉檢查", "body": "需要總數、在線、離線、從未連線;總數不得小於各分類加總。" }, + "perHostMatrix": { + "title": "逐主機矩陣必須補齊", + "body": "每列需包含節點別名、範圍角色、registry presence、狀態桶、最後連線狀態、群組 ref、agent ref、缺口原因與證據 ref。" + }, "timeWindow": { "title": "時間窗要覆蓋事故觀察區間", "body": "需要清單收集時間與最後連線起訖時間;不能只用單張畫面判定恢復。" diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index 00bb5220..e0d98ee3 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -2309,27 +2309,34 @@ const wazuhLiveMetadataEnvGateBoundaries = [ ] as const const wazuhOwnerEvidencePreflightSummary = [ - { key: 'fields', value: '18', icon: ClipboardCheck, tone: 'steady' }, - { key: 'checks', value: '7', icon: ListChecks, tone: 'steady' }, + { key: 'fields', value: '23', icon: ClipboardCheck, tone: 'steady' }, + { key: 'aliases', value: '6', icon: Server, tone: 'warn' }, + { key: 'checks', value: '10', icon: ListChecks, tone: 'steady' }, { key: 'received', value: '0', icon: FileWarning, tone: 'locked' }, { key: 'accepted', value: '0', icon: Lock, tone: 'locked' }, ] as const const wazuhOwnerEvidencePreflightItems: WazuhOwnerEvidencePreflightItem[] = [ + { key: 'scopeAliases', check: 'EV-0', state: '6 個別名', icon: Server, tone: 'warn' }, { key: 'registryCounts', check: 'EV-1', state: '待脫敏計數', icon: Server, tone: 'warn' }, - { key: 'timeWindow', check: 'EV-2', state: '待時間窗', icon: Clock3, tone: 'warn' }, - { key: 'healthRefs', check: 'EV-3', state: '待健康參照', icon: Activity, tone: 'warn' }, - { key: 'redaction', check: 'EV-4', state: '拒收敏感內容', icon: FileWarning, tone: 'locked' }, - { key: 'ownerDecision', check: 'EV-5', state: '待負責人', icon: ClipboardCheck, tone: 'warn' }, - { key: 'runtimeBoundary', check: 'EV-6', state: '不開執行', icon: Lock, tone: 'locked' }, + { key: 'perHostMatrix', check: 'EV-2', state: '9 欄逐主機', icon: ListChecks, tone: 'warn' }, + { key: 'timeWindow', check: 'EV-3', state: '待時間窗', icon: Clock3, tone: 'warn' }, + { key: 'healthRefs', check: 'EV-4', state: '待健康參照', icon: Activity, tone: 'warn' }, + { key: 'redaction', check: 'EV-5', state: '拒收敏感內容', icon: FileWarning, tone: 'locked' }, + { key: 'ownerDecision', check: 'EV-6', state: '待負責人', icon: ClipboardCheck, tone: 'warn' }, + { key: 'runtimeBoundary', check: 'EV-7', state: '不開執行', icon: Lock, tone: 'locked' }, ] as const const wazuhOwnerEvidencePreflightBoundaries = [ 'wazuh_agent_visibility_owner_evidence_preflight_visible=true', - 'wazuh_agent_visibility_owner_evidence_required_field_count=18', - 'wazuh_agent_visibility_owner_evidence_reviewer_check_count=7', + 'wazuh_agent_visibility_owner_evidence_required_field_count=23', + 'wazuh_agent_visibility_owner_evidence_reviewer_check_count=10', + 'wazuh_agent_visibility_owner_evidence_expected_scope_alias_count=6', + 'wazuh_agent_visibility_owner_evidence_per_host_required_field_count=9', 'wazuh_agent_visibility_owner_evidence_outcome_lane_count=5', 'wazuh_agent_visibility_owner_evidence_forbidden_payload_count=18', + 'wazuh_agent_visibility_owner_evidence_registry_export_received_count=0', + 'wazuh_agent_visibility_owner_evidence_registry_export_accepted_count=0', 'wazuh_agent_visibility_owner_evidence_received_count=0', 'wazuh_agent_visibility_owner_evidence_accepted_count=0', 'wazuh_agent_visibility_owner_evidence_runtime_gate_count=0', diff --git a/docs/security/WAZUH-AGENT-VISIBILITY-OWNER-EVIDENCE-PREFLIGHT.md b/docs/security/WAZUH-AGENT-VISIBILITY-OWNER-EVIDENCE-PREFLIGHT.md index d12a13a6..355b737d 100644 --- a/docs/security/WAZUH-AGENT-VISIBILITY-OWNER-EVIDENCE-PREFLIGHT.md +++ b/docs/security/WAZUH-AGENT-VISIBILITY-OWNER-EVIDENCE-PREFLIGHT.md @@ -15,6 +15,7 @@ Wazuh 用戶端消失事故不能用 Dashboard 畫面、代理服務在線、TCP - `decision` - `decision_reason` - `affected_scope` +- `collection_method` - `agent_total` - `agent_active` - `agent_disconnected` @@ -22,6 +23,10 @@ Wazuh 用戶端消失事故不能用 Dashboard 畫面、代理服務在線、TCP - `last_seen_window_start` - `last_seen_window_end` - `registry_collected_at` +- `registry_export_scope_aliases` +- `per_host_registry_matrix` +- `registry_gap_reason_by_alias` +- `registry_export_summary_ref` - `manager_health_ref` - `dashboard_api_status_ref` - `redacted_evidence_refs` @@ -33,6 +38,9 @@ Wazuh 用戶端消失事故不能用 Dashboard 畫面、代理服務在線、TCP - 欄位齊全且皆為脫敏 metadata。 - `agent_total` 不可小於 `agent_active + agent_disconnected + agent_never_connected`。 +- `collection_method` 只能是既有唯讀 API 或 manager 端脫敏 CLI 匯出,不可要求 secret 明文。 +- `registry_export_scope_aliases` 必須完整覆蓋 6 個公開節點別名。 +- `per_host_registry_matrix` 每列只能使用公開別名,不得包含內網位址、agent 原名或 raw payload。 - `last_seen` 時間窗需能覆蓋事故觀察區間。 - manager health ref 與 dashboard API status ref 不可互相替代。 - redacted evidence refs 不得包含 raw payload、截圖原文或主機完整輸出。 @@ -48,12 +56,53 @@ Wazuh 用戶端消失事故不能用 Dashboard 畫面、代理服務在線、TCP - authorization header、token、basic auth、password、cookie、private key、client key。 - 夾帶 active response、host write、firewall change、Nginx reload 或其他 runtime 操作要求。 +## Manager registry 脫敏匯出契約 + +Wazuh manager registry 才是判定「所有用戶端是否仍在」的主要來源。匯出時只能使用下列公開節點別名,不得放內網位址、主機原名、agent 原名或完整 CLI / API 輸出。 + +| 節點別名 | 用途 | +|---|---| +| `managed_core_node_a` | 核心服務節點 | +| `managed_core_node_b` | 資料服務節點 | +| `managed_dev_node_a` | 開發工作節點 | +| `managed_dev_node_b` | 開發工作節點 | +| `managed_control_node_a` | 控制平面節點 | +| `managed_control_node_b` | 控制平面節點 | + +允許的收集方式: + +- `wazuh_api_readonly_redacted_counts` +- `manager_agent_control_redacted_export` + +逐主機矩陣每列必填欄位: + +- `node_alias` +- `scope_role` +- `registry_presence` +- `agent_status_bucket` +- `last_seen_state` +- `manager_group_ref` +- `agent_id_redacted_ref` +- `gap_reason` +- `redacted_evidence_ref` + +脫敏要求: + +- 只允許公開節點別名,不允許內網位址、主機原名或 agent 原名。 +- agent id 僅能用不可逆 evidence ref,不得放完整值、雜湊、前後綴或 client key。 +- 每個缺口必須有 gap reason,不得以 Dashboard 空白或口頭說明補成綠燈。 +- 只收計數、狀態桶、時間窗與證據 ref,不收 raw API payload、完整 CLI output 或截圖原文。 + ## 現況計數 | 項目 | 計數 | |---|---:| -| 必要欄位 | `18` | -| Reviewer 檢查 | `7` | +| 必要欄位 | `23` | +| Reviewer 檢查 | `10` | +| 公開節點別名 | `6` | +| 逐主機必填欄位 | `9` | +| Registry export received | `0` | +| Registry export accepted | `0` | | Outcome lanes | `5` | | Forbidden payloads | `18` | | Owner evidence received | `0` | @@ -69,7 +118,7 @@ python3 scripts/security/wazuh-agent-visibility-owner-evidence-preflight.py --ro 預期: ```text -WAZUH_AGENT_VISIBILITY_OWNER_EVIDENCE_PREFLIGHT_OK fields=18 checks=7 received=0 accepted=0 runtime_gate=0 +WAZUH_AGENT_VISIBILITY_OWNER_EVIDENCE_PREFLIGHT_OK fields=23 checks=10 aliases=6 export_received=0 received=0 accepted=0 runtime_gate=0 ``` ## 邊界 diff --git a/docs/security/WAZUH-MANAGED-HOST-COVERAGE-GATE.md b/docs/security/WAZUH-MANAGED-HOST-COVERAGE-GATE.md index 4a28dd38..b6378228 100644 --- a/docs/security/WAZUH-MANAGED-HOST-COVERAGE-GATE.md +++ b/docs/security/WAZUH-MANAGED-HOST-COVERAGE-GATE.md @@ -26,17 +26,26 @@ ## 綠燈前必備證據 1. Manager registry agent counts:總數、在線、離線、從未連線、最後連線時間窗。 -2. 逐主機 agent scope matrix:只用公開別名,不列內網位址、agent 原名或 raw payload。 +2. 逐主機 agent scope matrix:只用公開別名,不列內網位址、agent 原名或 raw payload;欄位需符合 `WAZUH-AGENT-VISIBILITY-OWNER-EVIDENCE-PREFLIGHT.md` 的 manager registry 脫敏匯出契約。 3. Dashboard API / RBAC / TLS 修復讀回:stored API、rate-limit、run_as、TLS trust 都要有脫敏參照。 4. 唯讀認證中繼資料:只收 secret name、來源、owner、rotation / rollback owner,不收明文值、雜湊或片段。 5. Owner response:owner role / team、decision、decision reason、affected scope、redacted evidence refs、followup owner、rollback owner。 6. IwoooS 啟用後讀回:不得回傳 raw log、agent 原名、內網位址、secret 或 host output。 +## Manager registry export 最小格式 + +Wazuh agents 是否真的恢復,必須以 manager registry 脫敏匯出交叉檢查。IwoooS 目前接受下列最小格式: + +- 範圍:`managed_core_node_a`、`managed_core_node_b`、`managed_dev_node_a`、`managed_dev_node_b`、`managed_control_node_a`、`managed_control_node_b`。 +- 收集方式:`wazuh_api_readonly_redacted_counts` 或 `manager_agent_control_redacted_export`。 +- 逐主機欄位:`node_alias`、`scope_role`、`registry_presence`、`agent_status_bucket`、`last_seen_state`、`manager_group_ref`、`agent_id_redacted_ref`、`gap_reason`、`redacted_evidence_ref`。 +- 驗收邊界:`registry_export_received_count=0`、`registry_export_accepted_count=0` 時,不得宣稱所有主機已納管。 + ## 後續優先順序 | 優先 | 工作 | 完成度 | | --- | --- | ---: | -| P0-A | Manager registry 只讀計數與逐主機矩陣 | 0% | +| P0-A | Manager registry 只讀計數與逐主機矩陣脫敏匯出 | 0% | | P0-B | Dashboard stored API / RBAC / rate-limit / TLS 修復 | 0% | | P0-C | 直接無 transport 節點的合法只讀後檢 | 0% | | P0-D | SSH 受阻節點 owner export 或只讀 access | 0% | diff --git a/docs/security/wazuh-agent-visibility-owner-evidence-preflight.snapshot.json b/docs/security/wazuh-agent-visibility-owner-evidence-preflight.snapshot.json index ceddeefa..c5156146 100644 --- a/docs/security/wazuh-agent-visibility-owner-evidence-preflight.snapshot.json +++ b/docs/security/wazuh-agent-visibility-owner-evidence-preflight.snapshot.json @@ -21,6 +21,7 @@ "bearer_token", "basic_auth", "password", + "token", "cookie", "private_key", "client_keys", @@ -33,6 +34,7 @@ "operator_interpretation": [ "這是 Wazuh agent registry 脫敏證據收件前的預檢,不代表已收到或已接受 owner evidence。", "agent service active、TCP 連線存在、Dashboard 可見或口頭宣稱都不可替代 manager registry counts。", + "逐主機 registry export 必須使用固定公開節點別名與狀態桶,不能把 agent 原名或內網識別資訊帶到前台。", "若 evidence 夾帶 raw log、未脫敏截圖、內網位址、agent 原名或 secret,必須隔離,不得渲染到前台。", "任何 active response、host write、firewall、Nginx、Docker、K8s 或 secret 變更都要切獨立人工批准。" ], @@ -43,12 +45,46 @@ "reject_runtime_action_request", "ready_for_reviewer_validation" ], + "registry_export_contract": { + "allowed_collection_methods": [ + "wazuh_api_readonly_redacted_counts", + "manager_agent_control_redacted_export" + ], + "expected_scope_aliases": [ + "managed_core_node_a", + "managed_core_node_b", + "managed_dev_node_a", + "managed_dev_node_b", + "managed_control_node_a", + "managed_control_node_b" + ], + "per_host_required_fields": [ + "node_alias", + "scope_role", + "registry_presence", + "agent_status_bucket", + "last_seen_state", + "manager_group_ref", + "agent_id_redacted_ref", + "gap_reason", + "redacted_evidence_ref" + ], + "redaction_requirements": [ + "只允許公開節點別名,不允許內網位址、主機原名或 agent 原名。", + "agent id 僅能用不可逆 evidence ref,不得放完整值、雜湊、前後綴或 client key。", + "每個缺口必須有 gap reason,不得以 Dashboard 空白或口頭說明補成綠燈。", + "只收計數、狀態桶、時間窗與證據 ref,不收 raw API payload、完整 CLI output 或截圖原文。" + ], + "registry_export_accepted_count": 0, + "registry_export_received_count": 0 + }, "required_fields": [ "owner_role", "team", "decision", "decision_reason", "affected_scope", + "collection_method", "agent_total", "agent_active", "agent_disconnected", @@ -56,6 +92,10 @@ "last_seen_window_start", "last_seen_window_end", "registry_collected_at", + "registry_export_scope_aliases", + "per_host_registry_matrix", + "registry_gap_reason_by_alias", + "registry_export_summary_ref", "manager_health_ref", "dashboard_api_status_ref", "redacted_evidence_refs", @@ -66,6 +106,9 @@ "reviewer_checks": [ "欄位齊全且皆為脫敏 metadata。", "agent_total 不可小於 active + disconnected + never_connected。", + "collection_method 只能是既有唯讀 API 或 manager 端脫敏 CLI 匯出,不可要求 secret 明文。", + "registry_export_scope_aliases 必須完整覆蓋 6 個公開節點別名。", + "per_host_registry_matrix 每列只能使用公開別名,不得包含內網位址、agent 原名或 raw payload。", "last_seen 時間窗需能覆蓋事故觀察區間。", "manager health ref 與 dashboard API status ref 不可互相替代。", "redacted evidence refs 不得包含 raw payload、截圖原文或主機完整輸出。", @@ -77,15 +120,19 @@ "status": "owner_evidence_preflight_ready_no_runtime_action", "summary": { "active_response_authorized_count": 0, - "forbidden_payload_count": 17, + "expected_scope_alias_count": 6, + "forbidden_payload_count": 18, "host_write_authorized_count": 0, "outcome_lane_count": 5, "owner_evidence_accepted_count": 0, "owner_evidence_quarantined_count": 0, "owner_evidence_received_count": 0, "owner_evidence_rejected_count": 0, - "required_field_count": 18, - "reviewer_check_count": 7, + "per_host_required_field_count": 9, + "registry_export_accepted_count": 0, + "registry_export_received_count": 0, + "required_field_count": 23, + "reviewer_check_count": 10, "runtime_gate_count": 0, "secret_value_collection_allowed_count": 0 } diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 00bcbf24..fee145c1 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -29476,6 +29476,14 @@ def validate(root: Path) -> None: ] ) for expected in [ + "iwooos-wazuh-owner-evidence-preflight-board", + "wazuhOwnerEvidencePreflight", + "wazuh_agent_visibility_owner_evidence_required_field_count=23", + "wazuh_agent_visibility_owner_evidence_expected_scope_alias_count=6", + "wazuh_agent_visibility_owner_evidence_per_host_required_field_count=9", + "wazuh_agent_visibility_owner_evidence_registry_export_received_count=0", + "wazuh_agent_visibility_owner_evidence_registry_export_accepted_count=0", + "wazuh_agent_visibility_owner_evidence_runtime_gate_count=0", "iwooos-wazuh-managed-host-coverage-board", "wazuhManagedHostCoverage", "wazuh_managed_host_coverage_manager_registry_accepted_count=0", diff --git a/scripts/security/wazuh-agent-visibility-owner-evidence-preflight.py b/scripts/security/wazuh-agent-visibility-owner-evidence-preflight.py index a9e4b373..3bebfbfe 100644 --- a/scripts/security/wazuh-agent-visibility-owner-evidence-preflight.py +++ b/scripts/security/wazuh-agent-visibility-owner-evidence-preflight.py @@ -24,6 +24,7 @@ REQUIRED_FIELDS = [ "decision", "decision_reason", "affected_scope", + "collection_method", "agent_total", "agent_active", "agent_disconnected", @@ -31,6 +32,10 @@ REQUIRED_FIELDS = [ "last_seen_window_start", "last_seen_window_end", "registry_collected_at", + "registry_export_scope_aliases", + "per_host_registry_matrix", + "registry_gap_reason_by_alias", + "registry_export_summary_ref", "manager_health_ref", "dashboard_api_status_ref", "redacted_evidence_refs", @@ -42,6 +47,9 @@ REQUIRED_FIELDS = [ REVIEWER_CHECKS = [ "欄位齊全且皆為脫敏 metadata。", "agent_total 不可小於 active + disconnected + never_connected。", + "collection_method 只能是既有唯讀 API 或 manager 端脫敏 CLI 匯出,不可要求 secret 明文。", + "registry_export_scope_aliases 必須完整覆蓋 6 個公開節點別名。", + "per_host_registry_matrix 每列只能使用公開別名,不得包含內網位址、agent 原名或 raw payload。", "last_seen 時間窗需能覆蓋事故觀察區間。", "manager health ref 與 dashboard API status ref 不可互相替代。", "redacted evidence refs 不得包含 raw payload、截圖原文或主機完整輸出。", @@ -68,6 +76,7 @@ FORBIDDEN_PAYLOADS = [ "bearer_token", "basic_auth", "password", + "token", "cookie", "private_key", "client_keys", @@ -89,6 +98,39 @@ FORBIDDEN_TEXT_PATTERNS = [ re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"), ] +REGISTRY_EXPORT_SCOPE_ALIASES = [ + "managed_core_node_a", + "managed_core_node_b", + "managed_dev_node_a", + "managed_dev_node_b", + "managed_control_node_a", + "managed_control_node_b", +] + +REGISTRY_EXPORT_ALLOWED_COLLECTION_METHODS = [ + "wazuh_api_readonly_redacted_counts", + "manager_agent_control_redacted_export", +] + +PER_HOST_REGISTRY_REQUIRED_FIELDS = [ + "node_alias", + "scope_role", + "registry_presence", + "agent_status_bucket", + "last_seen_state", + "manager_group_ref", + "agent_id_redacted_ref", + "gap_reason", + "redacted_evidence_ref", +] + +REGISTRY_EXPORT_REDACTION_REQUIREMENTS = [ + "只允許公開節點別名,不允許內網位址、主機原名或 agent 原名。", + "agent id 僅能用不可逆 evidence ref,不得放完整值、雜湊、前後綴或 client key。", + "每個缺口必須有 gap reason,不得以 Dashboard 空白或口頭說明補成綠燈。", + "只收計數、狀態桶、時間窗與證據 ref,不收 raw API payload、完整 CLI output 或截圖原文。", +] + def load_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8")) @@ -143,11 +185,23 @@ def build_snapshot() -> dict[str, Any]: "reviewer_checks": REVIEWER_CHECKS, "outcome_lanes": OUTCOME_LANES, "forbidden_payloads": FORBIDDEN_PAYLOADS, + "registry_export_contract": { + "expected_scope_aliases": REGISTRY_EXPORT_SCOPE_ALIASES, + "allowed_collection_methods": REGISTRY_EXPORT_ALLOWED_COLLECTION_METHODS, + "per_host_required_fields": PER_HOST_REGISTRY_REQUIRED_FIELDS, + "redaction_requirements": REGISTRY_EXPORT_REDACTION_REQUIREMENTS, + "registry_export_received_count": 0, + "registry_export_accepted_count": 0, + }, "summary": { "required_field_count": len(REQUIRED_FIELDS), "reviewer_check_count": len(REVIEWER_CHECKS), "outcome_lane_count": len(OUTCOME_LANES), "forbidden_payload_count": len(FORBIDDEN_PAYLOADS), + "expected_scope_alias_count": len(REGISTRY_EXPORT_SCOPE_ALIASES), + "per_host_required_field_count": len(PER_HOST_REGISTRY_REQUIRED_FIELDS), + "registry_export_received_count": 0, + "registry_export_accepted_count": 0, "owner_evidence_received_count": 0, "owner_evidence_accepted_count": 0, "owner_evidence_rejected_count": 0, @@ -171,6 +225,7 @@ def build_snapshot() -> dict[str, Any]: "operator_interpretation": [ "這是 Wazuh agent registry 脫敏證據收件前的預檢,不代表已收到或已接受 owner evidence。", "agent service active、TCP 連線存在、Dashboard 可見或口頭宣稱都不可替代 manager registry counts。", + "逐主機 registry export 必須使用固定公開節點別名與狀態桶,不能把 agent 原名或內網識別資訊帶到前台。", "若 evidence 夾帶 raw log、未脫敏截圖、內網位址、agent 原名或 secret,必須隔離,不得渲染到前台。", "任何 active response、host write、firewall、Nginx、Docker、K8s 或 secret 變更都要切獨立人工批准。", ], @@ -194,13 +249,48 @@ def validate(root: Path) -> None: assert_equal("reviewer_checks", snapshot.get("reviewer_checks"), REVIEWER_CHECKS) assert_equal("outcome_lanes", snapshot.get("outcome_lanes"), OUTCOME_LANES) assert_equal("forbidden_payloads", snapshot.get("forbidden_payloads"), FORBIDDEN_PAYLOADS) + contract = snapshot.get("registry_export_contract", {}) + assert_equal( + "registry_export_contract.expected_scope_aliases", + contract.get("expected_scope_aliases"), + REGISTRY_EXPORT_SCOPE_ALIASES, + ) + assert_equal( + "registry_export_contract.allowed_collection_methods", + contract.get("allowed_collection_methods"), + REGISTRY_EXPORT_ALLOWED_COLLECTION_METHODS, + ) + assert_equal( + "registry_export_contract.per_host_required_fields", + contract.get("per_host_required_fields"), + PER_HOST_REGISTRY_REQUIRED_FIELDS, + ) + assert_equal( + "registry_export_contract.redaction_requirements", + contract.get("redaction_requirements"), + REGISTRY_EXPORT_REDACTION_REQUIREMENTS, + ) + assert_zero("registry_export_contract.registry_export_received_count", contract.get("registry_export_received_count")) + assert_zero("registry_export_contract.registry_export_accepted_count", contract.get("registry_export_accepted_count")) summary = snapshot.get("summary", {}) assert_equal("summary.required_field_count", summary.get("required_field_count"), len(REQUIRED_FIELDS)) assert_equal("summary.reviewer_check_count", summary.get("reviewer_check_count"), len(REVIEWER_CHECKS)) assert_equal("summary.outcome_lane_count", summary.get("outcome_lane_count"), len(OUTCOME_LANES)) assert_equal("summary.forbidden_payload_count", summary.get("forbidden_payload_count"), len(FORBIDDEN_PAYLOADS)) + assert_equal( + "summary.expected_scope_alias_count", + summary.get("expected_scope_alias_count"), + len(REGISTRY_EXPORT_SCOPE_ALIASES), + ) + assert_equal( + "summary.per_host_required_field_count", + summary.get("per_host_required_field_count"), + len(PER_HOST_REGISTRY_REQUIRED_FIELDS), + ) for key in [ + "registry_export_received_count", + "registry_export_accepted_count", "owner_evidence_received_count", "owner_evidence_accepted_count", "owner_evidence_rejected_count", @@ -245,6 +335,8 @@ def main() -> None: "WAZUH_AGENT_VISIBILITY_OWNER_EVIDENCE_PREFLIGHT_OK " f"fields={summary['required_field_count']} " f"checks={summary['reviewer_check_count']} " + f"aliases={summary['expected_scope_alias_count']} " + f"export_received={summary['registry_export_received_count']} " f"received={summary['owner_evidence_received_count']} " f"accepted={summary['owner_evidence_accepted_count']} " f"runtime_gate={summary['runtime_gate_count']}"