diff --git a/apps/api/src/api/v1/iwooos.py b/apps/api/src/api/v1/iwooos.py
index 4fcc95b5..2c28ef50 100644
--- a/apps/api/src/api/v1/iwooos.py
+++ b/apps/api/src/api/v1/iwooos.py
@@ -32,6 +32,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_managed_host_coverage import (
+ load_latest_iwooos_wazuh_managed_host_coverage,
+)
from src.services.iwooos_wazuh_owner_evidence_preflight import (
load_latest_iwooos_wazuh_owner_evidence_preflight,
)
@@ -116,6 +119,34 @@ async def get_iwooos_wazuh_owner_evidence_preflight() -> dict[str, Any]:
) from exc
+@router.get(
+ "/api/v1/iwooos/wazuh-managed-host-coverage",
+ response_model=dict[str, Any],
+ summary="取得 Wazuh 受管主機覆蓋只讀讀回",
+ description=(
+ "讀取已提交的 Wazuh 受管主機覆蓋快照,回傳公開別名主機矩陣、manager registry "
+ "接受數、缺口數、必要驗收證據與 0 / false 邊界。此端點不查 Wazuh API、"
+ "不讀主機、不重新註冊 agent、不重啟 Wazuh、不保存原始載荷、不收機密明文、"
+ "不啟用主動回應、不改 Nginx / Docker / K8s / firewall。"
+ ),
+)
+async def get_iwooos_wazuh_managed_host_coverage() -> dict[str, Any]:
+ """回傳 Wazuh 受管主機覆蓋公開安全只讀狀態。"""
+ try:
+ payload = await asyncio.to_thread(load_latest_iwooos_wazuh_managed_host_coverage)
+ 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_wazuh_managed_host_coverage.py b/apps/api/src/services/iwooos_wazuh_managed_host_coverage.py
new file mode 100644
index 00000000..838088c5
--- /dev/null
+++ b/apps/api/src/services/iwooos_wazuh_managed_host_coverage.py
@@ -0,0 +1,248 @@
+"""
+IwoooS Wazuh managed host coverage readback.
+
+This service exposes the committed Wazuh managed-host coverage snapshot as a
+public-safe, alias-only payload. It never queries Wazuh, reads secrets, reenrolls
+agents, restarts services, writes hosts, or enables active response.
+"""
+
+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-managed-host-coverage-gate.snapshot.json"
+_EXPECTED_SCHEMA = "wazuh_managed_host_coverage_gate_v1"
+
+_REQUIRED_FALSE_BOUNDARIES = {
+ "host_write_authorized",
+ "kali_active_scan_authorized",
+ "raw_wazuh_payload_storage_allowed",
+ "runtime_execution_authorized",
+ "secret_value_collection_allowed",
+ "wazuh_active_response_authorized",
+ "wazuh_agent_reenroll_authorized",
+ "wazuh_agent_restart_authorized",
+ "wazuh_api_live_query_authorized",
+ "wazuh_manager_restart_authorized",
+}
+
+_STATUS_LABELS = {
+ "agent_active_transport_observed": "代理服務與傳輸連線已觀察,仍待管理器清單驗收",
+ "no_agent_transport_observed": "未觀察到代理傳輸,需確認安裝、服務與負責人決策",
+ "ssh_readback_blocked": "合法只讀讀回仍受阻,需 owner export 或維護窗口",
+}
+
+_NEXT_GATE_LABELS = {
+ "manager_registry_cross_check": "補管理器清單交叉驗收",
+ "agent_install_or_service_owner_decision": "補代理安裝狀態或服務負責人決策",
+ "read_only_access_or_owner_export": "補只讀 access 或脫敏 owner export",
+}
+
+
+def load_latest_iwooos_wazuh_managed_host_coverage(
+ security_dir: Path | None = None,
+) -> dict[str, Any]:
+ """Load Wazuh managed-host coverage as an alias-only public readback."""
+ directory = security_dir or _DEFAULT_SECURITY_DIR
+ snapshot = _load_snapshot(directory)
+ _require_boundaries(snapshot)
+
+ summary = _summary(snapshot)
+ matrix = _host_scope_matrix(snapshot)
+ evidence = _required_evidence(snapshot)
+ merged_summary = {
+ "expected_host_scope_count": _int(summary.get("expected_host_scope_count")),
+ "manager_service_active_observed_count": _int(summary.get("manager_service_active_observed_count")),
+ "manager_api_unauthenticated_response_count": _int(summary.get("manager_api_unauthenticated_response_count")),
+ "manager_transport_established_connection_count": _int(
+ summary.get("manager_transport_established_connection_count")
+ ),
+ "direct_agent_active_observed_count": _int(summary.get("direct_agent_active_observed_count")),
+ "direct_agent_transport_observed_count": _int(summary.get("direct_agent_transport_observed_count")),
+ "direct_agent_missing_or_no_transport_count": _int(
+ summary.get("direct_agent_missing_or_no_transport_count")
+ ),
+ "ssh_readback_blocked_count": _int(summary.get("ssh_readback_blocked_count")),
+ "manager_registry_accepted_count": _int(summary.get("manager_registry_accepted_count")),
+ "manager_registry_gap_count": max(
+ _int(summary.get("expected_host_scope_count")) - _int(summary.get("manager_registry_accepted_count")),
+ 0,
+ ),
+ "dashboard_api_degraded_observed_count": _int(summary.get("dashboard_api_degraded_observed_count")),
+ "live_metadata_env_enabled_count": _int(summary.get("live_metadata_env_enabled_count")),
+ "active_response_authorized_count": _int(summary.get("active_response_authorized_count")),
+ "host_write_authorized_count": _int(summary.get("host_write_authorized_count")),
+ "agent_reenroll_authorized_count": _int(summary.get("agent_reenroll_authorized_count")),
+ "agent_restart_authorized_count": _int(summary.get("agent_restart_authorized_count")),
+ "runtime_gate_count": _int(summary.get("runtime_gate_count")),
+ "host_scope_matrix_count": len(matrix),
+ "required_evidence_before_green_count": len(evidence),
+ "required_evidence_accepted_count": sum(1 for item in evidence if item.get("accepted") is True),
+ }
+
+ return {
+ "schema_version": "iwooos_wazuh_managed_host_coverage_readback_v1",
+ "status": snapshot.get("status", "blocked_waiting_full_host_registry_readback"),
+ "mode": "committed_snapshot_readback_alias_only_no_wazuh_live_query",
+ "source_refs": [
+ f"docs/security/{_SNAPSHOT_FILE}",
+ "scripts/security/wazuh-managed-host-coverage-gate.py",
+ ],
+ "summary": merged_summary,
+ "host_scope_matrix": matrix,
+ "required_evidence_before_green": evidence,
+ "operator_interpretation": _list_of_strings(snapshot.get("operator_interpretation")),
+ "forbidden_completion_claims": _list_of_strings(snapshot.get("forbidden_completion_claims")),
+ "forbidden_actions": _list_of_strings(snapshot.get("forbidden_actions")),
+ "boundary_markers": _boundary_markers(merged_summary),
+ "boundaries": {
+ "wazuh_api_live_query_authorized": False,
+ "wazuh_active_response_authorized": False,
+ "wazuh_agent_reenroll_authorized": False,
+ "wazuh_agent_restart_authorized": False,
+ "wazuh_manager_restart_authorized": False,
+ "host_write_authorized": False,
+ "kali_active_scan_authorized": False,
+ "secret_value_collection_allowed": False,
+ "raw_wazuh_payload_storage_allowed": False,
+ "runtime_execution_authorized": False,
+ "not_authorization": True,
+ },
+ "no_false_green_rules": [
+ "Wazuh Dashboard 可見不等於 manager registry 已恢復",
+ "transport 連線、agent service active 或 HTTP 200 不可替代逐主機 registry matrix",
+ "manager_registry_accepted_count 維持 0 時不得宣稱所有主機已納管",
+ "重新註冊、重啟、active response、主機寫入、Nginx、firewall、Kali 掃描與機密調整都不是此讀回授權",
+ ],
+ }
+
+
+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 _list_of_strings(value: Any) -> list[str]:
+ if not isinstance(value, list):
+ return []
+ return [item for item in value if isinstance(item, str)]
+
+
+def _host_scope_matrix(payload: dict[str, Any]) -> list[dict[str, Any]]:
+ raw_items = payload.get("host_scope_matrix")
+ if not isinstance(raw_items, list):
+ raise ValueError("Wazuh 受管主機覆蓋 host_scope_matrix 缺失")
+
+ matrix: list[dict[str, Any]] = []
+ for raw_item in raw_items:
+ if not isinstance(raw_item, dict):
+ raise ValueError("Wazuh 受管主機覆蓋 host_scope_matrix 必須是 object list")
+ node_id = str(raw_item.get("node_id", ""))
+ role = str(raw_item.get("role", ""))
+ readback_status = str(raw_item.get("readback_status", ""))
+ next_gate = str(raw_item.get("next_gate", ""))
+ if not node_id.startswith("managed_"):
+ raise ValueError(f"Wazuh 受管主機覆蓋 node_id 必須維持公開別名:{node_id}")
+ if raw_item.get("manager_registry_accepted") is not False:
+ raise ValueError(f"Wazuh 受管主機覆蓋 {node_id} manager_registry_accepted 必須維持 false")
+ matrix.append(
+ {
+ "node_id": node_id,
+ "role": role,
+ "readback_status": readback_status,
+ "readback_status_label": _STATUS_LABELS.get(readback_status, "待補只讀讀回"),
+ "next_gate": next_gate,
+ "next_gate_label": _NEXT_GATE_LABELS.get(next_gate, "待補負責人驗收"),
+ "manager_registry_accepted": False,
+ }
+ )
+ return matrix
+
+
+def _required_evidence(payload: dict[str, Any]) -> list[dict[str, Any]]:
+ raw_items = payload.get("required_evidence_before_green")
+ if not isinstance(raw_items, list):
+ raise ValueError("Wazuh 受管主機覆蓋 required_evidence_before_green 缺失")
+ evidence: list[dict[str, Any]] = []
+ for raw_item in raw_items:
+ if not isinstance(raw_item, dict):
+ raise ValueError("Wazuh 受管主機覆蓋 required_evidence_before_green 必須是 object list")
+ evidence_id = str(raw_item.get("evidence_id", ""))
+ accepted = raw_item.get("accepted") is True
+ evidence.append({"evidence_id": evidence_id, "accepted": accepted})
+ return evidence
+
+
+def _boundary_markers(summary: dict[str, int]) -> list[str]:
+ return [
+ "wazuh_managed_host_coverage_gate_visible=true",
+ f"wazuh_managed_host_coverage_expected_host_scope_count={summary['expected_host_scope_count']}",
+ f"wazuh_managed_host_coverage_host_scope_matrix_count={summary['host_scope_matrix_count']}",
+ f"wazuh_managed_host_coverage_direct_agent_active_observed_count={summary['direct_agent_active_observed_count']}",
+ f"wazuh_managed_host_coverage_direct_agent_missing_or_no_transport_count={summary['direct_agent_missing_or_no_transport_count']}",
+ f"wazuh_managed_host_coverage_ssh_readback_blocked_count={summary['ssh_readback_blocked_count']}",
+ f"wazuh_managed_host_coverage_manager_registry_accepted_count={summary['manager_registry_accepted_count']}",
+ f"wazuh_managed_host_coverage_manager_registry_gap_count={summary['manager_registry_gap_count']}",
+ f"wazuh_managed_host_coverage_required_evidence_before_green_count={summary['required_evidence_before_green_count']}",
+ f"wazuh_managed_host_coverage_required_evidence_accepted_count={summary['required_evidence_accepted_count']}",
+ f"wazuh_managed_host_coverage_dashboard_api_degraded_observed_count={summary['dashboard_api_degraded_observed_count']}",
+ f"wazuh_managed_host_coverage_live_metadata_env_enabled_count={summary['live_metadata_env_enabled_count']}",
+ f"wazuh_managed_host_coverage_runtime_gate_count={summary['runtime_gate_count']}",
+ f"wazuh_agent_reenroll_authorized_count={summary['agent_reenroll_authorized_count']}",
+ f"wazuh_agent_restart_authorized_count={summary['agent_restart_authorized_count']}",
+ "wazuh_agent_reenroll_authorized=false",
+ "wazuh_agent_restart_authorized=false",
+ "wazuh_manager_restart_authorized=false",
+ "wazuh_active_response_authorized=false",
+ "host_write_authorized=false",
+ "secret_value_collection_allowed=false",
+ "raw_wazuh_payload_storage_allowed=false",
+ "runtime_execution_authorized=false",
+ "not_authorization=true",
+ ]
+
+
+def _require_boundaries(payload: dict[str, Any]) -> None:
+ summary = _summary(payload)
+ for key in (
+ "manager_registry_accepted_count",
+ "live_metadata_env_enabled_count",
+ "active_response_authorized_count",
+ "host_write_authorized_count",
+ "agent_reenroll_authorized_count",
+ "agent_restart_authorized_count",
+ "runtime_gate_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_wazuh_managed_host_coverage.py b/apps/api/tests/test_iwooos_wazuh_managed_host_coverage.py
new file mode 100644
index 00000000..ba53c84f
--- /dev/null
+++ b/apps/api/tests/test_iwooos_wazuh_managed_host_coverage.py
@@ -0,0 +1,89 @@
+from __future__ import annotations
+
+from fastapi import FastAPI
+from fastapi.testclient import TestClient
+
+from src.api.v1.iwooos import router
+from src.services.iwooos_wazuh_managed_host_coverage import (
+ load_latest_iwooos_wazuh_managed_host_coverage,
+)
+
+
+def _client() -> TestClient:
+ app = FastAPI()
+ app.include_router(router)
+ return TestClient(app)
+
+
+def test_iwooos_wazuh_managed_host_coverage_keeps_registry_gate_closed() -> None:
+ payload = load_latest_iwooos_wazuh_managed_host_coverage()
+
+ assert payload["schema_version"] == "iwooos_wazuh_managed_host_coverage_readback_v1"
+ assert payload["status"] == "blocked_waiting_full_host_registry_readback"
+ assert payload["mode"] == "committed_snapshot_readback_alias_only_no_wazuh_live_query"
+ assert payload["summary"]["expected_host_scope_count"] == 6
+ assert payload["summary"]["host_scope_matrix_count"] == 6
+ assert payload["summary"]["direct_agent_active_observed_count"] == 2
+ assert payload["summary"]["direct_agent_missing_or_no_transport_count"] == 1
+ assert payload["summary"]["ssh_readback_blocked_count"] == 3
+ assert payload["summary"]["manager_registry_accepted_count"] == 0
+ assert payload["summary"]["manager_registry_gap_count"] == 6
+ assert payload["summary"]["required_evidence_before_green_count"] == 6
+ assert payload["summary"]["required_evidence_accepted_count"] == 0
+ assert payload["summary"]["runtime_gate_count"] == 0
+ assert payload["summary"]["active_response_authorized_count"] == 0
+ assert payload["summary"]["host_write_authorized_count"] == 0
+ assert payload["summary"]["agent_reenroll_authorized_count"] == 0
+ assert payload["summary"]["agent_restart_authorized_count"] == 0
+
+ boundaries = payload["boundaries"]
+ assert boundaries["not_authorization"] is True
+ for key, value in boundaries.items():
+ if key == "not_authorization":
+ continue
+ assert value is False
+
+
+def test_iwooos_wazuh_managed_host_coverage_alias_matrix_is_complete() -> None:
+ payload = load_latest_iwooos_wazuh_managed_host_coverage()
+
+ matrix = payload["host_scope_matrix"]
+ assert [item["node_id"] for item in matrix] == [
+ "managed_core_node_a",
+ "managed_core_node_b",
+ "managed_dev_node_a",
+ "managed_dev_node_b",
+ "managed_control_node_a",
+ "managed_control_node_b",
+ ]
+ assert all(item["node_id"].startswith("managed_") for item in matrix)
+ assert all(item["manager_registry_accepted"] is False for item in matrix)
+ assert matrix[0]["readback_status"] == "agent_active_transport_observed"
+ assert matrix[0]["next_gate"] == "manager_registry_cross_check"
+ assert matrix[2]["readback_status"] == "no_agent_transport_observed"
+ assert matrix[3]["readback_status"] == "ssh_readback_blocked"
+
+
+def test_iwooos_wazuh_managed_host_coverage_api_is_public_safe() -> None:
+ response = _client().get("/api/v1/iwooos/wazuh-managed-host-coverage")
+
+ assert response.status_code == 200
+ data = response.json()
+ assert data["schema_version"] == "iwooos_wazuh_managed_host_coverage_readback_v1"
+ assert data["summary"]["expected_host_scope_count"] == 6
+ assert data["summary"]["manager_registry_accepted_count"] == 0
+ assert data["summary"]["manager_registry_gap_count"] == 6
+ assert data["summary"]["required_evidence_accepted_count"] == 0
+ assert data["summary"]["runtime_gate_count"] == 0
+ assert len(data["host_scope_matrix"]) == 6
+ assert any(marker == "wazuh_managed_host_coverage_host_scope_matrix_count=6" for marker in data["boundary_markers"])
+ assert any(marker == "wazuh_managed_host_coverage_manager_registry_accepted_count=0" for marker in data["boundary_markers"])
+ assert any(marker == "wazuh_managed_host_coverage_manager_registry_gap_count=6" for marker in data["boundary_markers"])
+ assert any(marker == "wazuh_managed_host_coverage_required_evidence_accepted_count=0" for marker in data["boundary_markers"])
+ assert any(rule.startswith("Wazuh Dashboard 可見不等於") 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 "source_thread_id" not in response.text
+ assert "owenhytsai/" 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 becb1679..0f5e479e 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -20729,8 +20729,20 @@
"subtitle": "這張卡把 Wazuh 納管拆成應納管範圍、直接觀察到的代理、覆蓋缺口與管理器清單驗收。現在只能確認部分節點有代理與連線,不能宣稱所有主機都已恢復。",
"checkLabel": "檢核",
"stateLabel": "狀態",
+ "loadingBoundary": "正在讀取 Wazuh 受管主機覆蓋只讀 API",
+ "matrixTitle": "公開別名主機矩陣",
+ "matrixSubtitle": "只顯示節點別名、角色、讀回狀態與下一個 Gate",
+ "matrixAcceptedLabel": "清單驗收",
+ "matrixNextGateLabel": "下一個 Gate",
+ "matrixLoading": "正在讀取 alias-only 受管主機矩陣。",
+ "matrixFallback": "受管主機矩陣尚未由正式 API 讀回,維持 fallback 靜態邊界。",
"boundaryTitle": "主機覆蓋邊界",
"boundaryIntro": "以下鍵值固定:連線存在、代理服務啟動或儀表板可開都不能替代管理器清單;重新註冊、重啟、主機寫入、機密調整與主動回應都需要獨立維護窗口與回滾負責人。",
+ "status": {
+ "loading": "正在讀取 Wazuh 主機覆蓋只讀 API",
+ "failed": "Wazuh 主機覆蓋 API 尚未部署或讀取失敗,維持 fallback 邊界",
+ "ready": "Wazuh 主機覆蓋只讀 API 已接上,manager registry accepted 仍為 0"
+ },
"summary": {
"scope": {
"label": "應納管範圍",
@@ -20742,7 +20754,7 @@
},
"coverageGap": {
"label": "覆蓋缺口",
- "detail": "包含無傳輸與尚未取得合法只讀讀回的節點。"
+ "detail": "以 manager registry 驗收為準,6 個節點目前都不能算完成。"
},
"registry": {
"label": "清單驗收",
diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json
index becb1679..0f5e479e 100644
--- a/apps/web/messages/zh-TW.json
+++ b/apps/web/messages/zh-TW.json
@@ -20729,8 +20729,20 @@
"subtitle": "這張卡把 Wazuh 納管拆成應納管範圍、直接觀察到的代理、覆蓋缺口與管理器清單驗收。現在只能確認部分節點有代理與連線,不能宣稱所有主機都已恢復。",
"checkLabel": "檢核",
"stateLabel": "狀態",
+ "loadingBoundary": "正在讀取 Wazuh 受管主機覆蓋只讀 API",
+ "matrixTitle": "公開別名主機矩陣",
+ "matrixSubtitle": "只顯示節點別名、角色、讀回狀態與下一個 Gate",
+ "matrixAcceptedLabel": "清單驗收",
+ "matrixNextGateLabel": "下一個 Gate",
+ "matrixLoading": "正在讀取 alias-only 受管主機矩陣。",
+ "matrixFallback": "受管主機矩陣尚未由正式 API 讀回,維持 fallback 靜態邊界。",
"boundaryTitle": "主機覆蓋邊界",
"boundaryIntro": "以下鍵值固定:連線存在、代理服務啟動或儀表板可開都不能替代管理器清單;重新註冊、重啟、主機寫入、機密調整與主動回應都需要獨立維護窗口與回滾負責人。",
+ "status": {
+ "loading": "正在讀取 Wazuh 主機覆蓋只讀 API",
+ "failed": "Wazuh 主機覆蓋 API 尚未部署或讀取失敗,維持 fallback 邊界",
+ "ready": "Wazuh 主機覆蓋只讀 API 已接上,manager registry accepted 仍為 0"
+ },
"summary": {
"scope": {
"label": "應納管範圍",
@@ -20742,7 +20754,7 @@
},
"coverageGap": {
"label": "覆蓋缺口",
- "detail": "包含無傳輸與尚未取得合法只讀讀回的節點。"
+ "detail": "以 manager registry 驗收為準,6 個節點目前都不能算完成。"
},
"registry": {
"label": "清單驗收",
diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx
index 9b413489..83e3efc4 100644
--- a/apps/web/src/app/[locale]/iwooos/page.tsx
+++ b/apps/web/src/app/[locale]/iwooos/page.tsx
@@ -44,6 +44,7 @@ import {
type IwoooSSecurityControlCoverageResponse,
type IwoooSWazuhLiveMetadataGateItem,
type IwoooSWazuhLiveMetadataGateResponse,
+ type IwoooSWazuhManagedHostCoverageResponse,
type IwoooSWazuhOwnerEvidencePreflightItem,
type IwoooSWazuhOwnerEvidencePreflightResponse,
} from '@/lib/api-client'
@@ -2433,13 +2434,6 @@ const wazuhOwnerEvidencePreflightBoundaries = [
'not_authorization=true',
] as const
-const wazuhManagedHostCoverageSummary = [
- { key: 'scope', value: '6', icon: Server, tone: 'warn' },
- { key: 'directActive', value: '2', icon: Activity, tone: 'warn' },
- { key: 'coverageGap', value: '4', icon: FileWarning, tone: 'locked' },
- { key: 'registry', value: '0', icon: Lock, tone: 'locked' },
-] as const
-
const wazuhManagedHostCoverageItems: WazuhManagedHostCoverageItem[] = [
{ key: 'registryTruth', check: 'HC-1', state: '接受 0', icon: Lock, tone: 'locked' },
{ key: 'coreTransport', check: 'HC-2', state: '2 有連線', icon: Activity, tone: 'warn' },
@@ -9481,6 +9475,74 @@ function IwoooSWazuhOwnerEvidencePreflightBoard() {
function IwoooSWazuhManagedHostCoverageBoard() {
const t = useTranslations('iwooos.wazuhManagedHostCoverage')
const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const }
+ const [data, setData] = useState
{item.node_id}
+
+ {t('matrixAcceptedLabel')}:0
+
+ + {item.readback_status_label} +
++ {t('matrixNextGateLabel')}:{item.next_gate_label} +
+
+ {item.next_gate}
+
+
+ operator_interpretation: string[]
+ forbidden_completion_claims: string[]
+ forbidden_actions: string[]
+ boundary_markers: string[]
+ boundaries: Record
+ no_false_green_rules: string[]
+}
+
export interface IwoooSSecurityControlCoverageDomain {
domain_id:
| 'high_value_asset_control'
@@ -508,6 +558,11 @@ export const apiClient = {
return handleResponse(res)
},
+ async getIwoooSWazuhManagedHostCoverage() {
+ const res = await fetch(`${API_BASE_URL}/iwooos/wazuh-managed-host-coverage`, { 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/docs/LOGBOOK.md b/docs/LOGBOOK.md
index 3de18dad..eff84b1a 100644
--- a/docs/LOGBOOK.md
+++ b/docs/LOGBOOK.md
@@ -47036,6 +47036,45 @@ production browser smoke:
- `P0-02` owner-provided redacted evidence intake:開始接收六條 lane 的脫敏 evidence refs,但仍先維持 request / received / accepted / runtime 0,直到 reviewer validation 成立。
- `P0-03` Wazuh manager registry accepted:只讀交叉驗收所有 expected host / product / agent scope,不把 Dashboard 可開、API 200 或前台 lane 可見當作全主機納管恢復。
+## 2026-06-27 — 14:40 IwoooS Wazuh 受管主機覆蓋 API / 前台讀回本地完成
+
+**時間與來源**:
+- 2026-06-27 14:26-14:40 Asia/Taipei。
+- 來源:`docs/security/wazuh-managed-host-coverage-gate.snapshot.json`、`scripts/security/wazuh-managed-host-coverage-gate.py`、本地 API / 前台 / guard 驗證。
+
+**完成內容**:
+- 新增 `GET /api/v1/iwooos/wazuh-managed-host-coverage`,以公開別名回傳 Wazuh 受管主機覆蓋矩陣、manager registry 接受數、缺口數、必要驗收證據與 0 / false 邊界。
+- `/zh-TW/iwooos` 的 Wazuh 主機納管覆蓋卡改為正式 API 讀回優先,並新增只讀 API 狀態、公開別名主機矩陣與下一個 Gate。
+- `scripts/security/security-mirror-progress-guard.py` 納入 API route、service schema、client method、public-safe 測試、前台矩陣與 no-false-green marker。
+- `zh-TW` 與目前鏡像訊息都維持繁體中文;未加入工作視窗對話、個人 namespace、內網位址或 secret。
+
+**本地驗證結果**:
+- `python3 -m py_compile apps/api/src/services/iwooos_wazuh_managed_host_coverage.py apps/api/src/api/v1/iwooos.py scripts/security/security-mirror-progress-guard.py`:通過。
+- `node -e "JSON.parse(...zh-TW.json); JSON.parse(...en.json)"`:通過。
+- `DATABASE_URL=sqlite:///test.db python3.11 -m pytest apps/api/tests/test_iwooos_wazuh_managed_host_coverage.py apps/api/tests/test_iwooos_runtime_security_readback.py apps/api/tests/test_iwooos_security_control_coverage.py apps/api/tests/test_iwooos_wazuh_api.py apps/api/tests/test_iwooos_owner_evidence_intake_preflight.py -q`:`21 passed`。
+- `python3 scripts/security/wazuh-managed-host-coverage-gate.py --root .`:`WAZUH_MANAGED_HOST_COVERAGE_GATE_OK scope=6 direct_active=2 no_transport=1 ssh_blocked=3 registry=0 runtime_gate=0`。
+- `python3 scripts/security/iwooos-frontend-display-redaction-guard.py --root .`:`IWOOOS_FRONTEND_DISPLAY_REDACTION_GUARD_OK`。
+- `python3 scripts/security/security-mirror-progress-guard.py --root .`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。
+- `pnpm --dir apps/web typecheck`:通過。
+
+**完成度與同步狀態**:
+- 本段「Wazuh 受管主機覆蓋 API / 前台讀回」本地:`0% -> 85%`。尚待 commit、push、CD、production API readback 與 desktop / mobile browser smoke。
+- IwoooS 整體:暫不拉高,仍保守維持 `67%`,等正式讀回完成後再調整。
+- Wazuh manager registry accepted:仍為 `0`;此段只建立可驗證讀回與前台 no-false-green,不代表所有主機已納管或 Wazuh 已修復完成。
+
+**仍維持 0 / false**:
+- `manager_registry_accepted_count=0`、`required_evidence_accepted_count=0`、`live_metadata_env_enabled_count=0`、`runtime_gate_count=0`、`active_response_authorized_count=0`、`host_write_authorized_count=0`、`agent_reenroll_authorized_count=0`、`agent_restart_authorized_count=0`。
+- `runtime_execution_authorized=false`、`wazuh_api_live_query_authorized=false`、`wazuh_active_response_authorized=false`、`wazuh_agent_reenroll_authorized=false`、`wazuh_agent_restart_authorized=false`、`wazuh_manager_restart_authorized=false`、`host_write_authorized=false`、`kali_active_scan_authorized=false`、`secret_value_collection_allowed=false`、`not_authorization=true`。
+
+**做過的命令類型**:
+- 寫入:repo API / test / frontend / i18n / guard / LOGBOOK。
+- 只讀:git fetch / diff、repo snapshot 驗證、本地測試與型別檢查。
+- 未做:沒有 host / Docker / systemd / Nginx / firewall / K8s / DB / Wazuh runtime 寫操作;沒有讀 secret 明文;沒有重新註冊 agent;沒有 Wazuh restart;沒有 Wazuh active response;沒有 Kali active scan;沒有 force push。
+
+**下一步**:
+- commit / push 到 Gitea 後等待 CD,正式驗證 `GET /api/v1/iwooos/wazuh-managed-host-coverage` 與 `/zh-TW/iwooos` desktop / mobile。
+- 若 production 讀回通過,再更新 deploy marker、Gitea runs、overflow、forbidden hits 與 IwoooS 完成度。
+
## 2026-06-27 — 14:25 IwoooS rollout risk 前台與 guard 更新完成
**時間與來源**:
diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py
index 167214c2..8ebf073f 100755
--- a/scripts/security/security-mirror-progress-guard.py
+++ b/scripts/security/security-mirror-progress-guard.py
@@ -335,6 +335,16 @@ def validate(root: Path) -> None:
platform_operator_service = (
root / "apps" / "api" / "src" / "services" / "platform_operator_service.py"
).read_text(encoding="utf-8")
+ iwooos_api_router = (root / "apps" / "api" / "src" / "api" / "v1" / "iwooos.py").read_text(
+ encoding="utf-8"
+ )
+ iwooos_api_client = (root / "apps" / "web" / "src" / "lib" / "api-client.ts").read_text(encoding="utf-8")
+ iwooos_wazuh_managed_host_coverage_service = (
+ root / "apps" / "api" / "src" / "services" / "iwooos_wazuh_managed_host_coverage.py"
+ ).read_text(encoding="utf-8")
+ iwooos_wazuh_managed_host_coverage_test = (
+ root / "apps" / "api" / "tests" / "test_iwooos_wazuh_managed_host_coverage.py"
+ ).read_text(encoding="utf-8")
tenants_api_contract = (
root / "apps" / "api" / "src" / "api" / "v1" / "platform" / "tenants.py"
).read_text(encoding="utf-8")
@@ -29513,6 +29523,14 @@ def validate(root: Path) -> None:
json.dumps(web_messages_en["iwooos"], ensure_ascii=False),
]
)
+ wazuh_managed_host_coverage_source_text = "\n".join(
+ [
+ iwooos_api_router,
+ iwooos_api_client,
+ iwooos_wazuh_managed_host_coverage_service,
+ iwooos_wazuh_managed_host_coverage_test,
+ ]
+ )
for expected in [
"iwooos-wazuh-owner-evidence-preflight-board",
"wazuhOwnerEvidencePreflight",
@@ -29526,11 +29544,33 @@ def validate(root: Path) -> None:
"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",
+ "iwooos-wazuh-managed-host-coverage-matrix",
"wazuhManagedHostCoverage",
+ "getIwoooSWazuhManagedHostCoverage",
+ "apiClient.getIwoooSWazuhManagedHostCoverage",
+ "Wazuh 主機覆蓋只讀 API 已接上",
"wazuh_managed_host_coverage_manager_registry_accepted_count=0",
"wazuh_managed_host_coverage_runtime_gate_count=0",
]:
assert_text_contains("iwooos_frontend_product_text.wazuh_managed_host_coverage", frontend_product_text, expected)
+ for expected in [
+ "/api/v1/iwooos/wazuh-managed-host-coverage",
+ "iwooos_wazuh_managed_host_coverage_readback_v1",
+ "test_iwooos_wazuh_managed_host_coverage_api_is_public_safe",
+ "managed_core_node_a",
+ "manager_registry_cross_check",
+ "wazuh_managed_host_coverage_host_scope_matrix_count=6",
+ "wazuh_managed_host_coverage_manager_registry_accepted_count=0",
+ "wazuh_managed_host_coverage_manager_registry_gap_count=6",
+ "wazuh_managed_host_coverage_required_evidence_accepted_count=0",
+ "wazuh_agent_reenroll_authorized=false",
+ "wazuh_agent_restart_authorized=false",
+ ]:
+ assert_text_contains(
+ "iwooos_wazuh_managed_host_coverage_source",
+ wazuh_managed_host_coverage_source_text,
+ expected,
+ )
for forbidden in [
"工作視窗",
"內部對話",