From fccd8874fce5c5b6dc29d806c57577bdeef73403 Mon Sep 17 00:00:00 2001
From: Your Name
Date: Sat, 27 Jun 2026 11:41:09 +0800
Subject: [PATCH] feat(iwooos): expose Wazuh owner evidence preflight
---
apps/api/src/api/v1/iwooos.py | 31 ++
.../iwooos_runtime_security_readback.py | 63 ++++-
.../iwooos_wazuh_owner_evidence_preflight.py | 264 ++++++++++++++++++
.../test_iwooos_runtime_security_readback.py | 59 +++-
apps/web/messages/en.json | 24 +-
apps/web/messages/zh-TW.json | 24 +-
apps/web/src/app/[locale]/iwooos/page.tsx | 125 ++++++++-
apps/web/src/lib/api-client.ts | 63 +++++
.../security-mirror-progress-guard.py | 5 +-
9 files changed, 635 insertions(+), 23 deletions(-)
create mode 100644 apps/api/src/services/iwooos_wazuh_owner_evidence_preflight.py
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 (
+
+
+ {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",