笔记 | Linux 下使用 Trivy 扫描 Docker 镜像漏洞

最近在做优化的时候涉及到了这块内容,觉得值得写下来,方便以后翻阅。

👨‍🎓博主简介
  🏅

这篇目录

三、命令及参数 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 小时内推送更新周期较长离线能力支持离线扫描多数依赖在线服务

二、在 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

方法三: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)

暂无评论