Day 24 的 AI Analysis Panel 最下方有一個 Human Decision 區塊,上面寫:AI 分析結果僅供參考,最終風險判斷與處置仍由 SOC 分析人員確認。
不過到目前為止,這只是一段提醒文字。分析人員看完 AI 的分析之後,沒有任何地方可以記錄自己的判斷。這筆事件到底是真的攻擊、還是誤判?有沒有人看過?系統完全不知道。
Day 1 把 AI 定位成 SOC Analyst Assistant,負責協助整理與解釋,而不是取代資安人員做最終判斷。既然最終判斷是人做的,系統就應該要有地方讓人把判斷記錄下來。今天要讓 Human Decision 變成一個真正可以操作的功能。
| Status | 意義 |
|---|---|
| Open | 尚未處理,所有事件的預設狀態 |
| Confirmed | 分析人員確認這是需要處理的資安威脅 |
| False Positive | 分析人員確認這是誤判,例如內部的弱點掃描或正常的管理行為 |
另外加上一個備註欄位,讓分析人員寫下判斷的依據。
False Positive 在 SOC 中很重要。IDS 的規則是依照特徵比對,同樣是 Nmap 掃描,可能是攻擊者,也可能是公司自己的資安人員在做檢測。如果誤判的告警沒有被標記,同樣的告警就會一再出現,消耗分析人員的時間。
這裡有一個設計上的決定:狀態只能由人來修改,AI 的分析結果不會自動改變狀態。這延續了 Day 19 的分工方式,也符合 Prompt 中「不得自動封鎖、停權或隔離」的規則。
修改 main.py
加入狀態的 Enum
class DecisionStatus(str, Enum):
OPEN = "Open"
CONFIRMED = "Confirmed"
FALSE_POSITIVE = "False Positive"
使用 Enum 之後,如果送進來的狀態不是這三種,FastAPI 會直接回傳 422,不會把奇怪的內容寫進資料庫。
在 Day 27 加入 timestamp 欄位的地方,繼續補上兩個欄位
if "status" not in columns:
conn.execute(
"ALTER TABLE events ADD COLUMN status TEXT NOT NULL DEFAULT 'Open'"
)
if "analyst_note" not in columns:
conn.execute(
"ALTER TABLE events ADD COLUMN analyst_note TEXT NOT NULL DEFAULT ''"
)
因為設定了 DEFAULT,原本已經存在的事件會自動變成 Open。
新增 Data Model
class Decision(BaseModel):
status: DecisionStatus
analyst_note: str = ""
新增 PATCH /events/{event_id}/decision
@app.patch("/events/{event_id}/decision")
def update_decision(event_id: int, decision: Decision):
conn = get_db_connection()
cursor = conn.execute("""
UPDATE events
SET status = ?, analyst_note = ?
WHERE id = ?
""", (
decision.status.value,
decision.analyst_note,
event_id
))
conn.commit()
conn.close()
if cursor.rowcount == 0:
return {
"error": "Event not found"
}
return {
"message": "Decision saved",
"event_id": event_id,
"status": decision.status.value,
"analyst_note": decision.analyst_note
}
這裡使用 PATCH 而不是 POST,是因為這個操作是修改既有事件的部分欄位,而不是建立新的資料。
在 build_analysis_response() 的 event 中加入兩個欄位,讓 Panel 可以顯示目前的狀態
"severity": severity_level.value,
"status": event["status"],
"analyst_note": event["analyst_note"]
最後在 GET /events/stats 加入狀態統計
# Human Decision 統計
status_rows = conn.execute("""
SELECT status, COUNT(*) AS count
FROM events
GROUP BY status
""").fetchall()
status_counts = {
row["status"]: row["count"]
for row in status_rows
}
回傳內容加入
"status": {
"open": status_counts.get("Open", 0),
"confirmed": status_counts.get("Confirmed", 0),
"false_positive": status_counts.get("False Positive", 0)
},
新增 State 保存備註內容
const [decisionNote, setDecisionNote] = useState("");
stats 的初始值加入 status
status: {
open: 0,
confirmed: 0,
false_positive: 0,
},
在 analyzeEvent() 取得分析結果時,一起帶入已經儲存的備註
setAiAnalysis(data);
setDecisionNote(data.event.analyst_note);
建立 saveDecision()
// 儲存分析人員的最終判斷
const saveDecision = async (status) => {
try {
const response = await fetch(
`http://127.0.0.1:8000/events/${aiAnalysis.event.id}/decision`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
status: status,
analyst_note: decisionNote,
}),
}
);
const data = await response.json();
setAiAnalysis({
...aiAnalysis,
event: {
...aiAnalysis.event,
status: data.status,
analyst_note: data.analyst_note,
},
});
searchEvents();
fetchStats();
} catch (error) {
console.error("Save decision failed:", error);
}
};
const statusClass = (status) =>
status === "Confirmed"
? "status-confirmed"
: status === "False Positive"
? "status-false-positive"
: "status-open";
儲存成功之後更新 Panel 上顯示的狀態、重新取得事件列表與重新取得統計數字。
在原本四張統計卡片的下方,加入第二排卡片
<div className="cards">
<div className="card">
<h3>Pending Review</h3>
<p>{stats.status.open}</p>
</div>
<div className="card">
<h3>Confirmed</h3>
<p>{stats.status.confirmed}</p>
</div>
<div className="card">
<h3>False Positive</h3>
<p>{stats.status.false_positive}</p>
</div>
</div>
Event Table 在 Severity 後面加入 Status 欄位
<th>Severity</th>
<th>Status</th>
<td>
<span
className={`status-badge ${statusClass(event.status)}`}
>
{event.status}
</span>
</td>
最後修改 Panel 最下方的 Human Decision 區塊
<div className="human-decision">
<div className="decision-header">
<strong>Human Decision</strong>
<span
className={`status-badge ${statusClass(
aiAnalysis.event.status
)}`}
>
{aiAnalysis.event.status}
</span>
</div>
<p>
AI 分析結果僅供參考,
最終風險判斷與處置仍由 SOC 分析人員確認。
</p>
<textarea
placeholder="分析人員備註:判斷依據、已採取的調查或處置..."
value={decisionNote}
onChange={(e) => setDecisionNote(e.target.value)}
/>
<div className="decision-buttons">
<button onClick={() => saveDecision("Confirmed")}>
Confirm Threat
</button>
<button onClick={() => saveDecision("False Positive")}>
False Positive
</button>
<button onClick={() => saveDecision("Open")}>
Keep Open
</button>
</div>
</div>
nano src/App.css 在最下面加入
.status-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
white-space: nowrap;
}
.status-open {
background: #e5e7eb;
color: #374151;
}
.status-confirmed {
background: #fee2e2;
color: #991b1b;
}
.status-false-positive {
background: #dcfce7;
color: #166534;
}
.decision-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.human-decision textarea {
box-sizing: border-box;
width: 100%;
min-height: 80px;
margin-top: 14px;
padding: 10px;
border: 1px solid #cbd5e1;
border-radius: 8px;
font-family: inherit;
font-size: 14px;
}
.decision-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 12px;
}
測試
重新啟動 FastAPI 並重新整理 Dashboard。上方多了一排卡片,目前 36 筆事件全部都是 Pending Review,事件列表也多了 Status 欄位。

選擇 Day 26 分析過的 SQL Injection 事件,查看 AI 分析之後,在備註輸入判斷依據並按下 Confirm Threat。
接著選擇來自內部主機 192.168.3.100 的 Nmap 掃描,備註寫上「內部弱點掃描主機的例行檢測」,按下 False Positive。
回到上方,Pending Review 變成 34,Confirmed 與 False Positive 各為 1,事件列表中的 Status 也同步更新。重新整理頁面之後,狀態與備註仍然保留。

小結
今天讓 Human Decision 從一段提醒文字,變成可以實際操作並保存的功能。
到這裡,一筆事件在系統中有了三層資訊: