diff --git a/apps/api/src/api/v1/iwooos.py b/apps/api/src/api/v1/iwooos.py index 14831566..06618c99 100644 --- a/apps/api/src/api/v1/iwooos.py +++ b/apps/api/src/api/v1/iwooos.py @@ -26,6 +26,9 @@ from src.services.iwooos_wazuh_readonly_status import ( from src.services.iwooos_wazuh_live_metadata_gate import ( load_latest_iwooos_wazuh_live_metadata_gate, ) +from src.services.iwooos_wazuh_owner_evidence_preflight import ( + load_latest_iwooos_wazuh_owner_evidence_preflight, +) from src.services.public_redaction import redact_public_lan_topology @@ -79,6 +82,34 @@ async def get_iwooos_wazuh_live_metadata_gate() -> dict[str, Any]: ) from exc +@router.get( + "/api/v1/iwooos/wazuh-owner-evidence-preflight", + response_model=dict[str, Any], + summary="取得 Wazuh 負責人證據收件預檢讀回", + description=( + "讀取已提交的 Wazuh 代理清單負責人證據收件預檢,回傳公開安全的欄位數、" + "審查檢查、分流、拒收內容計數與 0 / false 邊界。此端點不查 Wazuh、" + "不讀主機、不保存原始載荷、不收機密明文、不啟用主動回應、不改 Nginx / " + "Docker / K8s / firewall。" + ), +) +async def get_iwooos_wazuh_owner_evidence_preflight() -> dict[str, Any]: + """回傳 Wazuh manager registry 負責人證據收件預檢只讀狀態。""" + try: + payload = await asyncio.to_thread(load_latest_iwooos_wazuh_owner_evidence_preflight) + return redact_public_lan_topology(payload) + except FileNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(exc), + ) from exc + except (json.JSONDecodeError, ValueError) as exc: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"IwoooS Wazuh 負責人證據預檢無效:{exc}", + ) from exc + + @router.get( "/api/v1/iwooos/runtime-security-readback", response_model=dict[str, Any], diff --git a/apps/api/src/services/iwooos_runtime_security_readback.py b/apps/api/src/services/iwooos_runtime_security_readback.py index ad6b9df3..4790bf56 100644 --- a/apps/api/src/services/iwooos_runtime_security_readback.py +++ b/apps/api/src/services/iwooos_runtime_security_readback.py @@ -21,6 +21,7 @@ _SNAPSHOT_FILES = { "wazuh_coverage": "wazuh-managed-host-coverage-gate.snapshot.json", "wazuh_runtime": "wazuh-agent-visibility-runtime-gate.snapshot.json", "wazuh_live_metadata_gate": "wazuh-readonly-live-metadata-env-gate.snapshot.json", + "wazuh_owner_evidence_preflight": "wazuh-agent-visibility-owner-evidence-preflight.snapshot.json", "kali_status": "kali-integration-status.snapshot.json", "soc_control": "soc-siem-kali-wazuh-integration-control.snapshot.json", "alert_readability": "telegram-alert-readability-guard.snapshot.json", @@ -33,6 +34,7 @@ _EXPECTED_SCHEMAS = { "wazuh_coverage": "wazuh_managed_host_coverage_gate_v1", "wazuh_runtime": "wazuh_agent_visibility_runtime_gate_v1", "wazuh_live_metadata_gate": "iwooos_wazuh_readonly_live_metadata_env_gate_v1", + "wazuh_owner_evidence_preflight": "wazuh_agent_visibility_owner_evidence_preflight_v1", "kali_status": "kali_integration_status_v1", "soc_control": "soc_siem_kali_wazuh_integration_control_v1", "alert_readability": "telegram_alert_readability_guard_v1", @@ -73,6 +75,7 @@ def load_latest_iwooos_runtime_security_readback( owner_gap_summary = _summary(snapshots["owner_gap"]) wazuh_summary = _summary(snapshots["wazuh_coverage"]) live_metadata_gate_summary = _summary(snapshots["wazuh_live_metadata_gate"]) + owner_evidence_preflight_summary = _summary(snapshots["wazuh_owner_evidence_preflight"]) soc_summary = _summary(snapshots["soc_control"]) alert_summary = _summary(snapshots["alert_readability"]) dispatch_summary = _summary(snapshots["owner_dispatch"]) @@ -95,7 +98,7 @@ def load_latest_iwooos_runtime_security_readback( "source_refs": source_refs, "summary": { "source_snapshot_count": len(source_refs), - "p0_lane_count": 8, + "p0_lane_count": 9, "control_plane_visibility_percent": _average_percent( soc_summary.get("coverage_percent_after_soc_integration_control"), intrusion_summary.get("coverage_percent_after_prevention_control"), @@ -139,6 +142,36 @@ def load_latest_iwooos_runtime_security_readback( "wazuh_live_metadata_gate_live_query_authorized_count": _int( live_metadata_gate_summary.get("wazuh_api_live_query_authorized_count") ), + "wazuh_owner_evidence_required_field_count": _int( + owner_evidence_preflight_summary.get("required_field_count") + ), + "wazuh_owner_evidence_reviewer_check_count": _int( + owner_evidence_preflight_summary.get("reviewer_check_count") + ), + "wazuh_owner_evidence_outcome_lane_count": _int( + owner_evidence_preflight_summary.get("outcome_lane_count") + ), + "wazuh_owner_evidence_forbidden_payload_count": _int( + owner_evidence_preflight_summary.get("forbidden_payload_count") + ), + "wazuh_owner_evidence_expected_alias_count": _int( + owner_evidence_preflight_summary.get("expected_scope_alias_count") + ), + "wazuh_owner_evidence_registry_export_received_count": _int( + owner_evidence_preflight_summary.get("registry_export_received_count") + ), + "wazuh_owner_evidence_registry_export_accepted_count": _int( + owner_evidence_preflight_summary.get("registry_export_accepted_count") + ), + "wazuh_owner_evidence_received_count": _int( + owner_evidence_preflight_summary.get("owner_evidence_received_count") + ), + "wazuh_owner_evidence_accepted_count": _int( + owner_evidence_preflight_summary.get("owner_evidence_accepted_count") + ), + "wazuh_owner_evidence_runtime_gate_count": _int( + owner_evidence_preflight_summary.get("runtime_gate_count") + ), "kali_active_scan_authorized_count": _int(soc_summary.get("kali_active_scan_authorized_count")), "kali_execute_authorized_count": _int(soc_summary.get("kali_execute_authorized_count")), "kali_finding_envelope_accepted_count": _int(soc_summary.get("kali_finding_envelope_accepted_count")), @@ -206,6 +239,30 @@ def load_latest_iwooos_runtime_security_readback( }, ["docs/security/wazuh-readonly-live-metadata-env-gate.snapshot.json"], ), + _lane( + "wazuh_owner_evidence_preflight", + snapshots["wazuh_owner_evidence_preflight"].get( + "status", + "owner_evidence_preflight_ready_no_runtime_action", + ), + 0, + "locked", + "補齊 manager registry 脫敏封包、逐主機矩陣、Dashboard API 狀態與負責人決策", + { + "required_fields": owner_evidence_preflight_summary.get("required_field_count", 0), + "reviewer_checks": owner_evidence_preflight_summary.get("reviewer_check_count", 0), + "outcome_lanes": owner_evidence_preflight_summary.get("outcome_lane_count", 0), + "forbidden_payloads": owner_evidence_preflight_summary.get("forbidden_payload_count", 0), + "owner_received": owner_evidence_preflight_summary.get("owner_evidence_received_count", 0), + "owner_accepted": owner_evidence_preflight_summary.get("owner_evidence_accepted_count", 0), + "registry_export_accepted": owner_evidence_preflight_summary.get( + "registry_export_accepted_count", + 0, + ), + "runtime_gate": owner_evidence_preflight_summary.get("runtime_gate_count", 0), + }, + ["docs/security/wazuh-agent-visibility-owner-evidence-preflight.snapshot.json"], + ), _lane( "wazuh_dashboard_api", "degraded_api_connection_not_green", @@ -303,6 +360,7 @@ def load_latest_iwooos_runtime_security_readback( "告警格式合約不代表通知已實發或已取得 receipt", "Wazuh 正式只讀路由 disabled 或退化時仍是 P0 紅燈", "Wazuh 即時中繼資料必須先通過負責人、機密中繼資料、唯讀範圍與啟用後讀回", + "Wazuh 負責人證據預檢 ready 不代表已收件、已接受或可啟用 active response", ], } @@ -421,6 +479,9 @@ def _require_runtime_boundaries(snapshots: dict[str, dict[str, Any]]) -> None: "secret_value_collection_allowed_count", "wazuh_api_live_query_authorized_count", "wazuh_active_response_authorized_count", + "active_response_authorized_count", + "registry_export_accepted_count", + "owner_evidence_accepted_count", "post_enable_readback_passed_count", ): if key in summary and _int(summary.get(key)) != 0: diff --git a/apps/api/src/services/iwooos_wazuh_owner_evidence_preflight.py b/apps/api/src/services/iwooos_wazuh_owner_evidence_preflight.py new file mode 100644 index 00000000..ba35bf2c --- /dev/null +++ b/apps/api/src/services/iwooos_wazuh_owner_evidence_preflight.py @@ -0,0 +1,264 @@ +""" +IwoooS Wazuh owner evidence preflight readback. + +This module only exposes committed, public-safe preflight metadata. It never +queries Wazuh, never reads secret values, and never authorizes active response, +host writes, scans, restarts, reloads, or gateway changes. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from src.services.snapshot_paths import default_security_dir + +_DEFAULT_SECURITY_DIR = default_security_dir(Path(__file__)) +_SNAPSHOT_FILE = "wazuh-agent-visibility-owner-evidence-preflight.snapshot.json" +_EXPECTED_SCHEMA = "wazuh_agent_visibility_owner_evidence_preflight_v1" + +_REQUIRED_FALSE_BOUNDARIES = { + "agent_identity_public_display_allowed", + "host_write_authorized", + "internal_ip_public_display_allowed", + "raw_wazuh_payload_storage_allowed", + "runtime_execution_authorized", + "secret_value_collection_allowed", + "wazuh_active_response_authorized", + "wazuh_api_live_query_authorized", +} + + +def load_latest_iwooos_wazuh_owner_evidence_preflight( + security_dir: Path | None = None, +) -> dict[str, Any]: + """Load the committed Wazuh owner evidence preflight as a public-safe payload.""" + directory = security_dir or _DEFAULT_SECURITY_DIR + snapshot = _load_snapshot(directory) + _require_boundaries(snapshot) + + summary = _summary(snapshot) + contract = snapshot.get("registry_export_contract") + contract = contract if isinstance(contract, dict) else {} + merged_summary = { + "required_field_count": _int(summary.get("required_field_count")), + "reviewer_check_count": _int(summary.get("reviewer_check_count")), + "outcome_lane_count": _int(summary.get("outcome_lane_count")), + "forbidden_payload_count": _int(summary.get("forbidden_payload_count")), + "expected_scope_alias_count": _int(summary.get("expected_scope_alias_count")), + "per_host_required_field_count": _int(summary.get("per_host_required_field_count")), + "allowed_collection_method_count": _len(contract.get("allowed_collection_methods")), + "registry_export_received_count": _int(summary.get("registry_export_received_count")), + "registry_export_accepted_count": _int(summary.get("registry_export_accepted_count")), + "owner_evidence_received_count": _int(summary.get("owner_evidence_received_count")), + "owner_evidence_accepted_count": _int(summary.get("owner_evidence_accepted_count")), + "owner_evidence_rejected_count": _int(summary.get("owner_evidence_rejected_count")), + "owner_evidence_quarantined_count": _int(summary.get("owner_evidence_quarantined_count")), + "runtime_gate_count": _int(summary.get("runtime_gate_count")), + "wazuh_api_live_query_authorized_count": 0, + "active_response_authorized_count": _int(summary.get("active_response_authorized_count")), + "host_write_authorized_count": _int(summary.get("host_write_authorized_count")), + "secret_value_collection_allowed_count": _int(summary.get("secret_value_collection_allowed_count")), + } + + return { + "schema_version": "iwooos_wazuh_owner_evidence_preflight_readback_v1", + "status": snapshot.get("status", "owner_evidence_preflight_ready_no_runtime_action"), + "mode": "committed_snapshot_readback_redacted_metadata_only", + "source_refs": [ + f"docs/security/{_SNAPSHOT_FILE}", + "scripts/security/wazuh-agent-visibility-owner-evidence-preflight.py", + ], + "summary": merged_summary, + "items": _items(merged_summary), + "boundary_markers": _boundary_markers(merged_summary), + "boundaries": { + "wazuh_api_live_query_authorized": False, + "wazuh_active_response_authorized": False, + "host_write_authorized": False, + "secret_value_collection_allowed": False, + "raw_wazuh_payload_storage_allowed": False, + "agent_identity_public_display_allowed": False, + "internal_ip_public_display_allowed": False, + "runtime_execution_authorized": False, + "not_authorization": True, + }, + "no_false_green_rules": [ + "負責人證據預檢 ready 不代表已收件或已接受", + "Wazuh 儀表板可見不是 manager registry counts 已驗收", + "Dashboard index pattern 三綠勾不可替代 API connection、API version 或 manager registry", + "agent service active、TCP 連線或舊截圖不可替代逐主機 registry matrix", + "收件封包若夾帶原始紀錄、內網識別、agent 原名或機密,必須隔離,不得渲染到前台", + "active response、host write、firewall、Nginx、Docker、K8s 或 secret 變更一律不是這個預檢授權", + ], + } + + +def _load_snapshot(directory: Path) -> dict[str, Any]: + path = directory / _SNAPSHOT_FILE + if not path.is_file(): + raise FileNotFoundError(f"{path}: Wazuh 負責人證據預檢快照不存在") + with path.open(encoding="utf-8") as handle: + payload = json.load(handle) + if not isinstance(payload, dict): + raise ValueError(f"{path}: expected JSON object") + if payload.get("schema_version") != _EXPECTED_SCHEMA: + raise ValueError(f"{path}: expected schema_version={_EXPECTED_SCHEMA}") + return payload + + +def _summary(payload: dict[str, Any]) -> dict[str, Any]: + summary = payload.get("summary") + return summary if isinstance(summary, dict) else {} + + +def _int(value: Any) -> int: + return value if isinstance(value, int) else 0 + + +def _len(value: Any) -> int: + return len(value) if isinstance(value, list) else 0 + + +def _items(summary: dict[str, int]) -> list[dict[str, Any]]: + return [ + _item( + "scope_aliases", + "EV-0", + "scope_aliases_ready", + "warn", + { + "expected_scope_aliases": summary["expected_scope_alias_count"], + "allowed_collection_methods": summary["allowed_collection_method_count"], + }, + ), + _item( + "registry_counts", + "EV-1", + "waiting_redacted_counts", + "warn", + { + "registry_export_received": summary["registry_export_received_count"], + "registry_export_accepted": summary["registry_export_accepted_count"], + }, + ), + _item( + "per_host_matrix", + "EV-2", + "waiting_per_host_matrix", + "warn", + {"per_host_required_fields": summary["per_host_required_field_count"]}, + ), + _item( + "time_window", + "EV-3", + "waiting_time_window", + "warn", + {"owner_received": summary["owner_evidence_received_count"]}, + ), + _item( + "health_refs", + "EV-4", + "waiting_health_refs", + "warn", + {"reviewer_checks": summary["reviewer_check_count"]}, + ), + _item( + "redaction", + "EV-5", + "reject_sensitive_payloads", + "locked", + { + "forbidden_payloads": summary["forbidden_payload_count"], + "quarantined": summary["owner_evidence_quarantined_count"], + }, + ), + _item( + "owner_decision", + "EV-6", + "waiting_owner_decision", + "warn", + { + "owner_received": summary["owner_evidence_received_count"], + "owner_accepted": summary["owner_evidence_accepted_count"], + }, + ), + _item( + "runtime_boundary", + "EV-7", + "runtime_closed", + "locked", + { + "runtime_gate": summary["runtime_gate_count"], + "live_query": summary["wazuh_api_live_query_authorized_count"], + "active_response": summary["active_response_authorized_count"], + "host_write": summary["host_write_authorized_count"], + }, + ), + ] + + +def _item( + item_id: str, + check: str, + state_key: str, + tone: str, + metrics: dict[str, int], +) -> dict[str, Any]: + return { + "item_id": item_id, + "check": check, + "state_key": state_key, + "tone": tone, + "metrics": metrics, + } + + +def _boundary_markers(summary: dict[str, int]) -> list[str]: + return [ + f"必要欄位={summary['required_field_count']}", + f"審查檢查={summary['reviewer_check_count']}", + f"結果分流={summary['outcome_lane_count']}", + f"拒收敏感類型={summary['forbidden_payload_count']}", + f"公開節點別名={summary['expected_scope_alias_count']}", + f"逐主機矩陣欄位={summary['per_host_required_field_count']}", + f"允許收集方式={summary['allowed_collection_method_count']}", + f"registry export 已收件={summary['registry_export_received_count']}", + f"registry export 已接受={summary['registry_export_accepted_count']}", + f"負責人證據已收件={summary['owner_evidence_received_count']}", + f"負責人證據已接受={summary['owner_evidence_accepted_count']}", + f"負責人證據已隔離={summary['owner_evidence_quarantined_count']}", + f"執行期閘門={summary['runtime_gate_count']}", + f"Wazuh 即時查詢={summary['wazuh_api_live_query_authorized_count']}", + f"Wazuh 主動回應={summary['active_response_authorized_count']}", + f"主機寫入={summary['host_write_authorized_count']}", + f"機密明文收集={summary['secret_value_collection_allowed_count']}", + "原始 Wazuh 載荷保存=false", + "agent 身分前台顯示=false", + "內網識別前台顯示=false", + "不是執行授權=true", + ] + + +def _require_boundaries(payload: dict[str, Any]) -> None: + summary = _summary(payload) + for key in ( + "registry_export_accepted_count", + "owner_evidence_accepted_count", + "runtime_gate_count", + "active_response_authorized_count", + "host_write_authorized_count", + "secret_value_collection_allowed_count", + ): + if _int(summary.get(key)) != 0: + raise ValueError(f"Wazuh 負責人證據預檢 summary.{key} 必須維持 0") + + boundaries = payload.get("execution_boundaries") + if not isinstance(boundaries, dict): + raise ValueError("Wazuh 負責人證據預檢 execution_boundaries 缺失") + for key in _REQUIRED_FALSE_BOUNDARIES: + if boundaries.get(key) is not False: + raise ValueError(f"Wazuh 負責人證據預檢 execution_boundaries.{key} 必須維持 false") + if boundaries.get("not_authorization") is not True: + raise ValueError("Wazuh 負責人證據預檢 not_authorization 必須維持 true") diff --git a/apps/api/tests/test_iwooos_runtime_security_readback.py b/apps/api/tests/test_iwooos_runtime_security_readback.py index c12e829a..d739e807 100644 --- a/apps/api/tests/test_iwooos_runtime_security_readback.py +++ b/apps/api/tests/test_iwooos_runtime_security_readback.py @@ -20,8 +20,8 @@ def test_iwooos_runtime_security_readback_preserves_zero_runtime_gates() -> None assert payload["schema_version"] == "iwooos_runtime_security_readback_v1" assert payload["status"] == "blocked_waiting_owner_evidence_and_runtime_gates" - assert payload["summary"]["source_snapshot_count"] == 9 - assert payload["summary"]["p0_lane_count"] == 8 + assert payload["summary"]["source_snapshot_count"] == 10 + assert payload["summary"]["p0_lane_count"] == 9 assert payload["summary"]["runtime_gate_count"] == 0 assert payload["summary"]["owner_response_received_count"] == 0 assert payload["summary"]["owner_response_accepted_count"] == 0 @@ -36,6 +36,16 @@ def test_iwooos_runtime_security_readback_preserves_zero_runtime_gates() -> None assert payload["summary"]["wazuh_live_metadata_gate_readonly_scope_accepted_count"] == 0 assert payload["summary"]["wazuh_live_metadata_gate_post_enable_readback_count"] == 0 assert payload["summary"]["wazuh_live_metadata_gate_live_query_authorized_count"] == 0 + assert payload["summary"]["wazuh_owner_evidence_required_field_count"] == 28 + assert payload["summary"]["wazuh_owner_evidence_reviewer_check_count"] == 15 + assert payload["summary"]["wazuh_owner_evidence_outcome_lane_count"] == 8 + assert payload["summary"]["wazuh_owner_evidence_forbidden_payload_count"] == 22 + assert payload["summary"]["wazuh_owner_evidence_expected_alias_count"] == 6 + assert payload["summary"]["wazuh_owner_evidence_registry_export_received_count"] == 0 + assert payload["summary"]["wazuh_owner_evidence_registry_export_accepted_count"] == 0 + assert payload["summary"]["wazuh_owner_evidence_received_count"] == 0 + assert payload["summary"]["wazuh_owner_evidence_accepted_count"] == 0 + assert payload["summary"]["wazuh_owner_evidence_runtime_gate_count"] == 0 assert payload["summary"]["kali_active_scan_authorized_count"] == 0 assert payload["summary"]["kali_execute_authorized_count"] == 0 assert payload["summary"]["alert_receipt_runtime_send_count"] == 0 @@ -53,6 +63,7 @@ def test_iwooos_runtime_security_readback_lanes_are_candidate_only() -> None: "wazuh_registry", "wazuh_live_route", "wazuh_live_metadata_gate", + "wazuh_owner_evidence_preflight", "wazuh_dashboard_api", "kali_intake", "alert_readability", @@ -66,6 +77,7 @@ def test_iwooos_runtime_security_readback_lanes_are_candidate_only() -> None: assert all(lane["lane_id"] != "wazuh_registry" or lane["completion_percent"] == 0 for lane in payload["lanes"]) assert all(lane["lane_id"] != "wazuh_live_route" or lane["metrics"]["route_degraded"] == 1 for lane in payload["lanes"]) assert all(lane["lane_id"] != "wazuh_live_metadata_gate" or lane["completion_percent"] == 0 for lane in payload["lanes"]) + assert all(lane["lane_id"] != "wazuh_owner_evidence_preflight" or lane["metrics"]["owner_accepted"] == 0 for lane in payload["lanes"]) def test_iwooos_runtime_security_readback_api_is_public_safe(monkeypatch) -> None: @@ -85,6 +97,8 @@ def test_iwooos_runtime_security_readback_api_is_public_safe(monkeypatch) -> Non assert data["summary"]["wazuh_live_route_degraded_count"] == 1 assert data["summary"]["wazuh_live_metadata_available_count"] == 0 assert data["summary"]["wazuh_live_metadata_gate_live_query_authorized_count"] == 0 + assert data["summary"]["wazuh_owner_evidence_accepted_count"] == 0 + assert data["summary"]["wazuh_owner_evidence_runtime_gate_count"] == 0 assert data["boundaries"]["secret_value_collection_allowed"] is False assert "192.168.0." not in response.text assert "工作視窗" not in response.text @@ -169,3 +183,44 @@ def test_iwooos_wazuh_live_metadata_gate_api_is_public_safe(monkeypatch) -> None assert "工作視窗" not in response.text assert "批准!繼續" not in response.text assert "WAZUH_API_PASSWORD" not in response.text + + +def test_iwooos_wazuh_owner_evidence_preflight_api_is_public_safe(monkeypatch) -> None: + monkeypatch.delenv("IWOOOS_WAZUH_READONLY_ENABLED", raising=False) + monkeypatch.delenv("WAZUH_API_BASE_URL", raising=False) + monkeypatch.delenv("WAZUH_API_USERNAME", raising=False) + monkeypatch.delenv("WAZUH_API_PASSWORD", raising=False) + + response = _client().get("/api/v1/iwooos/wazuh-owner-evidence-preflight") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "iwooos_wazuh_owner_evidence_preflight_readback_v1" + assert data["status"] == "owner_evidence_preflight_ready_no_runtime_action" + assert data["summary"]["required_field_count"] == 28 + assert data["summary"]["reviewer_check_count"] == 15 + assert data["summary"]["outcome_lane_count"] == 8 + assert data["summary"]["forbidden_payload_count"] == 22 + assert data["summary"]["expected_scope_alias_count"] == 6 + assert data["summary"]["per_host_required_field_count"] == 9 + assert data["summary"]["registry_export_received_count"] == 0 + assert data["summary"]["registry_export_accepted_count"] == 0 + assert data["summary"]["owner_evidence_received_count"] == 0 + assert data["summary"]["owner_evidence_accepted_count"] == 0 + assert data["summary"]["runtime_gate_count"] == 0 + assert data["summary"]["wazuh_api_live_query_authorized_count"] == 0 + assert data["summary"]["active_response_authorized_count"] == 0 + assert data["summary"]["host_write_authorized_count"] == 0 + assert data["summary"]["secret_value_collection_allowed_count"] == 0 + assert data["boundaries"]["wazuh_api_live_query_authorized"] is False + assert data["boundaries"]["wazuh_active_response_authorized"] is False + assert data["boundaries"]["host_write_authorized"] is False + assert data["boundaries"]["runtime_execution_authorized"] is False + assert data["boundaries"]["not_authorization"] is True + assert len(data["items"]) == 8 + assert any(marker == "必要欄位=28" for marker in data["boundary_markers"]) + assert any(rule.startswith("負責人證據預檢 ready") for rule in data["no_false_green_rules"]) + assert "192.168.0." not in response.text + assert "工作視窗" not in response.text + assert "批准!繼續" not in response.text + assert "WAZUH_API_PASSWORD" not in response.text diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index de809cde..c175dccf 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -20300,7 +20300,7 @@ }, "runtimeSecurityReadback": { "eyebrow": "IwoooS Runtime 資安讀回", - "title": "八條 P0 資安線先接到同一張讀回板", + "title": "九條 P0 資安線先接到同一張讀回板", "subtitle": "這張板讀取後端彙整的只讀快照,並附上 Wazuh 正式只讀路由的公開安全 aggregate 讀回;它不保存 raw Wazuh payload、不啟動掃描、不送訊息、不改主機。", "statusLabel": "讀回狀態", "statusDetail": "讀回成功只代表 IwoooS 能看見目前的證據邊界;runtime 寫入、主動回應、掃描、重啟、Nginx reload、workflow 修改與機密操作仍全部關閉。", @@ -20361,6 +20361,10 @@ "title": "Wazuh 即時中繼資料閘門", "body": "正式路由讀回後仍必須補負責人回覆、機密來源中繼資料、管理節點健康、唯讀範圍與啟用後讀回;即時查詢授權維持 0。" }, + "wazuh_owner_evidence_preflight": { + "title": "Wazuh 負責人證據預檢", + "body": "把 manager registry 脫敏封包、逐主機矩陣、Dashboard API 狀態、拒收敏感類型與負責人決策先變成可驗收格式;收件、接受與執行期仍為 0。" + }, "wazuh_dashboard_api": { "title": "Wazuh Dashboard API", "body": "API connection / API version 還要補 readback;index pattern 通過不能宣稱 Wazuh 全綠。" @@ -20553,8 +20557,24 @@ "subtitle": "這張卡把 Wazuh 管理器代理清單真相的必要欄位、審查檢查、拒收分流與禁止內容公開給操作員;目前尚未收到或接受任何負責人證據,也不授權主機操作。", "checkLabel": "檢核", "stateLabel": "狀態", - "boundaryTitle": "Owner evidence 收件邊界", + "loadingBoundary": "正在讀取負責人證據預檢", + "boundaryTitle": "負責人證據收件邊界", "boundaryIntro": "以下鍵值固定:收件格式已準備好,且新增 6 個公開節點別名與逐主機匯出矩陣要求;registry export、已收件、已接受與執行閘門仍為 0。任何原始紀錄、未脫敏截圖、內網位址、代理原名或機密都必須拒收或隔離。", + "status": { + "loading": "正在讀取 Wazuh 負責人證據預檢", + "failed": "Wazuh 負責人證據預檢尚未部署或讀取失敗", + "ready": "Wazuh 負責人證據預檢已讀回,但收件、接受與執行期仍為 0" + }, + "states": { + "scope_aliases_ready": "公開別名已定義", + "waiting_redacted_counts": "待脫敏計數", + "waiting_per_host_matrix": "待逐主機矩陣", + "waiting_time_window": "待時間窗", + "waiting_health_refs": "待健康參照", + "reject_sensitive_payloads": "拒收敏感內容", + "waiting_owner_decision": "待負責人決策", + "runtime_closed": "執行期關閉" + }, "summary": { "fields": { "label": "必要欄位", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index de809cde..c175dccf 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -20300,7 +20300,7 @@ }, "runtimeSecurityReadback": { "eyebrow": "IwoooS Runtime 資安讀回", - "title": "八條 P0 資安線先接到同一張讀回板", + "title": "九條 P0 資安線先接到同一張讀回板", "subtitle": "這張板讀取後端彙整的只讀快照,並附上 Wazuh 正式只讀路由的公開安全 aggregate 讀回;它不保存 raw Wazuh payload、不啟動掃描、不送訊息、不改主機。", "statusLabel": "讀回狀態", "statusDetail": "讀回成功只代表 IwoooS 能看見目前的證據邊界;runtime 寫入、主動回應、掃描、重啟、Nginx reload、workflow 修改與機密操作仍全部關閉。", @@ -20361,6 +20361,10 @@ "title": "Wazuh 即時中繼資料閘門", "body": "正式路由讀回後仍必須補負責人回覆、機密來源中繼資料、管理節點健康、唯讀範圍與啟用後讀回;即時查詢授權維持 0。" }, + "wazuh_owner_evidence_preflight": { + "title": "Wazuh 負責人證據預檢", + "body": "把 manager registry 脫敏封包、逐主機矩陣、Dashboard API 狀態、拒收敏感類型與負責人決策先變成可驗收格式;收件、接受與執行期仍為 0。" + }, "wazuh_dashboard_api": { "title": "Wazuh Dashboard API", "body": "API connection / API version 還要補 readback;index pattern 通過不能宣稱 Wazuh 全綠。" @@ -20553,8 +20557,24 @@ "subtitle": "這張卡把 Wazuh 管理器代理清單真相的必要欄位、審查檢查、拒收分流與禁止內容公開給操作員;目前尚未收到或接受任何負責人證據,也不授權主機操作。", "checkLabel": "檢核", "stateLabel": "狀態", - "boundaryTitle": "Owner evidence 收件邊界", + "loadingBoundary": "正在讀取負責人證據預檢", + "boundaryTitle": "負責人證據收件邊界", "boundaryIntro": "以下鍵值固定:收件格式已準備好,且新增 6 個公開節點別名與逐主機匯出矩陣要求;registry export、已收件、已接受與執行閘門仍為 0。任何原始紀錄、未脫敏截圖、內網位址、代理原名或機密都必須拒收或隔離。", + "status": { + "loading": "正在讀取 Wazuh 負責人證據預檢", + "failed": "Wazuh 負責人證據預檢尚未部署或讀取失敗", + "ready": "Wazuh 負責人證據預檢已讀回,但收件、接受與執行期仍為 0" + }, + "states": { + "scope_aliases_ready": "公開別名已定義", + "waiting_redacted_counts": "待脫敏計數", + "waiting_per_host_matrix": "待逐主機矩陣", + "waiting_time_window": "待時間窗", + "waiting_health_refs": "待健康參照", + "reject_sensitive_payloads": "拒收敏感內容", + "waiting_owner_decision": "待負責人決策", + "runtime_closed": "執行期關閉" + }, "summary": { "fields": { "label": "必要欄位", diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index db325b00..fa870749 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -40,6 +40,8 @@ import { type IwoooSSecurityControlCoverageResponse, type IwoooSWazuhLiveMetadataGateItem, type IwoooSWazuhLiveMetadataGateResponse, + type IwoooSWazuhOwnerEvidencePreflightItem, + type IwoooSWazuhOwnerEvidencePreflightResponse, } from '@/lib/api-client' type PostureMetric = { @@ -2365,14 +2367,6 @@ const wazuhLiveMetadataEnvGateBoundaries = [ 'not_authorization=true', ] as const -const wazuhOwnerEvidencePreflightSummary = [ - { 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' }, @@ -2384,14 +2378,25 @@ const wazuhOwnerEvidencePreflightItems: WazuhOwnerEvidencePreflightItem[] = [ { key: 'runtimeBoundary', check: 'EV-7', state: '不開執行', icon: Lock, tone: 'locked' }, ] as const +const wazuhOwnerEvidencePreflightItemKeyById: Record = { + scope_aliases: 'scopeAliases', + registry_counts: 'registryCounts', + per_host_matrix: 'perHostMatrix', + time_window: 'timeWindow', + health_refs: 'healthRefs', + redaction: 'redaction', + owner_decision: 'ownerDecision', + runtime_boundary: 'runtimeBoundary', +} + const wazuhOwnerEvidencePreflightBoundaries = [ 'wazuh_agent_visibility_owner_evidence_preflight_visible=true', - 'wazuh_agent_visibility_owner_evidence_required_field_count=23', - 'wazuh_agent_visibility_owner_evidence_reviewer_check_count=10', + 'wazuh_agent_visibility_owner_evidence_required_field_count=28', + 'wazuh_agent_visibility_owner_evidence_reviewer_check_count=15', '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_outcome_lane_count=8', + 'wazuh_agent_visibility_owner_evidence_forbidden_payload_count=22', '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', @@ -9115,6 +9120,92 @@ function IwoooSWazuhLiveMetadataEnvGateBoard() { function IwoooSWazuhOwnerEvidencePreflightBoard() { const t = useTranslations('iwooos.wazuhOwnerEvidencePreflight') const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const } + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [failed, setFailed] = useState(false) + + useEffect(() => { + let mounted = true + + async function loadPreflight() { + setLoading(true) + setFailed(false) + try { + const payload = await apiClient.getIwoooSWazuhOwnerEvidencePreflight() + if (mounted) { + setData(payload) + } + } catch { + if (mounted) { + setData(null) + setFailed(true) + } + } finally { + if (mounted) { + setLoading(false) + } + } + } + + loadPreflight() + return () => { + mounted = false + } + }, []) + + const summary = data?.summary + const summaryItems = [ + { + key: 'fields', + value: summary ? String(summary.required_field_count) : loading ? '...' : '28', + icon: ClipboardCheck, + tone: 'steady', + }, + { + key: 'aliases', + value: summary ? String(summary.expected_scope_alias_count) : loading ? '...' : '6', + icon: Server, + tone: 'warn', + }, + { + key: 'checks', + value: summary ? String(summary.reviewer_check_count) : loading ? '...' : '15', + icon: ListChecks, + tone: 'steady', + }, + { + key: 'received', + value: summary ? String(summary.owner_evidence_received_count) : loading ? '...' : '0', + icon: FileWarning, + tone: 'locked', + }, + { + key: 'accepted', + value: summary ? String(summary.owner_evidence_accepted_count) : loading ? '...' : '0', + icon: Lock, + tone: 'locked', + }, + ] as const + const preflightItems = data?.items?.length + ? data.items.map(item => { + const key = wazuhOwnerEvidencePreflightItemKeyById[item.item_id] + const fallback = wazuhOwnerEvidencePreflightItems.find(candidate => candidate.key === key) + return { + key, + check: item.check, + state: t(`states.${item.state_key}` as never), + icon: fallback?.icon ?? FileWarning, + tone: item.tone, + } + }) + : wazuhOwnerEvidencePreflightItems + const boundaryMarkers = data?.boundary_markers?.length + ? data.boundary_markers + : loading + ? [t('loadingBoundary')] + : wazuhOwnerEvidencePreflightBoundaries + const statusText = loading ? t('status.loading') : failed ? t('status.failed') : t('status.ready') + const statusTone: 'steady' | 'warn' | 'locked' = loading || failed ? 'warn' : 'locked' return (
{t('subtitle')}

+
+ + {statusText} +
- {wazuhOwnerEvidencePreflightSummary.map(item => { + {summaryItems.map(item => { const Icon = item.icon return (
@@ -9163,7 +9258,7 @@ function IwoooSWazuhOwnerEvidencePreflightBoard() { gap: 10, }} > - {wazuhOwnerEvidencePreflightItems.map(item => { + {preflightItems.map(item => { const Icon = item.icon return (
- {wazuhOwnerEvidencePreflightBoundaries.map(item => ( + {boundaryMarkers.map(item => ( +} + +export interface IwoooSWazuhOwnerEvidencePreflightResponse { + schema_version: 'iwooos_wazuh_owner_evidence_preflight_readback_v1' + status: string + mode: string + source_refs: string[] + summary: { + required_field_count: number + reviewer_check_count: number + outcome_lane_count: number + forbidden_payload_count: number + expected_scope_alias_count: number + per_host_required_field_count: number + allowed_collection_method_count: number + registry_export_received_count: number + registry_export_accepted_count: number + owner_evidence_received_count: number + owner_evidence_accepted_count: number + owner_evidence_rejected_count: number + owner_evidence_quarantined_count: number + runtime_gate_count: number + wazuh_api_live_query_authorized_count: number + active_response_authorized_count: number + host_write_authorized_count: number + secret_value_collection_allowed_count: number + } + items: IwoooSWazuhOwnerEvidencePreflightItem[] + boundary_markers: string[] + boundaries: Record + no_false_green_rules: string[] +} + export interface IwoooSSecurityControlCoverageDomain { domain_id: | 'high_value_asset_control' @@ -320,6 +378,11 @@ export const apiClient = { return handleResponse(res) }, + async getIwoooSWazuhOwnerEvidencePreflight() { + const res = await fetch(`${API_BASE_URL}/iwooos/wazuh-owner-evidence-preflight`, { cache: 'no-store' }) + return handleResponse(res) + }, + async getIwoooSSecurityControlCoverage() { const res = await fetch(`${API_BASE_URL}/iwooos/security-control-coverage`, { cache: 'no-store' }) return handleResponse(res) diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 530106b4..649e4fed 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -29513,9 +29513,12 @@ 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_required_field_count=28", + "wazuh_agent_visibility_owner_evidence_reviewer_check_count=15", "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=8", + "wazuh_agent_visibility_owner_evidence_forbidden_payload_count=22", "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",