[安全測試] 003 CI/CD 安全整合:在 Pipeline 加入安全關卡——讓每次部署都通過資安體檢

「房子蓋好後,不是直接交屋就好,你得先請結構技師檢查、消防設備測試、水電驗收。
CI/CD Pipeline 也一樣——每一次部署,都應該先通過一輪資安體檢。
自動化不是為了取代人,而是讓安全不再被遺忘。」
— SSDLC by 飛飛


一、CI/CD 安全整合是什麼?為什麼你的 Pipeline 需要安全關卡?

在 SSDLC 蓋房子的旅程中,我們已經走過了安全需求定義(確認要防震防火防盜)、安全設計(畫好建築藍圖)、安全實作(使用合格建材施工)。現在,我們來到了階段四:安全驗證(Verification)——房子蓋好後,交屋前的「安全檢查」。

想像你蓋了一棟漂亮的房子,建材用的是防火等級的,電線也符合規範。但如果交屋那天,沒有人來驗收——沒人檢查消防灑水器能不能動、沒人測試逃生門能不能開、沒人看排水管有沒有接好——你敢住進去嗎?

CI/CD 安全整合,就是在你的軟體「交屋」(部署上線)之前,加入一道自動化的安全驗收流程。

傳統的做法是:開發者寫完程式碼、推上 Git、CI 跑一跑單元測試、通過了就部署。安全?等上線之後再找資安公司來滲透測試吧。

問題是,等到滲透測試報告出來,漏洞已經在線上跑了好幾週。就像房子已經住了三個月,才發現消防系統根本沒裝——代價極高。

CI/CD 安全整合的核心思想是:把安全檢查「左移」到部署流程中,讓每一次 git push 都自動觸發安全掃描。 發現問題就擋住、修好再上線,不讓漏洞有機會溜進生產環境。

飛飛觀點:
很多人覺得「加安全掃描會拖慢部署速度」。但想想看,你會因為安全帶「太麻煩」就不繫嗎?CI/CD 安全掃描通常只多花 3-8 分鐘,但它能在漏洞到達使用者之前就攔截下來。花 5 分鐘預防,好過花 5 天善後。


二、傳統做法 vs. DevSecOps:安全不該是最後一關

先來看看傳統做法和 DevSecOps 做法的差別:

面向 傳統做法 DevSecOps 做法
安全檢查時機 上線前或上線後,做一次性滲透測試 每次 push/PR 都自動掃描
發現漏洞的時機 開發結束後,可能已經過了好幾週 寫完程式碼的幾分鐘內就知道
修復成本 高(要回頭改已經整合的功能) 低(問題還在開發者腦中,馬上修)
責任歸屬 「資安團隊的事」 「每個開發者的事」
自動化程度 低,多靠人工審查 高,機器自動掃描、自動擋
覆蓋率 低,只檢查一次 高,每次變更都檢查

用蓋房子的比喻來說:

傳統做法就像房子蓋完才請消防隊來檢查——萬一不合格,整面牆要拆掉重來。

DevSecOps 就像每砌一面牆,工地監工就立刻檢查有沒有用對建材、有沒有預留管線——問題當下就能修正,不用返工。


三、安全 Pipeline 的四道關卡:你的 CI/CD 防火牆

一條完整的安全 Pipeline,就像房屋驗收時的四個檢查站:

┌─────────────┐    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  🔍 SAST    │───▶│  📦 SCA     │───▶│  🔐 Secret  │───▶│  🌐 DAST    │
│  靜態分析   │    │  依賴掃描   │    │  機密掃描   │    │  動態測試   │
│             │    │             │    │             │    │             │
│ 檢查程式碼  │    │ 檢查第三方  │    │ 檢查有沒有  │    │ 檢查運行中  │
│ 有沒有漏洞  │    │ 套件有沒有  │    │ 把密碼推上  │    │ 的網站有沒  │
│             │    │ 已知漏洞    │    │ Git         │    │ 有漏洞      │
└─────────────┘    └─────────────┘    └─────────────┘    └─────────────┘
    結構技師           建材檢驗員         保全系統檢查         消防測試

關卡一:SAST(靜態應用程式安全測試)

比喻: 結構技師看藍圖和施工品質,不用等房子蓋好就能發現問題。

SAST 直接掃描你的原始碼,在程式碼還沒執行前就找出潛在漏洞:SQL Injection、XSS、Path Traversal、硬編碼密碼等。

