昨天我們成功透過 Webhook,讓 Wazuh Server 能夠在觸發告警時,主動對我們的 FastAPI 接收站發送 HTTP POST 請求。
看著終端機印出 200 OK 固然令人興奮,但目前的程式碼只是單純印出一句「收到資料」,我們其實根本不知道到底發生了什麼事。Wazuh 傳遞過來的資料是一大包 JSON 格式的 Payload,裡面包含了時間、主機名稱、觸發規則、甚至詳細的 CVE 漏洞資訊。
今天,我們就要來實作「資料解析(Data Parsing)」,就像是在海量的帳本中精準抓出我們要的稽核數字一樣,我們要把這包 JSON 拆開,萃取出真正有價值的資安情報。
在寫程式之前,我們必須先了解 Wazuh 傳過來的 JSON 長什麼樣子。Wazuh 的告警結構是一層包一層的巢狀字典(Nested Dictionary),以下是擷取了重要欄位的結構範例:
{
"timestamp": "2026-09-11T15:00:00.000+0800",
"rule": {
"level": 7,
"description": "Vulnerability severity: High",
"id": "23505"
},
"agent": {
"id": "001",
"name": "LAPTOP-3007V8UH",
"ip": "192.168.1.100"
},
"data": {
"vulnerability": {
"cve": "CVE-2023-12345",
"severity": "High",
"package": {
"name": "python3",
"version": "3.10.6"
}
}
}
}
可以觀察到,我們最關心的資訊分散在三個主要區塊中:
了解結構後,我們回到 main.py,把原本那個只會印出「叮咚」的路由重新改寫。
在處理這類巢狀 JSON 時,最忌諱直接使用中括號 payload["rule"]["level"] 來取值。因為資安告警的種類非常多,並不是每一次的告警都一定包含 vulnerability 欄位(例如有人密碼輸入錯誤,就不會有 CVE 資料)。如果直接存取不存在的 Key,程式就會直接拋出 KeyError 導致伺服器崩潰。
專業的寫法:善用 .get() 方法
在 Python 的字典操作中,使用 .get("key", default_value) 可以在找不到該鍵值時回傳預設值,確保程式的強健性(Robustness)。
請將 main.py 內的路由改寫為以下程式碼:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any
import logging
import uvicorn
# ==========================================
# 1. 初始化日誌系統 (取代傳統 print)
# ==========================================
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)-7s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
logger = logging.getLogger("Wazuh-Webhook")
# ==========================================
# 2. 定義 Pydantic 資料模型 (Data Models)
# ==========================================
# 預先定義 Wazuh 傳來的 JSON 結構,FastAPI 會自動進行反序列化與型別驗證
class RuleModel(BaseModel):
level: int = Field(default=0, description="告警等級")
description: str = Field(default="No description", description="告警描述")
class AgentModel(BaseModel):
name: str = Field(default="Unknown", description="主機名稱")
ip: Optional[str] = Field(default=None, description="主機IP")
class WazuhPayload(BaseModel):
rule: RuleModel
agent: AgentModel
# 針對動態欄位 (如 vulnerability),保留彈性的 Dict 結構
data: Optional[Dict[str, Any]] = None
# ==========================================
# 3. 建立 FastAPI 實體與路由
# ==========================================
app = FastAPI(title="SecOps Backend API", version="1.0.0")
@app.post("/webhook/wazuh")
async def receive_wazuh_alert(payload: WazuhPayload):
"""
接收 Wazuh 主動推送的 Webhook 告警資料
"""
try:
# 1. 輸出基礎告警日誌 (直接透過 payload.物件屬性 存取)
logger.info(f"[告警接收] 主機: {payload.agent.name} | 等級: {payload.rule.level} | 描述: {payload.rule.description}")
# 2. 針對「漏洞偵測 (Vulnerability)」進行進階解析
if payload.data and "vulnerability" in payload.data:
vuln = payload.data["vulnerability"]
cve_id = vuln.get("cve", "Unknown CVE")
severity = vuln.get("severity", "Unknown")
pkg_name = vuln.get("package", {}).get("name", "Unknown Package")
# 偵測到漏洞,改用 WARNING 等級輸出
logger.warning(f"🚨 [漏洞情報] {cve_id} | 受害套件: {pkg_name} (嚴重度: {severity})")
return {"status": "success", "message": "Alert processed"}
except Exception as e:
# 捕捉未預期錯誤,並回傳 500 狀態碼
logger.error(f"解析失敗: {str(e)}")
raise HTTPException(status_code=500, detail="Internal Server Error")
if __name__ == "__main__":
# 使用字串 "main:app" 取代直接傳入 app,這也是生產環境的標準作法
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
修改完成後,在終端機按下 Ctrl + C 終止伺服器,再重新執行 python main.py 啟動。
只要我們靜靜等待 Wazuh 發送下一波告警,你就會看到原本單調的終端機,現在會輸出結構化、極度清晰的威脅情報:
2026-09-14 15:30:22 | INFO | [SECURITY_EVENT] Agent: LAPTOP-3007V8UH | Rule_Level: 7 | Desc: Vulnerability severity: High
2026-09-14 15:30:22 | WARNING | [CVE_DETECTION] CVE_ID: CVE-2023-38325 | Package: curl | Severity: High
這種格式排除了多餘的視覺干擾,明確切分出時間戳記、日誌等級與事件核心數據。到這裡,我們的 FastAPI 已經正式進化為一個具備強大解析能力的「情報過濾中心」。
目前我們已經成功將資安情報收斂至 Python 的變數中。下一步,我們不能只讓這些資訊留在終端機裡。回想我們在專案初期建立的 n8n 資料夾,明天我們即將引入強大的開源自動化工具 n8n,把這些淬鍊好的資安情報,自動推播到我們的通訊軟體上!