feat(iwooos): add runtime security readback board
Some checks failed
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m44s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Successful in 5m14s
CD Pipeline / post-deploy-checks (push) Successful in 1m39s

This commit is contained in:
Your Name
2026-06-26 17:48:33 +08:00
parent 61ff9e8083
commit 1d4b3df5e4
9 changed files with 897 additions and 1 deletions

View File

@@ -7,15 +7,22 @@ Wazuh 接線採用只讀 metadata 模式:預設關閉、不保存 raw payload
from __future__ import annotations
import asyncio
import json
import os
from base64 import b64encode
from typing import Any
from urllib.parse import urljoin, urlparse
import httpx
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException, status
from fastapi.responses import JSONResponse
from src.services.iwooos_runtime_security_readback import (
load_latest_iwooos_runtime_security_readback,
)
from src.services.public_redaction import redact_public_lan_topology
router = APIRouter(tags=["IwoooS Security"])
REQUEST_TIMEOUT_SECONDS = 5.0
@@ -198,3 +205,30 @@ async def get_iwooos_wazuh_readonly_status_compat() -> JSONResponse:
@router.get("/api/v1/iwooos/wazuh")
async def get_iwooos_wazuh_readonly_status_v1() -> JSONResponse:
return await _wazuh_readonly_status()
@router.get(
"/api/v1/iwooos/runtime-security-readback",
response_model=dict[str, Any],
summary="取得 IwoooS runtime security readback",
description=(
"讀取最新已提交的 IwoooS 資安只讀快照,彙總 Wazuh、Kali、SOC/SIEM、"
"告警可讀性、owner dispatch 與外部入侵防護 Gate。此端點不呼叫 Wazuh / Kali / "
"主機 / Docker / Nginx / firewall / Telegram不收集 secret不授權 runtime 寫入。"
),
)
async def get_iwooos_runtime_security_readback() -> dict[str, Any]:
"""回傳 IwoooS 資安 runtime readback 只讀總板。"""
try:
payload = await asyncio.to_thread(load_latest_iwooos_runtime_security_readback)
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 runtime security readback 無效:{exc}",
) from exc

View File

