From d4f3953847888f720d1f67b7fd26a244e113ac73 Mon Sep 17 00:00:00 2001 From: ogt Date: Thu, 25 Jun 2026 09:48:19 +0800 Subject: [PATCH] feat(awooop): surface AI alert card delivery readback --- .../web/src/app/[locale]/awooop/runs/page.tsx | 254 ++++++++++++++++++ docs/LOGBOOK.md | 28 ++ ...APPEARANCE-INCIDENT-READBACK-2026-06-24.md | 6 +- 3 files changed, 285 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/[locale]/awooop/runs/page.tsx b/apps/web/src/app/[locale]/awooop/runs/page.tsx index eb2786e55..a9abdea9a 100644 --- a/apps/web/src/app/[locale]/awooop/runs/page.tsx +++ b/apps/web/src/app/[locale]/awooop/runs/page.tsx @@ -538,6 +538,59 @@ interface CallbackRepliesResponse { cache?: OperatorSummaryCacheInfo | null; } +interface AiAlertCardDeliverySummary { + schema_version: string; + project_id: string; + event_type?: string | null; + lane?: string | null; + status: string; + total: number; + sent_total: number; + failed_total: number; + pending_total: number; + shadow_total: number; + delivery_receipt_required_total: number; + runtime_write_gate_open_count: number; + runtime_write_allowed: boolean; + latest_sent_at?: string | null; + latest_queued_at?: string | null; + production_write_count: number; +} + +interface AiAlertCardDeliveryItem { + message_id: string; + run_id: string; + project_id: string; + event_at?: string | null; + channel_type: string; + message_type: string; + send_status: string; + send_error?: string | null; + provider_message_id?: string | null; + triggered_by_state?: string | null; + event_type: string; + lane: string; + target: string; + gates: string[]; + runtime_write_gate_count: number; + runtime_write_allowed: boolean; + candidate_only: boolean; + delivery_receipt_readback_required: boolean; + source_refs: Record; + run_state?: string | null; + agent_id?: string | null; + run_created_at?: string | null; + run_detail_href?: string | null; +} + +interface AiAlertCardsResponse { + items?: AiAlertCardDeliveryItem[]; + total?: number; + page?: number; + per_page?: number; + summary?: AiAlertCardDeliverySummary | null; +} + interface AiRoutePolicyItem { priority: number; provider_name: string; @@ -647,6 +700,8 @@ const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ""; const PER_PAGE = 50; const AUTO_REFRESH_INTERVAL = 30_000; // 30 秒 const INCIDENT_ID_FILTER_RE = /^INC-\d{8}-[A-Z0-9]{4,}$/; +const WAZUH_ALERT_CARD_EVENT_TYPE = "wazuh_dashboard_api_readback_degraded"; +const WAZUH_ALERT_CARD_LANE = "siem_observability_readback_degraded"; const STATE_CONFIG: Record< RunState, @@ -3517,6 +3572,162 @@ function CallbackReplyEvidencePanel({ ); } +function AiAlertCardDeliveryPanel({ + items, + summary, + loading, + error, +}: { + items: AiAlertCardDeliveryItem[]; + summary?: AiAlertCardDeliverySummary | null; + loading: boolean; + error: string | null; +}) { + const latestValue = summary?.latest_sent_at ?? summary?.latest_queued_at; + const latest = latestValue + ? new Date(latestValue).toLocaleTimeString("zh-TW", { + hour: "2-digit", + minute: "2-digit", + }) + : "--"; + const total = summary?.total ?? 0; + const sent = summary?.sent_total ?? 0; + const failed = summary?.failed_total ?? 0; + const pending = summary?.pending_total ?? 0; + const runtimeGateOpen = summary?.runtime_write_gate_open_count ?? 0; + const statusLabel = total === 0 + ? "尚無送達讀回" + : failed > 0 + ? "有送達失敗" + : pending > 0 + ? "等待送達" + : "已有送達讀回"; + const statusClass = failed > 0 + ? "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]" + : total === 0 || pending > 0 + ? "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]" + : "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]"; + const sendStatusLabel = (status: string) => ({ + sent: "已送達", + failed: "失敗", + pending: "等待", + shadow: "影子", + }[status] ?? "未知狀態"); + const laneLabel = (lane: string) => ({ + siem_observability_readback_degraded: "SIEM 觀測讀回退化", + }[lane] ?? "未分類分流"); + const targetLabel = (target: string) => ({ + wazuh_dashboard_api: "Wazuh 儀表板/API 讀回", + }[target] ?? "未分類目標"); + + return ( +
+
+
+
+
+ + {statusLabel} + + + 執行閘門 {runtimeGateOpen} + +
+
+ +
+ {[ + { label: "總數", value: total, detail: `最新 ${latest}` }, + { label: "已送達", value: sent, detail: "Telegram 鏡像已送達" }, + { label: "失敗", value: failed, detail: "需查送達錯誤" }, + { label: "等待", value: pending, detail: `影子 ${summary?.shadow_total ?? 0}` }, + ].map((metric) => ( +
+

{metric.label}

+

{metric.value}

+

{metric.detail}

+
+ ))} +
+ + {error ? ( +
讀回失敗:{error}
+ ) : items.length > 0 ? ( +
+ {items.slice(0, 6).map((item) => { + const eventTime = item.event_at + ? new Date(item.event_at).toLocaleTimeString("zh-TW", { + hour: "2-digit", + minute: "2-digit", + }) + : "--"; + const sourceRefCount = Object.values(item.source_refs ?? {}).reduce((count, value) => { + return count + (Array.isArray(value) ? value.length : 0); + }, 0); + const runHref = item.run_detail_href + ?? `/awooop/runs/${item.run_id}?project_id=${encodeURIComponent(item.project_id)}`; + + return ( +
+
+
+

+ {item.run_id.slice(0, 8)} +

+

+ {publicProjectText(item.project_id)} · {eventTime} +

+
+ + {sendStatusLabel(item.send_status)} + +
+
+

分流:{laneLabel(item.lane)}

+

目標:{targetLabel(item.target)}

+

證據索引:{sourceRefCount}

+

執行閘門:{item.runtime_write_gate_count}

+
+ +
+ ); + })} +
+ ) : loading ? ( +
讀取事件卡送達讀回中...
+ ) : ( +
+ 尚未觀測到 Wazuh 儀表板/API 讀回退化事件卡送達。 +
+ )} +
+ ); +} + function aiRouteHealthLabelKey(status?: string | null) { if ( status === "healthy" || @@ -3981,6 +4192,10 @@ export default function RunsPage() { const [callbackEventsLoading, setCallbackEventsLoading] = useState(true); const [callbackCacheInfo, setCallbackCacheInfo] = useState(null); const [callbackEventsError, setCallbackEventsError] = useState(null); + const [aiAlertCardItems, setAiAlertCardItems] = useState([]); + const [aiAlertCardSummary, setAiAlertCardSummary] = useState(null); + const [aiAlertCardLoading, setAiAlertCardLoading] = useState(true); + const [aiAlertCardError, setAiAlertCardError] = useState(null); const [aiRouteStatus, setAiRouteStatus] = useState(null); const [aiRouteStatusError, setAiRouteStatusError] = useState(null); const [automationQualitySummary, setAutomationQualitySummary] = @@ -4159,6 +4374,37 @@ export default function RunsPage() { setCallbackEventsLoading(false); } + const aiAlertParams = new URLSearchParams(); + aiAlertParams.set("project_id", projectFilter || "awoooi"); + aiAlertParams.set("event_type", WAZUH_ALERT_CARD_EVENT_TYPE); + aiAlertParams.set("lane", WAZUH_ALERT_CARD_LANE); + aiAlertParams.set("per_page", "6"); + if (options?.refresh) { + aiAlertParams.set("refresh", "true"); + } + setAiAlertCardLoading(true); + try { + const aiAlertRes = await fetch( + `${API_BASE}/api/v1/platform/runs/ai-alert-cards?${aiAlertParams.toString()}` + ); + if (aiAlertRes.ok) { + const aiAlertData: AiAlertCardsResponse = await aiAlertRes.json(); + setAiAlertCardItems(Array.isArray(aiAlertData.items) ? aiAlertData.items : []); + setAiAlertCardSummary(aiAlertData.summary ?? null); + setAiAlertCardError(null); + } else { + setAiAlertCardItems([]); + setAiAlertCardSummary(null); + setAiAlertCardError(`端點尚未部署或暫時不可用(HTTP ${aiAlertRes.status})`); + } + } catch (aiAlertError) { + setAiAlertCardItems([]); + setAiAlertCardSummary(null); + setAiAlertCardError("無法讀回 AI 事件卡送達狀態"); + } finally { + setAiAlertCardLoading(false); + } + try { const qualityParams = new URLSearchParams(); qualityParams.set("project_id", projectFilter || "awoooi"); @@ -4189,6 +4435,7 @@ export default function RunsPage() { } catch (err) { setError(err instanceof Error ? err.message : "載入失敗"); setCallbackEventsLoading(false); + setAiAlertCardLoading(false); } finally { setLoading(false); } @@ -4423,6 +4670,13 @@ export default function RunsPage() { + + 92%` source-side、`0%` production receipt。 +- SOC / Wazuh no-false-green 納管:`60% -> 63%`。 +- production deploy、production live outbound readback、IwoooS 前台整合、Wazuh manager registry 驗收、Dashboard stored API 修復:仍維持 `0%`。 + +**邊界**:本輪沒有送 Telegram、沒有 DB migration、沒有 runtime deploy、沒有 Wazuh / 112 / host / Nginx / Docker / firewall / secret 寫入,也沒有 active scan;本地 smoke 只驗證 source-side render。 + ## 2026-06-25|AwoooP AI 事件卡 delivery readback API **背景**:上一段已讓 Telegram outbound mirror 保存 `ai_automation_alert_card_mirror_v1` metadata,但尚未有正式 readback API 可查「Wazuh Dashboard/API 讀回退化事件卡是否已送出、是否失敗、是否仍只停在 source-side」。本輪補只讀 API contract,不實發 Telegram、不修改 Wazuh、不寫 incident。 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 154c50bbf..616ff48f3 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 @@ -73,7 +73,7 @@ | 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;本輪已新增 `wazuh_dashboard_api_readback_degraded` formatter / test / guard 與 AwoooP `/runs/ai-alert-cards` delivery readback contract | `88%` source-side、`0%` production receipt | +| P0-D | Wazuh agent disappearance alert card | 產出 `ai_automation_alert_card_v1`,包含 agent count delta、Dashboard API status、manager health、next gate、owner;本輪已新增 `wazuh_dashboard_api_readback_degraded` formatter / test / guard、AwoooP `/runs/ai-alert-cards` delivery readback contract 與 Runs 前台面板 | `92%` source-side、`0%` production receipt | | 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%` | @@ -84,7 +84,7 @@ 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 事件卡正式 readback:source-side formatter 已能把 Dashboard/API mismatch 分類為 `wazuh_dashboard_api_readback_degraded`,AwoooP 已有 `/api/v1/platform/runs/ai-alert-cards` 只讀 delivery readback contract;下一步需 production deploy、live outbound readback、AwoooP timeline 顯示與 IwoooS 前台 readback。 +4. 補 IwoooS AI 事件卡正式 readback:source-side formatter 已能把 Dashboard/API mismatch 分類為 `wazuh_dashboard_api_readback_degraded`,AwoooP 已有 `/api/v1/platform/runs/ai-alert-cards` 只讀 delivery readback contract 與 Runs 前台面板;下一步需 production deploy、live outbound readback、AwoooP timeline 顯示與 IwoooS 前台 readback。 ## 7. 完成度 @@ -92,5 +92,5 @@ - 真正 agent registry 驗收:`0%`。 - IwoooS live readback production:`0%`。 - Dashboard stored API 修復:`0%`。 -- SOC / Wazuh no-false-green 納管:`60%`。 +- SOC / Wazuh no-false-green 納管:`63%`。 - active response / host write / auto block:`0%`,保持關閉。