推薦工具:

工具 特色 費用
Semgrep 輕量快速、規則豐富、支援多語言 免費(開源)
SonarQube 全面的程式碼品質 + 安全掃描 Community 版免費
CodeQL GitHub 原生整合,語意分析強大 GitHub 公開 Repo 免費
ESLint Security Node.js 專用,整合 IDE 即時提示 免費(開源)

關卡二:SCA(軟體組成分析)

比喻: 建材檢驗員確認你用的水泥、鋼筋、磁磚是不是合格品。

SCA 掃描你專案的所有第三方依賴套件,檢查有沒有已知的 CVE 漏洞。還記得 2021 年的 Log4Shell 嗎?一個 Log4j 的漏洞影響了全世界數百萬系統——如果你的 Pipeline 有 SCA 掃描,就能在第一時間收到警報。

推薦工具:

工具 特色 費用
npm audit Node.js 內建 免費
Snyk 自動修復建議、整合豐富 免費方案可用
OWASP Dependency-Check 開源、支援多語言 免費(開源)
Trivy 容器 + 依賴 + IaC 全方位掃描 免費(開源)

關卡三:Secret Scanning(機密掃描)

比喻: 保全公司檢查你有沒有把鑰匙插在門上忘了拔。

Secret Scanning 掃描你的程式碼和 Git 歷史紀錄,找出不小心被推上去的 API Key、資料庫密碼、JWT Secret 等機密資訊。根據 GitGuardian 的統計,平均每 1,000 次 commit 就有 5.5 個機密被意外推上公開 Repo。

推薦工具:

工具 特色 費用
GitHub Secret Scanning GitHub 原生、支援多種 Token 格式 公開 Repo 免費
GitLeaks 快速、可自訂規則 免費(開源)
TruffleHog 深度掃描 Git 歷史 免費(開源)

關卡四:DAST(動態應用程式安全測試)

比喻: 消防測試——實際點火看灑水器會不會動。

DAST 對執行中的應用程式發送惡意請求,模擬真實攻擊。它不看原始碼,而是看系統的實際行為——就像真正的攻擊者會做的事情。

推薦工具:

工具 特色 費用
OWASP ZAP 開源標準、功能強大 免費(開源)
Nuclei 模板化掃描、社群維護規則 免費(開源)
Burp Suite 業界標準,功能最全面 社群版免費

飛飛觀點:
不需要一次全部導入。如果你的團隊剛開始做 CI/CD 安全整合,先從 SCA(npm audit)和 Secret Scanning(GitLeaks)開始——這兩個最容易導入、誤報率最低、投入產出比最高。穩定之後再加入 SAST 和 DAST。


四、GitHub Actions 安全掃描實戰設定

接下來,我們用 GitHub Actions 來實作一條完整的安全 Pipeline。以台灣常見的 Node.js 電商專案為例,一步步加入四道安全關卡。

4.1 基礎架構:安全掃描 Workflow

先建立 <code>.github/workflows/security.yml</code>:

# .github/workflows/security.yml
name: 🔒 Security Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]
  schedule:
    # 每週一早上 9 點(UTC+8)自動掃描,確保依賴漏洞被及時發現
    - cron: '0 1 * * 1'

permissions:
  contents: read
  security-events: write  # 讓掃描結果可以顯示在 GitHub Security Tab

