From c8912204ceaff7e366e7313fd2d1130d875b83f3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 26 Jun 2026 18:18:45 +0800 Subject: [PATCH] feat(iwooos): add security control coverage board --- apps/api/src/api/v1/iwooos.py | 30 ++ .../iwooos_security_control_coverage.py | 386 ++++++++++++++++++ .../test_iwooos_security_control_coverage.py | 80 ++++ apps/web/messages/en.json | 69 ++++ apps/web/messages/zh-TW.json | 69 ++++ apps/web/src/app/[locale]/iwooos/page.tsx | 285 ++++++++++++- apps/web/src/lib/api-client.ts | 72 ++++ docs/LOGBOOK.md | 31 ++ 8 files changed, 1021 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/services/iwooos_security_control_coverage.py create mode 100644 apps/api/tests/test_iwooos_security_control_coverage.py diff --git a/apps/api/src/api/v1/iwooos.py b/apps/api/src/api/v1/iwooos.py index c05b9f34e..80d1eaf71 100644 --- a/apps/api/src/api/v1/iwooos.py +++ b/apps/api/src/api/v1/iwooos.py @@ -21,6 +21,9 @@ from fastapi.responses import JSONResponse from src.services.iwooos_runtime_security_readback import ( load_latest_iwooos_runtime_security_readback, ) +from src.services.iwooos_security_control_coverage import ( + load_latest_iwooos_security_control_coverage, +) from src.services.public_redaction import redact_public_lan_topology @@ -232,3 +235,30 @@ async def get_iwooos_runtime_security_readback() -> dict[str, Any]: status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"IwoooS runtime security readback 無效:{exc}", ) from exc + + +@router.get( + "/api/v1/iwooos/security-control-coverage", + response_model=dict[str, Any], + summary="取得 IwoooS 資安納管覆蓋總表", + description=( + "彙整已提交的主機、產品、服務、配置、監控、Wazuh、AI Agent 與 agent-bounty " + "資安納管 snapshot,形成只讀覆蓋總表。此端點不查 live host、不讀 secret、不啟動掃描、" + "不送告警、不開 runtime gate。" + ), +) +async def get_iwooos_security_control_coverage() -> dict[str, Any]: + """回傳 IwoooS 資安納管覆蓋只讀總表。""" + try: + payload = await asyncio.to_thread(load_latest_iwooos_security_control_coverage) + return redact_public_lan_topology(payload) + except FileNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(exc), + ) from exc + except (json.JSONDecodeError, ValueError) as exc: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"IwoooS security control coverage 無效:{exc}", + ) from exc diff --git a/apps/api/src/services/iwooos_security_control_coverage.py b/apps/api/src/services/iwooos_security_control_coverage.py new file mode 100644 index 000000000..434542db6 --- /dev/null +++ b/apps/api/src/services/iwooos_security_control_coverage.py @@ -0,0 +1,386 @@ +""" +IwoooS security control coverage rollup. + +This service consolidates committed security inventory snapshots into one +read-only control-plane view. It never queries live hosts, Wazuh, Kali, +Kubernetes, Docker, Nginx, Telegram, or secret stores. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from src.services.snapshot_paths import default_evaluations_dir, default_security_dir + +_DEFAULT_SECURITY_DIR = default_security_dir(Path(__file__)) +_DEFAULT_EVALUATIONS_DIR = default_evaluations_dir(Path(__file__)) +_SCHEMA_VERSION = "iwooos_security_control_coverage_v1" + +_SECURITY_SNAPSHOTS = { + "asset_ledger": "security-asset-control-ledger.snapshot.json", + "host_service": "host-service-config-inventory.snapshot.json", + "monitoring": "monitoring-alerting-observability-inventory.snapshot.json", + "ssh_network": "ssh-network-access-inventory.snapshot.json", + "wazuh_hosts": "wazuh-managed-host-coverage-gate.snapshot.json", + "agent_bounty": "agent-bounty-iwooos-onboarding-handoff.snapshot.json", +} + + +def load_latest_iwooos_security_control_coverage( + security_dir: Path | None = None, + evaluations_dir: Path | None = None, +) -> dict[str, Any]: + """Load committed security-control inventory snapshots as one rollup.""" + sec_dir = security_dir or _DEFAULT_SECURITY_DIR + eval_dir = evaluations_dir or _DEFAULT_EVALUATIONS_DIR + + snapshots = { + key: _load_json(sec_dir / filename) + for key, filename in _SECURITY_SNAPSHOTS.items() + } + runtime_inventory = _load_latest(eval_dir, "runtime_surface_inventory_*.json") + ai_inventory = _load_latest(eval_dir, "ai_agent_automation_inventory_snapshot_*.json") + snapshots["runtime_surface"] = runtime_inventory + snapshots["ai_agent_automation"] = ai_inventory + + _require_snapshot_schemas(snapshots) + _require_zero_runtime_boundaries(snapshots) + + domains = _build_domains(snapshots) + summary = _build_summary(snapshots, domains) + + return { + "schema_version": _SCHEMA_VERSION, + "status": "committed_scope_rollup_ready_runtime_control_blocked", + "mode": "committed_snapshot_rollup_only_no_live_runtime_query", + "summary": summary, + "domains": domains, + "p0_next_actions": _build_p0_next_actions(), + "no_false_green_rules": [ + "納管覆蓋總表只代表 committed snapshot 可讀,不代表所有主機已被 Wazuh manager registry 驗收。", + "route 200、transport observed、UI 可見、一般工作批准都不能當成 runtime 授權。", + "owner response received / accepted、live evidence accepted、active scan、active response、Telegram send、host write 仍必須維持 0 / false,直到有脫敏證據與明確維護窗口。", + "Nginx、Firewall、Workflow、Secret、K8s、Docker、systemd、AI Agent provider 變更都必須先進 owner packet 與 guard,不得直接套用。", + ], + "source_refs": [ + "docs/security/security-asset-control-ledger.snapshot.json", + "docs/security/host-service-config-inventory.snapshot.json", + "docs/security/monitoring-alerting-observability-inventory.snapshot.json", + "docs/security/ssh-network-access-inventory.snapshot.json", + "docs/security/wazuh-managed-host-coverage-gate.snapshot.json", + "docs/security/agent-bounty-iwooos-onboarding-handoff.snapshot.json", + f"docs/evaluations/{runtime_inventory['_snapshot_name']}", + f"docs/evaluations/{ai_inventory['_snapshot_name']}", + ], + } + + +def _load_json(path: Path) -> dict[str, Any]: + with path.open(encoding="utf-8") as handle: + payload = json.load(handle) + if not isinstance(payload, dict): + raise ValueError(f"{path}: expected JSON object") + payload["_snapshot_name"] = path.name + return payload + + +def _load_latest(directory: Path, pattern: str) -> dict[str, Any]: + candidates = sorted(directory.glob(pattern)) + if not candidates: + raise FileNotFoundError(f"no snapshots found: {directory / pattern}") + return _load_json(candidates[-1]) + + +def _require_snapshot_schemas(snapshots: dict[str, dict[str, Any]]) -> None: + expected = { + "asset_ledger": "security_asset_control_ledger_v1", + "host_service": "host_service_config_inventory_v1", + "monitoring": "monitoring_alerting_observability_inventory_v1", + "ssh_network": "ssh_network_access_inventory_v1", + "wazuh_hosts": "wazuh_managed_host_coverage_gate_v1", + "agent_bounty": "agent_bounty_iwooos_onboarding_handoff_v1", + "runtime_surface": "runtime_surface_inventory_v1", + "ai_agent_automation": "ai_agent_automation_inventory_snapshot_v1", + } + for key, schema in expected.items(): + actual = snapshots[key].get("schema_version") + if actual != schema: + raise ValueError(f"{key}: expected schema_version={schema}, got {actual!r}") + + +def _require_zero_runtime_boundaries(snapshots: dict[str, dict[str, Any]]) -> None: + for key in ("asset_ledger", "host_service", "monitoring", "ssh_network", "wazuh_hosts"): + boundaries = snapshots[key].get("execution_boundaries") or {} + allowed = sorted( + name + for name, value in boundaries.items() + if name != "not_authorization" and value is not False + ) + if allowed: + raise ValueError(f"{key}: execution boundaries must stay false: {allowed}") + + runtime_approvals = snapshots["runtime_surface"].get("approval_boundaries") or {} + allowed_runtime = sorted(name for name, value in runtime_approvals.items() if value is not False) + if allowed_runtime: + raise ValueError(f"runtime_surface: approval boundaries must stay false: {allowed_runtime}") + + runtime_operations = snapshots["runtime_surface"].get("operation_boundaries") or {} + if runtime_operations.get("read_only_api_allowed") is not True: + raise ValueError("runtime_surface: read_only_api_allowed must remain true") + blocked_runtime_ops = { + "live_k8s_query_allowed", + "kubectl_allowed", + "rollout_allowed", + "restart_allowed", + "scale_allowed", + "delete_allowed", + "secret_read_allowed", + "secret_plaintext_allowed", + "active_scan_allowed", + "production_route_change_allowed", + } + allowed_ops = sorted(name for name in blocked_runtime_ops if runtime_operations.get(name) is not False) + if allowed_ops: + raise ValueError(f"runtime_surface: operation boundaries must stay false: {allowed_ops}") + + ai_program = snapshots["ai_agent_automation"].get("program_status") or {} + if ai_program.get("read_only_mode") is not True: + raise ValueError("ai_agent_automation: read_only_mode must remain true") + ai_approvals = snapshots["ai_agent_automation"].get("approval_boundaries") or {} + allowed_ai = sorted(name for name, value in ai_approvals.items() if value is not False) + if allowed_ai: + raise ValueError(f"ai_agent_automation: approval boundaries must stay false: {allowed_ai}") + + agent_bounty_summary = snapshots["agent_bounty"].get("summary") or {} + blocked_agent_bounty = { + "owner_response_received", + "owner_response_accepted", + "repo_refs_truth_accepted", + "data_classification_accepted", + "deployment_boundary_accepted", + "external_agent_boundary_accepted", + "runtime_execution_authorized", + "runtime_gate_open", + "production_deploy_authorized", + "workflow_modification_authorized", + "secret_value_collection_authorized", + "external_agent_autonomy_authorized", + "auto_claim_submit_authorized", + "bounty_payout_authorized", + } + allowed_agent_bounty = sorted( + name for name in blocked_agent_bounty if agent_bounty_summary.get(name) is not False + ) + if allowed_agent_bounty: + raise ValueError(f"agent_bounty: blocked summary flags must stay false: {allowed_agent_bounty}") + + +def _build_domains(snapshots: dict[str, dict[str, Any]]) -> list[dict[str, Any]]: + asset_summary = snapshots["asset_ledger"].get("summary") or {} + host_summary = snapshots["host_service"].get("summary") or {} + monitoring_summary = snapshots["monitoring"].get("summary") or {} + ssh_summary = snapshots["ssh_network"].get("summary") or {} + wazuh_summary = snapshots["wazuh_hosts"].get("summary") or {} + runtime_rollups = snapshots["runtime_surface"].get("rollups") or {} + runtime_program = snapshots["runtime_surface"].get("program_status") or {} + agent_bounty_summary = snapshots["agent_bounty"].get("summary") or {} + ai_program = snapshots["ai_agent_automation"].get("program_status") or {} + ai_assets = snapshots["ai_agent_automation"].get("assets") or [] + + return [ + { + "domain_id": "high_value_asset_control", + "label": "高價值資產與配置總帳", + "priority": "P0", + "coverage_percent": _as_int(asset_summary.get("security_asset_control_ledger_completion_percent")), + "scope_count": _as_int(asset_summary.get("asset_group_count")), + "write_capable_count": _as_int(asset_summary.get("c0_asset_group_count")), + "accepted_count": _as_int(asset_summary.get("owner_response_accepted_count")), + "blocked_count": _as_int(asset_summary.get("owner_packet_required_count")), + "status": "owner_packet_required", + "next_gate": "owner_packet_and_redacted_live_evidence", + "source_refs": ["docs/security/security-asset-control-ledger.snapshot.json"], + }, + { + "domain_id": "host_service_runtime", + "label": "主機服務 / Docker / systemd", + "priority": "P0", + "coverage_percent": _as_int(host_summary.get("coverage_percent_after_inventory")), + "scope_count": _as_int(host_summary.get("surface_count")), + "write_capable_count": _as_int(host_summary.get("write_capable_surface_count")), + "accepted_count": _as_int(host_summary.get("owner_response_accepted_count")), + "blocked_count": _as_int(host_summary.get("surfaces_requiring_owner_response_count")), + "status": "waiting_live_hash_and_owner_response", + "next_gate": "host_live_hash_restart_window_rollback_owner", + "source_refs": ["docs/security/host-service-config-inventory.snapshot.json"], + }, + { + "domain_id": "monitoring_alerting_observability", + "label": "監控 / 告警 / 可觀測性 / 通知", + "priority": "P0", + "coverage_percent": _as_int(monitoring_summary.get("coverage_percent_after_inventory")), + "scope_count": _as_int(monitoring_summary.get("surface_count")), + "write_capable_count": _as_int(monitoring_summary.get("write_capable_surface_count")), + "accepted_count": _as_int(monitoring_summary.get("owner_response_accepted_count")), + "blocked_count": _as_int(monitoring_summary.get("surfaces_requiring_owner_response_count")), + "status": "waiting_receiver_route_and_receipt_evidence", + "next_gate": "alert_readability_receiver_receipt_reload_owner", + "source_refs": ["docs/security/monitoring-alerting-observability-inventory.snapshot.json"], + }, + { + "domain_id": "ssh_firewall_network_access", + "label": "SSH / Firewall / WireGuard / NodePort / NetworkPolicy", + "priority": "P0", + "coverage_percent": _as_int(ssh_summary.get("coverage_percent_after_inventory")), + "scope_count": _as_int(ssh_summary.get("surface_count")), + "write_capable_count": _as_int(ssh_summary.get("write_capable_surface_count")), + "accepted_count": _as_int(ssh_summary.get("owner_response_accepted_count")), + "blocked_count": _as_int(ssh_summary.get("surfaces_requiring_owner_response_count")), + "status": "waiting_actor_before_after_and_recurrence_guard", + "next_gate": "post_incident_network_change_readback", + "source_refs": ["docs/security/ssh-network-access-inventory.snapshot.json"], + }, + { + "domain_id": "awoooi_runtime_surfaces", + "label": "AWOOOI runtime 工作負載 / Secret / Ingress / CronJob", + "priority": "P0", + "coverage_percent": _as_int(runtime_program.get("overall_completion_percent")), + "scope_count": _as_int(runtime_rollups.get("total_surfaces")), + "write_capable_count": len(runtime_rollups.get("secret_surface_ids") or []), + "accepted_count": 0, + "blocked_count": len(runtime_rollups.get("action_required_surface_ids") or []), + "status": "manifest_mapped_read_only_runtime_gate_closed", + "next_gate": "live_runtime_binding_owner_acceptance", + "source_refs": [f"docs/evaluations/{snapshots['runtime_surface']['_snapshot_name']}"], + }, + { + "domain_id": "wazuh_managed_host_coverage", + "label": "Wazuh 主機納管與 manager registry", + "priority": "P0", + "coverage_percent": 0, + "scope_count": _as_int(wazuh_summary.get("expected_host_scope_count")), + "write_capable_count": _as_int(wazuh_summary.get("agent_reenroll_authorized_count")), + "accepted_count": _as_int(wazuh_summary.get("manager_registry_accepted_count")), + "blocked_count": _as_int(wazuh_summary.get("expected_host_scope_count")), + "status": "waiting_manager_registry_readback", + "next_gate": "manager_registry_cross_check_all_expected_hosts", + "source_refs": ["docs/security/wazuh-managed-host-coverage-gate.snapshot.json"], + }, + { + "domain_id": "agent_bounty_protocol", + "label": "agent-bounty-protocol 產品邊界 / MCP / A2A", + "priority": "P0", + "coverage_percent": _as_int(agent_bounty_summary.get("onboarding_handoff_completion_percent")), + "scope_count": len(snapshots["agent_bounty"].get("product_surfaces") or []), + "write_capable_count": 0, + "accepted_count": 0, + "blocked_count": len(snapshots["agent_bounty"].get("product_surfaces") or []), + "status": "draft_waiting_owner_review_runtime_gate_closed", + "next_gate": "repo_refs_data_classification_deployment_boundary_owner_acceptance", + "source_refs": ["docs/security/agent-bounty-iwooos-onboarding-handoff.snapshot.json"], + }, + { + "domain_id": "ai_agent_automation", + "label": "AI Agent / Provider / 自動化資產", + "priority": "P0", + "coverage_percent": _as_int(ai_program.get("overall_completion_percent")), + "scope_count": len(ai_assets), + "write_capable_count": 0, + "accepted_count": 0, + "blocked_count": len( + [ + asset + for asset in ai_assets + if isinstance(asset, dict) and asset.get("status") != "done" + ] + ), + "status": "read_only_inventory_runtime_write_gate_closed", + "next_gate": "owner_approved_runtime_write_gate_and_replay_shadow_canary", + "source_refs": [f"docs/evaluations/{snapshots['ai_agent_automation']['_snapshot_name']}"], + }, + ] + + +def _build_summary(snapshots: dict[str, dict[str, Any]], domains: list[dict[str, Any]]) -> dict[str, Any]: + visible_scope_count = sum(_as_int(domain.get("scope_count")) for domain in domains) + write_capable_count = sum(_as_int(domain.get("write_capable_count")) for domain in domains) + blocked_count = sum(_as_int(domain.get("blocked_count")) for domain in domains) + accepted_count = sum(_as_int(domain.get("accepted_count")) for domain in domains) + coverage_values = [_as_int(domain.get("coverage_percent")) for domain in domains] + + return { + "source_snapshot_count": 8, + "control_domain_count": len(domains), + "visible_scope_unit_count": visible_scope_count, + "write_capable_scope_count": write_capable_count, + "blocked_scope_count": blocked_count, + "accepted_scope_count": accepted_count, + "control_plane_visibility_percent": round(sum(coverage_values) / len(coverage_values)), + "actual_runtime_acceptance_percent": 0, + "runtime_gate_count": 0, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "live_evidence_accepted_count": 0, + "wazuh_manager_registry_accepted_count": 0, + "active_scan_authorized_count": 0, + "active_response_authorized_count": 0, + "telegram_send_authorized_count": 0, + "host_write_authorized_count": 0, + "secret_value_collected_count": 0, + "agent_bounty_runtime_gate_open_count": 0, + "ai_agent_runtime_write_gate_open_count": 0, + "asset_group_count": _summary_int(snapshots["asset_ledger"], "asset_group_count"), + "host_service_surface_count": _summary_int(snapshots["host_service"], "surface_count"), + "monitoring_surface_count": _summary_int(snapshots["monitoring"], "surface_count"), + "ssh_network_surface_count": _summary_int(snapshots["ssh_network"], "surface_count"), + "runtime_surface_count": _as_int((snapshots["runtime_surface"].get("rollups") or {}).get("total_surfaces")), + "wazuh_expected_host_scope_count": _summary_int(snapshots["wazuh_hosts"], "expected_host_scope_count"), + "agent_bounty_product_surface_count": len(snapshots["agent_bounty"].get("product_surfaces") or []), + "ai_agent_asset_count": len(snapshots["ai_agent_automation"].get("assets") or []), + "all_scope_runtime_controlled": False, + } + + +def _build_p0_next_actions() -> list[dict[str, str]]: + return [ + { + "priority": "P0-01", + "title": "Wazuh manager registry 全主機交叉驗收", + "required_evidence": "manager registry、agent status、dashboard API / RBAC / TLS readback、缺席主機 owner decision。", + }, + { + "priority": "P0-02", + "title": "Host / Docker / systemd live hash 與維護窗口", + "required_evidence": "live config hash、restart window、rollback owner、post-check 指標、drift disposition。", + }, + { + "priority": "P0-03", + "title": "Nginx / 公開入口 / Firewall 變更控管", + "required_evidence": "脫敏 live conf export、rendered diff、nginx -t、route smoke、actor before / after。", + }, + { + "priority": "P0-04", + "title": "監控告警卡片化與 receipt", + "required_evidence": "可讀卡片格式、receiver route、Telegram / webhook receipt、silence / dedup / inhibit review。", + }, + { + "priority": "P0-05", + "title": "AI Agent runtime write gate", + "required_evidence": "人工批准、replay / shadow / canary、成本與安全邊界、post-write verifier。", + }, + { + "priority": "P0-06", + "title": "agent-bounty-protocol 安全納管", + "required_evidence": "repo refs truth、data classification、MCP / A2A abuse boundary、deployment boundary、owner acceptance。", + }, + ] + + +def _summary_int(snapshot: dict[str, Any], key: str) -> int: + return _as_int((snapshot.get("summary") or {}).get(key)) + + +def _as_int(value: Any) -> int: + return value if isinstance(value, int) else 0 diff --git a/apps/api/tests/test_iwooos_security_control_coverage.py b/apps/api/tests/test_iwooos_security_control_coverage.py new file mode 100644 index 000000000..8998bfab0 --- /dev/null +++ b/apps/api/tests/test_iwooos_security_control_coverage.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from src.api.v1.iwooos import router +from src.services.iwooos_security_control_coverage import ( + load_latest_iwooos_security_control_coverage, +) + + +def _client() -> TestClient: + app = FastAPI() + app.include_router(router) + return TestClient(app) + + +def test_iwooos_security_control_coverage_rolls_up_core_scopes() -> None: + payload = load_latest_iwooos_security_control_coverage() + + assert payload["schema_version"] == "iwooos_security_control_coverage_v1" + assert payload["status"] == "committed_scope_rollup_ready_runtime_control_blocked" + assert payload["summary"]["source_snapshot_count"] == 8 + assert payload["summary"]["control_domain_count"] == 8 + assert payload["summary"]["visible_scope_unit_count"] == 160 + assert payload["summary"]["asset_group_count"] == 16 + assert payload["summary"]["host_service_surface_count"] == 9 + assert payload["summary"]["monitoring_surface_count"] == 60 + assert payload["summary"]["ssh_network_surface_count"] == 16 + assert payload["summary"]["runtime_surface_count"] == 22 + assert payload["summary"]["wazuh_expected_host_scope_count"] == 6 + assert payload["summary"]["agent_bounty_product_surface_count"] == 7 + assert payload["summary"]["ai_agent_asset_count"] == 24 + + domain_ids = {domain["domain_id"] for domain in payload["domains"]} + assert domain_ids == { + "high_value_asset_control", + "host_service_runtime", + "monitoring_alerting_observability", + "ssh_firewall_network_access", + "awoooi_runtime_surfaces", + "wazuh_managed_host_coverage", + "agent_bounty_protocol", + "ai_agent_automation", + } + + +def test_iwooos_security_control_coverage_keeps_runtime_gates_closed() -> None: + payload = load_latest_iwooos_security_control_coverage() + summary = payload["summary"] + + assert summary["actual_runtime_acceptance_percent"] == 0 + assert summary["runtime_gate_count"] == 0 + assert summary["owner_response_received_count"] == 0 + assert summary["owner_response_accepted_count"] == 0 + assert summary["live_evidence_accepted_count"] == 0 + assert summary["wazuh_manager_registry_accepted_count"] == 0 + assert summary["active_scan_authorized_count"] == 0 + assert summary["active_response_authorized_count"] == 0 + assert summary["telegram_send_authorized_count"] == 0 + assert summary["host_write_authorized_count"] == 0 + assert summary["secret_value_collected_count"] == 0 + assert summary["agent_bounty_runtime_gate_open_count"] == 0 + assert summary["ai_agent_runtime_write_gate_open_count"] == 0 + assert summary["all_scope_runtime_controlled"] is False + assert all(domain["accepted_count"] == 0 for domain in payload["domains"]) + + +def test_iwooos_security_control_coverage_api_is_public_safe() -> None: + response = _client().get("/api/v1/iwooos/security-control-coverage") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "iwooos_security_control_coverage_v1" + assert data["summary"]["runtime_gate_count"] == 0 + assert data["summary"]["visible_scope_unit_count"] == 160 + assert any(action["priority"] == "P0-01" for action in data["p0_next_actions"]) + assert "192.168.0." not in response.text + assert "工作視窗" not in response.text + assert "批准!繼續" not in response.text diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index d112fc64d..63f6485e0 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -20220,6 +20220,75 @@ } } }, + "securityControlCoverage": { + "eyebrow": "IwoooS 資安納管覆蓋總表", + "title": "主機、產品、服務、工具與 AI Agent 先收斂成同一張總帳", + "subtitle": "這張板彙整 committed snapshot,集中顯示高價值配置、主機服務、監控告警、SSH / Firewall、runtime surface、Wazuh、agent-bounty-protocol 與 AI Agent 納管狀態;它不查 live host、不讀 secret、不啟動掃描、不送告警、不開 runtime gate。", + "statusLabel": "總表狀態", + "statusDetail": "納管總表只代表控制面可讀與缺口已集中,不代表所有主機、產品、服務或 AI Agent 已完成 runtime 控管。", + "emptyDomains": "尚未讀回控制域,維持阻擋狀態。", + "domainStatusLabel": "狀態", + "p0Title": "P0 優先順序", + "p0Intro": "以下是下一階段真正能降低即時風險的順序;每一項都需要脫敏證據與 owner acceptance,不能靠 UI 可見或一般批准跳過。", + "requiredEvidenceLabel": "需要證據", + "boundaryTitle": "不可假綠燈規則", + "status": { + "loading": "正在讀取資安納管總表", + "failed": "納管總表尚未部署或讀取失敗", + "ready": "納管總表已讀回,但 runtime 控管仍未完成" + }, + "summary": { + "visibleScopes": { + "label": "可見納管單位", + "detail": "目前只統計 committed snapshot 可讀範圍。" + }, + "controlDomains": { + "label": "控制域", + "detail": "主機、服務、網路、監控、Wazuh、產品與 AI Agent 已合併顯示。" + }, + "controlPlane": { + "label": "控制面可視", + "detail": "只代表盤點可見度,不代表 runtime 成熟。" + }, + "runtimeAcceptance": { + "label": "Runtime 驗收", + "detail": "實際 runtime acceptance 仍維持 0%。" + }, + "ownerAccepted": { + "label": "Owner accepted", + "detail": "正式 owner response accepted 仍為 0。" + }, + "wazuhAccepted": { + "label": "Wazuh accepted", + "detail": "Manager registry accepted 仍為 0。" + } + }, + "domainMetric": { + "scope": "範圍", + "blocked": "待補", + "accepted": "接受" + }, + "domainStatus": { + "owner_packet_required": "等待 owner packet 與脫敏 live evidence", + "waiting_live_hash_and_owner_response": "等待 live hash、維護窗口與 owner response", + "waiting_receiver_route_and_receipt_evidence": "等待 receiver route、告警 receipt 與 reload owner", + "waiting_actor_before_after_and_recurrence_guard": "等待 actor、before / after 與防再發證據", + "manifest_mapped_read_only_runtime_gate_closed": "Manifest 已映射,runtime gate 仍關閉", + "waiting_manager_registry_readback": "等待 Wazuh manager registry 全量讀回", + "draft_waiting_owner_review_runtime_gate_closed": "等待 owner review,runtime gate 仍關閉", + "read_only_inventory_runtime_write_gate_closed": "只讀盤點完成,AI runtime write gate 仍關閉" + }, + "domainBody": { + "high_value_asset_control": "Nginx、DNS / TLS、K8s、secret、workflow、runner、runtime config、backup 與 agent-bounty runtime 都先放進高價值配置總帳。", + "host_service_runtime": "Docker Compose、systemd、repair bot、port binding、process / persistence baseline 需要 live hash 與維護窗口。", + "monitoring_alerting_observability": "Prometheus、Alertmanager、Grafana、SigNoz、Sentry、Langfuse、OTEL 與通知路由都需要 receipt 與 owner acceptance。", + "ssh_firewall_network_access": "SSH target、known_hosts、deploy SSH、sudoers、NodePort、NetworkPolicy、WireGuard 與 firewall 需要 before / after 證據。", + "awoooi_runtime_surfaces": "API、Web、Worker、Ingress、Secret、CronJob 與 K8s runtime surface 只做 manifest 讀回,不代表可 rollout。", + "wazuh_managed_host_coverage": "受管主機必須由 manager registry 交叉驗收;transport observed 不能當完成。", + "agent_bounty_protocol": "agent-bounty-protocol 已納入產品邊界,但 MCP、A2A、claim、submit、payout 與 cron 仍鎖住。", + "ai_agent_automation": "OpenClaw、Hermes、Nemotron、provider 與自動化資產維持只讀;replay / shadow / canary 前不可切 production。" + } + }, "wazuhLiveRouteReadback": { "eyebrow": "Wazuh 正式路由只讀讀回", "title": "正式站必須直接顯示 Wazuh 只讀路由狀態", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index d112fc64d..63f6485e0 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -20220,6 +20220,75 @@ } } }, + "securityControlCoverage": { + "eyebrow": "IwoooS 資安納管覆蓋總表", + "title": "主機、產品、服務、工具與 AI Agent 先收斂成同一張總帳", + "subtitle": "這張板彙整 committed snapshot,集中顯示高價值配置、主機服務、監控告警、SSH / Firewall、runtime surface、Wazuh、agent-bounty-protocol 與 AI Agent 納管狀態;它不查 live host、不讀 secret、不啟動掃描、不送告警、不開 runtime gate。", + "statusLabel": "總表狀態", + "statusDetail": "納管總表只代表控制面可讀與缺口已集中,不代表所有主機、產品、服務或 AI Agent 已完成 runtime 控管。", + "emptyDomains": "尚未讀回控制域,維持阻擋狀態。", + "domainStatusLabel": "狀態", + "p0Title": "P0 優先順序", + "p0Intro": "以下是下一階段真正能降低即時風險的順序;每一項都需要脫敏證據與 owner acceptance,不能靠 UI 可見或一般批准跳過。", + "requiredEvidenceLabel": "需要證據", + "boundaryTitle": "不可假綠燈規則", + "status": { + "loading": "正在讀取資安納管總表", + "failed": "納管總表尚未部署或讀取失敗", + "ready": "納管總表已讀回,但 runtime 控管仍未完成" + }, + "summary": { + "visibleScopes": { + "label": "可見納管單位", + "detail": "目前只統計 committed snapshot 可讀範圍。" + }, + "controlDomains": { + "label": "控制域", + "detail": "主機、服務、網路、監控、Wazuh、產品與 AI Agent 已合併顯示。" + }, + "controlPlane": { + "label": "控制面可視", + "detail": "只代表盤點可見度,不代表 runtime 成熟。" + }, + "runtimeAcceptance": { + "label": "Runtime 驗收", + "detail": "實際 runtime acceptance 仍維持 0%。" + }, + "ownerAccepted": { + "label": "Owner accepted", + "detail": "正式 owner response accepted 仍為 0。" + }, + "wazuhAccepted": { + "label": "Wazuh accepted", + "detail": "Manager registry accepted 仍為 0。" + } + }, + "domainMetric": { + "scope": "範圍", + "blocked": "待補", + "accepted": "接受" + }, + "domainStatus": { + "owner_packet_required": "等待 owner packet 與脫敏 live evidence", + "waiting_live_hash_and_owner_response": "等待 live hash、維護窗口與 owner response", + "waiting_receiver_route_and_receipt_evidence": "等待 receiver route、告警 receipt 與 reload owner", + "waiting_actor_before_after_and_recurrence_guard": "等待 actor、before / after 與防再發證據", + "manifest_mapped_read_only_runtime_gate_closed": "Manifest 已映射,runtime gate 仍關閉", + "waiting_manager_registry_readback": "等待 Wazuh manager registry 全量讀回", + "draft_waiting_owner_review_runtime_gate_closed": "等待 owner review,runtime gate 仍關閉", + "read_only_inventory_runtime_write_gate_closed": "只讀盤點完成,AI runtime write gate 仍關閉" + }, + "domainBody": { + "high_value_asset_control": "Nginx、DNS / TLS、K8s、secret、workflow、runner、runtime config、backup 與 agent-bounty runtime 都先放進高價值配置總帳。", + "host_service_runtime": "Docker Compose、systemd、repair bot、port binding、process / persistence baseline 需要 live hash 與維護窗口。", + "monitoring_alerting_observability": "Prometheus、Alertmanager、Grafana、SigNoz、Sentry、Langfuse、OTEL 與通知路由都需要 receipt 與 owner acceptance。", + "ssh_firewall_network_access": "SSH target、known_hosts、deploy SSH、sudoers、NodePort、NetworkPolicy、WireGuard 與 firewall 需要 before / after 證據。", + "awoooi_runtime_surfaces": "API、Web、Worker、Ingress、Secret、CronJob 與 K8s runtime surface 只做 manifest 讀回,不代表可 rollout。", + "wazuh_managed_host_coverage": "受管主機必須由 manager registry 交叉驗收;transport observed 不能當完成。", + "agent_bounty_protocol": "agent-bounty-protocol 已納入產品邊界,但 MCP、A2A、claim、submit、payout 與 cron 仍鎖住。", + "ai_agent_automation": "OpenClaw、Hermes、Nemotron、provider 與自動化資產維持只讀;replay / shadow / canary 前不可切 production。" + } + }, "wazuhLiveRouteReadback": { "eyebrow": "Wazuh 正式路由只讀讀回", "title": "正式站必須直接顯示 Wazuh 只讀路由狀態", diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index 0a61cfdff..bbc7d658d 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -33,7 +33,12 @@ 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' +import { + apiClient, + type IwoooSRuntimeSecurityReadbackResponse, + type IwoooSSecurityControlCoverageDomain, + type IwoooSSecurityControlCoverageResponse, +} from '@/lib/api-client' type PostureMetric = { key: string @@ -327,6 +332,13 @@ type RuntimeSecurityReadbackSummaryItem = { tone: 'steady' | 'warn' | 'locked' } +type SecurityControlCoverageSummaryItem = { + key: 'visibleScopes' | 'controlDomains' | 'controlPlane' | 'runtimeAcceptance' | 'ownerAccepted' | 'wazuhAccepted' + value: string + icon: typeof ShieldCheck + tone: 'steady' | 'warn' | 'locked' +} + type SocSiemKaliWazuhIntegrationItem = { key: string check: string @@ -8281,6 +8293,276 @@ function IwoooSRuntimeSecurityReadbackBoard() { ) } +function coverageTone(domain: IwoooSSecurityControlCoverageDomain): 'steady' | 'warn' | 'locked' { + if (domain.domain_id === 'wazuh_managed_host_coverage') return 'locked' + if (domain.coverage_percent >= 80) return 'steady' + if (domain.coverage_percent >= 50) return 'warn' + return 'locked' +} + +function IwoooSSecurityControlCoverageBoard() { + const t = useTranslations('iwooos.securityControlCoverage') + 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 loadCoverage() { + setLoading(true) + setFailed(false) + try { + const payload = await apiClient.getIwoooSSecurityControlCoverage() + if (mounted) { + setData(payload) + } + } catch { + if (mounted) { + setData(null) + setFailed(true) + } + } finally { + if (mounted) { + setLoading(false) + } + } + } + + loadCoverage() + return () => { + mounted = false + } + }, []) + + const summary = data?.summary + const statusKey = loading ? 'loading' : failed || !data ? 'failed' : 'ready' + const statusTone: 'steady' | 'warn' | 'locked' = loading ? 'warn' : failed || !data ? 'warn' : 'locked' + const summaryItems: SecurityControlCoverageSummaryItem[] = [ + { + key: 'visibleScopes', + value: summary ? String(summary.visible_scope_unit_count) : '...', + icon: Boxes, + tone: summary && summary.visible_scope_unit_count > 0 ? 'steady' : 'warn', + }, + { + key: 'controlDomains', + value: summary ? String(summary.control_domain_count) : '...', + icon: Network, + tone: summary && summary.control_domain_count > 0 ? 'steady' : 'warn', + }, + { + key: 'controlPlane', + value: summary ? `${summary.control_plane_visibility_percent}%` : '...', + icon: ShieldCheck, + tone: summary && summary.control_plane_visibility_percent >= 70 ? 'steady' : 'warn', + }, + { + key: 'runtimeAcceptance', + value: summary ? `${summary.actual_runtime_acceptance_percent}%` : '...', + icon: Lock, + tone: 'locked', + }, + { + key: 'ownerAccepted', + value: summary ? String(summary.owner_response_accepted_count) : '...', + icon: ClipboardCheck, + tone: 'locked', + }, + { + key: 'wazuhAccepted', + value: summary ? String(summary.wazuh_manager_registry_accepted_count) : '...', + icon: Radar, + tone: 'locked', + }, + ] + const domains = data?.domains ?? [] + const p0Actions = data?.p0_next_actions ?? [] + + return ( +
+
+
+
+
+ + {t('eyebrow')} +
+

{t('title')}

+

+ {t('subtitle')} +

+
+ +
+
+ {t('statusLabel')} + +
+
+ {t(`status.${statusKey}` as never)} +
+

+ {t('statusDetail')} +

+
+
+ +
+ {summaryItems.map(item => { + const Icon = item.icon + return ( +
+
+ {t(`summary.${item.key}.label` as never)} + +
+
+ {item.value} +
+

+ {t(`summary.${item.key}.detail` as never)} +

+
+ ) + })} +
+ +
+ {domains.length === 0 ? ( +
+ {t('emptyDomains')} +
+ ) : domains.map(domain => { + const tone = coverageTone(domain) + return ( +
+
+
+
{domain.label}
+
+ {t(`domainBody.${domain.domain_id}` as never)} +
+
+ {domain.coverage_percent}% +
+ +
+ {([ + ['scope', domain.scope_count], + ['blocked', domain.blocked_count], + ['accepted', domain.accepted_count], + ] as const).map(([key, value]) => ( +
+
{t(`domainMetric.${key}` as never)}
+
+ {value} +
+
+ ))} +
+ +
+
{t('domainStatusLabel')}
+
+ {t(`domainStatus.${domain.status}` as never)} +
+
+
+ ) + })} +
+ +
+
+ + {t('p0Title')} +
+

+ {t('p0Intro')} +

+
+ {p0Actions.map(action => ( +
+
+ {action.priority} + {action.title} +
+

+ {t('requiredEvidenceLabel')}:{action.required_evidence} +

+
+ ))} +
+
+ +
+ + {t('boundaryTitle')} + +
+ {(data?.no_false_green_rules ?? []).map(item => ( + + {item} + + ))} +
+
+
+
+ ) +} + function wazuhReadonlyStatusKey(httpStatus: number | null, data: WazuhReadonlyStatusResponse | null) { if (httpStatus === 404) return 'predeploy' if (!data) return 'unavailable' @@ -22178,6 +22460,7 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) { + diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index d06844a6b..57fe8ba70 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -144,6 +144,73 @@ export interface IwoooSRuntimeSecurityReadbackResponse { no_false_green_rules: string[] } +export interface IwoooSSecurityControlCoverageDomain { + domain_id: + | 'high_value_asset_control' + | 'host_service_runtime' + | 'monitoring_alerting_observability' + | 'ssh_firewall_network_access' + | 'awoooi_runtime_surfaces' + | 'wazuh_managed_host_coverage' + | 'agent_bounty_protocol' + | 'ai_agent_automation' + label: string + priority: string + coverage_percent: number + scope_count: number + write_capable_count: number + accepted_count: number + blocked_count: number + status: string + next_gate: string + source_refs: string[] +} + +export interface IwoooSSecurityControlCoverageResponse { + schema_version: 'iwooos_security_control_coverage_v1' + status: string + mode: string + summary: { + source_snapshot_count: number + control_domain_count: number + visible_scope_unit_count: number + write_capable_scope_count: number + blocked_scope_count: number + accepted_scope_count: number + control_plane_visibility_percent: number + actual_runtime_acceptance_percent: number + runtime_gate_count: number + owner_response_received_count: number + owner_response_accepted_count: number + live_evidence_accepted_count: number + wazuh_manager_registry_accepted_count: number + active_scan_authorized_count: number + active_response_authorized_count: number + telegram_send_authorized_count: number + host_write_authorized_count: number + secret_value_collected_count: number + agent_bounty_runtime_gate_open_count: number + ai_agent_runtime_write_gate_open_count: number + asset_group_count: number + host_service_surface_count: number + monitoring_surface_count: number + ssh_network_surface_count: number + runtime_surface_count: number + wazuh_expected_host_scope_count: number + agent_bounty_product_surface_count: number + ai_agent_asset_count: number + all_scope_runtime_controlled: boolean + } + domains: IwoooSSecurityControlCoverageDomain[] + p0_next_actions: Array<{ + priority: string + title: string + required_evidence: string + }> + no_false_green_rules: string[] + source_refs: string[] +} + async function handleResponse(response: Response): Promise { if (!response.ok) { const error = await response.json().catch(() => ({})) @@ -182,6 +249,11 @@ export const apiClient = { return handleResponse(res) }, + async getIwoooSSecurityControlCoverage() { + const res = await fetch(`${API_BASE_URL}/iwooos/security-control-coverage`, { cache: 'no-store' }) + return handleResponse(res) + }, + // Agent async getAgentStatus() { const res = await fetch(`${API_BASE_URL}/agent/status`) diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 1221161fb..ee4d57ed9 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,34 @@ +## 2026-06-26|IwoooS 資安納管覆蓋總表:主機、產品、服務、工具、Wazuh、AI Agent 統一讀回 + +**背景**:使用者要求不要再只靠散落文件判斷「所有主機、產品、網站、套件、服務、工具、AI Agent 是否納入資安機制」。本段把現有 committed snapshot 收斂成單一只讀 API 與 IwoooS 前台總表,明確顯示可見納管範圍、控制域、待補缺口與仍為 `0 / false` 的 runtime 邊界。 + +**完成**: +- 新增 `iwooos_security_control_coverage_v1` 後端 aggregator,彙整 8 份 committed snapshot:security asset ledger、host service config、monitoring / alerting / observability、SSH / network access、Wazuh managed host coverage、agent-bounty onboarding、runtime surface inventory、AI Agent automation inventory。 +- 新增 `GET /api/v1/iwooos/security-control-coverage`;端點只讀 repo snapshot,不查 live host、不讀 secret、不連 Wazuh / Kali、不 SSH、不啟動掃描、不送 Telegram、不開 runtime gate。 +- 前台 `/zh-TW/iwooos` 新增 `IwoooS 資安納管覆蓋總表`,顯示 `visible_scope_unit_count=160`、`control_domain_count=8`、控制面可視 `71%`、runtime acceptance `0%`、owner accepted `0`、Wazuh manager registry accepted `0`。 +- 納管域包含:高價值資產與配置總帳、主機服務 / Docker / systemd、監控 / 告警 / 可觀測性、SSH / Firewall / WireGuard / NodePort / NetworkPolicy、AWOOOI runtime surfaces、Wazuh 主機納管、agent-bounty-protocol、AI Agent / Provider / 自動化資產。 + +**只讀驗證結果**: +- `pytest apps/api/tests/test_iwooos_security_control_coverage.py apps/api/tests/test_iwooos_runtime_security_readback.py apps/api/tests/test_iwooos_wazuh_api.py -q`:`12 passed`。 +- `pnpm --dir apps/web typecheck`:通過。 +- `python3 -m json.tool apps/web/messages/zh-TW.json` / `apps/web/messages/en.json`:通過。 +- `python3.11 -m py_compile apps/api/src/api/v1/iwooos.py apps/api/src/services/iwooos_security_control_coverage.py apps/api/src/services/iwooos_runtime_security_readback.py apps/api/src/services/snapshot_paths.py`:通過。 +- `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_security_control_coverage.py apps/api/src/services/iwooos_runtime_security_readback.py`:`DOC_SECRET_SANITY_OK scanned_files=274`。 +- `git diff --check`:通過。 + +**完成度同步**: +- 後端納管覆蓋 aggregator / API source:`100%`。 +- 前台 IwoooS 納管覆蓋總表 source:`100%`。 +- 本地驗證:`95%`;production deploy / desktop / mobile 驗證待補。 +- 資安 runtime acceptance:仍 `0%`。 +- Owner response received / accepted:仍 `0 / 0`。 +- Wazuh manager registry accepted:仍 `0`。 +- Active scan / active response / host write / secret collection / Telegram send:仍 `0 / false`。 + +**邊界**:本段只做只讀 API、前台讀回與測試;沒有宣稱所有資產已完成 runtime 控管,沒有修 Wazuh agent,沒有改 Nginx / Firewall / Docker / systemd / K8s / workflow / secret,沒有 active scan、active response 或任何主機寫入。 + ## 2026-06-26|IwoooS Runtime 資安讀回總板:Wazuh / Kali / 告警 / owner dispatch 六條 P0 線接上只讀 API 與前台 **背景**:IwoooS 資安工作不能停在文件、靜態卡片或截圖;Wazuh、資安觀測節點、告警可讀性、owner dispatch、外部入侵防護與 runtime gate 必須能被前台讀回,同時不得把 UI 可見、route 200、transport count 或一般操作批准誤判成 runtime 授權。