如何使用 Trivy 在開發階段提前發現安全漏洞

前言
在工作上開發專案時,通常都會定期進行弱點掃描(弱掃)。我們公司是透過 AWS ECR 內建的掃描功能來檢查所有專案的 Docker image。現有的工具看似完善,但實際運作起來卻有個痛點:只有 DevOps 部門跟主管層級的人才有 AWS 控制台的存取權限。也就是說,身為 RD 的我,根本沒辦法自己去看弱掃報告。
整個流程通常是這樣:
DevOps 掃完 → 整理報告 → 通知 RD → RD 才知道要修哪些套件
這種被動等通知的流程,在趕交付的時候特別讓人焦慮。有幾次甚至是快要交付了,才突然收到一份通知說有幾個高風險漏洞要修,然後就得在最後關頭緊急升版本、重新測試、重新建置 image……
更麻煩的是,修完套件之後,因為弱掃工具不再自己手上,我也沒辦法立刻驗證是否真的修乾淨了,只能等 DevOps 重新跑一次掃描才知道結果。
後來同事推薦了 Trivy 這個開源掃描工具,它能夠在本地端直接掃描 Docker image,完美解決了我無法自主掌握掃描進度的問題。而且最棒的是,Trivy 甚至可以直接用 docker run 來執行,完全不用在本機安裝任何軟體,使用方式非常簡單。
Trivy 是什麼?
Trivy 是一款開源且多功能的雲端原生安全掃描工具。它的設計哲學就如同它的名字(取自 Trivial),旨在讓安全掃描變得極度簡單。
相較於其他掃描工具,Trivy 提供了非常全面的掃描目標(Target)與種類(Scanners):
掃描目標(Target)
| 目標類型 | 說明 |
|---|---|
image | 掃描 Docker / OCI Container image |
fs / rootfs | 掃描本地檔案系統或目錄 |
repo | 直接掃描遠端 Git Repository |
sbom | 針對 SBOM(Software Bill of Materials)文件進行掃描 |
k8s | 直接掃描 Kubernetes 叢集 |
掃描類型(Scanners)
| 掃描類型 | 說明 |
|---|---|
vuln | 已知的 CVE 安全漏洞檢查 |
misconfig | 設定檔錯誤檢查(如 Dockerfile、Kubernetes YAML、Terraform) |
secret | 掃描程式碼中硬編碼的 Secret 或 API Key |
license | 檢查套件的開源授權條款合規性 |
除了在本地手動執行,Trivy 也非常適合整合進 CI/CD Pipeline 中。目前的 官方文檔 也提供了與 GitHub Actions, GitLab CI, Jenkins 等主流 CI/CD 平台的整合方案。例如在 GitHub Actions 中,可以直接使用官方提供的 aquasecurity/trivy-action,當 PR 建立時自動掃描 image 並將結果輸出至 GitHub Security 分頁,藉此達到自動化的防護。
實際使用方式
我日常的開發習慣是:先把專案的 Docker image build 出來,然後用 Trivy 掃這個 image。相較於掃 filesystem,掃 image 的好處是它反映的是最終部署到生產環境的真實狀態,不會有「本機能跑、但 container 裡面環境有漏洞」的落差。
Trivy 最方便的地方在於不需安裝,直接透過 Docker 執行即可:
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v $(pwd)/.trivycache:/root/.cache/trivy \
-v $(pwd):/output \
aquasec/trivy:latest image \
--scanners vuln \
--format json \
--output /output/raw-report.json \
your-image:latest
幾個掛載點說明:
-v /var/run/docker.sock:/var/run/docker.sock:讓 Trivy container 可以存取本機的 Docker daemon,讀取剛 build 好的 image。-v $(pwd)/.trivycache:/root/.cache/trivy:把漏洞資料庫快取到本地目錄,避免每次執行都重新下載。-v $(pwd):/output:將當前目錄掛載進去,讓掃描報告能存回本機。
這樣執行完後,Trivy 會輸出一份原汁原味的 JSON 報告(raw-report.json)。
觀察 raw-report.json 的結構
這份 raw-report.json 非常詳細,動輒幾萬行程式碼。裡面包含了各個套件的完整依賴樹、Image config 等等資訊。但身為想知道「哪裡有洞、該升哪個版本」的開發者,大部分資訊其實我們不需要看:
展開查看 raw-report.json 結構範例
{
"SchemaVersion": 2,
"ArtifactName": "your-image:latest",
"Metadata": {
"OS": { "Family": "alpine", "Name": "3.21.5" },
"ImageConfig": { /* ... */ }
},
"Results": [
{
"Target": "your-image:latest (alpine 3.21.5)",
"Class": "os-pkgs",
"Type": "alpine",
"Vulnerabilities": [
{
"VulnerabilityID": "CVE-2025-15467",
"PkgName": "libcrypto3",
"Severity": "CRITICAL",
"FixedVersion": "3.3.6-r0",
"Title": "...",
"Description": "..."
}
]
},
{
"Target": "Node.js",
"Class": "lang-pkgs",
"Type": "npm",
"Vulnerabilities": [
{
"VulnerabilityID": "CVE-2026-25639",
"PkgName": "axios",
"PkgPath": "usr/src/app/node_modules/axios/package.json",
"Severity": "HIGH",
"FixedVersion": "1.13.5, 0.30.3"
}
]
}
]
}
從上面的結構可以觀察出,漏洞都統整在 Results 陣列裡的 Vulnerabilities 欄位中,並且可以透過 Class 欄位來區別這個漏洞是屬於系統套件還是語言套件。
如何區分漏洞的位置?
第一層:用 Class 區分系統套件與語言套件
Trivy 預設會根據 Class 欄位將安全漏洞分成幾個類型:
| Class | 說明 |
|---|---|
os-pkgs | 作業系統套件:由 OS 的套件管理員安裝的套件(如 Alpine 的 apk、Debian 的 apt) |
lang-pkgs | 語言生態系套件:npm、pip、gem、maven 等安裝的套件 |
不同類型的漏洞,我們的處理對策也不同:
os-pkgs:通常需要升級 Base Image(例如從node:22-alpine3.20換成node:22-alpine3.21),或在 Dockerfile 中明確執行安裝更新版的系統套件指令。lang-pkgs:需要在package.json等依賴清單中提升套件版本,並重新安裝以進行更新。
第二層:透過 PkgPath 細分 npm 套件的歸屬
lang-pkgs 裡面雖然都是 npm 套件,但並不代表全部都是 這個專案 自己安裝的。以 Node.js 的 image 為例,常見的套件路徑有兩種:
-
usr/local/lib/node_modules/npm/node_modules/...這其實是 npm 工具本身的依賴套件,是從 Node.js 官方 image 帶進來的環境問題。這些套件漏洞通常需要等官方推出新版 Node image 才能一併解決,我們若只改專案內的package.json是沒辦法修補到它的。 -
usr/src/app/node_modules/...這才是我們專案透過npm install裝進來的套件,這也是我們主要需要動手升級版本的目標。
因此,每當我看到標註 HIGH 或 CRITICAL 的漏洞時,我不會急著去翻 package.json,而是先看一眼 PkgPath,藉此確認漏洞的來源。
用 jq 整理報告內容
既然我們知道要看什麼欄位了,我通常會用一段 jq 腳本,把那動輒幾萬行的 raw-report.json ,精簡成只包含 Class 分類以及我們需 要的重點屬性:
展開查看完整腳本與 jq 過濾器
#!/bin/bash
TARGET_IMAGE="your-image:latest"
echo "🚀 開始掃描 $TARGET_IMAGE..."
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v $(pwd)/.trivycache:/root/.cache/trivy \
-v $(pwd):/output \
aquasec/trivy:latest image \
--scanners vuln \
--format json \
--output /output/raw-report.json \
$TARGET_IMAGE
echo "✅ 掃描完成,正在整理報告..."
jq 'reduce (.Results[]? | select(.Vulnerabilities)) as $res ({};
.[$res.Class] += ($res.Vulnerabilities | map({
VulnerabilityID: .VulnerabilityID,
PkgID: .PkgID,
PkgName: .PkgName,
PkgPath: .PkgPath,
Title: .Title,
Description: .Description,
Severity: .Severity,
FixedVersion: .FixedVersion
}))
)' raw-report.json > clean-report.json
echo "🎉 整理完成!請查看檔案: clean-report.json"
先找出 Results 內帶有 Vulnerabilities 的節點,接著根據我們剛剛討論過的 Class 來做歸類(map),最後只留下 VulnerabilityID、PkgPath、FixedVersion 等能夠幫助我們迅速定位和修復漏洞的核心屬性。
進階指令與使用技巧
為了適應不同的情境,我們也可以加上常見的 CLI flags 來調整 Trivy 掃描的行為。(可參閱 官方 CLI 說明 了解更多)
--download-db-only:預先下載漏洞資料庫
首次執行 Trivy 時,程式會先從遠端下載數百 MB 的漏洞資料庫。如果希望將下載資料與後續的掃描任務拆開,可以使用這個 flag 先把資料庫庫存抓下來:
docker run --rm \
-v $(pwd)/.trivycache:/root/.cache/trivy \
aquasec/trivy:latest image \
--download-db-only
這在環境網路不穩定,或是設計 CI/CD pipeline 階段的優化時相當實用。
--skip-update:跳過資料庫更新
如果在離線環境,或是已經確認快取資料是最新的,不想在每次跑掃描時都做一次更新查詢的話,加上 --skip-update 便能直接使用目前的本地資料庫快取,大幅節省執行時間。
docker run --rm \
# ... 省略 volume 掛載等基本指令 ...
aquasec/trivy:latest image \
--scanners vuln \
--skip-update \
--format json \
--output /output/raw-report.json \
your-image:latest
--ignore-unfixed:過濾掉未有修復版本的漏洞
有些掃描出來的漏洞,當前可能還沒有發布對應的修復版本(Fixed Version)。這些問題雖然客觀存在,但從實務維護的角度來看,因為開發團隊也沒有升級選項可以選擇,我們其實難以著手修補這些問題。
加上 --ignore-unfixed 就能讓 Trivy 將這些沒有對應修復版本的項目從報告中隱藏:
docker run --rm \
# ... 省略 volume 掛載等基本指令 ...
aquasec/trivy:latest image \
--scanners vuln \
--ignore-unfixed \
--format json \
--output /output/raw-report.json \
your-image:latest
這個選項幫助我在看報表時,可以把精力聚焦在「現在立刻能動手修復」的重大風險上,避免被過多無能為力的警告給分散了注意力。
結語
自從將 Trivy 納入日常開發流程後,現在不用再被動地等 DevOps 通知,在開發階段便有能力提前發現並修補存在的漏洞,漏洞修補完成後也能夠自行確認,這點對於大幅提升整個團隊的工作與溝通效率非常有幫助。
不過,這篇文章主要著重在「如何透過 Trivy 提早找出專案套件的 CVE 漏洞」。當我們拿到掃描報告,知道哪裡有洞之後,下一步就是實際動手把洞補起來。而關於「該怎麼修補 Node.js 專案的依賴漏洞」,這部分其實也有不少眉角,之後有時間再與大家分享我的實戰做法。