jobs:
  # ═══════════════════════════════════════
  # 關卡一:依賴套件漏洞掃描(SCA)
  # ═══════════════════════════════════════
  dependency-check:
    name: 📦 依賴套件安全檢查
    runs-on: ubuntu-latest
    steps:
      - name: Checkout 程式碼
        uses: actions/checkout@v4

      - name: 安裝 Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: 安裝依賴
        run: npm ci

      # npm audit:Node.js 內建的依賴漏洞掃描
      - name: npm audit — 掃描已知漏洞
        run: |
          echo "🔍 正在掃描 npm 依賴套件漏洞..."
          npm audit --audit-level=high --omit=dev 2>&1 | tee audit-result.txt
          # 只阻擋 high 和 critical 等級的漏洞
          # --omit=dev 忽略開發依賴,減少誤報

      # (進階)使用 Snyk 做更深度的 SCA 掃描
      # - name: Snyk 依賴掃描
      #   uses: snyk/actions/node@master
      #   env:
      #     SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
      #   with:
      #     args: --severity-threshold=high

  # ═══════════════════════════════════════
  # 關卡二:機密掃描(Secret Scanning)
  # ═══════════════════════════════════════
  secret-scan:
    name: 🔐 機密資訊掃描
    runs-on: ubuntu-latest
    steps:
      - name: Checkout 程式碼(含完整 Git 歷史)
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # 掃描完整 Git 歷史

      - name: GitLeaks — 掃描洩漏的機密
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          # GitLeaks 會掃描:API Key、資料庫密碼、JWT Secret、
          # AWS Access Key、Private Key 等常見機密格式

  # ═══════════════════════════════════════
  # 關卡三:靜態程式碼安全分析(SAST)
  # ═══════════════════════════════════════
  sast-scan:
    name: 🔍 靜態安全分析
    runs-on: ubuntu-latest
    steps:
      - name: Checkout 程式碼
        uses: actions/checkout@v4

      # Semgrep:輕量級 SAST 工具,支援 OWASP Top 10 規則
      - name: Semgrep — 掃描程式碼安全漏洞
        uses: returntocorp/semgrep-action@v1
        with:
          config: >-
            p/owasp-top-ten
            p/nodejs
            p/javascript
            p/typescript
          # p/owasp-top-ten:涵蓋 OWASP Top 10 的掃描規則
          # p/nodejs:Node.js 特定的安全規則
          # p/javascript:JavaScript 通用安全規則

  # ═══════════════════════════════════════
  # 關卡四:容器映像掃描(如果使用 Docker)
  # ═══════════════════════════════════════
  container-scan:
    name: 🐳 容器安全掃描
    runs-on: ubuntu-latest
    if: hashFiles('Dockerfile') != ''  # 只在有 Dockerfile 時執行
    steps:
      - name: Checkout 程式碼
        uses: actions/checkout@v4

      - name: 建置 Docker 映像
        run: docker build -t my-app:scan .

      # Trivy:掃描容器映像中的 OS 套件和應用程式依賴漏洞
      - name: Trivy — 掃描容器漏洞
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'my-app:scan'
          format: 'table'
          exit-code: '1'            # 發現漏洞就失敗
          severity: 'HIGH,CRITICAL' # 只阻擋高風險和嚴重漏洞
          ignore-unfixed: true      # 忽略尚無修補的漏洞

4.2 進階:ESLint 安全規則整合

除了 CI 的掃描,在開發階段就能用 ESLint 安全外掛即時提醒開發者:

// .eslintrc.js — 加入安全規則
module.exports = {
  plugins: ['security'],
  extends: ['plugin:security/recommended'],
  rules: {
    // 偵測不安全的正規表達式(ReDoS 攻擊)
    'security/detect-unsafe-regex': 'error',
    // 偵測 eval 相關的危險用法
    'security/detect-eval-with-expression': 'error',
    // 偵測可能的物件注入(Prototype Pollution)
    'security/detect-object-injection': 'warn',
    // 偵測不安全的 Buffer 用法
    'security/detect-buffer-noassert': 'error',
    // 偵測可能的 timing attack
    'security/detect-possible-timing-attacks': 'warn',
    // 偵測非字面值的正規表達式(可能的 ReDoS)
    'security/detect-non-literal-regexp': 'warn',
    // 偵測非字面值的 require(可能的任意檔案載入)
    'security/detect-non-literal-require': 'warn',
  },
};

把 ESLint 安全檢查也加進 CI:

  # 加在 security.yml 的 jobs 裡
  lint-security:
    name: 📝 ESLint 安全規則檢查
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - name: ESLint 安全檢查
        run: npx eslint . --ext .js,.ts --format stylish

4.3 完整的安全 Pipeline 流程圖

 開發者 git push
      │
      ▼
┌──────────────────┐
│   GitHub Actions  │
│   觸發 Workflow   │
└──────┬───────────┘
       │
       ├──▶ 📦 dependency-check (SCA)
       │      └── npm audit --audit-level=high
       │
       ├──▶ 🔐 secret-scan
       │      └── GitLeaks 掃描完整 Git 歷史
       │
       ├──▶ 🔍 sast-scan (SAST)
       │      └── Semgrep OWASP Top 10 規則
       │
       ├──▶ 📝 lint-security
       │      └── ESLint Security Plugin
       │
       └──▶ 🐳 container-scan(如果有 Dockerfile)
              └── Trivy 容器映像掃描
       │
       ▼
