Compare commits
1 Commits
codex/110-
...
codex/iwoo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e359886af |
@@ -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
|
||||
|
||||
322
apps/api/src/services/iwooos_runtime_security_readback.py
Normal file
322
apps/api/src/services/iwooos_runtime_security_readback.py
Normal 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}")
|
||||
@@ -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"
|
||||
|
||||
67
apps/api/tests/test_iwooos_runtime_security_readback.py
Normal file
67
apps/api/tests/test_iwooos_runtime_security_readback.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.api.v1.iwooos import router
|
||||
from src.services.iwooos_runtime_security_readback import (
|
||||
load_latest_iwooos_runtime_security_readback,
|
||||
)
|
||||
|
||||
|
||||
def _client() -> TestClient:
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def test_iwooos_runtime_security_readback_preserves_zero_runtime_gates() -> None:
|
||||
payload = load_latest_iwooos_runtime_security_readback()
|
||||
|
||||
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"] == 8
|
||||
assert payload["summary"]["p0_lane_count"] == 6
|
||||
assert payload["summary"]["runtime_gate_count"] == 0
|
||||
assert payload["summary"]["owner_response_received_count"] == 0
|
||||
assert payload["summary"]["owner_response_accepted_count"] == 0
|
||||
assert payload["summary"]["wazuh_manager_registry_accepted_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
|
||||
assert payload["boundaries"]["runtime_execution_authorized"] is False
|
||||
assert payload["boundaries"]["active_scan_authorized"] is False
|
||||
assert payload["boundaries"]["wazuh_active_response_authorized"] is False
|
||||
assert payload["boundaries"]["telegram_send_authorized"] is False
|
||||
|
||||
|
||||
def test_iwooos_runtime_security_readback_lanes_are_candidate_only() -> None:
|
||||
payload = load_latest_iwooos_runtime_security_readback()
|
||||
|
||||
lane_ids = {lane["lane_id"] for lane in payload["lanes"]}
|
||||
assert lane_ids == {
|
||||
"wazuh_registry",
|
||||
"wazuh_dashboard_api",
|
||||
"kali_intake",
|
||||
"alert_readability",
|
||||
"owner_dispatch",
|
||||
"intrusion_prevention",
|
||||
}
|
||||
assert all(lane["metrics"] for lane in payload["lanes"])
|
||||
assert all(lane["next_gate"] for lane in payload["lanes"])
|
||||
assert all(lane["source_refs"] for lane in payload["lanes"])
|
||||
assert any(lane["completion_percent"] > 0 for lane in payload["lanes"])
|
||||
assert all(lane["lane_id"] != "wazuh_registry" or lane["completion_percent"] == 0 for lane in payload["lanes"])
|
||||
|
||||
|
||||
def test_iwooos_runtime_security_readback_api_is_public_safe() -> None:
|
||||
response = _client().get("/api/v1/iwooos/runtime-security-readback")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["schema_version"] == "iwooos_runtime_security_readback_v1"
|
||||
assert data["summary"]["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
|
||||
assert "批准!繼續" not in response.text
|
||||
@@ -20151,6 +20151,75 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"runtimeSecurityReadback": {
|
||||
"eyebrow": "IwoooS Runtime 資安讀回",
|
||||
"title": "六條 P0 資安線先接到同一張讀回板",
|
||||
"subtitle": "這張板讀取後端彙整的只讀快照,集中顯示 Wazuh 清單驗收、儀表板 API、資安觀測節點 intake、告警可讀性、負責人送件與外部入侵防護;它不查 live Wazuh、不啟動掃描、不送訊息、不改主機。",
|
||||
"statusLabel": "讀回狀態",
|
||||
"statusDetail": "讀回成功只代表 IwoooS 能看見目前的證據邊界;runtime 寫入、主動回應、掃描、重啟、Nginx reload、workflow 修改與機密操作仍全部關閉。",
|
||||
"laneStatusLabel": "目前狀態",
|
||||
"laneNextGateLabel": "下一個 Gate",
|
||||
"emptyLanes": "尚未讀回 P0 線,維持阻擋狀態。",
|
||||
"boundaryTitle": "禁止誤判規則",
|
||||
"boundaryIntro": "以下規則由後端 payload 回傳;若讀回失敗,前端只顯示保守 fallback。任何一條都不能被當成 runtime 授權。",
|
||||
"status": {
|
||||
"loading": "正在讀取只讀資安總板",
|
||||
"failed": "只讀總板尚未部署或讀取失敗",
|
||||
"readbackReady": "只讀總板已讀回,但 runtime 閘門仍關閉"
|
||||
},
|
||||
"summary": {
|
||||
"controlPlane": {
|
||||
"label": "控制面可視",
|
||||
"detail": "框架、候選與證據線已接上,但不等於 runtime 完成。"
|
||||
},
|
||||
"runtimeAcceptance": {
|
||||
"label": "執行驗收",
|
||||
"detail": "實際 runtime acceptance 仍維持 0%。"
|
||||
},
|
||||
"wazuhRegistry": {
|
||||
"label": "Wazuh 清單",
|
||||
"detail": "管理器清單接受數仍為 0。"
|
||||
},
|
||||
"ownerAccepted": {
|
||||
"label": "負責人驗收",
|
||||
"detail": "收到 / 接受都必須由正式 owner response 證明。"
|
||||
},
|
||||
"kaliRuntime": {
|
||||
"label": "Kali 執行",
|
||||
"detail": "active scan / execute 都維持 0。"
|
||||
},
|
||||
"runtimeGate": {
|
||||
"label": "Runtime Gate",
|
||||
"detail": "任何修復、封鎖、重啟或 reload 都還沒開門。"
|
||||
}
|
||||
},
|
||||
"lanes": {
|
||||
"wazuh_registry": {
|
||||
"title": "Wazuh manager registry",
|
||||
"body": "必須拿到總數、在線、離線、從未連線與時間窗;transport 或儀表板可見不能代替驗收。"
|
||||
},
|
||||
"wazuh_dashboard_api": {
|
||||
"title": "Wazuh Dashboard API",
|
||||
"body": "API connection / API version 還要補 readback;index pattern 通過不能宣稱 Wazuh 全綠。"
|
||||
},
|
||||
"kali_intake": {
|
||||
"title": "資安觀測節點 intake",
|
||||
"body": "先收 health、scope、工具版本與 normalized finding envelope;active scan 與 /execute 仍需獨立批准。"
|
||||
},
|
||||
"alert_readability": {
|
||||
"title": "告警可讀性 receipt",
|
||||
"body": "格式合約已能阻擋 raw output,但 Telegram 實發、receipt 與修復結果仍未啟用。"
|
||||
},
|
||||
"owner_dispatch": {
|
||||
"title": "負責人送件",
|
||||
"body": "目前只有送件草案與欄位包;request sent、received、accepted 全都不可假性上修。"
|
||||
},
|
||||
"intrusion_prevention": {
|
||||
"title": "外部入侵防護",
|
||||
"body": "候選已按公開入口、SSH、firewall、主機 runtime、runner、secret、Wazuh 等域分類;圍堵仍只能是候選。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"wazuhLiveRouteReadback": {
|
||||
"eyebrow": "Wazuh 正式路由只讀讀回",
|
||||
"title": "正式站必須直接顯示 Wazuh 只讀路由狀態",
|
||||
|
||||
@@ -20151,6 +20151,75 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"runtimeSecurityReadback": {
|
||||
"eyebrow": "IwoooS Runtime 資安讀回",
|
||||
"title": "六條 P0 資安線先接到同一張讀回板",
|
||||
"subtitle": "這張板讀取後端彙整的只讀快照,集中顯示 Wazuh 清單驗收、儀表板 API、資安觀測節點 intake、告警可讀性、負責人送件與外部入侵防護;它不查 live Wazuh、不啟動掃描、不送訊息、不改主機。",
|
||||
"statusLabel": "讀回狀態",
|
||||
"statusDetail": "讀回成功只代表 IwoooS 能看見目前的證據邊界;runtime 寫入、主動回應、掃描、重啟、Nginx reload、workflow 修改與機密操作仍全部關閉。",
|
||||
"laneStatusLabel": "目前狀態",
|
||||
"laneNextGateLabel": "下一個 Gate",
|
||||
"emptyLanes": "尚未讀回 P0 線,維持阻擋狀態。",
|
||||
"boundaryTitle": "禁止誤判規則",
|
||||
"boundaryIntro": "以下規則由後端 payload 回傳;若讀回失敗,前端只顯示保守 fallback。任何一條都不能被當成 runtime 授權。",
|
||||
"status": {
|
||||
"loading": "正在讀取只讀資安總板",
|
||||
"failed": "只讀總板尚未部署或讀取失敗",
|
||||
"readbackReady": "只讀總板已讀回,但 runtime 閘門仍關閉"
|
||||
},
|
||||
"summary": {
|
||||
"controlPlane": {
|
||||
"label": "控制面可視",
|
||||
"detail": "框架、候選與證據線已接上,但不等於 runtime 完成。"
|
||||
},
|
||||
"runtimeAcceptance": {
|
||||
"label": "執行驗收",
|
||||
"detail": "實際 runtime acceptance 仍維持 0%。"
|
||||
},
|
||||
"wazuhRegistry": {
|
||||
"label": "Wazuh 清單",
|
||||
"detail": "管理器清單接受數仍為 0。"
|
||||
},
|
||||
"ownerAccepted": {
|
||||
"label": "負責人驗收",
|
||||
"detail": "收到 / 接受都必須由正式 owner response 證明。"
|
||||
},
|
||||
"kaliRuntime": {
|
||||
"label": "Kali 執行",
|
||||
"detail": "active scan / execute 都維持 0。"
|
||||
},
|
||||
"runtimeGate": {
|
||||
"label": "Runtime Gate",
|
||||
"detail": "任何修復、封鎖、重啟或 reload 都還沒開門。"
|
||||
}
|
||||
},
|
||||
"lanes": {
|
||||
"wazuh_registry": {
|
||||
"title": "Wazuh manager registry",
|
||||
"body": "必須拿到總數、在線、離線、從未連線與時間窗;transport 或儀表板可見不能代替驗收。"
|
||||
},
|
||||
"wazuh_dashboard_api": {
|
||||
"title": "Wazuh Dashboard API",
|
||||
"body": "API connection / API version 還要補 readback;index pattern 通過不能宣稱 Wazuh 全綠。"
|
||||
},
|
||||
"kali_intake": {
|
||||
"title": "資安觀測節點 intake",
|
||||
"body": "先收 health、scope、工具版本與 normalized finding envelope;active scan 與 /execute 仍需獨立批准。"
|
||||
},
|
||||
"alert_readability": {
|
||||
"title": "告警可讀性 receipt",
|
||||
"body": "格式合約已能阻擋 raw output,但 Telegram 實發、receipt 與修復結果仍未啟用。"
|
||||
},
|
||||
"owner_dispatch": {
|
||||
"title": "負責人送件",
|
||||
"body": "目前只有送件草案與欄位包;request sent、received、accepted 全都不可假性上修。"
|
||||
},
|
||||
"intrusion_prevention": {
|
||||
"title": "外部入侵防護",
|
||||
"body": "候選已按公開入口、SSH、firewall、主機 runtime、runner、secret、Wazuh 等域分類;圍堵仍只能是候選。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"wazuhLiveRouteReadback": {
|
||||
"eyebrow": "Wazuh 正式路由只讀讀回",
|
||||
"title": "正式站必須直接顯示 Wazuh 只讀路由狀態",
|
||||
|
||||
@@ -33,6 +33,7 @@ import Link from 'next/link'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { apiClient, type IwoooSRuntimeSecurityReadbackResponse } from '@/lib/api-client'
|
||||
|
||||
type PostureMetric = {
|
||||
key: string
|
||||
@@ -319,6 +320,13 @@ type WazuhReadonlyStatusResponse = {
|
||||
}
|
||||
}
|
||||
|
||||
type RuntimeSecurityReadbackSummaryItem = {
|
||||
key: 'controlPlane' | 'runtimeAcceptance' | 'wazuhRegistry' | 'ownerAccepted' | 'kaliRuntime' | 'runtimeGate'
|
||||
value: string
|
||||
icon: typeof ShieldCheck
|
||||
tone: 'steady' | 'warn' | 'locked'
|
||||
}
|
||||
|
||||
type SocSiemKaliWazuhIntegrationItem = {
|
||||
key: string
|
||||
check: string
|
||||
@@ -1143,6 +1151,15 @@ const progressIntegrityRibbonBoundaries = [
|
||||
'not_authorization=true',
|
||||
]
|
||||
|
||||
const runtimeSecurityReadbackFallbackRules = [
|
||||
'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',
|
||||
]
|
||||
|
||||
const firstScreenDepthLayers: FirstScreenDepthLayer[] = [
|
||||
{ key: 'visible', value: '4', icon: ShieldCheck, tone: 'steady' },
|
||||
{ key: 'advanced', value: '2', icon: SearchCheck, tone: 'warn', href: '#iwooos-decision-gate-visuals' },
|
||||
@@ -8037,6 +8054,233 @@ function IwoooSWazuhIntrusionReadbackBoard() {
|
||||
)
|
||||
}
|
||||
|
||||
function IwoooSRuntimeSecurityReadbackBoard() {
|
||||
const t = useTranslations('iwooos.runtimeSecurityReadback')
|
||||
const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const }
|
||||
const [data, setData] = useState<IwoooSRuntimeSecurityReadbackResponse | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [failed, setFailed] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
|
||||
async function loadReadback() {
|
||||
setLoading(true)
|
||||
setFailed(false)
|
||||
try {
|
||||
const payload = await apiClient.getIwoooSRuntimeSecurityReadback()
|
||||
if (mounted) {
|
||||
setData(payload)
|
||||
}
|
||||
} catch {
|
||||
if (mounted) {
|
||||
setData(null)
|
||||
setFailed(true)
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadReadback()
|
||||
return () => {
|
||||
mounted = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const summary = data?.summary
|
||||
const statusKey = loading ? 'loading' : failed || !data ? 'failed' : 'readbackReady'
|
||||
const statusTone: 'steady' | 'warn' | 'locked' = loading ? 'warn' : failed || !data ? 'warn' : 'locked'
|
||||
const summaryItems: RuntimeSecurityReadbackSummaryItem[] = [
|
||||
{
|
||||
key: 'controlPlane',
|
||||
value: summary ? `${summary.control_plane_visibility_percent}%` : '...',
|
||||
icon: ShieldCheck,
|
||||
tone: summary && summary.control_plane_visibility_percent > 0 ? 'steady' : 'warn',
|
||||
},
|
||||
{
|
||||
key: 'runtimeAcceptance',
|
||||
value: summary ? `${summary.actual_runtime_acceptance_percent}%` : '...',
|
||||
icon: Lock,
|
||||
tone: 'locked',
|
||||
},
|
||||
{
|
||||
key: 'wazuhRegistry',
|
||||
value: summary ? String(summary.wazuh_manager_registry_accepted_count) : '...',
|
||||
icon: Radar,
|
||||
tone: 'locked',
|
||||
},
|
||||
{
|
||||
key: 'ownerAccepted',
|
||||
value: summary ? `${summary.owner_response_accepted_count}/${summary.owner_response_received_count}` : '...',
|
||||
icon: ClipboardCheck,
|
||||
tone: 'locked',
|
||||
},
|
||||
{
|
||||
key: 'kaliRuntime',
|
||||
value: summary ? `${summary.kali_active_scan_authorized_count}/${summary.kali_execute_authorized_count}` : '...',
|
||||
icon: SearchCheck,
|
||||
tone: 'locked',
|
||||
},
|
||||
{
|
||||
key: 'runtimeGate',
|
||||
value: summary ? String(summary.runtime_gate_count) : '...',
|
||||
icon: Lock,
|
||||
tone: 'locked',
|
||||
},
|
||||
]
|
||||
const lanes = data?.lanes ?? []
|
||||
|
||||
return (
|
||||
<section
|
||||
style={{ marginBottom: 14, maxWidth: '100%', overflow: 'hidden' }}
|
||||
data-testid="iwooos-runtime-security-readback-board"
|
||||
>
|
||||
<div style={{ ...band, padding: 16, background: '#fbfcfb', borderColor: '#c9ded2' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 300px), 1fr))', gap: 14 }}>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#315f45', fontSize: 12, fontWeight: 700 }}>
|
||||
<ShieldCheck size={17} color="#2f7d48" />
|
||||
{t('eyebrow')}
|
||||
</div>
|
||||
<h2 style={{ fontSize: 17, margin: '8px 0 0', color: '#141413' }}>{t('title')}</h2>
|
||||
<p style={{ fontSize: 12, color: '#405f4f', margin: '6px 0 0', lineHeight: 1.55, ...textWrap }}>
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ border: '0.5px solid #d3e4d9', borderRadius: 8, padding: 12, background: '#fff', minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
|
||||
<span style={{ fontSize: 11, color: '#627466', fontWeight: 700 }}>{t('statusLabel')}</span>
|
||||
<ToneDot tone={statusTone} />
|
||||
</div>
|
||||
<div style={{ marginTop: 8, fontSize: 15, color: toneColors[statusTone], fontWeight: 700, ...textWrap }}>
|
||||
{t(`status.${statusKey}` as never)}
|
||||
</div>
|
||||
<p style={{ fontSize: 11, color: '#405f4f', lineHeight: 1.5, margin: '8px 0 0', ...textWrap }}>
|
||||
{t('statusDetail')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: 14,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 132px), 1fr))',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
{summaryItems.map(item => {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<div key={item.key} style={{ border: '0.5px solid #d3e4d9', borderRadius: 8, padding: 12, background: '#fff', minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
||||
<span style={{ fontSize: 11, color: '#627466', ...textWrap }}>{t(`summary.${item.key}.label` as never)}</span>
|
||||
<Icon size={16} color={toneColors[item.tone]} />
|
||||
</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 700, color: toneColors[item.tone], lineHeight: 1.1, marginTop: 8, ...textWrap }}>
|
||||
{item.value}
|
||||
</div>
|
||||
<p style={{ fontSize: 11, color: '#405f4f', lineHeight: 1.45, margin: '8px 0 0', ...textWrap }}>
|
||||
{t(`summary.${item.key}.detail` as never)}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: 14,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 220px), 1fr))',
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
{lanes.length === 0 ? (
|
||||
<div style={{ border: '0.5px solid #d3e4d9', borderRadius: 8, background: '#fff', padding: 13, color: '#405f4f', fontSize: 12 }}>
|
||||
{t('emptyLanes')}
|
||||
</div>
|
||||
) : lanes.map(lane => (
|
||||
<div
|
||||
key={lane.lane_id}
|
||||
style={{
|
||||
border: `0.5px solid ${lane.tone === 'steady' ? '#bddfc9' : lane.tone === 'warn' ? '#e6c8b8' : '#dad7ce'}`,
|
||||
borderRadius: 8,
|
||||
background: lane.tone === 'warn' ? '#fffaf7' : lane.tone === 'locked' ? '#f8f7f3' : '#fff',
|
||||
padding: 13,
|
||||
display: 'grid',
|
||||
gap: 10,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
||||
<div style={{ fontSize: 12, color: toneColors[lane.tone], fontWeight: 700, ...textWrap }}>
|
||||
{t(`lanes.${lane.lane_id}.title` as never)}
|
||||
</div>
|
||||
<span style={{ fontSize: 12, color: toneColors[lane.tone], fontWeight: 700 }}>{lane.completion_percent}%</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#405f4f', lineHeight: 1.45, ...textWrap }}>
|
||||
{t(`lanes.${lane.lane_id}.body` as never)}
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 6 }}>
|
||||
<div style={{ border: '0.5px solid #e1e8df', borderRadius: 8, background: '#fff', padding: 8, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 10, color: '#627466', fontWeight: 700 }}>{t('laneStatusLabel')}</div>
|
||||
<div style={{ marginTop: 5, fontSize: 11, color: '#141413', fontWeight: 700, ...textWrap }}>{lane.status}</div>
|
||||
</div>
|
||||
<div style={{ border: '0.5px solid #e1e8df', borderRadius: 8, background: '#fff', padding: 8, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 10, color: '#627466', fontWeight: 700 }}>{t('laneNextGateLabel')}</div>
|
||||
<div style={{ marginTop: 5, fontSize: 11, color: '#141413', fontWeight: 700, ...textWrap }}>{lane.next_gate}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<details
|
||||
data-testid="iwooos-runtime-security-readback-boundaries"
|
||||
style={{
|
||||
marginTop: 12,
|
||||
border: '0.5px solid #d3e4d9',
|
||||
borderRadius: 8,
|
||||
background: '#fff',
|
||||
padding: '8px 10px',
|
||||
}}
|
||||
>
|
||||
<summary style={{ cursor: 'pointer', fontSize: 12, fontWeight: 700, color: '#315f45' }}>
|
||||
{t('boundaryTitle')}
|
||||
</summary>
|
||||
<p style={{ fontSize: 11, color: '#405f4f', lineHeight: 1.5, margin: '8px 0', ...textWrap }}>
|
||||
{t('boundaryIntro')}
|
||||
</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))', gap: 6 }}>
|
||||
{(data?.no_false_green_rules ?? runtimeSecurityReadbackFallbackRules).map(item => (
|
||||
<code
|
||||
key={item}
|
||||
style={{
|
||||
border: '0.5px solid #d3e4d9',
|
||||
borderRadius: 8,
|
||||
padding: '6px 8px',
|
||||
color: '#405f4f',
|
||||
fontSize: 11,
|
||||
lineHeight: 1.4,
|
||||
background: '#f7fbf8',
|
||||
overflowWrap: 'anywhere',
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function wazuhReadonlyStatusKey(httpStatus: number | null, data: WazuhReadonlyStatusResponse | null) {
|
||||
if (httpStatus === 404) return 'predeploy'
|
||||
if (!data) return 'unavailable'
|
||||
@@ -21933,6 +22177,7 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) {
|
||||
<IwoooSAgentBountySecurityOnboardingBoard />
|
||||
<IwoooSRolloutRiskReadOnlyBoard />
|
||||
<IwoooSWazuhIntrusionReadbackBoard />
|
||||
<IwoooSRuntimeSecurityReadbackBoard />
|
||||
<IwoooSWazuhLiveRouteReadbackBoard />
|
||||
<IwoooSWazuhReleaseGateBoard />
|
||||
<IwoooSWazuhOwnerEvidencePreflightBoard />
|
||||
|
||||
@@ -95,6 +95,55 @@ export class ApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export type IwoooSRuntimeSecurityReadbackTone = 'steady' | 'warn' | 'locked'
|
||||
|
||||
export interface IwoooSRuntimeSecurityReadbackLane {
|
||||
lane_id:
|
||||
| 'wazuh_registry'
|
||||
| 'wazuh_dashboard_api'
|
||||
| 'kali_intake'
|
||||
| 'alert_readability'
|
||||
| 'owner_dispatch'
|
||||
| 'intrusion_prevention'
|
||||
status: string
|
||||
completion_percent: number
|
||||
tone: IwoooSRuntimeSecurityReadbackTone
|
||||
next_gate: string
|
||||
metrics: Record<string, number>
|
||||
source_refs: string[]
|
||||
}
|
||||
|
||||
export interface IwoooSRuntimeSecurityReadbackResponse {
|
||||
schema_version: 'iwooos_runtime_security_readback_v1'
|
||||
status: string
|
||||
mode: string
|
||||
source_refs: string[]
|
||||
summary: {
|
||||
source_snapshot_count: number
|
||||
p0_lane_count: number
|
||||
control_plane_visibility_percent: number
|
||||
actual_runtime_acceptance_percent: number
|
||||
owner_response_received_count: number
|
||||
owner_response_accepted_count: number
|
||||
redacted_evidence_refs_received_count: number
|
||||
request_sent_count: number
|
||||
wazuh_expected_host_scope_count: number
|
||||
wazuh_manager_registry_accepted_count: number
|
||||
wazuh_transport_observed_count: number
|
||||
wazuh_dashboard_api_degraded_observed_count: number
|
||||
kali_active_scan_authorized_count: number
|
||||
kali_execute_authorized_count: number
|
||||
kali_finding_envelope_accepted_count: number
|
||||
alert_formatter_contract_marker_count: number
|
||||
alert_receipt_runtime_send_count: number
|
||||
intrusion_prevention_candidate_count: number
|
||||
runtime_gate_count: number
|
||||
}
|
||||
lanes: IwoooSRuntimeSecurityReadbackLane[]
|
||||
boundaries: Record<string, boolean>
|
||||
no_false_green_rules: string[]
|
||||
}
|
||||
|
||||
async function handleResponse<T>(response: Response): Promise<T> {
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}))
|
||||
@@ -128,6 +177,11 @@ export const apiClient = {
|
||||
}>(res)
|
||||
},
|
||||
|
||||
async getIwoooSRuntimeSecurityReadback() {
|
||||
const res = await fetch(`${API_BASE_URL}/iwooos/runtime-security-readback`, { cache: 'no-store' })
|
||||
return handleResponse<IwoooSRuntimeSecurityReadbackResponse>(res)
|
||||
},
|
||||
|
||||
// Agent
|
||||
async getAgentStatus() {
|
||||
const res = await fetch(`${API_BASE_URL}/agent/status`)
|
||||
|
||||
@@ -1,3 +1,34 @@
|
||||
## 2026-06-26|IwoooS Runtime 資安讀回總板:Wazuh / Kali / 告警 / owner dispatch 六條 P0 線接上只讀 API 與前台
|
||||
|
||||
**背景**:IwoooS 資安工作不能停在文件、靜態卡片或截圖;Wazuh、資安觀測節點、告警可讀性、owner dispatch、外部入侵防護與 runtime gate 必須能被前台讀回,同時不得把 UI 可見、route 200、transport count 或一般操作批准誤判成 runtime 授權。
|
||||
|
||||
**完成**:
|
||||
- 新增 `iwooos_runtime_security_readback_v1` 後端只讀 service,彙整 8 份已提交資安 snapshot:S4.9 owner gap、Wazuh managed host coverage、Wazuh agent visibility runtime gate、Kali integration、SOC/SIEM/Kali/Wazuh integration、Telegram alert readability、monitoring owner request draft、external host intrusion prevention control。
|
||||
- 新增 `GET /api/v1/iwooos/runtime-security-readback`;端點只讀 repo snapshot,不呼叫 Wazuh live API、不呼叫 Kali、不 SSH、不碰 Docker / Nginx / firewall / Telegram / secret。
|
||||
- 前台 `/zh-TW/iwooos` 新增 `IwoooS Runtime 資安讀回` 總板,顯示六條 P0 線:`wazuh_registry`、`wazuh_dashboard_api`、`kali_intake`、`alert_readability`、`owner_dispatch`、`intrusion_prevention`。
|
||||
- `zh-TW` 與目前鏡像 `en` 都新增繁中產品文案;新增 API client 型別與 `getIwoooSRuntimeSecurityReadback()`。
|
||||
|
||||
**只讀驗證結果**:
|
||||
- `pytest apps/api/tests/test_iwooos_runtime_security_readback.py apps/api/tests/test_iwooos_wazuh_api.py -q`:`9 passed`。
|
||||
- `python3.11 -m py_compile apps/api/src/api/v1/iwooos.py apps/api/src/services/iwooos_runtime_security_readback.py apps/api/src/services/snapshot_paths.py`:通過。
|
||||
- `pnpm --dir apps/web typecheck`:通過;本輪先以離線 pnpm store 補最小依賴連結,未下載新套件。
|
||||
- `python3 scripts/security/security-mirror-progress-guard.py --root .`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。
|
||||
- `python3 scripts/security/source-control-owner-response-guard.py --root .`:`SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK`。
|
||||
- `python3 scripts/ops/doc-secrets-sanity-check.py docs/security docs/templates apps/web/messages docs/LOGBOOK.md 'apps/web/src/app/[locale]/iwooos/page.tsx' apps/web/src/lib/api-client.ts apps/api/src/api/v1/iwooos.py apps/api/src/services/iwooos_runtime_security_readback.py`:`DOC_SECRET_SANITY_OK scanned_files=274`。
|
||||
- `git diff --check`:通過。
|
||||
- readback summary:`control_plane_visibility_percent=84`、`actual_runtime_acceptance_percent=0`、`runtime_gate_count=0`、`owner_response_received_count=0`、`owner_response_accepted_count=0`、`wazuh_manager_registry_accepted_count=0`、`kali_active_scan_authorized_count=0`、`alert_receipt_runtime_send_count=0`。
|
||||
|
||||
**完成度同步**:
|
||||
- 後端 readback API source:`100%`。
|
||||
- 前台 IwoooS runtime readback source:`100%`。
|
||||
- 本地驗證:`95%`;尚未做 production deploy / desktop / mobile 驗證。
|
||||
- IwoooS runtime acceptance:仍 `0%`。
|
||||
- Wazuh manager registry accepted:仍 `0`。
|
||||
- Owner response received / accepted:仍 `0 / 0`。
|
||||
- Active runtime gate / active scan / active response / Telegram send:仍 `0 / false`。
|
||||
|
||||
**邊界**:本段只做只讀 API、前台讀回與測試;不主動修 Wazuh、不重新註冊 agent、不重啟 manager / dashboard、不啟動 Kali scan、不執行 `/execute`、不改 Nginx、不 reload、不改 firewall、不收 secret、不送 Telegram、不開 runtime gate。
|
||||
|
||||
## 2026-06-26|AwoooP Owner release AI 預填草案正式上線:把人工處理縮成決策確認,不再整包丟回值班者
|
||||
|
||||
**背景**:使用者指出 `node-exporter-110` 類 Telegram 告警仍顯示 `manual_required` / `DRAFT_READY`,但最後看起來仍要人工處理所有欄位。前一段已把 Work Items 顯示到 apply gate readiness;本段進一步把 `execution_release_contract` 中可由 AI 先產出的 owner release 資訊預填,讓 operator 看到 AI 已準備哪些欄位、人工只剩哪些決策,不再把維護窗口、rollback、blast radius、verifier、KM / PlayBook trust 全部標成 raw missing。
|
||||
|
||||
Reference in New Issue
Block a user