feat(iwooos): expose wazuh managed host coverage readback
Some checks failed
Code Review / ai-code-review (push) Successful in 17s
CD Pipeline / tests (push) Successful in 1m39s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-27 14:43:09 +08:00
parent 9dffbf8100
commit 4ed96a83a5
9 changed files with 666 additions and 11 deletions

View File

@@ -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")