┌──────────────────┐
│  全部通過?       │
│  ✅ → 允許合併    │
│  ❌ → 阻擋部署    │
└──────────────────┘

五、設定安全品質門檻(Security Quality Gate)

有了安全掃描工具還不夠,你還需要定義明確的品質門檻——什麼情況下要阻擋部署、什麼情況下只是警告。

5.1 品質門檻的三個等級

就像房屋驗收有不同等級的缺失:

等級 說明 Pipeline 行為 範例
🔴 Critical(嚴重) 立即可被利用的漏洞 阻擋部署,必須修復 SQL Injection、RCE、硬編碼的 API Key
🟡 High(高風險) 有風險但需要特定條件才能利用 阻擋 PR 合併,限期修復 XSS、CSRF、高風險 CVE 依賴
🟢 Medium/Low(中低風險) 風險較低或難以利用 發出警告,排入 backlog 資訊洩漏、低風險 CVE、代碼品質問題

5.2 實作品質門檻:建立統一的安全檢查腳本

// scripts/security-gate.js — 統一的安全品質門檻判斷
const fs = require('fs');

/**
 * 安全品質門檻設定
 * 根據你的團隊風險承受度調整
 */
const SECURITY_GATE = {
  // SCA(依賴漏洞)
  sca: {
    block: ['critical', 'high'],      // 這些等級會阻擋部署
    warn: ['moderate'],                // 這些等級只發出警告
    ignore: ['low', 'info'],           // 這些等級靜默忽略
    maxAge: 30,                        // 已知漏洞超過 30 天未修會升級為 block
  },

  // SAST(程式碼漏洞)
  sast: {
    block: ['ERROR'],                  // Semgrep ERROR 等級阻擋
    warn: ['WARNING'],                 // WARNING 等級發出提醒
    maxFindings: 0,                    // block 等級的漏洞數量上限
  },

  // Secret(機密洩漏)
  secret: {
    block: true,                       // 只要偵測到機密就阻擋,零容忍
  },

  // Container(容器漏洞)
  container: {
    block: ['CRITICAL', 'HIGH'],
    ignoreUnfixed: true,               // 忽略尚無修補的漏洞
  },
};

/**
 * 檢查 npm audit 結果
 */
function checkSCAResults(auditOutput) {
  const vulnerabilities = JSON.parse(auditOutput);
  const blocked = [];
  const warned = [];

  for (const [name, detail] of Object.entries(vulnerabilities.vulnerabilities || {})) {
    if (SECURITY_GATE.sca.block.includes(detail.severity)) {
      blocked.push(<code class="kb-btn">🔴 [${detail.severity.toUpperCase()}] ${name}: ${detail.title || detail.url}</code>);
    } else if (SECURITY_GATE.sca.warn.includes(detail.severity)) {
      warned.push(<code class="kb-btn">🟡 [${detail.severity.toUpperCase()}] ${name}: ${detail.title || detail.url}</code>);
    }
  }

  return { blocked, warned, passed: blocked.length === 0 };
}

/**
 * 輸出報告
 */
function printReport(results) {
  console.log('\n╔══════════════════════════════════════╗');
  console.log('║    🔒 Security Quality Gate Report    ║');
  console.log('╚══════════════════════════════════════╝\n');

  if (results.blocked.length > 0) {
    console.log('❌ 以下問題必須修復才能部署:\n');
    results.blocked.forEach(item => console.log(<code class="kb-btn">  ${item}</code>));
    console.log('');
  }

  if (results.warned.length > 0) {
    console.log('⚠️  以下問題建議修復(不阻擋部署):\n');
    results.warned.forEach(item => console.log(<code class="kb-btn">  ${item}</code>));
    console.log('');
  }

  if (results.passed) {
    console.log('✅ Security Quality Gate: PASSED\n');
  } else {
    console.log('❌ Security Quality Gate: FAILED\n');
    console.log('請修復以上 🔴 標記的問題後重新提交。');
    process.exit(1);  // 非零退出碼 → CI 失敗 → 阻擋部署
  }
}

module.exports = { SECURITY_GATE, checkSCAResults, printReport };