@@ -0,0 +1,322 @@
"""
IwoooS runtime security readback.
Loads committed security snapshots and exposes a public-safe, read-only runtime
security board. This module never queries Wazuh, Kali, hosts, Docker, Nginx,
firewalls, databases, Telegram, or secrets.
"""
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_FILES = {
"owner_gap": "s4-9-owner-response-gap-audit.snapshot.json",
"wazuh_coverage": "wazuh-managed-host-coverage-gate.snapshot.json",
"wazuh_runtime": "wazuh-agent-visibility-runtime-gate.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",
"owner_dispatch": "monitoring-owner-request-draft.snapshot.json",
"intrusion_prevention": "external-host-intrusion-prevention-control.snapshot.json",
}
_EXPECTED_SCHEMAS = {
"owner_gap": "s4_9_owner_response_gap_audit_v1",
"wazuh_coverage": "wazuh_managed_host_coverage_gate_v1",
"wazuh_runtime": "wazuh_agent_visibility_runtime_gate_v1",
"kali_status": "kali_integration_status_v1",
"soc_control": "soc_siem_kali_wazuh_integration_control_v1",
"alert_readability": "telegram_alert_readability_guard_v1",
"owner_dispatch": "monitoring_owner_request_draft_v1",
"intrusion_prevention": "external_host_intrusion_prevention_control_v1",
}
_FALSE_BOUNDARY_KEYS = {
"active_scan_authorized",
"alertmanager_reload_authorized",
"auto_block_authorized",
"credentialed_scan_authorized",
"firewall_change_authorized",
"host_write_authorized",
"kali_execute_authorized",
"kali_scan_authorized",
"nginx_reload_authorized",
"production_write_authorized",
"runtime_execution_authorized",
"runtime_gate_open",
"secret_value_collection_allowed",
"telegram_send_authorized",
"wazuh_active_response_authorized",
"wazuh_api_live_query_authorized",
}
def load_latest_iwooos_runtime_security_readback(
security_dir: Path | None = None,
) -> dict[str, Any]:
"""Load and normalize the current IwoooS runtime security readback."""
directory = security_dir or _DEFAULT_SECURITY_DIR
snapshots = {key: _load_snapshot(directory, key, filename) for key, filename in _SNAPSHOT_FILES.items()}
_require_runtime_boundaries(snapshots)
owner_gap_summary = _summary(snapshots["owner_gap"])
wazuh_summary = _summary(snapshots["wazuh_coverage"])
soc_summary = _summary(snapshots["soc_control"])
alert_summary = _summary(snapshots["alert_readability"])
dispatch_summary = _summary(snapshots["owner_dispatch"])
intrusion_summary = _summary(snapshots["intrusion_prevention"])
source_refs = [f"docs/security/{filename}" for filename in _SNAPSHOT_FILES.values()]
runtime_gate_count = _max_summary_count(
snapshots,
"runtime_gate_count",
"active_response_authorized_count",
"kali_active_scan_authorized_count",
"telegram_send_authorized_count",
)
return {
"schema_version": "iwooos_runtime_security_readback_v1",
"status": "blocked_waiting_owner_evidence_and_runtime_gates",
"mode": "committed_snapshot_readback_only_no_runtime_query",
"source_refs": source_refs,
"summary": {
"source_snapshot_count": len(source_refs),
"p0_lane_count": 6,
"control_plane_visibility_percent": _average_percent(
soc_summary.get("coverage_percent_after_soc_integration_control"),
intrusion_summary.get("coverage_percent_after_prevention_control"),
_alert_contract_percent(alert_summary),
),
"actual_runtime_acceptance_percent": 0,
"owner_response_received_count": _int(owner_gap_summary.get("owner_response_received_count")),
"owner_response_accepted_count": _int(owner_gap_summary.get("owner_response_accepted_count")),
"redacted_evidence_refs_received_count": 0,
"request_sent_count": _int(dispatch_summary.get("request_sent_count")),
"wazuh_expected_host_scope_count": _int(wazuh_summary.get("expected_host_scope_count")),
"wazuh_manager_registry_accepted_count": _int(wazuh_summary.get("manager_registry_accepted_count")),
"wazuh_transport_observed_count": _int(wazuh_summary.get("manager_transport_established_connection_count")),
"wazuh_dashboard_api_degraded_observed_count": _int(
wazuh_summary.get("dashboard_api_degraded_observed_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")),
"alert_formatter_contract_marker_count": _int(alert_summary.get("source_formatter_marker_count")),
"alert_receipt_runtime_send_count": _int(alert_summary.get("telegram_send_authorized_count")),
"intrusion_prevention_candidate_count": _int(intrusion_summary.get("urgent_prevention_candidate_count")),
"runtime_gate_count": runtime_gate_count,
},
"lanes": [
_lane(
"wazuh_registry",
"blocked_waiting_manager_registry",
0,
"locked",
"manager_registry_cross_check",
{
"expected_hosts": wazuh_summary.get("expected_host_scope_count", 0),
"transport_observed": wazuh_summary.get("manager_transport_established_connection_count", 0),
"registry_accepted": wazuh_summary.get("manager_registry_accepted_count", 0),
},
["docs/security/wazuh-managed-host-coverage-gate.snapshot.json"],
),
_lane(
"wazuh_dashboard_api",
"degraded_api_connection_not_green",
0,
"warn",
"dashboard_api_rbac_tls_repair_readback",
{
"dashboard_api_degraded": wazuh_summary.get("dashboard_api_degraded_observed_count", 0),
"runtime_gate": wazuh_summary.get("runtime_gate_count", 0),
"accepted_evidence": _accepted_evidence_count(snapshots["wazuh_runtime"]),
},
[
"docs/security/wazuh-managed-host-coverage-gate.snapshot.json",
"docs/security/wazuh-agent-visibility-runtime-gate.snapshot.json",
],
),
_lane(
"kali_intake",
snapshots["kali_status"].get("status", "blocked_waiting_kali_scope"),
0,
"locked",
"kali_scope_and_finding_envelope_accepted",
{
"active_scan_authorized": soc_summary.get("kali_active_scan_authorized_count", 0),
"execute_authorized": soc_summary.get("kali_execute_authorized_count", 0),
"finding_envelope_accepted": soc_summary.get("kali_finding_envelope_accepted_count", 0),
},
[
"docs/security/kali-integration-status.snapshot.json",
"docs/security/soc-siem-kali-wazuh-integration-control.snapshot.json",
],
),
_lane(
"alert_readability",
"contract_ready_no_send_receipt",
_alert_contract_percent(alert_summary),
"warn",
"alert_route_receipt_available",
{
"formatter_markers": alert_summary.get("source_formatter_marker_count", 0),
"required_markers": alert_summary.get("required_output_marker_count", 0),
"telegram_send": alert_summary.get("telegram_send_authorized_count", 0),
},
["docs/security/telegram-alert-readability-guard.snapshot.json"],
),
_lane(
"owner_dispatch",
snapshots["owner_dispatch"].get("status", "owner_request_draft_ready_not_dispatched"),
0,
"locked",
"owner_response_packet_delivery",
{
"request_drafts": dispatch_summary.get("request_draft_count", 0),
"request_sent": dispatch_summary.get("request_sent_count", 0),
"owner_accepted": dispatch_summary.get("owner_response_accepted_count", 0),
},
["docs/security/monitoring-owner-request-draft.snapshot.json"],
),
_lane(
"intrusion_prevention",
"candidate_only_no_runtime_containment",
_int(intrusion_summary.get("coverage_percent_after_prevention_control")),
"warn",
"redacted_evidence_refs_and_maintenance_window",
{
"urgent_candidates": intrusion_summary.get("urgent_prevention_candidate_count", 0),
"evidence_received": intrusion_summary.get("evidence_ref_received_count", 0),
"containment_accepted": intrusion_summary.get("containment_decision_accepted_count", 0),
},
["docs/security/external-host-intrusion-prevention-control.snapshot.json"],
),
],
"boundaries": {
"active_response_authorized": False,
"active_scan_authorized": False,
"action_buttons_allowed": False,
"host_write_authorized": False,
"kali_execute_authorized": False,
"nginx_reload_authorized": False,
"raw_payload_storage_allowed": False,
"runtime_execution_authorized": False,
"secret_value_collection_allowed": False,
"telegram_send_authorized": False,
"wazuh_active_response_authorized": False,
"wazuh_api_live_query_authorized": False,
"workflow_modification_authorized": False,
"not_authorization": True,
},
"no_false_green_rules": [
"dashboard_route_200_is_not_wazuh_registry_recovery",
"transport_count_is_not_full_host_management",
"ui_visible_is_not_runtime_authorization",
"owner_request_draft_is_not_owner_acceptance",
"kali_health_is_not_active_scan_authorization",
"alert_format_contract_is_not_telegram_send_receipt",
],
}
def _load_snapshot(directory: Path, key: str, filename: str) -> dict[str, Any]:
path = directory / filename
if not path.is_file():
raise FileNotFoundError(f"{path}: security snapshot not found")
with path.open(encoding="utf-8") as handle:
payload = json.load(handle)
if not isinstance(payload, dict):
raise ValueError(f"{path}: expected JSON object")
expected_schema = _EXPECTED_SCHEMAS[key]
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 _average_percent(*values: Any) -> int:
percents = [_int(value) for value in values if isinstance(value, int)]
return int(round(sum(percents) / len(percents))) if percents else 0
def _alert_contract_percent(summary: dict[str, Any]) -> int:
source_markers = _int(summary.get("source_formatter_marker_count"))
required_markers = max(1, _int(summary.get("required_output_marker_count")))
return min(100, int(round((source_markers / required_markers) * 100)))
def _accepted_evidence_count(payload: dict[str, Any]) -> int:
evidence = payload.get("required_evidence_before_green")
if not isinstance(evidence, list):
return 0
return sum(1 for item in evidence if isinstance(item, dict) and item.get("accepted") is True)
def _lane(
lane_id: str,
status_text: Any,
completion_percent: int,
tone: str,
next_gate: str,
metrics: dict[str, Any],
source_refs: list[str],
) -> dict[str, Any]:
return {
"lane_id": lane_id,
"status": str(status_text),
"completion_percent": completion_percent,
"tone": tone,
"next_gate": next_gate,
"metrics": {key: _int(value) for key, value in metrics.items()},
"source_refs": source_refs,
}
def _max_summary_count(
snapshots: dict[str, dict[str, Any]],
*keys: str,
) -> int:
return max((_int(_summary(payload).get(key)) for payload in snapshots.values() for key in keys), default=0)
def _require_runtime_boundaries(snapshots: dict[str, dict[str, Any]]) -> None:
for name, payload in snapshots.items():
summary = _summary(payload)
if _int(summary.get("runtime_gate_count")) != 0:
raise ValueError(f"{name}: runtime_gate_count must remain 0")
for key in (
"owner_response_accepted_count",
"wazuh_active_response_enabled_count",
"active_response_enabled_count",
"active_scan_authorized_count",
"kali_active_scan_authorized_count",
"telegram_send_authorized_count",
"host_write_authorized_count",
"secret_value_collection_allowed_count",
):
if key in summary and _int(summary.get(key)) != 0:
raise ValueError(f"{name}: {key} must remain 0")
boundaries = payload.get("execution_boundaries")
if isinstance(boundaries, dict):
invalid = sorted(
key for key in _FALSE_BOUNDARY_KEYS if key in boundaries and boundaries.get(key) is not False
)
if invalid:
raise ValueError(f"{name}: execution boundaries must remain false: {invalid}")

View File

@@ -35,3 +35,8 @@ def default_evaluations_dir(anchor: Path) -> Path:
def default_operations_dir(anchor: Path) -> Path:
"""Resolve the default committed operations snapshot directory."""
return resolve_repo_root(anchor) / "docs" / "operations"
def default_security_dir(anchor: Path) -> Path:
"""Resolve the default committed security snapshot directory."""
return resolve_repo_root(anchor) / "docs" / "security"