diff --git a/apps/api/src/api/v1/platform/operator_runs.py b/apps/api/src/api/v1/platform/operator_runs.py index cbc48c55..c2e34499 100644 --- a/apps/api/src/api/v1/platform/operator_runs.py +++ b/apps/api/src/api/v1/platform/operator_runs.py @@ -43,6 +43,9 @@ from src.services.platform_operator_service import ( from src.services.platform_operator_service import ( list_callback_replies as list_callback_replies_svc, ) +from src.services.platform_operator_service import ( + list_ai_alert_card_delivery_readback as list_ai_alert_card_delivery_readback_svc, +) from src.services.platform_operator_service import ( list_runs as list_runs_svc, ) @@ -112,6 +115,59 @@ class CallbackReplyItem(BaseModel): run_detail_href: str | None = None +class AiAlertCardDeliveryItem(BaseModel): + message_id: UUID + run_id: UUID + project_id: str + event_at: datetime | None = None + channel_type: str + message_type: str + send_status: str + send_error: str | None = None + provider_message_id: str | None = None + triggered_by_state: str | None = None + event_type: str + lane: str + target: str + gates: list[str] + runtime_write_gate_count: int + runtime_write_allowed: bool + candidate_only: bool + delivery_receipt_readback_required: bool + source_refs: dict[str, Any] + run_state: str | None = None + agent_id: str | None = None + run_created_at: datetime | None = None + run_detail_href: str | None = None + + +class AiAlertCardDeliverySummary(BaseModel): + schema_version: str + project_id: str + event_type: str | None = None + lane: str | None = None + status: str + total: int + sent_total: int + failed_total: int + pending_total: int + shadow_total: int + delivery_receipt_required_total: int + runtime_write_gate_open_count: int + runtime_write_allowed: bool + latest_sent_at: datetime | None = None + latest_queued_at: datetime | None = None + production_write_count: int = 0 + + +class ListAiAlertCardsResponse(BaseModel): + items: list[AiAlertCardDeliveryItem] + total: int + page: int + per_page: int + summary: AiAlertCardDeliverySummary + + class OutboundReplyMarkupGapPrefix(BaseModel): prefix: str total: int @@ -331,6 +387,33 @@ async def list_callback_replies( ) +@router.get( + "/runs/ai-alert-cards", + response_model=ListAiAlertCardsResponse, + summary="列出 AI 自動化事件卡送達讀回", + description=( + "從 AwoooP outbound mirror 查詢 ai_automation_alert_card_v1 的" + "結構化送達讀回;只讀,不送 Telegram、不修改 incident、run 或 Wazuh 狀態。" + ), +) +async def list_ai_alert_card_delivery_readback( + project_id: str | None = Query("awoooi", description="租戶 ID"), + event_type: str | None = Query(None, description="事件類型 filter"), + lane: str | None = Query(None, description="AIOps lane filter"), + page: int = Query(1, ge=1, description="頁碼,從 1 開始"), + per_page: int = Query(20, ge=1, le=_MAX_PER_PAGE, description="每頁筆數"), + refresh: bool = Query(False, description="略過短 TTL 快取並重新聚合"), +) -> dict[str, Any]: + return await list_ai_alert_card_delivery_readback_svc( + project_id=project_id, + event_type=event_type, + lane=lane, + page=page, + per_page=per_page, + refresh=refresh, + ) + + @router.get( "/cicd/events", response_model=ListCicdEventsResponse, diff --git a/apps/api/src/services/platform_operator_service.py b/apps/api/src/services/platform_operator_service.py index 9d9eb789..851ac72f 100644 --- a/apps/api/src/services/platform_operator_service.py +++ b/apps/api/src/services/platform_operator_service.py @@ -86,6 +86,9 @@ _ADR100_GATE5_PROJECTION_TRIGGER = "adr100_runtime_replay_gate5" _CALLBACK_REPLY_CACHE_TTL_SECONDS = int( os.getenv("AWOOOP_CALLBACK_REPLY_CACHE_TTL_SECONDS", "20") ) +_AI_ALERT_CARD_CACHE_TTL_SECONDS = int( + os.getenv("AWOOOP_AI_ALERT_CARD_CACHE_TTL_SECONDS", "20") +) _INCIDENT_ID_RE = re.compile(r"\bINC-\d{8}-[A-Z0-9]{4,}\b") _REMEDIATION_STATUS_FILTERS = { "mcp_observed", @@ -1271,6 +1274,247 @@ async def list_callback_replies( ) +async def list_ai_alert_card_delivery_readback( + *, + project_id: str | None = None, + event_type: str | None = None, + lane: str | None = None, + page: int = 1, + per_page: int = 20, + refresh: bool = False, +) -> dict[str, Any]: + """Read-only AwoooP delivery readback for AI automation alert cards.""" + normalized_project_id = project_id or "awoooi" + normalized_event_type = str(event_type or "").strip() + normalized_lane = str(lane or "").strip() + normalized_page = max(int(page or 1), 1) + normalized_per_page = min(max(int(per_page or 20), 1), _MAX_PER_PAGE) + + cache_key = { + "project_id": normalized_project_id, + "event_type": normalized_event_type, + "lane": normalized_lane, + "page": normalized_page, + "per_page": normalized_per_page, + } + if not refresh: + cached_response = await get_cached_operator_summary_async( + "ai_alert_card_delivery_readback", + cache_key, + ttl_seconds=_AI_ALERT_CARD_CACHE_TTL_SECONDS, + ) + if cached_response is not None: + logger.info( + "operator_ai_alert_card_delivery_readback_cache_hit", + project_id=normalized_project_id, + event_type=normalized_event_type, + lane=normalized_lane, + page=normalized_page, + per_page=normalized_per_page, + ttl_seconds=_AI_ALERT_CARD_CACHE_TTL_SECONDS, + ) + return cached_response + + where_clauses = [ + "m.project_id = :project_id", + "m.channel_type = 'telegram'", + "m.source_envelope ? 'ai_automation_alert_card'", + ] + params: dict[str, Any] = { + "project_id": normalized_project_id, + "limit": normalized_per_page, + "offset": (normalized_page - 1) * normalized_per_page, + } + if normalized_event_type: + where_clauses.append( + "m.source_envelope #>> '{ai_automation_alert_card,event_type}' = :event_type" + ) + params["event_type"] = normalized_event_type + if normalized_lane: + where_clauses.append( + "m.source_envelope #>> '{ai_automation_alert_card,lane}' = :lane" + ) + params["lane"] = normalized_lane + + where_sql = " AND ".join(where_clauses) + summary_sql = text(f""" + SELECT + COUNT(*) AS total, + COUNT(*) FILTER (WHERE m.send_status = 'sent') AS sent_total, + COUNT(*) FILTER (WHERE m.send_status = 'failed') AS failed_total, + COUNT(*) FILTER (WHERE m.send_status = 'pending') AS pending_total, + COUNT(*) FILTER (WHERE m.send_status = 'shadow') AS shadow_total, + COUNT(*) FILTER ( + WHERE COALESCE( + m.source_envelope #>> + '{{ai_automation_alert_card,delivery_receipt_readback_required}}', + '' + ) = 'true' + ) AS delivery_receipt_required_total, + COUNT(*) FILTER ( + WHERE COALESCE( + m.source_envelope #>> + '{{ai_automation_alert_card,runtime_write_gate_count}}', + '0' + ) <> '0' + ) AS runtime_write_gate_open_count, + MAX(m.sent_at) AS latest_sent_at, + MAX(m.queued_at) AS latest_queued_at + FROM awooop_outbound_message m + WHERE {where_sql} + """) + list_sql = text(f""" + SELECT + m.message_id, + m.project_id, + m.run_id, + m.channel_type, + m.message_type, + m.provider_message_id, + m.send_status, + m.send_error, + m.queued_at, + m.sent_at, + m.triggered_by_state, + m.source_envelope -> 'ai_automation_alert_card' AS alert_card, + m.source_envelope -> 'source_refs' AS source_refs, + r.agent_id, + r.state AS run_state, + r.created_at AS run_created_at + FROM awooop_outbound_message m + LEFT JOIN awooop_run_state r + ON r.project_id = m.project_id + AND r.run_id = m.run_id + WHERE {where_sql} + ORDER BY COALESCE(m.sent_at, m.queued_at) DESC, m.message_id DESC + LIMIT :limit OFFSET :offset + """) + + async with get_db_context(normalized_project_id) as db: + summary_result = await db.execute(summary_sql, params) + summary_row = summary_result.mappings().first() or {} + rows_result = await db.execute(list_sql, params) + rows = list(rows_result.mappings().all()) + + summary = _ai_alert_card_delivery_summary_from_row( + summary_row, + project_id=normalized_project_id, + event_type=normalized_event_type or None, + lane=normalized_lane or None, + ) + response = { + "items": [_ai_alert_card_delivery_item(row) for row in rows], + "total": summary["total"], + "page": normalized_page, + "per_page": normalized_per_page, + "summary": summary, + } + logger.info( + "operator_ai_alert_card_delivery_readback_fetched", + project_id=normalized_project_id, + event_type=normalized_event_type, + lane=normalized_lane, + page=normalized_page, + per_page=normalized_per_page, + total=summary["total"], + cache_status="miss", + cache_ttl_seconds=_AI_ALERT_CARD_CACHE_TTL_SECONDS, + ) + return await store_operator_summary_async( + "ai_alert_card_delivery_readback", + cache_key, + response, + ttl_seconds=_AI_ALERT_CARD_CACHE_TTL_SECONDS, + ) + + +def _ai_alert_card_delivery_summary_from_row( + row: Mapping[str, Any], + *, + project_id: str, + event_type: str | None, + lane: str | None, +) -> dict[str, Any]: + """Normalize AI alert card delivery summary counts.""" + total = _safe_int(row.get("total")) + sent_total = _safe_int(row.get("sent_total")) + failed_total = _safe_int(row.get("failed_total")) + pending_total = _safe_int(row.get("pending_total")) + shadow_total = _safe_int(row.get("shadow_total")) + runtime_write_gate_open_count = _safe_int( + row.get("runtime_write_gate_open_count") + ) + status_value = "no_delivery_receipt" if total == 0 else "observed" + if failed_total > 0: + status_value = "delivery_failure_observed" + elif pending_total > 0: + status_value = "delivery_pending_observed" + + return { + "schema_version": "awooop_ai_alert_card_delivery_readback_v1", + "project_id": project_id, + "event_type": event_type, + "lane": lane, + "status": status_value, + "total": total, + "sent_total": sent_total, + "failed_total": failed_total, + "pending_total": pending_total, + "shadow_total": shadow_total, + "delivery_receipt_required_total": _safe_int( + row.get("delivery_receipt_required_total") + ), + "runtime_write_gate_open_count": runtime_write_gate_open_count, + "runtime_write_allowed": runtime_write_gate_open_count > 0, + "latest_sent_at": row.get("latest_sent_at"), + "latest_queued_at": row.get("latest_queued_at"), + "production_write_count": 0, + } + + +def _ai_alert_card_delivery_item(row: Mapping[str, Any]) -> dict[str, Any]: + """Convert one AI alert-card outbound mirror row into delivery evidence.""" + alert_card = _as_dict(row.get("alert_card")) + source_refs = _as_dict(row.get("source_refs")) + run_id = row.get("run_id") + project_id = str(row.get("project_id") or "") + runtime_write_gate_count = _safe_int( + alert_card.get("runtime_write_gate_count") + ) + event_at = row.get("sent_at") or row.get("queued_at") + return { + "message_id": row.get("message_id"), + "run_id": run_id, + "project_id": project_id, + "event_at": event_at, + "channel_type": row.get("channel_type"), + "message_type": row.get("message_type"), + "send_status": row.get("send_status"), + "send_error": row.get("send_error"), + "provider_message_id": row.get("provider_message_id"), + "triggered_by_state": row.get("triggered_by_state"), + "event_type": str(alert_card.get("event_type") or ""), + "lane": str(alert_card.get("lane") or ""), + "target": str(alert_card.get("target") or ""), + "gates": alert_card.get("gates") if isinstance(alert_card.get("gates"), list) else [], + "runtime_write_gate_count": runtime_write_gate_count, + "runtime_write_allowed": runtime_write_gate_count > 0, + "candidate_only": bool(alert_card.get("candidate_only")), + "delivery_receipt_readback_required": bool( + alert_card.get("delivery_receipt_readback_required") + ), + "source_refs": source_refs, + "run_state": row.get("run_state"), + "agent_id": row.get("agent_id"), + "run_created_at": row.get("run_created_at"), + "run_detail_href": ( + f"/awooop/runs/{run_id}?project_id={project_id}" + if run_id and project_id + else None + ), + } + + async def _fetch_callback_reply_audit_summary( db: Any, *, diff --git a/apps/api/tests/test_awooop_operator_timeline_labels.py b/apps/api/tests/test_awooop_operator_timeline_labels.py index 4c8fd423..83e17cc8 100644 --- a/apps/api/tests/test_awooop_operator_timeline_labels.py +++ b/apps/api/tests/test_awooop_operator_timeline_labels.py @@ -11,6 +11,7 @@ from fastapi import HTTPException import src.services.platform_operator_service as platform_operator_service from src.api.v1.platform.operator_runs import ( AiRouteStatusResponse, + ListAiAlertCardsResponse, ListApprovalsResponse, ListCallbackRepliesResponse, ListCicdEventsResponse, @@ -31,6 +32,8 @@ from src.services.platform_operator_service import ( _cicd_duration_seconds, _cicd_event_item_from_row, _collect_run_incident_ids, + _ai_alert_card_delivery_item, + _ai_alert_card_delivery_summary_from_row, _is_source_correlation_applied_link, _iter_run_context_batches, _legacy_mcp_timeline_status, @@ -908,6 +911,151 @@ def test_list_callback_replies_response_preserves_callback_evidence() -> None: assert dumped["summary"]["snapshot_status"] == "partial" +def test_ai_alert_card_delivery_item_uses_metadata_without_raw_content() -> None: + run_id = UUID("5c0306e0-591a-5445-9a33-80f499426b38") + message_id = UUID("66cdb6ad-46a4-48f5-9d3b-b1ac9c0b2e92") + item = _ai_alert_card_delivery_item({ + "message_id": message_id, + "run_id": run_id, + "project_id": "awoooi", + "channel_type": "telegram", + "message_type": "final", + "provider_message_id": "13152", + "send_status": "sent", + "send_error": None, + "queued_at": datetime(2026, 6, 25, 9, 40, 0), + "sent_at": datetime(2026, 6, 25, 9, 40, 5), + "triggered_by_state": "legacy_gateway", + "alert_card": { + "schema_version": "ai_automation_alert_card_mirror_v1", + "card_schema": "ai_automation_alert_card_v1", + "event_type": "wazuh_dashboard_api_readback_degraded", + "lane": "siem_observability_readback_degraded", + "target": "wazuh_dashboard_api", + "gates": ["candidate_only", "runtime_write_gate=0"], + "candidate_only": True, + "runtime_write_gate_count": 0, + "delivery_receipt_readback_required": True, + }, + "source_refs": { + "alert_ids": ["wazuh_dashboard_api_readback_degraded"], + "fingerprints": [ + "ai_automation_alert_card:wazuh_dashboard_api_readback_degraded:siem_observability_readback_degraded" + ], + }, + "run_state": "completed", + "agent_id": "legacy-telegram-gateway", + "run_created_at": datetime(2026, 6, 25, 9, 39, 0), + }) + + assert item["event_type"] == "wazuh_dashboard_api_readback_degraded" + assert item["lane"] == "siem_observability_readback_degraded" + assert item["target"] == "wazuh_dashboard_api" + assert item["runtime_write_gate_count"] == 0 + assert item["runtime_write_allowed"] is False + assert item["delivery_receipt_readback_required"] is True + assert item["source_refs"]["alert_ids"] == [ + "wazuh_dashboard_api_readback_degraded" + ] + assert "content_preview" not in item + assert "content_redacted" not in item + assert item["run_detail_href"].endswith("project_id=awoooi") + + +def test_ai_alert_card_delivery_summary_keeps_no_false_green_status() -> None: + summary = _ai_alert_card_delivery_summary_from_row( + { + "total": 2, + "sent_total": 1, + "failed_total": 1, + "pending_total": 0, + "shadow_total": 0, + "delivery_receipt_required_total": 2, + "runtime_write_gate_open_count": 0, + "latest_sent_at": datetime(2026, 6, 25, 9, 40, 5), + "latest_queued_at": datetime(2026, 6, 25, 9, 40, 0), + }, + project_id="awoooi", + event_type="wazuh_dashboard_api_readback_degraded", + lane="siem_observability_readback_degraded", + ) + + assert summary["schema_version"] == "awooop_ai_alert_card_delivery_readback_v1" + assert summary["status"] == "delivery_failure_observed" + assert summary["delivery_receipt_required_total"] == 2 + assert summary["runtime_write_gate_open_count"] == 0 + assert summary["runtime_write_allowed"] is False + assert summary["production_write_count"] == 0 + + +def test_list_ai_alert_cards_response_preserves_delivery_metadata() -> None: + run_id = UUID("5c0306e0-591a-5445-9a33-80f499426b38") + message_id = UUID("66cdb6ad-46a4-48f5-9d3b-b1ac9c0b2e92") + response = ListAiAlertCardsResponse.model_validate({ + "items": [ + { + "message_id": message_id, + "run_id": run_id, + "project_id": "awoooi", + "event_at": datetime(2026, 6, 25, 9, 40, 5), + "channel_type": "telegram", + "message_type": "final", + "send_status": "sent", + "send_error": None, + "provider_message_id": "13152", + "triggered_by_state": "legacy_gateway", + "event_type": "wazuh_dashboard_api_readback_degraded", + "lane": "siem_observability_readback_degraded", + "target": "wazuh_dashboard_api", + "gates": ["candidate_only", "runtime_write_gate=0"], + "runtime_write_gate_count": 0, + "runtime_write_allowed": False, + "candidate_only": True, + "delivery_receipt_readback_required": True, + "source_refs": { + "alert_ids": ["wazuh_dashboard_api_readback_degraded"], + }, + "run_state": "completed", + "agent_id": "legacy-telegram-gateway", + "run_created_at": datetime(2026, 6, 25, 9, 39, 0), + "run_detail_href": ( + "/awooop/runs/5c0306e0-591a-5445-9a33-80f499426b38" + "?project_id=awoooi" + ), + } + ], + "total": 1, + "page": 1, + "per_page": 20, + "summary": { + "schema_version": "awooop_ai_alert_card_delivery_readback_v1", + "project_id": "awoooi", + "event_type": "wazuh_dashboard_api_readback_degraded", + "lane": "siem_observability_readback_degraded", + "status": "observed", + "total": 1, + "sent_total": 1, + "failed_total": 0, + "pending_total": 0, + "shadow_total": 0, + "delivery_receipt_required_total": 1, + "runtime_write_gate_open_count": 0, + "runtime_write_allowed": False, + "latest_sent_at": datetime(2026, 6, 25, 9, 40, 5), + "latest_queued_at": datetime(2026, 6, 25, 9, 40, 0), + "production_write_count": 0, + }, + }) + + dumped = response.model_dump(mode="json") + assert dumped["items"][0]["event_type"] == ( + "wazuh_dashboard_api_readback_degraded" + ) + assert dumped["items"][0]["runtime_write_allowed"] is False + assert dumped["summary"]["delivery_receipt_required_total"] == 1 + assert dumped["summary"]["production_write_count"] == 0 + + def test_list_callback_replies_keeps_audit_summary_separate_from_km_summary() -> None: source = inspect.getsource(platform_operator_service.list_callback_replies) diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index a01d8877..b4f92773 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,28 @@ +## 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。 + +**完成**: +- 新增 `GET /api/v1/platform/runs/ai-alert-cards`,支援 `project_id`、`event_type`、`lane`、`page`、`per_page`、`refresh`。 +- service 層只查 `awooop_outbound_message.source_envelope ? 'ai_automation_alert_card'`,不新增 DB migration。 +- 回傳 `awooop_ai_alert_card_delivery_readback_v1` summary:`sent_total`、`failed_total`、`pending_total`、`shadow_total`、`delivery_receipt_required_total`、`runtime_write_gate_open_count`、`production_write_count=0`。 +- item 只回 metadata、source refs、send status、run ref,不回完整 Telegram text、raw alert、raw Wazuh payload、內網 URL 或主機路徑。 +- AwoooP 通知模型與 Wazuh agent disappearance readback 文件已同步 API 路徑與禁止解讀。 + +**驗證**: +- `DATABASE_URL=postgresql://postgres:postgres@localhost:5432/awoooi_test pytest apps/api/tests/test_awooop_operator_timeline_labels.py apps/api/tests/test_telegram_gateway_error_sanitizer.py apps/api/tests/test_telegram_message_templates.py -q`:`144 passed`。 +- `python3 -m py_compile apps/api/src/services/platform_operator_service.py apps/api/src/api/v1/platform/operator_runs.py apps/api/src/services/telegram_gateway.py`:通過。 +- `python3 scripts/security/security-mirror-progress-guard.py --root .`:通過。 +- `git diff --check`:通過。 + +**完成度同步**: +- AwoooP AI 事件卡 delivery readback API source-side:`100%`。 +- Wazuh P0-D alert card / delivery readiness:`82% -> 88%` source-side、`0%` production receipt。 +- SOC / Wazuh no-false-green 納管:`56% -> 60%`。 +- production deploy、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。 + ## 2026-06-25|AwoooP mirror 事件卡 metadata 讀回基礎 **背景**:Wazuh Dashboard/API 讀回退化事件卡已能把 raw 429/500 轉成 `ai_automation_alert_card_v1`,但若 AwoooP outbound mirror 只保存一般文字,後續 delivery receipt 與 timeline 仍難以用結構化方式查詢 Wazuh lane / gate。 diff --git a/docs/awooop/TELEGRAM-INCIDENT-NOTIFICATION-MODEL.md b/docs/awooop/TELEGRAM-INCIDENT-NOTIFICATION-MODEL.md index f1b5d8d7..e3affb64 100644 --- a/docs/awooop/TELEGRAM-INCIDENT-NOTIFICATION-MODEL.md +++ b/docs/awooop/TELEGRAM-INCIDENT-NOTIFICATION-MODEL.md @@ -163,6 +163,7 @@ Live 2026-05-12 evidence shows this gate is not yet green: - `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。 +- `GET /api/v1/platform/runs/ai-alert-cards` 只讀查詢 `ai_automation_alert_card_v1` delivery readback,可用 `project_id`、`event_type`、`lane` 篩選;此路徑只回 metadata、source refs 與 send status,不回完整 Telegram text,也不代表 Telegram 已實發或 Wazuh registry 已驗收。 - 成本告警、審批執行結果、自愈 rollback 提案已由 direct Bot API 改走 `TelegramGateway._send_request()`,避免繞過 outbound mirror。 - `telegram_gateway.py` 內部歷史直打 `sendMessage` 路徑已收斂;多 Bot `_send_as_bot()` 因需指定 token 保留 direct HTTP,但成功後同樣鏡像到 `awooop_outbound_message`。 - 既有 `詳情 / 重診 / 歷史` 按鈕保留,讓 Telegram 保持輕量,細節回到控制台。 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 501c5a55..154c50bb 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 | `70%` source-side、`0%` delivery 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 | `88%` 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`;下一步需接 delivery receipt、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;下一步需 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 納管:`52%`。 +- SOC / Wazuh no-false-green 納管:`60%`。 - active response / host write / auto block:`0%`,保持關閉。