[安全維運] 002 事件回應 SOP 完整指南:發生資安事件怎麼辦?NIST 四階段流程與 Node.js 實戰範例
「房子住進去之後,最怕的不是牆壁裂開,而是半夜火災時不知道滅火器在哪裡。
系統上線之後也一樣——你不怕出事,怕的是出事之後手忙腳亂、沒有人知道該做什麼。
事件回應不是『希望用不到』的東西,而是『用到時能救命』的東西。」
— SSDLC by 飛飛
一、事件回應是什麼?為什麼每個開發團隊都需要一份 SOP?
想像你住在一棟新蓋好的公寓裡。建商用了防火建材、裝了煙霧偵測器、也有消防灑水系統。這些都是之前在設計和施工階段做好的安全措施。
但有一天,三樓真的冒煙了。
這時候你需要的不是再去研究防火建材的規格,而是:
- 誰負責打 119?
- 住戶要從哪個樓梯逃生?
- 消防栓在幾樓?
- 怎麼確認所有人都安全撤離了?
- 火滅了之後,要怎麼評估損失、修復房屋?
這就是事件回應(Incident Response, IR)——當資安事件真的發生時,你的團隊要按照什麼流程來處理,才能把傷害降到最低、盡快恢復正常。
在 SSDLC 的旅程中,我們已經走過了安全需求(確認要防火)、安全設計(畫好消防通道)、安全實作(裝好偵測器)、安全驗證(測試灑水系統能不能動)。現在我們來到了階段五:部署與維運——房子住進去之後,真的發生狀況要怎麼辦?
為什麼這件事這麼重要?
因為不管你的防禦做得多好,沒有任何系統是 100% 不會被入侵的。就像不管防火建材多好,你還是需要滅火器和逃生計畫。資安界有句名言:
「被入侵不是『會不會』的問題,而是『什麼時候』的問題。」
根據 IBM 的統計,擁有事件回應計畫並定期演練的組織,平均每次資料外洩事件可以節省超過 NT$5,000 萬的損失。不是因為他們不會被攻擊,而是因為他們知道被攻擊後該怎麼做。
二、事件回應的四大階段:用火災應變來理解
事件回應有一個經典的框架,來自 NIST SP 800-61(Computer Security Incident Handling Guide)。它把事件回應分成四個階段,我們繼續用公寓火災的比喻來理解:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 🛡️ 階段一 │───▶│ 🔍 階段二 │───▶│ 🔧 階段三 │───▶│ 📝 階段四 │
│ 準備 │ │ 偵測與分析 │ │ 封鎖、消除 │ │ 事後活動 │
│ Preparation │ │ Detection & │ │ 與復原 │ │ Post- │
│ │ │ Analysis │ │ Containment │ │ Incident │
│ │ │ │ │ Eradication │ │ Activity │
│ │ │ │ │ & Recovery │ │ │
│ 🏠 準備好 │ │ 🏠 發現冒煙 │ │ 🏠 滅火+ │ │ 🏠 檢討+ │
│ 滅火器和 │ │ 確認是火災 │ │ 修復+ │ │ 改進 │
│ 逃生計畫 │ │ 還是燒焦味 │ │ 重新入住 │ │ 防災措施 │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
│ │
└──────────────────────────────────────────────────────────┘
持續改善的循環
這四個階段形成一個循環,每次處理完事件的經驗,都會回饋到準備階段,讓下一次的應變更好。
階段一:準備(Preparation)——滅火器要在火災前就買好
公寓比喻:在火災發生之前,你要做的事:
- 買滅火器放在走廊
- 畫好逃生路線圖貼在電梯旁
- 確認所有住戶都知道逃生出口在哪
- 跟管委會確認消防公司的電話
- 每半年做一次消防演練
在資安領域,準備階段要做這些事:
1. 組建事件回應團隊(CSIRT)
CSIRT(Computer Security Incident Response Team)就是你的「消防隊」。不需要是專職的資安團隊,但每個角色必須明確:
| 角色 | 職責 | 公寓比喻 |
|---|---|---|
| 事件指揮官(Incident Commander) | 統籌整個事件處理流程,做最終決策 | 管委會主委 |
| 技術負責人(Technical Lead) | 帶領技術調查與修復 | 水電師傅 |
| 溝通聯絡人(Communications Lead) | 對內對外溝通,包含通知客戶、主管機關 | 總幹事 |
| 法務/合規(Legal/Compliance) | 確認法規通報義務、保存證據 | 社區法律顧問 |
| 管理層代表(Management) | 核准重大決策(如關閉系統) | 建設公司代表 |
飛飛觀點:
對於 5-10 人的小團隊,不需要每個角色都是不同的人。CTO 可以同時是事件指揮官和技術負責人,PM 可以兼溝通聯絡人。重點是事先指定,而不是事發後才在群組裡問「誰來處理?」
2. 準備工具與資源
就像滅火器要在火災前就放好,以下工具要在事件發生前就準備好:
□ 日誌集中管理系統(如 ELK Stack、Graylog)
□ 系統監控與告警工具(如 Grafana、CloudWatch)
□ 事件追蹤系統(如 Jira、PagerDuty)
□ 安全的溝通管道(不要用可能被入侵的系統來討論事件!)
□ 數位鑑識工具(如 Volatility、Autopsy)
□ 離線備份(確認備份真的可以還原)
□ 聯絡清單(團隊成員手機、外部資安廠商、主管機關聯繫方式)
3. 建立事件分類與分級標準
不是每個告警都是世界末日。你需要一套分級標準,讓團隊知道什麼時候該全員召集、什麼時候看一下就好:
| 等級 | 名稱 | 說明 | 回應時效 | 範例 |
|---|---|---|---|---|
| P1 | 緊急(Critical) | 系統核心功能受損或大規模資料外洩 | 15 分鐘內回應 | 資料庫被拖走、勒索軟體感染 |
| P2 | 高(High) | 部分功能受影響或有確認的入侵跡象 | 1 小時內回應 | 管理後台被未授權存取、異常大量 API 呼叫 |
| P3 | 中(Medium) | 可疑活動但尚未確認影響 | 4 小時內回應 | 多次登入失敗、可疑的掃描行為 |
| P4 | 低(Low) | 資訊性事件,需要記錄但不需緊急處理 | 下一個工作日 | 弱點掃描發現低風險問題 |
4. 定期演練
有逃生計畫但從來不演練,等真的火災來了,大家還是會慌。建議每季做一次桌上演練(Tabletop Exercise)——不需要真的動系統,團隊坐下來,模擬一個資安事件情境,走一遍流程。
演練情境範例(台灣電商場景):
情境:你們的電商平台在週五晚上 10 點收到客戶投訴,
說他的帳號被用來下了一筆 NT$50,000 的訂單,但他本人沒有操作。
同時,監控系統顯示過去 1 小時有超過 500 個帳號從同一個 IP 段嘗試登入,
其中 30 個帳號登入成功。
請討論:
1. 這是什麼等級的事件?
2. 誰應該被通知?
3. 第一步要做什麼?
4. 需要通報主管機關嗎?(提示:個資法)
5. 要不要暫時關閉登入功能?
階段二:偵測與分析(Detection & Analysis)——確認是真的火災,還是只是鄰居在烤肉
公寓比喻:煙霧偵測器響了,你要先搞清楚:
- 是真的失火了嗎?還是有人在廚房燒焦了東西?
- 如果是火災,火源在哪?幾樓?
- 範圍多大?有擴散嗎?
- 有沒有人受傷?
在資安領域,偵測與分析要回答這些問題:
1. 事件來源有哪些?
資安事件的「煙霧偵測器」有很多種:
| 偵測來源 | 說明 | 範例 |
|---|---|---|
| 監控系統告警 | SIEM、IDS/IPS 發出的自動告警 | 「同一 IP 在 5 分鐘內嘗試登入 200 次」 |
| 使用者回報 | 客戶或內部員工通報 | 「我的帳號被盜了」「網站出現奇怪的頁面」 |
| 第三方通知 | 外部資安研究者或合作夥伴告知 | 「在暗網看到你們的客戶資料在賣」 |
| 例行檢查 | 定期的日誌審查或掃描 | 「弱點掃描發現新的 Critical 弱點」 |
| 執法機關通知 | 警察或調查局告知 | 「你們的系統IP出現在犯罪調查中」 |
2. 初步分析:確認事件的真實性與範圍
收到告警後,不要馬上恐慌。先做初步分析:
// 事件初步分析 Checklist(Node.js 日誌查詢範例)
const analyzeIncident = async (alertData) => {
console.log('=== 事件初步分析 ===');
// 1. 確認告警來源是否可信
console.log('📌 告警來源:', alertData.source);
console.log('📌 告警時間:', alertData.timestamp);
// 2. 查詢相關日誌
const relatedLogs = await queryLogs({
timeRange: {
start: new Date(alertData.timestamp - 3600000), // 往前看 1 小時
end: new Date(alertData.timestamp + 1800000), // 往後看 30 分鐘
},
filters: {
sourceIP: alertData.sourceIP,
userId: alertData.userId,
}
});
// 3. 初步判斷
const analysis = {
isConfirmed: false, // 是否確認為真實事件
severity: 'UNKNOWN', // 嚴重等級
affectedSystems: [], // 受影響的系統
affectedUsers: 0, // 受影響的使用者數量
dataExposure: 'UNKNOWN', // 是否有資料外洩
isOngoing: true, // 攻擊是否仍在進行
};
// 4. 記錄分析結果(保留證據!)
await saveToIncidentLog({
incidentId: generateIncidentId(), // IR-2026-001
analyst: 'on-call-engineer',
timestamp: new Date(),
findings: analysis,
rawLogs: relatedLogs,
});
return analysis;
};
3. 事件時間線建立
確認是真實事件後,最重要的事情之一就是建立事件時間線(Timeline)。這就像火災調查員會重建火災的發展過程:
📅 事件時間線範例:帳號大規模盜用事件
2026-02-27 21:30 監控系統偵測到異常登入行為
(同一 IP 段 103.xx.xx.0/24 大量登入嘗試)
2026-02-27 21:45 告警發送至 on-call 工程師
2026-02-27 21:50 工程師確認為 Credential Stuffing 攻擊
(撞庫攻擊,使用外洩的帳密清單嘗試登入)
2026-02-27 22:00 第一位客戶來電投訴帳號被盜
2026-02-27 22:10 初步統計:30 個帳號被成功登入
其中 5 個帳號已被用來下單
2026-02-27 22:15 升級為 P1 事件,通知事件指揮官
2026-02-27 22:20 ▶ 進入階段三:封鎖與消除
飛飛觀點:
時間線是事件回應中最有價值的產出之一。它不只是事後寫報告用的,在事件處理過程中,它能幫助你的團隊保持頭腦清醒——在壓力下,人很容易忘記自己做了什麼、什麼時間做的。養成習慣,每做一個動作就記一筆。
階段三:封鎖、消除與復原(Containment, Eradication & Recovery)——滅火、清理現場、重新入住
這是事件回應中最「動手」的階段。公寓比喻:
- 封鎖(Containment):先阻止火勢蔓延——關閉門窗、隔離起火樓層
- 消除(Eradication):把火撲滅、找到火源並確認完全熄滅
- 復原(Recovery):修復受損區域、確認安全後讓住戶回來
封鎖(Containment):先止血
封鎖的目標是阻止事件繼續擴大。分為短期封鎖和長期封鎖:
| 類型 | 目的 | 時機 | 範例 |
|---|---|---|---|
| 短期封鎖 | 立即止血,減少損害 | 確認事件後立即執行 | 封鎖攻擊來源 IP、停用被入侵帳號 |
| 長期封鎖 | 建立臨時防線,等待根本修復 | 短期封鎖後 | 啟用額外的認證機制、部署 WAF 規則 |
實戰範例:處理 Credential Stuffing 攻擊的封鎖措施
// 短期封鎖措施
const shortTermContainment = async (incidentData) => {
// 1. 封鎖攻擊來源 IP 段
await firewall.blockIPRange('103.xx.xx.0/24');
logAction('封鎖 IP 段 103.xx.xx.0/24');
// 2. 強制登出所有被入侵的帳號
for (const userId of incidentData.compromisedUsers) {
await session.revokeAllSessions(userId);
logAction(<code>強制登出使用者 ${userId} 所有 Session</code>);
}
// 3. 暫時提高登入的 Rate Limit
await rateLimiter.update({
endpoint: '/api/auth/login',
maxAttempts: 3, // 從 10 次降到 3 次
windowMinutes: 15, // 15 分鐘內
blockDuration: 60, // 超過就封鎖 60 分鐘
});
logAction('提高登入 Rate Limit 限制');
// 4. 凍結可疑訂單
for (const orderId of incidentData.suspiciousOrders) {
await orders.freeze(orderId);
logAction(<code class="kb-btn">凍結可疑訂單 ${orderId}</code>);
}
};
// 長期封鎖措施
const longTermContainment = async () => {
// 1. 對所有被入侵帳號強制重設密碼
for (const userId of incidentData.compromisedUsers) {
await auth.forcePasswordReset(userId);
await notification.send(userId, {
subject: '【重要】您的帳號安全通知',
template: 'account-compromised',
});
logAction(<code>強制重設使用者 ${userId} 密碼並發送通知</code>);
}
// 2. 部署 WAF 規則阻擋自動化攻擊
await waf.addRule({
name: 'block-credential-stuffing',
condition: 'login_failure_rate > 5/min per IP',
action: 'CAPTCHA_CHALLENGE',
});
logAction('部署 WAF Credential Stuffing 防護規則');
};
消除(Eradication):把火源徹底清除
封鎖只是「止血」,消除才是「治療」。你要找到根本原因並清除:
消除階段 Checklist:
□ 根本原因已確認
→ 此案例:使用者密碼來自其他平台的外洩資料,且未啟用 MFA
□ 攻擊者的存取路徑已關閉
→ 所有被入侵帳號已重設密碼
□ 攻擊者植入的後門已清除(如有)
→ 檢查是否有新增的 API Key、OAuth App、Webhook
□ 受影響的系統已修補
→ 新增 Credential Stuffing 防護機制
□ 相關的 IOC(Indicators of Compromise)已記錄
→ 攻擊 IP 段、攻擊模式特徵
復原(Recovery):確認安全後重新開放
復原不是「打開開關就好」,而是要逐步恢復、持續監控:
復原步驟:
1. ✅ 確認封鎖和消除措施都已到位
2. ✅ 在測試環境驗證修復方案
3. ✅ 逐步恢復服務(先灰度發布、再全量開放)
4. ✅ 加強監控(至少 72 小時高頻監控)
5. ✅ 確認被入侵帳號的使用者已收到通知
6. ✅ 確認可疑訂單已被正確處理(退款或確認)
飛飛觀點:
復原階段最常犯的錯誤就是「太急著恢復正常」。我見過有團隊在攻擊還沒完全消除的情況下就急著把系統打開,結果攻擊者馬上又進來了。寧可多花幾個小時確認,也不要讓同一個事件發生兩次。
階段四:事後活動(Post-Incident Activity)——火災之後的檢討與改善
火滅了、房子修好了、住戶回來了。但最重要的一步還沒做——從這次事件中學到什麼?
這個階段的核心就是無咎事後檢討會議(Blameless Postmortem)。
事後檢討會議的結構
建議在事件結束後 3-5 個工作天內召開(太早大家還在善後,太晚就忘記細節了):
# 事件回顧報告
## IR-2026-001:電商平台 Credential Stuffing 攻擊事件
### 事件摘要
- 事件類型:Credential Stuffing(撞庫攻擊)
- 發生時間:2026-02-27 21:30 ~ 2026-02-28 02:00
- 持續時間:約 4.5 小時
- 嚴重等級:P1
- 影響範圍:30 個帳號被入侵,5 筆可疑訂單(總金額 NT$127,000)
### 時間線
(從偵測到復原的完整時間線,略)
### 根本原因
使用者在其他平台使用相同的帳號密碼,該平台資料外洩後,
攻擊者利用外洩的帳密清單對我們的登入 API 進行撞庫攻擊。
我們的系統缺乏:
1. 登入行為異常偵測(同 IP 大量登入嘗試未觸發告警)
2. 強制或鼓勵 MFA
3. 已知外洩密碼的比對機制
### 做得好的地方 ✅
- 監控系統在攻擊開始後 15 分鐘內發出告警
- On-call 工程師在 5 分鐘內回應
- 事件時間線記錄完整
- 客服團隊快速回應客戶詢問
### 需要改進的地方 🔧
- 告警規則的閾值太高,應該更早觸發
- 沒有預先準備好的 Credential Stuffing 應變 Playbook
- 封鎖 IP 的流程需要手動操作 AWS Console,耗時太久
- 客戶通知信的範本沒有事先準備好
### 改善行動項目
| 項目 | 負責人 | 預計完成日 | 優先順序 |
|------|--------|-----------|---------|
| 導入 MFA 並在登入頁面推廣 | 前端組 Alice | 2026-03-15 | P1 |
| 建立自動化 IP 封鎖機制 | 後端組 Bob | 2026-03-10 | P1 |
| 整合 HaveIBeenPwned API 檢查密碼是否外洩 | 後端組 Charlie | 2026-03-20 | P2 |
| 調整登入異常偵測的告警閾值 | SRE Diana | 2026-03-08 | P1 |
| 準備客戶通知信範本 | PM Eve | 2026-03-05 | P2 |
| 撰寫 Credential Stuffing Playbook | 全隊 | 2026-03-31 | P2 |
飛飛觀點:
事後檢討會議的「無咎」原則非常重要。不要問「是誰的錯」,要問「是什麼讓這件事發生了」。如果工程師怕被責備,下次發現異常就不敢回報,那才是最大的損失。
三、建立你的事件處理流程:從零到一的實戰指南
了解了四個階段之後,讓我們來建立一份實際可用的事件回應 SOP。
3.1 事件回應流程圖
┌──────────────┐
│ 偵測到告警 │
│ 或接到通報 │
└──────┬───────┘
│
┌──────▼───────┐
│ 初步評估 │
│ 是否為真實 │
│ 安全事件? │
└──────┬───────┘
│
┌────────┴────────┐
│ │
┌─────▼─────┐ ┌─────▼─────┐
│ 是:分級 │ │ 否:記錄 │
│ 並啟動 │ │ 並關閉 │
│ 回應流程 │ │ │
└─────┬─────┘ └───────────┘
│
┌────────┼────────┐
│ │ │
┌────▼───┐ ┌──▼──┐ ┌──▼────┐
│ P1/P2 │ │ P3 │ │ P4 │
│ 立即 │ │ 4hr │ │ 下個 │
│ 召集 │ │ 內 │ │ 工作日 │
│ 團隊 │ │ 回應 │ │ 處理 │
└────┬───┘ └──┬──┘ └──┬────┘
│ │ │
┌────▼────────▼───────▼────┐
│ 封鎖與止血 │
│ • 隔離受影響系統 │
│ • 封鎖攻擊來源 │
│ • 保存證據 │
└────────────┬─────────────┘
│
┌────────────▼─────────────┐
│ 消除與修復 │
│ • 找到根本原因 │
│ • 清除攻擊者存取 │
│ • 修補弱點 │
└────────────┬─────────────┘
│
┌────────────▼─────────────┐
│ 復原與監控 │
│ • 逐步恢復服務 │
│ • 加強監控 72 小時 │
│ • 通知受影響使用者 │
└────────────┬─────────────┘
│
┌────────────▼─────────────┐
│ 事後活動 │
│ • 撰寫事件報告 │
│ • 召開檢討會議 │
│ • 執行改善項目 │
│ • 法規通報(如需要) │
└──────────────────────────┘
3.2 事件回應 Playbook 模板
Playbook 就像是針對特定類型事件的「標準作業程序」。與其在事件發生時才想該做什麼,不如先為常見的攻擊類型寫好劇本。
以下是一個可以直接使用的 Playbook 模板:
# Playbook:[事件類型名稱]
## 適用情境
- 什麼情況下要啟動這份 Playbook
## 嚴重等級判斷
- P1 條件:...
- P2 條件:...
## 初步確認步驟
1. 檢查 [具體的日誌/監控]
2. 確認 [具體的指標]
3. 判斷 [真實性條件]
## 封鎖措施
### 立即執行(前 15 分鐘)
- [ ] 動作一
- [ ] 動作二
### 短期措施(前 1 小時)
- [ ] 動作一
- [ ] 動作二
## 消除步驟
- [ ] 找出根本原因
- [ ] 清除 [具體項目]
## 復原步驟
- [ ] 驗證修復
- [ ] 恢復服務
- [ ] 加強監控
## 通知清單
- [ ] 內部:[誰]
- [ ] 外部:[誰]
- [ ] 主管機關(如適用)
## 證據保存
- [ ] 需要保存的日誌
- [ ] 需要保存的系統快照
3.3 台灣法規通報要求
在台灣,資安事件發生後可能需要通報主管機關。這不是「做好人」,而是法律義務:
| 法規 | 通報對象 | 通報時限 | 適用情況 |
|---|---|---|---|
| 個人資料保護法(2025 年修正) | 個資保護委員會 + 當事人 | 發現後 72 小時內通報機關;查明後 30 日內通知當事人 | 個資外洩事件 |
| 資通安全管理法 | 主管機關 + 上級機關 | 1 小時內通報(依嚴重等級) | 公務機關與特定非公務機關 |
| 上市櫃公司資通安全管控指引 | 證交所/櫃買中心 | 發生後儘速通報 | 上市櫃公司 |
飛飛觀點:
2025 年個資法修正後,個資外洩的通報義務變得更加嚴格。通報不是「示弱」,拖延通報才是真正的風險——不只有行政裁罰,還可能面臨民事求償。及時通報反而能展現組織的負責態度,降低後續的法律風險。
四、事件回應工具箱:實用的 Node.js 輔助工具
以下是幾個你可以整合進系統中的事件回應輔助工具範例:
4.1 安全事件自動告警
// middleware/securityMonitor.js
// 簡易的安全事件偵測中介軟體
const Redis = require('ioredis');
const redis = new Redis();
const ALERT_THRESHOLDS = {
LOGIN_FAILURE: { count: 10, windowSeconds: 300 }, // 5 分鐘內 10 次登入失敗
API_RATE: { count: 100, windowSeconds: 60 }, // 1 分鐘內 100 次 API 請求
SENSITIVE_ACCESS: { count: 5, windowSeconds: 600 }, // 10 分鐘內 5 次敏感資料存取
};
async function trackSecurityEvent(eventType, identifier) {
const key = <code class="kb-btn">security:${eventType}:${identifier}</code>;
const threshold = ALERT_THRESHOLDS[eventType];
if (!threshold) return;
const count = await redis.incr(key);
if (count === 1) {
await redis.expire(key, threshold.windowSeconds);
}
if (count >= threshold.count) {
await triggerAlert({
type: eventType,
identifier,
count,
window: threshold.windowSeconds,
timestamp: new Date().toISOString(),
});
}
}
async function triggerAlert(alertData) {
console.error('[SECURITY ALERT]', JSON.stringify(alertData));
// 發送到 Slack
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: <code>🚨 安全告警:${alertData.type}\n</code> +
<code>來源:${alertData.identifier}\n</code> +
<code>次數:${alertData.count} 次(${alertData.window} 秒內)\n</code> +
<code class="kb-btn">時間:${alertData.timestamp}</code>,
}),
});
// 記錄到事件追蹤系統
await saveIncidentAlert(alertData);
}
module.exports = { trackSecurityEvent };
4.2 證據保存腳本
事件發生時,第一件事就是保存證據。以下腳本可以快速收集關鍵資訊:
// scripts/collectEvidence.js
// 資安事件證據收集腳本
const fs = require('fs');
const { execSync } = require('child_process');
const path = require('path');
async function collectEvidence(incidentId) {
const evidenceDir = path.join('/secure-evidence', incidentId);
fs.mkdirSync(evidenceDir, { recursive: true });
console.log(<code class="kb-btn">📁 證據收集開始:${incidentId}</code>);
console.log(<code class="kb-btn">📂 儲存目錄:${evidenceDir}</code>);
// 1. 收集系統日誌
console.log('📋 收集系統日誌...');
execSync(<code>journalctl --since "24 hours ago" > ${evidenceDir}/system.log</code>);
// 2. 收集應用程式日誌
console.log('📋 收集應用程式日誌...');
execSync(<code>cp /var/log/app/*.log ${evidenceDir}/</code>);
// 3. 收集網路連線狀態
console.log('🌐 收集網路連線狀態...');
execSync(<code>netstat -tlnp > ${evidenceDir}/network-connections.txt</code>);
execSync(<code>ss -tlnp >> ${evidenceDir}/network-connections.txt</code>);
// 4. 收集執行中的程序
console.log('⚙️ 收集程序清單...');
execSync(<code>ps auxf > ${evidenceDir}/processes.txt</code>);
// 5. 收集最近登入記錄
console.log('👤 收集登入記錄...');
execSync(<code>last -50 > ${evidenceDir}/login-history.txt</code>);
execSync(<code>lastb -50 > ${evidenceDir}/failed-logins.txt 2>/dev/null || true</code>);
// 6. 計算所有證據檔案的 Hash(確保證據完整性)
console.log('🔏 計算證據 Hash...');
const files = fs.readdirSync(evidenceDir);
const hashes = {};
for (const file of files) {
const filePath = path.join(evidenceDir, file);
const hash = execSync(<code class="kb-btn">sha256sum ${filePath}</code>).toString().trim();
hashes[file] = hash.split(' ')[0];
}
fs.writeFileSync(
path.join(evidenceDir, 'evidence-hashes.json'),
JSON.stringify(hashes, null, 2)
);
console.log(<code>\n✅ 證據收集完成,共 ${files.length} 個檔案</code>);
console.log(<code class="kb-btn">📂 存放位置:${evidenceDir}</code>);
return evidenceDir;
}
// 使用方式
// node scripts/collectEvidence.js IR-2026-001
const incidentId = process.argv[2] || <code class="kb-btn">IR-${new Date().getFullYear()}-${Date.now()}</code>;
collectEvidence(incidentId);
五、事件回應 SOP 完整模板
以下是一份可以直接用在你的團隊的事件回應 SOP 模板:
# [公司名稱] 資安事件回應標準作業程序(SOP)
## 版本紀錄
| 版本 | 日期 | 修改人 | 修改內容 |
|------|------|--------|---------|
| 1.0 | YYYY-MM-DD | OOO | 初版制定 |
## 一、目的
定義資安事件的處理流程與責任分工,確保事件發生時能迅速、
有效地回應,將損害降到最低。
## 二、適用範圍
適用於本組織所有資訊系統、網路設備與資料資產相關的資安事件。
## 三、事件回應團隊(CSIRT)
| 角色 | 姓名 | 聯繫方式 | 備援人員 |
|------|------|---------|---------|
| 事件指揮官 | | | |
| 技術負責人 | | | |
| 溝通聯絡人 | | | |
| 法務/合規 | | | |
## 四、事件分級
(參考前文的 P1-P4 分級表)
## 五、處理流程
### 5.1 偵測與通報
- 收到告警或通報後,由 on-call 人員進行初步評估
- 判定為真實事件後,依據分級標準通知對應人員
- 建立事件追蹤單(IR-YYYY-NNN)
### 5.2 封鎖與止血
- 依據對應的 Playbook 執行封鎖措施
- 保存所有相關證據
- 記錄所有操作於事件時間線
### 5.3 消除與修復
- 找出根本原因
- 清除攻擊者存取路徑
- 修補相關弱點
### 5.4 復原
- 在測試環境驗證修復方案
- 逐步恢復服務
- 加強監控至少 72 小時
### 5.5 事後活動
- 事件結束後 3-5 個工作天內召開檢討會議
- 撰寫事件報告
- 追蹤改善項目
## 六、法規通報
- 個資外洩:72 小時內通報個資保護委員會
- 資安事件(受資安法管轄者):依等級 1 小時內通報
## 七、文件附錄
- 附錄 A:Playbook 清單
- 附錄 B:聯絡清單
- 附錄 C:證據收集指引
- 附錄 D:事件報告模板
六、團隊落地的務實建議
第一步:先有,再完善
不要試圖一次寫出完美的 SOP。先寫一份「能用」的版本,可能只有一頁——誰是 on-call、出事打什麼電話、最基本的封鎖步驟。有了這個起點,每次處理完事件後再逐步補充。
第二步:從常見攻擊開始寫 Playbook
不用一次寫完所有攻擊類型的 Playbook。建議從以下五個最常見的開始:
□ Playbook 1:帳號被入侵 / Credential Stuffing
□ Playbook 2:DDoS 攻擊
□ Playbook 3:Web 應用程式弱點被利用(SQL Injection、XSS 等)
□ Playbook 4:惡意軟體 / 勒索軟體感染
□ Playbook 5:個資外洩
第三步:每季做一次桌上演練
演練不需要動到真實系統。把團隊拉到一個會議室(或視訊),丟一個情境,走一遍流程。每次演練後記錄發現的問題,更新 SOP。
第四步:把事件回應整合進 on-call 制度
如果你的團隊有 on-call 輪值,把安全事件的回應也納入。on-call 工程師不需要會處理所有事件,但至少要會做初步評估和分級,然後知道該 call 誰。
七、常見問題 FAQ
Q1:我們是小團隊,只有 5 個人,也需要事件回應 SOP 嗎?
A:尤其需要。大公司可能有專職的資安團隊來處理事件,但小團隊發生資安事件時,影響的就是所有人。一份簡單的 SOP(即使只有一頁)可以在壓力最大的時候,幫你的團隊保持冷靜和有序。從一份簡單的聯絡清單和分級標準開始就好。
Q2:我們沒有 SIEM 或 SOC,怎麼偵測事件?
A:不需要昂貴的工具才能開始。你可以從以下低成本方案開始:
- 應用程式日誌:確保你的 Node.js 應用程式記錄了關鍵的安全事件(登入失敗、權限錯誤等)
- 雲端服務內建告警:AWS CloudWatch、GCP Cloud Monitoring、Azure Monitor 都有免費額度
- 開源工具:Grafana + Loki 做日誌監控、Wazuh 做入侵偵測
- 第三方服務:Uptime Robot(免費版本)監控網站可用性
重點不是工具有多厲害,而是你有沒有在看那些日誌。
Q3:事件發生時,要不要先關閉系統?
A:這取決於事件的性質和嚴重程度,沒有標準答案。一般原則:
- 應該關閉:確認有持續的資料外洩、勒索軟體正在擴散、攻擊者仍在系統中且無法即時阻斷
- 不應輕易關閉:關閉會導致證據消失(記憶體中的攻擊痕跡)、影響範圍可以透過隔離控制、關閉的損失大於事件本身的損失
這個決策應該由事件指揮官根據當下情況判斷,不要由工程師獨自決定。
Q4:怎麼跟客戶溝通資安事件?
A:透明、及時、負責任。以下是溝通的三個原則:
- 說你知道的事實:「我們在某月某日偵測到未授權的系統存取」
- 說你正在做什麼:「我們已經封鎖了攻擊來源,並正在全面調查影響範圍」
- 說使用者應該做什麼:「建議您立即更改密碼,並開啟雙因素認證」
- 不要猜測:還沒查清楚的事情不要亂講,「調查仍在進行中」是完全可以接受的說法
八、結語:最好的事件回應,是讓團隊不再害怕事件
很多團隊害怕資安事件,就像很多人害怕火災。但消防隊員不怕火災,不是因為他們覺得火燒不到自己,而是因為他們知道該怎麼做。
事件回應 SOP 的真正價值,不是那份文件本身,而是準備的過程。當你的團隊一起討論「如果 XXX 發生了怎麼辦」,大家就會開始思考如何預防、如何偵測、如何回應。這個過程本身,就是在強化你的安全文化。
記住 NIST SP 800-61 的核心理念:
「事件回應不是在事件發生時才開始,而是在事件發生之前就已經在進行了。」
從今天開始,花 30 分鐘跟你的團隊坐下來,回答這三個問題:
- 如果我們的系統現在被入侵了,第一個被通知的人是誰?
- 他/她知道接下來該做什麼嗎?
- 我們有沒有一份寫下來的流程可以參考?
如果有任何一個答案是「不知道」或「沒有」,那就是你開始建立事件回應 SOP 的最好時機。
安全不是恐懼,而是創造的基礎。 一份好的事件回應計畫,讓你的團隊在面對資安事件時,從「恐慌」變成「有序處理」——這就是專業。
延伸閱讀
官方資源:
- NIST SP 800-61 Rev.2:Computer Security Incident Handling Guide
- NIST CSF 2.0 — RESPOND & RECOVER 功能
- OWASP Incident Response Cheat Sheet
台灣法規:
系列文章:
[安全維運] 001 安全監控與告警:建立你的資安儀表板|關鍵安全指標監控與異常行為偵測實戰指南
「房子交屋後,不是把鑰匙交出去就沒事了。你還需要監視器、煙霧偵測器、和一個 24 小時運作的保全系統。
沒有監控的系統,就像一棟沒裝煙霧偵測器的大樓——火燒起來了,你卻是最後一個知道的人。」
— SSDLC by 飛飛
一、安全監控是什麼?為什麼你的系統需要一個「保全中心」?
在 SSDLC 蓋房子的旅程中,我們已經走過了安全需求定義(確認要防震防火防盜)、安全設計(畫好建築藍圖)、安全實作(用防火建材施工)、安全驗證(請結構技師來驗收)。現在,房子交屋了,住戶搬進去了——我們來到了階段五:部署與維運(Deployment & Operation)。
想像你蓋了一棟設計完善、建材頂級、驗收通過的大樓。但交屋之後呢?如果沒有監視器,有人半夜闖入你不知道;如果沒有煙霧偵測器,五樓起火了你在一樓還在看電視;如果沒有保全系統,小偷偷走東西三天後你才發現。
安全監控,就是你的系統的「保全中心」。 它負責即時觀察系統的一切動態,在異常發生的第一時間發出警報,讓你能夠快速反應,而不是等到使用者打電話來罵、媒體報導了、或是資料已經在暗網上賣了,你才知道出事了。
在資安的世界裡,有一句老話:
「不是『會不會』被攻擊的問題,而是『什麼時候』被攻擊的問題。」
既然攻擊遲早會來,那問題就變成:你能多快發現?發現之後能多快反應? 這就是安全監控要回答的核心問題。
飛飛觀點:
我見過太多團隊把 90% 的資安預算花在「防禦」上——WAF、加密、權限控制,卻只花不到 5% 在「偵測」上。這就像花大錢裝了最頂級的門鎖,卻連監視器都沒有。門鎖遲早會被撬開,但如果你有監視器,至少能在小偷還在翻牆的時候就報警。
二、傳統做法 vs. 現代安全監控:從「事後看 Log」到「即時告警」
先來看看傳統做法和現代安全監控的差別:
| 面向 | 傳統做法 | 現代安全監控 |
|---|---|---|
| 監控方式 | 出事了才去翻 Log 檔 | 即時儀表板 + 自動告警 |
| 發現問題的時機 | 幾天甚至幾週後 | 幾秒到幾分鐘內 |
| 分析方法 | 人工 grep 搜尋 | 自動化規則 + 異常偵測 |
| 告警機制 | 沒有(靠使用者回報) | Slack / Email / PagerDuty 即時通知 |
| 涵蓋範圍 | 只有應用程式 Log | 應用程式 + 基礎設施 + 業務指標 |
| 可視性 | 看不到全貌,像瞎子摸象 | 統一儀表板,一目瞭然 |
用蓋房子的比喻來說:
傳統做法就像大樓管理員只在住戶投訴「我家被偷了」之後才去調監視器畫面——問題是,監視器可能根本沒開,或是錄影帶已經被覆蓋了。
現代安全監控就像有一個 24 小時的保全中心,畫面上顯示每一層樓的即時影像,煙霧偵測器連動消防系統,有人在非上班時間刷卡進入機房,警報立刻響起。
三、安全監控的三大支柱:你需要監控什麼?
安全監控不是把所有 Log 丟進一個資料庫就完事了。你需要有系統地思考「監控什麼」、「怎麼監控」、「發現異常後怎麼辦」。
支柱一:安全日誌(Security Logging)— 記錄發生了什麼
日誌是一切的基礎。沒有日誌,就像犯罪現場沒有監視器畫面——你連「發生了什麼」都說不清楚。
該記錄的事件:
| 事件類別 | 具體內容 | 為什麼重要 |
|---|---|---|
| 認證事件 | 登入成功/失敗、登出、密碼變更、MFA 驗證 | 偵測帳號被盜或暴力破解 |
| 授權事件 | 權限被拒絕的存取嘗試、角色變更、權限提升 | 偵測越權存取或權限提升攻擊 |
| 資料存取 | 敏感資料的查詢、匯出、修改、刪除 | 偵測資料外洩或內部威脅 |
| 系統事件 | 服務啟停、設定變更、部署紀錄 | 偵測未授權的系統修改 |
| 輸入驗證 | 被拒絕的輸入(XSS、SQL Injection 嘗試) | 偵測攻擊探測行為 |
| 業務邏輯 | 異常交易、大額操作、批次操作 | 偵測業務層面的攻擊 |
不該記錄的資料:
// ❌ 絕對不要記錄這些
logger.info('User login', {
username: 'alice',
password: 'MyS3cret!', // ❌ 密碼
creditCard: '4111111111111111', // ❌ 信用卡號
sessionToken: 'eyJhbGciOi...', // ❌ Session Token
idNumber: 'A123456789', // ❌ 身分證字號
});
// ✅ 安全的日誌記錄
logger.info('User login', {
username: 'alice',
status: 'success',
ip: '203.0.113.42',
userAgent: 'Mozilla/5.0...',
timestamp: '2026-02-28T10:30:00Z',
requestId: 'req-abc-123',
});
飛飛觀點:
日誌記錄有兩個極端都不好:記太少,出事了沒東西可查;記太多,敏感資料反而從日誌洩露。我見過一個案例,某公司的日誌裡記了完整的信用卡號,結果日誌伺服器被入侵,攻擊者直接從 Log 裡撈到幾萬筆信用卡資料。日誌本身也是需要保護的資產。
支柱二:關鍵安全指標(Security Metrics)— 衡量系統的健康狀態
光有日誌還不夠,你需要把日誌轉化為可量化的指標,才能知道「現在的狀況是正常的還是異常的」。
就像你去看醫生,醫生不會把你所有的血液成分都列出來,而是看幾個關鍵指標:血壓、血糖、心率。安全監控也是一樣——你需要定義幾個關鍵指標(KPI),放在儀表板上即時監控。
核心安全指標清單:
| 指標名稱 | 計算方式 | 正常基線 | 告警閾值 | 意義 |
|---|---|---|---|---|
| 登入失敗率 | 失敗次數 / 總登入次數 | < 5% | > 15% | 可能正在被暴力破解 |
| 單一 IP 請求數 | 每分鐘某 IP 的請求數 | < 100/min | > 500/min | 可能是 DDoS 或爬蟲 |
| 403 錯誤比例 | 403 回應 / 總回應數 | < 1% | > 5% | 可能有人在嘗試越權存取 |
| 敏感 API 呼叫量 | /api/users/export 等端點的呼叫次數 | < 10/day | > 50/day | 可能有人在大量匯出資料 |
| 新帳號註冊速率 | 每小時新註冊帳號數 | < 20/hr | > 100/hr | 可能是自動化註冊攻擊 |
| 平均回應時間 | API 回應時間的 P95 | < 500ms | > 2000ms | 可能正在被 DoS 攻擊或有效能問題 |
| 異常地理位置登入 | 來自非常用地區的登入次數 | 0 | ≥ 1 | 帳號可能被盜用 |
支柱三:告警與回應(Alerting & Response)— 發現問題後怎麼辦
監控的最終目的不是「看到問題」,而是「解決問題」。一個好的告警系統應該做到:發現異常 → 通知對的人 → 提供足夠的資訊讓人判斷 → 觸發應變流程。
告警分級制度:
| 等級 | 名稱 | 條件範例 | 通知方式 | 回應時間 |
|---|---|---|---|---|
| P1 | 緊急(Critical) | 資料外洩、系統被入侵、服務全面中斷 | 電話 + Slack + PagerDuty | 15 分鐘內 |
| P2 | 嚴重(High) | 大量登入失敗、可疑的資料匯出、DDoS 攻擊 | Slack + Email | 1 小時內 |
| P3 | 中等(Medium) | 異常地理位置登入、單一 IP 高頻請求 | Slack | 4 小時內 |
| P4 | 低(Low) | 少量 403 錯誤、非尖峰時段的管理者登入 | Email(每日彙整) | 下個工作日 |
飛飛觀點:
告警最怕的不是「太少」,而是「太多」。當你的 Slack 頻道每天噴出 500 則告警通知,團隊成員會開始「告警疲勞」——看到告警直接忽略,就像住在消防局旁邊的人聽到警笛聲已經無感了。寧可少但精準,也不要多但雜亂。
四、實戰:用 Node.js 建立安全監控系統
讓我們用一個台灣電商平台的場景,從頭到尾建立一套安全監控系統。
4.1 建立結構化安全日誌
第一步,是建立一個能產出結構化日誌的模組。結構化日誌(Structured Logging)意味著每一筆日誌都是可被程式解析的格式(通常是 JSON),而不是一行文字。
// security-logger.js — 安全日誌模組
const winston = require('winston');
// 定義敏感欄位,自動遮蔽
const SENSITIVE_FIELDS = [
'password', 'creditCard', 'cardNumber', 'cvv',
'token', 'sessionToken', 'apiKey', 'secret',
'idNumber', 'ssn',
];
function redactSensitive(obj) {
if (!obj || typeof obj !== 'object') return obj;
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
for (const key of Object.keys(redacted)) {
if (SENSITIVE_FIELDS.some(f => key.toLowerCase().includes(f.toLowerCase()))) {
redacted[key] = '***REDACTED***';
} else if (typeof redacted[key] === 'object') {
redacted[key] = redactSensitive(redacted[key]);
}
}
return redacted;
}
// 建立安全日誌 Logger
const securityLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }),
winston.format.json()
),
defaultMeta: {
service: 'ecommerce-api',
environment: process.env.NODE_ENV || 'development',
},
transports: [
// 安全事件專用的日誌檔
new winston.transports.File({
filename: 'logs/security.log',
level: 'warn',
}),
// 所有事件的日誌檔
new winston.transports.File({
filename: 'logs/combined.log',
}),
],
});
// 封裝安全事件記錄函式
function logSecurityEvent(eventType, severity, details) {
const safeDetails = redactSensitive(details);
securityLogger.log({
level: severity === 'CRITICAL' ? 'error' : severity === 'HIGH' ? 'warn' : 'info',
eventType,
severity,
...safeDetails,
timestamp: new Date().toISOString(),
});
}
module.exports = { logSecurityEvent, securityLogger };
4.2 在關鍵端點加入安全日誌
// middleware/security-audit.js — 安全稽核 Middleware
const { logSecurityEvent } = require('../security-logger');
// 登入事件記錄
function logLoginAttempt(req, success, userId = null, reason = null) {
logSecurityEvent(
success ? 'AUTH_LOGIN_SUCCESS' : 'AUTH_LOGIN_FAILURE',
success ? 'INFO' : 'MEDIUM',
{
userId,
ip: req.ip,
userAgent: req.get('User-Agent'),
reason,
path: req.path,
method: req.method,
}
);
}
// 敏感操作記錄
function logSensitiveAction(req, action, target, details = {}) {
logSecurityEvent('SENSITIVE_ACTION', 'HIGH', {
userId: req.user?.id,
action, // 例如:'DATA_EXPORT', 'ROLE_CHANGE', 'PASSWORD_RESET'
target, // 例如:'user:12345', 'order:67890'
ip: req.ip,
userAgent: req.get('User-Agent'),
...details,
});
}
// 存取被拒絕記錄
function logAccessDenied(req, resource, reason) {
logSecurityEvent('ACCESS_DENIED', 'MEDIUM', {
userId: req.user?.id,
resource,
reason,
ip: req.ip,
userAgent: req.get('User-Agent'),
path: req.originalUrl,
method: req.method,
});
}
module.exports = { logLoginAttempt, logSensitiveAction, logAccessDenied };
4.3 建立異常偵測引擎
接下來,我們建立一個簡單但有效的異常偵測引擎,用來即時分析安全事件並觸發告警。
// anomaly-detector.js — 異常偵測引擎
const Redis = require('ioredis');
const redis = new Redis();
// 異常偵測規則定義
const ANOMALY_RULES = {
// 規則一:暴力破解偵測
BRUTE_FORCE: {
description: '單一帳號短時間內大量登入失敗',
window: 15 * 60, // 15 分鐘視窗
threshold: 5, // 5 次失敗
severity: 'HIGH',
key: (event) => <code class="kb-btn">login_fail:${event.userId || event.ip}</code>,
},
// 規則二:帳號列舉偵測
ACCOUNT_ENUM: {
description: '單一 IP 嘗試大量不同帳號',
window: 10 * 60, // 10 分鐘視窗
threshold: 10, // 10 個不同帳號
severity: 'HIGH',
key: (event) => <code class="kb-btn">account_enum:${event.ip}</code>,
},
// 規則三:異常資料匯出偵測
DATA_EXFILTRATION: {
description: '短時間內大量匯出敏感資料',
window: 60 * 60, // 1 小時視窗
threshold: 5, // 5 次匯出
severity: 'CRITICAL',
key: (event) => <code class="kb-btn">data_export:${event.userId}</code>,
},
// 規則四:非上班時間敏感操作
OFF_HOURS_ACCESS: {
description: '非上班時間存取管理後台',
window: null, // 不需要計數視窗
threshold: 1,
severity: 'MEDIUM',
check: (event) => {
const hour = new Date().getHours(); // 使用台灣時間 (UTC+8)
return (hour < 7 || hour > 22) && event.path?.includes('/admin');
},
},
// 規則五:速率異常偵測
RATE_ANOMALY: {
description: '單一 IP 請求頻率異常',
window: 60, // 1 分鐘視窗
threshold: 500, // 500 次請求
severity: 'HIGH',
key: (event) => <code class="kb-btn">rate:${event.ip}</code>,
},
};
async function checkAnomaly(event) {
const alerts = [];
for (const [ruleName, rule] of Object.entries(ANOMALY_RULES)) {
// 特殊條件檢查(如非上班時間)
if (rule.check && rule.check(event)) {
alerts.push({
rule: ruleName,
description: rule.description,
severity: rule.severity,
event,
});
continue;
}
// 計數型規則
if (rule.key) {
const redisKey = rule.key(event);
if (!redisKey) continue;
const count = await redis.incr(redisKey);
if (count === 1) {
await redis.expire(redisKey, rule.window);
}
if (count >= rule.threshold) {
alerts.push({
rule: ruleName,
description: rule.description,
severity: rule.severity,
count,
threshold: rule.threshold,
event,
});
}
}
}
return alerts;
}
module.exports = { checkAnomaly, ANOMALY_RULES };
4.4 建立告警通知系統
// alerter.js — 告警通知系統
const axios = require('axios');
// Slack Webhook 通知
async function sendSlackAlert(alert) {
const color = {
CRITICAL: '#FF0000',
HIGH: '#FF8C00',
MEDIUM: '#FFD700',
LOW: '#36A64F',
}[alert.severity] || '#808080';
const payload = {
attachments: [{
color,
title: <code class="kb-btn">🚨 安全告警 [${alert.severity}]:${alert.rule}</code>,
text: alert.description,
fields: [
{ title: '觸發規則', value: alert.rule, short: true },
{ title: '嚴重等級', value: alert.severity, short: true },
{ title: '來源 IP', value: alert.event?.ip || 'N/A', short: true },
{ title: '相關使用者', value: alert.event?.userId || 'N/A', short: true },
{ title: '計數', value: <code class="kb-btn">${alert.count || 1} / ${alert.threshold || 1}</code>, short: true },
{ title: '時間', value: new Date().toLocaleString('zh-TW', { timeZone: 'Asia/Taipei' }), short: true },
],
footer: 'SSDLC Security Monitor',
ts: Math.floor(Date.now() / 1000),
}],
};
try {
await axios.post(process.env.SLACK_WEBHOOK_URL, payload);
} catch (error) {
console.error('Slack 通知發送失敗:', error.message);
}
}
// Email 通知(用於 P1 級別)
async function sendEmailAlert(alert) {
// 使用 nodemailer 或其他 Email 服務
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: 587,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
await transporter.sendMail({
from: '"Security Monitor" <security@yourcompany.com>',
to: process.env.SECURITY_TEAM_EMAIL,
subject: <code class="kb-btn">🚨 [${alert.severity}] 安全告警:${alert.rule}</code>,
html: `
<h2>安全告警通知</h2>
<p><strong>規則:</strong>${alert.rule}</p>
<p><strong>描述:</strong>${alert.description}</p>
<p><strong>嚴重等級:</strong>${alert.severity}</p>
<p><strong>來源 IP:</strong>${alert.event?.ip || 'N/A'}</p>
<p><strong>使用者:</strong>${alert.event?.userId || 'N/A'}</p>
<p><strong>時間:</strong>${new Date().toLocaleString('zh-TW')}</p>
<hr>
<p>請立即處理此安全事件。</p>
`,
});
}
// 統一告警分派
async function dispatchAlert(alert) {
switch (alert.severity) {
case 'CRITICAL':
await sendSlackAlert(alert);
await sendEmailAlert(alert);
// P1 級別也可以整合 PagerDuty
break;
case 'HIGH':
await sendSlackAlert(alert);
await sendEmailAlert(alert);
break;
case 'MEDIUM':
await sendSlackAlert(alert);
break;
case 'LOW':
// 低級別不即時通知,累積到每日報告
break;
}
}
module.exports = { dispatchAlert, sendSlackAlert };
4.5 整合到 Express 應用程式
把前面的模組整合到你的 Express 應用程式中:
// app.js — 整合安全監控
const express = require('express');
const { logLoginAttempt, logSensitiveAction, logAccessDenied } = require('./middleware/security-audit');
const { checkAnomaly } = require('./anomaly-detector');
const { dispatchAlert } = require('./alerter');
const app = express();
// ===== 全域安全監控 Middleware =====
app.use(async (req, res, next) => {
// 記錄請求開始時間
req.startTime = Date.now();
// 每個請求都做速率異常檢查
const alerts = await checkAnomaly({
ip: req.ip,
path: req.path,
method: req.method,
userId: req.user?.id,
});
// 如果觸發告警,立即通知
for (const alert of alerts) {
await dispatchAlert(alert);
}
// 記錄回應狀態
res.on('finish', () => {
if (res.statusCode === 403) {
logAccessDenied(req, req.originalUrl, 'HTTP 403 Forbidden');
}
});
next();
});
// ===== 登入端點 =====
app.post('/api/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await authenticate(email, password);
if (!user) {
// 記錄登入失敗
logLoginAttempt(req, false, null, 'INVALID_CREDENTIALS');
// 檢查暴力破解
const alerts = await checkAnomaly({
ip: req.ip,
userId: email,
eventType: 'LOGIN_FAILURE',
});
for (const alert of alerts) {
await dispatchAlert(alert);
}
return res.status(401).json({ error: '帳號或密碼錯誤' });
}
// 記錄登入成功
logLoginAttempt(req, true, user.id);
// 檢查異常登入(例如新 IP、新裝置)
const isNewLocation = await checkNewLoginLocation(user.id, req.ip);
if (isNewLocation) {
logSecurityEvent('NEW_LOGIN_LOCATION', 'MEDIUM', {
userId: user.id,
ip: req.ip,
country: await geolocate(req.ip),
});
}
const token = generateToken(user);
res.json({ token });
} catch (error) {
logSecurityEvent('AUTH_ERROR', 'HIGH', { error: error.message });
res.status(500).json({ error: '系統錯誤,請稍後再試' });
}
});
// ===== 敏感操作端點:會員資料匯出 =====
app.get('/api/admin/users/export', authorize('admin'), async (req, res) => {
// 記錄敏感操作
logSensitiveAction(req, 'DATA_EXPORT', 'users', {
format: req.query.format,
filters: req.query.filters,
});
// 檢查異常匯出行為
const alerts = await checkAnomaly({
userId: req.user.id,
eventType: 'DATA_EXPORT',
ip: req.ip,
});
for (const alert of alerts) {
await dispatchAlert(alert);
}
// 執行匯出邏輯...
});
五、建立你的資安儀表板:該看什麼?
一個好的資安儀表板,不是把所有圖表都塞上去,而是讓你「一眼就能判斷系統是否安全」。就像汽車儀表板——你不需要看到引擎每個零件的溫度,你只需要看到速度、油量、和警示燈。
5.1 儀表板架構建議
┌─────────────────────────────────────────────────────┐
│ 🔒 資安儀表板 — 總覽 │
├──────────────────┬──────────────────────────────────┤
│ 即時狀態 │ 過去 24 小時趨勢 │
│ ┌──────┐ │ 📈 登入失敗率趨勢圖 │
│ │ 🟢 │ 系統 │ 📈 異常請求趨勢圖 │
│ │ 正常 │ 狀態 │ 📈 API 回應時間趨勢圖 │
│ └──────┘ │ │
│ 待處理告警:3 │ │
│ P1: 0 P2: 1 │ │
│ P3: 2 P4: 5 │ │
├──────────────────┴──────────────────────────────────┤
│ 關鍵指標 │
│ 登入失敗率: 2.3% │ 403 錯誤: 0.4% │ 平均回應: 180ms│
│ 新註冊: 15/hr │ 資料匯出: 3/day │ 異常IP: 0 │
├─────────────────────────────────────────────────────┤
│ 最近安全事件(最新 10 筆) │
│ 10:32 [MEDIUM] 來自新加坡 IP 的管理者登入 │
│ 10:15 [LOW] /admin 在非上班時間被存取 │
│ 09:58 [HIGH] 帳號 user@test.com 登入失敗 5 次 │
│ ... │
└─────────────────────────────────────────────────────┘
5.2 開源工具推薦
如果你的團隊不想從零開始建儀表板,以下是常見的開源方案:
| 工具 | 用途 | 適合場景 | 費用 |
|---|---|---|---|
| ELK Stack(Elasticsearch + Logstash + Kibana) | 日誌收集、搜尋、視覺化 | 中大型團隊,需要強大的搜尋能力 | 開源免費(付費版有額外功能) |
| Grafana + Loki | 日誌視覺化與告警 | 已使用 Prometheus 的團隊 | 開源免費 |
| Wazuh | SIEM(安全資訊與事件管理) | 需要合規要求(如 PCI DSS)的團隊 | 開源免費 |
| OSSEC | 主機入侵偵測系統(HIDS) | 需要監控主機層級活動的場景 | 開源免費 |
給小型團隊的務實建議:
如果你是 3-5 人的小型開發團隊,不需要一開始就搞 ELK Stack。可以先用以下組合:
Winston(Node.js 日誌)
→ 輸出到 JSON 檔案
→ 用 Grafana Loki 收集
→ 在 Grafana 建立儀表板和告警規則
→ 告警發送到 Slack
這個方案簡單、免費、能在一天內搞定,就足以應付大部分小型專案的需求。
六、實戰案例:台灣電商平台的安全監控方案
場景
你的團隊負責一個台灣電商平台,日活躍用戶約 5 萬人,處理信用卡支付和個資。老闆要求你在一個月內建立基本的安全監控。
監控方案設計
第一週:基礎日誌建設
// 定義需要監控的安全事件
const SECURITY_EVENTS = {
// 認證相關
AUTH_LOGIN_SUCCESS: { severity: 'INFO', alert: false },
AUTH_LOGIN_FAILURE: { severity: 'MEDIUM', alert: true },
AUTH_PASSWORD_CHANGE: { severity: 'HIGH', alert: true },
AUTH_MFA_FAILURE: { severity: 'HIGH', alert: true },
// 授權相關
ACCESS_DENIED: { severity: 'MEDIUM', alert: true },
PRIVILEGE_ESCALATION: { severity: 'CRITICAL', alert: true },
// 資料相關
DATA_EXPORT: { severity: 'HIGH', alert: true },
BULK_DATA_ACCESS: { severity: 'HIGH', alert: true },
DATA_DELETION: { severity: 'CRITICAL', alert: true },
// 支付相關
PAYMENT_FAILURE: { severity: 'MEDIUM', alert: false },
PAYMENT_ANOMALY: { severity: 'HIGH', alert: true }, // 如異常金額、頻率
REFUND_REQUEST: { severity: 'MEDIUM', alert: false },
BULK_REFUND: { severity: 'CRITICAL', alert: true }, // 短時間大量退款
// 系統相關
CONFIG_CHANGE: { severity: 'HIGH', alert: true },
SERVICE_DOWN: { severity: 'CRITICAL', alert: true },
};
第二週:異常偵測規則
針對電商場景,設定以下偵測規則:
| 規則名稱 | 偵測邏輯 | 為什麼重要 |
|---|---|---|
| 暴力破解 | 15 分鐘內同一帳號或 IP 登入失敗 ≥ 5 次 | 防止帳號被盜 |
| 帳號列舉 | 10 分鐘內同一 IP 嘗試登入 ≥ 10 個不同帳號 | 攻擊者在確認有效帳號 |
| 異常支付 | 單一帳號 1 小時內消費金額 > NT$50,000 | 可能使用盜刷的信用卡 |
| 批次退款 | 單一管理員 1 小時內核准退款 ≥ 10 筆 | 可能是內部人員舞弊 |
| 大量資料匯出 | 24 小時內匯出用戶資料 ≥ 3 次 | 可能正在竊取客戶資料 |
| 非工時管理行為 | 深夜 23:00-06:00 有管理員登入 | 可能帳號被盜 |
| 地理位置異常 | 同一帳號 1 小時內從不同國家登入 | 帳號幾乎確定被盜 |
第三、四週:儀表板與告警
建立 Grafana 儀表板,設定以下面板:
Dashboard: 電商平台安全監控
Panel 1: 即時告警摘要(Stat Panel)
- P1 告警數, P2 告警數, P3 告警數
Panel 2: 登入失敗率(Time Series)
- Query: rate(login_failures[5m]) / rate(login_total[5m])
- Alert: > 15% for 5 minutes
Panel 3: Top 10 被封鎖的 IP(Table)
- 顯示被速率限制或暴力破解規則封鎖的 IP
Panel 4: 敏感操作時間軸(Logs Panel)
- 過濾 severity = HIGH 或 CRITICAL 的事件
Panel 5: API 錯誤率(Time Series)
- 4xx 和 5xx 錯誤的趨勢
Panel 6: 支付異常(Time Series)
- 異常交易金額和退款趨勢
七、安全監控 Checklist:你的團隊可以直接用
日誌建設 Checklist
## 日誌記錄
- [ ] 所有認證事件(登入成功/失敗、登出、密碼變更)有記錄
- [ ] 所有授權失敗事件(403)有記錄
- [ ] 敏感資料存取行為有記錄
- [ ] 管理後台操作有記錄
- [ ] 系統設定變更有記錄
- [ ] 日誌中不包含密碼、Token、信用卡等敏感資料
- [ ] 日誌格式為結構化 JSON
- [ ] 每筆日誌包含:時間戳、事件類型、來源 IP、使用者 ID、請求 ID
## 日誌保存
- [ ] 日誌保存至少 6 個月(依法規要求調整)
- [ ] 日誌有備份機制
- [ ] 日誌存取有權限控制(不是所有人都能看日誌)
- [ ] 日誌完整性有保護(防止被竄改)
## 異常偵測
- [ ] 暴力破解偵測規則已設定
- [ ] 帳號列舉偵測規則已設定
- [ ] 異常存取模式偵測規則已設定
- [ ] 非上班時間敏感操作偵測規則已設定
- [ ] 異常資料匯出偵測規則已設定
## 告警與回應
- [ ] 告警分級制度已定義(P1-P4)
- [ ] P1/P2 告警有即時通知管道(Slack / Email / 電話)
- [ ] 告警通知有指定的負責人
- [ ] 告警有升級(Escalation)機制
- [ ] 定期檢視告警規則,減少誤報
## 儀表板
- [ ] 有即時安全狀態總覽
- [ ] 有關鍵安全指標趨勢圖
- [ ] 有最近安全事件清單
- [ ] 儀表板存取有權限控制
八、團隊落地建議:讓安全監控變成日常
建議一:從「三個最重要的指標」開始
不要一開始就想建一個完美的 SIEM 系統。先問自己:「如果只能看三個指標,我要看哪三個?」
對大多數 Web 應用來說,這三個最關鍵:
- 登入失敗率 — 偵測帳號被盜或暴力破解
- 403/401 錯誤趨勢 — 偵測越權存取嘗試
- API 回應時間 P95 — 偵測 DoS 攻擊或效能異常
先把這三個監控好、告警設好,再逐步擴展。
建議二:每週花 15 分鐘做「安全巡檢」
就像大樓管理員每天巡邏一樣,指定一位團隊成員(可以輪值),每週花 15 分鐘看一下資安儀表板:
## 每週安全巡檢報告
日期:2026/02/28
巡檢人:工程師 A
### 本週告警摘要
- P1: 0 件
- P2: 2 件(已處理)
- P3: 8 件(5 件已處理,3 件評估為誤報)
### 異常發現
- 來自越南 IP 的登入嘗試增加 30%(已加入黑名單)
- /api/users/export 被同一個管理員呼叫 12 次(已確認為正常業務需求)
### 行動項目
- [ ] 調整帳號列舉規則的閾值(目前誤報率偏高)
- [ ] 確認 AWS 帳單是否有異常(本週流量增加 15%)
建議三:把監控和事件回應串起來
監控只是偵測,偵測到之後需要有標準的處理流程。建議建立一個簡單的事件回應 SOP:
發現異常
│
├─ P1(緊急)→ 立即通知資安負責人 → 15 分鐘內開始處理
│ → 同步通知主管和相關人員
│
├─ P2(嚴重)→ Slack 通知 → 1 小時內開始調查
│ → 記錄在事件追蹤系統
│
├─ P3(中等)→ Slack 通知 → 4 小時內評估
│ → 判斷是否為誤報
│
└─ P4(低) → 每日彙整報告 → 下個工作日處理
建議四:別忘了監控「監控系統本身」
這聽起來很像雞生蛋蛋生雞的問題,但非常重要。如果你的日誌系統掛了、告警通知沒送出去,你根本不知道自己「瞎了」。
簡單的做法:設定一個「心跳檢查」,每 5 分鐘讓監控系統送一個測試告警到你的 Slack。如果超過 10 分鐘沒收到心跳,代表監控系統本身可能有問題。
九、與台灣法規的連結
安全監控不只是技術需求,也是法規要求。在前面的法規遵循指南中,我們提到:
| 法規 | 監控相關要求 |
|---|---|
| 個資法 | 建立個資檔案安全維護計畫,包含「事故之預防、通報及應變機制」 |
| 資安法 | 資安事件須在 1~72 小時內通報(依等級),系統必須有偵測與告警能力 |
| 金融資安行動方案 | 核心系統須有即時告警,資安事件須在 30 分鐘~24 小時內通報 |
| 上市櫃資安管控指引 | 須建立資安事件偵測、告警與通報機制 |
沒有監控,你連「有沒有發生資安事件」都不知道——更別說在規定時間內通報了。
常見問題 FAQ
Q1:小團隊預算有限,用什麼方案最務實?
用 Winston(日誌)+ Redis(計數器)+ Slack Webhook(告警) 這個組合,幾乎零成本就能建立基本的安全監控。不需要 ELK、不需要 Splunk。等團隊規模和流量長大了,再考慮升級到 Grafana + Loki 或 ELK Stack。先有比完美更重要。
Q2:告警太多,團隊已經「告警疲勞」了怎麼辦?
這是很常見的問題。解決方法有三個:一是提高告警閾值,寧可漏掉一些低風險的,也不要淹沒在噪音裡;二是做告警聚合,同一種類型的告警在 5 分鐘內只通知一次,附上數量統計;三是定期清理規則,每個月檢視一次,把過去 30 天裡「收到之後從來沒有需要處理」的告警規則降級或移除。
Q3:要監控到什麼程度才算「夠」?
沒有標準答案,但可以用這個檢驗標準:如果現在有人在竊取你的使用者資料,你能在多久內發現? 如果答案是「不知道」或「幾天後」,那監控絕對不夠。目標是把這個時間縮短到「幾分鐘到幾小時內」。IBM 的年度報告統計,全球企業平均要 194 天才能發現資料外洩——你不會想成為這個統計數字的一部分。
Q4:日誌要保存多久?
台灣法規的最低要求通常是 6 個月,但建議保存至少 1 年。如果你是金融業或處理大量個資,考慮保存 2-3 年。日誌存儲的成本遠低於事後無法調查的代價。
十、結語:看見,才能保護
回到蓋房子的比喻。你花了大量心血設計防震結構、選用防火建材、安裝堅固門鎖。但如果沒有煙霧偵測器,一場小火可能在你睡夢中燒毀一切;如果沒有監視器,小偷可以從容地搬走你的家當。
安全監控就是你系統的「眼睛」和「耳朵」。它不會阻止攻擊發生,但它能確保你在第一時間知道、第一時間反應。
「安全不是恐懼,而是創造的基礎。而監控,就是讓你安心創造的守護者。」
下一篇,我們將進入事件回應 SOP——當告警真的響起來了,你和你的團隊該怎麼辦?別擔心,有了監控的基礎,你已經贏在起跑點了。
延伸閱讀
- OWASP Logging Cheat Sheet
- NIST SP 800-92 Guide to Computer Security Log Management
- OWASP Application Security Verification Standard (ASVS) — V7: Error Handling and Logging
- Grafana Loki Documentation
- Wazuh — Open Source Security Platform
- SSDLC by 飛飛 — 學習路徑
- SSDLC — 安全需求:台灣法規遵循指南
- SSDLC — CI/CD 安全整合:在 Pipeline 加入安全關卡