From 121e5b8861f66de8a6be0f68ee2d0fb293de51d1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 27 Jun 2026 13:14:23 +0800 Subject: [PATCH] feat(iwooos): expose high-value config control coverage --- apps/api/src/api/v1/iwooos.py | 31 +++ ...ooos_high_value_config_control_coverage.py | 213 ++++++++++++++++++ ...ooos_high_value_config_control_coverage.py | 77 +++++++ apps/web/messages/en.json | 5 + apps/web/messages/zh-TW.json | 5 + apps/web/src/app/[locale]/iwooos/page.tsx | 108 ++++++++- apps/web/src/lib/api-client.ts | 52 +++++ docs/LOGBOOK.md | 41 ++++ .../security/iwooos-config-control-guard.py | 37 +++ 9 files changed, 564 insertions(+), 5 deletions(-) create mode 100644 apps/api/src/services/iwooos_high_value_config_control_coverage.py create mode 100644 apps/api/tests/test_iwooos_high_value_config_control_coverage.py diff --git a/apps/api/src/api/v1/iwooos.py b/apps/api/src/api/v1/iwooos.py index 06618c993..091c134e7 100644 --- a/apps/api/src/api/v1/iwooos.py +++ b/apps/api/src/api/v1/iwooos.py @@ -17,6 +17,9 @@ from fastapi.responses import JSONResponse from src.services.iwooos_runtime_security_readback import ( load_latest_iwooos_runtime_security_readback, ) +from src.services.iwooos_high_value_config_control_coverage import ( + load_latest_iwooos_high_value_config_control_coverage, +) from src.services.iwooos_security_control_coverage import ( load_latest_iwooos_security_control_coverage, ) @@ -168,3 +171,31 @@ async def get_iwooos_security_control_coverage() -> dict[str, Any]: status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"IwoooS security control coverage 無效:{exc}", ) from exc + + +@router.get( + "/api/v1/iwooos/high-value-config-control-coverage", + response_model=dict[str, Any], + summary="取得 IwoooS 高價值配置控管覆蓋矩陣", + description=( + "讀取已提交的高價值配置控管 snapshot,回傳 Nginx、DNS / TLS、K8s、" + "Secrets、runner、Firewall、Backup、AI provider 與 agent-bounty runtime 的" + "公開安全只讀投影。此端點不查 live host、不讀 secret、不執行 nginx -t、" + "不 reload、不 sync、不啟動掃描、不開 runtime gate。" + ), +) +async def get_iwooos_high_value_config_control_coverage() -> dict[str, Any]: + """回傳高價值配置控管矩陣公開安全只讀狀態。""" + try: + payload = await asyncio.to_thread(load_latest_iwooos_high_value_config_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 high-value config control coverage 無效:{exc}", + ) from exc diff --git a/apps/api/src/services/iwooos_high_value_config_control_coverage.py b/apps/api/src/services/iwooos_high_value_config_control_coverage.py new file mode 100644 index 000000000..714db3236 --- /dev/null +++ b/apps/api/src/services/iwooos_high_value_config_control_coverage.py @@ -0,0 +1,213 @@ +""" +IwoooS 高價值配置控管覆蓋矩陣只讀讀回。 + +此服務只回傳已提交 snapshot 的公開安全投影,不查 live host、Nginx、 +DNS、Kubernetes、Docker、runner、scanner、Telegram 或 secret store。 +""" + +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__)) +_SCHEMA_VERSION = "iwooos_high_value_config_control_coverage_v1" +_SOURCE_SCHEMA_VERSION = "high_value_config_control_coverage_v1" +_SNAPSHOT_NAME = "high-value-config-control-coverage.snapshot.json" + +_PUBLIC_LABELS = { + "nginx_public_gateway": "Nginx / 公開入口 / 反向代理", + "dns_tls_certbot": "DNS / TLS / 憑證續期", + "k8s_production_gitops": "K8s / ArgoCD / 正式環境宣告", + "secret_metadata": "機密中繼資料 / 注入 / 遮罩", + "gitea_workflow_runner_source_control": "Gitea 工作流程 / 執行器 / 原始碼控制", + "public_admin_api_runtime_config": "公開 / 後台 / API runtime config", + "backup_restore_credential": "備份 / 還原 / 金庫", + "agent_bounty_protocol_runtime": "代理賞金協議 runtime / MCP / A2A / 金流邊界", + "monitoring_alerting_observability": "監控 / 告警 / 可觀測性", + "docker_compose_systemd_host_config": "Docker / systemd / 主機服務配置", + "ssh_firewall_network_access": "SSH / 防火牆 / 網路存取", + "ai_provider_model_routing": "AI 供應商 / 模型路由", + "product_surface_runtime_routes": "產品前後台 / 路由 / webhook", + "security_evidence_tooling": "資安證據 / snapshot / guard 工具", +} + +_FALSE_BOUNDARY_KEYS = { + "runtime_execution_authorized", + "host_write_authorized", + "host_live_conf_read_authorized", + "nginx_test_authorized", + "nginx_reload_authorized", + "public_gateway_reload_authorized", + "dns_tls_change_authorized", + "certbot_renew_authorized", + "argocd_sync_authorized", + "kubectl_action_authorized", + "workflow_modification_authorized", + "runner_change_authorized", + "secret_value_collection_allowed", + "backup_run_authorized", + "restore_run_authorized", + "active_scan_authorized", + "agent_bounty_runtime_authorized", + "payout_or_withdrawal_authorized", + "action_buttons_allowed", +} + + +def load_latest_iwooos_high_value_config_control_coverage( + security_dir: Path | None = None, +) -> dict[str, Any]: + """載入已提交的高價值配置控管 snapshot。""" + sec_dir = security_dir or _DEFAULT_SECURITY_DIR + snapshot = _load_json(sec_dir / _SNAPSHOT_NAME) + _validate_snapshot(snapshot) + + categories = snapshot.get("coverage_categories") or [] + category_by_id = { + category["category_id"]: _public_category(category) + for category in categories + if isinstance(category, dict) and category.get("category_id") + } + priority_lanes = [ + category_by_id[category_id] + for category_id in snapshot.get("next_collection_order") or [] + if category_id in category_by_id + ] + + return { + "schema_version": _SCHEMA_VERSION, + "source_schema_version": _SOURCE_SCHEMA_VERSION, + "status": "coverage_matrix_ready_no_runtime_action", + "mode": "committed_snapshot_projection_only_no_live_runtime_query", + "summary": snapshot["summary"], + "priority_lanes": priority_lanes, + "lowest_coverage_categories": [ + category_by_id[item["category_id"]] + for item in snapshot.get("lowest_coverage_categories") or [] + if isinstance(item, dict) and item.get("category_id") in category_by_id + ], + "boundary_markers": _build_boundary_markers(snapshot), + "execution_boundaries": _public_boundaries(snapshot), + "no_false_green_rules": [ + "高價值配置已註冊不代表 Nginx 可 reload、ArgoCD 可 sync、workflow 可修改或主機可寫入。", + "Nginx、DNS / TLS、K8s、secret、runner、防火牆、備份與 AI provider 只能先收 owner-provided redacted evidence。", + "前台顯示、API 200、snapshot 可讀或一般工作批准,都不能當成 runtime gate 已開。", + "owner response received / accepted、live evidence accepted、runtime gate 與 action button 必須維持 0,直到獨立驗收通過。", + ], + "source_refs": [ + "docs/security/high-value-config-control-coverage.snapshot.json", + "scripts/security/high-value-config-control-coverage.py", + "scripts/security/iwooos-config-control-guard.py", + ], + } + + +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") + return payload + + +def _validate_snapshot(snapshot: dict[str, Any]) -> None: + if snapshot.get("schema_version") != _SOURCE_SCHEMA_VERSION: + raise ValueError("high-value config coverage schema mismatch") + + summary = snapshot.get("summary") or {} + required_summary = { + "category_count": 14, + "c0_category_count": 8, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "runtime_gate_count": 0, + "action_button_count": 0, + } + for key, expected in required_summary.items(): + if summary.get(key) != expected: + raise ValueError(f"summary.{key} must be {expected!r}") + + boundaries = snapshot.get("execution_boundaries") or {} + opened = sorted(key for key in _FALSE_BOUNDARY_KEYS if boundaries.get(key) is not False) + if opened: + raise ValueError(f"execution boundaries must stay false: {opened}") + + categories = snapshot.get("coverage_categories") or [] + if len(categories) != summary["category_count"]: + raise ValueError("coverage category count mismatch") + + for category in categories: + if not isinstance(category, dict): + raise ValueError("coverage category must be an object") + category_id = category.get("category_id") + if category_id not in _PUBLIC_LABELS: + raise ValueError(f"unknown high-value config category: {category_id!r}") + for key in ( + "owner_response_received", + "owner_response_accepted", + "runtime_gate_open", + "action_buttons_allowed", + ): + if category.get(key) is not False: + raise ValueError(f"{category_id}.{key} must remain false") + + +def _public_category(category: dict[str, Any]) -> dict[str, Any]: + category_id = str(category["category_id"]) + evidence_refs = category.get("evidence_refs") or [] + required_validation = category.get("required_validation") or [] + return { + "category_id": category_id, + "label": _PUBLIC_LABELS[category_id], + "priority": str(category.get("priority") or ""), + "control_tier": str(category.get("control_tier") or ""), + "coverage_percent": _as_int(category.get("coverage_percent")), + "coverage_status": str(category.get("coverage_status") or ""), + "current_gap": str(category.get("current_gap") or ""), + "next_owner_action": str(category.get("next_owner_action") or ""), + "owner_response_required": category.get("owner_response_required") is True, + "owner_response_received": False, + "owner_response_accepted": False, + "runtime_gate_open": False, + "action_buttons_allowed": False, + "evidence_ref_count": len(evidence_refs) if isinstance(evidence_refs, list) else 0, + "required_validation_count": len(required_validation) if isinstance(required_validation, list) else 0, + } + + +def _build_boundary_markers(snapshot: dict[str, Any]) -> list[str]: + summary = snapshot.get("summary") or {} + markers = [ + f"high_value_config_control_coverage_category_count={summary.get('category_count', 0)}", + f"high_value_config_control_coverage_c0_category_count={summary.get('c0_category_count', 0)}", + f"high_value_config_control_coverage_c1_category_count={summary.get('c1_category_count', 0)}", + f"high_value_config_control_coverage_average_percent={summary.get('average_coverage_percent', 0)}", + f"high_value_config_control_coverage_needs_live_evidence_count={summary.get('needs_live_evidence_count', 0)}", + f"high_value_config_control_coverage_owner_response_required_count={summary.get('owner_response_required_count', 0)}", + "high_value_config_control_coverage_owner_response_received_count=0", + "high_value_config_control_coverage_owner_response_accepted_count=0", + "high_value_config_control_coverage_runtime_gate_count=0", + "high_value_config_control_coverage_action_button_count=0", + ] + return markers + + +def _public_boundaries(snapshot: dict[str, Any]) -> dict[str, bool]: + source_boundaries = snapshot.get("execution_boundaries") or {} + boundaries = { + key: False for key in sorted(_FALSE_BOUNDARY_KEYS) + } + for key in boundaries: + if source_boundaries.get(key) is not False: + raise ValueError(f"execution boundary must stay false: {key}") + boundaries[key] = False + boundaries["not_authorization"] = True + return boundaries + + +def _as_int(value: Any) -> int: + return value if isinstance(value, int) else 0 diff --git a/apps/api/tests/test_iwooos_high_value_config_control_coverage.py b/apps/api/tests/test_iwooos_high_value_config_control_coverage.py new file mode 100644 index 000000000..7ea6e174e --- /dev/null +++ b/apps/api/tests/test_iwooos_high_value_config_control_coverage.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from src.api.v1.iwooos import router +from src.services.iwooos_high_value_config_control_coverage import ( + load_latest_iwooos_high_value_config_control_coverage, +) + + +def _client() -> TestClient: + app = FastAPI() + app.include_router(router) + return TestClient(app) + + +def test_iwooos_high_value_config_control_coverage_prioritizes_nginx_and_configs() -> None: + payload = load_latest_iwooos_high_value_config_control_coverage() + + assert payload["schema_version"] == "iwooos_high_value_config_control_coverage_v1" + assert payload["status"] == "coverage_matrix_ready_no_runtime_action" + assert payload["summary"]["category_count"] == 14 + assert payload["summary"]["c0_category_count"] == 8 + assert payload["summary"]["average_coverage_percent"] == 73 + assert payload["summary"]["owner_response_received_count"] == 0 + assert payload["summary"]["owner_response_accepted_count"] == 0 + assert payload["summary"]["runtime_gate_count"] == 0 + assert payload["summary"]["action_button_count"] == 0 + + priority_ids = [lane["category_id"] for lane in payload["priority_lanes"]] + assert priority_ids[:6] == [ + "nginx_public_gateway", + "dns_tls_certbot", + "k8s_production_gitops", + "secret_metadata", + "gitea_workflow_runner_source_control", + "public_admin_api_runtime_config", + ] + assert payload["priority_lanes"][0]["label"] == "Nginx / 公開入口 / 反向代理" + assert payload["priority_lanes"][0]["coverage_percent"] == 92 + assert payload["priority_lanes"][0]["runtime_gate_open"] is False + + +def test_iwooos_high_value_config_control_coverage_keeps_boundaries_closed() -> None: + payload = load_latest_iwooos_high_value_config_control_coverage() + + boundaries = payload["execution_boundaries"] + assert boundaries["not_authorization"] is True + for key, value in boundaries.items(): + if key == "not_authorization": + continue + assert value is False + + assert all(lane["owner_response_received"] is False for lane in payload["priority_lanes"]) + assert all(lane["owner_response_accepted"] is False for lane in payload["priority_lanes"]) + assert all(lane["runtime_gate_open"] is False for lane in payload["priority_lanes"]) + assert all(lane["action_buttons_allowed"] is False for lane in payload["priority_lanes"]) + assert "high_value_config_control_coverage_runtime_gate_count=0" in payload["boundary_markers"] + assert "high_value_config_control_coverage_action_button_count=0" in payload["boundary_markers"] + + +def test_iwooos_high_value_config_control_coverage_api_is_public_safe() -> None: + response = _client().get("/api/v1/iwooos/high-value-config-control-coverage") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "iwooos_high_value_config_control_coverage_v1" + assert data["summary"]["runtime_gate_count"] == 0 + assert data["priority_lanes"][0]["category_id"] == "nginx_public_gateway" + assert data["priority_lanes"][0]["label"] == "Nginx / 公開入口 / 反向代理" + assert "192.168.0." not in response.text + assert "工作視窗" not in response.text + assert "批准!繼續" not in response.text + assert "source_thread_id" not in response.text + assert "owenhytsai/" not in response.text + assert "raw Wazuh payload" not in response.text diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 93e971d3e..f98d09c63 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -21291,6 +21291,11 @@ "coverageLabel": "只讀成熟度", "boundaryTitle": "配置覆蓋矩陣邊界", "boundaryIntro": "以下鍵值固定:矩陣只顯示分類、證據與下一步收件順序,不代表 live evidence 已取得,也不代表 reload、sync、scan、secret rotation、agent-bounty runtime 或主機操作已授權。", + "apiStatus": { + "loading": "正在讀取只讀 API", + "ready": "只讀 API 已接上", + "fallback": "API 未回,使用靜態 fallback" + }, "summary": { "categories": { "label": "註冊類別", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 93e971d3e..f98d09c63 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -21291,6 +21291,11 @@ "coverageLabel": "只讀成熟度", "boundaryTitle": "配置覆蓋矩陣邊界", "boundaryIntro": "以下鍵值固定:矩陣只顯示分類、證據與下一步收件順序,不代表 live evidence 已取得,也不代表 reload、sync、scan、secret rotation、agent-bounty runtime 或主機操作已授權。", + "apiStatus": { + "loading": "正在讀取只讀 API", + "ready": "只讀 API 已接上", + "fallback": "API 未回,使用靜態 fallback" + }, "summary": { "categories": { "label": "註冊類別", diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index 1a4ab1443..6dcac352f 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -35,6 +35,8 @@ import { useEffect, useRef, useState, type ReactNode } from 'react' import { AppLayout } from '@/components/layout' import { apiClient, + type IwoooSHighValueConfigControlCoverageCategory, + type IwoooSHighValueConfigControlCoverageResponse, type IwoooSRuntimeSecurityReadbackResponse, type IwoooSSecurityControlCoverageDomain, type IwoooSSecurityControlCoverageResponse, @@ -407,6 +409,11 @@ type HighValueConfigControlCoverageItem = { tone: 'steady' | 'warn' | 'locked' } +type HighValueConfigControlCoverageDisplayItem = HighValueConfigControlCoverageItem & { + title?: string + body?: string +} + type CriticalConfigPriorityItem = { key: string rank: string @@ -2822,6 +2829,21 @@ const highValueConfigControlCoverageItems: HighValueConfigControlCoverageItem[] { key: 'backupRestore', rank: 'P1-5', value: '66%', icon: Database, tone: 'warn' }, ] +const highValueConfigControlCoverageIconByCategoryId: Record = { + nginx_public_gateway: Route, + dns_tls_certbot: Cloud, + k8s_production_gitops: Workflow, + secret_metadata: Lock, + gitea_workflow_runner_source_control: GitBranch, + public_admin_api_runtime_config: Code2, + ssh_firewall_network_access: Network, + docker_compose_systemd_host_config: Server, + monitoring_alerting_observability: Bell, + agent_bounty_protocol_runtime: ClipboardCheck, + ai_provider_model_routing: Cloud, + backup_restore_credential: Database, +} + const highValueConfigControlCoverageBoundaries = [ 'high_value_config_control_coverage_frontstage_summary_count=4', 'high_value_config_control_coverage_frontstage_item_count=5', @@ -10413,6 +10435,78 @@ function IwoooSCriticalConfigPriorityBoard() { function IwoooSHighValueConfigControlCoverageBoard() { const t = useTranslations('iwooos.highValueConfigControlCoverage') const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const } + const [data, setData] = useState(null) + const [failed, setFailed] = useState(false) + + useEffect(() => { + let mounted = true + + async function loadCoverage() { + setFailed(false) + try { + const payload = await apiClient.getIwoooSHighValueConfigControlCoverage() + if (mounted) { + setData(payload) + } + } catch { + if (mounted) { + setData(null) + setFailed(true) + } + } + } + + loadCoverage() + return () => { + mounted = false + } + }, []) + + const summary = data?.summary + const summaryItems = [ + { + key: 'categories', + value: summary ? String(summary.category_count) : highValueConfigControlCoverageSummary[0].value, + icon: ListChecks, + tone: 'steady' as const, + }, + { + key: 'c0', + value: summary ? String(summary.c0_category_count) : highValueConfigControlCoverageSummary[1].value, + icon: AlertTriangle, + tone: 'warn' as const, + }, + { + key: 'coverage', + value: summary ? `${summary.average_coverage_percent}%` : highValueConfigControlCoverageSummary[2].value, + icon: ShieldCheck, + tone: 'warn' as const, + }, + { + key: 'runtimeGate', + value: summary ? String(summary.runtime_gate_count) : highValueConfigControlCoverageSummary[3].value, + icon: Lock, + tone: 'locked' as const, + }, + ] + const dynamicItems: HighValueConfigControlCoverageDisplayItem[] = (data?.priority_lanes ?? []) + .slice(0, 6) + .map((item: IwoooSHighValueConfigControlCoverageCategory, index: number) => ({ + key: item.category_id, + rank: item.priority || `P0-${index + 1}`, + value: `${item.coverage_percent}%`, + title: item.label, + body: item.next_owner_action, + icon: highValueConfigControlCoverageIconByCategoryId[item.category_id] ?? ShieldCheck, + tone: item.runtime_gate_open ? 'steady' : item.control_tier === 'C0' ? 'warn' : 'locked', + })) + const displayItems: HighValueConfigControlCoverageDisplayItem[] = dynamicItems.length + ? dynamicItems + : highValueConfigControlCoverageItems + const boundaryMarkers = data?.boundary_markers?.length + ? data.boundary_markers + : highValueConfigControlCoverageBoundaries + const statusTone: 'steady' | 'warn' | 'locked' = failed ? 'warn' : data ? 'locked' : 'warn' return (
@@ -10427,10 +10521,14 @@ function IwoooSHighValueConfigControlCoverageBoard() {

{t('subtitle')}

+
+ + {data ? t('apiStatus.ready') : failed ? t('apiStatus.fallback') : t('apiStatus.loading')} +
- {highValueConfigControlCoverageSummary.map(item => { + {summaryItems.map(item => { const Icon = item.icon return (
@@ -10458,7 +10556,7 @@ function IwoooSHighValueConfigControlCoverageBoard() { gap: 10, }} > - {highValueConfigControlCoverageItems.map(item => { + {displayItems.map(item => { const Icon = item.icon return (
- {t(`items.${item.key}.title` as never)} + {item.title ?? t(`items.${item.key}.title` as never)}
{t('coverageLabel')}:{item.value}

- {t(`items.${item.key}.body` as never)} + {item.body ?? t(`items.${item.key}.body` as never)}

) @@ -10514,7 +10612,7 @@ function IwoooSHighValueConfigControlCoverageBoard() { {t('boundaryIntro')}

- {highValueConfigControlCoverageBoundaries.map(item => ( + {boundaryMarkers.map(item => ( + no_false_green_rules: string[] + source_refs: string[] +} + async function handleResponse(response: Response): Promise { if (!response.ok) { const error = await response.json().catch(() => ({})) @@ -388,6 +435,11 @@ export const apiClient = { return handleResponse(res) }, + async getIwoooSHighValueConfigControlCoverage() { + const res = await fetch(`${API_BASE_URL}/iwooos/high-value-config-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 e6ceeaf6d..b04322499 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -46795,3 +46795,44 @@ production browser smoke: - DR credential escrow evidence 仍缺 `5`:不得宣稱 `DR_COMPLETE`。 - Wazuh manager registry accepted 仍為 `0`:不得宣稱 Wazuh 全主機納管恢復。 - Runtime action / host write / credential marker write / Wazuh active response / Kali active scan 仍全部 `0 / false`。 + +## 2026-06-27 — 13:11 IwoooS 高價值配置控管 API 化本地完成 + +**時間與來源**: +- 2026-06-27 13:11 Asia/Taipei。 +- 來源:`docs/security/high-value-config-control-coverage.snapshot.json`、新增 `apps/api/src/services/iwooos_high_value_config_control_coverage.py`、`GET /api/v1/iwooos/high-value-config-control-coverage`、`apps/web/src/app/[locale]/iwooos/page.tsx`、`scripts/security/iwooos-config-control-guard.py`。 + +**完成內容**: +- 將高價值配置控管 snapshot 做成 IwoooS 公開安全只讀 API:`GET /api/v1/iwooos/high-value-config-control-coverage`。 +- API 只回傳公開安全投影,優先順序固定為 Nginx / 公開入口、DNS / TLS、K8s / ArgoCD、secret metadata、Gitea workflow / runner / source control、公開 / 後台 / API runtime config。 +- 前台「高價值配置覆蓋矩陣」改為讀取只讀 API;API 未回時才使用靜態 fallback,且前台只顯示繁中 label、覆蓋率、下一步收件與固定邊界,不顯示 raw status、內網 IP、個人 namespace、工作視窗內容或 secret。 +- `scripts/security/iwooos-config-control-guard.py` 新增 API / 前台讀回路徑檢查,要求 service、router、test、API client 與 IwoooS 頁面都存在對應標記。 +- 新增 API 測試,驗證 Nginx 被排為第一優先、owner response / runtime gate / action button 維持 0、公開輸出不含內網 IP、工作視窗內容、`source_thread_id`、個人 repo namespace 或 raw Wazuh payload。 + +**本地驗證結果**: +- `python3 -m py_compile apps/api/src/services/iwooos_high_value_config_control_coverage.py apps/api/src/api/v1/iwooos.py scripts/security/iwooos-config-control-guard.py`:通過。 +- `python3 -m json.tool apps/web/messages/zh-TW.json`、`python3 -m json.tool apps/web/messages/en.json`:通過。 +- `env DATABASE_URL=postgresql+asyncpg://test:test@localhost:5432/test MIGRATION_DATABASE_URL=postgresql://test:test@localhost:5432/test REDIS_URL=redis://localhost:6379/0 JWT_SECRET=test WEBHOOK_HMAC_SECRET=test AWOOOP_OPERATOR_API_KEY=test pytest apps/api/tests/test_iwooos_high_value_config_control_coverage.py apps/api/tests/test_iwooos_security_control_coverage.py apps/api/tests/test_iwooos_runtime_security_readback.py`:`12 passed`。 +- `python3 scripts/security/iwooos-config-control-guard.py --root .`:`IWOOOS_CONFIG_CONTROL_GUARD_OK`。 +- `python3 scripts/security/iwooos-frontend-display-redaction-guard.py --root .`:`IWOOOS_FRONTEND_DISPLAY_REDACTION_GUARD_OK`。 +- `pnpm --dir apps/web typecheck`:通過。 +- `python3 scripts/security/security-mirror-progress-guard.py --root .`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 +- `git diff --check`:通過。 + +**完成度與同步狀態**: +- 本段「高價值配置控管 API 化 / 前台動態讀回」:`85% -> 100%` 本地完成,待 commit / push / CD / production readback。 +- IwoooS 整體:保守維持約 `64-65%`,因為此段提高配置控管可視性,但不代表 runtime 授權或 live 修復完成。 +- 高價值配置控管框架:`92% -> 94%`,Nginx / DNS / K8s / secret / runner / runtime config 已可被 IwoooS 前台動態讀回。 + +**仍維持 0 / false**: +- `owner_response_received_count=0`、`owner_response_accepted_count=0`、`runtime_gate_count=0`、`action_button_count=0`。 +- `runtime_execution_authorized=false`、`host_write_authorized=false`、`nginx_reload_authorized=false`、`argocd_sync_authorized=false`、`kubectl_action_authorized=false`、`workflow_modification_authorized=false`、`runner_change_authorized=false`、`secret_value_collection_allowed=false`、`backup_run_authorized=false`、`restore_run_authorized=false`、`active_scan_authorized=false`。 + +**做過的命令類型**: +- 寫入:repo API service / router / tests / frontend API client / IwoooS page / i18n / guard / LOGBOOK。 +- 只讀:snapshot readback、本地測試與 source guard。 +- 未做:沒有 host / Docker / systemd / Nginx / firewall / K8s / DB / Wazuh runtime 寫操作;沒有讀 secret 明文;沒有 Nginx reload;沒有 ArgoCD sync;沒有 workflow 修改;沒有 Wazuh active response;沒有 Kali active scan。 + +**下一步**: +- commit / push 後等待 Gitea CD;CD 完成後驗證正式 API schema、Nginx 第一優先、前台桌機 / 手機無水平溢出、禁止字串不出現。 +- 下一個 P0 仍是 owner-provided redacted evidence intake、Wazuh manager registry accepted、Nginx live diff evidence gate 與 controlled apply 前置 verifier。 diff --git a/scripts/security/iwooos-config-control-guard.py b/scripts/security/iwooos-config-control-guard.py index f279d1697..39e086946 100644 --- a/scripts/security/iwooos-config-control-guard.py +++ b/scripts/security/iwooos-config-control-guard.py @@ -1147,6 +1147,14 @@ def assert_path_exists(root: Path, relative_path: str) -> None: fail(f"path missing: {relative_path}") +def assert_file_contains(root: Path, relative_path: str, marker: str) -> None: + path = root / relative_path + assert_path_exists(root, relative_path) + text = path.read_text(encoding="utf-8") + if marker not in text: + fail(f"{relative_path}: missing marker {marker!r}") + + def assert_summary_zero_boundaries(label: str, summary: dict[str, Any]) -> None: for key, value in summary.items(): if not isinstance(value, int): @@ -1250,11 +1258,40 @@ def validate_supply_chain_manifest(root: Path) -> None: fail(f"supply_chain.{contract}.consumption_mode: unsafe value {item.get('consumption_mode')!r}") +def validate_high_value_config_api_path(root: Path) -> None: + assert_file_contains( + root, + "apps/api/src/services/iwooos_high_value_config_control_coverage.py", + "iwooos_high_value_config_control_coverage_v1", + ) + assert_file_contains( + root, + "apps/api/src/api/v1/iwooos.py", + "/api/v1/iwooos/high-value-config-control-coverage", + ) + assert_file_contains( + root, + "apps/api/tests/test_iwooos_high_value_config_control_coverage.py", + "test_iwooos_high_value_config_control_coverage_api_is_public_safe", + ) + assert_file_contains( + root, + "apps/web/src/lib/api-client.ts", + "getIwoooSHighValueConfigControlCoverage", + ) + assert_file_contains( + root, + "apps/web/src/app/[locale]/iwooos/page.tsx", + "apiClient.getIwoooSHighValueConfigControlCoverage", + ) + + def validate(root: Path) -> None: for relative_path in REQUIRED_CONTROL_DOCS: assert_path_exists(root, relative_path) validate_coverage_snapshot(root) validate_supply_chain_manifest(root) + validate_high_value_config_api_path(root) for spec in ARTIFACT_SPECS: validate_artifact_spec(root, spec)