docs(iwooos): guard Wazuh agent visibility incident

This commit is contained in:
ogt
2026-06-24 23:34:09 +08:00
parent 2eb3b66657
commit 10fbad64cc
5 changed files with 334 additions and 2 deletions

View File

@@ -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。

View File

@@ -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%`,保持關閉。

View File

@@ -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 registryWazuh 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"
]
}

View File

@@ -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")
)

View File

@@ -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()