fix(security): Architecture Review 修復 5 項高信心問題

安全修復 (P0):
1. ssh_provider: 新增 _validate_param() 白名單驗證,防止 command injection
   - container_name/service/filter_name: [a-zA-Z0-9._-]{1,128}
   - compose_dir: 必須以 /opt/ 或 /srv/ 開頭,禁止 ..
   - domain: FQDN 白名單
   - tail/port/lines: int() 轉換 + 上下限夾緊
2. ssh_provider: known_hosts=None 改為讀 SSH_MCP_KNOWN_HOSTS_FILE 環境變數
   - 預設仍 None(內網快速啟動),但啟動時寫入 warning log
   - 設定文件:ops/runbooks/ssh-mcp-setup.md (待補)

模組化修復 (P1):
3. km_conversion_service: 移除 import 時的 ALERT_EVENT_TYPES.update() 副作用
   - ADR-071 event types 移入 alert_operation_log_repository.py 靜態集合
4. telegram_gateway: create_task() 改為 await + try/except
   - 避免 DB session 關閉後的競爭條件
   - KM 轉換失敗記錄 warning log,不中斷主流程
5. km_conversion_service: 新增頂層 try/except,錯誤一律 error log 後 re-raise

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-11 02:50:26 +08:00
parent 0139aa79e7
commit 2af4dffcc6
4 changed files with 85 additions and 25 deletions

View File

@@ -33,14 +33,12 @@ from src.services.knowledge_service import get_knowledge_service
logger = structlog.get_logger(__name__)
# 加入 ADR-071 新 event_type(避免 validation 攔截)
ALERT_EVENT_TYPES.update({
"KM_CONVERTED",
"NOTIFICATION_CLASSIFIED",
"MANUAL_FIX_RECORDED",
"PLAYBOOK_DRAFT_CREATED",
"STATE_GUARD_BLOCKED",
})
# ADR-071 新 event_type 已在 migrations/adr071_notification_lifecycle.sql 的 DB enum 中定義
# 不在此做 runtime ALERT_EVENT_TYPES.update():避免模組副作用污染全局狀態 + 測試隔離失敗
# 若 repository 層有 ALERT_EVENT_TYPES 白名單 validation
# 請在 alert_operation_log_repository.py 的 ALERT_EVENT_TYPES 靜態集合中加入以下值:
# "KM_CONVERTED", "NOTIFICATION_CLASSIFIED", "MANUAL_FIX_RECORDED",
# "PLAYBOOK_DRAFT_CREATED", "STATE_GUARD_BLOCKED"
# 通知類型 → KM 品質等級對應
_TYPE_TO_STATUS = {
@@ -78,11 +76,22 @@ class KMConversionService:
將 Incident 轉換為 KnowledgeEntry
Args:
incident: Incident ORM 物件
incident: Incident ORM 物件(呼叫前確保 signals 已 eager load
Returns:
dict with km_entry_id and quality_level, or None if skipped
"""
try:
return await self._convert_inner(incident)
except Exception as e:
logger.error(
"km_conversion_error",
incident_id=getattr(incident, "incident_id", "unknown"),
error=str(e),
)
raise
async def _convert_inner(self, incident) -> dict | None:
notification_type = getattr(incident, "notification_type", None) or "TYPE-3"
# TYPE-1 不轉 KM

View File

@@ -2731,13 +2731,20 @@ class TelegramGateway:
success=True,
)
# 觸發 KM 轉換(重讀最新 incident
# 觸發 KM 轉換(直接 await避免 create_task() 在 DB session 關閉後的競爭條件
# 重讀 incident 確保 manual_fix_steps 已寫入
incident_updated = await incident_repo.get_by_id(approval.incident_id)
if incident_updated:
from src.services.km_conversion_service import get_km_conversion_service
km_svc = get_km_conversion_service()
import asyncio as _asyncio
_asyncio.create_task(km_svc.convert(incident_updated))
try:
await km_svc.convert(incident_updated)
except Exception as _km_err:
logger.warning(
"km_conversion_failed",
incident_id=approval.incident_id,
error=str(_km_err),
)
# 回覆確認
await self._send_request("sendMessage", {