feat(awooop): mirror AI alert card metadata

This commit is contained in:
ogt
2026-06-25 09:20:14 +08:00
parent 0bea34efda
commit dc91dc76e4
4 changed files with 121 additions and 1 deletions

View File

@@ -154,6 +154,9 @@ _BEARER_RE = re.compile(r"(?i)\bBearer\s+[A-Za-z0-9._~+/=-]+")
_AI_SIGNAL_TARGET_LABEL_RE = re.compile(
r"\b(?:host|target|service|domain|agent\.name|node|route)\s*=\s*\"?(?P<target>[A-Za-z0-9_.:-]+)\"?"
)
_ALERT_CARD_CODE_VALUE_RE = re.compile(
r"(?P<label>事件類型|Target|Lane)<code>(?P<value>[^<]+)</code>"
)
@dataclass(frozen=True)
@@ -2058,9 +2061,45 @@ def _reply_markup_domain_source_refs(payload: dict) -> dict[str, list[str]]:
return {key: values[:20] for key, values in refs.items() if values}
def _ai_automation_alert_card_metadata(text: str) -> dict[str, object] | None:
"""Extract safe AwoooP mirror metadata from normalized AI alert cards."""
if "ai_automation_alert_card_v1" not in text:
return None
values: dict[str, str] = {}
for match in _ALERT_CARD_CODE_VALUE_RE.finditer(text):
values[match.group("label")] = html.unescape(match.group("value")).strip()
event_type = values.get("事件類型", "")
lane = values.get("Lane", "")
if not event_type or not lane:
return None
gates: list[str] = []
if "candidate_only" in text:
gates.append("candidate_only")
if "runtime_write_gate=0" in text:
gates.append("runtime_write_gate=0")
metadata: dict[str, object] = {
"schema_version": "ai_automation_alert_card_mirror_v1",
"card_schema": "ai_automation_alert_card_v1",
"event_type": event_type,
"lane": lane,
"target": values.get("Target", ""),
"gates": gates,
"candidate_only": "candidate_only" in gates,
"runtime_write_gate_count": 0,
"delivery_receipt_readback_required": True,
"mirror_source": "legacy_telegram_gateway_outbound_message",
}
return metadata
def _outbound_source_envelope(method: str, payload: dict) -> dict[str, object]:
"""Build a redaction-friendly source envelope for Channel Hub replay."""
text = str(payload.get("text") or payload.get("caption") or "")
alert_card_metadata = _ai_automation_alert_card_metadata(text)
incident_ids = sorted(
set(_INCIDENT_ID_RE.findall(text))
| set(_reply_markup_incident_ids(payload))
@@ -2080,8 +2119,23 @@ def _outbound_source_envelope(method: str, payload: dict) -> dict[str, object]:
if key == "event_ids":
continue
source_refs[key] = values[:20]
if alert_card_metadata:
_append_source_ref(
source_refs,
"alert_ids",
alert_card_metadata.get("event_type"),
)
_append_source_ref(
source_refs,
"fingerprints",
(
"ai_automation_alert_card:"
f"{alert_card_metadata.get('event_type')}:"
f"{alert_card_metadata.get('lane')}"
),
)
return {
envelope: dict[str, object] = {
"adapter": "legacy_telegram_gateway",
"method": method,
"payload_sha256": _outbound_payload_hash(payload),
@@ -2092,6 +2146,9 @@ def _outbound_source_envelope(method: str, payload: dict) -> dict[str, object]:
"reply_markup": _reply_markup_summary(payload),
"source_refs": source_refs,
}
if alert_card_metadata:
envelope["ai_automation_alert_card"] = alert_card_metadata
return envelope
def _callback_reply_source_envelope_extra(

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
from src.services.telegram_gateway import (
format_aiops_signal_alert_card,
_outbound_source_envelope,
_sanitize_telegram_error,
)
@@ -139,3 +140,39 @@ def test_outbound_source_envelope_reads_ai_advisory_refs_without_raw_callback()
"ai_advisory:coverage_gap:auto_rule_creation"
]
assert "ai_advisory_handled:coverage_gap:auto_rule_creation" not in str(envelope)
def test_outbound_source_envelope_marks_wazuh_ai_alert_card_for_awooop_readback() -> None:
raw_alert = """
wazuh_dashboard_api_readback_degraded dashboard agent list disappeared
POST /api/check-stored-api status=429 POST /api/check-api status=500
https://127.0.0.1:55000 is unreachable manager registry readback blocked
full_log=/var/ossec/logs/alerts/alerts.json Authorization: Bearer abcdefghijklmnopqrstuvwxyz
"""
card = format_aiops_signal_alert_card(raw_alert)
payload = {
"chat_id": "-100123",
"text": card,
"parse_mode": "HTML",
}
envelope = _outbound_source_envelope("sendMessage", payload)
card_metadata = envelope["ai_automation_alert_card"]
assert card_metadata["schema_version"] == "ai_automation_alert_card_mirror_v1"
assert card_metadata["card_schema"] == "ai_automation_alert_card_v1"
assert card_metadata["event_type"] == "wazuh_dashboard_api_readback_degraded"
assert card_metadata["lane"] == "siem_observability_readback_degraded"
assert card_metadata["target"] == "wazuh_dashboard_api"
assert card_metadata["gates"] == ["candidate_only", "runtime_write_gate=0"]
assert card_metadata["runtime_write_gate_count"] == 0
assert card_metadata["delivery_receipt_readback_required"] is True
assert envelope["source_refs"]["alert_ids"] == [
"wazuh_dashboard_api_readback_degraded"
]
assert envelope["source_refs"]["fingerprints"] == [
"ai_automation_alert_card:wazuh_dashboard_api_readback_degraded:siem_observability_readback_degraded"
]
assert "127.0.0.1:55000" not in str(envelope)
assert "/var/ossec" not in str(envelope)
assert "abcdefghijkl" not in str(envelope)

View File

@@ -1,3 +1,28 @@
## 2026-06-25AwoooP mirror 事件卡 metadata 讀回基礎
**背景**Wazuh Dashboard/API 讀回退化事件卡已能把 raw 429/500 轉成 `ai_automation_alert_card_v1`,但若 AwoooP outbound mirror 只保存一般文字,後續 delivery receipt 與 timeline 仍難以用結構化方式查詢 Wazuh lane / gate。
**完成**
- `TelegramGateway._outbound_source_envelope()` 會在 normalized 事件卡送出後,補 `ai_automation_alert_card_mirror_v1` metadata。
- metadata 僅保存 `event_type``lane``target``gates``runtime_write_gate_count=0``delivery_receipt_readback_required=true` 與 mirror source。
- `source_refs.alert_ids``source_refs.fingerprints` 會加入事件卡索引,讓 AwoooP timeline / delivery receipt 後續可用 structured ref 查 Wazuh 事件。
- 文件同步補上 mirror envelope 規則:不得保存 raw alert、完整 Telegram text、token、內網 URL、主機路徑或 raw Wazuh payload。
**驗證**
- `DATABASE_URL=postgresql://postgres:postgres@localhost:5432/awoooi_test pytest apps/api/tests/test_telegram_gateway_error_sanitizer.py apps/api/tests/test_telegram_message_templates.py -q``79 passed`
- `python3 scripts/security/telegram-alert-readability-guard.py --root .``tests=11 ai_lanes=7 host_lanes=6 runtime_gate=0`
- `python3 scripts/security/security-mirror-progress-guard.py --root .`:通過。
- `python3 -m py_compile apps/api/src/services/telegram_gateway.py`:通過。
- `git diff --check`:通過。
**完成度同步**
- AwoooP mirror 事件卡 metadata source-side`100%`
- Wazuh P0-D delivery receipt readiness`70% -> 82%` source-side、`0%` production receipt。
- SOC / Wazuh no-false-green 納管:`52% -> 56%`
- Telegram 實發、AwoooP live outbound readback、IwoooS production live metadata、Wazuh registry 驗收:仍維持 `0%`
**邊界**:本輪沒有送 Telegram、沒有新增 runtime route、沒有 DB migration、沒有 Wazuh / host / Nginx / Docker / firewall / secret 寫入,沒有把工作視窗對話放進文件或前端。
## 2026-06-25Wazuh Dashboard/API 讀回退化事件卡分支同步
**背景**Wazuh 用戶端消失事故仍不能用 Dashboard 畫面當成 registry truth。本段只同步上一段 source-side 事件卡與 no-false-green guard 的 Gitea 分支狀態,避免另一個工作視窗重複修改或以舊分支為準。

View File

@@ -162,6 +162,7 @@ Live 2026-05-12 evidence shows this gate is not yet green:
- `append_incident_update()` 對同一 incident 的相同狀態做 5 分鐘 Redis 去重。
- `append_incident_update()` 對相同的「AI 自動修復失敗 / AI 診斷工具失敗」摘要增加 10 分鐘跨 incident 去重;每個 incident 仍會移除原卡危險按鈕,但 Telegram 不再重複 reply 同一個失敗摘要。
- `TelegramGateway._send_request()` 對成功送出的 legacy `sendMessage` 增加 AwoooP `awooop_outbound_message` 鏡像。鏡像失敗只記錄 `telegram_outbound_mirror_failed`,不能影響 Telegram 正常送達。
- `ai_automation_alert_card_v1` 送出後AwoooP mirror envelope 需帶 `ai_automation_alert_card_mirror_v1` metadata至少包含 `event_type``lane``target``gates``runtime_write_gate_count=0``delivery_receipt_readback_required=true`;不得把 raw alert、完整 Telegram text、token、內網 URL、主機路徑或 raw Wazuh payload 放進 envelope。
- 成本告警、審批執行結果、自愈 rollback 提案已由 direct Bot API 改走 `TelegramGateway._send_request()`,避免繞過 outbound mirror。
- `telegram_gateway.py` 內部歷史直打 `sendMessage` 路徑已收斂;多 Bot `_send_as_bot()` 因需指定 token 保留 direct HTTP但成功後同樣鏡像到 `awooop_outbound_message`
- 既有 `詳情 / 重診 / 歷史` 按鈕保留,讓 Telegram 保持輕量,細節回到控制台。