diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 1c279ae1..c6424b87 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -72,9 +72,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 變更。 @@ -85,7 +87,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 195b17b5..6f2440e2 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 00000000..09203f32 --- /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 23a7d8d9..15d8463b 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 00000000..37418fc5 --- /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()