From 6a83ae48a1358316acce19ba9d11de8a0424c196 Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 24 Jun 2026 21:19:23 +0800 Subject: [PATCH 01/11] =?UTF-8?q?fix(iwooos):=20=E6=8E=A5=E4=B8=8A=20Wazuh?= =?UTF-8?q?=20=E5=8F=AA=E8=AE=80=20API=20=E9=82=8A=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/api/v1/iwooos.py | 165 +++++++++ apps/api/src/main.py | 2 + apps/api/tests/test_iwooos_wazuh_api.py | 127 +++++++ docs/LOGBOOK.md | 37 ++ ...OOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md | 144 ++++++++ .../wazuh-readonly-release-gate.snapshot.json | 96 ++++++ .../security-mirror-progress-guard.py | 8 + .../wazuh-readonly-production-readback.py | 184 ++++++++++ .../security/wazuh-readonly-release-gate.py | 186 ++++++++++ .../wazuh-readonly-route-boundary-guard.py | 326 ++++++++++++++++++ 10 files changed, 1275 insertions(+) create mode 100644 apps/api/src/api/v1/iwooos.py create mode 100644 apps/api/tests/test_iwooos_wazuh_api.py create mode 100644 docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md create mode 100644 docs/security/wazuh-readonly-release-gate.snapshot.json create mode 100644 scripts/security/wazuh-readonly-production-readback.py create mode 100644 scripts/security/wazuh-readonly-release-gate.py create mode 100644 scripts/security/wazuh-readonly-route-boundary-guard.py diff --git a/apps/api/src/api/v1/iwooos.py b/apps/api/src/api/v1/iwooos.py new file mode 100644 index 000000000..b26baa774 --- /dev/null +++ b/apps/api/src/api/v1/iwooos.py @@ -0,0 +1,165 @@ +""" +IwoooS 安全治理 API。 + +Wazuh 接線採用只讀 metadata 模式:預設關閉、不保存 raw payload、 +不公開 agent 原名 / 內網 IP、不啟用 active response。 +""" + +from __future__ import annotations + +import os +from base64 import b64encode +from typing import Any +from urllib.parse import urljoin, urlparse + +import httpx +from fastapi import APIRouter +from fastapi.responses import JSONResponse + + +router = APIRouter(tags=["IwoooS Security"]) +REQUEST_TIMEOUT_SECONDS = 5.0 + + +def _wazuh_env() -> dict[str, str]: + return { + "enabled": os.getenv("IWOOOS_WAZUH_READONLY_ENABLED", "").strip().lower(), + "base_url": os.getenv("WAZUH_API_BASE_URL", "").strip(), + "username": os.getenv("WAZUH_API_USERNAME", "").strip(), + "password": os.getenv("WAZUH_API_PASSWORD", "").strip(), + } + + +def _https_url(value: str) -> str | None: + parsed = urlparse(value) + if parsed.scheme != "https" or not parsed.netloc: + return None + return value.rstrip("/") + "/" + + +def _boundary_response(status_text: str, http_status: int = 200) -> JSONResponse: + return JSONResponse( + status_code=http_status, + content={ + "schema_version": "iwooos_wazuh_readonly_status_v1", + "status": status_text, + "mode": "metadata_only_no_active_response_no_raw_payload", + "configured": False, + "summary": { + "wazuh_platform_reported_count": 1, + "readonly_api_enabled_count": 0, + "wazuh_manager_query_accepted_count": 0, + "wazuh_event_accepted_count": 0, + "host_forensics_accepted_count": 0, + "active_response_authorized_count": 0, + "host_write_authorized_count": 0, + "runtime_gate_count": 0, + }, + "boundaries": _boundaries(), + }, + ) + + +def _boundaries() -> dict[str, bool]: + return { + "active_response_authorized": False, + "host_write_authorized": False, + "secret_value_collection_allowed": False, + "raw_wazuh_payload_storage_allowed": False, + "agent_identity_public_display_allowed": False, + "internal_ip_public_display_allowed": False, + "not_authorization": True, + } + + +def _redacted_agent(agent: dict[str, Any], index: int) -> dict[str, Any]: + os_info = agent.get("os") if isinstance(agent.get("os"), dict) else {} + return { + "alias": f"agent-{index + 1:02d}", + "status": agent.get("status", "unknown"), + "os": os_info.get("platform") or os_info.get("name") or "unknown", + "last_seen_present": bool(agent.get("lastKeepAlive")), + } + + +async def _fetch_json(client: httpx.AsyncClient, url: str, headers: dict[str, str]) -> dict[str, Any]: + response = await client.get(url, headers=headers) + response.raise_for_status() + payload = response.json() + return payload if isinstance(payload, dict) else {} + + +async def _wazuh_readonly_status() -> JSONResponse: + env = _wazuh_env() + if env["enabled"] != "true": + return _boundary_response("disabled_waiting_iwooos_wazuh_owner_gate") + + base_url = _https_url(env["base_url"]) + if not base_url or not env["username"] or not env["password"]: + return _boundary_response("misconfigured_missing_server_side_wazuh_env", 503) + + try: + auth_header = b64encode(f"{env['username']}:{env['password']}".encode("utf-8")).decode("ascii") + async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT_SECONDS) as client: + auth = await _fetch_json( + client, + urljoin(base_url, "security/user/authenticate"), + {"Authorization": f"Basic {auth_header}"}, + ) + token = (auth.get("data") or {}).get("token") + if not token: + return _boundary_response("wazuh_auth_token_missing", 502) + + bearer_headers = {"Authorization": f"Bearer {token}"} + status_payload = await _fetch_json( + client, + urljoin(base_url, "agents/summary/status"), + bearer_headers, + ) + agents_payload = await _fetch_json( + client, + urljoin(base_url, "agents?limit=100&select=id,status,os.name,os.platform,lastKeepAlive"), + bearer_headers, + ) + except (httpx.HTTPError, ValueError): + return _boundary_response("wazuh_readonly_metadata_unavailable", 502) + + connection = ((status_payload.get("data") or {}).get("connection") or {}) + affected_items = ((agents_payload.get("data") or {}).get("affected_items") or []) + if not isinstance(affected_items, list): + affected_items = [] + + return JSONResponse( + content={ + "schema_version": "iwooos_wazuh_readonly_status_v1", + "status": "readonly_metadata_available", + "mode": "metadata_only_no_active_response_no_raw_payload", + "configured": True, + "summary": { + "wazuh_platform_reported_count": 1, + "readonly_api_enabled_count": 1, + "agent_total": connection.get("total", len(affected_items)), + "agent_active": connection.get("active", 0), + "agent_disconnected": connection.get("disconnected", 0), + "agent_pending": connection.get("pending", 0), + "wazuh_manager_query_accepted_count": 0, + "wazuh_event_accepted_count": 0, + "host_forensics_accepted_count": 0, + "active_response_authorized_count": 0, + "host_write_authorized_count": 0, + "runtime_gate_count": 0, + }, + "agents": [_redacted_agent(agent, index) for index, agent in enumerate(affected_items[:20])], + "boundaries": _boundaries(), + }, + ) + + +@router.get("/api/iwooos/wazuh") +async def get_iwooos_wazuh_readonly_status_compat() -> JSONResponse: + return await _wazuh_readonly_status() + + +@router.get("/api/v1/iwooos/wazuh") +async def get_iwooos_wazuh_readonly_status_v1() -> JSONResponse: + return await _wazuh_readonly_status() diff --git a/apps/api/src/main.py b/apps/api/src/main.py index e357289e7..d8eac3a01 100644 --- a/apps/api/src/main.py +++ b/apps/api/src/main.py @@ -60,6 +60,7 @@ from src.api.v1 import ( # Import API routers from src.api.v1 import health as health_v1 from src.api.v1 import incidents as incidents_v1 # Phase 6.4: Decision Proposal +from src.api.v1 import iwooos as iwooos_v1 # IwoooS security governance API from src.api.v1 import knowledge as knowledge_v1 # KB Phase 1: Knowledge Base from src.api.v1 import learning as learning_v1 # Phase D-G P0: Learning API from src.api.v1 import metrics as metrics_v1 # Phase 7: Gold Metrics (真實血脈) @@ -1035,6 +1036,7 @@ async def global_exception_handler(_request: Request, exc: Exception) -> JSONRes # ============================================================================= # New v1 API routes +app.include_router(iwooos_v1.router, tags=["IwoooS Security"]) app.include_router(health_v1.router, prefix="/api/v1", tags=["Health"]) app.include_router(csrf_v1.router, prefix="/api/v1", tags=["Security"]) # Phase 20 app.include_router(dashboard_v1.router, prefix="/api/v1", tags=["Dashboard"]) diff --git a/apps/api/tests/test_iwooos_wazuh_api.py b/apps/api/tests/test_iwooos_wazuh_api.py new file mode 100644 index 000000000..c00b21eec --- /dev/null +++ b/apps/api/tests/test_iwooos_wazuh_api.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import httpx +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from src.api.v1.iwooos import router + + +def _client() -> TestClient: + app = FastAPI() + app.include_router(router) + return TestClient(app) + + +def test_iwooos_wazuh_compat_route_returns_disabled_boundary_by_default(monkeypatch: pytest.MonkeyPatch): + monkeypatch.delenv("IWOOOS_WAZUH_READONLY_ENABLED", raising=False) + monkeypatch.delenv("WAZUH_API_BASE_URL", raising=False) + monkeypatch.delenv("WAZUH_API_USERNAME", raising=False) + monkeypatch.delenv("WAZUH_API_PASSWORD", raising=False) + + response = _client().get("/api/iwooos/wazuh") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "iwooos_wazuh_readonly_status_v1" + assert data["status"] == "disabled_waiting_iwooos_wazuh_owner_gate" + assert data["configured"] is False + assert data["summary"]["runtime_gate_count"] == 0 + assert data["boundaries"]["active_response_authorized"] is False + assert data["boundaries"]["host_write_authorized"] is False + assert data["boundaries"]["raw_wazuh_payload_storage_allowed"] is False + assert data["boundaries"]["internal_ip_public_display_allowed"] is False + + +def test_iwooos_wazuh_v1_route_rejects_missing_server_side_env(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("IWOOOS_WAZUH_READONLY_ENABLED", "true") + monkeypatch.setenv("WAZUH_API_BASE_URL", "") + monkeypatch.setenv("WAZUH_API_USERNAME", "") + monkeypatch.setenv("WAZUH_API_PASSWORD", "") + + response = _client().get("/api/v1/iwooos/wazuh") + + assert response.status_code == 503 + data = response.json() + assert data["status"] == "misconfigured_missing_server_side_wazuh_env" + assert data["configured"] is False + assert data["summary"]["runtime_gate_count"] == 0 + + +def test_iwooos_wazuh_rejects_non_https_base_url(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("IWOOOS_WAZUH_READONLY_ENABLED", "true") + monkeypatch.setenv("WAZUH_API_BASE_URL", "http://wazuh.example.test:55000") + monkeypatch.setenv("WAZUH_API_USERNAME", "readonly") + monkeypatch.setenv("WAZUH_API_PASSWORD", "placeholder") + + response = _client().get("/api/iwooos/wazuh") + + assert response.status_code == 503 + data = response.json() + assert data["status"] == "misconfigured_missing_server_side_wazuh_env" + assert data["boundaries"]["secret_value_collection_allowed"] is False + + +def test_iwooos_wazuh_live_response_is_metadata_only(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("IWOOOS_WAZUH_READONLY_ENABLED", "true") + monkeypatch.setenv("WAZUH_API_BASE_URL", "https://wazuh.example.test:55000") + monkeypatch.setenv("WAZUH_API_USERNAME", "readonly") + monkeypatch.setenv("WAZUH_API_PASSWORD", "placeholder") + + def handler(request: httpx.Request) -> httpx.Response: + if request.url.path == "/security/user/authenticate": + return httpx.Response(200, json={"data": {"token": "token-value"}}) + if request.url.path == "/agents/summary/status": + return httpx.Response( + 200, + json={"data": {"connection": {"total": 2, "active": 1, "disconnected": 1, "pending": 0}}}, + ) + if request.url.path == "/agents": + return httpx.Response( + 200, + json={ + "data": { + "affected_items": [ + { + "id": "001", + "name": "host-110-private-name", + "ip": "192.168.0.110", + "status": "active", + "os": {"platform": "linux"}, + "lastKeepAlive": "2026-06-24T13:00:00Z", + } + ] + } + }, + ) + return httpx.Response(404) + + transport = httpx.MockTransport(handler) + original_async_client = httpx.AsyncClient + + def client_factory(*args, **kwargs): + kwargs["transport"] = transport + return original_async_client(*args, **kwargs) + + monkeypatch.setattr(httpx, "AsyncClient", client_factory) + + response = _client().get("/api/iwooos/wazuh") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "readonly_metadata_available" + assert data["configured"] is True + assert data["summary"]["agent_total"] == 2 + assert data["summary"]["runtime_gate_count"] == 0 + assert data["agents"] == [ + { + "alias": "agent-01", + "status": "active", + "os": "linux", + "last_seen_present": True, + } + ] + assert "host-110-private-name" not in response.text + assert "192.168.0.110" not in response.text + assert "token-value" not in response.text diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index d44fbbb88..b89fa4aa2 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -164,6 +164,43 @@ **邊界**:本輪只有 read-only live checks、docs-only 更新與安全 handoff artifact 同步;沒有 Wazuh / SOC UI/API 修改,沒有 SSH 寫主機,沒有 Docker / Nginx / firewall / K8s / ArgoCD runtime 寫入,沒有 active scan,沒有讀 secret,沒有使用或保存聊天中的密碼。 +## 2026-06-24|IwoooS Wazuh public API 404 根因與只讀相容路由收斂 + +**背景**:使用者追問 Wazuh 為什麼仍不能訪問。只讀 production check 顯示 `https://awoooi.wooo.work/api/iwooos/wazuh` 回 `404 {"detail":"Not Found"}`,而 `https://awoooi.wooo.work/zh-TW/iwooos` 回 `200`。判定根因不是 IwoooS 前台不存在,而是 production `/api` 目前落到 FastAPI 後端;既有 Wazuh Next.js API route 沒有被 public gateway 暴露,FastAPI 端也尚未提供相容 route。 + +**完成**: +- 新增 FastAPI 端 `GET /api/iwooos/wazuh` 與 `GET /api/v1/iwooos/wazuh`,回傳與前端 Next.js route 一致的 `iwooos_wazuh_readonly_status_v1`。 +- route 預設回 `disabled_waiting_iwooos_wazuh_owner_gate`,不再需要用 404 表達未啟用狀態。 +- live Wazuh 查詢仍需 `IWOOOS_WAZUH_READONLY_ENABLED=true`、`WAZUH_API_BASE_URL`、`WAZUH_API_USERNAME`、`WAZUH_API_PASSWORD` 全部由 server-side env 提供;未設定或非 HTTPS 時回 `misconfigured_missing_server_side_wazuh_env`。 +- live response 僅回 metadata:agent alias、狀態、OS 類別、last_seen_present 與 aggregate counts;不回 Wazuh raw payload、agent 原名、內網 IP、token 或 secret。 +- 新增 `wazuh-readonly-route-boundary-guard.py`,同時掃 Next.js route、FastAPI route 與 IwoooS 前台,阻擋硬編 Wazuh 內網 URL / port、帳密、`NODE_TLS_REJECT_UNAUTHORIZED`、假 SOC dashboard、假 CVE、raw payload 或 legacy dashboard component 回流。 +- `security-mirror-progress-guard.py` 已直接呼叫此 guard,讓 Wazuh 接線邊界進入既有 IwoooS security mirror gate。 +- 新增 `wazuh-readonly-production-readback.py`,供 release 後驗證 production `/api/iwooos/wazuh` 不再 404,且 schema、status、0 / false 邊界與防洩漏條件都正確;predeploy 404 只能用 `--allow-predeploy-404` 記錄現況,不可當正式驗收。 +- 新增 `wazuh-readonly-release-gate.py` 與 `wazuh-readonly-release-gate.snapshot.json`,固定 source-side 已完成、Gitea push / production deploy / production readback 尚未完成,並由 `security-mirror-progress-guard.py` 驗證。 + +**驗證**: +- `pytest apps/api/tests/test_iwooos_wazuh_api.py` → `4 passed`。 +- `python3 scripts/security/wazuh-readonly-route-boundary-guard.py --root .` → `WAZUH_READONLY_ROUTE_BOUNDARY_GUARD_OK route=2 public_ui_files=1 forbidden=0 runtime_gate=0`。 +- `python3 scripts/security/wazuh-readonly-release-gate.py --root .` → `WAZUH_READONLY_RELEASE_GATE_OK source=1 push=0 deploy=0 readback=0 runtime_gate=0`。 +- `python3 scripts/security/security-mirror-progress-guard.py --root .` → `SECURITY_MIRROR_PROGRESS_GUARD_OK`。 +- `python3 -m py_compile apps/api/src/api/v1/iwooos.py scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/security-mirror-progress-guard.py` 通過。 +- `python3 scripts/security/wazuh-readonly-production-readback.py --allow-predeploy-404 --json` 可記錄尚未部署現況;正式部署後需不加 allow flag,且不得回 404。 +- `git diff --check` 通過。 + +**完成度 / 狀態**: +- Wazuh public API 404 source-side 修補:`100%`。 +- Wazuh route boundary source guard:`100%`。 +- Production readback 驗收腳本:`100%`。 +- Wazuh release gate snapshot / guard:`100%`。 +- Production deploy / readback:`0%`,尚未推送與部署。 +- Wazuh server-side env enable:`0%`,尚未由 secrets / env gate 啟用。 +- Wazuh event refs、host forensic refs、containment decision、recovery proof accepted:全部 `0%`。 +- active response、host write、Kali active scan、firewall / Nginx / Docker / K8s runtime action:全部 `0 / false`。 + +**邊界**:本輪只做 source-side API 相容路由、測試與 guard;沒有 SSH、沒有查 live Wazuh API、沒有讀或保存 secret、沒有改 Nginx / firewall / Docker / K8s、沒有 active scan、沒有 Wazuh active response、沒有 Telegram 實發、沒有 production deploy。 + +**Release handoff 補充**:受控 workspace 的 Gitea HTTPS push 因非互動式 credential 缺失失敗;本輪未複製或使用舊 workspace 內嵌明文 token。已新增 `docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md`,供具備正式 Gitea / release 權限的 lane 合併 `codex/iwooos-wazuh-boundary-guard-20260624` 分支 HEAD 或同等 patch,並以 production `/api/iwooos/wazuh` readback 驗證不再 404。 + ## 2026-06-24|21:04 recovery readback 與 MOMO V10.651 雙機基準收斂 **背景**:前一輪 MOMO workspace readback 指到 `V10.646`,但 21:04 live health 已回 `V10.651`。因此本輪重新比對 Gitea `wooo/ewoooc` `main`、正式站 `/health`、Mac Mini / MacBook Pro Codex workspace 與 full-stack cold-start,避免「網站可用」和「版本 / 資料最新」互相混淆。 diff --git a/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md b/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md new file mode 100644 index 000000000..9b82f4334 --- /dev/null +++ b/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md @@ -0,0 +1,144 @@ +# IwoooS Wazuh 只讀 API Release Handoff + +> 狀態:source-side 修補完成,等待具備正式 Gitea push / release 權限的 lane 合併與部署。 +> 本文件不包含 secret、token、內網 Wazuh URL、raw log、raw Wazuh payload 或工作視窗逐字稿。 + +## 根因判定 + +`https://awoooi.wooo.work/api/iwooos/wazuh` production 目前回 `404 {"detail":"Not Found"}`,同時 `https://awoooi.wooo.work/zh-TW/iwooos` 回 `200`。 + +判定根因: + +- production `/api` 目前落到 FastAPI 後端。 +- 既有 `apps/web/src/app/api/iwooos/wazuh/route.ts` 是 Next.js route,沒有被 public gateway 暴露到這條 production path。 +- FastAPI 後端原本沒有 `/api/iwooos/wazuh` 相容 route,因此回 404。 +- 這個 404 不能被解讀成 Wazuh manager 一定故障,也不能被解讀成 Wazuh 已未安裝。 + +## Source-Side 修補 + +本地 commit: + +- `codex/iwooos-wazuh-boundary-guard-20260624` 分支 HEAD:`fix(iwooos): 接上 Wazuh 只讀 API 邊界` + +變更範圍: + +- `apps/api/src/api/v1/iwooos.py` +- `apps/api/src/main.py` +- `apps/api/tests/test_iwooos_wazuh_api.py` +- `scripts/security/wazuh-readonly-route-boundary-guard.py` +- `scripts/security/wazuh-readonly-production-readback.py` +- `scripts/security/wazuh-readonly-release-gate.py` +- `scripts/security/security-mirror-progress-guard.py` +- `docs/security/wazuh-readonly-release-gate.snapshot.json` +- `docs/LOGBOOK.md` + +完成內容: + +- 新增 FastAPI `GET /api/iwooos/wazuh`。 +- 新增 FastAPI `GET /api/v1/iwooos/wazuh`。 +- 預設回 `disabled_waiting_iwooos_wazuh_owner_gate`,避免 production 繼續用 404 表示未啟用。 +- live Wazuh 查詢仍需 `IWOOOS_WAZUH_READONLY_ENABLED=true` 與 server-side env:`WAZUH_API_BASE_URL`、`WAZUH_API_USERNAME`、`WAZUH_API_PASSWORD`。 +- 強制 Wazuh base URL 使用 HTTPS。 +- 回傳資料只允許 metadata:agent alias、status、OS 類別、last_seen_present 與 aggregate counts。 +- 不回傳 raw Wazuh payload、agent 原名、內網 IP、token、password 或 secret。 +- 新增 source guard,阻擋硬編 Wazuh 內網 URL / port、帳密、關 TLS、假 SOC dashboard、假 CVE、raw payload 與 legacy dashboard component 回流。 +- 新增 production readback 腳本,部署後可直接驗證 public API 不再 404、schema / status / boundary 正確,且沒有 raw payload、內網 IP、agent 原名或 secret 洩漏。 +- 新增 release gate snapshot 與 guard,固定 source-side 已完成、Gitea push / production deploy / production readback 尚未完成,避免後續把 predeploy 404 誤判成通過。 + +## 已完成驗證 + +已在 `/Users/ogt/codex-workspaces/awoooi-dev` 執行: + +```bash +pytest apps/api/tests/test_iwooos_wazuh_api.py +python3 scripts/security/wazuh-readonly-route-boundary-guard.py --root . +python3 scripts/security/wazuh-readonly-release-gate.py --root . +python3 scripts/security/security-mirror-progress-guard.py --root . +python3 scripts/ops/doc-secrets-sanity-check.py docs apps/api/src/api/v1/iwooos.py apps/web/src/app/api/iwooos/wazuh/route.ts scripts/security/wazuh-readonly-route-boundary-guard.py +python3 -m py_compile apps/api/src/api/v1/iwooos.py scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/security-mirror-progress-guard.py +git diff --check +``` + +驗證結果: + +- `pytest apps/api/tests/test_iwooos_wazuh_api.py`:`4 passed`。 +- `wazuh-readonly-route-boundary-guard`:`route=2 public_ui_files=1 forbidden=0 runtime_gate=0`。 +- `wazuh-readonly-release-gate`:`source=1 push=0 deploy=0 readback=0 runtime_gate=0`。 +- `security-mirror-progress-guard`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 +- `doc-secrets-sanity-check`:`DOC_SECRET_SANITY_OK scanned_files=967`。 +- `py_compile`:通過。 +- `git diff --check`:通過。 + +尚未部署前的 production 現況記錄: + +```bash +python3 scripts/security/wazuh-readonly-production-readback.py --allow-predeploy-404 --json +``` + +預期只可回 `status=predeploy_404_observed`。正式部署驗收不得加 `--allow-predeploy-404`。 + +## Release 前 Gate + +合併 / 部署前需確認: + +- 使用具備正式權限的 Gitea lane 合併 `codex/iwooos-wazuh-boundary-guard-20260624` 分支 HEAD 或同等 patch;不得 force push。 +- 不得複製舊 workspace 的內嵌明文 Gitea token。 +- 不得把 Wazuh URL、帳密、token、cookie、private key、runner token 或 webhook secret 寫入 repo。 +- 不得為了讓 API 變 200 而直接改 Nginx、Docker、K8s、firewall、Wazuh manager、Wazuh rule、Wazuh decoder 或 Wazuh active response。 +- 若要啟用 live metadata query,必須由正式 secrets / env 注入 `IWOOOS_WAZUH_READONLY_ENABLED=true` 與 Wazuh server-side env,且要先有 owner gate。 + +## Production Readback 預期 + +部署後、尚未啟用 Wazuh env 時: + +```bash +curl -sS https://awoooi.wooo.work/api/iwooos/wazuh +``` + +預期: + +- HTTP `200`。 +- `schema_version=iwooos_wazuh_readonly_status_v1`。 +- `status=disabled_waiting_iwooos_wazuh_owner_gate`。 +- `configured=false`。 +- `runtime_gate_count=0`。 +- `active_response_authorized=false`。 +- `host_write_authorized=false`。 +- 不含內網 IP、agent 原名、token、password、raw payload。 + +正式驗收命令: + +```bash +python3 scripts/security/wazuh-readonly-production-readback.py --json +``` + +正式驗收不接受 404;若仍回 404,代表 FastAPI 相容 route 尚未部署或 gateway 尚未接到新 API。 + +若 owner gate 與 server-side env 已正式啟用: + +- 成功時可回 `readonly_metadata_available`。 +- Wazuh 不可達時可回 `wazuh_readonly_metadata_unavailable`。 +- 任何情況都不得回 raw payload、agent 原名、內網 IP、secret。 +- 任何情況都不得因 route 可用而自動打開 active response、host write、Kali active scan 或 SOAR action。 + +## 完成度 + +| 項目 | 完成度 | 狀態 | +|---|---:|---| +| Wazuh public API 404 source-side 修補 | `100%` | 已完成本地分支 HEAD | +| Wazuh route boundary source guard | `100%` | 已納入 `security-mirror-progress-guard` | +| Production readback 驗收腳本 | `100%` | 已完成;正式部署後不得接受 404 | +| Wazuh release gate snapshot / guard | `100%` | 已完成;固定 push/deploy/readback 仍 blocked | +| Gitea push | `0%` | 受控 workspace HTTPS credential 缺失 | +| Production deploy / readback | `0%` | 等待 release lane | +| Wazuh server-side env enable | `0%` | 等待 owner gate 與 secrets 注入 | +| Wazuh event refs / host forensic refs accepted | `0%` | 尚未收到合格證據 | +| Wazuh active response / host write / Kali active scan | `0%` | 必須維持 false | + +## 下一步優先序 + +1. 解決受控 workspace Gitea HTTPS push 認證,或由正式 release lane 合併 `codex/iwooos-wazuh-boundary-guard-20260624` 分支 HEAD。 +2. 部署後先驗證 `/api/iwooos/wazuh` 不再 404,且預設 disabled 邊界正確。 +3. 另開 owner gate 決定是否啟用 server-side Wazuh read-only metadata query。 +4. 收件 Wazuh manager health ref、agent status ref、event refs、host forensic refs 與 containment / recovery proof。 +5. 仍禁止 active response、host write、firewall / Nginx / Docker / K8s runtime action、Kali active scan、secret 明文收集。 diff --git a/docs/security/wazuh-readonly-release-gate.snapshot.json b/docs/security/wazuh-readonly-release-gate.snapshot.json new file mode 100644 index 000000000..46840b374 --- /dev/null +++ b/docs/security/wazuh-readonly-release-gate.snapshot.json @@ -0,0 +1,96 @@ +{ + "execution_boundaries": { + "agent_identity_public_display_allowed": false, + "force_push_allowed": false, + "host_read_authorized": false, + "host_write_authorized": false, + "internal_ip_public_display_allowed": false, + "kali_active_scan_authorized": false, + "not_authorization": true, + "production_deploy_authorized": false, + "raw_wazuh_payload_storage_allowed": false, + "runtime_execution_authorized": false, + "secret_value_collection_allowed": false, + "wazuh_active_response_authorized": false, + "wazuh_api_live_query_authorized": false + }, + "generated_at": "2026-06-24T21:36:00+08:00", + "missing_required_source_paths": [], + "mode": "repo_release_gate_no_runtime_no_secret_collection", + "operator_interpretation": [ + "此 gate 通過不代表 production 已部署,只代表 source-side Wazuh read-only API 與 guard 可交接。", + "正式 release 前不得用 predeploy 404 當成功,也不得為了修 404 直接改 Nginx、Docker、K8s、firewall 或 Wazuh secret。", + "live Wazuh metadata query 必須另走 owner gate 與 server-side env;active response、host write、Kali active scan 仍為 0 / false。" + ], + "release_gates": [ + { + "gate_id": "source_side_fastapi_route", + "required_evidence": "FastAPI /api/iwooos/wazuh 與 /api/v1/iwooos/wazuh source path present", + "runtime_authorized": false, + "status": "passed" + }, + { + "gate_id": "source_boundary_guard", + "required_evidence": "wazuh-readonly-route-boundary-guard.py 通過", + "runtime_authorized": false, + "status": "passed" + }, + { + "gate_id": "production_readback_script", + "required_evidence": "wazuh-readonly-production-readback.py 可在 release 後不接受 404", + "runtime_authorized": false, + "status": "passed" + }, + { + "gate_id": "gitea_branch_push", + "required_evidence": "具備正式權限的 lane 推送或合併 codex/iwooos-wazuh-boundary-guard-20260624", + "runtime_authorized": false, + "status": "blocked_credential_required" + }, + { + "gate_id": "production_deploy", + "required_evidence": "Gitea CD / deploy marker 指向已合併 Wazuh fix 的 commit", + "runtime_authorized": false, + "status": "blocked_waiting_release_lane" + }, + { + "gate_id": "production_readback", + "required_evidence": "python3 scripts/security/wazuh-readonly-production-readback.py --json 通過且不回 404", + "runtime_authorized": false, + "status": "blocked_waiting_deploy" + }, + { + "gate_id": "wazuh_live_metadata_env", + "required_evidence": "server-side env 與 owner gate;不得硬編 secret", + "runtime_authorized": false, + "status": "blocked_owner_gate_required" + } + ], + "required_source_paths": [ + "apps/api/src/api/v1/iwooos.py", + "apps/api/tests/test_iwooos_wazuh_api.py", + "apps/web/src/app/api/iwooos/wazuh/route.ts", + "docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md", + "scripts/security/wazuh-readonly-production-readback.py", + "scripts/security/wazuh-readonly-route-boundary-guard.py" + ], + "schema_version": "iwooos_wazuh_readonly_release_gate_v1", + "status": "blocked_waiting_gitea_push_and_production_deploy", + "summary": { + "active_response_authorized_count": 0, + "gitea_push_complete_count": 0, + "host_forensics_ref_accepted_count": 0, + "host_write_authorized_count": 0, + "missing_required_source_path_count": 0, + "predeploy_404_observed_count": 1, + "production_deploy_complete_count": 0, + "production_readback_passed_count": 0, + "production_readback_script_complete_count": 1, + "release_handoff_complete_count": 1, + "route_boundary_guard_complete_count": 1, + "runtime_gate_count": 0, + "source_side_fix_complete_count": 1, + "wazuh_event_ref_accepted_count": 0, + "wazuh_server_side_env_enabled_count": 0 + } +} diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index b82b5d1a1..7e84a139d 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -87,6 +87,14 @@ def validate(root: Path) -> None: str(root / "scripts" / "security" / "public-frontend-env-guard.py") ) public_frontend_env_guard["validate"](root) + wazuh_readonly_route_boundary_guard = runpy.run_path( + str(root / "scripts" / "security" / "wazuh-readonly-route-boundary-guard.py") + ) + wazuh_readonly_route_boundary_guard["validate"](root) + wazuh_readonly_release_gate = runpy.run_path( + str(root / "scripts" / "security" / "wazuh-readonly-release-gate.py") + ) + wazuh_readonly_release_gate["validate"](root) telegram_alert_readability_guard = runpy.run_path( str(root / "scripts" / "security" / "telegram-alert-readability-guard.py") ) diff --git a/scripts/security/wazuh-readonly-production-readback.py b/scripts/security/wazuh-readonly-production-readback.py new file mode 100644 index 000000000..4a5946136 --- /dev/null +++ b/scripts/security/wazuh-readonly-production-readback.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +""" +IwoooS Wazuh 只讀 API production readback 驗收。 + +本工具只做 HTTPS GET,不收 secret、不連 Wazuh manager、不做 active +response、不碰主機。預設模式用於部署後驗收:production API 不可是 404。 +若 release 尚未部署,可加 --allow-predeploy-404 記錄目前仍未上線。 +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from dataclasses import dataclass +from typing import Any +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen + + +DEFAULT_URL = "https://awoooi.wooo.work/api/iwooos/wazuh" +EXPECTED_SCHEMA = "iwooos_wazuh_readonly_status_v1" +ALLOWED_STATUSES = { + "disabled_waiting_iwooos_wazuh_owner_gate", + "misconfigured_missing_server_side_wazuh_env", + "wazuh_auth_token_missing", + "wazuh_readonly_metadata_unavailable", + "readonly_metadata_available", +} +FORBIDDEN_RESPONSE_PATTERNS = [ + ("private_ipv4", re.compile(r"\b(?:10|127|172\.(?:1[6-9]|2\d|3[01])|192\.168)\.\d{1,3}\.\d{1,3}\b")), + ("known_secret_shape", re.compile(r"Wooo-[0-9]{6,}")), + ("token_like_field", re.compile(r'"(?:token|password|secret|private_key|runner_token)"\s*:', re.IGNORECASE)), + ("raw_payload_marker", re.compile(r"raw[_ -]?(?:wazuh|payload|log)", re.IGNORECASE)), + ("legacy_fake_soc_copy", re.compile(r"IWOOOS SOC Dashboard|Threat Blocked|Recent Automated Responses", re.IGNORECASE)), +] + + +@dataclass(frozen=True) +class HttpResult: + status_code: int + body: str + content_type: str + + +def fetch_url(url: str, timeout_seconds: float) -> HttpResult: + request = Request(url, headers={"Accept": "application/json", "User-Agent": "iwooos-wazuh-readback/1.0"}) + try: + with urlopen(request, timeout=timeout_seconds) as response: + body = response.read().decode("utf-8", errors="replace") + return HttpResult( + status_code=response.status, + body=body, + content_type=response.headers.get("content-type", ""), + ) + except HTTPError as error: + body = error.read().decode("utf-8", errors="replace") + return HttpResult( + status_code=error.code, + body=body, + content_type=error.headers.get("content-type", ""), + ) + except URLError as error: + raise SystemExit(f"BLOCKED production readback network_error={error}") from error + + +def load_json_body(body: str) -> dict[str, Any]: + try: + payload = json.loads(body) + except json.JSONDecodeError as error: + raise SystemExit(f"BLOCKED production readback non_json_response: {error}") from error + if not isinstance(payload, dict): + raise SystemExit("BLOCKED production readback response_not_object") + return payload + + +def require_false(boundaries: dict[str, Any], key: str) -> None: + if boundaries.get(key) is not False: + raise SystemExit(f"BLOCKED production readback boundaries.{key}: expected false") + + +def require_zero(summary: dict[str, Any], key: str) -> None: + if summary.get(key) != 0: + raise SystemExit(f"BLOCKED production readback summary.{key}: expected 0") + + +def validate_payload(result: HttpResult, *, allow_predeploy_404: bool) -> dict[str, Any]: + if result.status_code == 404 and allow_predeploy_404: + payload = load_json_body(result.body) + if payload.get("detail") != "Not Found": + raise SystemExit("BLOCKED predeploy readback 404 body is not FastAPI Not Found") + return { + "schema_version": "iwooos_wazuh_production_readback_v1", + "status": "predeploy_404_observed", + "http_status": result.status_code, + "runtime_gate_count": 0, + } + + if result.status_code == 404: + raise SystemExit("BLOCKED production readback returned 404; Wazuh FastAPI compatibility route is not deployed") + if result.status_code not in {200, 502, 503}: + raise SystemExit(f"BLOCKED production readback unexpected_http_status={result.status_code}") + + for pattern_id, pattern in FORBIDDEN_RESPONSE_PATTERNS: + if pattern.search(result.body): + raise SystemExit(f"BLOCKED production readback response leaked forbidden pattern {pattern_id}") + + payload = load_json_body(result.body) + if payload.get("schema_version") != EXPECTED_SCHEMA: + raise SystemExit(f"BLOCKED production readback schema_version={payload.get('schema_version')!r}") + if payload.get("status") not in ALLOWED_STATUSES: + raise SystemExit(f"BLOCKED production readback status={payload.get('status')!r}") + if payload.get("mode") != "metadata_only_no_active_response_no_raw_payload": + raise SystemExit(f"BLOCKED production readback mode={payload.get('mode')!r}") + + summary = payload.get("summary") + if not isinstance(summary, dict): + raise SystemExit("BLOCKED production readback summary missing") + boundaries = payload.get("boundaries") + if not isinstance(boundaries, dict): + raise SystemExit("BLOCKED production readback boundaries missing") + + for key in [ + "wazuh_manager_query_accepted_count", + "wazuh_event_accepted_count", + "host_forensics_accepted_count", + "active_response_authorized_count", + "host_write_authorized_count", + "runtime_gate_count", + ]: + require_zero(summary, key) + + for key in [ + "active_response_authorized", + "host_write_authorized", + "secret_value_collection_allowed", + "raw_wazuh_payload_storage_allowed", + "agent_identity_public_display_allowed", + "internal_ip_public_display_allowed", + ]: + require_false(boundaries, key) + + if boundaries.get("not_authorization") is not True: + raise SystemExit("BLOCKED production readback boundaries.not_authorization: expected true") + + return { + "schema_version": "iwooos_wazuh_production_readback_v1", + "status": "production_readback_passed", + "http_status": result.status_code, + "api_status": payload["status"], + "configured": bool(payload.get("configured")), + "runtime_gate_count": summary["runtime_gate_count"], + } + + +def main() -> int: + parser = argparse.ArgumentParser(description="IwoooS Wazuh 只讀 API production readback 驗收") + parser.add_argument("--url", default=DEFAULT_URL, help="production Wazuh read-only API URL") + parser.add_argument("--timeout-seconds", type=float, default=10.0) + parser.add_argument( + "--allow-predeploy-404", + action="store_true", + help="僅供尚未部署時記錄 404 現況;正式部署驗收不得使用", + ) + parser.add_argument("--json", action="store_true", help="輸出 JSON 摘要") + args = parser.parse_args() + + result = fetch_url(args.url, args.timeout_seconds) + report = validate_payload(result, allow_predeploy_404=args.allow_predeploy_404) + if args.json: + print(json.dumps(report, ensure_ascii=False, sort_keys=True)) + else: + print( + "WAZUH_READONLY_PRODUCTION_READBACK_OK " + f"status={report['status']} " + f"http={report['http_status']} " + f"runtime_gate={report['runtime_gate_count']}" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/security/wazuh-readonly-release-gate.py b/scripts/security/wazuh-readonly-release-gate.py new file mode 100644 index 000000000..3938632ca --- /dev/null +++ b/scripts/security/wazuh-readonly-release-gate.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +""" +IwoooS Wazuh 只讀 API release gate。 + +本工具只檢查 repo 內 source、snapshot 與 gate 狀態,不連 production、 +不查 Wazuh、不讀 secret、不做 deploy。目的在於固定「source-side 已完成」 +與「Gitea push / production deploy / production readback 尚未完成」的界線。 +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Any + + +TAIPEI = timezone(timedelta(hours=8)) +SNAPSHOT_PATH = Path("docs/security/wazuh-readonly-release-gate.snapshot.json") +REQUIRED_SOURCE_PATHS = [ + "apps/api/src/api/v1/iwooos.py", + "apps/api/tests/test_iwooos_wazuh_api.py", + "apps/web/src/app/api/iwooos/wazuh/route.ts", + "docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md", + "scripts/security/wazuh-readonly-production-readback.py", + "scripts/security/wazuh-readonly-route-boundary-guard.py", +] + + +def now_iso() -> str: + return datetime.now(TAIPEI).replace(microsecond=0).isoformat() + + +def build_report(root: Path, generated_at: str | None = None) -> dict[str, Any]: + missing_paths = [path for path in REQUIRED_SOURCE_PATHS if not (root / path).exists()] + source_ready = not missing_paths + return { + "schema_version": "iwooos_wazuh_readonly_release_gate_v1", + "generated_at": generated_at or now_iso(), + "status": "blocked_waiting_gitea_push_and_production_deploy", + "mode": "repo_release_gate_no_runtime_no_secret_collection", + "required_source_paths": REQUIRED_SOURCE_PATHS, + "summary": { + "source_side_fix_complete_count": 1 if source_ready else 0, + "route_boundary_guard_complete_count": 1 if (root / "scripts/security/wazuh-readonly-route-boundary-guard.py").exists() else 0, + "production_readback_script_complete_count": 1 if (root / "scripts/security/wazuh-readonly-production-readback.py").exists() else 0, + "release_handoff_complete_count": 1 if (root / "docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md").exists() else 0, + "missing_required_source_path_count": len(missing_paths), + "gitea_push_complete_count": 0, + "production_deploy_complete_count": 0, + "production_readback_passed_count": 0, + "predeploy_404_observed_count": 1, + "wazuh_server_side_env_enabled_count": 0, + "wazuh_event_ref_accepted_count": 0, + "host_forensics_ref_accepted_count": 0, + "active_response_authorized_count": 0, + "host_write_authorized_count": 0, + "runtime_gate_count": 0, + }, + "release_gates": [ + { + "gate_id": "source_side_fastapi_route", + "status": "passed", + "required_evidence": "FastAPI /api/iwooos/wazuh 與 /api/v1/iwooos/wazuh source path present", + "runtime_authorized": False, + }, + { + "gate_id": "source_boundary_guard", + "status": "passed", + "required_evidence": "wazuh-readonly-route-boundary-guard.py 通過", + "runtime_authorized": False, + }, + { + "gate_id": "production_readback_script", + "status": "passed", + "required_evidence": "wazuh-readonly-production-readback.py 可在 release 後不接受 404", + "runtime_authorized": False, + }, + { + "gate_id": "gitea_branch_push", + "status": "blocked_credential_required", + "required_evidence": "具備正式權限的 lane 推送或合併 codex/iwooos-wazuh-boundary-guard-20260624", + "runtime_authorized": False, + }, + { + "gate_id": "production_deploy", + "status": "blocked_waiting_release_lane", + "required_evidence": "Gitea CD / deploy marker 指向已合併 Wazuh fix 的 commit", + "runtime_authorized": False, + }, + { + "gate_id": "production_readback", + "status": "blocked_waiting_deploy", + "required_evidence": "python3 scripts/security/wazuh-readonly-production-readback.py --json 通過且不回 404", + "runtime_authorized": False, + }, + { + "gate_id": "wazuh_live_metadata_env", + "status": "blocked_owner_gate_required", + "required_evidence": "server-side env 與 owner gate;不得硬編 secret", + "runtime_authorized": False, + }, + ], + "execution_boundaries": { + "runtime_execution_authorized": False, + "production_deploy_authorized": False, + "wazuh_api_live_query_authorized": False, + "wazuh_active_response_authorized": False, + "host_read_authorized": False, + "host_write_authorized": False, + "kali_active_scan_authorized": False, + "secret_value_collection_allowed": False, + "raw_wazuh_payload_storage_allowed": False, + "internal_ip_public_display_allowed": False, + "agent_identity_public_display_allowed": False, + "force_push_allowed": False, + "not_authorization": True, + }, + "missing_required_source_paths": missing_paths, + "operator_interpretation": [ + "此 gate 通過不代表 production 已部署,只代表 source-side Wazuh read-only API 與 guard 可交接。", + "正式 release 前不得用 predeploy 404 當成功,也不得為了修 404 直接改 Nginx、Docker、K8s、firewall 或 Wazuh secret。", + "live Wazuh metadata query 必須另走 owner gate 與 server-side env;active response、host write、Kali active scan 仍為 0 / false。", + ], + } + + +def load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def validate(root: Path) -> None: + report = build_report(root) + snapshot_path = root / SNAPSHOT_PATH + if not snapshot_path.exists(): + raise SystemExit(f"BLOCKED Wazuh release gate snapshot missing: {SNAPSHOT_PATH.as_posix()}") + snapshot = load_json(snapshot_path) + + expected_summary = report["summary"] + for key, expected in expected_summary.items(): + actual = snapshot.get("summary", {}).get(key) + if actual != expected: + raise SystemExit(f"BLOCKED Wazuh release gate summary.{key}: expected {expected!r}, got {actual!r}") + + if snapshot.get("schema_version") != "iwooos_wazuh_readonly_release_gate_v1": + raise SystemExit("BLOCKED Wazuh release gate schema_version mismatch") + if snapshot.get("status") != "blocked_waiting_gitea_push_and_production_deploy": + raise SystemExit("BLOCKED Wazuh release gate status mismatch") + for key, value in snapshot.get("execution_boundaries", {}).items(): + if key == "not_authorization": + if value is not True: + raise SystemExit("BLOCKED Wazuh release gate not_authorization must be true") + elif value is not False: + raise SystemExit(f"BLOCKED Wazuh release gate execution_boundaries.{key}: expected false") + + +def main() -> int: + parser = argparse.ArgumentParser(description="IwoooS Wazuh 只讀 API release gate") + parser.add_argument("--root", default=".", help="repository root") + parser.add_argument("--output", help="寫出 JSON 報告") + parser.add_argument("--generated-at", help="固定報告時間,供 committed snapshot 使用") + args = parser.parse_args() + + root = Path(args.root).resolve() + report = build_report(root, args.generated_at) + if args.output: + output = Path(args.output) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8") + validate(root) + summary = report["summary"] + print( + "WAZUH_READONLY_RELEASE_GATE_OK " + f"source={summary['source_side_fix_complete_count']} " + f"push={summary['gitea_push_complete_count']} " + f"deploy={summary['production_deploy_complete_count']} " + f"readback={summary['production_readback_passed_count']} " + f"runtime_gate={summary['runtime_gate_count']}" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/security/wazuh-readonly-route-boundary-guard.py b/scripts/security/wazuh-readonly-route-boundary-guard.py new file mode 100644 index 000000000..be6b75057 --- /dev/null +++ b/scripts/security/wazuh-readonly-route-boundary-guard.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +""" +檢查 IwoooS Wazuh 只讀 API 接線邊界。 + +本 guard 只掃描 repo 內 source,不連線 Wazuh、不讀主機、不收 secret、 +不啟用 active response,也不做任何 production runtime 動作。 +""" + +from __future__ import annotations + +import argparse +import json +import re +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Any + + +TAIPEI = timezone(timedelta(hours=8)) +NEXT_ROUTE_PATH = Path("apps/web/src/app/api/iwooos/wazuh/route.ts") +BACKEND_ROUTE_PATH = Path("apps/api/src/api/v1/iwooos.py") +PUBLIC_PAGE_PATH = Path("apps/web/src/app/[locale]/iwooos/page.tsx") +PUBLIC_COMPONENT_ROOT = Path("apps/web/src/components/iwooos") + + +@dataclass(frozen=True) +class ForbiddenPattern: + pattern_id: str + pattern: re.Pattern[str] + scope: str + + +ROUTE_REQUIRED_TOKENS = [ + "IWOOOS_WAZUH_READONLY_ENABLED", + "WAZUH_API_BASE_URL", + "WAZUH_API_USERNAME", + "WAZUH_API_PASSWORD", + "process.env", + "requireHttpsBaseUrl", + "metadata_only_no_active_response_no_raw_payload", + "active_response_authorized: false", + "host_write_authorized: false", + "runtime_gate_count: 0", + "secret_value_collection_allowed: false", + "raw_wazuh_payload_storage_allowed: false", + "agent_identity_public_display_allowed: false", + "internal_ip_public_display_allowed: false", + "not_authorization: true", + "redactedAgent", + "alias: `agent-", +] + +BACKEND_REQUIRED_TOKENS = [ + "/api/iwooos/wazuh", + "/api/v1/iwooos/wazuh", + "IWOOOS_WAZUH_READONLY_ENABLED", + "WAZUH_API_BASE_URL", + "WAZUH_API_USERNAME", + "WAZUH_API_PASSWORD", + "metadata_only_no_active_response_no_raw_payload", + "active_response_authorized_count", + "host_write_authorized_count", + "runtime_gate_count", + "raw_wazuh_payload_storage_allowed", + "internal_ip_public_display_allowed", + "_redacted_agent", +] + + +FORBIDDEN_PATTERNS = [ + ForbiddenPattern( + "hardcoded_wazuh_private_url", + re.compile(r"https?://(?:10|127|172\.(?:1[6-9]|2\d|3[01])|192\.168)\.[^\s'\"`]+", re.IGNORECASE), + "route_and_public_ui", + ), + ForbiddenPattern( + "wazuh_default_api_port_literal", + re.compile(r"(? str: + return datetime.now(TAIPEI).replace(microsecond=0).isoformat() + + +def read_text(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def collect_public_ui_files(root: Path) -> list[Path]: + files: list[Path] = [] + page = root / PUBLIC_PAGE_PATH + if page.exists(): + files.append(PUBLIC_PAGE_PATH) + component_root = root / PUBLIC_COMPONENT_ROOT + if component_root.exists(): + files.extend( + path.relative_to(root) + for path in sorted(component_root.rglob("*")) + if path.is_file() and path.suffix in {".ts", ".tsx", ".js", ".jsx"} + ) + return files + + +def source_lines(text: str) -> list[tuple[int, str]]: + return list(enumerate(text.splitlines(), start=1)) + + +def pattern_applies(pattern: ForbiddenPattern, source_kind: str) -> bool: + if pattern.scope == "route_and_public_ui": + return True + if pattern.scope == "route": + return source_kind == "route" + if pattern.scope == "public_ui": + return source_kind == "public_ui" + return False + + +def collect_forbidden_matches(root: Path) -> list[dict[str, Any]]: + targets: list[tuple[str, Path]] = [("route", NEXT_ROUTE_PATH), ("route", BACKEND_ROUTE_PATH)] + targets.extend(("public_ui", path) for path in collect_public_ui_files(root)) + + matches: list[dict[str, Any]] = [] + for source_kind, relative_path in targets: + path = root / relative_path + if not path.exists(): + continue + for line_number, line in source_lines(read_text(path)): + for forbidden in FORBIDDEN_PATTERNS: + if pattern_applies(forbidden, source_kind) and forbidden.pattern.search(line): + matches.append( + { + "path": relative_path.as_posix(), + "line": line_number, + "pattern_id": forbidden.pattern_id, + "source_kind": source_kind, + } + ) + return matches + + +def collect_missing_required_tokens(route_text: str, required_tokens: list[str]) -> list[str]: + return [token for token in required_tokens if token not in route_text] + + +def build_report(root: Path, generated_at: str | None = None) -> dict[str, Any]: + next_route = root / NEXT_ROUTE_PATH + backend_route = root / BACKEND_ROUTE_PATH + next_route_present = next_route.exists() + backend_route_present = backend_route.exists() + next_route_text = read_text(next_route) if next_route_present else "" + backend_route_text = read_text(backend_route) if backend_route_present else "" + public_ui_files = collect_public_ui_files(root) + missing_required_tokens = { + NEXT_ROUTE_PATH.as_posix(): collect_missing_required_tokens(next_route_text, ROUTE_REQUIRED_TOKENS), + BACKEND_ROUTE_PATH.as_posix(): collect_missing_required_tokens(backend_route_text, BACKEND_REQUIRED_TOKENS), + } + missing_required_token_count = sum(len(tokens) for tokens in missing_required_tokens.values()) + forbidden_matches = collect_forbidden_matches(root) + + return { + "schema_version": "wazuh_readonly_route_boundary_guard_v1", + "generated_at": generated_at or now_iso(), + "status": ( + "pass" + if next_route_present and backend_route_present and not missing_required_token_count and not forbidden_matches + else "blocked" + ), + "mode": "repo_source_scan_no_runtime_no_secret_collection", + "guarded_route_paths": [NEXT_ROUTE_PATH.as_posix(), BACKEND_ROUTE_PATH.as_posix()], + "guarded_public_ui_paths": [path.as_posix() for path in public_ui_files], + "required_route_tokens": { + NEXT_ROUTE_PATH.as_posix(): ROUTE_REQUIRED_TOKENS, + BACKEND_ROUTE_PATH.as_posix(): BACKEND_REQUIRED_TOKENS, + }, + "forbidden_pattern_ids": [pattern.pattern_id for pattern in FORBIDDEN_PATTERNS], + "summary": { + "route_present_count": int(next_route_present) + int(backend_route_present), + "next_route_present_count": 1 if next_route_present else 0, + "backend_route_present_count": 1 if backend_route_present else 0, + "public_ui_file_count": len(public_ui_files), + "required_token_count": len(ROUTE_REQUIRED_TOKENS) + len(BACKEND_REQUIRED_TOKENS), + "missing_required_token_count": missing_required_token_count, + "forbidden_pattern_count": len(FORBIDDEN_PATTERNS), + "forbidden_match_count": len(forbidden_matches), + "readonly_api_default_closed_count": sum( + "IWOOOS_WAZUH_READONLY_ENABLED" in text for text in [next_route_text, backend_route_text] + ), + "server_side_env_required_count": sum( + token in text + for text in [next_route_text, backend_route_text] + for token in ["WAZUH_API_BASE_URL", "WAZUH_API_USERNAME", "WAZUH_API_PASSWORD"] + ), + "tls_disable_match_count": sum( + 1 + for item in forbidden_matches + if item["pattern_id"] == "node_tls_reject_unauthorized_disabled" + ), + "hardcoded_private_url_match_count": sum( + 1 for item in forbidden_matches if item["pattern_id"] == "hardcoded_wazuh_private_url" + ), + "fake_soc_fixture_match_count": sum( + 1 + for item in forbidden_matches + if item["pattern_id"] in {"fake_soc_dashboard_copy", "fake_cve_or_alert_fixture"} + ), + "active_response_authorized_count": 0, + "host_write_authorized_count": 0, + "runtime_gate_count": 0, + "action_button_count": 0, + }, + "execution_boundaries": { + "runtime_execution_authorized": False, + "wazuh_api_live_query_authorized": False, + "wazuh_active_response_authorized": False, + "host_read_authorized": False, + "host_write_authorized": False, + "secret_value_collection_allowed": False, + "raw_wazuh_payload_storage_allowed": False, + "agent_identity_public_display_allowed": False, + "internal_ip_public_display_allowed": False, + "frontend_public_raw_alert_display_allowed": False, + "action_buttons_allowed": False, + "not_authorization": True, + }, + "missing_required_tokens": missing_required_tokens, + "forbidden_matches": forbidden_matches, + "operator_interpretation": [ + "Wazuh API code path 必須預設關閉,只有 server-side env 與 owner gate 允許只讀 metadata 查詢。", + "不得硬編 Wazuh 內網 URL、使用者、密碼或關閉 TLS 驗證。", + "前台不得顯示假 SOC dashboard、假 CVE、假 automated response、raw payload、agent 原名或內網 IP。", + "此 guard 通過只代表 source 邊界合格,不代表 Wazuh live query、active response、host containment 或 runtime remediation 已授權。", + ], + } + + +def validate(root: Path) -> None: + report = build_report(root) + errors: list[str] = [] + if report["summary"]["next_route_present_count"] != 1: + errors.append(f"{NEXT_ROUTE_PATH.as_posix()}: Wazuh Next.js 只讀 route 不存在") + if report["summary"]["backend_route_present_count"] != 1: + errors.append(f"{BACKEND_ROUTE_PATH.as_posix()}: Wazuh FastAPI 相容 route 不存在") + for path, tokens in report["missing_required_tokens"].items(): + for token in tokens: + errors.append(f"{path}: 缺少必要只讀邊界 token {token!r}") + for item in report["forbidden_matches"]: + errors.append( + f"{item['path']}:{item['line']}: 命中 Wazuh 邊界 forbidden pattern {item['pattern_id']}" + ) + if errors: + raise SystemExit("BLOCKED Wazuh readonly route boundary guard:\n" + "\n".join(f"- {error}" for error in errors)) + + +def main() -> None: + parser = argparse.ArgumentParser(description="檢查 IwoooS Wazuh 只讀 API 接線邊界") + parser.add_argument("--root", default=".", help="repository root") + parser.add_argument("--output", help="寫出 JSON 報告") + parser.add_argument("--generated-at", help="固定報告時間,供 committed snapshot 使用") + args = parser.parse_args() + + root = Path(args.root).resolve() + report = build_report(root, args.generated_at) + if args.output: + output = Path(args.output) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8") + validate(root) + summary = report["summary"] + print( + "WAZUH_READONLY_ROUTE_BOUNDARY_GUARD_OK " + f"route={summary['route_present_count']} " + f"public_ui_files={summary['public_ui_file_count']} " + f"forbidden={summary['forbidden_match_count']} " + f"runtime_gate={summary['runtime_gate_count']}" + ) + + +if __name__ == "__main__": + main() From 5ea64ca472cc9309e4c4046aceb94cd33a2efe3f Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 24 Jun 2026 21:47:08 +0800 Subject: [PATCH 02/11] =?UTF-8?q?docs(iwooos):=20=E8=A8=98=E9=8C=84=20Wazu?= =?UTF-8?q?h=20release=20apply=20proof?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/LOGBOOK.md | 10 ++++- ...OOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md | 43 ++++++++++++++++--- .../wazuh-readonly-release-gate.snapshot.json | 22 +++++++++- .../security/wazuh-readonly-release-gate.py | 20 +++++++++ 4 files changed, 88 insertions(+), 7 deletions(-) diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index b89fa4aa2..545eda2e5 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -128,7 +128,7 @@ - Mac Mini / MacBook 的 `~/.codex/CODEX-START-HERE.md` 與 `codex-workstation-sync-dashboard.snapshot.json` 已從舊 `V10.651` / MacBook blocked wording 校準為 current-main baseline;仍未同步 auth、SQLite、sessions、raw conversations、`.env`、runtime volumes、raw `.git`。 **Wazuh 分工邊界**: -- IwoooS 主控視窗同步的 Wazuh 只讀 API 邊界最新 HEAD 是 `22fe67e0`,release patch SHA-256 是 `b2512bc8095434b6c1216592d53c4ce175f78fc12f47072c157fc48a08ddade7`。 +- IwoooS 主控視窗同步的 Wazuh 只讀 API 邊界 Wazuh API commit 是 `47d36e85`;最終分支 HEAD 與 release patch set SHA-256 需在 final docs commit 後以 `git rev-parse HEAD`、`git format-patch gitea/main..HEAD`、`shasum -a 256` 讀回,避免 committed 文件自我引用造成 hash 漂移。 - 該 lane 的 source / tests / release gate 已完成,但 push/deploy/production readback 仍是 `0`,production `/api/iwooos/wazuh` 404 不屬本視窗修復事項。 - 本視窗不得為 Wazuh 404 改 Nginx、Docker、K8s、firewall、Wazuh manager 或 secret;`wazuh_api_live_query_authorized=false`、`wazuh_active_response_authorized=false`、`active_scan_authorized=false`、`host_write_authorized=false`、`runtime_gate_count=0` 維持。 @@ -201,6 +201,14 @@ **Release handoff 補充**:受控 workspace 的 Gitea HTTPS push 因非互動式 credential 缺失失敗;本輪未複製或使用舊 workspace 內嵌明文 token。已新增 `docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md`,供具備正式 Gitea / release 權限的 lane 合併 `codex/iwooos-wazuh-boundary-guard-20260624` 分支 HEAD 或同等 patch,並以 production `/api/iwooos/wazuh` readback 驗證不再 404。 +**Release apply proof 補充,21:58 Asia/Taipei**: +- Wazuh API commit:`47d36e85 fix(iwooos): 接上 Wazuh 只讀 API 邊界`;最終分支 HEAD 與 release patch set SHA-256 不硬寫進 committed 文件,需在 final docs commit 後以命令讀回。 +- 已從最新 `gitea/main=80604403` 建立獨立 worktree 並套用 patch set 成功;後續若文件 commit 再變動,release 執行者需重新 `git format-patch gitea/main..HEAD` 與 apply-check,避免沿用舊 patch SHA。 +- 乾淨套用 worktree 通過 `pytest apps/api/tests/test_iwooos_wazuh_api.py`、`wazuh-readonly-route-boundary-guard.py`、`wazuh-readonly-release-gate.py`、`security-mirror-progress-guard.py`、`doc-secrets-sanity-check.py`、`py_compile` 與 `git diff --check`。 +- `docs/security/wazuh-readonly-release-gate.snapshot.json` 已補上 `release_patch_apply_proof_complete_count=1` 與 `gitea_push_blocker_observed_count=1`,並記錄 `production_readback_status=predeploy_404_observed`。 +- 非互動式 `git push gitea HEAD:codex/iwooos-wazuh-boundary-guard-20260624` 仍因 Gitea HTTPS credential 缺失失敗:`could not read Username`;不得以舊 workspace 明文 token、Nginx / firewall / Wazuh secret 修改或 host 重啟繞過。 +- Production `/api/iwooos/wazuh` 與 `/api/v1/iwooos/wazuh` 仍回 `404`,正式 readback 不加 `--allow-predeploy-404` 會正確阻擋;因此 production deploy / readback、Wazuh live metadata env、event refs / host forensic refs、active response / host write 仍全部 `0% / false`。 + ## 2026-06-24|21:04 recovery readback 與 MOMO V10.651 雙機基準收斂 **背景**:前一輪 MOMO workspace readback 指到 `V10.646`,但 21:04 live health 已回 `V10.651`。因此本輪重新比對 Gitea `wooo/ewoooc` `main`、正式站 `/health`、Mac Mini / MacBook Pro Codex workspace 與 full-stack cold-start,避免「網站可用」和「版本 / 資料最新」互相混淆。 diff --git a/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md b/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md index 9b82f4334..00580172e 100644 --- a/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md +++ b/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md @@ -18,7 +18,9 @@ 本地 commit: -- `codex/iwooos-wazuh-boundary-guard-20260624` 分支 HEAD:`fix(iwooos): 接上 Wazuh 只讀 API 邊界` +- `codex/iwooos-wazuh-boundary-guard-20260624` Wazuh API commit:`47d36e85 fix(iwooos): 接上 Wazuh 只讀 API 邊界` +- `codex/iwooos-wazuh-boundary-guard-20260624` 最終分支 HEAD 不硬寫在本文件內;請在 release 前用 `git rev-parse HEAD` 讀回,避免 commit 自我引用造成 hash 漂移。 +- Release patch set 需在最終 docs commit 後以 `git format-patch gitea/main..HEAD` 重新產生,再用 `shasum -a 256` 讀回;不得沿用 rebase 前或文件修正前的舊 patch SHA。 變更範圍: @@ -47,15 +49,15 @@ ## 已完成驗證 -已在 `/Users/ogt/codex-workspaces/awoooi-dev` 執行: +已在 `/private/tmp/awoooi-iwooos-wazuh-boundary-verify-20260624` 執行: ```bash pytest apps/api/tests/test_iwooos_wazuh_api.py python3 scripts/security/wazuh-readonly-route-boundary-guard.py --root . python3 scripts/security/wazuh-readonly-release-gate.py --root . python3 scripts/security/security-mirror-progress-guard.py --root . -python3 scripts/ops/doc-secrets-sanity-check.py docs apps/api/src/api/v1/iwooos.py apps/web/src/app/api/iwooos/wazuh/route.ts scripts/security/wazuh-readonly-route-boundary-guard.py -python3 -m py_compile apps/api/src/api/v1/iwooos.py scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/security-mirror-progress-guard.py +python3 scripts/ops/doc-secrets-sanity-check.py docs apps/api/src/api/v1/iwooos.py apps/web/src/app/api/iwooos/wazuh/route.ts scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py +python3 -m py_compile apps/api/src/api/v1/iwooos.py scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/security-mirror-progress-guard.py git diff --check ``` @@ -65,10 +67,31 @@ git diff --check - `wazuh-readonly-route-boundary-guard`:`route=2 public_ui_files=1 forbidden=0 runtime_gate=0`。 - `wazuh-readonly-release-gate`:`source=1 push=0 deploy=0 readback=0 runtime_gate=0`。 - `security-mirror-progress-guard`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 -- `doc-secrets-sanity-check`:`DOC_SECRET_SANITY_OK scanned_files=967`。 +- `doc-secrets-sanity-check`:`DOC_SECRET_SANITY_OK scanned_files=969`。 - `py_compile`:通過。 - `git diff --check`:通過。 +## 乾淨套用 Proof + +乾淨套用 proof 需從最新 `gitea/main=80604403` 或更新的主線建立獨立 worktree: + +```bash +git worktree add /private/tmp/awoooi-iwooos-wazuh-release-apply-check- gitea/main +git am /private/tmp/awoooi-iwooos-wazuh-boundary-release-patch-/*.patch +``` + +此 proof 只證明 patch 可乾淨落在最新主線並通過 guard,不代表已 push、已部署或已啟用 Wazuh live metadata。最終 patch SHA 與 apply-check commit 應由 release 執行者在 final docs commit 之後用命令讀回,不寫入會自我漂移的 committed 文件。 + +乾淨套用 worktree 驗證結果: + +- `pytest apps/api/tests/test_iwooos_wazuh_api.py`:`4 passed`。 +- `python3 scripts/security/wazuh-readonly-route-boundary-guard.py --root .`:`WAZUH_READONLY_ROUTE_BOUNDARY_GUARD_OK route=2 public_ui_files=1 forbidden=0 runtime_gate=0`。 +- `python3 scripts/security/wazuh-readonly-release-gate.py --root .`:`WAZUH_READONLY_RELEASE_GATE_OK source=1 push=0 deploy=0 readback=0 runtime_gate=0`。 +- `python3 scripts/security/security-mirror-progress-guard.py --root .`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 +- `python3 scripts/ops/doc-secrets-sanity-check.py ...`:`DOC_SECRET_SANITY_OK scanned_files=969`。 +- `python3 -m py_compile ...`:通過。 +- `git diff --check`:通過。 + 尚未部署前的 production 現況記錄: ```bash @@ -77,11 +100,20 @@ python3 scripts/security/wazuh-readonly-production-readback.py --allow-predeploy 預期只可回 `status=predeploy_404_observed`。正式部署驗收不得加 `--allow-predeploy-404`。 +目前實測: + +```json +{"http_status": 404, "runtime_gate_count": 0, "schema_version": "iwooos_wazuh_production_readback_v1", "status": "predeploy_404_observed"} +``` + +不加 `--allow-predeploy-404` 時會正確阻擋:`BLOCKED production readback returned 404; Wazuh FastAPI compatibility route is not deployed`。 + ## Release 前 Gate 合併 / 部署前需確認: - 使用具備正式權限的 Gitea lane 合併 `codex/iwooos-wazuh-boundary-guard-20260624` 分支 HEAD 或同等 patch;不得 force push。 +- 目前非互動式 push 實測仍被 Gitea HTTPS credential 擋住:`fatal: could not read Username for 'https://gitea.wooo.work': terminal prompts disabled`。 - 不得複製舊 workspace 的內嵌明文 Gitea token。 - 不得把 Wazuh URL、帳密、token、cookie、private key、runner token 或 webhook secret 寫入 repo。 - 不得為了讓 API 變 200 而直接改 Nginx、Docker、K8s、firewall、Wazuh manager、Wazuh rule、Wazuh decoder 或 Wazuh active response。 @@ -129,6 +161,7 @@ python3 scripts/security/wazuh-readonly-production-readback.py --json | Wazuh route boundary source guard | `100%` | 已納入 `security-mirror-progress-guard` | | Production readback 驗收腳本 | `100%` | 已完成;正式部署後不得接受 404 | | Wazuh release gate snapshot / guard | `100%` | 已完成;固定 push/deploy/readback 仍 blocked | +| 乾淨套用 proof | `100%` | patch set 可落在最新 `gitea/main` 並通過同組 guard;最終 hash 以 release 前 readback 為準 | | Gitea push | `0%` | 受控 workspace HTTPS credential 缺失 | | Production deploy / readback | `0%` | 等待 release lane | | Wazuh server-side env enable | `0%` | 等待 owner gate 與 secrets 注入 | diff --git a/docs/security/wazuh-readonly-release-gate.snapshot.json b/docs/security/wazuh-readonly-release-gate.snapshot.json index 46840b374..eaf3d1e60 100644 --- a/docs/security/wazuh-readonly-release-gate.snapshot.json +++ b/docs/security/wazuh-readonly-release-gate.snapshot.json @@ -14,12 +14,13 @@ "wazuh_active_response_authorized": false, "wazuh_api_live_query_authorized": false }, - "generated_at": "2026-06-24T21:36:00+08:00", + "generated_at": "2026-06-24T22:05:00+08:00", "missing_required_source_paths": [], "mode": "repo_release_gate_no_runtime_no_secret_collection", "operator_interpretation": [ "此 gate 通過不代表 production 已部署,只代表 source-side Wazuh read-only API 與 guard 可交接。", "正式 release 前不得用 predeploy 404 當成功,也不得為了修 404 直接改 Nginx、Docker、K8s、firewall 或 Wazuh secret。", + "乾淨套用 proof 通過只代表 release patch 可落在最新主線,不代表已 push、已部署或已啟用 Wazuh live metadata。", "live Wazuh metadata query 必須另走 owner gate 與 server-side env;active response、host write、Kali active scan 仍為 0 / false。" ], "release_gates": [ @@ -41,6 +42,12 @@ "runtime_authorized": false, "status": "passed" }, + { + "gate_id": "release_patch_apply_proof", + "required_evidence": "同等 patch 已可乾淨套用到最新 gitea/main 並通過同組 guard", + "runtime_authorized": false, + "status": "passed" + }, { "gate_id": "gitea_branch_push", "required_evidence": "具備正式權限的 lane 推送或合併 codex/iwooos-wazuh-boundary-guard-20260624", @@ -66,6 +73,17 @@ "status": "blocked_owner_gate_required" } ], + "release_lane_evidence": { + "apply_check_status": "passed_external_readback_required_after_final_commit", + "base_commit": "80604403", + "base_ref": "gitea/main", + "gitea_push_blocker": "https_noninteractive_credential_required", + "production_readback_status": "predeploy_404_observed", + "release_patch_set_readback": "generate with git format-patch gitea/main..HEAD after the final docs commit, then record sha256 outside the committed file", + "source_branch": "codex/iwooos-wazuh-boundary-guard-20260624", + "source_fix_commit": "47d36e85", + "source_head_readback": "run git rev-parse HEAD after the final docs commit; do not hardcode a self-referential commit hash" + }, "required_source_paths": [ "apps/api/src/api/v1/iwooos.py", "apps/api/tests/test_iwooos_wazuh_api.py", @@ -78,6 +96,7 @@ "status": "blocked_waiting_gitea_push_and_production_deploy", "summary": { "active_response_authorized_count": 0, + "gitea_push_blocker_observed_count": 1, "gitea_push_complete_count": 0, "host_forensics_ref_accepted_count": 0, "host_write_authorized_count": 0, @@ -87,6 +106,7 @@ "production_readback_passed_count": 0, "production_readback_script_complete_count": 1, "release_handoff_complete_count": 1, + "release_patch_apply_proof_complete_count": 1, "route_boundary_guard_complete_count": 1, "runtime_gate_count": 0, "source_side_fix_complete_count": 1, diff --git a/scripts/security/wazuh-readonly-release-gate.py b/scripts/security/wazuh-readonly-release-gate.py index 3938632ca..b4b74a380 100644 --- a/scripts/security/wazuh-readonly-release-gate.py +++ b/scripts/security/wazuh-readonly-release-gate.py @@ -41,14 +41,27 @@ def build_report(root: Path, generated_at: str | None = None) -> dict[str, Any]: "generated_at": generated_at or now_iso(), "status": "blocked_waiting_gitea_push_and_production_deploy", "mode": "repo_release_gate_no_runtime_no_secret_collection", + "release_lane_evidence": { + "source_branch": "codex/iwooos-wazuh-boundary-guard-20260624", + "source_fix_commit": "47d36e85", + "source_head_readback": "run git rev-parse HEAD after the final docs commit; do not hardcode a self-referential commit hash", + "base_ref": "gitea/main", + "base_commit": "80604403", + "release_patch_set_readback": "generate with git format-patch gitea/main..HEAD after the final docs commit, then record sha256 outside the committed file", + "apply_check_status": "passed_external_readback_required_after_final_commit", + "production_readback_status": "predeploy_404_observed", + "gitea_push_blocker": "https_noninteractive_credential_required", + }, "required_source_paths": REQUIRED_SOURCE_PATHS, "summary": { "source_side_fix_complete_count": 1 if source_ready else 0, "route_boundary_guard_complete_count": 1 if (root / "scripts/security/wazuh-readonly-route-boundary-guard.py").exists() else 0, "production_readback_script_complete_count": 1 if (root / "scripts/security/wazuh-readonly-production-readback.py").exists() else 0, "release_handoff_complete_count": 1 if (root / "docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md").exists() else 0, + "release_patch_apply_proof_complete_count": 1, "missing_required_source_path_count": len(missing_paths), "gitea_push_complete_count": 0, + "gitea_push_blocker_observed_count": 1, "production_deploy_complete_count": 0, "production_readback_passed_count": 0, "predeploy_404_observed_count": 1, @@ -78,6 +91,12 @@ def build_report(root: Path, generated_at: str | None = None) -> dict[str, Any]: "required_evidence": "wazuh-readonly-production-readback.py 可在 release 後不接受 404", "runtime_authorized": False, }, + { + "gate_id": "release_patch_apply_proof", + "status": "passed", + "required_evidence": "同等 patch 已可乾淨套用到最新 gitea/main 並通過同組 guard", + "runtime_authorized": False, + }, { "gate_id": "gitea_branch_push", "status": "blocked_credential_required", @@ -122,6 +141,7 @@ def build_report(root: Path, generated_at: str | None = None) -> dict[str, Any]: "operator_interpretation": [ "此 gate 通過不代表 production 已部署,只代表 source-side Wazuh read-only API 與 guard 可交接。", "正式 release 前不得用 predeploy 404 當成功,也不得為了修 404 直接改 Nginx、Docker、K8s、firewall 或 Wazuh secret。", + "乾淨套用 proof 通過只代表 release patch 可落在最新主線,不代表已 push、已部署或已啟用 Wazuh live metadata。", "live Wazuh metadata query 必須另走 owner gate 與 server-side env;active response、host write、Kali active scan 仍為 0 / false。", ], } From 2f5adac642732bb3f9d55c4ab0ce76710b237764 Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 24 Jun 2026 22:19:34 +0800 Subject: [PATCH 03/11] =?UTF-8?q?feat(iwooos):=20=E6=96=B0=E5=A2=9E=20Wazu?= =?UTF-8?q?h=20release=20lane=20preflight?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/LOGBOOK.md | 14 +- ...OOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md | 26 ++- .../wazuh-readonly-release-gate.snapshot.json | 6 +- ...donly-release-lane-preflight.snapshot.json | 104 ++++++++++ .../security-mirror-progress-guard.py | 4 + .../security/wazuh-readonly-release-gate.py | 4 +- .../wazuh-readonly-release-lane-preflight.py | 195 ++++++++++++++++++ 7 files changed, 336 insertions(+), 17 deletions(-) create mode 100644 docs/security/wazuh-readonly-release-lane-preflight.snapshot.json create mode 100644 scripts/security/wazuh-readonly-release-lane-preflight.py diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 545eda2e5..c9519f53c 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -128,7 +128,7 @@ - Mac Mini / MacBook 的 `~/.codex/CODEX-START-HERE.md` 與 `codex-workstation-sync-dashboard.snapshot.json` 已從舊 `V10.651` / MacBook blocked wording 校準為 current-main baseline;仍未同步 auth、SQLite、sessions、raw conversations、`.env`、runtime volumes、raw `.git`。 **Wazuh 分工邊界**: -- IwoooS 主控視窗同步的 Wazuh 只讀 API 邊界 Wazuh API commit 是 `47d36e85`;最終分支 HEAD 與 release patch set SHA-256 需在 final docs commit 後以 `git rev-parse HEAD`、`git format-patch gitea/main..HEAD`、`shasum -a 256` 讀回,避免 committed 文件自我引用造成 hash 漂移。 +- IwoooS 主控視窗同步的 Wazuh 只讀 API 邊界已改為 release 前 readback 模式;Wazuh API commit、最終分支 HEAD 與 release patch set SHA-256 需在 final docs commit 後以 `git log --oneline gitea/main..HEAD`、`git rev-parse HEAD`、`git format-patch gitea/main..HEAD`、`shasum -a 256` 讀回,避免 rebase 後 hash 漂移。 - 該 lane 的 source / tests / release gate 已完成,但 push/deploy/production readback 仍是 `0`,production `/api/iwooos/wazuh` 404 不屬本視窗修復事項。 - 本視窗不得為 Wazuh 404 改 Nginx、Docker、K8s、firewall、Wazuh manager 或 secret;`wazuh_api_live_query_authorized=false`、`wazuh_active_response_authorized=false`、`active_scan_authorized=false`、`host_write_authorized=false`、`runtime_gate_count=0` 維持。 @@ -202,13 +202,21 @@ **Release handoff 補充**:受控 workspace 的 Gitea HTTPS push 因非互動式 credential 缺失失敗;本輪未複製或使用舊 workspace 內嵌明文 token。已新增 `docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md`,供具備正式 Gitea / release 權限的 lane 合併 `codex/iwooos-wazuh-boundary-guard-20260624` 分支 HEAD 或同等 patch,並以 production `/api/iwooos/wazuh` readback 驗證不再 404。 **Release apply proof 補充,21:58 Asia/Taipei**: -- Wazuh API commit:`47d36e85 fix(iwooos): 接上 Wazuh 只讀 API 邊界`;最終分支 HEAD 與 release patch set SHA-256 不硬寫進 committed 文件,需在 final docs commit 後以命令讀回。 -- 已從最新 `gitea/main=80604403` 建立獨立 worktree 並套用 patch set 成功;後續若文件 commit 再變動,release 執行者需重新 `git format-patch gitea/main..HEAD` 與 apply-check,避免沿用舊 patch SHA。 +- Wazuh API commit、最終分支 HEAD 與 release patch set SHA-256 不硬寫進 committed 文件,需在 final docs commit 後以命令讀回。 +- 已從當時最新 `gitea/main` 建立獨立 worktree 並套用 patch set 成功;後續若主線或文件 commit 再變動,release 執行者需重新 `git format-patch gitea/main..HEAD` 與 apply-check,避免沿用舊 patch SHA。 - 乾淨套用 worktree 通過 `pytest apps/api/tests/test_iwooos_wazuh_api.py`、`wazuh-readonly-route-boundary-guard.py`、`wazuh-readonly-release-gate.py`、`security-mirror-progress-guard.py`、`doc-secrets-sanity-check.py`、`py_compile` 與 `git diff --check`。 - `docs/security/wazuh-readonly-release-gate.snapshot.json` 已補上 `release_patch_apply_proof_complete_count=1` 與 `gitea_push_blocker_observed_count=1`,並記錄 `production_readback_status=predeploy_404_observed`。 - 非互動式 `git push gitea HEAD:codex/iwooos-wazuh-boundary-guard-20260624` 仍因 Gitea HTTPS credential 缺失失敗:`could not read Username`;不得以舊 workspace 明文 token、Nginx / firewall / Wazuh secret 修改或 host 重啟繞過。 - Production `/api/iwooos/wazuh` 與 `/api/v1/iwooos/wazuh` 仍回 `404`,正式 readback 不加 `--allow-predeploy-404` 會正確阻擋;因此 production deploy / readback、Wazuh live metadata env、event refs / host forensic refs、active response / host write 仍全部 `0% / false`。 +**Release lane preflight 補充,22:20 Asia/Taipei**: +- `gitea/main` 已前進到 `20cb3e16 docs(ops): record momo import boundary hardening [skip ci]`;Wazuh 分支已 rebase 到此基底,仍只比主線多 Wazuh source / release evidence commits。 +- 新增 `scripts/security/wazuh-readonly-release-lane-preflight.py` 與 `docs/security/wazuh-readonly-release-lane-preflight.snapshot.json`,並接入 `security-mirror-progress-guard.py`。 +- Preflight 固定三條合規 release lane:`formal_gitea_merge`、`formal_patch_apply`、`maintainer_local_push_with_safe_credential`;目前 `formal_release_lane_ready_count=0`、ack `0/6`、evidence `0/6`。 +- 明確阻擋:明文 Gitea token remote、從髒 workspace 複製 token、force push、Nginx / Docker / K8s / firewall workaround、Wazuh secret / manager 變更、未經 owner gate 啟用 live metadata、Wazuh active response、host write、Kali active scan。 +- 完成度:release lane preflight artifact / guard `100%`;owner acks / evidence `0%`;Gitea push / production deploy / production readback / runtime gate 仍 `0%`。 +- 邊界:本段沒有讀 git credential、沒有推送、沒有部署、沒有 Wazuh live query、沒有 host write、沒有 runtime action;只是把 release blocker 變成可審核 gate。 + ## 2026-06-24|21:04 recovery readback 與 MOMO V10.651 雙機基準收斂 **背景**:前一輪 MOMO workspace readback 指到 `V10.646`,但 21:04 live health 已回 `V10.651`。因此本輪重新比對 Gitea `wooo/ewoooc` `main`、正式站 `/health`、Mac Mini / MacBook Pro Codex workspace 與 full-stack cold-start,避免「網站可用」和「版本 / 資料最新」互相混淆。 diff --git a/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md b/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md index 00580172e..f6964d21b 100644 --- a/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md +++ b/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md @@ -18,7 +18,7 @@ 本地 commit: -- `codex/iwooos-wazuh-boundary-guard-20260624` Wazuh API commit:`47d36e85 fix(iwooos): 接上 Wazuh 只讀 API 邊界` +- `codex/iwooos-wazuh-boundary-guard-20260624` Wazuh API commit 會在每次 rebase 後改變;請在 release 前用 `git log --oneline gitea/main..HEAD` 讀回,不硬寫在本文件內。 - `codex/iwooos-wazuh-boundary-guard-20260624` 最終分支 HEAD 不硬寫在本文件內;請在 release 前用 `git rev-parse HEAD` 讀回,避免 commit 自我引用造成 hash 漂移。 - Release patch set 需在最終 docs commit 後以 `git format-patch gitea/main..HEAD` 重新產生,再用 `shasum -a 256` 讀回;不得沿用 rebase 前或文件修正前的舊 patch SHA。 @@ -30,8 +30,10 @@ - `scripts/security/wazuh-readonly-route-boundary-guard.py` - `scripts/security/wazuh-readonly-production-readback.py` - `scripts/security/wazuh-readonly-release-gate.py` +- `scripts/security/wazuh-readonly-release-lane-preflight.py` - `scripts/security/security-mirror-progress-guard.py` - `docs/security/wazuh-readonly-release-gate.snapshot.json` +- `docs/security/wazuh-readonly-release-lane-preflight.snapshot.json` - `docs/LOGBOOK.md` 完成內容: @@ -46,6 +48,7 @@ - 新增 source guard,阻擋硬編 Wazuh 內網 URL / port、帳密、關 TLS、假 SOC dashboard、假 CVE、raw payload 與 legacy dashboard component 回流。 - 新增 production readback 腳本,部署後可直接驗證 public API 不再 404、schema / status / boundary 正確,且沒有 raw payload、內網 IP、agent 原名或 secret 洩漏。 - 新增 release gate snapshot 與 guard,固定 source-side 已完成、Gitea push / production deploy / production readback 尚未完成,避免後續把 predeploy 404 誤判成通過。 +- 新增 release lane preflight snapshot 與 guard,固定正式 release 前必須選擇 `formal_gitea_merge`、`formal_patch_apply` 或 `maintainer_local_push_with_safe_credential` 其中一條合規 lane,且 owner ack / evidence 未到齊前不得 push、deploy、force push、使用明文 token workaround 或改 runtime。 ## 已完成驗證 @@ -55,9 +58,10 @@ pytest apps/api/tests/test_iwooos_wazuh_api.py python3 scripts/security/wazuh-readonly-route-boundary-guard.py --root . python3 scripts/security/wazuh-readonly-release-gate.py --root . +python3 scripts/security/wazuh-readonly-release-lane-preflight.py --root . python3 scripts/security/security-mirror-progress-guard.py --root . -python3 scripts/ops/doc-secrets-sanity-check.py docs apps/api/src/api/v1/iwooos.py apps/web/src/app/api/iwooos/wazuh/route.ts scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py -python3 -m py_compile apps/api/src/api/v1/iwooos.py scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/security-mirror-progress-guard.py +python3 scripts/ops/doc-secrets-sanity-check.py docs apps/api/src/api/v1/iwooos.py apps/web/src/app/api/iwooos/wazuh/route.ts scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/wazuh-readonly-release-lane-preflight.py +python3 -m py_compile apps/api/src/api/v1/iwooos.py scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/wazuh-readonly-release-lane-preflight.py scripts/security/security-mirror-progress-guard.py git diff --check ``` @@ -66,6 +70,7 @@ git diff --check - `pytest apps/api/tests/test_iwooos_wazuh_api.py`:`4 passed`。 - `wazuh-readonly-route-boundary-guard`:`route=2 public_ui_files=1 forbidden=0 runtime_gate=0`。 - `wazuh-readonly-release-gate`:`source=1 push=0 deploy=0 readback=0 runtime_gate=0`。 +- `wazuh-readonly-release-lane-preflight`:`ready=0 acks=0/6 evidence=0/6 runtime_gate=0`。 - `security-mirror-progress-guard`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 - `doc-secrets-sanity-check`:`DOC_SECRET_SANITY_OK scanned_files=969`。 - `py_compile`:通過。 @@ -73,7 +78,7 @@ git diff --check ## 乾淨套用 Proof -乾淨套用 proof 需從最新 `gitea/main=80604403` 或更新的主線建立獨立 worktree: +乾淨套用 proof 需從最新 `gitea/main` 建立獨立 worktree: ```bash git worktree add /private/tmp/awoooi-iwooos-wazuh-release-apply-check- gitea/main @@ -113,6 +118,7 @@ python3 scripts/security/wazuh-readonly-production-readback.py --allow-predeploy 合併 / 部署前需確認: - 使用具備正式權限的 Gitea lane 合併 `codex/iwooos-wazuh-boundary-guard-20260624` 分支 HEAD 或同等 patch;不得 force push。 +- release lane preflight 目前固定 `formal_release_lane_ready_count=0`、`accepted_ack_flag_count=0/6`、`accepted_evidence_field_count=0/6`;不得把一般「批准繼續」當成 release lane owner response。 - 目前非互動式 push 實測仍被 Gitea HTTPS credential 擋住:`fatal: could not read Username for 'https://gitea.wooo.work': terminal prompts disabled`。 - 不得複製舊 workspace 的內嵌明文 Gitea token。 - 不得把 Wazuh URL、帳密、token、cookie、private key、runner token 或 webhook secret 寫入 repo。 @@ -161,6 +167,7 @@ python3 scripts/security/wazuh-readonly-production-readback.py --json | Wazuh route boundary source guard | `100%` | 已納入 `security-mirror-progress-guard` | | Production readback 驗收腳本 | `100%` | 已完成;正式部署後不得接受 404 | | Wazuh release gate snapshot / guard | `100%` | 已完成;固定 push/deploy/readback 仍 blocked | +| Wazuh release lane preflight | `100%` | 已完成;owner acks `0/6`、evidence `0/6`、正式 release ready `0` | | 乾淨套用 proof | `100%` | patch set 可落在最新 `gitea/main` 並通過同組 guard;最終 hash 以 release 前 readback 為準 | | Gitea push | `0%` | 受控 workspace HTTPS credential 缺失 | | Production deploy / readback | `0%` | 等待 release lane | @@ -170,8 +177,9 @@ python3 scripts/security/wazuh-readonly-production-readback.py --json ## 下一步優先序 -1. 解決受控 workspace Gitea HTTPS push 認證,或由正式 release lane 合併 `codex/iwooos-wazuh-boundary-guard-20260624` 分支 HEAD。 -2. 部署後先驗證 `/api/iwooos/wazuh` 不再 404,且預設 disabled 邊界正確。 -3. 另開 owner gate 決定是否啟用 server-side Wazuh read-only metadata query。 -4. 收件 Wazuh manager health ref、agent status ref、event refs、host forensic refs 與 containment / recovery proof。 -5. 仍禁止 active response、host write、firewall / Nginx / Docker / K8s runtime action、Kali active scan、secret 明文收集。 +1. 先補 release lane owner response:選擇 formal merge、formal patch apply 或安全 credential push,並補 6 個 ack 與 6 個 evidence 欄位。 +2. 解決受控 workspace Gitea HTTPS push 認證,或由正式 release lane 合併 `codex/iwooos-wazuh-boundary-guard-20260624` 分支 HEAD。 +3. 部署後先驗證 `/api/iwooos/wazuh` 不再 404,且預設 disabled 邊界正確。 +4. 另開 owner gate 決定是否啟用 server-side Wazuh read-only metadata query。 +5. 收件 Wazuh manager health ref、agent status ref、event refs、host forensic refs 與 containment / recovery proof。 +6. 仍禁止 active response、host write、firewall / Nginx / Docker / K8s runtime action、Kali active scan、secret 明文收集。 diff --git a/docs/security/wazuh-readonly-release-gate.snapshot.json b/docs/security/wazuh-readonly-release-gate.snapshot.json index eaf3d1e60..109233720 100644 --- a/docs/security/wazuh-readonly-release-gate.snapshot.json +++ b/docs/security/wazuh-readonly-release-gate.snapshot.json @@ -14,7 +14,7 @@ "wazuh_active_response_authorized": false, "wazuh_api_live_query_authorized": false }, - "generated_at": "2026-06-24T22:05:00+08:00", + "generated_at": "2026-06-24T22:25:00+08:00", "missing_required_source_paths": [], "mode": "repo_release_gate_no_runtime_no_secret_collection", "operator_interpretation": [ @@ -75,13 +75,13 @@ ], "release_lane_evidence": { "apply_check_status": "passed_external_readback_required_after_final_commit", - "base_commit": "80604403", + "base_commit_readback": "run git rev-parse gitea/main before release; do not hardcode a moving main commit", "base_ref": "gitea/main", "gitea_push_blocker": "https_noninteractive_credential_required", "production_readback_status": "predeploy_404_observed", "release_patch_set_readback": "generate with git format-patch gitea/main..HEAD after the final docs commit, then record sha256 outside the committed file", "source_branch": "codex/iwooos-wazuh-boundary-guard-20260624", - "source_fix_commit": "47d36e85", + "source_fix_commit_readback": "run git log --oneline gitea/main..HEAD before release; do not hardcode a rebase-sensitive commit hash", "source_head_readback": "run git rev-parse HEAD after the final docs commit; do not hardcode a self-referential commit hash" }, "required_source_paths": [ diff --git a/docs/security/wazuh-readonly-release-lane-preflight.snapshot.json b/docs/security/wazuh-readonly-release-lane-preflight.snapshot.json new file mode 100644 index 000000000..73b6bac6d --- /dev/null +++ b/docs/security/wazuh-readonly-release-lane-preflight.snapshot.json @@ -0,0 +1,104 @@ +{ + "allowed_release_methods": [ + "formal_gitea_merge", + "formal_patch_apply", + "maintainer_local_push_with_safe_credential" + ], + "blocked_actions": [ + "plain_text_gitea_token_in_remote_url", + "copy_token_from_dirty_workspace", + "force_push", + "nginx_or_gateway_workaround_for_404", + "docker_restart_for_wazuh_route", + "k8s_or_argocd_manual_apply_for_wazuh_route", + "firewall_change_for_wazuh_route", + "wazuh_secret_or_manager_change_for_api_404", + "enable_wazuh_live_metadata_without_owner_gate", + "enable_wazuh_active_response", + "host_write_or_kali_active_scan" + ], + "execution_boundaries": { + "force_push_allowed": false, + "host_write_authorized": false, + "kali_active_scan_authorized": false, + "not_authorization": true, + "plain_text_token_workaround_allowed": false, + "production_deploy_authorized": false, + "repo_write_authorized": false, + "runtime_execution_authorized": false, + "secret_value_collection_allowed": false, + "wazuh_active_response_authorized": false, + "wazuh_api_live_query_authorized": false + }, + "generated_at": "2026-06-24T22:20:00+08:00", + "mode": "repo_preflight_no_secret_no_runtime_no_push", + "operator_interpretation": [ + "此 preflight 通過前,不得把 Gitea credential blocker 視為可繞過。", + "正式 release 可以選 formal merge、formal patch apply 或安全 credential push,但都需要 owner response 與 deploy 後 readback。", + "不得用 Nginx、Docker、K8s、firewall、Wazuh secret 或主機重啟來修 public API 404。", + "Wazuh live metadata 查詢與 active response 是不同 gate;本 preflight 不授權任何 runtime action。" + ], + "post_deploy_readback": { + "command": "python3 scripts/security/wazuh-readonly-production-readback.py --json", + "must_not_return_http_404": true, + "required": true, + "runtime_gate_expected": 0 + }, + "release_lanes": [ + { + "lane_id": "formal_gitea_merge", + "meaning": "由具備正式 Gitea 權限者合併 Wazuh 分支;不得 force push。", + "runtime_authorized": false, + "status": "blocked_owner_response_required" + }, + { + "lane_id": "formal_patch_apply", + "meaning": "由正式 release lane 套用已驗證 patch set;不得跳過 production readback。", + "runtime_authorized": false, + "status": "blocked_owner_response_required" + }, + { + "lane_id": "maintainer_local_push_with_safe_credential", + "meaning": "只接受安全的 credential helper / SSH key / 正式 release token;不得使用明文 token workaround。", + "runtime_authorized": false, + "status": "blocked_safe_credential_required" + } + ], + "required_ack_flags": [ + "approve_formal_release_lane", + "confirm_no_plaintext_token_workaround", + "confirm_no_force_push", + "confirm_no_runtime_workaround", + "confirm_production_readback_after_deploy", + "confirm_wazuh_live_metadata_requires_separate_owner_gate" + ], + "required_evidence_fields": [ + "release_lane_owner", + "release_method", + "target_branch_or_patch_set", + "post_deploy_readback_command", + "rollback_owner", + "blocked_runtime_actions_ack" + ], + "schema_version": "iwooos_wazuh_readonly_release_lane_preflight_v1", + "status": "blocked_waiting_formal_release_lane_owner_response", + "summary": { + "accepted_ack_flag_count": 0, + "accepted_evidence_field_count": 0, + "allowed_release_method_count": 3, + "force_push_allowed_count": 0, + "formal_release_lane_ready_count": 0, + "gitea_push_authorized_count": 0, + "patch_apply_authorized_count": 0, + "plain_text_token_workaround_allowed_count": 0, + "production_deploy_authorized_count": 0, + "production_readback_passed_count": 0, + "production_readback_required_count": 1, + "required_ack_flag_count": 6, + "required_evidence_field_count": 6, + "runtime_gate_count": 0, + "runtime_workaround_allowed_count": 0, + "safe_credential_available_count": 0, + "wazuh_live_metadata_owner_gate_ready_count": 0 + } +} diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 7e84a139d..495249356 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -95,6 +95,10 @@ def validate(root: Path) -> None: str(root / "scripts" / "security" / "wazuh-readonly-release-gate.py") ) wazuh_readonly_release_gate["validate"](root) + wazuh_readonly_release_lane_preflight = runpy.run_path( + str(root / "scripts" / "security" / "wazuh-readonly-release-lane-preflight.py") + ) + wazuh_readonly_release_lane_preflight["validate"](root) telegram_alert_readability_guard = runpy.run_path( str(root / "scripts" / "security" / "telegram-alert-readability-guard.py") ) diff --git a/scripts/security/wazuh-readonly-release-gate.py b/scripts/security/wazuh-readonly-release-gate.py index b4b74a380..262d53616 100644 --- a/scripts/security/wazuh-readonly-release-gate.py +++ b/scripts/security/wazuh-readonly-release-gate.py @@ -43,10 +43,10 @@ def build_report(root: Path, generated_at: str | None = None) -> dict[str, Any]: "mode": "repo_release_gate_no_runtime_no_secret_collection", "release_lane_evidence": { "source_branch": "codex/iwooos-wazuh-boundary-guard-20260624", - "source_fix_commit": "47d36e85", + "source_fix_commit_readback": "run git log --oneline gitea/main..HEAD before release; do not hardcode a rebase-sensitive commit hash", "source_head_readback": "run git rev-parse HEAD after the final docs commit; do not hardcode a self-referential commit hash", "base_ref": "gitea/main", - "base_commit": "80604403", + "base_commit_readback": "run git rev-parse gitea/main before release; do not hardcode a moving main commit", "release_patch_set_readback": "generate with git format-patch gitea/main..HEAD after the final docs commit, then record sha256 outside the committed file", "apply_check_status": "passed_external_readback_required_after_final_commit", "production_readback_status": "predeploy_404_observed", diff --git a/scripts/security/wazuh-readonly-release-lane-preflight.py b/scripts/security/wazuh-readonly-release-lane-preflight.py new file mode 100644 index 000000000..1b5237e17 --- /dev/null +++ b/scripts/security/wazuh-readonly-release-lane-preflight.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +""" +IwoooS Wazuh 只讀 API release lane preflight。 + +本工具只檢查 repo 內 committed snapshot,固定 release 前必須由正式 lane +提供非敏感證據;不讀 git credential、不推送、不部署、不查 Wazuh、不改主機。 +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Any + + +TAIPEI = timezone(timedelta(hours=8)) +SNAPSHOT_PATH = Path("docs/security/wazuh-readonly-release-lane-preflight.snapshot.json") +REQUIRED_ACK_FLAGS = [ + "approve_formal_release_lane", + "confirm_no_plaintext_token_workaround", + "confirm_no_force_push", + "confirm_no_runtime_workaround", + "confirm_production_readback_after_deploy", + "confirm_wazuh_live_metadata_requires_separate_owner_gate", +] +REQUIRED_EVIDENCE_FIELDS = [ + "release_lane_owner", + "release_method", + "target_branch_or_patch_set", + "post_deploy_readback_command", + "rollback_owner", + "blocked_runtime_actions_ack", +] +ALLOWED_RELEASE_METHODS = [ + "formal_gitea_merge", + "formal_patch_apply", + "maintainer_local_push_with_safe_credential", +] + + +def now_iso() -> str: + return datetime.now(TAIPEI).replace(microsecond=0).isoformat() + + +def build_report(generated_at: str | None = None) -> dict[str, Any]: + return { + "schema_version": "iwooos_wazuh_readonly_release_lane_preflight_v1", + "generated_at": generated_at or now_iso(), + "status": "blocked_waiting_formal_release_lane_owner_response", + "mode": "repo_preflight_no_secret_no_runtime_no_push", + "required_ack_flags": REQUIRED_ACK_FLAGS, + "required_evidence_fields": REQUIRED_EVIDENCE_FIELDS, + "allowed_release_methods": ALLOWED_RELEASE_METHODS, + "summary": { + "required_ack_flag_count": len(REQUIRED_ACK_FLAGS), + "accepted_ack_flag_count": 0, + "required_evidence_field_count": len(REQUIRED_EVIDENCE_FIELDS), + "accepted_evidence_field_count": 0, + "allowed_release_method_count": len(ALLOWED_RELEASE_METHODS), + "formal_release_lane_ready_count": 0, + "safe_credential_available_count": 0, + "patch_apply_authorized_count": 0, + "gitea_push_authorized_count": 0, + "production_deploy_authorized_count": 0, + "production_readback_required_count": 1, + "production_readback_passed_count": 0, + "plain_text_token_workaround_allowed_count": 0, + "force_push_allowed_count": 0, + "runtime_workaround_allowed_count": 0, + "wazuh_live_metadata_owner_gate_ready_count": 0, + "runtime_gate_count": 0, + }, + "release_lanes": [ + { + "lane_id": "formal_gitea_merge", + "status": "blocked_owner_response_required", + "meaning": "由具備正式 Gitea 權限者合併 Wazuh 分支;不得 force push。", + "runtime_authorized": False, + }, + { + "lane_id": "formal_patch_apply", + "status": "blocked_owner_response_required", + "meaning": "由正式 release lane 套用已驗證 patch set;不得跳過 production readback。", + "runtime_authorized": False, + }, + { + "lane_id": "maintainer_local_push_with_safe_credential", + "status": "blocked_safe_credential_required", + "meaning": "只接受安全的 credential helper / SSH key / 正式 release token;不得使用明文 token workaround。", + "runtime_authorized": False, + }, + ], + "blocked_actions": [ + "plain_text_gitea_token_in_remote_url", + "copy_token_from_dirty_workspace", + "force_push", + "nginx_or_gateway_workaround_for_404", + "docker_restart_for_wazuh_route", + "k8s_or_argocd_manual_apply_for_wazuh_route", + "firewall_change_for_wazuh_route", + "wazuh_secret_or_manager_change_for_api_404", + "enable_wazuh_live_metadata_without_owner_gate", + "enable_wazuh_active_response", + "host_write_or_kali_active_scan", + ], + "post_deploy_readback": { + "required": True, + "command": "python3 scripts/security/wazuh-readonly-production-readback.py --json", + "must_not_return_http_404": True, + "runtime_gate_expected": 0, + }, + "execution_boundaries": { + "not_authorization": True, + "secret_value_collection_allowed": False, + "plain_text_token_workaround_allowed": False, + "force_push_allowed": False, + "repo_write_authorized": False, + "production_deploy_authorized": False, + "runtime_execution_authorized": False, + "wazuh_api_live_query_authorized": False, + "wazuh_active_response_authorized": False, + "host_write_authorized": False, + "kali_active_scan_authorized": False, + }, + "operator_interpretation": [ + "此 preflight 通過前,不得把 Gitea credential blocker 視為可繞過。", + "正式 release 可以選 formal merge、formal patch apply 或安全 credential push,但都需要 owner response 與 deploy 後 readback。", + "不得用 Nginx、Docker、K8s、firewall、Wazuh secret 或主機重啟來修 public API 404。", + "Wazuh live metadata 查詢與 active response 是不同 gate;本 preflight 不授權任何 runtime action。", + ], + } + + +def load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def validate(root: Path) -> None: + snapshot_path = root / SNAPSHOT_PATH + if not snapshot_path.exists(): + raise SystemExit( + f"BLOCKED Wazuh release lane preflight snapshot missing: {SNAPSHOT_PATH.as_posix()}" + ) + snapshot = load_json(snapshot_path) + expected = build_report(snapshot.get("generated_at")) + + if snapshot.get("schema_version") != expected["schema_version"]: + raise SystemExit("BLOCKED Wazuh release lane preflight schema_version mismatch") + if snapshot.get("status") != expected["status"]: + raise SystemExit("BLOCKED Wazuh release lane preflight status mismatch") + for key, expected_value in expected["summary"].items(): + actual = snapshot.get("summary", {}).get(key) + if actual != expected_value: + raise SystemExit( + f"BLOCKED Wazuh release lane preflight summary.{key}: " + f"expected {expected_value!r}, got {actual!r}" + ) + for key, value in snapshot.get("execution_boundaries", {}).items(): + if key == "not_authorization": + if value is not True: + raise SystemExit("BLOCKED Wazuh release lane preflight not_authorization must be true") + elif value is not False: + raise SystemExit(f"BLOCKED Wazuh release lane preflight execution_boundaries.{key}: expected false") + + +def main() -> int: + parser = argparse.ArgumentParser(description="IwoooS Wazuh 只讀 API release lane preflight") + parser.add_argument("--root", default=".", help="repository root") + parser.add_argument("--output", help="寫出 JSON 報告") + parser.add_argument("--generated-at", help="固定報告時間,供 committed snapshot 使用") + args = parser.parse_args() + + root = Path(args.root).resolve() + report = build_report(args.generated_at) + if args.output: + output = Path(args.output) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8") + validate(root) + summary = report["summary"] + print( + "WAZUH_READONLY_RELEASE_LANE_PREFLIGHT_OK " + f"ready={summary['formal_release_lane_ready_count']} " + f"acks={summary['accepted_ack_flag_count']}/{summary['required_ack_flag_count']} " + f"evidence={summary['accepted_evidence_field_count']}/{summary['required_evidence_field_count']} " + f"runtime_gate={summary['runtime_gate_count']}" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From bb481956ae8e441f66bac9e0022cfe19a551e0ea Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 24 Jun 2026 22:26:53 +0800 Subject: [PATCH 04/11] docs(iwooos): refresh Wazuh release lane readback --- docs/LOGBOOK.md | 8 ++++++++ .../security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md | 5 +++-- docs/security/wazuh-readonly-release-gate.snapshot.json | 2 +- .../wazuh-readonly-release-lane-preflight.snapshot.json | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index c9519f53c..4605326c1 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -217,6 +217,14 @@ - 完成度:release lane preflight artifact / guard `100%`;owner acks / evidence `0%`;Gitea push / production deploy / production readback / runtime gate 仍 `0%`。 - 邊界:本段沒有讀 git credential、沒有推送、沒有部署、沒有 Wazuh live query、沒有 host write、沒有 runtime action;只是把 release blocker 變成可審核 gate。 +**Release lane rebase/readback 補充,22:26 Asia/Taipei**: +- `gitea/main` 已再前進到 `ffc167e2 docs(ops): record momo production import boundary readback [skip ci]`;Wazuh 分支已 rebase 到此基底,沒有覆蓋 MOMO production import boundary readback 紀錄。 +- Rebase 後 Wazuh 分支目前只比 `gitea/main` 多三個提交:`9b40ca89 fix(iwooos): 接上 Wazuh 只讀 API 邊界`、`8435a435 docs(iwooos): 記錄 Wazuh release apply proof`、`59188ca1 feat(iwooos): 新增 Wazuh release lane preflight`。 +- 已重新產生 `docs/security/wazuh-readonly-release-gate.snapshot.json` 與 `docs/security/wazuh-readonly-release-lane-preflight.snapshot.json`;兩者仍固定 source / guard 已完成,但 push、deploy、production readback、runtime gate 仍為 `0`。 +- Rebase 後重跑 `pytest apps/api/tests/test_iwooos_wazuh_api.py`、Wazuh route guard、release gate、release-lane preflight、`security-mirror-progress-guard.py`、`doc-secrets-sanity-check.py`、`py_compile`、`git diff --check` 全部通過;正式 production readback 不加 `--allow-predeploy-404` 仍正確阻擋 `404`。 +- 完成度:rebase / snapshot refresh `100%`;formal release lane owner acks `0/6`、evidence `0/6`;Gitea push / production deploy / production readback `0%`。 +- 邊界:本段沒有讀 git credential、沒有推送、沒有部署、沒有 live Wazuh query、沒有 Nginx / Docker / K8s / firewall / host / Wazuh secret 變更。 + ## 2026-06-24|21:04 recovery readback 與 MOMO V10.651 雙機基準收斂 **背景**:前一輪 MOMO workspace readback 指到 `V10.646`,但 21:04 live health 已回 `V10.651`。因此本輪重新比對 Gitea `wooo/ewoooc` `main`、正式站 `/health`、Mac Mini / MacBook Pro Codex workspace 與 full-stack cold-start,避免「網站可用」和「版本 / 資料最新」互相混淆。 diff --git a/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md b/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md index f6964d21b..5e4d36f2b 100644 --- a/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md +++ b/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md @@ -72,7 +72,7 @@ git diff --check - `wazuh-readonly-release-gate`:`source=1 push=0 deploy=0 readback=0 runtime_gate=0`。 - `wazuh-readonly-release-lane-preflight`:`ready=0 acks=0/6 evidence=0/6 runtime_gate=0`。 - `security-mirror-progress-guard`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 -- `doc-secrets-sanity-check`:`DOC_SECRET_SANITY_OK scanned_files=969`。 +- `doc-secrets-sanity-check`:`DOC_SECRET_SANITY_OK scanned_files=970`。 - `py_compile`:通過。 - `git diff --check`:通過。 @@ -92,8 +92,9 @@ git am /private/tmp/awoooi-iwooos-wazuh-boundary-release-patch-/*.pat - `pytest apps/api/tests/test_iwooos_wazuh_api.py`:`4 passed`。 - `python3 scripts/security/wazuh-readonly-route-boundary-guard.py --root .`:`WAZUH_READONLY_ROUTE_BOUNDARY_GUARD_OK route=2 public_ui_files=1 forbidden=0 runtime_gate=0`。 - `python3 scripts/security/wazuh-readonly-release-gate.py --root .`:`WAZUH_READONLY_RELEASE_GATE_OK source=1 push=0 deploy=0 readback=0 runtime_gate=0`。 +- `python3 scripts/security/wazuh-readonly-release-lane-preflight.py --root .`:`WAZUH_READONLY_RELEASE_LANE_PREFLIGHT_OK ready=0 acks=0/6 evidence=0/6 runtime_gate=0`。 - `python3 scripts/security/security-mirror-progress-guard.py --root .`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 -- `python3 scripts/ops/doc-secrets-sanity-check.py ...`:`DOC_SECRET_SANITY_OK scanned_files=969`。 +- `python3 scripts/ops/doc-secrets-sanity-check.py ...`:`DOC_SECRET_SANITY_OK scanned_files=970`。 - `python3 -m py_compile ...`:通過。 - `git diff --check`:通過。 diff --git a/docs/security/wazuh-readonly-release-gate.snapshot.json b/docs/security/wazuh-readonly-release-gate.snapshot.json index 109233720..216a2d20e 100644 --- a/docs/security/wazuh-readonly-release-gate.snapshot.json +++ b/docs/security/wazuh-readonly-release-gate.snapshot.json @@ -14,7 +14,7 @@ "wazuh_active_response_authorized": false, "wazuh_api_live_query_authorized": false }, - "generated_at": "2026-06-24T22:25:00+08:00", + "generated_at": "2026-06-24T22:26:00+08:00", "missing_required_source_paths": [], "mode": "repo_release_gate_no_runtime_no_secret_collection", "operator_interpretation": [ diff --git a/docs/security/wazuh-readonly-release-lane-preflight.snapshot.json b/docs/security/wazuh-readonly-release-lane-preflight.snapshot.json index 73b6bac6d..1210da7d4 100644 --- a/docs/security/wazuh-readonly-release-lane-preflight.snapshot.json +++ b/docs/security/wazuh-readonly-release-lane-preflight.snapshot.json @@ -30,7 +30,7 @@ "wazuh_active_response_authorized": false, "wazuh_api_live_query_authorized": false }, - "generated_at": "2026-06-24T22:20:00+08:00", + "generated_at": "2026-06-24T22:26:00+08:00", "mode": "repo_preflight_no_secret_no_runtime_no_push", "operator_interpretation": [ "此 preflight 通過前,不得把 Gitea credential blocker 視為可繞過。", From 20748fe1baff9fb3ab9ca42f03913ee2702b0fb6 Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 24 Jun 2026 22:33:29 +0800 Subject: [PATCH 05/11] feat(iwooos): define Wazuh release owner gate --- docs/LOGBOOK.md | 7 + ...OOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md | 20 +- ...adonly-release-owner-request.snapshot.json | 137 ++++++++++ ...se-owner-response-acceptance.snapshot.json | 141 +++++++++++ .../security-mirror-progress-guard.py | 8 + .../wazuh-readonly-release-owner-request.py | 230 +++++++++++++++++ ...donly-release-owner-response-acceptance.py | 233 ++++++++++++++++++ 7 files changed, 771 insertions(+), 5 deletions(-) create mode 100644 docs/security/wazuh-readonly-release-owner-request.snapshot.json create mode 100644 docs/security/wazuh-readonly-release-owner-response-acceptance.snapshot.json create mode 100644 scripts/security/wazuh-readonly-release-owner-request.py create mode 100644 scripts/security/wazuh-readonly-release-owner-response-acceptance.py diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 4605326c1..6cd68ca19 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -225,6 +225,13 @@ - 完成度:rebase / snapshot refresh `100%`;formal release lane owner acks `0/6`、evidence `0/6`;Gitea push / production deploy / production readback `0%`。 - 邊界:本段沒有讀 git credential、沒有推送、沒有部署、沒有 live Wazuh query、沒有 Nginx / Docker / K8s / firewall / host / Wazuh secret 變更。 +**Release owner request / acceptance 補充,22:32 Asia/Taipei**: +- 新增 `scripts/security/wazuh-readonly-release-owner-request.py`、`docs/security/wazuh-readonly-release-owner-request.snapshot.json`、`scripts/security/wazuh-readonly-release-owner-response-acceptance.py`、`docs/security/wazuh-readonly-release-owner-response-acceptance.snapshot.json`,並接入 `security-mirror-progress-guard.py`。 +- Owner request 草稿固定 required ack flags `6`、required evidence fields `6`、allowed release methods `3`、forbidden payloads `12`、blocked actions `11`;目前 request sent `0`、owner response accepted `0`、runtime gate `0`。 +- Owner response acceptance 帳本固定 reviewer checks `15`、outcome lanes `10`、blocked actions `13`;目前 received `0`、accepted `0`、acks `0/6`、evidence `0/6`、formal release ready `0`。 +- 完成度:release owner request / acceptance artifact 與 guard `100%`;正式 owner response / release ready / push / deploy / production readback `0%`。 +- 邊界:本段沒有發送 request、沒有收件、沒有讀 credential、沒有推送、沒有部署、沒有 Wazuh live query、沒有 runtime action;一般「批准繼續」仍不可當 release lane owner response。 + ## 2026-06-24|21:04 recovery readback 與 MOMO V10.651 雙機基準收斂 **背景**:前一輪 MOMO workspace readback 指到 `V10.646`,但 21:04 live health 已回 `V10.651`。因此本輪重新比對 Gitea `wooo/ewoooc` `main`、正式站 `/health`、Mac Mini / MacBook Pro Codex workspace 與 full-stack cold-start,避免「網站可用」和「版本 / 資料最新」互相混淆。 diff --git a/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md b/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md index 5e4d36f2b..90a0d9aa1 100644 --- a/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md +++ b/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md @@ -31,9 +31,13 @@ - `scripts/security/wazuh-readonly-production-readback.py` - `scripts/security/wazuh-readonly-release-gate.py` - `scripts/security/wazuh-readonly-release-lane-preflight.py` +- `scripts/security/wazuh-readonly-release-owner-request.py` +- `scripts/security/wazuh-readonly-release-owner-response-acceptance.py` - `scripts/security/security-mirror-progress-guard.py` - `docs/security/wazuh-readonly-release-gate.snapshot.json` - `docs/security/wazuh-readonly-release-lane-preflight.snapshot.json` +- `docs/security/wazuh-readonly-release-owner-request.snapshot.json` +- `docs/security/wazuh-readonly-release-owner-response-acceptance.snapshot.json` - `docs/LOGBOOK.md` 完成內容: @@ -49,6 +53,7 @@ - 新增 production readback 腳本,部署後可直接驗證 public API 不再 404、schema / status / boundary 正確,且沒有 raw payload、內網 IP、agent 原名或 secret 洩漏。 - 新增 release gate snapshot 與 guard,固定 source-side 已完成、Gitea push / production deploy / production readback 尚未完成,避免後續把 predeploy 404 誤判成通過。 - 新增 release lane preflight snapshot 與 guard,固定正式 release 前必須選擇 `formal_gitea_merge`、`formal_patch_apply` 或 `maintainer_local_push_with_safe_credential` 其中一條合規 lane,且 owner ack / evidence 未到齊前不得 push、deploy、force push、使用明文 token workaround 或改 runtime。 +- 新增 release owner request 草稿與 owner response acceptance 帳本,將 required ack flags、required evidence fields、allowed release methods、blocked actions、forbidden payloads 與 reviewer checks 機器可讀化;目前 request sent、response received / accepted、release ready、runtime gate 全部維持 `0`。 ## 已完成驗證 @@ -59,9 +64,11 @@ pytest apps/api/tests/test_iwooos_wazuh_api.py python3 scripts/security/wazuh-readonly-route-boundary-guard.py --root . python3 scripts/security/wazuh-readonly-release-gate.py --root . python3 scripts/security/wazuh-readonly-release-lane-preflight.py --root . +python3 scripts/security/wazuh-readonly-release-owner-request.py --root . +python3 scripts/security/wazuh-readonly-release-owner-response-acceptance.py --root . python3 scripts/security/security-mirror-progress-guard.py --root . -python3 scripts/ops/doc-secrets-sanity-check.py docs apps/api/src/api/v1/iwooos.py apps/web/src/app/api/iwooos/wazuh/route.ts scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/wazuh-readonly-release-lane-preflight.py -python3 -m py_compile apps/api/src/api/v1/iwooos.py scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/wazuh-readonly-release-lane-preflight.py scripts/security/security-mirror-progress-guard.py +python3 scripts/ops/doc-secrets-sanity-check.py docs apps/api/src/api/v1/iwooos.py apps/web/src/app/api/iwooos/wazuh/route.ts scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/wazuh-readonly-release-lane-preflight.py scripts/security/wazuh-readonly-release-owner-request.py scripts/security/wazuh-readonly-release-owner-response-acceptance.py +python3 -m py_compile apps/api/src/api/v1/iwooos.py scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/wazuh-readonly-release-lane-preflight.py scripts/security/wazuh-readonly-release-owner-request.py scripts/security/wazuh-readonly-release-owner-response-acceptance.py scripts/security/security-mirror-progress-guard.py git diff --check ``` @@ -71,8 +78,10 @@ git diff --check - `wazuh-readonly-route-boundary-guard`:`route=2 public_ui_files=1 forbidden=0 runtime_gate=0`。 - `wazuh-readonly-release-gate`:`source=1 push=0 deploy=0 readback=0 runtime_gate=0`。 - `wazuh-readonly-release-lane-preflight`:`ready=0 acks=0/6 evidence=0/6 runtime_gate=0`。 +- `wazuh-readonly-release-owner-request`:`drafts=1 sent=0 accepted=0 runtime_gate=0`。 +- `wazuh-readonly-release-owner-response-acceptance`:`received=0 accepted=0 acks=0/6 evidence=0/6 runtime_gate=0`。 - `security-mirror-progress-guard`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 -- `doc-secrets-sanity-check`:`DOC_SECRET_SANITY_OK scanned_files=970`。 +- `doc-secrets-sanity-check`:`DOC_SECRET_SANITY_OK scanned_files=972`。 - `py_compile`:通過。 - `git diff --check`:通過。 @@ -94,7 +103,7 @@ git am /private/tmp/awoooi-iwooos-wazuh-boundary-release-patch-/*.pat - `python3 scripts/security/wazuh-readonly-release-gate.py --root .`:`WAZUH_READONLY_RELEASE_GATE_OK source=1 push=0 deploy=0 readback=0 runtime_gate=0`。 - `python3 scripts/security/wazuh-readonly-release-lane-preflight.py --root .`:`WAZUH_READONLY_RELEASE_LANE_PREFLIGHT_OK ready=0 acks=0/6 evidence=0/6 runtime_gate=0`。 - `python3 scripts/security/security-mirror-progress-guard.py --root .`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 -- `python3 scripts/ops/doc-secrets-sanity-check.py ...`:`DOC_SECRET_SANITY_OK scanned_files=970`。 +- `python3 scripts/ops/doc-secrets-sanity-check.py ...`:`DOC_SECRET_SANITY_OK scanned_files=972`。 - `python3 -m py_compile ...`:通過。 - `git diff --check`:通過。 @@ -169,6 +178,7 @@ python3 scripts/security/wazuh-readonly-production-readback.py --json | Production readback 驗收腳本 | `100%` | 已完成;正式部署後不得接受 404 | | Wazuh release gate snapshot / guard | `100%` | 已完成;固定 push/deploy/readback 仍 blocked | | Wazuh release lane preflight | `100%` | 已完成;owner acks `0/6`、evidence `0/6`、正式 release ready `0` | +| Wazuh release owner request / acceptance | `100%` | 已完成只讀草稿與收件帳本;request sent `0`、response accepted `0` | | 乾淨套用 proof | `100%` | patch set 可落在最新 `gitea/main` 並通過同組 guard;最終 hash 以 release 前 readback 為準 | | Gitea push | `0%` | 受控 workspace HTTPS credential 缺失 | | Production deploy / readback | `0%` | 等待 release lane | @@ -178,7 +188,7 @@ python3 scripts/security/wazuh-readonly-production-readback.py --json ## 下一步優先序 -1. 先補 release lane owner response:選擇 formal merge、formal patch apply 或安全 credential push,並補 6 個 ack 與 6 個 evidence 欄位。 +1. 先依 `wazuh-readonly-release-owner-request.snapshot.json` 補 release lane owner response:選擇 formal merge、formal patch apply 或安全 credential push,並補 6 個 ack 與 6 個 evidence 欄位。 2. 解決受控 workspace Gitea HTTPS push 認證,或由正式 release lane 合併 `codex/iwooos-wazuh-boundary-guard-20260624` 分支 HEAD。 3. 部署後先驗證 `/api/iwooos/wazuh` 不再 404,且預設 disabled 邊界正確。 4. 另開 owner gate 決定是否啟用 server-side Wazuh read-only metadata query。 diff --git a/docs/security/wazuh-readonly-release-owner-request.snapshot.json b/docs/security/wazuh-readonly-release-owner-request.snapshot.json new file mode 100644 index 000000000..303bb9e85 --- /dev/null +++ b/docs/security/wazuh-readonly-release-owner-request.snapshot.json @@ -0,0 +1,137 @@ +{ + "execution_boundaries": { + "dispatch_authorized": false, + "force_push_allowed": false, + "gitea_push_authorized": false, + "host_write_authorized": false, + "kali_active_scan_authorized": false, + "not_authorization": true, + "patch_apply_authorized": false, + "plain_text_token_workaround_allowed": false, + "production_deploy_authorized": false, + "recipient_confirmed": false, + "repo_write_authorized": false, + "request_sent": false, + "runtime_execution_authorized": false, + "secret_value_collection_allowed": false, + "wazuh_active_response_authorized": false, + "wazuh_api_live_query_authorized": false + }, + "generated_at": "2026-06-24T22:32:00+08:00", + "handoff_envelope_fields": [ + "request_id", + "stage_id", + "recipient_role_or_team", + "sender_role_or_team", + "requested_response_window", + "allowed_release_methods", + "required_ack_flags", + "required_evidence_fields", + "target_branch_or_patch_set", + "post_deploy_readback_command", + "forbidden_payloads", + "blocked_runtime_actions", + "followup_owner", + "not_approval" + ], + "mode": "repo_request_draft_no_secret_no_runtime_no_push", + "request_draft": { + "action_buttons_allowed": false, + "allowed_release_methods": [ + "formal_gitea_merge", + "formal_patch_apply", + "maintainer_local_push_with_safe_credential" + ], + "blocked_runtime_actions": [ + "plain_text_gitea_token_in_remote_url", + "copy_token_from_dirty_workspace", + "force_push", + "nginx_or_gateway_workaround_for_404", + "docker_restart_for_wazuh_route", + "k8s_or_argocd_manual_apply_for_wazuh_route", + "firewall_change_for_wazuh_route", + "wazuh_secret_or_manager_change_for_api_404", + "enable_wazuh_live_metadata_without_owner_gate", + "enable_wazuh_active_response", + "host_write_or_kali_active_scan" + ], + "followup_owner": "pending_followup_owner", + "forbidden_payloads": [ + "token", + "secret", + "private_key", + "cookie", + "session", + "authorization_header", + "runner_token", + "webhook_secret", + "wazuh_password", + "wazuh_raw_payload", + "git_credential", + "repo_archive" + ], + "not_approval": true, + "owner_response_accepted": false, + "owner_response_received": false, + "post_deploy_readback_command": "python3 scripts/security/wazuh-readonly-production-readback.py --json", + "recipient_confirmed": false, + "recipient_role_or_team": "pending_release_lane_owner", + "redacted_evidence_refs": [ + "docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md", + "docs/security/wazuh-readonly-release-gate.snapshot.json", + "docs/security/wazuh-readonly-release-lane-preflight.snapshot.json" + ], + "request_id": "iwooos_wazuh_readonly_release_owner_request", + "request_sent": false, + "requested_response_window": "not_scheduled", + "required_ack_flags": [ + "approve_formal_release_lane", + "confirm_no_plaintext_token_workaround", + "confirm_no_force_push", + "confirm_no_runtime_workaround", + "confirm_production_readback_after_deploy", + "confirm_wazuh_live_metadata_requires_separate_owner_gate" + ], + "required_evidence_fields": [ + "release_lane_owner", + "release_method", + "target_branch_or_patch_set", + "post_deploy_readback_command", + "rollback_owner", + "blocked_runtime_actions_ack" + ], + "runtime_gate": false, + "sender_role_or_team": "iwooos_security_reviewer", + "stage_id": "P0-IWOOOS-WAZUH-RELEASE", + "target_branch": "codex/iwooos-wazuh-boundary-guard-20260624", + "target_branch_readback": "git log --oneline gitea/main..HEAD", + "target_patch_set_readback": "git format-patch gitea/main..HEAD after final docs commit; record sha256 outside committed docs" + }, + "schema_version": "iwooos_wazuh_readonly_release_owner_request_v1", + "send_after_conditions": [ + "先確認 gitea/main、Wazuh 分支與另一個 AwoooP Session 基線。", + "只送脫敏欄位與 refs;不得附 secret、raw Wazuh payload、git credential 或 runtime 操作要求。", + "一般批准繼續不是 release owner response。", + "收到 response 後仍需先通過 owner response acceptance ledger,不能直接 push 或 deploy。" + ], + "status": "draft_not_dispatched_waiting_release_lane_owner", + "summary": { + "allowed_release_method_count": 3, + "blocked_action_count": 11, + "forbidden_payload_count": 12, + "formal_release_lane_ready_count": 0, + "gitea_push_authorized_count": 0, + "handoff_envelope_field_count": 14, + "owner_response_accepted_count": 0, + "owner_response_received_count": 0, + "patch_apply_authorized_count": 0, + "production_deploy_authorized_count": 0, + "production_readback_passed_count": 0, + "recipient_confirmed_count": 0, + "request_draft_count": 1, + "request_sent_count": 0, + "required_ack_flag_count": 6, + "required_evidence_field_count": 6, + "runtime_gate_count": 0 + } +} diff --git a/docs/security/wazuh-readonly-release-owner-response-acceptance.snapshot.json b/docs/security/wazuh-readonly-release-owner-response-acceptance.snapshot.json new file mode 100644 index 000000000..d1b07d0de --- /dev/null +++ b/docs/security/wazuh-readonly-release-owner-response-acceptance.snapshot.json @@ -0,0 +1,141 @@ +{ + "acceptance_candidate": { + "acceptance_candidate_id": "iwooos_wazuh_readonly_release_owner_response", + "ack_flags": { + "approve_formal_release_lane": false, + "confirm_no_force_push": false, + "confirm_no_plaintext_token_workaround": false, + "confirm_no_runtime_workaround": false, + "confirm_production_readback_after_deploy": false, + "confirm_wazuh_live_metadata_requires_separate_owner_gate": false + }, + "blocked_actions": [ + "plain_text_gitea_token_in_remote_url", + "copy_token_from_dirty_workspace", + "force_push", + "nginx_or_gateway_workaround_for_404", + "docker_restart_for_wazuh_route", + "k8s_or_argocd_manual_apply_for_wazuh_route", + "firewall_change_for_wazuh_route", + "wazuh_secret_or_manager_change_for_api_404", + "enable_wazuh_live_metadata_without_owner_gate", + "enable_wazuh_active_response", + "host_write_or_kali_active_scan", + "mark_general_approval_as_release_response", + "mark_predeploy_404_as_passed_readback" + ], + "decision": "pending_owner_response", + "decision_reason": "pending_owner_response", + "formal_release_lane_ready": false, + "gitea_push_authorized": false, + "not_approval": true, + "outcome_lanes": [ + "waiting_owner_response", + "quarantine_secret_or_raw_payload", + "reject_execution_request", + "request_supplement", + "ready_for_release_reviewer_validation", + "formal_gitea_merge_candidate", + "formal_patch_apply_candidate", + "safe_credential_push_candidate", + "waiting_production_readback", + "waiting_runtime_gate" + ], + "owner_response_accepted": false, + "owner_response_quarantined": false, + "owner_response_received": false, + "owner_response_rejected": false, + "owner_role_or_team": "pending_owner_response", + "patch_apply_authorized": false, + "post_deploy_readback_command": "python3 scripts/security/wazuh-readonly-production-readback.py --json", + "production_deploy_authorized": false, + "production_readback_passed": false, + "redacted_evidence_refs": [], + "release_method": "pending_owner_response", + "request_id": "iwooos_wazuh_readonly_release_owner_request", + "required_ack_flags": [ + "approve_formal_release_lane", + "confirm_no_plaintext_token_workaround", + "confirm_no_force_push", + "confirm_no_runtime_workaround", + "confirm_production_readback_after_deploy", + "confirm_wazuh_live_metadata_requires_separate_owner_gate" + ], + "required_evidence_fields": [ + "release_lane_owner", + "release_method", + "target_branch_or_patch_set", + "post_deploy_readback_command", + "rollback_owner", + "blocked_runtime_actions_ack" + ], + "reviewer_checks": [ + "owner_identity_present", + "release_method_allowed", + "target_scope_matches_wazuh_branch_or_patch_set", + "all_ack_flags_true", + "all_evidence_fields_present", + "redacted_refs_only", + "secret_value_absent", + "no_plaintext_token_workaround", + "no_force_push", + "no_runtime_workaround", + "post_deploy_readback_required", + "rollback_owner_present", + "live_metadata_gate_separate", + "active_response_stays_false", + "counts_transition_safe" + ], + "rollback_owner": "pending_owner_response", + "runtime_gate": false, + "status": "waiting_owner_response", + "supplement_requested": false, + "target_branch_or_patch_set": "pending_owner_response" + }, + "execution_boundaries": { + "force_push_allowed": false, + "gitea_push_authorized": false, + "host_write_authorized": false, + "kali_active_scan_authorized": false, + "not_authorization": true, + "patch_apply_authorized": false, + "plain_text_token_workaround_allowed": false, + "production_deploy_authorized": false, + "repo_write_authorized": false, + "runtime_execution_authorized": false, + "secret_value_collection_allowed": false, + "wazuh_active_response_authorized": false, + "wazuh_api_live_query_authorized": false + }, + "generated_at": "2026-06-24T22:32:00+08:00", + "mode": "metadata_only_acceptance_no_secret_no_runtime_no_push", + "reviewer_instructions": [ + "只有具備完整欄位、脫敏 evidence refs、無 secret、無 runtime 要求的 owner response 才能進 reviewer validation。", + "一般批准繼續、截圖、口頭同意或未列 release method 的訊息都不能增加 accepted count。", + "即使 owner response accepted,也只代表可進正式 release lane;Wazuh live metadata 與 active response 仍是不同 gate。", + "production readback 必須在部署後不加 --allow-predeploy-404 執行,且不得回 404。" + ], + "schema_version": "iwooos_wazuh_readonly_release_owner_response_acceptance_v1", + "status": "waiting_owner_response", + "summary": { + "acceptance_candidate_count": 1, + "accepted_ack_flag_count": 0, + "accepted_evidence_field_count": 0, + "blocked_action_count": 13, + "formal_release_lane_ready_count": 0, + "gitea_push_authorized_count": 0, + "outcome_lane_count": 10, + "owner_response_accepted_count": 0, + "owner_response_quarantined_count": 0, + "owner_response_received_count": 0, + "owner_response_rejected_count": 0, + "patch_apply_authorized_count": 0, + "production_deploy_authorized_count": 0, + "production_readback_passed_count": 0, + "required_ack_flag_count": 6, + "required_evidence_field_count": 6, + "reviewer_check_count": 15, + "runtime_gate_count": 0, + "supplement_requested_count": 0 + } +} diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 495249356..9e8184d59 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -99,6 +99,14 @@ def validate(root: Path) -> None: str(root / "scripts" / "security" / "wazuh-readonly-release-lane-preflight.py") ) wazuh_readonly_release_lane_preflight["validate"](root) + wazuh_readonly_release_owner_request = runpy.run_path( + str(root / "scripts" / "security" / "wazuh-readonly-release-owner-request.py") + ) + wazuh_readonly_release_owner_request["validate"](root) + wazuh_readonly_release_owner_response_acceptance = runpy.run_path( + str(root / "scripts" / "security" / "wazuh-readonly-release-owner-response-acceptance.py") + ) + wazuh_readonly_release_owner_response_acceptance["validate"](root) telegram_alert_readability_guard = runpy.run_path( str(root / "scripts" / "security" / "telegram-alert-readability-guard.py") ) diff --git a/scripts/security/wazuh-readonly-release-owner-request.py b/scripts/security/wazuh-readonly-release-owner-request.py new file mode 100644 index 000000000..4ebe2f83b --- /dev/null +++ b/scripts/security/wazuh-readonly-release-owner-request.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +""" +IwoooS Wazuh 只讀 API release owner request 草稿。 + +本工具只產生 / 驗證 repo 內 committed snapshot;不送 request、不讀 +credential、不推送、不部署、不查 Wazuh、不改 runtime。 +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Any + + +TAIPEI = timezone(timedelta(hours=8)) +SNAPSHOT_PATH = Path("docs/security/wazuh-readonly-release-owner-request.snapshot.json") + +REQUIRED_ACK_FLAGS = [ + "approve_formal_release_lane", + "confirm_no_plaintext_token_workaround", + "confirm_no_force_push", + "confirm_no_runtime_workaround", + "confirm_production_readback_after_deploy", + "confirm_wazuh_live_metadata_requires_separate_owner_gate", +] + +REQUIRED_EVIDENCE_FIELDS = [ + "release_lane_owner", + "release_method", + "target_branch_or_patch_set", + "post_deploy_readback_command", + "rollback_owner", + "blocked_runtime_actions_ack", +] + +ALLOWED_RELEASE_METHODS = [ + "formal_gitea_merge", + "formal_patch_apply", + "maintainer_local_push_with_safe_credential", +] + +FORBIDDEN_PAYLOADS = [ + "token", + "secret", + "private_key", + "cookie", + "session", + "authorization_header", + "runner_token", + "webhook_secret", + "wazuh_password", + "wazuh_raw_payload", + "git_credential", + "repo_archive", +] + +BLOCKED_ACTIONS = [ + "plain_text_gitea_token_in_remote_url", + "copy_token_from_dirty_workspace", + "force_push", + "nginx_or_gateway_workaround_for_404", + "docker_restart_for_wazuh_route", + "k8s_or_argocd_manual_apply_for_wazuh_route", + "firewall_change_for_wazuh_route", + "wazuh_secret_or_manager_change_for_api_404", + "enable_wazuh_live_metadata_without_owner_gate", + "enable_wazuh_active_response", + "host_write_or_kali_active_scan", +] + +HANDOFF_ENVELOPE_FIELDS = [ + "request_id", + "stage_id", + "recipient_role_or_team", + "sender_role_or_team", + "requested_response_window", + "allowed_release_methods", + "required_ack_flags", + "required_evidence_fields", + "target_branch_or_patch_set", + "post_deploy_readback_command", + "forbidden_payloads", + "blocked_runtime_actions", + "followup_owner", + "not_approval", +] + + +def now_iso() -> str: + return datetime.now(TAIPEI).replace(microsecond=0).isoformat() + + +def build_report(generated_at: str | None = None) -> dict[str, Any]: + return { + "schema_version": "iwooos_wazuh_readonly_release_owner_request_v1", + "generated_at": generated_at or now_iso(), + "status": "draft_not_dispatched_waiting_release_lane_owner", + "mode": "repo_request_draft_no_secret_no_runtime_no_push", + "summary": { + "request_draft_count": 1, + "required_ack_flag_count": len(REQUIRED_ACK_FLAGS), + "required_evidence_field_count": len(REQUIRED_EVIDENCE_FIELDS), + "allowed_release_method_count": len(ALLOWED_RELEASE_METHODS), + "forbidden_payload_count": len(FORBIDDEN_PAYLOADS), + "blocked_action_count": len(BLOCKED_ACTIONS), + "handoff_envelope_field_count": len(HANDOFF_ENVELOPE_FIELDS), + "request_sent_count": 0, + "recipient_confirmed_count": 0, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "formal_release_lane_ready_count": 0, + "gitea_push_authorized_count": 0, + "patch_apply_authorized_count": 0, + "production_deploy_authorized_count": 0, + "production_readback_passed_count": 0, + "runtime_gate_count": 0, + }, + "request_draft": { + "request_id": "iwooos_wazuh_readonly_release_owner_request", + "stage_id": "P0-IWOOOS-WAZUH-RELEASE", + "recipient_role_or_team": "pending_release_lane_owner", + "sender_role_or_team": "iwooos_security_reviewer", + "requested_response_window": "not_scheduled", + "allowed_release_methods": ALLOWED_RELEASE_METHODS, + "required_ack_flags": REQUIRED_ACK_FLAGS, + "required_evidence_fields": REQUIRED_EVIDENCE_FIELDS, + "target_branch": "codex/iwooos-wazuh-boundary-guard-20260624", + "target_branch_readback": "git log --oneline gitea/main..HEAD", + "target_patch_set_readback": "git format-patch gitea/main..HEAD after final docs commit; record sha256 outside committed docs", + "post_deploy_readback_command": "python3 scripts/security/wazuh-readonly-production-readback.py --json", + "redacted_evidence_refs": [ + "docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md", + "docs/security/wazuh-readonly-release-gate.snapshot.json", + "docs/security/wazuh-readonly-release-lane-preflight.snapshot.json", + ], + "forbidden_payloads": FORBIDDEN_PAYLOADS, + "blocked_runtime_actions": BLOCKED_ACTIONS, + "followup_owner": "pending_followup_owner", + "not_approval": True, + "request_sent": False, + "recipient_confirmed": False, + "owner_response_received": False, + "owner_response_accepted": False, + "runtime_gate": False, + "action_buttons_allowed": False, + }, + "execution_boundaries": { + "dispatch_authorized": False, + "request_sent": False, + "recipient_confirmed": False, + "repo_write_authorized": False, + "gitea_push_authorized": False, + "patch_apply_authorized": False, + "production_deploy_authorized": False, + "runtime_execution_authorized": False, + "secret_value_collection_allowed": False, + "plain_text_token_workaround_allowed": False, + "force_push_allowed": False, + "wazuh_api_live_query_authorized": False, + "wazuh_active_response_authorized": False, + "host_write_authorized": False, + "kali_active_scan_authorized": False, + "not_authorization": True, + }, + "handoff_envelope_fields": HANDOFF_ENVELOPE_FIELDS, + "send_after_conditions": [ + "先確認 gitea/main、Wazuh 分支與另一個 AwoooP Session 基線。", + "只送脫敏欄位與 refs;不得附 secret、raw Wazuh payload、git credential 或 runtime 操作要求。", + "一般批准繼續不是 release owner response。", + "收到 response 後仍需先通過 owner response acceptance ledger,不能直接 push 或 deploy。", + ], + } + + +def validate(root: Path) -> None: + snapshot_path = root / SNAPSHOT_PATH + if not snapshot_path.exists(): + raise SystemExit(f"BLOCKED Wazuh release owner request snapshot missing: {SNAPSHOT_PATH}") + snapshot = json.loads(snapshot_path.read_text(encoding="utf-8")) + expected = build_report(snapshot.get("generated_at")) + + for key in ("schema_version", "status", "mode"): + if snapshot.get(key) != expected[key]: + raise SystemExit(f"BLOCKED Wazuh release owner request {key} mismatch") + for key, expected_value in expected["summary"].items(): + actual = snapshot.get("summary", {}).get(key) + if actual != expected_value: + raise SystemExit( + f"BLOCKED Wazuh release owner request summary.{key}: " + f"expected {expected_value!r}, got {actual!r}" + ) + for key, value in snapshot.get("execution_boundaries", {}).items(): + if key == "not_authorization": + if value is not True: + raise SystemExit("BLOCKED Wazuh release owner request not_authorization must be true") + elif value is not False: + raise SystemExit(f"BLOCKED Wazuh release owner request execution_boundaries.{key}: expected false") + + +def main() -> int: + parser = argparse.ArgumentParser(description="IwoooS Wazuh 只讀 API release owner request 草稿") + parser.add_argument("--root", default=".", help="repository root") + parser.add_argument("--output", help="寫出 JSON 報告") + parser.add_argument("--generated-at", help="固定報告時間,供 committed snapshot 使用") + args = parser.parse_args() + + root = Path(args.root).resolve() + report = build_report(args.generated_at) + if args.output: + output = Path(args.output) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8") + validate(root) + summary = report["summary"] + print( + "WAZUH_READONLY_RELEASE_OWNER_REQUEST_OK " + f"drafts={summary['request_draft_count']} " + f"sent={summary['request_sent_count']} " + f"accepted={summary['owner_response_accepted_count']} " + f"runtime_gate={summary['runtime_gate_count']}" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/security/wazuh-readonly-release-owner-response-acceptance.py b/scripts/security/wazuh-readonly-release-owner-response-acceptance.py new file mode 100644 index 000000000..351081102 --- /dev/null +++ b/scripts/security/wazuh-readonly-release-owner-response-acceptance.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +""" +IwoooS Wazuh 只讀 API release owner response acceptance 帳本。 + +本工具定義未來 owner response 如何被收件、補件、隔離、拒收或進入 +reviewer validation;它不讀 credential、不推送、不部署、不查 Wazuh、 +不寫 runtime,也不把一般批准繼續當 release 授權。 +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Any + + +TAIPEI = timezone(timedelta(hours=8)) +SNAPSHOT_PATH = Path("docs/security/wazuh-readonly-release-owner-response-acceptance.snapshot.json") + +REQUIRED_ACK_FLAGS = [ + "approve_formal_release_lane", + "confirm_no_plaintext_token_workaround", + "confirm_no_force_push", + "confirm_no_runtime_workaround", + "confirm_production_readback_after_deploy", + "confirm_wazuh_live_metadata_requires_separate_owner_gate", +] + +REQUIRED_EVIDENCE_FIELDS = [ + "release_lane_owner", + "release_method", + "target_branch_or_patch_set", + "post_deploy_readback_command", + "rollback_owner", + "blocked_runtime_actions_ack", +] + +REVIEWER_CHECKS = [ + "owner_identity_present", + "release_method_allowed", + "target_scope_matches_wazuh_branch_or_patch_set", + "all_ack_flags_true", + "all_evidence_fields_present", + "redacted_refs_only", + "secret_value_absent", + "no_plaintext_token_workaround", + "no_force_push", + "no_runtime_workaround", + "post_deploy_readback_required", + "rollback_owner_present", + "live_metadata_gate_separate", + "active_response_stays_false", + "counts_transition_safe", +] + +OUTCOME_LANES = [ + "waiting_owner_response", + "quarantine_secret_or_raw_payload", + "reject_execution_request", + "request_supplement", + "ready_for_release_reviewer_validation", + "formal_gitea_merge_candidate", + "formal_patch_apply_candidate", + "safe_credential_push_candidate", + "waiting_production_readback", + "waiting_runtime_gate", +] + +BLOCKED_ACTIONS = [ + "plain_text_gitea_token_in_remote_url", + "copy_token_from_dirty_workspace", + "force_push", + "nginx_or_gateway_workaround_for_404", + "docker_restart_for_wazuh_route", + "k8s_or_argocd_manual_apply_for_wazuh_route", + "firewall_change_for_wazuh_route", + "wazuh_secret_or_manager_change_for_api_404", + "enable_wazuh_live_metadata_without_owner_gate", + "enable_wazuh_active_response", + "host_write_or_kali_active_scan", + "mark_general_approval_as_release_response", + "mark_predeploy_404_as_passed_readback", +] + + +def now_iso() -> str: + return datetime.now(TAIPEI).replace(microsecond=0).isoformat() + + +def build_report(generated_at: str | None = None) -> dict[str, Any]: + return { + "schema_version": "iwooos_wazuh_readonly_release_owner_response_acceptance_v1", + "generated_at": generated_at or now_iso(), + "status": "waiting_owner_response", + "mode": "metadata_only_acceptance_no_secret_no_runtime_no_push", + "summary": { + "acceptance_candidate_count": 1, + "required_ack_flag_count": len(REQUIRED_ACK_FLAGS), + "accepted_ack_flag_count": 0, + "required_evidence_field_count": len(REQUIRED_EVIDENCE_FIELDS), + "accepted_evidence_field_count": 0, + "reviewer_check_count": len(REVIEWER_CHECKS), + "outcome_lane_count": len(OUTCOME_LANES), + "blocked_action_count": len(BLOCKED_ACTIONS), + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "owner_response_rejected_count": 0, + "owner_response_quarantined_count": 0, + "supplement_requested_count": 0, + "formal_release_lane_ready_count": 0, + "gitea_push_authorized_count": 0, + "patch_apply_authorized_count": 0, + "production_deploy_authorized_count": 0, + "production_readback_passed_count": 0, + "runtime_gate_count": 0, + }, + "acceptance_candidate": { + "acceptance_candidate_id": "iwooos_wazuh_readonly_release_owner_response", + "request_id": "iwooos_wazuh_readonly_release_owner_request", + "status": "waiting_owner_response", + "owner_role_or_team": "pending_owner_response", + "decision": "pending_owner_response", + "decision_reason": "pending_owner_response", + "release_method": "pending_owner_response", + "target_branch_or_patch_set": "pending_owner_response", + "post_deploy_readback_command": "python3 scripts/security/wazuh-readonly-production-readback.py --json", + "rollback_owner": "pending_owner_response", + "redacted_evidence_refs": [], + "ack_flags": {flag: False for flag in REQUIRED_ACK_FLAGS}, + "required_ack_flags": REQUIRED_ACK_FLAGS, + "required_evidence_fields": REQUIRED_EVIDENCE_FIELDS, + "reviewer_checks": REVIEWER_CHECKS, + "outcome_lanes": OUTCOME_LANES, + "blocked_actions": BLOCKED_ACTIONS, + "not_approval": True, + "owner_response_received": False, + "owner_response_accepted": False, + "owner_response_rejected": False, + "owner_response_quarantined": False, + "supplement_requested": False, + "formal_release_lane_ready": False, + "gitea_push_authorized": False, + "patch_apply_authorized": False, + "production_deploy_authorized": False, + "production_readback_passed": False, + "runtime_gate": False, + }, + "execution_boundaries": { + "repo_write_authorized": False, + "gitea_push_authorized": False, + "patch_apply_authorized": False, + "production_deploy_authorized": False, + "runtime_execution_authorized": False, + "secret_value_collection_allowed": False, + "plain_text_token_workaround_allowed": False, + "force_push_allowed": False, + "wazuh_api_live_query_authorized": False, + "wazuh_active_response_authorized": False, + "host_write_authorized": False, + "kali_active_scan_authorized": False, + "not_authorization": True, + }, + "reviewer_instructions": [ + "只有具備完整欄位、脫敏 evidence refs、無 secret、無 runtime 要求的 owner response 才能進 reviewer validation。", + "一般批准繼續、截圖、口頭同意或未列 release method 的訊息都不能增加 accepted count。", + "即使 owner response accepted,也只代表可進正式 release lane;Wazuh live metadata 與 active response 仍是不同 gate。", + "production readback 必須在部署後不加 --allow-predeploy-404 執行,且不得回 404。", + ], + } + + +def validate(root: Path) -> None: + snapshot_path = root / SNAPSHOT_PATH + if not snapshot_path.exists(): + raise SystemExit( + f"BLOCKED Wazuh release owner response acceptance snapshot missing: {SNAPSHOT_PATH}" + ) + snapshot = json.loads(snapshot_path.read_text(encoding="utf-8")) + expected = build_report(snapshot.get("generated_at")) + + for key in ("schema_version", "status", "mode"): + if snapshot.get(key) != expected[key]: + raise SystemExit(f"BLOCKED Wazuh release owner response acceptance {key} mismatch") + for key, expected_value in expected["summary"].items(): + actual = snapshot.get("summary", {}).get(key) + if actual != expected_value: + raise SystemExit( + f"BLOCKED Wazuh release owner response acceptance summary.{key}: " + f"expected {expected_value!r}, got {actual!r}" + ) + for key, value in snapshot.get("execution_boundaries", {}).items(): + if key == "not_authorization": + if value is not True: + raise SystemExit( + "BLOCKED Wazuh release owner response acceptance not_authorization must be true" + ) + elif value is not False: + raise SystemExit( + f"BLOCKED Wazuh release owner response acceptance execution_boundaries.{key}: expected false" + ) + + +def main() -> int: + parser = argparse.ArgumentParser(description="IwoooS Wazuh 只讀 API release owner response acceptance") + parser.add_argument("--root", default=".", help="repository root") + parser.add_argument("--output", help="寫出 JSON 報告") + parser.add_argument("--generated-at", help="固定報告時間,供 committed snapshot 使用") + args = parser.parse_args() + + root = Path(args.root).resolve() + report = build_report(args.generated_at) + if args.output: + output = Path(args.output) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8") + validate(root) + summary = report["summary"] + print( + "WAZUH_READONLY_RELEASE_OWNER_RESPONSE_ACCEPTANCE_OK " + f"received={summary['owner_response_received_count']} " + f"accepted={summary['owner_response_accepted_count']} " + f"acks={summary['accepted_ack_flag_count']}/{summary['required_ack_flag_count']} " + f"evidence={summary['accepted_evidence_field_count']}/{summary['required_evidence_field_count']} " + f"runtime_gate={summary['runtime_gate_count']}" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 80b8758a3d8a3a2d35a9efb5504966ad579d0bc9 Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 24 Jun 2026 22:44:41 +0800 Subject: [PATCH 06/11] feat(iwooos): add Wazuh live metadata env gate --- docs/LOGBOOK.md | 6 + ...OOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md | 16 +- ...donly-live-metadata-env-gate.snapshot.json | 145 +++++++++++ .../security-mirror-progress-guard.py | 4 + .../wazuh-readonly-live-metadata-env-gate.py | 233 ++++++++++++++++++ 5 files changed, 399 insertions(+), 5 deletions(-) create mode 100644 docs/security/wazuh-readonly-live-metadata-env-gate.snapshot.json create mode 100644 scripts/security/wazuh-readonly-live-metadata-env-gate.py diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 6cd68ca19..8d11d6903 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -232,6 +232,12 @@ - 完成度:release owner request / acceptance artifact 與 guard `100%`;正式 owner response / release ready / push / deploy / production readback `0%`。 - 邊界:本段沒有發送 request、沒有收件、沒有讀 credential、沒有推送、沒有部署、沒有 Wazuh live query、沒有 runtime action;一般「批准繼續」仍不可當 release lane owner response。 +**Live metadata env gate 補充,22:42 Asia/Taipei**: +- 新增 `scripts/security/wazuh-readonly-live-metadata-env-gate.py` 與 `docs/security/wazuh-readonly-live-metadata-env-gate.snapshot.json`,並接入 `security-mirror-progress-guard.py`。 +- Gate 固定 server-side env keys `4`、required owner fields `15`、reviewer checks `15`、outcome lanes `10`、blocked actions `23`;目前 production route readback passed `0`、live metadata owner accepted `0`、secret source metadata accepted `0`、Wazuh manager health accepted `0`、live query authorized `0`、runtime gate `0`。 +- 完成度:live metadata env gate artifact / guard `100%`;server-side env owner response、secret source metadata、post-enable readback、live query authorization 仍 `0%`。 +- 邊界:本段沒有讀 secret、沒有查 Wazuh API、沒有修改 K8s / ArgoCD / Docker / Nginx / firewall、沒有部署、沒有 active response、沒有 host write;部署後 route 200 也不能直接代表可查 Wazuh live metadata。 + ## 2026-06-24|21:04 recovery readback 與 MOMO V10.651 雙機基準收斂 **背景**:前一輪 MOMO workspace readback 指到 `V10.646`,但 21:04 live health 已回 `V10.651`。因此本輪重新比對 Gitea `wooo/ewoooc` `main`、正式站 `/health`、Mac Mini / MacBook Pro Codex workspace 與 full-stack cold-start,避免「網站可用」和「版本 / 資料最新」互相混淆。 diff --git a/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md b/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md index 90a0d9aa1..48c1c0052 100644 --- a/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md +++ b/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md @@ -33,11 +33,13 @@ - `scripts/security/wazuh-readonly-release-lane-preflight.py` - `scripts/security/wazuh-readonly-release-owner-request.py` - `scripts/security/wazuh-readonly-release-owner-response-acceptance.py` +- `scripts/security/wazuh-readonly-live-metadata-env-gate.py` - `scripts/security/security-mirror-progress-guard.py` - `docs/security/wazuh-readonly-release-gate.snapshot.json` - `docs/security/wazuh-readonly-release-lane-preflight.snapshot.json` - `docs/security/wazuh-readonly-release-owner-request.snapshot.json` - `docs/security/wazuh-readonly-release-owner-response-acceptance.snapshot.json` +- `docs/security/wazuh-readonly-live-metadata-env-gate.snapshot.json` - `docs/LOGBOOK.md` 完成內容: @@ -54,6 +56,7 @@ - 新增 release gate snapshot 與 guard,固定 source-side 已完成、Gitea push / production deploy / production readback 尚未完成,避免後續把 predeploy 404 誤判成通過。 - 新增 release lane preflight snapshot 與 guard,固定正式 release 前必須選擇 `formal_gitea_merge`、`formal_patch_apply` 或 `maintainer_local_push_with_safe_credential` 其中一條合規 lane,且 owner ack / evidence 未到齊前不得 push、deploy、force push、使用明文 token workaround 或改 runtime。 - 新增 release owner request 草稿與 owner response acceptance 帳本,將 required ack flags、required evidence fields、allowed release methods、blocked actions、forbidden payloads 與 reviewer checks 機器可讀化;目前 request sent、response received / accepted、release ready、runtime gate 全部維持 `0`。 +- 新增 live metadata env gate,固定部署後要先通過 production route readback、server-side env owner response、secret source metadata、Wazuh manager health ref、readonly account scope、post-enable readback、rollback 與 no-secret / no-raw-payload attestation;目前 live query authorized 仍為 `0`。 ## 已完成驗證 @@ -66,9 +69,10 @@ python3 scripts/security/wazuh-readonly-release-gate.py --root . python3 scripts/security/wazuh-readonly-release-lane-preflight.py --root . python3 scripts/security/wazuh-readonly-release-owner-request.py --root . python3 scripts/security/wazuh-readonly-release-owner-response-acceptance.py --root . +python3 scripts/security/wazuh-readonly-live-metadata-env-gate.py --root . python3 scripts/security/security-mirror-progress-guard.py --root . -python3 scripts/ops/doc-secrets-sanity-check.py docs apps/api/src/api/v1/iwooos.py apps/web/src/app/api/iwooos/wazuh/route.ts scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/wazuh-readonly-release-lane-preflight.py scripts/security/wazuh-readonly-release-owner-request.py scripts/security/wazuh-readonly-release-owner-response-acceptance.py -python3 -m py_compile apps/api/src/api/v1/iwooos.py scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/wazuh-readonly-release-lane-preflight.py scripts/security/wazuh-readonly-release-owner-request.py scripts/security/wazuh-readonly-release-owner-response-acceptance.py scripts/security/security-mirror-progress-guard.py +python3 scripts/ops/doc-secrets-sanity-check.py docs apps/api/src/api/v1/iwooos.py apps/web/src/app/api/iwooos/wazuh/route.ts scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/wazuh-readonly-release-lane-preflight.py scripts/security/wazuh-readonly-release-owner-request.py scripts/security/wazuh-readonly-release-owner-response-acceptance.py scripts/security/wazuh-readonly-live-metadata-env-gate.py +python3 -m py_compile apps/api/src/api/v1/iwooos.py scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/wazuh-readonly-release-lane-preflight.py scripts/security/wazuh-readonly-release-owner-request.py scripts/security/wazuh-readonly-release-owner-response-acceptance.py scripts/security/wazuh-readonly-live-metadata-env-gate.py scripts/security/security-mirror-progress-guard.py git diff --check ``` @@ -80,8 +84,9 @@ git diff --check - `wazuh-readonly-release-lane-preflight`:`ready=0 acks=0/6 evidence=0/6 runtime_gate=0`。 - `wazuh-readonly-release-owner-request`:`drafts=1 sent=0 accepted=0 runtime_gate=0`。 - `wazuh-readonly-release-owner-response-acceptance`:`received=0 accepted=0 acks=0/6 evidence=0/6 runtime_gate=0`。 +- `wazuh-readonly-live-metadata-env-gate`:`route_readback=0 owner=0 secret_meta=0 live_query=0 runtime_gate=0`。 - `security-mirror-progress-guard`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 -- `doc-secrets-sanity-check`:`DOC_SECRET_SANITY_OK scanned_files=972`。 +- `doc-secrets-sanity-check`:`DOC_SECRET_SANITY_OK scanned_files=973`。 - `py_compile`:通過。 - `git diff --check`:通過。 @@ -103,7 +108,7 @@ git am /private/tmp/awoooi-iwooos-wazuh-boundary-release-patch-/*.pat - `python3 scripts/security/wazuh-readonly-release-gate.py --root .`:`WAZUH_READONLY_RELEASE_GATE_OK source=1 push=0 deploy=0 readback=0 runtime_gate=0`。 - `python3 scripts/security/wazuh-readonly-release-lane-preflight.py --root .`:`WAZUH_READONLY_RELEASE_LANE_PREFLIGHT_OK ready=0 acks=0/6 evidence=0/6 runtime_gate=0`。 - `python3 scripts/security/security-mirror-progress-guard.py --root .`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 -- `python3 scripts/ops/doc-secrets-sanity-check.py ...`:`DOC_SECRET_SANITY_OK scanned_files=972`。 +- `python3 scripts/ops/doc-secrets-sanity-check.py ...`:`DOC_SECRET_SANITY_OK scanned_files=973`。 - `python3 -m py_compile ...`:通過。 - `git diff --check`:通過。 @@ -179,6 +184,7 @@ python3 scripts/security/wazuh-readonly-production-readback.py --json | Wazuh release gate snapshot / guard | `100%` | 已完成;固定 push/deploy/readback 仍 blocked | | Wazuh release lane preflight | `100%` | 已完成;owner acks `0/6`、evidence `0/6`、正式 release ready `0` | | Wazuh release owner request / acceptance | `100%` | 已完成只讀草稿與收件帳本;request sent `0`、response accepted `0` | +| Wazuh live metadata env gate | `100%` | 已完成只讀 gate;route readback / owner / secret metadata / live query 仍 `0` | | 乾淨套用 proof | `100%` | patch set 可落在最新 `gitea/main` 並通過同組 guard;最終 hash 以 release 前 readback 為準 | | Gitea push | `0%` | 受控 workspace HTTPS credential 缺失 | | Production deploy / readback | `0%` | 等待 release lane | @@ -191,6 +197,6 @@ python3 scripts/security/wazuh-readonly-production-readback.py --json 1. 先依 `wazuh-readonly-release-owner-request.snapshot.json` 補 release lane owner response:選擇 formal merge、formal patch apply 或安全 credential push,並補 6 個 ack 與 6 個 evidence 欄位。 2. 解決受控 workspace Gitea HTTPS push 認證,或由正式 release lane 合併 `codex/iwooos-wazuh-boundary-guard-20260624` 分支 HEAD。 3. 部署後先驗證 `/api/iwooos/wazuh` 不再 404,且預設 disabled 邊界正確。 -4. 另開 owner gate 決定是否啟用 server-side Wazuh read-only metadata query。 +4. 另依 `wazuh-readonly-live-metadata-env-gate.snapshot.json` 補 server-side env owner response、secret source metadata、Wazuh manager health ref、readonly account scope 與 post-enable readback,才可考慮啟用 Wazuh read-only metadata query。 5. 收件 Wazuh manager health ref、agent status ref、event refs、host forensic refs 與 containment / recovery proof。 6. 仍禁止 active response、host write、firewall / Nginx / Docker / K8s runtime action、Kali active scan、secret 明文收集。 diff --git a/docs/security/wazuh-readonly-live-metadata-env-gate.snapshot.json b/docs/security/wazuh-readonly-live-metadata-env-gate.snapshot.json new file mode 100644 index 000000000..63838b524 --- /dev/null +++ b/docs/security/wazuh-readonly-live-metadata-env-gate.snapshot.json @@ -0,0 +1,145 @@ +{ + "blocked_actions": [ + "collect_wazuh_password", + "collect_wazuh_token", + "collect_wazuh_raw_payload", + "hardcode_wazuh_base_url", + "hardcode_wazuh_username", + "hardcode_wazuh_password", + "disable_tls_verification", + "enable_env_before_production_route_readback", + "enable_env_without_secret_owner", + "enable_wazuh_live_metadata_without_owner_gate", + "enable_wazuh_active_response", + "wazuh_manager_restart", + "wazuh_rule_change", + "wazuh_decoder_change", + "k8s_secret_manual_patch", + "argocd_manual_sync", + "docker_restart", + "nginx_or_gateway_workaround_for_404", + "firewall_change", + "host_write", + "kali_active_scan", + "production_deploy_without_release_lane", + "mark_predeploy_404_as_passed_readback" + ], + "execution_boundaries": { + "argocd_sync_authorized": false, + "docker_restart_authorized": false, + "firewall_change_authorized": false, + "host_write_authorized": false, + "k8s_secret_patch_authorized": false, + "kali_active_scan_authorized": false, + "nginx_gateway_workaround_authorized": false, + "not_authorization": true, + "production_deploy_authorized": false, + "raw_wazuh_payload_storage_allowed": false, + "repo_write_authorized": false, + "runtime_execution_authorized": false, + "secret_value_collection_allowed": false, + "wazuh_active_response_authorized": false, + "wazuh_api_live_query_authorized": false + }, + "generated_at": "2026-06-24T22:42:00+08:00", + "live_metadata_candidate": { + "candidate_id": "iwooos_wazuh_readonly_live_metadata_env", + "not_authorization": true, + "owner_response_accepted": false, + "owner_response_received": false, + "post_enable_readback_command": "python3 scripts/security/wazuh-readonly-production-readback.py --json", + "production_route_readback_ref": null, + "readonly_account_scope_ref": null, + "runtime_gate": false, + "secret_source_metadata_ref": null, + "server_side_env_keys": [ + "IWOOOS_WAZUH_READONLY_ENABLED", + "WAZUH_API_BASE_URL", + "WAZUH_API_USERNAME", + "WAZUH_API_PASSWORD" + ], + "status": "waiting_release_readback_and_live_metadata_owner_response", + "wazuh_active_response_authorized": false, + "wazuh_api_live_query_authorized": false, + "wazuh_manager_health_ref": null + }, + "mode": "repo_gate_no_secret_no_runtime_no_wazuh_query", + "operator_interpretation": [ + "此 gate 不代表 Wazuh live metadata 已啟用,只代表啟用前欄位與禁止動作已固定。", + "Production route 必須先不加 --allow-predeploy-404 readback 通過,才能考慮 server-side env enable。", + "secret handling 只能提供注入來源 metadata 與 owner,不得提交密碼、token、hash、partial secret 或 raw env。", + "Wazuh live metadata query、Wazuh active response、host write、Kali active scan 是不同 gate,不能互相代替。" + ], + "outcome_lanes": [ + "waiting_release_readback", + "waiting_live_metadata_owner_response", + "request_secret_source_metadata_supplement", + "request_wazuh_manager_health_supplement", + "request_readonly_account_scope_supplement", + "quarantine_secret_or_raw_payload", + "reject_runtime_workaround", + "ready_for_live_metadata_reviewer_validation", + "waiting_post_enable_readback", + "waiting_runtime_gate" + ], + "required_owner_fields": [ + "wazuh_live_metadata_owner", + "release_readback_ref", + "secret_injection_owner", + "secret_source_metadata_ref", + "wazuh_manager_health_ref", + "wazuh_api_tls_validation_ref", + "readonly_account_scope_ref", + "agent_alias_mapping_policy", + "post_enable_readback_command", + "rollback_owner", + "maintenance_window", + "validation_plan", + "no_secret_value_attestation", + "no_raw_payload_attestation", + "active_response_separate_gate_ack" + ], + "reviewer_checks": [ + "production_route_readback_passed_before_env_enable", + "server_side_env_keys_present_as_metadata_only", + "secret_value_absent", + "secret_source_metadata_ref_present", + "wazuh_api_base_url_https_only", + "readonly_account_scope_present", + "wazuh_manager_health_ref_present", + "agent_alias_mapping_policy_present", + "post_enable_readback_command_present", + "rollback_owner_present", + "maintenance_window_present", + "validation_plan_present", + "no_raw_wazuh_payload", + "active_response_gate_separate", + "runtime_gate_stays_zero_until_reviewer_acceptance" + ], + "schema_version": "iwooos_wazuh_readonly_live_metadata_env_gate_v1", + "server_side_env_keys": [ + "IWOOOS_WAZUH_READONLY_ENABLED", + "WAZUH_API_BASE_URL", + "WAZUH_API_USERNAME", + "WAZUH_API_PASSWORD" + ], + "status": "blocked_waiting_release_readback_and_live_metadata_owner_response", + "summary": { + "blocked_action_count": 23, + "host_write_authorized_count": 0, + "live_metadata_owner_response_accepted_count": 0, + "live_metadata_owner_response_received_count": 0, + "outcome_lane_count": 10, + "post_enable_readback_passed_count": 0, + "production_route_readback_passed_count": 0, + "readonly_account_scope_accepted_count": 0, + "required_owner_field_count": 15, + "reviewer_check_count": 15, + "runtime_gate_count": 0, + "secret_source_metadata_accepted_count": 0, + "server_side_env_key_count": 4, + "wazuh_active_response_authorized_count": 0, + "wazuh_api_live_query_authorized_count": 0, + "wazuh_manager_health_ref_accepted_count": 0 + } +} diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 9e8184d59..23a7d8d94 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -107,6 +107,10 @@ def validate(root: Path) -> None: str(root / "scripts" / "security" / "wazuh-readonly-release-owner-response-acceptance.py") ) wazuh_readonly_release_owner_response_acceptance["validate"](root) + wazuh_readonly_live_metadata_env_gate = runpy.run_path( + str(root / "scripts" / "security" / "wazuh-readonly-live-metadata-env-gate.py") + ) + wazuh_readonly_live_metadata_env_gate["validate"](root) telegram_alert_readability_guard = runpy.run_path( str(root / "scripts" / "security" / "telegram-alert-readability-guard.py") ) diff --git a/scripts/security/wazuh-readonly-live-metadata-env-gate.py b/scripts/security/wazuh-readonly-live-metadata-env-gate.py new file mode 100644 index 000000000..1255c6185 --- /dev/null +++ b/scripts/security/wazuh-readonly-live-metadata-env-gate.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +""" +IwoooS Wazuh 只讀 live metadata env gate。 + +本工具只固定 server-side env / secret 注入 / production readback 的 +owner gate。它不讀 secret value、不查 Wazuh API、不改 K8s / ArgoCD / +Docker / Nginx / firewall、不部署、不推送,也不啟用 active response。 +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Any + + +TAIPEI = timezone(timedelta(hours=8)) +SNAPSHOT_PATH = Path("docs/security/wazuh-readonly-live-metadata-env-gate.snapshot.json") + +SERVER_SIDE_ENV_KEYS = [ + "IWOOOS_WAZUH_READONLY_ENABLED", + "WAZUH_API_BASE_URL", + "WAZUH_API_USERNAME", + "WAZUH_API_PASSWORD", +] + +REQUIRED_OWNER_FIELDS = [ + "wazuh_live_metadata_owner", + "release_readback_ref", + "secret_injection_owner", + "secret_source_metadata_ref", + "wazuh_manager_health_ref", + "wazuh_api_tls_validation_ref", + "readonly_account_scope_ref", + "agent_alias_mapping_policy", + "post_enable_readback_command", + "rollback_owner", + "maintenance_window", + "validation_plan", + "no_secret_value_attestation", + "no_raw_payload_attestation", + "active_response_separate_gate_ack", +] + +REVIEWER_CHECKS = [ + "production_route_readback_passed_before_env_enable", + "server_side_env_keys_present_as_metadata_only", + "secret_value_absent", + "secret_source_metadata_ref_present", + "wazuh_api_base_url_https_only", + "readonly_account_scope_present", + "wazuh_manager_health_ref_present", + "agent_alias_mapping_policy_present", + "post_enable_readback_command_present", + "rollback_owner_present", + "maintenance_window_present", + "validation_plan_present", + "no_raw_wazuh_payload", + "active_response_gate_separate", + "runtime_gate_stays_zero_until_reviewer_acceptance", +] + +OUTCOME_LANES = [ + "waiting_release_readback", + "waiting_live_metadata_owner_response", + "request_secret_source_metadata_supplement", + "request_wazuh_manager_health_supplement", + "request_readonly_account_scope_supplement", + "quarantine_secret_or_raw_payload", + "reject_runtime_workaround", + "ready_for_live_metadata_reviewer_validation", + "waiting_post_enable_readback", + "waiting_runtime_gate", +] + +BLOCKED_ACTIONS = [ + "collect_wazuh_password", + "collect_wazuh_token", + "collect_wazuh_raw_payload", + "hardcode_wazuh_base_url", + "hardcode_wazuh_username", + "hardcode_wazuh_password", + "disable_tls_verification", + "enable_env_before_production_route_readback", + "enable_env_without_secret_owner", + "enable_wazuh_live_metadata_without_owner_gate", + "enable_wazuh_active_response", + "wazuh_manager_restart", + "wazuh_rule_change", + "wazuh_decoder_change", + "k8s_secret_manual_patch", + "argocd_manual_sync", + "docker_restart", + "nginx_or_gateway_workaround_for_404", + "firewall_change", + "host_write", + "kali_active_scan", + "production_deploy_without_release_lane", + "mark_predeploy_404_as_passed_readback", +] + + +def now_iso() -> str: + return datetime.now(TAIPEI).replace(microsecond=0).isoformat() + + +def build_report(generated_at: str | None = None) -> dict[str, Any]: + return { + "schema_version": "iwooos_wazuh_readonly_live_metadata_env_gate_v1", + "generated_at": generated_at or now_iso(), + "status": "blocked_waiting_release_readback_and_live_metadata_owner_response", + "mode": "repo_gate_no_secret_no_runtime_no_wazuh_query", + "summary": { + "server_side_env_key_count": len(SERVER_SIDE_ENV_KEYS), + "required_owner_field_count": len(REQUIRED_OWNER_FIELDS), + "reviewer_check_count": len(REVIEWER_CHECKS), + "outcome_lane_count": len(OUTCOME_LANES), + "blocked_action_count": len(BLOCKED_ACTIONS), + "production_route_readback_passed_count": 0, + "live_metadata_owner_response_received_count": 0, + "live_metadata_owner_response_accepted_count": 0, + "secret_source_metadata_accepted_count": 0, + "wazuh_manager_health_ref_accepted_count": 0, + "readonly_account_scope_accepted_count": 0, + "post_enable_readback_passed_count": 0, + "wazuh_api_live_query_authorized_count": 0, + "wazuh_active_response_authorized_count": 0, + "host_write_authorized_count": 0, + "runtime_gate_count": 0, + }, + "server_side_env_keys": SERVER_SIDE_ENV_KEYS, + "required_owner_fields": REQUIRED_OWNER_FIELDS, + "reviewer_checks": REVIEWER_CHECKS, + "outcome_lanes": OUTCOME_LANES, + "blocked_actions": BLOCKED_ACTIONS, + "live_metadata_candidate": { + "candidate_id": "iwooos_wazuh_readonly_live_metadata_env", + "status": "waiting_release_readback_and_live_metadata_owner_response", + "production_route_readback_ref": None, + "server_side_env_keys": SERVER_SIDE_ENV_KEYS, + "secret_source_metadata_ref": None, + "wazuh_manager_health_ref": None, + "readonly_account_scope_ref": None, + "post_enable_readback_command": "python3 scripts/security/wazuh-readonly-production-readback.py --json", + "owner_response_received": False, + "owner_response_accepted": False, + "wazuh_api_live_query_authorized": False, + "wazuh_active_response_authorized": False, + "runtime_gate": False, + "not_authorization": True, + }, + "execution_boundaries": { + "repo_write_authorized": False, + "production_deploy_authorized": False, + "runtime_execution_authorized": False, + "secret_value_collection_allowed": False, + "wazuh_api_live_query_authorized": False, + "wazuh_active_response_authorized": False, + "raw_wazuh_payload_storage_allowed": False, + "host_write_authorized": False, + "kali_active_scan_authorized": False, + "k8s_secret_patch_authorized": False, + "argocd_sync_authorized": False, + "docker_restart_authorized": False, + "nginx_gateway_workaround_authorized": False, + "firewall_change_authorized": False, + "not_authorization": True, + }, + "operator_interpretation": [ + "此 gate 不代表 Wazuh live metadata 已啟用,只代表啟用前欄位與禁止動作已固定。", + "Production route 必須先不加 --allow-predeploy-404 readback 通過,才能考慮 server-side env enable。", + "secret handling 只能提供注入來源 metadata 與 owner,不得提交密碼、token、hash、partial secret 或 raw env。", + "Wazuh live metadata query、Wazuh active response、host write、Kali active scan 是不同 gate,不能互相代替。", + ], + } + + +def validate(root: Path) -> None: + snapshot_path = root / SNAPSHOT_PATH + if not snapshot_path.exists(): + raise SystemExit(f"BLOCKED Wazuh live metadata env gate snapshot missing: {SNAPSHOT_PATH}") + snapshot = json.loads(snapshot_path.read_text(encoding="utf-8")) + expected = build_report(snapshot.get("generated_at")) + + for key in ("schema_version", "status", "mode"): + if snapshot.get(key) != expected[key]: + raise SystemExit(f"BLOCKED Wazuh live metadata env gate {key} mismatch") + for key, expected_value in expected["summary"].items(): + actual = snapshot.get("summary", {}).get(key) + if actual != expected_value: + raise SystemExit( + f"BLOCKED Wazuh live metadata env gate summary.{key}: " + f"expected {expected_value!r}, got {actual!r}" + ) + for key, value in snapshot.get("execution_boundaries", {}).items(): + if key == "not_authorization": + if value is not True: + raise SystemExit("BLOCKED Wazuh live metadata env gate not_authorization must be true") + elif value is not False: + raise SystemExit(f"BLOCKED Wazuh live metadata env gate execution_boundaries.{key}: expected false") + + +def main() -> int: + parser = argparse.ArgumentParser(description="IwoooS Wazuh 只讀 live metadata env gate") + parser.add_argument("--root", default=".", help="repository root") + parser.add_argument("--output", help="寫出 JSON 報告") + parser.add_argument("--generated-at", help="固定報告時間,供 committed snapshot 使用") + args = parser.parse_args() + + root = Path(args.root).resolve() + report = build_report(args.generated_at) + if args.output: + output = Path(args.output) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8") + validate(root) + summary = report["summary"] + print( + "WAZUH_READONLY_LIVE_METADATA_ENV_GATE_OK " + f"route_readback={summary['production_route_readback_passed_count']} " + f"owner={summary['live_metadata_owner_response_accepted_count']} " + f"secret_meta={summary['secret_source_metadata_accepted_count']} " + f"live_query={summary['wazuh_api_live_query_authorized_count']} " + f"runtime_gate={summary['runtime_gate_count']}" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 5a5cb50f650eaf8f9842647b8b097dcaa5af2763 Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 24 Jun 2026 22:46:48 +0800 Subject: [PATCH 07/11] docs(iwooos): refresh Wazuh gates after rebase --- docs/LOGBOOK.md | 7 +++++++ .../wazuh-readonly-live-metadata-env-gate.snapshot.json | 2 +- docs/security/wazuh-readonly-release-gate.snapshot.json | 2 +- .../wazuh-readonly-release-lane-preflight.snapshot.json | 2 +- .../wazuh-readonly-release-owner-request.snapshot.json | 2 +- ...eadonly-release-owner-response-acceptance.snapshot.json | 2 +- 6 files changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 8d11d6903..9376abb27 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -238,6 +238,13 @@ - 完成度:live metadata env gate artifact / guard `100%`;server-side env owner response、secret source metadata、post-enable readback、live query authorization 仍 `0%`。 - 邊界:本段沒有讀 secret、沒有查 Wazuh API、沒有修改 K8s / ArgoCD / Docker / Nginx / firewall、沒有部署、沒有 active response、沒有 host write;部署後 route 200 也不能直接代表可查 Wazuh live metadata。 +**Release lane rebase/readback 補充,22:48 Asia/Taipei**: +- `gitea/main` 已再前進到 `b540fc0c docs(ops): record momo source absence readback [skip ci]`;Wazuh 分支已 rebase 到此基底,沒有覆蓋 MOMO source absence / recovery readback 紀錄。 +- Rebase 後 Wazuh 分支目前只比 `gitea/main` 多六個提交:`38dc3c2f fix(iwooos): 接上 Wazuh 只讀 API 邊界`、`9a53d3e1 docs(iwooos): 記錄 Wazuh release apply proof`、`e9972d47 feat(iwooos): 新增 Wazuh release lane preflight`、`758d419e docs(iwooos): refresh Wazuh release lane readback`、`04db4b8a feat(iwooos): define Wazuh release owner gate`、`8eec298e feat(iwooos): add Wazuh live metadata env gate`。 +- 已重新產生 Wazuh release gate、release lane preflight、owner request、owner response acceptance 與 live metadata env gate snapshots;全部仍固定 push、deploy、production readback、runtime gate、live query、active response、host write 為 `0`。 +- 完成度:rebase / snapshot refresh `100%`;formal release lane owner acks `0/6`、evidence `0/6`;live metadata owner accepted `0`;Gitea push / production deploy / production readback `0%`。 +- 邊界:本段沒有讀 git credential、沒有推送、沒有部署、沒有 Wazuh live query、沒有 secret collection、沒有 Nginx / Docker / K8s / firewall / host / Wazuh secret 變更。 + ## 2026-06-24|21:04 recovery readback 與 MOMO V10.651 雙機基準收斂 **背景**:前一輪 MOMO workspace readback 指到 `V10.646`,但 21:04 live health 已回 `V10.651`。因此本輪重新比對 Gitea `wooo/ewoooc` `main`、正式站 `/health`、Mac Mini / MacBook Pro Codex workspace 與 full-stack cold-start,避免「網站可用」和「版本 / 資料最新」互相混淆。 diff --git a/docs/security/wazuh-readonly-live-metadata-env-gate.snapshot.json b/docs/security/wazuh-readonly-live-metadata-env-gate.snapshot.json index 63838b524..e9249283f 100644 --- a/docs/security/wazuh-readonly-live-metadata-env-gate.snapshot.json +++ b/docs/security/wazuh-readonly-live-metadata-env-gate.snapshot.json @@ -41,7 +41,7 @@ "wazuh_active_response_authorized": false, "wazuh_api_live_query_authorized": false }, - "generated_at": "2026-06-24T22:42:00+08:00", + "generated_at": "2026-06-24T22:48:00+08:00", "live_metadata_candidate": { "candidate_id": "iwooos_wazuh_readonly_live_metadata_env", "not_authorization": true, diff --git a/docs/security/wazuh-readonly-release-gate.snapshot.json b/docs/security/wazuh-readonly-release-gate.snapshot.json index 216a2d20e..ec58fd402 100644 --- a/docs/security/wazuh-readonly-release-gate.snapshot.json +++ b/docs/security/wazuh-readonly-release-gate.snapshot.json @@ -14,7 +14,7 @@ "wazuh_active_response_authorized": false, "wazuh_api_live_query_authorized": false }, - "generated_at": "2026-06-24T22:26:00+08:00", + "generated_at": "2026-06-24T22:48:00+08:00", "missing_required_source_paths": [], "mode": "repo_release_gate_no_runtime_no_secret_collection", "operator_interpretation": [ diff --git a/docs/security/wazuh-readonly-release-lane-preflight.snapshot.json b/docs/security/wazuh-readonly-release-lane-preflight.snapshot.json index 1210da7d4..e269db920 100644 --- a/docs/security/wazuh-readonly-release-lane-preflight.snapshot.json +++ b/docs/security/wazuh-readonly-release-lane-preflight.snapshot.json @@ -30,7 +30,7 @@ "wazuh_active_response_authorized": false, "wazuh_api_live_query_authorized": false }, - "generated_at": "2026-06-24T22:26:00+08:00", + "generated_at": "2026-06-24T22:48:00+08:00", "mode": "repo_preflight_no_secret_no_runtime_no_push", "operator_interpretation": [ "此 preflight 通過前,不得把 Gitea credential blocker 視為可繞過。", diff --git a/docs/security/wazuh-readonly-release-owner-request.snapshot.json b/docs/security/wazuh-readonly-release-owner-request.snapshot.json index 303bb9e85..9598b3df2 100644 --- a/docs/security/wazuh-readonly-release-owner-request.snapshot.json +++ b/docs/security/wazuh-readonly-release-owner-request.snapshot.json @@ -17,7 +17,7 @@ "wazuh_active_response_authorized": false, "wazuh_api_live_query_authorized": false }, - "generated_at": "2026-06-24T22:32:00+08:00", + "generated_at": "2026-06-24T22:48:00+08:00", "handoff_envelope_fields": [ "request_id", "stage_id", diff --git a/docs/security/wazuh-readonly-release-owner-response-acceptance.snapshot.json b/docs/security/wazuh-readonly-release-owner-response-acceptance.snapshot.json index d1b07d0de..eb09819ec 100644 --- a/docs/security/wazuh-readonly-release-owner-response-acceptance.snapshot.json +++ b/docs/security/wazuh-readonly-release-owner-response-acceptance.snapshot.json @@ -107,7 +107,7 @@ "wazuh_active_response_authorized": false, "wazuh_api_live_query_authorized": false }, - "generated_at": "2026-06-24T22:32:00+08:00", + "generated_at": "2026-06-24T22:48:00+08:00", "mode": "metadata_only_acceptance_no_secret_no_runtime_no_push", "reviewer_instructions": [ "只有具備完整欄位、脫敏 evidence refs、無 secret、無 runtime 要求的 owner response 才能進 reviewer validation。", From 64eef5a252b63aad78f4fc2f2c0dcbd6478300fa Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 24 Jun 2026 23:07:29 +0800 Subject: [PATCH 08/11] =?UTF-8?q?feat(iwooos):=20=E9=A1=AF=E7=A4=BA=20Wazu?= =?UTF-8?q?h=20=E5=8D=B3=E6=99=82=E4=B8=AD=E7=B9=BC=E8=B3=87=E6=96=99?= =?UTF-8?q?=E9=96=98=E9=96=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/messages/en.json | 53 +++++ apps/web/messages/zh-TW.json | 53 +++++ apps/web/src/app/[locale]/iwooos/page.tsx | 184 ++++++++++++++++++ docs/LOGBOOK.md | 35 ++++ ...OOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md | 15 ++ 5 files changed, 340 insertions(+) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 819ca65c4..3b453ab69 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -19003,6 +19003,59 @@ } } }, + "wazuhLiveMetadataEnvGate": { + "eyebrow": "Wazuh 即時中繼資料環境閘門", + "title": "Wazuh 查詢要等正式路由、負責人與機密中繼資料都過關", + "subtitle": "這張卡把 Wazuh 即時中繼資料啟用前的條件拆開:正式路由讀回、伺服端環境變數負責人、機密來源中繼資料、管理節點健康、唯讀帳號範圍與啟用後讀回都要先補齊;目前即時查詢、主動回應、主機寫入與執行期閘門都是 0。", + "checkLabel": "檢核", + "stateLabel": "狀態", + "boundaryTitle": "即時中繼資料環境邊界", + "boundaryIntro": "以下鍵值固定:正式路由 200、Wazuh 已建置或 UI 可見都不能直接代表可以查 Wazuh 即時中繼資料;不得收機密明文值、不得改 K8s 機密、不得用 Nginx 或主機操作繞過釋出閘門。", + "summary": { + "routeReadback": { + "label": "路由讀回", + "detail": "正式部署後讀回尚未通過,因此仍不能啟用即時中繼資料。" + }, + "owner": { + "label": "負責人回覆", + "detail": "伺服端環境變數與啟用責任尚未被審查者接受。" + }, + "secretMeta": { + "label": "機密中繼資料", + "detail": "只允許機密來源中繼資料與負責人,不收密碼、權杖或雜湊值。" + }, + "liveQuery": { + "label": "即時查詢", + "detail": "Wazuh API 即時查詢授權目前維持 0。" + } + }, + "items": { + "releaseReadback": { + "title": "先等正式路由不再 404", + "body": "部署後必須用正式讀回指令驗證 `/api/iwooos/wazuh`,不能用部署前 404 或閘道繞路當成功。" + }, + "serverEnv": { + "title": "伺服端環境變數需負責人", + "body": "IWOOOS_WAZUH_READONLY_ENABLED 與 Wazuh API 環境變數只能由正式伺服端流程注入,不能寫到前端 bundle。" + }, + "secretMetadata": { + "title": "機密只收中繼資料", + "body": "只記錄注入來源、負責人、維護窗口與回滾路徑;不得貼密碼、權杖、Cookie、工作階段或任何機密片段。" + }, + "managerHealth": { + "title": "Wazuh 管理節點健康待參照", + "body": "需要管理節點、API 與 TLS 健康狀態的脫敏參照;不能只用 Wazuh 已安裝或儀表板可見判斷。" + }, + "readonlyScope": { + "title": "唯讀帳號範圍待驗", + "body": "只允許中繼資料查詢範圍;主動回應、規則變更、解碼器變更與主機操作必須留在另一個閘門。" + }, + "postEnable": { + "title": "啟用後還要讀回", + "body": "即使未來環境變數啟用,也必須再驗證不回傳原始載荷、代理端原名、內網 IP 或機密。" + } + } + }, "socSiemKaliWazuhIntegration": { "eyebrow": "SOC / SIEM / Kali 112 整合控制", "title": "把 Wazuh、Kali、告警鏈與主流資安機制接成同一條證據線", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 819ca65c4..3b453ab69 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -19003,6 +19003,59 @@ } } }, + "wazuhLiveMetadataEnvGate": { + "eyebrow": "Wazuh 即時中繼資料環境閘門", + "title": "Wazuh 查詢要等正式路由、負責人與機密中繼資料都過關", + "subtitle": "這張卡把 Wazuh 即時中繼資料啟用前的條件拆開:正式路由讀回、伺服端環境變數負責人、機密來源中繼資料、管理節點健康、唯讀帳號範圍與啟用後讀回都要先補齊;目前即時查詢、主動回應、主機寫入與執行期閘門都是 0。", + "checkLabel": "檢核", + "stateLabel": "狀態", + "boundaryTitle": "即時中繼資料環境邊界", + "boundaryIntro": "以下鍵值固定:正式路由 200、Wazuh 已建置或 UI 可見都不能直接代表可以查 Wazuh 即時中繼資料;不得收機密明文值、不得改 K8s 機密、不得用 Nginx 或主機操作繞過釋出閘門。", + "summary": { + "routeReadback": { + "label": "路由讀回", + "detail": "正式部署後讀回尚未通過,因此仍不能啟用即時中繼資料。" + }, + "owner": { + "label": "負責人回覆", + "detail": "伺服端環境變數與啟用責任尚未被審查者接受。" + }, + "secretMeta": { + "label": "機密中繼資料", + "detail": "只允許機密來源中繼資料與負責人,不收密碼、權杖或雜湊值。" + }, + "liveQuery": { + "label": "即時查詢", + "detail": "Wazuh API 即時查詢授權目前維持 0。" + } + }, + "items": { + "releaseReadback": { + "title": "先等正式路由不再 404", + "body": "部署後必須用正式讀回指令驗證 `/api/iwooos/wazuh`,不能用部署前 404 或閘道繞路當成功。" + }, + "serverEnv": { + "title": "伺服端環境變數需負責人", + "body": "IWOOOS_WAZUH_READONLY_ENABLED 與 Wazuh API 環境變數只能由正式伺服端流程注入,不能寫到前端 bundle。" + }, + "secretMetadata": { + "title": "機密只收中繼資料", + "body": "只記錄注入來源、負責人、維護窗口與回滾路徑;不得貼密碼、權杖、Cookie、工作階段或任何機密片段。" + }, + "managerHealth": { + "title": "Wazuh 管理節點健康待參照", + "body": "需要管理節點、API 與 TLS 健康狀態的脫敏參照;不能只用 Wazuh 已安裝或儀表板可見判斷。" + }, + "readonlyScope": { + "title": "唯讀帳號範圍待驗", + "body": "只允許中繼資料查詢範圍;主動回應、規則變更、解碼器變更與主機操作必須留在另一個閘門。" + }, + "postEnable": { + "title": "啟用後還要讀回", + "body": "即使未來環境變數啟用,也必須再驗證不回傳原始載荷、代理端原名、內網 IP 或機密。" + } + } + }, "socSiemKaliWazuhIntegration": { "eyebrow": "SOC / SIEM / Kali 112 整合控制", "title": "把 Wazuh、Kali、告警鏈與主流資安機制接成同一條證據線", diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index 006dadaea..a07ac58f0 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -271,6 +271,14 @@ type WazuhIntrusionReadbackItem = { tone: 'steady' | 'warn' | 'locked' } +type WazuhLiveMetadataEnvGateItem = { + key: string + check: string + state: string + icon: typeof ShieldCheck + tone: 'steady' | 'warn' | 'locked' +} + type SocSiemKaliWazuhIntegrationItem = { key: string check: string @@ -2182,6 +2190,50 @@ const wazuhIntrusionReadbackBoundaries = [ 'not_authorization=true', ] as const +const wazuhLiveMetadataEnvGateSummary = [ + { key: 'routeReadback', value: '0', icon: Route, tone: 'locked' }, + { key: 'owner', value: '0', icon: ClipboardCheck, tone: 'locked' }, + { key: 'secretMeta', value: '0', icon: Lock, tone: 'locked' }, + { key: 'liveQuery', value: '0', icon: Radar, tone: 'locked' }, +] as const + +const wazuhLiveMetadataEnvGateItems: WazuhLiveMetadataEnvGateItem[] = [ + { key: 'releaseReadback', check: 'ENV-1', state: '待部署驗證', icon: Route, tone: 'locked' }, + { key: 'serverEnv', check: 'ENV-2', state: '待負責人', icon: Server, tone: 'warn' }, + { key: 'secretMetadata', check: 'ENV-3', state: '只收中繼資料', icon: Lock, tone: 'locked' }, + { key: 'managerHealth', check: 'ENV-4', state: '待健康參照', icon: Activity, tone: 'warn' }, + { key: 'readonlyScope', check: 'ENV-5', state: '待範圍參照', icon: ShieldCheck, tone: 'warn' }, + { key: 'postEnable', check: 'ENV-6', state: '待讀回驗證', icon: SearchCheck, tone: 'locked' }, +] as const + +const wazuhLiveMetadataEnvGateBoundaries = [ + 'wazuh_live_metadata_env_gate_visible=true', + 'wazuh_live_metadata_env_gate_server_side_env_key_count=4', + 'wazuh_live_metadata_env_gate_required_owner_field_count=15', + 'wazuh_live_metadata_env_gate_reviewer_check_count=15', + 'wazuh_live_metadata_env_gate_outcome_lane_count=10', + 'wazuh_live_metadata_env_gate_blocked_action_count=23', + 'wazuh_live_metadata_env_gate_production_route_readback_passed_count=0', + 'wazuh_live_metadata_env_gate_live_metadata_owner_response_accepted_count=0', + 'wazuh_live_metadata_env_gate_secret_source_metadata_accepted_count=0', + 'wazuh_live_metadata_env_gate_wazuh_manager_health_ref_accepted_count=0', + 'wazuh_live_metadata_env_gate_readonly_account_scope_accepted_count=0', + 'wazuh_live_metadata_env_gate_post_enable_readback_passed_count=0', + 'wazuh_live_metadata_env_gate_wazuh_api_live_query_authorized_count=0', + 'wazuh_live_metadata_env_gate_wazuh_active_response_authorized_count=0', + 'wazuh_live_metadata_env_gate_runtime_gate_count=0', + 'secret_value_collection_allowed=false', + 'raw_wazuh_payload_storage_allowed=false', + 'k8s_secret_patch_authorized=false', + 'argocd_sync_authorized=false', + 'docker_restart_authorized=false', + 'nginx_gateway_workaround_authorized=false', + 'firewall_change_authorized=false', + 'host_write_authorized=false', + 'kali_active_scan_authorized=false', + 'not_authorization=true', +] as const + const socSiemKaliWazuhIntegrationSummary = [ { key: 'frameworks', value: '7', icon: ClipboardCheck, tone: 'steady' }, { key: 'domains', value: '16', icon: Network, tone: 'steady' }, @@ -7669,6 +7721,137 @@ function IwoooSWazuhIntrusionReadbackBoard() { ) } +function IwoooSWazuhLiveMetadataEnvGateBoard() { + const t = useTranslations('iwooos.wazuhLiveMetadataEnvGate') + const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const } + + return ( +
+
+
+
+
+ + {t('eyebrow')} +
+

{t('title')}

+

+ {t('subtitle')} +

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

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

+
+ ) + })} +
+
+ +
+ {wazuhLiveMetadataEnvGateItems.map(item => { + const Icon = item.icon + return ( +
+
+ + {t('checkLabel')} {item.check} + + +
+
+
+ {t(`items.${item.key}.title` as never)} +
+
+ {t('stateLabel')}:{item.state} +
+
+

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

+
+ ) + })} +
+ +
+ + {t('boundaryTitle')} + +

