[安全維運] 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——當告警真的響起來了,你和你的團隊該怎麼辦?別擔心,有了監控的基礎,你已經贏在起跑點了。


延伸閱讀