docs(iwooos): guard Wazuh agent visibility incident
This commit is contained in:
@@ -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。
|
||||
|
||||
@@ -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%`,保持關閉。
|
||||
|
||||
133
docs/security/wazuh-agent-visibility-runtime-gate.snapshot.json
Normal file
133
docs/security/wazuh-agent-visibility-runtime-gate.snapshot.json
Normal 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 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"
|
||||
]
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
175
scripts/security/wazuh-agent-visibility-runtime-gate.py
Normal file
175
scripts/security/wazuh-agent-visibility-runtime-gate.py
Normal 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()
|
||||
Reference in New Issue
Block a user