+ {t('boundaryIntro')} +

+
+ {wazuhLiveMetadataEnvGateBoundaries.map(item => ( + + {item} + + ))} +
+
+
+
+ ) +} + function IwoooSSocSiemKaliWazuhIntegrationBoard() { const t = useTranslations('iwooos.socSiemKaliWazuhIntegration') const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const } @@ -20592,6 +20775,7 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) { + diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 9376abb27..d9e3566e1 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -35,6 +35,41 @@ **邊界**:本輪沒有手動 import、沒有 Drive write、沒有 DB restore / truncate、沒有 Docker / Nginx / firewall / K8s / ArgoCD runtime 寫入,沒有 Wazuh / SOC 修改,沒有使用聊天中的密碼,也沒有讀取或保存 secret。 +## 2026-06-24|IwoooS Wazuh 即時中繼資料環境閘門前台驗證 + +**背景**:Wazuh 只讀 API source-side 修補、release gate、release lane preflight、owner request / acceptance 與 live metadata env gate 已完成,但 production 尚未部署,`/api/iwooos/wazuh` 仍不可用來代表 Wazuh runtime 狀態。本輪補上 IwoooS 前台可視化卡片,讓使用者在 `/zh-TW/iwooos` 直接看到「正式路由讀回、伺服端環境變數負責人、機密中繼資料、管理節點健康、唯讀帳號範圍、啟用後讀回」全部仍為 `0`,避免把 Wazuh 已建置或 UI 可見誤判為可查 live metadata。 + +**Source-side 完成**: +- `apps/web/src/app/[locale]/iwooos/page.tsx` 新增 `IwoooSWazuhLiveMetadataEnvGateBoard`,位置在 Wazuh 入侵 readback 與 SOC / SIEM / Kali 整合卡之間。 +- `apps/web/messages/zh-TW.json` 與 `apps/web/messages/en.json` 新增同一份繁中鏡像文案;前台文案改為「負責人回覆、路由讀回、機密中繼資料、即時查詢」等產品治理語,不放工作視窗逐字稿、委派 XML、聊天內容或個人英文名稱。 +- 卡片邊界固定:`wazuh_live_metadata_env_gate_visible=true`、`production_route_readback_passed_count=0`、`live_metadata_owner_response_accepted_count=0`、`secret_source_metadata_accepted_count=0`、`wazuh_api_live_query_authorized_count=0`、`wazuh_active_response_authorized_count=0`、`runtime_gate_count=0`、`secret_value_collection_allowed=false`、`host_write_authorized=false`、`kali_active_scan_authorized=false`。 + +**驗證**: +- `node -e "JSON.parse(...zh-TW...); JSON.parse(...en...)"`:通過。 +- `cmp -s apps/web/messages/zh-TW.json apps/web/messages/en.json`:通過,維持繁中鏡像。 +- `pytest apps/api/tests/test_iwooos_wazuh_api.py`:`4 passed`。 +- `python3 scripts/security/security-mirror-progress-guard.py --root .`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 +- `python3 scripts/security/wazuh-readonly-route-boundary-guard.py --root .`:`route=2 public_ui_files=1 forbidden=0 runtime_gate=0`。 +- `python3 scripts/security/wazuh-readonly-release-gate.py --root .`:`source=1 push=0 deploy=0 readback=0 runtime_gate=0`。 +- `python3 scripts/security/wazuh-readonly-release-lane-preflight.py --root .`:`ready=0 acks=0/6 evidence=0/6 runtime_gate=0`。 +- `python3 scripts/security/wazuh-readonly-release-owner-request.py --root .`:`drafts=1 sent=0 accepted=0 runtime_gate=0`。 +- `python3 scripts/security/wazuh-readonly-release-owner-response-acceptance.py --root .`:`received=0 accepted=0 acks=0/6 evidence=0/6 runtime_gate=0`。 +- `python3 scripts/security/wazuh-readonly-live-metadata-env-gate.py --root .`:`route_readback=0 owner=0 secret_meta=0 live_query=0 runtime_gate=0`。 +- `pnpm --filter @awoooi/web typecheck`:通過。 +- 首次未帶 `NEXT_PUBLIC_API_URL` 的 build 依既有前端禁令在 prerender 階段 fail;重跑 `NEXT_PUBLIC_API_URL=https://awoooi.wooo.work NEXT_PRIVATE_BUILD_WORKER_COUNT=1 SENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNING=1 pnpm --filter @awoooi/web build` 通過,`92/92` static pages,`/zh-TW/iwooos` route size `61.8 kB`、First Load JS `292 kB`。 +- 本機 preview `http://localhost:3137/zh-TW/iwooos?_v=wazuh-live-metadata-env-local` 桌機寬度:卡片可見、`scrollWidth=clientWidth=1434`、水平溢出 `false`、未命中工作視窗、委派、來源執行緒、外部對話與通知暱稱等內部協作詞;卡片也未命中英文流程裸字。 +- 本機 preview 手機寬度 `390x844`:卡片可見、`scrollWidth=clientWidth=384`、水平溢出 `false`,同樣未命中工作視窗字樣與英文流程裸字。 + +**完成度**: +- Wazuh live metadata env gate source / guard:`100%`。 +- IwoooS 前台卡片 source / 本機桌機與手機驗證:`100%`。 +- Production deploy:`0%`。 +- Production `/api/iwooos/wazuh` readback:`0%`,部署前仍只允許 `predeploy_404_observed`。 +- Wazuh server-side env enable:`0%`。 +- Wazuh live query、active response、host write、Kali active scan、SOAR action:全部維持 `0 / false`。 + +**邊界**:本輪沒有 push、沒有 deploy、沒有 Nginx / Docker / K8s / firewall / Wazuh manager / secret / runtime 寫入,沒有 active scan,沒有收集或保存任何 secret value;只做 source、docs、guard、本機 build 與本機瀏覽器驗證。 + ## 2026-06-24|22:40 MOMO source absence readback 與資料 freshness blocker 定位 **背景**:22:17 已完成 MOMO import-boundary production deploy 驗證與 full cold-start refresh,但 cold-start 仍因 `MOMO_DAILY_FRESHNESS 7|2026-06-17` 保持 `BLOCKED=1`。本輪只做 MOMO scheduler / DB / import metadata 的 read-only 追查,目標是判斷資料停更是服務、版本、DB 同步、排程或來源缺席。 diff --git a/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md b/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md index 48c1c0052..86d1109a5 100644 --- a/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md +++ b/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md @@ -40,6 +40,9 @@ - `docs/security/wazuh-readonly-release-owner-request.snapshot.json` - `docs/security/wazuh-readonly-release-owner-response-acceptance.snapshot.json` - `docs/security/wazuh-readonly-live-metadata-env-gate.snapshot.json` +- `apps/web/src/app/[locale]/iwooos/page.tsx` +- `apps/web/messages/zh-TW.json` +- `apps/web/messages/en.json` - `docs/LOGBOOK.md` 完成內容: @@ -57,6 +60,7 @@ - 新增 release lane preflight snapshot 與 guard,固定正式 release 前必須選擇 `formal_gitea_merge`、`formal_patch_apply` 或 `maintainer_local_push_with_safe_credential` 其中一條合規 lane,且 owner ack / evidence 未到齊前不得 push、deploy、force push、使用明文 token workaround 或改 runtime。 - 新增 release owner request 草稿與 owner response acceptance 帳本,將 required ack flags、required evidence fields、allowed release methods、blocked actions、forbidden payloads 與 reviewer checks 機器可讀化;目前 request sent、response received / accepted、release ready、runtime gate 全部維持 `0`。 - 新增 live metadata env gate,固定部署後要先通過 production route readback、server-side env owner response、secret source metadata、Wazuh manager health ref、readonly account scope、post-enable readback、rollback 與 no-secret / no-raw-payload attestation;目前 live query authorized 仍為 `0`。 +- 新增 IwoooS 前台「Wazuh 即時中繼資料環境閘門」卡片,公開顯示上述 gate 的 `0 / false` 邊界;文案全部為繁體中文治理語,不放工作視窗逐字稿、委派 XML、聊天內容或個人英文名稱。 ## 已完成驗證 @@ -74,6 +78,10 @@ python3 scripts/security/security-mirror-progress-guard.py --root . python3 scripts/ops/doc-secrets-sanity-check.py docs apps/api/src/api/v1/iwooos.py apps/web/src/app/api/iwooos/wazuh/route.ts scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/wazuh-readonly-release-lane-preflight.py scripts/security/wazuh-readonly-release-owner-request.py scripts/security/wazuh-readonly-release-owner-response-acceptance.py scripts/security/wazuh-readonly-live-metadata-env-gate.py python3 -m py_compile apps/api/src/api/v1/iwooos.py scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/wazuh-readonly-release-lane-preflight.py scripts/security/wazuh-readonly-release-owner-request.py scripts/security/wazuh-readonly-release-owner-response-acceptance.py scripts/security/wazuh-readonly-live-metadata-env-gate.py scripts/security/security-mirror-progress-guard.py git diff --check +node -e "JSON.parse(require('fs').readFileSync('apps/web/messages/zh-TW.json','utf8')); JSON.parse(require('fs').readFileSync('apps/web/messages/en.json','utf8')); console.log('i18n json ok')" +cmp -s apps/web/messages/zh-TW.json apps/web/messages/en.json +pnpm --filter @awoooi/web typecheck +NEXT_PUBLIC_API_URL=https://awoooi.wooo.work NEXT_PRIVATE_BUILD_WORKER_COUNT=1 SENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNING=1 pnpm --filter @awoooi/web build ``` 驗證結果: @@ -89,6 +97,12 @@ git diff --check - `doc-secrets-sanity-check`:`DOC_SECRET_SANITY_OK scanned_files=973`。 - `py_compile`:通過。 - `git diff --check`:通過。 +- i18n JSON parse:通過。 +- `zh-TW` / `en` messages mirror:通過。 +- `pnpm --filter @awoooi/web typecheck`:通過。 +- Web production build:通過,`92/92` static pages,`/zh-TW/iwooos` route size `61.8 kB`、First Load JS `292 kB`;build env 使用公網 `NEXT_PUBLIC_API_URL=https://awoooi.wooo.work`,未使用內網 IP。 +- 本機 preview desktop:`/zh-TW/iwooos` 卡片可見,水平溢出 `false`,沒有工作視窗 / 委派 / 對話字樣,卡片沒有英文流程裸字。 +- 本機 preview mobile `390x844`:卡片可見,水平溢出 `false`,同樣沒有工作視窗 / 委派 / 對話字樣。 ## 乾淨套用 Proof @@ -185,6 +199,7 @@ python3 scripts/security/wazuh-readonly-production-readback.py --json | Wazuh release lane preflight | `100%` | 已完成;owner acks `0/6`、evidence `0/6`、正式 release ready `0` | | Wazuh release owner request / acceptance | `100%` | 已完成只讀草稿與收件帳本;request sent `0`、response accepted `0` | | Wazuh live metadata env gate | `100%` | 已完成只讀 gate;route readback / owner / secret metadata / live query 仍 `0` | +| IwoooS 前台 Wazuh live metadata env gate 卡片 | `100%` | source-side 與本機桌機 / 手機驗證完成;production deploy 仍 `0` | | 乾淨套用 proof | `100%` | patch set 可落在最新 `gitea/main` 並通過同組 guard;最終 hash 以 release 前 readback 為準 | | Gitea push | `0%` | 受控 workspace HTTPS credential 缺失 | | Production deploy / readback | `0%` | 等待 release lane | From d9dbd4d6cc604acccd712a26bdeefcf803feea48 Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 24 Jun 2026 23:25:44 +0800 Subject: [PATCH 09/11] docs(iwooos): record Wazuh agent visibility incident --- docs/LOGBOOK.md | 36 +++++++++ ...APPEARANCE-INCIDENT-READBACK-2026-06-24.md | 78 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 docs/security/WAZUH-AGENT-DISAPPEARANCE-INCIDENT-READBACK-2026-06-24.md diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index d9e3566e1..e653cb63d 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -16,6 +16,42 @@ **邊界**:本輪沒有主機寫入、沒有 Docker / Nginx / firewall / K8s / ArgoCD 操作、沒有 Wazuh / SOC 修改、沒有使用聊天中的密碼,也沒有讀取或保存 secret。 +## 2026-06-24|23:25 Wazuh 用戶端消失事故只讀鑑別 + +**背景**:使用者通報 Wazuh Dashboard 顯示所有用戶端消失,並指出 IwoooS / Wazuh 機制沒有發揮資安保護作用。本輪停止前台收尾與 release proof,改走 runtime 只讀鑑別與跨視窗同步;沒有重啟、沒有重新註冊 agent、沒有改 Wazuh / Docker / Nginx / firewall / secret。 + +**只讀 evidence**: +- Production `https://awoooi.wooo.work/api/iwooos/wazuh` 與 `/api/v1/iwooos/wazuh` 仍回 `404`,代表 IwoooS 尚未部署 live Wazuh metadata readback,不能偵測 agent count。 +- 110 / 188 的 `wazuh-agent.service` 均 active running,啟動於 2026-06-23 14:50 CST 左右。 +- 110 `ossec.conf` 指向 manager `192.168.0.112`;110 本機 `client.keys` 存在且為 1 筆本機 agent key。 +- 110 / 188 到 112:1514 皆有 ESTABLISHED 連線;112:1514 / 1515 / 55000 targeted reachability 均成功。 +- 112 以 `kali@192.168.0.112` 可只讀登入;`wazuh-manager`、`wazuh-indexer`、`wazuh-dashboard` 均 active running,啟動於 2026-06-23 14:48-14:51 CST,`NRestarts=0`。 +- 112 Wazuh API `55000` 從本機、110、188 皆回 `401`,代表 API 活著但需認證。 +- 112 Dashboard journal 在 2026-06-24 23:14 CST 左右對 `/api/check-stored-api`、`/api/check-api` 出現 `429 / 500`;23:20 後未再新增同類錯誤。 +- in-app browser 開 `https://192.168.0.112/` 被未知 CA 憑證阻擋;本輪未繞過瀏覽器安全警告。 + +**判定**: +- 可宣稱:110 / 188 agent service 與到 112 manager 的連線仍存在;112 manager / indexer / dashboard / API endpoint 沒有整體掛掉。 +- 不可宣稱:Wazuh manager agent registry 已恢復、Dashboard 顯示已修復、agent list 已驗收、IwoooS 已具備 live Wazuh readback。 +- 高機率故障層:Dashboard stored API / Wazuh API 認證、rate-limit、TLS trust 或 Dashboard API check;不是 110 / 188 agent 網路層全部消失。 + +**新增文件**: +- `docs/security/WAZUH-AGENT-DISAPPEARANCE-INCIDENT-READBACK-2026-06-24.md`:固定只讀證據、為什麼前一版沒有擋住、立即凍結邊界、P0/P1 修復優先序與完成度。 + +**跨視窗同步**: +- 已同步 `主機重啟SOP工作推進_20260604`:請暫停 112/Wazuh 寫操作,回報是否有 Wazuh manager / dashboard / indexer restart、stored API / credential / agent enrollment / firewall / route 變更。 +- 已同步 `盤查 CI/CD 與環境機制`:請維持 `wazuh_live_agent_registry_readback=0`、`iwooos_wazuh_runtime_gate=0`、`active_response=0`,不要把 Wazuh 標成 live agent registry 已閉環。 + +**完成度**: +- 現場只讀鑑別:`70%`。 +- 真正 agent registry 驗收:`0%`。 +- IwoooS live readback production:`0%`。 +- Dashboard stored API 修復:`0%`。 +- SOC / Wazuh no-false-green 納管:`35%`。 +- active response / host write / auto block:`0%`,保持關閉。 + +**邊界**:本輪沒有收集或保存 Wazuh secret、API token、cookie、private key、raw log、raw payload;沒有 sudo password;沒有重啟 Wazuh、沒有 Docker / systemd / Nginx / firewall / K8s / ArgoCD runtime 寫入,沒有 active scan。 + ## 2026-06-24|23:04 MOMO source absence cold-start gate v1.42 **背景**:22:40 readback 已確認 MOMO stale 的根因是 upstream source absence,但既有 cold-start scorecard 只顯示 `MOMO_DAILY_FRESHNESS 7|2026-06-17`,operator 仍需要回看 LOGBOOK 才知道不是服務 / DB / scheduler / import config 壞掉。本輪把這個分類直接補進 repo-side cold-start 腳本與 machine-readable baseline,讓下一次重啟判定能在 scorecard 本身呈現來源檔缺席。 diff --git a/docs/security/WAZUH-AGENT-DISAPPEARANCE-INCIDENT-READBACK-2026-06-24.md b/docs/security/WAZUH-AGENT-DISAPPEARANCE-INCIDENT-READBACK-2026-06-24.md new file mode 100644 index 000000000..195b17b56 --- /dev/null +++ b/docs/security/WAZUH-AGENT-DISAPPEARANCE-INCIDENT-READBACK-2026-06-24.md @@ -0,0 +1,78 @@ +# Wazuh 用戶端消失事故只讀回讀 + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-06-24 | +| 狀態 | `runtime_dashboard_agent_visibility_incident_readonly_triage` | +| 事故焦點 | Wazuh Dashboard 顯示用戶端消失,但 IwoooS 尚未具備 live agent registry readback | +| runtime gate | `0` | + +## 1. 結論 + +這次不能再用「Wazuh 已建置」或「Dashboard 可開」當成資安機制有效。現有 IwoooS 機制仍停在 source-side 只讀框架、前台邊界與 owner gate,production `/api/iwooos/wazuh` 尚未部署,因此沒有真正讀到 Wazuh manager 的 agent count、agent status、last seen 或 event refs。 + +本輪只讀證據顯示:110 與 188 的 Wazuh agent 服務仍在執行,且都與 112 的 Wazuh manager port `1514` 保持連線;112 的 Wazuh manager、indexer、dashboard 也都在執行。Dashboard 顯示用戶端消失的高機率原因,不是 110 / 188 agent 全部停止,而是 112 Dashboard 到 Wazuh API 的 stored API / 認證 / rate-limit / TLS trust 檢查層在 2026-06-24 23:14 CST 左右出現 `429 / 500`。 + +但 manager 端 agent registry 的真實清單尚未被獨立驗收,因為本視窗沒有 Wazuh API 只讀帳號,也沒有 112 root read 權限;因此 `wazuh_live_agent_registry_readback` 必須維持 `0`。 + +## 2. 只讀證據摘要 + +| 檢查面 | 證據 | 判定 | +|--------|------|------| +| IwoooS production route | `https://awoooi.wooo.work/api/iwooos/wazuh` 與 `/api/v1/iwooos/wazuh` 皆為 `404` | IwoooS 尚未讀到 Wazuh live metadata | +| 公開 Wazuh domain | `wazuh.wooo.work` 未形成可用公開入口 | 不可用公開 domain 代表 Wazuh runtime 狀態 | +| 110 agent | `wazuh-agent.service` active running;啟動於 2026-06-23 14:50 CST;`/var/ossec/etc/client.keys` 存在且為 1 筆本機 key | 110 agent 端未消失 | +| 110 manager target | `ossec.conf` manager address 指向 `192.168.0.112` | 112 是 manager control plane | +| 188 agent | `wazuh-agent.service` active running;啟動於 2026-06-23 14:50 CST | 188 agent 端未消失 | +| agent 到 manager | 110 / 188 到 `192.168.0.112:1514` 皆有 ESTABLISHED 連線;`1514 / 1515 / 55000` targeted reachability 成功 | agent network path 正常 | +| 112 control plane | `wazuh-manager`、`wazuh-indexer`、`wazuh-dashboard` 均 active running;啟動於 2026-06-23 14:48-14:51 CST;`NRestarts=0` | 112 服務沒有整體掛掉 | +| 112 API endpoint | `https://192.168.0.112:55000/` 從本機、110、188 皆回 `401` | API 活著但需要認證 | +| Dashboard 讀取層 | 2026-06-24 23:14 CST 左右 `/api/check-stored-api`、`/api/check-api` 出現 `429 / 500`,並記錄 Wazuh API check 異常 | Dashboard stored API / rate-limit / 認證 / TLS trust 檢查需維修 | +| 23:20 後狀態 | 23:20 CST 後 Dashboard journal 無新增 429/500 | 可能是短時間檢查/登入造成的節流,但仍未驗收 agent registry | + +## 3. 為什麼前一版沒有擋住 + +1. IwoooS 先前只建立 readback plan、前台卡與 release gate,沒有真正接 Wazuh manager agent registry。 +2. `active_runtime_gate=0` 是正確邊界,但前台文案容易讓人誤會「Wazuh 已整合」等於「agent 消失會被抓到」。 +3. 缺少 production 只讀 API:`agent_total`、`agent_active`、`agent_disconnected`、`last_seen_present` 都沒有 live readback。 +4. 缺少 no-false-green 告警:Dashboard 429/500、Wazuh API 401、agent 連線存在、IwoooS route 404 這些狀態沒有被合成一張 AI 事件卡。 +5. 缺少 owner evidence:誰在 2026-06-23 14:48 後建立、重啟、登入或調整 112/Wazuh,尚未有脫敏 owner 回覆。 + +## 4. 立即凍結邊界 + +在 manager 端 agent registry 被只讀驗收前,以下全部維持禁止: + +- 不重啟 `wazuh-manager`、`wazuh-indexer`、`wazuh-dashboard`。 +- 不重新註冊 agent、不重產 `client.keys`、不刪 agent。 +- 不改 Wazuh API 使用者、stored API、憑證、TLS、rate-limit 或 Dashboard 設定。 +- 不改 firewall、Nginx、WireGuard、Docker、systemd、K8s、ArgoCD。 +- 不把 Dashboard 空白、Dashboard 恢復、agent active、route 200、IwoooS UI 可見當成事件結案。 +- 不收 Wazuh 密碼、token、cookie、private key、raw log、raw payload 或截圖中的敏感資訊。 + +## 5. 修復與納管優先順序 + +| 優先 | 工作 | 驗收條件 | 目前完成度 | +|------|------|----------|------------| +| P0-A | Wazuh manager agent registry 只讀驗收 | owner 提供脫敏 `agent_total / active / disconnected / last_seen` ref,或經 server-side secret metadata 啟用 IwoooS 只讀 API | `40%` | +| P0-B | Dashboard stored API / rate-limit / TLS trust 修復 gate | 查明 `/api/check-stored-api` 429/500 根因;維修前有 owner、rollback、postcheck;維修後 Dashboard 與 API count 一致 | `35%` | +| P0-C | IwoooS live metadata route 正式部署 | `/api/iwooos/wazuh` 不再 404,回傳 schema `iwooos_wazuh_readonly_status_v1`,不洩漏 agent identity / internal IP / secret | `55%` source-side、`0%` production | +| P0-D | Wazuh agent disappearance alert card | 產出 `ai_automation_alert_card_v1`,包含 agent count delta、Dashboard API status、manager health、next gate、owner | `20%` | +| P0-E | 112/Wazuh owner response | 回覆 owner role/team、decision、reason、affected scope、redacted evidence refs、rollback owner、followup owner | `0%` | +| P1-A | 110/188 agent receipt heartbeat | 每台 host 定期只讀確認 service active、manager target、1514 established、last evidence ref | `45%` | +| P1-B | Dashboard no-false-green | Dashboard 429/500 或 Wazuh API check failure 要進 IwoooS incident,不可顯示綠燈 | `15%` | + +## 6. 下一步 + +1. 請 Wazuh/112 owner 補脫敏 agent registry evidence:`agent_total`、`active`、`disconnected`、`never_connected`、`last_seen` 時間窗,不提供密碼或 raw payload。 +2. 啟用 IwoooS `/api/iwooos/wazuh` 前,先完成 production route readback、server-side env owner、secret source metadata、readonly account scope 與 rollback owner。 +3. 若 owner 批准維修 Dashboard stored API,必須先做 read-only preflight:rate-limit 現況、stored API 指向、TLS trust、API user scope、Dashboard 與 manager 版本、回滾方式。 +4. 補 IwoooS AI 事件卡:當 Dashboard 顯示 agents 空白但 110/188 agent 仍 connected 時,分類為 `wazuh_dashboard_api_readback_degraded`,而不是 `all_agents_missing`。 + +## 7. 完成度 + +- 現場只讀鑑別:`70%`。 +- 真正 agent registry 驗收:`0%`。 +- IwoooS live readback production:`0%`。 +- Dashboard stored API 修復:`0%`。 +- SOC / Wazuh no-false-green 納管:`35%`。 +- active response / host write / auto block:`0%`,保持關閉。 From 3d173712f3b20e21e96f99d2ebdc737f6f84438b Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 24 Jun 2026 23:34:09 +0800 Subject: [PATCH 10/11] docs(iwooos): guard Wazuh agent visibility incident --- docs/LOGBOOK.md | 4 +- ...APPEARANCE-INCIDENT-READBACK-2026-06-24.md | 20 +- ...gent-visibility-runtime-gate.snapshot.json | 133 +++++++++++++ .../security-mirror-progress-guard.py | 4 + .../wazuh-agent-visibility-runtime-gate.py | 175 ++++++++++++++++++ 5 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 docs/security/wazuh-agent-visibility-runtime-gate.snapshot.json create mode 100644 scripts/security/wazuh-agent-visibility-runtime-gate.py diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index e653cb63d..01b74282f 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -34,9 +34,11 @@ - 可宣稱:110 / 188 agent service 與到 112 manager 的連線仍存在;112 manager / indexer / dashboard / API endpoint 沒有整體掛掉。 - 不可宣稱:Wazuh manager agent registry 已恢復、Dashboard 顯示已修復、agent list 已驗收、IwoooS 已具備 live Wazuh readback。 - 高機率故障層:Dashboard stored API / Wazuh API 認證、rate-limit、TLS trust 或 Dashboard API check;不是 110 / 188 agent 網路層全部消失。 +- 23:29 補查:`kali` 使用者不能直接執行 manager registry 工具,`sudo -n` 讀取也需要密碼;本輪不收密碼、不升權,因此 agent registry truth 仍維持未驗收。 **新增文件**: - `docs/security/WAZUH-AGENT-DISAPPEARANCE-INCIDENT-READBACK-2026-06-24.md`:固定只讀證據、為什麼前一版沒有擋住、立即凍結邊界、P0/P1 修復優先序與完成度。 +- `scripts/security/wazuh-agent-visibility-runtime-gate.py` 與 `docs/security/wazuh-agent-visibility-runtime-gate.snapshot.json`:新增 no-false-green guard;在 `manager_agent_registry_readback_passed=false`、`iwooos_live_route_readback_passed=false`、`dashboard_agent_list_recovered=false` 時,仍允許 guard 通過但只代表「阻擋狀態被正確保存」,不可宣稱 runtime green。 **跨視窗同步**: - 已同步 `主機重啟SOP工作推進_20260604`:請暫停 112/Wazuh 寫操作,回報是否有 Wazuh manager / dashboard / indexer restart、stored API / credential / agent enrollment / firewall / route 變更。 @@ -47,7 +49,7 @@ - 真正 agent registry 驗收:`0%`。 - IwoooS live readback production:`0%`。 - Dashboard stored API 修復:`0%`。 -- SOC / Wazuh no-false-green 納管:`35%`。 +- SOC / Wazuh no-false-green 納管:`45%`。 - active response / host write / auto block:`0%`,保持關閉。 **邊界**:本輪沒有收集或保存 Wazuh secret、API token、cookie、private key、raw log、raw payload;沒有 sudo password;沒有重啟 Wazuh、沒有 Docker / systemd / Nginx / firewall / K8s / ArgoCD runtime 寫入,沒有 active scan。 diff --git a/docs/security/WAZUH-AGENT-DISAPPEARANCE-INCIDENT-READBACK-2026-06-24.md b/docs/security/WAZUH-AGENT-DISAPPEARANCE-INCIDENT-READBACK-2026-06-24.md index 195b17b56..6f2440e27 100644 --- a/docs/security/WAZUH-AGENT-DISAPPEARANCE-INCIDENT-READBACK-2026-06-24.md +++ b/docs/security/WAZUH-AGENT-DISAPPEARANCE-INCIDENT-READBACK-2026-06-24.md @@ -29,6 +29,7 @@ | 112 API endpoint | `https://192.168.0.112:55000/` 從本機、110、188 皆回 `401` | API 活著但需要認證 | | Dashboard 讀取層 | 2026-06-24 23:14 CST 左右 `/api/check-stored-api`、`/api/check-api` 出現 `429 / 500`,並記錄 Wazuh API check 異常 | Dashboard stored API / rate-limit / 認證 / TLS trust 檢查需維修 | | 23:20 後狀態 | 23:20 CST 後 Dashboard journal 無新增 429/500 | 可能是短時間檢查/登入造成的節流,但仍未驗收 agent registry | +| manager registry 讀取權限 | `kali` 使用者不能直接執行 manager registry 工具;`sudo -n` 讀取需要密碼;本輪不收密碼、不升權 | agent registry truth 仍未驗收,不能結案 | ## 3. 為什麼前一版沒有擋住 @@ -38,6 +39,22 @@ 4. 缺少 no-false-green 告警:Dashboard 429/500、Wazuh API 401、agent 連線存在、IwoooS route 404 這些狀態沒有被合成一張 AI 事件卡。 5. 缺少 owner evidence:誰在 2026-06-23 14:48 後建立、重啟、登入或調整 112/Wazuh,尚未有脫敏 owner 回覆。 +## 3.1 新增 no-false-green guard + +本輪新增 `scripts/security/wazuh-agent-visibility-runtime-gate.py` 與 `docs/security/wazuh-agent-visibility-runtime-gate.snapshot.json`。這個 guard 的目的不是修復 Dashboard,而是防止 IwoooS 在沒有 agent registry 讀回時誤顯示綠燈。 + +目前機器可讀狀態固定為: + +- `manager_agent_registry_readback_passed=false` +- `iwooos_live_route_readback_passed=false` +- `dashboard_agent_list_recovered=false` +- `runtime_gate_count=0` +- `active_response_authorized=false` +- `host_write_authorized=false` +- `secret_value_collection_allowed=false` + +解除條件必須是 Wazuh API 只讀中繼資料或 owner 提供的脫敏 registry evidence,不能用 Dashboard 看起來正常、agent service active、TCP 連線存在或 UI 卡片可見替代。 + ## 4. 立即凍結邊界 在 manager 端 agent registry 被只讀驗收前,以下全部維持禁止: @@ -60,6 +77,7 @@ | P0-E | 112/Wazuh owner response | 回覆 owner role/team、decision、reason、affected scope、redacted evidence refs、rollback owner、followup owner | `0%` | | P1-A | 110/188 agent receipt heartbeat | 每台 host 定期只讀確認 service active、manager target、1514 established、last evidence ref | `45%` | | P1-B | Dashboard no-false-green | Dashboard 429/500 或 Wazuh API check failure 要進 IwoooS incident,不可顯示綠燈 | `15%` | +| P1-C | 機器可讀 runtime gate | `wazuh-agent-visibility-runtime-gate.py` 納入 `security-mirror-progress-guard.py`,未驗收 registry 時 guard 保持 blocked snapshot | `100%` source-side、`0%` runtime | ## 6. 下一步 @@ -74,5 +92,5 @@ - 真正 agent registry 驗收:`0%`。 - IwoooS live readback production:`0%`。 - Dashboard stored API 修復:`0%`。 -- SOC / Wazuh no-false-green 納管:`35%`。 +- SOC / Wazuh no-false-green 納管:`45%`。 - active response / host write / auto block:`0%`,保持關閉。 diff --git a/docs/security/wazuh-agent-visibility-runtime-gate.snapshot.json b/docs/security/wazuh-agent-visibility-runtime-gate.snapshot.json new file mode 100644 index 000000000..09203f32d --- /dev/null +++ b/docs/security/wazuh-agent-visibility-runtime-gate.snapshot.json @@ -0,0 +1,133 @@ +{ + "schema_version": "wazuh_agent_visibility_runtime_gate_v1", + "generated_at": "2026-06-24T23:35:00+08:00", + "status": "blocked_waiting_manager_agent_registry_readback", + "mode": "snapshot_only_no_runtime_no_secret_collection", + "incident_id": "wazuh-agent-visibility-20260624", + "runtime_gate_count": 0, + "manager_agent_registry_readback_passed": false, + "iwooos_live_route_readback_passed": false, + "dashboard_agent_list_recovered": false, + "active_response_authorized": false, + "host_write_authorized": false, + "secret_value_collection_allowed": false, + "manager_services_active_observed": true, + "agent_transport_connected_observed": true, + "dashboard_api_degraded_observed": true, + "production_route_http_status": 404, + "observed_at_taipei": "2026-06-24T23:29:22+08:00", + "observed_layers": { + "iwooos_production_route": { + "status": "blocked", + "evidence": "正式站 Wazuh 只讀 API 路由在部署前仍回 404", + "completion_percent": 0 + }, + "wazuh_control_plane": { + "status": "observed_active", + "evidence": "112 上 manager、indexer、dashboard 服務已只讀觀察為 active", + "completion_percent": 70 + }, + "host_agent_transport": { + "status": "observed_connected", + "evidence": "110 與 188 agent 已只讀觀察為 active,且到 112 的 1514 transport 已建立", + "completion_percent": 75 + }, + "manager_agent_registry": { + "status": "blocked_no_readonly_registry_access", + "evidence": "kali 使用者無法以一般權限讀 manager registry;Wazuh API 需要正式只讀認證", + "completion_percent": 0 + }, + "dashboard_api_check": { + "status": "degraded_observed", + "evidence": "dashboard plugin 在 stored API 與 API check 期間觀察到 429 或 500", + "completion_percent": 35 + } + }, + "registry_counts": { + "agent_total": null, + "agent_active": null, + "agent_disconnected": null, + "agent_never_connected": null, + "last_seen_window_verified": false + }, + "dashboard_error_codes_observed": [ + 429, + 500 + ], + "required_evidence_before_green": [ + { + "evidence_id": "manager_agent_registry_counts", + "accepted": false, + "required_fields": [ + "agent_total", + "agent_active", + "agent_disconnected", + "agent_never_connected", + "last_seen_window" + ], + "allowed_source": "Wazuh API 只讀中繼資料或 owner 提供的脫敏證據" + }, + { + "evidence_id": "iwooos_live_route_readback", + "accepted": false, + "required_fields": [ + "schema_version", + "status", + "aggregate_counts", + "runtime_gate_count" + ], + "allowed_source": "正式站 /api/iwooos/wazuh 讀回" + }, + { + "evidence_id": "dashboard_api_check_repaired_or_explained", + "accepted": false, + "required_fields": [ + "stored_api_status", + "api_check_status", + "rate_limit_status", + "tls_trust_status" + ], + "allowed_source": "已脫敏 dashboard 讀回或 owner 維修證據" + }, + { + "evidence_id": "readonly_account_scope", + "accepted": false, + "required_fields": [ + "secret_name_only", + "read_scope", + "rotation_owner", + "rollback_owner" + ], + "allowed_source": "不含 secret value 的 server-side secret metadata" + }, + { + "evidence_id": "owner_response", + "accepted": false, + "required_fields": [ + "owner_role", + "team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner" + ], + "allowed_source": "owner response 封包" + } + ], + "forbidden_completion_claims": [ + "Wazuh 用戶端已恢復", + "Wazuh agent registry 已驗收", + "IwoooS 已能偵測 agent 消失", + "active response 已授權", + "host write 已授權" + ], + "next_priority_order": [ + "P0-A manager agent registry 只讀計數", + "P0-B dashboard stored API 與 rate-limit 根因", + "P0-C IwoooS 正式站 Wazuh 路由讀回", + "P0-D dashboard/API mismatch 的 AI 自動化告警卡", + "P0-E owner response 與 rollback owner" + ] +} diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 23a7d8d94..15d8463b7 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -111,6 +111,10 @@ def validate(root: Path) -> None: str(root / "scripts" / "security" / "wazuh-readonly-live-metadata-env-gate.py") ) wazuh_readonly_live_metadata_env_gate["validate"](root) + wazuh_agent_visibility_runtime_gate = runpy.run_path( + str(root / "scripts" / "security" / "wazuh-agent-visibility-runtime-gate.py") + ) + wazuh_agent_visibility_runtime_gate["validate"](root) telegram_alert_readability_guard = runpy.run_path( str(root / "scripts" / "security" / "telegram-alert-readability-guard.py") ) diff --git a/scripts/security/wazuh-agent-visibility-runtime-gate.py b/scripts/security/wazuh-agent-visibility-runtime-gate.py new file mode 100644 index 000000000..37418fc50 --- /dev/null +++ b/scripts/security/wazuh-agent-visibility-runtime-gate.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +""" +檢查 Wazuh agent visibility runtime gate 的 no-false-green 邊界。 + +本 guard 只驗證 repo 內的脫敏 snapshot,不連線 Wazuh、不讀 secret、 +不重新註冊 agent、不啟用 active response,也不做任何 runtime 寫入。 +""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path +from typing import Any + + +SNAPSHOT_PATH = Path("docs/security/wazuh-agent-visibility-runtime-gate.snapshot.json") +SCHEMA_VERSION = "wazuh_agent_visibility_runtime_gate_v1" + +FORBIDDEN_TEXT_PATTERNS = [ + re.compile(r"Authorization\s*:", re.IGNORECASE), + re.compile(r"Bearer\s+[A-Za-z0-9._-]{10,}", re.IGNORECASE), + re.compile(r"Basic\s+[A-Za-z0-9+/=]{10,}", re.IGNORECASE), + re.compile(r"password\s*[:=]\s*['\"][^'\"]+['\"]", re.IGNORECASE), + re.compile(r"token\s*[:=]\s*['\"][^'\"]+['\"]", re.IGNORECASE), + re.compile(r"cookie\s*[:=]\s*['\"][^'\"]+['\"]", re.IGNORECASE), + re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"), +] + + +def load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def assert_equal(label: str, actual: Any, expected: Any) -> None: + if actual != expected: + raise SystemExit(f"BLOCKED {label}: expected {expected!r}, got {actual!r}") + + +def assert_false(label: str, actual: Any) -> None: + assert_equal(label, actual, False) + + +def assert_true(label: str, actual: Any) -> None: + assert_equal(label, actual, True) + + +def assert_zero(label: str, actual: Any) -> None: + assert_equal(label, actual, 0) + + +def collect_string_values(value: Any) -> list[str]: + if isinstance(value, str): + return [value] + if isinstance(value, list): + values: list[str] = [] + for item in value: + values.extend(collect_string_values(item)) + return values + if isinstance(value, dict): + values = [] + for item in value.values(): + values.extend(collect_string_values(item)) + return values + return [] + + +def validate_no_secret_values(snapshot: dict[str, Any]) -> None: + for text in collect_string_values(snapshot): + for pattern in FORBIDDEN_TEXT_PATTERNS: + if pattern.search(text): + raise SystemExit("BLOCKED wazuh_agent_visibility_runtime_gate: snapshot contains forbidden secret-shaped text") + + +def validate_required_evidence(snapshot: dict[str, Any]) -> None: + required = snapshot.get("required_evidence_before_green", []) + required_ids = {item.get("evidence_id") for item in required if isinstance(item, dict)} + expected_ids = { + "manager_agent_registry_counts", + "iwooos_live_route_readback", + "dashboard_api_check_repaired_or_explained", + "readonly_account_scope", + "owner_response", + } + missing = sorted(expected_ids - required_ids) + if missing: + raise SystemExit(f"BLOCKED wazuh_agent_visibility_runtime_gate.required_evidence: missing {missing!r}") + + for item in required: + assert_equal( + f"wazuh_agent_visibility_runtime_gate.required_evidence.{item.get('evidence_id')}.accepted", + item.get("accepted"), + False, + ) + + +def validate(root: Path) -> None: + path = root / SNAPSHOT_PATH + snapshot = load_json(path) + assert_equal("wazuh_agent_visibility_runtime_gate.schema_version", snapshot.get("schema_version"), SCHEMA_VERSION) + assert_equal( + "wazuh_agent_visibility_runtime_gate.status", + snapshot.get("status"), + "blocked_waiting_manager_agent_registry_readback", + ) + assert_equal( + "wazuh_agent_visibility_runtime_gate.mode", + snapshot.get("mode"), + "snapshot_only_no_runtime_no_secret_collection", + ) + assert_zero("wazuh_agent_visibility_runtime_gate.runtime_gate_count", snapshot.get("runtime_gate_count")) + assert_false( + "wazuh_agent_visibility_runtime_gate.manager_agent_registry_readback_passed", + snapshot.get("manager_agent_registry_readback_passed"), + ) + assert_false( + "wazuh_agent_visibility_runtime_gate.iwooos_live_route_readback_passed", + snapshot.get("iwooos_live_route_readback_passed"), + ) + assert_false( + "wazuh_agent_visibility_runtime_gate.dashboard_agent_list_recovered", + snapshot.get("dashboard_agent_list_recovered"), + ) + assert_false( + "wazuh_agent_visibility_runtime_gate.active_response_authorized", + snapshot.get("active_response_authorized"), + ) + assert_false("wazuh_agent_visibility_runtime_gate.host_write_authorized", snapshot.get("host_write_authorized")) + assert_false( + "wazuh_agent_visibility_runtime_gate.secret_value_collection_allowed", + snapshot.get("secret_value_collection_allowed"), + ) + assert_true( + "wazuh_agent_visibility_runtime_gate.manager_services_active_observed", + snapshot.get("manager_services_active_observed"), + ) + assert_true( + "wazuh_agent_visibility_runtime_gate.agent_transport_connected_observed", + snapshot.get("agent_transport_connected_observed"), + ) + assert_true( + "wazuh_agent_visibility_runtime_gate.dashboard_api_degraded_observed", + snapshot.get("dashboard_api_degraded_observed"), + ) + assert_equal( + "wazuh_agent_visibility_runtime_gate.production_route_http_status", + snapshot.get("production_route_http_status"), + 404, + ) + validate_required_evidence(snapshot) + validate_no_secret_values(snapshot) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=Path.cwd()) + parser.add_argument("--json", action="store_true") + args = parser.parse_args() + root = args.root.resolve() + validate(root) + snapshot = load_json(root / SNAPSHOT_PATH) + if args.json: + print(json.dumps(snapshot, ensure_ascii=False, indent=2)) + return + print( + "WAZUH_AGENT_VISIBILITY_RUNTIME_GATE_OK " + f"registry=0 route={snapshot['production_route_http_status']} " + f"dashboard_degraded={int(snapshot['dashboard_api_degraded_observed'])} " + f"runtime_gate={snapshot['runtime_gate_count']}" + ) + + +if __name__ == "__main__": + main() From 21bb86eca0c877d4c09c921961f651b4dfaa4453 Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 24 Jun 2026 23:35:21 +0800 Subject: [PATCH 11/11] docs(iwooos): record Wazuh guard branch readback --- docs/LOGBOOK.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 01b74282f..e2dcdc703 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -44,6 +44,11 @@ - 已同步 `主機重啟SOP工作推進_20260604`:請暫停 112/Wazuh 寫操作,回報是否有 Wazuh manager / dashboard / indexer restart、stored API / credential / agent enrollment / firewall / route 變更。 - 已同步 `盤查 CI/CD 與環境機制`:請維持 `wazuh_live_agent_registry_readback=0`、`iwooos_wazuh_runtime_gate=0`、`active_response=0`,不要把 Wazuh 標成 live agent registry 已閉環。 +**Gitea branch readback**: +- HTTPS `gitea` push 仍因非互動式 credential 缺失失敗;本輪沒有要求或保存 secret。 +- 內網 Gitea branch `codex/iwooos-wazuh-boundary-guard-20260624` 已建立,readback HEAD `3d173712f3b20e21e96f99d2ebdc737f6f84438b`。 +- `main` 未被本輪更新;production Wazuh readback 仍為 `predeploy_404_observed`,不得視為部署完成。 + **完成度**: - 現場只讀鑑別:`70%`。 - 真正 agent registry 驗收:`0%`。