5.3 在 GitHub Actions 中套用品質門檻

  # 在所有掃描完成後,統一評估是否通過品質門檻
  security-gate:
    name: 🚦 安全品質門檻評估
    needs: [dependency-check, secret-scan, sast-scan, lint-security]
    runs-on: ubuntu-latest
    if: always()  # 即使前面的 job 失敗也要跑,才能給出完整報告
    steps:
      - name: 檢查所有安全掃描結果
        run: |
          echo "╔══════════════════════════════════════╗"
          echo "║    🔒 Security Quality Gate Report    ║"
          echo "╚══════════════════════════════════════╝"
          echo ""

          FAILED=0

          # 檢查依賴掃描
          if [ "${{ needs.dependency-check.result }}" == "failure" ]; then
            echo "❌ 依賴套件掃描:發現高風險漏洞"
            FAILED=1
          else
            echo "✅ 依賴套件掃描:通過"
          fi

          # 檢查機密掃描
          if [ "${{ needs.secret-scan.result }}" == "failure" ]; then
            echo "❌ 機密資訊掃描:偵測到洩漏的機密!"
            FAILED=1
          else
            echo "✅ 機密資訊掃描:通過"
          fi

          # 檢查 SAST
          if [ "${{ needs.sast-scan.result }}" == "failure" ]; then
            echo "❌ 靜態安全分析:發現程式碼漏洞"
            FAILED=1
          else
            echo "✅ 靜態安全分析:通過"
          fi

          # 檢查 ESLint 安全
          if [ "${{ needs.lint-security.result }}" == "failure" ]; then
            echo "⚠️  ESLint 安全規則:有警告(不阻擋)"
          else
            echo "✅ ESLint 安全規則:通過"
          fi

          echo ""
          if [ "$FAILED" -eq 1 ]; then
            echo "❌ Security Quality Gate: FAILED"
            echo "請修復以上問題後重新提交 PR。"
            exit 1
          else
            echo "✅ Security Quality Gate: PASSED"
            echo "所有安全檢查通過,可以安全合併。"
          fi

5.4 設定 Branch Protection 強制安全檢查

光有 CI 掃描還不夠,你要確保「不通過就不能合併」。在 GitHub 設定 Branch Protection Rules:

GitHub → Repository Settings → Branches → Branch protection rules

☑️ Require a pull request before merging
☑️ Require status checks to pass before merging
    → 搜尋並勾選:
    ✅ 📦 依賴套件安全檢查
    ✅ 🔐 機密資訊掃描
    ✅ 🔍 靜態安全分析
    ✅ 🚦 安全品質門檻評估
☑️ Require branches to be up to date before merging
☑️ Do not allow bypassing the above settings

這樣就算是 admin 也不能跳過安全檢查直接合併——就像消防檢查不合格的房子,不管你是屋主還是建商,都不能強制交屋。


六、實戰案例:台灣電商平台的安全 Pipeline

讓我們用一個完整的台灣電商情境,從頭到尾走一遍安全 Pipeline 的運作方式。

情境:ShopTW 電商平台

ShopTW 是一個台灣的電商平台,使用 Node.js + Express + PostgreSQL,部署在 AWS 上。團隊有 5 位工程師,剛開始導入 SSDLC。

一個普通的工作日:

工程師小明正在開發「商品評論功能」,他寫完了 API 程式碼,準備提交 PR:

// routes/reviews.js — 小明寫的商品評論 API
const express = require('express');
const router = express.Router();
const db = require('../db');

// 新增評論
router.post('/api/products/:id/reviews', async (req, res) => {
  const productId = req.params.id;
  const { rating, comment } = req.body;
  const userId = req.user.id;

  // ⚠️ 問題 1:SQL 字串拼接
  const query = `INSERT INTO reviews (product_id, user_id, rating, comment) 
                 VALUES ('${productId}', '${userId}', ${rating}, '${comment}')`;

  await db.query(query);
  res.json({ message: '評論已送出' });
});

// 查詢評論
router.get('/api/products/:id/reviews', async (req, res) => {
  const productId = req.params.id;
  const sort = req.query.sort || 'created_at';

  // ⚠️ 問題 2:排序欄位沒有白名單驗證
  const query = <code class="kb-btn">SELECT * FROM reviews WHERE product_id = '${productId}' ORDER BY ${sort}</code>;
  const result = await db.query(query);

  // ⚠️ 問題 3:直接回傳完整資料,可能包含敏感資訊
  res.json(result.rows);
});

