[安全維運] 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 分鐘跟你的團隊坐下來,回答這三個問題:

  1. 如果我們的系統現在被入侵了,第一個被通知的人是誰?
  2. 他/她知道接下來該做什麼嗎?
  3. 我們有沒有一份寫下來的流程可以參考?

如果有任何一個答案是「不知道」或「沒有」,那就是你開始建立事件回應 SOP 的最好時機。

安全不是恐懼,而是創造的基礎。 一份好的事件回應計畫,讓你的團隊在面對資安事件時,從「恐慌」變成「有序處理」——這就是專業。


延伸閱讀

官方資源:

台灣法規:

系列文章:

[安全維運] 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 應用來說,這三個最關鍵:

  1. 登入失敗率 — 偵測帳號被盜或暴力破解
  2. 403/401 錯誤趨勢 — 偵測越權存取嘗試
  3. 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——當告警真的響起來了,你和你的團隊該怎麼辦?別擔心,有了監控的基礎,你已經贏在起跑點了。


延伸閱讀