最近在做优化的时候涉及到了这块内容,觉得值得写下来,方便以后翻阅。
👨🎓博主简介
🏅
这篇目录
三、命令及参数 3.4 fs 命令参数详解3.5 k8s 命令参数详解3.6 全局选项3.7 环境变量 四、基础扫描实战 五、高级扫描技巧 六、CI/CD 集成实战 七、Kubernetes 场景:Trivy Operator八、最佳实践与避坑指南 九、常用错误 十、总结在容器化部署成为主流的今天,镜像安全不再是"可选项",而是生产环境的"必选项"。Trivy 作为 Aqua Security 开源的漏洞扫描工具,凭借零配置、速度快、覆盖广的特点,已成为 GitHub 上 Star 数 18k+ 的明星项目,被苹果、Adobe、网易等大厂广泛采用。
一、为什么选择 Trivy?
在选型容器扫描工具时,Trivy 相比传统方案(如 Clair)有显著优势:
特性Trivy传统扫描器安装复杂度单二进制文件,开箱即用需配置数据库、策略文件扫描速度比 Clair 快约 3 倍首次扫描需长时间初始化覆盖范围镜像 + 文件系统 + K8s + IaC通常仅支持镜像漏洞库更新最快 4 小时内推送更新周期较长离线能力支持离线扫描多数依赖在线服务
- Trivy 官网:https://trivy.dev/
- 开源地址:https://github.com/aquasecurity/trivy
二、在 Linux 上安装 Trivy
推荐使用方法2,理由为:安全性高、支持离线安装、版本可控、卸载方便、安装位置灵活、便于批量部署和自动化
方法一:官方脚本快速安装(推荐)
# 一键安装到 /usr/local/bin
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# 验证安装
trivy --version
方法二:手动下载安装
# 下载对应版本(以 v0.73.0 为例)
wget https://github.com/aquasecurity/trivy/releases/download/v0.73.0/trivy_0.73.0_Linux-64bit.tar.gz
# 解压并安装
tar -zxvf trivy_0.73.0_Linux-64bit.tar.gz
chmod +x trivy
cp trivy /usr/local/bin/
# 验证安装
trivy --version
- github各版本下载地址:https://github.com/aquasecurity/trivy/tags
- 版本的多种部署方法下载地址:https://github.com/aquasecurity/trivy/releases/tag/v0.73.0(包含:rpm、deb、arm、linux、windows等等,根据自己所需进行下载)
方法三:Docker 方法运行
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:latest image nginx:1.25.0
三、命令及参数
3.1 命令结构
trivy [全局选项] [命令选项]
3.2 核心命令
命令说明示例image扫描容器镜像trivy image nginx:1.25.0fs扫描本地文件系统trivy fs /path/to/projectrepo扫描远程代码仓库trivy repo https://github.com/user/reposbom扫描 SBOM 文件trivy sbom /path/to/sbom.jsonk8s扫描 Kubernetes 集群trivy k8s --report summary clustervm扫描虚拟机镜像trivy vm disk.imgaws扫描 AWS 云资源trivy aws --region us-east-1convert转换报告格式trivy convert -f html -o report.html result.jsonserver启动 Trivy 服务器模式trivy server --listen 0.0.0.0:8080
3.3 image 命令参数详解
基础参数
参数简写说明示例--severity-s指定漏洞严重级别--severity HIGH,CRITICAL--vuln-type漏洞类型:os/library/os,library--vuln-type os,library--ignore-unfixed忽略无修复方案的漏洞--ignore-unfixed--exit-code注意到漏洞时的退出码--exit-code 1--timeout扫描超时时间--timeout 10m--skip-db-update跳过漏洞数据库更新--skip-db-update--skip-java-db-update跳过 Java 数据库更新--skip-java-db-update--download-db-only仅下载数据库,不扫描--download-db-only--input扫描镜像 tar 包--input nginx.tar
漏洞严重级别
级别(英文)中文含义CVSS 分数范围风险说明CRITICAL严重9.0 - 10.0极易被利用,可能导致系统完全沦陷,必须立即修复HIGH高危7.0 - 8.9容易被利用,可能造成严重数据泄露或权限提升,应尽快修复MEDIUM中危4.0 - 6.9利用条件较复杂,有一定安全风险,建议规划修复LOW低危0.1 - 3.9难以利用,影响有限,可按常规周期处理UNKNOWN未知N/A暂无评分信息,需要人工评估
输出格式参数
参数简写说明示例--format-f输出格式-f json--output-o输出到文件-o report.json--template-t使用自定义模板-t @html.tpl--list-all-pkgs列出所有包(不仅是有漏洞的)--list-all-pkgs
支持的格式:
table(默认,终端表格)json(JSON 格式)sarif(SARIF 标准格式,用于 GitHub Security)cyclonedx(CycloneDX SBOM 格式)spdx(SPDX SBOM 格式)template(自定义模板)
3.4 fs 命令参数详解
trivy fs [选项]
参数说明示例--severity指定严重级别--severity CRITICAL--scanners扫描器类型--scanners vuln,secret,config--skip-dirs跳过指定目录--skip-dirs node_modules,vendor--skip-files跳过指定文件--skip-files "*.test.js"--include-dev-deps包含开发依赖--include-dev-deps
3.5 k8s 命令参数详解
trivy k8s [选项] [资源]
参数说明示例--report报告格式:summary/all--report summary--namespace指定命名空间--namespace default--components扫描组件--components workload
3.6 全局选项
参数简写说明示例--cache-dir缓存目录--cache-dir /tmp/trivy-cache--quiet-q静默模式,只输出结果-q--debug-d调试模式,输出详细日志-d--insecure-k跳过 TLS 证书验证-k--offline-scan完全离线扫描--offline-scan
3.7 环境变量
变量说明示例TRIVY_DB_REPOSITORY漏洞数据库镜像地址registry.cn-hangzhou.aliyuncs.com/aquasec/trivy-dbTRIVY_JAVA_DB_REPOSITORYJava 数据库镜像地址registry.cn-hangzhou.aliyuncs.com/aquasec/trivy-java-dbTRIVY_SEVERITY默认严重级别CRITICAL,HIGHTRIVY_IGNORE_UNFIXED默认忽略无修复漏洞trueHTTP_PROXY / HTTPS_PROXY代 理设置http://proxy:8080
四、基础扫描实战
4.1 扫描本地 Docker 镜像
# 基础扫描(表格形式输出)
trivy image nginx:1.25.0
# 扫描指定严重级别的漏洞
trivy image --severity HIGH,CRITICAL nginx:1.25.0
# 只显示有修复方案的漏洞(避免被大量无修复漏洞刷屏)
trivy image --ignore-unfixed nginx:1.25.0
输出示例:
┌────────────────────┬─────────────────────┬──────────┬──────────────┬─────────────────────────┬──────────────────────────┬──────────────────────────────────────────────────────────────┐
│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │
├────────────────────┼─────────────────────┼──────────┼──────────────┼─────────────────────────┼──────────────────────────┼──────────────────────────────────────────────────────────────┤
│ apt │ CVE-2011-3374 │ LOW │ affected │ 2.2.4 │ │ It was found that apt-key in apt, all versions, do not │
│ │ │ │ │ │ │ correctly... │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2011-3374 │
└────────────────────┴─────────────────────┴──────────┴──────────────┴─────────────────────────┴──────────────────────────┴──────────────────────────────────────────────────────────────┘
4.2 扫描远程仓库镜像
# Docker Hub 镜像
trivy image 172.16.10.224:10010/library/nginx:1.25.0
# 私有仓库(需认证)
trivy image --username admin --password $PASS \
registry.example.com/app:v1.0
4.3 扫描镜像 tar 包
# 导出镜像后扫描
docker save nginx:1.25.0 -o nginx-1.25.0.tar
trivy image --input nginx-1.25.0.tar
五、高级扫描技巧
5.1 多种输出格式
# 基础扫描(表格形式输出)并存到指定文件中,方便保存及阅读
trivy image nginx:1.25.0 -o nginx-1.25.0.log
# JSON 格式(适合程序化处理)
trivy image -f json -o nginx-1.25.0.json nginx:1.25.0
# SARIF 格式(可导入 GitHub Security tab)
trivy image -f sarif -o results.sarif nginx:1.25.0
# HTML 报告(适合人工审阅,没有模板的可以看下一步)
trivy image \
--format template \
--template @/usr/local/share/trivy/contrib/html.tpl \
-o report.html nginx:1.25.0
- 添加 html 模板
# 创建模板目录
mkdir -p ~/.config/trivy/templates
# 可以下载官方 HTML 模板
curl -L -o ~/.config/trivy/templates/html.tpl \
https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/html.tpl
# 也可以采用我修改过的模板,官方的模板和默认的表格形式的不一样,所以我改了一下,加了筛选。
- 模板内容如下:
{{- if . }}
* {
font-family: Arial, Helvetica, sans-serif;
box-sizing: border-box;
}
body {
margin: 0;
padding: 10px 16px 24px;
background: #ffffff;
color: #111111;
}
h1 {
text-align: center;
margin: 8px 0 12px;
}
h2 {
margin: 22px 0 10px;
padding: 8px 12px;
background: #f0f2f5;
border-left: 6px solid #4472C4;
}
h3 {
margin: 12px 0 6px;
padding: 8px 12px;
background: #f7f7f7;
border: 1px solid #dddddd;
}
.target-path {
font-weight: bold;
word-break: break-all;
}
.target-meta {
margin-left: 10px;
color: #666666;
font-size: 13px;
font-weight: normal;
}
table, th, td {
border: 1px solid black;
border-collapse: collapse;
padding: .3em;
}
table {
width: 100%;
margin: 0 auto 14px;
}
th {
background: #fafafa;
white-space: nowrap;
}
td {
vertical-align: top;
}
.table-wrap {
width: 100%;
overflow-x: auto;
}
.severity {
text-align: center;
font-weight: bold;
color: #fafafa;
white-space: nowrap;
}
.severity-LOW .severity { background-color: #00B050; }
.severity-MEDIUM .severity { background-color: #FFC000; color: #111111; }
.severity-HIGH .severity { background-color: #FF6600; }
.severity-CRITICAL .severity { background-color: #C00000; }
.severity-UNKNOWN .severity { background-color: #808080; }
.severity-LOW { background-color: #00B05030; }
.severity-MEDIUM { background-color: #FFC00030; }
.severity-HIGH { background-color: #FF660030; }
.severity-CRITICAL { background-color: #C0000030; }
.severity-UNKNOWN { background-color: #80808030; }
.pkg-name, .target-cell {
font-weight: bold;
}
.target-cell {
white-space: normal;
word-break: break-all;
}
.title-cell, .links {
white-space: normal;
min-width: 320px;
word-break: break-word;
}
.pkg-name, .pkg-version, .fixed-version {
white-space: normal;
word-break: break-word;
}
.jar-cell {
font-weight: bold;
white-space: nowrap;
word-break: keep-all;
}
.jar-target-cell {
white-space: normal;
word-break: break-all;
color: #333333;
}
.status-cell {
text-align: center;
white-space: nowrap;
}
.no-data {
text-align: center;
color: #666666;
font-weight: bold;
}
.summary-bar {
text-align: center;
margin: 10px 0;
font-size: 14px;
}
.summary-bar span {
display: inline-block;
margin: 3px 6px;
padding: 4px 10px;
border-radius: 3px;
font-weight: bold;
}
.summary-critical { background: #C00000; color: white; }
.summary-high { background: #FF6600; color: white; }
.summary-medium { background: #FFC000; color: #111111; }
.summary-low { background: #00B050; color: white; }
.summary-unknown { background: #808080; color: white; }
.category-summary {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
margin: 8px 0 12px;
}
.category-card {
min-width: 260px;
padding: 10px 14px;
border: 1px solid #dddddd;
border-radius: 6px;
background: #fafafa;
text-align: center;
}
.category-card strong {
display: block;
margin-bottom: 4px;
font-size: 16px;
}
.language-bar {
text-align: right;
margin: 4px 0 10px;
}
.lang-btn {
margin-left: 6px;
padding: 6px 12px;
border: 1px solid #4472C4;
border-radius: 4px;
background: #ffffff;
color: #4472C4;
cursor: pointer;
font-weight: bold;
}
.lang-btn.active {
background: #4472C4;
color: #ffffff;
}
.filter-bar {
text-align: center;
margin: 14px 0 18px;
padding: 10px;
background: #f5f5f5;
border-radius: 5px;
}
.filter-btn {
margin: 4px 5px;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
color: white;
}
.filter-btn.all { background: #4472C4; }
.filter-btn.os { background: #5B9BD5; }
.filter-btn.java { background: #70AD47; }
.filter-btn.critical { background: #C00000; }
.filter-btn.high { background: #FF6600; }
.filter-btn.medium { background: #FFC000; color: #111111; }
.filter-btn.low { background: #00B050; }
.filter-btn.unknown { background: #808080; }
.filter-btn.active { outline: 3px solid #333333; }
.filter-btn:hover { opacity: 0.85; }
.summary-table th, .summary-table td {
text-align: center;
white-space: nowrap;
}
.summary-table .target-cell {
text-align: left;
}
.summary-table .jar-target .target-cell::before {
content: "JAR ";
display: inline-block;
margin-right: 5px;
padding: 1px 5px;
border-radius: 3px;
background: #70AD47;
color: #ffffff;
font-size: 12px;
}
.summary-table .os-target .target-cell::before {
content: "OS ";
display: inline-block;
margin-right: 5px;
padding: 1px 5px;
border-radius: 3px;
background: #5B9BD5;
color: #ffffff;
font-size: 12px;
}
.links a,
.links[data-more-links=on] a {
display: block;
}
.links[data-more-links=off] a:nth-of-type(1n+5) {
display: none;
}
a.toggle-more-links { cursor: pointer; }
{{- escapeXML ( index . 0 ).Target }} - Trivy Report - {{ now }}
var severityOrder = { "CRITICAL": 1, "HIGH": 2, "MEDIUM": 3, "LOW": 4, "UNKNOWN": 5 };
var currentCategory = "ALL";
var currentSeverity = "ALL";
var currentLang = "en";
var I18N = {
en: {
languageLabel: "Language",
catAll: "All Types",
catOs: "OS / Base Image Only",
catJava: "Java / JAR Only",
sevAll: "All Severities",
sevCritical: "CRITICAL",
sevHigh: "HIGH",
sevMedium: "MEDIUM",
sevLow: "LOW",
sevUnknown: "UNKNOWN",
summaryTitle: "Scan Object Summary",
osSectionTitle: "OS / Base Image Vulnerabilities",
javaSectionTitle: "Java / JAR Vulnerabilities",
javaNote: "Note: each Java vulnerability shows only the outermost JAR in the JAR column. The Library column shows only the dependency name and does not repeat the JAR name.",
target: "Target",
type: "Type",
vulnerabilities: "Vulnerabilities",
jar: "JAR",
library: "Library",
vulnerability: "Vulnerability",
severity: "Severity",
status: "Status",
installedVersion: "Installed Version",
fixedVersion: "Fixed Version",
titleLinks: "Title / Links",
noVulns: "No Vulnerabilities found",
loading: "Loading...",
moreLinks: "Show / Hide More Links",
osSummaryTitle: "OS / Base Image Vulnerabilities",
osSummaryText: "Targets: {targets}, Vulnerabilities: {vulns}",
javaSummaryTitle: "Java / JAR Vulnerabilities",
javaSummaryText: "JARs: {targets}, Vulnerabilities: {vulns}",
misconfigurations: "Misconfigurations",
misconfType: "Type",
misconfId: "Misconf ID",
check: "Check",
message: "Message"
},
zh: {
languageLabel: "语言",
catAll: "全部类型",
catOs: "仅操作系统/基础镜像",
catJava: "仅 Java / JAR",
sevAll: "全部级别",
sevCritical: "严重 (CRITICAL)",
sevHigh: "高危 (HIGH)",
sevMedium: "中危 (MEDIUM)",
sevLow: "低危 (LOW)",
sevUnknown: "未知 (UNKNOWN)",
summaryTitle: "扫描对象概览",
osSectionTitle: "操作系统/基础镜像漏洞",
javaSectionTitle: "Java / JAR 漏洞",
javaNote: "说明:每条 Java 漏洞只在 JAR 列显示最外层 Jar 包;Library 列只显示依赖名称,不再重复显示 Jar 包名称。",
target: "扫描对象",
type: "类型",
vulnerabilities: "漏洞数",
jar: "Jar 包",
library: "依赖库",
vulnerability: "漏洞编号",
severity: "严重级别",
status: "状态",
installedVersion: "当前版本",
fixedVersion: "修复版本",
titleLinks: "标题 / 链接",
noVulns: "未发现漏洞",
loading: "加载中...",
moreLinks: "显示/隐藏更多链接",
osSummaryTitle: "操作系统/基础镜像漏洞",
osSummaryText: "扫描对象:{targets} 个,漏洞:{vulns} 个",
javaSummaryTitle: "Java / JAR 漏洞",
javaSummaryText: "JAR 包:{targets} 个,漏洞:{vulns} 个",
misconfigurations: "配置问题",
misconfType: "类型",
misconfId: "配置项编号",
check: "检查项",
message: "说明"
}
};
window.onload = function() {
sortTablesBySeverity();
prepareLinks();
prepareJarRows();
rebuildJavaSummaryRows();
rebuildJavaCleanRows();
setLanguage('zh');
updateSummary();
applyFilters();
};
function t(key) {
return (I18N[currentLang] && I18N[currentLang][key]) || I18N.en[key] || key;
}
function setLanguage(lang) {
currentLang = lang || 'en';
document.documentElement.setAttribute('lang', currentLang === 'zh' ? 'zh-CN' : 'en');
document.querySelectorAll('[data-i18n]').forEach(function(el) {
var key = el.getAttribute('data-i18n');
el.textContent = t(key);
});
document.querySelectorAll('.lang-btn').forEach(function(btn) {
btn.classList.toggle('active', btn.getAttribute('data-lang') === currentLang);
});
document.querySelectorAll('a.toggle-more-links').forEach(function(link) {
link.textContent = t('moreLinks');
});
updateSummary();
}
function prepareLinks() {
document.querySelectorAll('td.links').forEach(function(linkCell) {
var links = Array.prototype.slice.call(linkCell.querySelectorAll('a:not(.toggle-more-links)'));
links.sort(function(a, b) {
return a.href > b.href ? 1 : -1;
});
links.forEach(function(link, idx) {
if (links.length > 4 && idx === 4) {
var toggleLink = document.createElement('a');
toggleLink.innerText = t("moreLinks");
toggleLink.href = "#toggleMore";
toggleLink.setAttribute("class", "toggle-more-links");
linkCell.appendChild(toggleLink);
}
linkCell.appendChild(link);
});
});
document.querySelectorAll('a.toggle-more-links').forEach(function(toggleLink) {
toggleLink.onclick = function() {
var expanded = toggleLink.parentElement.getAttribute("data-more-links");
toggleLink.parentElement.setAttribute("data-more-links", "on" === expanded ? "off" : "on");
return false;
};
});
}
function basenameOf(path) {
if (!path) { return ''; }
var text = String(path).trim();
return text.split(/[\\/]/).pop() || text;
}
function outermostArchivePath(path) {
var text = String(path || '').trim();
if (!text) { return ''; }
var normalized = text.replace(/\\/g, '/');
var match = normalized.match(/^(.+?\.(?:jar|war|ear|par))(?:[!\/].*)?$/i);
return match ? match[1] : text;
}
function extractJarFromLibrary(libraryText) {
var text = String(libraryText || '');
var match = text.match(/\(([^()]*\.(?:jar|war|ear|par))\)\s*$/i);
return match ? outermostArchivePath(match[1]) : '';
}
function stripTrailingArchiveSuffix(text) {
return String(text || '').replace(/\s*\([^()]*\.(?:jar|war|ear|par)\)\s*$/i, '').trim();
}
function resolveJarPath(row) {
var jarPath = outermostArchivePath((row.getAttribute('data-jar-path') || '').trim());
var resultTarget = outermostArchivePath((row.getAttribute('data-result-target') || '').trim());
var libraryText = row.getAttribute('data-library-raw') || '';
var jarFromLibrary = extractJarFromLibrary(libraryText);
if (!jarPath || /^java$/i.test(jarPath)) {
jarPath = jarFromLibrary || resultTarget;
}
if (!jarPath || /^java$/i.test(jarPath)) {
jarPath = '未知Jar包';
}
return jarPath;
}
function prepareJarRows() {
document.querySelectorAll('tr.vuln-row[data-category="java"]').forEach(function(row) {
var jarPath = resolveJarPath(row);
var jarName = basenameOf(jarPath);
var jarCell = row.querySelector('.jar-cell');
var libraryCell = row.querySelector('.pkg-name');
var rawLibrary = row.getAttribute('data-library-raw') || (libraryCell ? libraryCell.textContent : '');
row.setAttribute('data-target', jarPath);
row.setAttribute('data-jar-full', jarPath);
row.setAttribute('data-jar-name', jarName);
if (jarCell) {
// Java / JAR 漏洞明细只显示 JAR 文件名;
// 完整路径仍保留在 title / data-jar-full 中,概览继续显示完整路径。
jarCell.setAttribute('title', jarPath);
jarCell.textContent = jarName;
}
if (libraryCell) {
var shownLibrary = stripTrailingArchiveSuffix(rawLibrary);
libraryCell.textContent = shownLibrary || rawLibrary;
libraryCell.setAttribute('title', rawLibrary);
}
});
}
function rebuildJavaSummaryRows() {
var tbody = document.querySelector('#targetSummary tbody');
if (!tbody) { return; }
// Java Result.Target 通常只是 "Java",不能把它当成 JAR 名称。
// JAR 的真实路径优先来自 Result.Packages[].FilePath;
// 如果 Packages 中没有,则用 Vulnerabilities[].PkgPath 作为兼容回退。
tbody.querySelectorAll('tr[data-category="java"]').forEach(function(row) {
row.parentNode.removeChild(row);
});
var jars = {};
document.querySelectorAll('.jar-package-source').forEach(function(source) {
var jarPath = outermostArchivePath(source.getAttribute('data-jar-path') || '');
if (jarPath && /\.(?:jar|war|ear|par)$/i.test(jarPath)) {
jars[jarPath] = true;
}
});
// 兼容没有 Packages 数据、但存在漏洞的情况。
document.querySelectorAll('tr.vuln-row[data-category="java"]').forEach(function(row) {
var jarPath = resolveJarPath(row);
if (jarPath && !/^未知Jar包$/i.test(jarPath)) {
jars[jarPath] = true;
}
});
Object.keys(jars).sort(function(a, b) { return a.localeCompare(b); }).forEach(function(jarPath) {
var tr = document.createElement('tr');
tr.className = 'jar-target';
tr.setAttribute('data-category', 'java');
tr.setAttribute('data-target', jarPath);
tr.innerHTML =
'' + escapeHtml(jarPath) + '' +
'jar' +
'0' +
'0' +
'0' +
'0' +
'0' +
'0';
tbody.appendChild(tr);
});
}
function rebuildJavaCleanRows() {
document.querySelectorAll('tr.clean-jar-row').forEach(function(row) {
row.parentNode.removeChild(row);
});
document.querySelectorAll('.target-block[data-category="java"]').forEach(function(block) {
var resultTarget = block.getAttribute('data-result-target') || block.getAttribute('data-target') || '';
var tbody = block.querySelector('tbody.vuln-body');
if (!tbody) { return; }
var inventory = {};
document.querySelectorAll('.jar-package-source').forEach(function(source) {
var sourceTarget = source.getAttribute('data-result-target') || '';
if (sourceTarget !== resultTarget) { return; }
var jarPath = outermostArchivePath(source.getAttribute('data-jar-path') || '');
if (jarPath && /\.(?:jar|war|ear|par)$/i.test(jarPath)) {
inventory[jarPath] = true;
}
});
// 漏洞中出现、但 package inventory 未出现的 JAR 也纳入展示。
block.querySelectorAll('tr.vuln-row[data-category="java"]').forEach(function(row) {
var jarPath = resolveJarPath(row);
if (jarPath && !/^未知Jar包$/i.test(jarPath)) {
inventory[jarPath] = true;
}
});
Object.keys(inventory).sort(function(a, b) { return a.localeCompare(b); }).forEach(function(jarPath) {
var hasVulnerability = Array.prototype.some.call(
block.querySelectorAll('tr.vuln-row[data-category="java"]'),
function(row) { return resolveJarPath(row) === jarPath; }
);
if (hasVulnerability) { return; }
var tr = document.createElement('tr');
tr.className = 'clean-jar-row';
tr.setAttribute('data-category', 'java');
tr.setAttribute('data-target', jarPath);
var jarName = basenameOf(jarPath);
tr.innerHTML =
'' + escapeHtml(jarName) + '' +
'No Vulnerabilities found';
tbody.appendChild(tr);
});
});
}
function escapeHtml(text) {
return String(text || '').replace(/[&"']/g, function(ch) {
return ({ '&': '&', '': '>', '"': '"', "'": ''' })[ch];
});
}
function sortTablesBySeverity() {
document.querySelectorAll('tbody.vuln-body').forEach(function(tbody) {
var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr.vuln-row'));
rows.sort(function(a, b) {
var sevA = a.getAttribute('data-severity') || 'UNKNOWN';
var sevB = b.getAttribute('data-severity') || 'UNKNOWN';
return severityOrder[sevA] - severityOrder[sevB];
});
rows.forEach(function(row) { tbody.appendChild(row); });
});
}
function setCategoryFilter(category) {
currentCategory = category;
setActiveButton('category', category);
applyFilters();
}
function setSeverityFilter(severity) {
currentSeverity = severity;
setActiveButton('severity', severity);
applyFilters();
}
function setActiveButton(group, value) {
document.querySelectorAll('.filter-btn[data-filter-group="' + group + '"]').forEach(function(btn) {
btn.classList.toggle('active', btn.getAttribute('data-filter-value') === value);
});
}
function applyFilters() {
document.querySelectorAll('tr.vuln-row').forEach(function(row) {
var rowCategory = row.getAttribute('data-category');
var rowSeverity = row.getAttribute('data-severity');
var showCategory = (currentCategory === 'ALL' || rowCategory === currentCategory);
var showSeverity = (currentSeverity === 'ALL' || rowSeverity === currentSeverity);
row.style.display = (showCategory && showSeverity) ? '' : 'none';
});
document.querySelectorAll('tr.clean-jar-row').forEach(function(row) {
var rowCategory = row.getAttribute('data-category');
var showCategory = (currentCategory === 'ALL' || rowCategory === currentCategory);
row.style.display = (showCategory && currentSeverity === 'ALL') ? '' : 'none';
});
document.querySelectorAll('.target-block').forEach(function(block) {
var blockCategory = block.getAttribute('data-category');
var categoryOk = (currentCategory === 'ALL' || blockCategory === currentCategory);
var allVulnRows = block.querySelectorAll('tr.vuln-row').length;
var visibleRows = block.querySelectorAll('tr.vuln-row:not([style*="display: none"])').length;
/*
* 在“全部严重级别”下,0 漏洞对象也要显示。
* 这样只要 Trivy 扫描到了 JAR,对应 block 就会保留,
* 并显示模板已有的“未发现漏洞”提示。
* 选择具体严重级别时,仍只展示真正命中的对象。
*/
var showBlock = false;
if (categoryOk) {
if (currentSeverity === 'ALL') {
showBlock = true;
} else {
showBlock = (allVulnRows > 0 && visibleRows > 0);
}
}
block.style.display = showBlock ? '' : 'none';
});
document.querySelectorAll('.category-section').forEach(function(section) {
var visibleBlocks = section.querySelectorAll('.target-block:not([style*="display: none"])').length;
section.style.display = visibleBlocks > 0 ? '' : 'none';
});
}
function updateSummary() {
var total = { "CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "UNKNOWN": 0 };
var category = {
"os": { targets: 0, vulns: 0 },
"java": { targets: 0, vulns: 0 }
};
var byTarget = {};
document.querySelectorAll('#targetSummary tbody tr').forEach(function(row) {
var rowCategory = row.getAttribute('data-category');
if (category[rowCategory]) {
category[rowCategory].targets++;
}
});
document.querySelectorAll('tr.vuln-row').forEach(function(row) {
var sev = row.getAttribute('data-severity') || 'UNKNOWN';
var rowCategory = row.getAttribute('data-category') || 'os';
var target = row.getAttribute('data-target') || '';
// Java 漏洞必须按真实 JAR 路径统计,不能按 Result.Target=Java 汇总。
if (rowCategory === 'java') {
target = resolveJarPath(row);
}
if (!total[sev] && total[sev] !== 0) {
total[sev] = 0;
}
total[sev]++;
if (category[rowCategory]) {
category[rowCategory].vulns++;
}
if (!byTarget[target]) {
byTarget[target] = { "CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "UNKNOWN": 0, total: 0 };
}
if (!byTarget[target][sev] && byTarget[target][sev] !== 0) {
byTarget[target][sev] = 0;
}
byTarget[target][sev]++;
byTarget[target].total++;
});
var summaryDiv = document.getElementById('summary');
if (summaryDiv) {
summaryDiv.innerHTML =
'CRITICAL: ' + total.CRITICAL + '' +
'HIGH: ' + total.HIGH + '' +
'MEDIUM: ' + total.MEDIUM + '' +
'LOW: ' + total.LOW + '' +
'UNKNOWN: ' + total.UNKNOWN + '';
}
var osCard = document.getElementById('osSummary');
if (osCard) {
osCard.innerHTML = '**' + escapeHtml(t('osSummaryTitle')) + '**' +
escapeHtml(t('osSummaryText').replace('{targets}', category.os.targets).replace('{vulns}', category.os.vulns));
}
var javaCard = document.getElementById('javaSummary');
if (javaCard) {
javaCard.innerHTML = '**' + escapeHtml(t('javaSummaryTitle')) + '**' +
escapeHtml(t('javaSummaryText').replace('{targets}', category.java.targets).replace('{vulns}', category.java.vulns));
}
document.querySelectorAll('#targetSummary tbody tr').forEach(function(row) {
var target = row.getAttribute('data-target') || '';
var counts = byTarget[target] || { "CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "UNKNOWN": 0, total: 0 };
setCell(row, 'target-total', counts.total);
setCell(row, 'target-critical', counts.CRITICAL);
setCell(row, 'target-high', counts.HIGH);
setCell(row, 'target-medium', counts.MEDIUM);
setCell(row, 'target-low', counts.LOW);
setCell(row, 'target-unknown', counts.UNKNOWN);
});
}
function setCell(row, className, value) {
var cell = row.querySelector('.' + className);
if (cell) {
cell.innerText = value;
}
}
{{- end }}
{{- if . }}
# {{- escapeXML ( index . 0 ).Target }} - Trivy Report - {{ now }}
Language:
中文
English
Loading...
Loading...
Loading...
All Types
OS / Base Image Only
Java / JAR Only
All Severities
CRITICAL
HIGH
MEDIUM
LOW
UNKNOWN
## Scan Object Summary
Target
Type
Vulnerabilities
CRITICAL
HIGH
MEDIUM
LOW
UNKNOWN
{{- range $idx, $result := . }}
{{- $type := ($result.Type | toString) }}
{{- if ne $type "jar" }}
{{ escapeXML $result.Target }}
{{ escapeXML $type }}
{{ len $result.Vulnerabilities }}
0
0
0
0
0
{{- end }}
{{- end }}
{{- range $idx, $result := . }}
{{- $type := ($result.Type | toString) }}
{{- if eq $type "jar" }}
{{- range $pkg := $result.Packages }}
{{- if $pkg.FilePath }}
{{- end }}
{{- end }}
{{- range $vuln := $result.Vulnerabilities }}
{{- if $vuln.PkgPath }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
## OS / Base Image Vulnerabilities
{{- range $idx, $result := . }}
{{- $type := ($result.Type | toString) }}
{{- if ne $type "jar" }}
### {{ escapeXML $result.Target }}Type: {{ escapeXML $type }} | Vulnerabilities: {{ len $result.Vulnerabilities }}
{{- if eq (len $result.Vulnerabilities) 0 }}
No Vulnerabilities found
{{- else }}
Library
Vulnerability
Severity
Status
Installed Version
Fixed Version
Title / Links
{{- range $result.Vulnerabilities }}
{{ escapeXML .PkgName }}
{{ escapeXML .VulnerabilityID }}
{{ escapeXML .Severity }}
{{- $status := (printf "%d" .Status) -}}{{- if eq $status "0" -}}unknown{{- else if eq $status "1" -}}not_affected{{- else if eq $status "2" -}}affected{{- else if eq $status "3" -}}fixed{{- else if eq $status "4" -}}under_investigation{{- else if eq $status "5" -}}will_not_fix{{- else if eq $status "6" -}}fix_deferred{{- else if eq $status "7" -}}end_of_life{{- else -}}{{ escapeXML $status }}{{- end -}}
{{ escapeXML .InstalledVersion }}
{{ escapeXML .FixedVersion }}
{{ escapeXML .Title }}
{{- if .PrimaryURL }}{{ escapeXML .PrimaryURL }}{{ end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
## Java / JAR Vulnerabilities
Note: each Java vulnerability shows only the outermost JAR in the JAR column. The Library column shows only the dependency name and does not repeat the JAR name.
{{- range $idx, $result := . }}
{{- $type := ($result.Type | toString) }}
{{- if eq $type "jar" }}
### Java / JARType: {{ escapeXML $type }} | Vulnerabilities: {{ len $result.Vulnerabilities }}
JAR
Library
Vulnerability
Severity
Status
Installed Version
Fixed Version
Title / Links
{{- range $result.Vulnerabilities }}
{{ escapeXML .PkgName }}
{{ escapeXML .VulnerabilityID }}
{{ escapeXML .Severity }}
{{- $status := (printf "%d" .Status) -}}{{- if eq $status "0" -}}unknown{{- else if eq $status "1" -}}not_affected{{- else if eq $status "2" -}}affected{{- else if eq $status "3" -}}fixed{{- else if eq $status "4" -}}under_investigation{{- else if eq $status "5" -}}will_not_fix{{- else if eq $status "6" -}}fix_deferred{{- else if eq $status "7" -}}end_of_life{{- else -}}{{ escapeXML $status }}{{- end -}}
{{ escapeXML .InstalledVersion }}
{{ escapeXML .FixedVersion }}
{{ escapeXML .Title }}
{{- if .PrimaryURL }}{{ escapeXML .PrimaryURL }}{{ end }}
{{- end }}
{{- end }}
{{- end }}
{{- range . }}
{{- if gt (len .Misconfigurations) 0 }}
## Misconfigurations - {{ escapeXML .Target }}
Type
Misconf ID
Check
Severity
Message
{{- range .Misconfigurations }}
{{ escapeXML .Type }}
{{ escapeXML .ID }}
{{ escapeXML .Title }}
{{ escapeXML .Severity }}
{{ escapeXML .Message }}
{{- if .PrimaryURL }}{{ escapeXML .PrimaryURL }}{{ end }}
{{- end }}
{{- end }}
{{- end }}
{{- else }}
# Trivy Returned Empty Report
{{- end }}
- 使用模板生成报告(指定
-t要使用绝对路径)
trivy image \
--format template \
--template @/root/.config/trivy/templates/html.tpl \
-o nginx-1.25.0.html \
nginx:1.25.0
5.2 指定扫描类型
# 仅扫描 OS 包漏洞
trivy image --vuln-type os nginx:1.25.0
# 仅扫描应用依赖库漏洞
trivy image --vuln-type library nginx:1.25.0
# 同时扫描 OS 包和库(默认)
trivy image --vuln-type os,library nginx:1.25.0
5.3 设置超时与退出码
# 设置超时时间为 10 分钟
trivy image --timeout 10m nginx:1.25.0
# 发现漏洞时返回非零退出码(CI 流水线必备)
trivy image --exit-code 1 --severity CRITICAL nginx:1.25.0
⚠️ 重要提示1:Trivy 默认即使注意到漏洞也返回退出码 0,这是linux中命令只要执行成功了就是0,但在 CI/CD 中务必加上 --exit-code 1,否则流水线会"绿灯通过"存在严重漏洞的镜像。 ⚠️ 重要提示2:使用注意到漏洞时返回非零退出码时,他并不是不返回漏洞数据,而是:先输出完整的漏洞报告,最后再返回退出码,退出码可以使用命令查看:echo $?。
5.4 离线扫描(内网环境)
# 首次联网下载漏洞库
trivy image --download-db-only
# 下载java相关漏洞库
trivy image --download-java-db-only
# 后续离线扫描(跳过指定数据库进行扫描)
trivy image --skip-db-update --skip-java-db-update nginx:1.25.0
六、CI/CD 集成实战
6.1 GitHub Actions 集成
name: Security Scan
on: [push, pull_request]
jobs:
trivy-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker Image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Trivy Scan
uses: aquasecurity/trivy-action@0.29.0
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
ignore-unfixed: true
exit-code: '1'
- name: Upload to GitHub Security
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
- 加注释
name: Security Scan
# 工作流的名称,显示在 GitHub Actions 页面
on: [push, pull_request]
# 触发条件:当代码被 push(推送)或 pull_request(提交 PR)时自动运行
jobs:
# 定义要执行的任务列表
trivy-scan:
# 任务名称:安全扫描
runs-on: ubuntu-latest
# 运行环境:使用最新的 Ubuntu 虚拟机
steps:
# 步骤列表,按顺序执行
- uses: actions/checkout@v4
# 步骤1:检出代码
# uses: 引用官方 action
# actions/checkout@v4: GitHub 官方提供的代码检出 action,v4 是版本号
# 作用:把当前仓库的代码下载到 runner 工作目录
- name: Build Docker Image
# 步骤2:构建 Docker 镜像
# name: 步骤显示名称
run: docker build -t myapp:${{ github.sha }} .
# run: 执行 shell 命令
# docker build: 构建 Docker 镜像
# -t myapp:${{ github.sha }}: 给镜像打标签,标签名是本次提交的 SHA 值(唯一标识)
# .: 使用当前目录下的 Dockerfile 构建
- name: Run Trivy Scan
# 步骤3:运行 Trivy 漏洞扫描
uses: aquasecurity/trivy-action@0.29.0
# 引用 Aqua Security 官方的 Trivy action,版本 0.29.0
with:
# 传入 action 的参数
image-ref: 'myapp:${{ github.sha }}'
# 要扫描的镜像引用,就是上一步构建的镜像
format: 'sarif'
# 输出格式:SARIF(静态分析结果交换格式)
# SARIF 是标准化格式,可被 GitHub Security 识别
output: 'trivy-results.sarif'
# 扫描结果输出到文件:trivy-results.sarif
severity: 'CRITICAL,HIGH'
# 只报告 CRITICAL(严重)和 HIGH(高危)级别的漏洞
ignore-unfixed: true
# 忽略没有修复方案的漏洞(减少噪音)
exit-code: '1'
# 发现漏洞时返回退出码 1
# 在 CI 中,非零退出码会导致当前步骤失败,阻断后续流程
- name: Upload to GitHub Security
# 步骤4:上传扫描结果到 GitHub Security 标签页
uses: github/codeql-action/upload-sarif@v3
# 引用 GitHub 官方的 SARIF 上传 action,版本 v3
with:
sarif_file: 'trivy-results.sarif'
# 指定要上传的 SARIF 文件路径
# 上传后可在 GitHub 仓库的 Security → Code scanning alerts 中查看
- 流程图
代码 push / PR ──→ 检出代码 ──→ 构建镜像 ──→ Trivy 扫描 ──→ 上传报告到 GitHub Security
│ │ │ │
│ │ │ └── 在 Security tab 查看
│ │ └── 发现漏洞 → 阻断流水线
│ └── 生成 myapp:abc1234
└── git checkout
6.2 Jenkins Pipeline 集成
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'docker build -t myapp:$BUILD_NUMBER .'
}
}
stage('Security Scan') {
steps {
sh '''
trivy image \
--exit-code 1 \
--severity CRITICAL,HIGH \
--ignore-unfixed \
myapp:$BUILD_NUMBER
'''
}
}
}
}
- 加注释
pipeline {
// 声明这是一个 Jenkins Pipeline
agent any
// 指定运行节点:any 表示任意可用的 Jenkins agent/slave 节点
stages {
// 定义流水线阶段(stage),按顺序执行
stage('Build') {
// 阶段1:构建 Docker 镜像
steps {
// steps 块内写具体执行步骤
sh 'docker build -t myapp:$BUILD_NUMBER .'
// sh: 执行 shell 命令
// docker build: 构建 Docker 镜像
// -t myapp:$BUILD_NUMBER: 镜像标签,$BUILD_NUMBER 是 Jenkins 内置变量(每次构建的唯一序号,如 1, 2, 3...)
// .: 使用当前目录的 Dockerfile 构建
}
}
stage('Security Scan') {
// 阶段2:安全扫描
steps {
sh '''
// 三个单引号 ''' 表示多行字符串,保留换行和格式
trivy image \
// 使用 trivy 扫描镜像
--exit-code 1 \
// 发现漏洞时返回退出码 1(非零 = 失败)
// Jenkins 检测到非零退出码会标记当前 stage 为失败
--severity CRITICAL,HIGH \
// 只报告 CRITICAL(严重)和 HIGH(高危)级别的漏洞
// 中危/低危不报告,减少噪音
--ignore-unfixed \
// 忽略没有修复方案的漏洞
// 只关注"有补丁可打"的漏洞,避免报了也修不了的尴尬
myapp:$BUILD_NUMBER
// 扫描目标:上一步构建的镜像
'''
}
}
}
}
- 流程图
┌─────────────────────────────────────────────────────────────┐
│ Jenkins Pipeline 启动 │
│ 触发条件:代码提交 / 定时触发 / 手动触发 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Stage 1: Build(构建) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ sh 'docker build -t myapp:$BUILD_NUMBER .' │ │
│ │ │ │
│ │ 1. 读取当前目录 Dockerfile │ │
│ │ 2. 逐层构建镜像 │ │
│ │ 3. 打标签:myapp:1(假设 BUILD_NUMBER=1) │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 构建成功? │
│ / \ │
│ 是 否 │
│ │ │ │
│ ▼ ▼ │
│ 进入下一阶段 Stage 失败 ❌ │
│ (标红,停止流水线) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Stage 2: Security Scan(安全扫描) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ trivy image \ │ │
│ │ --exit-code 1 \ ← 有漏洞就返回 1(失败) │ │
│ │ --severity CRITICAL,HIGH \ ← 只看严重/高危 │ │
│ │ --ignore-unfixed \ ← 忽略无修复方案的 │ │
│ │ myapp:$BUILD_NUMBER ← 扫描刚构建的镜像 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 发现 CRITICAL/HIGH 漏洞? │
│ / \ │
│ 是 否 │
│ │ │ │
│ ▼ ▼ │
│ exit-code=1 exit-code=0 │
│ Stage 失败 ❌ Stage 通过 ✅ │
│ (标红) (变绿,流水线继续) │
│ 阻断后续部署 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 后续 Stage(如 Deploy 部署) │
│ 只有 Security Scan 通过才会执行到这里 │
└─────────────────────────────────────────────────────────────┘
6.3 GitLab CI 集成
stages:
- build
- scan
build:
stage: build
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
scan:
stage: scan
image: aquasec/trivy:latest
script:
- trivy image --exit-code 1 --severity CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
allow_failure: false
- 加注释
stages:
# 定义流水线阶段(stage)的执行顺序
# stages 是 GitLab CI 的关键字,声明所有阶段
- build
# 阶段1:构建镜像
- scan
# 阶段2:安全扫描
# 按顺序执行:build 完成后才会执行 scan
build:
# 定义名为 build 的 job(任务)
stage: build
# 指定该 job 属于 build 阶段
# 同一个 stage 的 job 可以并行执行
script:
# script 块:定义具体执行的 shell 命令
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
# docker build: 构建 Docker 镜像
# -t: 给镜像打标签
# $CI_REGISTRY_IMAGE: GitLab 内置变量,当前项目的镜像仓库地址
# 例如:registry.gitlab.com/group/project
# $CI_COMMIT_SHA: GitLab 内置变量,当前提交的完整 SHA 哈希值
# 例如:a1b2c3d4...(40位十六进制字符串,唯一标识一次提交)
# .: 使用当前目录的 Dockerfile 构建
scan:
# 定义名为 scan 的 job
stage: scan
# 指定该 job 属于 scan 阶段
# 必须等 build 阶段的所有 job 成功后才会执行
image: aquasec/trivy:latest
# 指定运行该 job 的 Docker 镜像
# 不用宿主机安装的 trivy,而是用官方最新镜像运行
# 优点:版本可控,无需在 runner 上预装 trivy
script:
# 在 trivy 镜像内执行的命令
- trivy image --exit-code 1 --severity CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
# trivy image: 扫描容器镜像
# --exit-code 1: 发现漏洞时返回退出码 1(非零 = 失败)
# --severity CRITICAL: 只报告 CRITICAL(严重)级别的漏洞
# $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA: 扫描上一步构建的镜像
allow_failure: false
# 是否允许失败:false = 不允许
# 该 job 失败时,整个流水线标记为失败,阻断后续阶段
# 如果设为 true,job 失败但流水线继续(不推荐用于安全扫描)
- 流程图
┌─────────────────────────────────────────────────────────────┐
│ GitLab CI Pipeline 触发 │
│ 触发条件:push / merge request / tag / 手动触发 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Stage: build(构建阶段) │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Job: build │ │
│ │ Runner: 默认镜像(如 alpine/ruby:2.7) │ │
│ │ │ │
│ │ script: │ │
│ │ docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA │ │
│ │ │ │
│ │ 1. 读取项目根目录 Dockerfile │ │
│ │ 2. 构建镜像层 │ │
│ │ 3. 打标签:registry.gitlab.com/group/project:a1b2c3d │ │
│ │ 4. 推送到 GitLab Container Registry(默认自动推送) │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ build 阶段成功? │
│ / \ │
│ 是 否 │
│ │ │ │
│ ▼ ▼ │
│ 进入下一阶段 Job 失败 ❌ │
│ Pipeline 失败 │
│ (标红,停止) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Stage: scan(扫描阶段) │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Job: scan │ │
│ │ image: aquasec/trivy:latest ← 专门的扫描镜像 │ │
│ │ │ │
│ │ script: │ │
│ │ trivy image \ │ │
│ │ --exit-code 1 \ ← 有漏洞就失败 │ │
│ │ --severity CRITICAL \ ← 只看严重级别 │ │
│ │ $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA │ │
│ │ │ │
│ │ 1. 从 Registry 拉取镜像 │ │
│ │ 2. 解压分析镜像层 │ │
│ │ 3. 比对漏洞数据库 │ │
│ │ 4. 输出报告 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 发现 CRITICAL 漏洞? │
│ / \ │
│ 是 否 │
│ │ │ │
│ ▼ ▼ │
│ exit-code=1 exit-code=0 │
│ Job 失败 ❌ Job 通过 ✅ │
│ allow_failure: false │
│ Pipeline 失败 Pipeline 成功 │
│ (标红) (标绿) │
│ 阻断部署 │
└─────────────────────────────────────────────────────────────┘
七、Kubernetes 场景:Trivy Operator
对于已在 K8s 集群中运行的应用,可以部署 Trivy Operator 实现持续监控:
# 安装 Trivy Operator
helm repo add aqua https://aquasecurity.github.io/helm-charts/
helm repo update
helm install trivy-operator aqua/trivy-operator \
--namespace trivy-system \
--create-namespace \
--set trivy.ignoreUnfixed=true
# 查看漏洞报告
kubectl get vulnerabilityreports -A
kubectl describe vulnerabilityreport -n default
八、最佳实践与避坑指南
✅ 推荐做法
实践说明三层扫描开发阶段 trivy fs → CI 阶段 trivy image → 运行时 Trivy Operator设置严重级别阈值生产环境阻断 CRITICAL/HIGH,开发环境可放宽至 MEDIUM定期更新漏洞库漏洞库超过 12 小时未更新会自动触发下载使用 .trivyignore对确认误报或已评估风险的 CVE 进行忽略,但需标注原因和过期时间缓存数据库CI 环境中缓存 ~/.cache/trivy 目录,避免每次重复下载
❌ 常用坑点
1. 不设置 --exit-code 1 → 漏洞报告被忽略,流水线"假通过"
2. 不忽略无修复漏洞 → 老旧镜像产生大量噪音,开发者逐渐对告警免疫
3. 不提交 lock 文件 → trivy fs 扫描结果不完整,漏掉依赖漏洞
4. 无限增长的 .trivyignore → 变成"风险藏身处",需每季度审计
5. 使用 @master 引用 Action → 上游更新可能静默破坏流水线,应固定版本标签
九、常见错误
拉取最新漏洞库超时 timed out
报错:
2026-08-10T23:14:45+08:00 FATAL Fatal error run error: init error: DB error: failed to download vulnerability DB: OCI artifact error: failed to download vulnerability DB: failed to download artifact from mirror.gcr.io/aquasec/trivy-db:2: OCI repository error: 1 error occurred:
* Get "https://mirror.gcr.io/v2/": dial tcp 142.251.188.82:443: connect: connection timed out
原因:
这个错误是因为 Trivy 默认从 mirror.gcr.io(Google 容器镜像仓库)下载漏洞数据库,而各位的网络环境无法访问 Google 服务,导致连接超时。
解决方法:
方法一:使用国内镜像源
Trivy 支持通过环境变量指定数据库镜像地址,各位可以配置阿里云或其他国内镜像:
# 配置国内镜像源(阿里云)
export TRIVY_DB_REPOSITORY=registry.cn-hangzhou.aliyuncs.com/aquasec/trivy-db
export TRIVY_JAVA_DB_REPOSITORY=registry.cn-hangzhou.aliyuncs.com/aquasec/trivy-java-db
# 然后执行扫描
trivy image nginx:1.25.0
或者写入 ~/.bashrc 永久生效:
echo 'export TRIVY_DB_REPOSITORY=registry.cn-hangzhou.aliyuncs.com/aquasec/trivy-db' >> ~/.bashrc
echo 'export TRIVY_JAVA_DB_REPOSITORY=registry.cn-hangzhou.aliyuncs.com/aquasec/trivy-java-db' >> ~/.bashrc
source ~/.bashrc
方法二:使用代 理
如果各位有可用的 HTTP/HTTPS 代 理:
具体的代 理使用可以参考:docker解决超时报错 - 通过代 理软件 的方式,不需要按照里面创建目录,这是docker的操作,只需要打开windows代 理,按需求开启之后,记住代 理的端口和windows的ip,加到如下,进行扫描,就可以自动更新漏洞库。
# 临时设置`代 理`
export HTTP_PROXY=http://your-proxy:port
export HTTPS_PROXY=http://your-proxy:port
# 执行扫描
trivy image nginx:1.25.0
方法三:离线导入数据库(完全内网环境)
如果完全无法联网,可以在有外网的机器上下载数据库,再导入到内网(请定期更新离线数据库):
步骤1:外网机器下载数据库(当然也是需要代 理才能下载下来)
# 在外网机器上安装 trivy 并下载漏洞数据库
trivy image --download-db-only
# 下载 Java 数据库(如果扫描 Java 项目需要)
trivy image --download-java-db-only
# 数据库会缓存在 ~/.cache/trivy/db/
# 打包缓存目录
tar -czvf trivy-db-backup.tar.gz ~/.cache/trivy/
步骤2:传输到内网机器并恢复数据库
# 将缓存的数据库打包好之后传入内网任意地方,需要解压
# 解压到对应位置
mkdir -p ~/.cache/trivy
tar xf trivy-db-backup.tar.gz -C /
# 检查
ls ~/.cache/trivy/
du -sh ~/.cache/trivy/
# 扫描
trivy image nginx:1.25.0
# 扫描时跳过数据库更新
trivy image --skip-db-update --skip-java-db-update nginx:1.25.0
# 扫描时跳过数据库更新并保存到指定文件中
trivy image --skip-db-update --skip-java-db-update nginx:1.25.0 -o nginx-1.25.0.log
十、总结
Trivy 以其零配置、高性能、全场景覆盖的特点,成为容器安全扫描的事实标准工具。从本地开发到 CI/CD 流水线,再到 Kubernetes 运行时,Trivy 提供了完整的安全左移方案。
核心命令速查:
# 基础扫描
trivy image nginx:1.25.0
# 基础扫描生成html
trivy image --skip-db-update --skip-java-db-update -f template -t @/root/.config/trivy/templates/html.tpl -o nginx-1.25.0.html nginx:1.25.0
# 生产级扫描(CI 推荐)
trivy image --exit-code 1 -s CRITICAL,HIGH --ignore-unfixed --timeout 10m nginx:1.25.0
安全不是一次性的检查,而是持续的过程。将 Trivy 集成到开发工作流中,让漏洞在到达生产环境之前就被发现和修复。
本次分享就到这里。技术这东西越研究越有意思,后续有新的收获我也会继续更新。
评论 (0)
暂无评论