module.exports = router;

小明還不小心在設定檔裡留了一段:

// config/database.js
// ⚠️ 問題 4:硬編碼的資料庫密碼
module.exports = {
  host: 'shopdb.abc123.ap-northeast-1.rds.amazonaws.com',
  user: 'shop_admin',
  password: 'ShopTW2024!SuperSecret',  // TODO: 之後改用環境變數
  database: 'shoptw_production',
};

小明提交 PR 後,安全 Pipeline 自動啟動:

🔍 SAST (Semgrep) 掃描結果:
  ❌ [ERROR] sql-injection — routes/reviews.js:12
     SQL query built with string concatenation.
     Use parameterized queries instead.

  ❌ [ERROR] sql-injection — routes/reviews.js:23
     User-controlled data in SQL ORDER BY clause.
     Use an allowlist for sort fields.

📦 SCA (npm audit) 掃描結果:
  ⚠️ [HIGH] express@4.17.1 — Prototype Pollution (CVE-2024-XXXX)
     Recommendation: Upgrade to express@4.21.0

🔐 Secret Scanning (GitLeaks) 掃描結果:
  ❌ [CRITICAL] Hardcoded password detected — config/database.js:5
     Pattern: password = 'ShopTW2024!SuperSecret'

🚦 Security Quality Gate: ❌ FAILED
   修復 2 個 SQL Injection 漏洞和 1 個機密洩漏後重新提交。

Pipeline 阻擋了合併。 小明看到報告後,逐一修復:

// ✅ 修復後的 routes/reviews.js
const express = require('express');
const router = express.Router();
const db = require('../db');
const { body, param, query, validationResult } = require('express-validator');

// 排序欄位白名單
const ALLOWED_SORT_FIELDS = ['created_at', 'rating'];

// 新增評論 — 使用參數化查詢 + 輸入驗證
router.post('/api/products/:id/reviews',
  [
    param('id').isUUID(),
    body('rating').isInt({ min: 1, max: 5 }),
    body('comment').isString().isLength({ min: 1, max: 1000 }).trim().escape(),
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ error: '輸入資料格式不正確' });
    }

    const { id: productId } = req.params;
    const { rating, comment } = req.body;
    const userId = req.user.id;

    // ✅ 參數化查詢,避免 SQL Injection
    const query = `INSERT INTO reviews (product_id, user_id, rating, comment) 
                   VALUES ($1, $2, $3, $4) RETURNING id`;
    const result = await db.query(query, [productId, userId, rating, comment]);

    res.status(201).json({ 
      message: '評論已送出',
      reviewId: result.rows[0].id 
    });
  }
);

// 查詢評論 — 白名單驗證排序欄位
router.get('/api/products/:id/reviews',
  [
    param('id').isUUID(),
    query('sort').optional().isIn(ALLOWED_SORT_FIELDS),
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ error: '查詢參數不正確' });
    }

    const { id: productId } = req.params;
    const sort = ALLOWED_SORT_FIELDS.includes(req.query.sort) 
      ? req.query.sort 
      : 'created_at';

    // ✅ 參數化查詢 + 白名單排序
    const result = await db.query(
      `SELECT id, rating, comment, created_at 
       FROM reviews WHERE product_id = $1 ORDER BY ${sort} DESC`,
      [productId]
    );

    // ✅ 只回傳必要欄位
    res.json({ reviews: result.rows });
  }
);

module.exports = router;
// ✅ 修復後的 config/database.js — 使用環境變數
module.exports = {
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
};

小明重新推上程式碼,Pipeline 再次執行:

🔍 SAST: ✅ 通過(0 個漏洞)
📦 SCA:  ⚠️ 1 個中風險依賴(已排入下週更新計畫)
🔐 Secret: ✅ 通過(無機密洩漏)
🚦 Security Quality Gate: ✅ PASSED

PR 順利合併。整個修復過程只花了 30 分鐘。如果這些漏洞等到上線後才被滲透測試發現,可能需要好幾天的緊急修補,還有個資外洩的風險。


七、可複用的安全 Pipeline 模板

以下是一個可以直接複製到你專案的完整模板:

GitHub Actions 安全 Pipeline 模板

