[安全測試] 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 會告訴你。
安全不是恐懼,而是創造的基礎。
當你的每一次部署都通過了自動化安全檢查,你就能更有信心地交付產品——因為你知道,你的房子不只漂亮,更經得起考驗。