# .github/workflows/security.yml
# 
# SSDLC 安全 Pipeline 模板
# 適用於 Node.js 專案
# 
# 使用方式:
# 1. 將此檔案複製到 .github/workflows/ 目錄
# 2. 設定 Branch Protection Rules 要求這些 checks 必須通過
# 3. 根據團隊需求調整 severity threshold
#
name: 🔒 Security Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]
  schedule:
    - cron: '0 1 * * 1'  # 每週一 UTC 01:00(台灣時間 09:00)

permissions:
  contents: read
  security-events: write

jobs:
  sca:
    name: 📦 SCA — 依賴漏洞掃描
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - run: npm audit --audit-level=high --omit=dev

  secrets:
    name: 🔐 Secrets — 機密掃描
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  sast:
    name: 🔍 SAST — 靜態安全分析
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: returntocorp/semgrep-action@v1
        with:
          config: >-
            p/owasp-top-ten
            p/nodejs

  gate:
    name: 🚦 Security Gate
    needs: [sca, secrets, sast]
    runs-on: ubuntu-latest
    if: always()
    steps:
      - run: |
          echo "=== Security Quality Gate ==="
          PASS=true
          [ "${{ needs.sca.result }}" = "failure" ] && echo "❌ SCA" && PASS=false || echo "✅ SCA"
          [ "${{ needs.secrets.result }}" = "failure" ] && echo "❌ Secrets" && PASS=false || echo "✅ Secrets"
          [ "${{ needs.sast.result }}" = "failure" ] && echo "❌ SAST" && PASS=false || echo "✅ SAST"
          [ "$PASS" = "false" ] && exit 1
          echo "✅ All security checks passed"

安全 Pipeline 導入 Checklist

## CI/CD 安全整合導入 Checklist

### 第一週:基礎建設
- [ ] 建立 .github/workflows/security.yml
- [ ] 設定 npm audit(SCA 掃描)
- [ ] 設定 GitLeaks(機密掃描)
- [ ] 設定 Branch Protection Rules

### 第二週:強化掃描
- [ ] 加入 Semgrep(SAST 掃描)
- [ ] 設定 ESLint Security Plugin
- [ ] 定義安全品質門檻(block / warn / ignore)
- [ ] 設定每週排程掃描(cron job)

### 第三週:團隊協作
- [ ] 撰寫「安全掃描結果處理 SOP」
- [ ] 設定 Slack/Teams 通知(掃描失敗時通知頻道)
- [ ] 為團隊做一次安全 Pipeline 教學
- [ ] 建立 .gitleaksignore(合理的排除清單)

### 第四週:持續優化
- [ ] 檢視過去一個月的掃描結果,調整誤報規則
- [ ] 評估是否加入 DAST(動態掃描)
- [ ] 評估是否加入容器掃描(如果使用 Docker)
- [ ] 將安全掃描時間納入團隊的 Sprint 規劃

八、團隊落地建議:讓安全 Pipeline 不只是「裝飾品」

建議一:從「不阻擋」到「阻擋」的漸進式導入

很多團隊一開始就設定「有任何漏洞就擋住」,結果開發者天天被擋、產生抗拒心理,最後乾脆把安全 Pipeline 關掉。

漸進式導入策略:

階段 時間 策略 說明
觀察期 第 1-2 週 只掃描、不阻擋 讓團隊熟悉工具,觀察誤報率
試行期 第 3-4 週 只阻擋 Critical 只擋住最嚴重的問題(如機密洩漏、RCE)
正式期 第 5 週起 阻擋 Critical + High 開始對高風險漏洞說「不」
成熟期 第 9 週起 完整品質門檻 包含 Medium 的限期修復機制

建議二:處理誤報——別讓「狼來了」毀掉信任

安全掃描工具一定會有誤報(False Positive)。如果團隊每天看到一堆「漏洞警告」結果都是誤報,最後大家就會習慣性地忽略——就像每天喊「狼來了」的牧童。

處理誤報的方式:

# .semgrepignore — Semgrep 排除清單
# 排除測試檔案(測試中的硬編碼值是正常的)
tests/
__tests__/
*.test.js
*.spec.js

# 排除第三方產生的程式碼
node_modules/
dist/
build/
# .gitleaksignore — GitLeaks 排除清單
# 排除測試用的假密碼
test-fixtures/fake-credentials.json
# 排除已知的 false positive(附上 issue 連結)
# Issue: https://github.com/your-repo/issues/123
config/example.env

關鍵原則:
每一條排除規則都要附上原因和 Issue 連結。不要只是靜默忽略,要讓其他人知道「為什麼這不是問題」。

建議三:讓掃描結果「可見」

不要讓安全掃描結果只藏在 CI log 裡。讓它出現在團隊日常能看到的地方:

  • PR Comment:安全掃描結果自動貼在 PR 的留言區
  • Slack 通知:掃描失敗時自動通知 #security 頻道
  • Dashboard:用 GitHub Security Tab 追蹤漏洞趨勢
  # 在安全 Pipeline 中加入 Slack 通知
  notify:
    name: 📢 通知
    needs: [gate]
    runs-on: ubuntu-latest
    if: failure()
    steps:
      - name: 通知 Slack
        uses: slackapi/slack-github-action@v1
        with:
          channel-id: 'C_SECURITY'
          slack-message: |
            🚨 安全掃描失敗!
            Repository: ${{ github.repository }}
            PR: ${{ github.event.pull_request.html_url }}
            請相關開發者盡快檢視。
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

飛飛觀點:
安全 Pipeline 的成功不是看「擋了多少漏洞」,而是看「團隊有沒有習慣看掃描結果」。如果開發者提交 PR 後會主動檢查安全掃描有沒有通過——恭喜你,安全文化已經開始扎根了。


九、常見問題 FAQ

Q1:安全掃描會不會讓 CI 變很慢?

不會太慢。以一個中型 Node.js 專案(10 萬行程式碼)為例,各項掃描的平均耗時:npm audit 約 10-30 秒、GitLeaks 約 20-60 秒、Semgrep 約 1-3 分鐘、ESLint Security 約 30-60 秒。全部跑完約 3-5 分鐘。而且這四項可以平行執行(GitHub Actions 的 jobs 預設就是平行的),所以總耗時約等於最慢的那一項。相比整條 CI Pipeline 通常 10-20 分鐘的總耗時,安全掃描只佔了一小部分。

Q2:我的專案不是 Node.js,這些工具還能用嗎?

大部分都能。Semgrep 支援 Python、Java、Go、Ruby、PHP 等 20 多種語言。GitLeaks 不看語言,它掃的是 Git commit 中的機密模式。Trivy 支援所有容器映像。你只需要把 <code>npm audit</code> 替換成你語言對應的 SCA 工具即可,例如 Python 用 <code>safety</code> 或 <code>pip-audit</code>、Java 用 <code>OWASP Dependency-Check</code>。

Q3:開源的免費工具夠用嗎?還是要買商業工具?

對大多數中小型團隊來說,免費工具完全足夠。 Semgrep + npm audit + GitLeaks + Trivy 這套組合能覆蓋 80% 以上的常見漏洞。商業工具(如 Snyk、Checkmarx)的優勢在於更低的誤報率、更好的修復建議、企業級報表和合規支援。建議先用免費工具建立基礎,等到團隊成熟、有明確需求時再評估商業工具。

Q4:Vibe Coding 時代,AI 生成的程式碼需要額外注意什麼?

AI 生成的程式碼更需要安全 Pipeline。根據實際經驗,AI 生成的程式碼常見的安全問題包括:SQL 字串拼接(不使用參數化查詢)、缺少輸入驗證、硬編碼測試用密碼或金鑰、使用已知有漏洞的舊版套件。建議把 AI 生成的程式碼視為「需要安全審查的外部貢獻」——先過安全 Pipeline 掃描,再做 Code Review。安全 Pipeline 正是 AI 時代開發的安全網。


十、結語:讓安全成為部署的「自然動作」

回到蓋房子的比喻。

沒有人會說:「交屋前的驗收檢查太麻煩了,我們跳過吧。」因為驗收不合格的房子,住進去是要出事的。

CI/CD 安全整合也是一樣。當你在 Pipeline 裡加入安全關卡,安全就不再是「上線前臨時想起來的事」,而是「每次部署都自然而然會經過的檢查站」。

最棒的是,這一切都是自動的。你不需要每次都請資安團隊手動審查,你只需要設定好規則,讓機器替你把關。開發者該做的,就是寫好程式、推上 Git——剩下的,Pipeline 會告訴你。

安全不是恐懼,而是創造的基礎。
當你的每一次部署都通過了自動化安全檢查,你就能更有信心地交付產品——因為你知道,你的房子不只漂亮,更經得起考驗。


延伸閱讀