NVD 精简了漏洞评分,容器安全扫描还能靠谁?

2026-05-13 23 预计阅读时间:1 分钟
来源:docker.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:12 分钟

4 月 15 日,NIST 宣布对国家漏洞数据库(NVD)采用"优先富化"模型:绝大多数 CVE 仍会发布,但只有部分会获得 CVSS 评分、CPE 匹配和 CWE 分类。换句话说——漏洞条目还在,但你最依赖的那几列数据,开始大面积留空了。

这并非突发变故。任何长期拉取 NVD 数据的人早就注意到评分延迟和字段缺失在逐年恶化,NIST 只是把既有趋势正式化了。但对容器安全扫描器和合规流程来说,这等于地基被抽走了一角:Trivy、Grype 等工具的严重性排序依赖 CVSS;CPE 匹配决定了"这个 CVE 到底影不影响我的镜像";CWE 分类则支撑着合规报告里的缺陷类型统计。当这些字段从"默认提供"变成"可能缺失",扫描结果的可信度和可操作性都会打折。

哪些场景最先受冲击

严重性排序失准。 CI 流程里常见的门槛是"CVSS ≥ 7.0 的 CVE 阻断发布"。如果 NVD 不给评分,扫描器要么跳过该 CVE(漏报),要么回退到默认中等严重性(误报),要么让你手动补评分——三种结果都不理想。

组件匹配模糊。 CPE 是 NVD 把 CVE 和具体软件包绑定的核心机制。没有 CPE 映射,扫描器只能靠包名和版本号做模糊匹配,误报率上升。容器镜像里一个 openssl 1.1.1k,到底受不受 CVE-2021-3711 影响?没有 CPE 你很难自动化判断。

合规报告断档。 SOC 2、PCI-DSS 等合规审计要求你按 CWE 类别统计缺陷分布。CWE 字段缺失后,你要么手动标注,要么在报告里留一大块"未分类",审计师不会喜欢后者。

扫描器已经在做的事

主流容器扫描器并非只用 NVD 单一数据源。Trivy 默认合并 GitHub Advisory Database(GHSA)和 OSV;Grype 也支持多数据源。但问题在于:当 NVD 缺评分时,这些替代源能否无缝补位?

实际情况是——GHSA 对 Python/Node 生态覆盖较好,对 C 库和系统包偏弱;OSV 聚合了多个生态的 advisory,但 CVSS 评分同样不一定齐全。也就是说,替代源能补一部分,但补不全,尤其在容器最关心的底层系统库(glibc、openssl、libcurl)上。

实操:搭建多源漏洞富化管道

与其等 NVD 补数据,不如在自己的扫描流水线里加一层富化。下面是一个可运行的 Python 脚本,它接收扫描器输出的 CVE ID 列表,依次查询 NVD 和 OSV,把缺失的 CVSS 和 CWE 补上,输出 JSON 供下游工具消费。

#!/usr/bin/env python3
"""
cve-enrich.py — 多源漏洞富化脚本
用法: python3 cve-enrich.py CVE-2021-3711 CVE-2023-44487 ...
依赖: pip install requests
"""

import json, sys, requests, time

NVD_API = "https://services.nvd.nist.gov/rest/json/cves/2.0"
OSV_API = "https://api.osv.dev/v1/vulns"

def query_nvd(cve_id: str) -> dict:
    """查询 NVD,提取 CVSS 和 CWE;若字段缺失返回空值"""
    r = requests.get(NVD_API, params={"cveId": cve_id}, timeout=15)
    r.raise_for_status()
    data = r.json()
    vulns = data.get("vulnerabilities", [])
    if not vulns:
        return {"cvss": None, "cwe": None, "source": "nvd", "status": "not_found"}
    cve = vulns[0]["cve"]
    # 提取 CVSS v3.1
    metrics = cve.get("metrics", {})
    cvss31 = metrics.get("cvssMetricV31", [{}])[0]
    cvss_score = cvss31.get("cvssData", {}).get("baseScore")
    # 提取 CWE
    weaknesses = cve.get("weaknesses", [])
    cwe_ids = []
    for w in weaknesses:
        for desc in w.get("description", []):
            cwe_ids.append(desc.get("value"))
    return {
        "cvss": cvss_score,
        "cwe": cwe_ids or None,
        "source": "nvd",
        "status": "ok" if cvss_score else "missing_enrichment",
    }

def query_osv(cve_id: str) -> dict:
    """查询 OSV,尝试补充 NVD 缺失的评分"""
    r = requests.get(f"{OSV_API}/{cve_id}", timeout=15)
    if r.status_code != 200:
        return {"cvss": None, "cwe": None, "source": "osv", "status": "not_found"}
    data = r.json()
    # OSV 的 severity 字段可能包含 CVSS
    severity = data.get("severity", [])
    cvss_score = None
    for s in severity:
        if s.get("type") == "CVSS_V3":
            # 从向量字符串解析 baseScore(简化:取 score 字段或从向量提取)
            cvss_score = s.get("score")
            if isinstance(cvss_score, str) and cvss_score.startswith("CVSS:3.1"):
                # 向量格式,提取 baseScore 需解析;此处取近似
                cvss_score = None
            break
    # OSV 不一定有 CWE,但 database_specific 里可能有
    cwe = data.get("database_specific", {}).get("cwe_ids")
    return {
        "cvss": cvss_score,
        "cwe": cwe,
        "source": "osv",
        "status": "ok" if cvss_score else "missing_enrichment",
    }

def enrich(cve_id: str) -> dict:
    """先查 NVD,缺失则补 OSV"""
    nvd = query_nvd(cve_id)
    result = {"cve_id": cve_id, "nvd": nvd}
    if nvd["cvss"] is None or nvd["cwe"] is None:
        time.sleep(0.6)  # NVD API rate limit 礼貌等待
        osv = query_osv(cve_id)
        result["osv"] = osv
        # 合并:NVD 优先,OSV 补缺
        result["final_cvss"] = nvd["cvss"] or osv["cvss"]
        result["final_cwe"] = nvd["cwe"] or osv["cwe"]
        result["enriched_by"] = "osv" if nvd["cvss"] is None and osv["cvss"] else "nvd"
    else:
        result["final_cvss"] = nvd["cvss"]
        result["final_cwe"] = nvd["cwe"]
        result["enriched_by"] = "nvd"
    return result

if __name__ == "__main__":
    cve_ids = sys.argv[1:]
    if not cve_ids:
        print("用法: python3 cve-enrich.py CVE-2021-3711 CVE-2023-44487 ...")
        sys.exit(1)
    results = []
    for cid in cve_ids:
        results.append(enrich(cid))
        time.sleep(0.6)  # 遵守 NVD 速率限制
    print(json.dumps(results, indent=2, ensure_ascii=False))

运行示例:

pip install requests
python3 cve-enrich.py CVE-2021-3711 CVE-2023-44487

输出会告诉你每个 CVE 的 CVSS 和 CWE 来自 NVD 还是 OSV,以及哪些字段仍然缺失。把这个脚本嵌入 CI 流水线,在 Trivy 扫描之后跑一遍,就能把"无评分 CVE"的盲区缩小。

扫描器配置层面的调整

除了自建富化,还可以调整扫描器本身的数据源策略。以 Trivy 为例:

# 让 Trivy 同时使用 GitHub Advisory 和 OSV(默认行为),并输出缺失评分的 CVE
trivy image --format json --severity HIGH,Critical your-image:tag | \
  jq '[.Results[].Vulnerabilities[] | select(.CVSS == null or .CVSS.nvd.V3Score == null)]'

# 如果你想把 OSV 作为主要源,可以在 Trivy 配置中指定
# trivy.yaml 示例
# trivy.yaml — 数据源配置片段
db:
  repository: ghcr.io/aquasecurity/trivy-db
skipDbUpdate: false
# Trivy 0.50+ 支持额外数据源
additionalDbs:
  - repository: ghcr.io/aquasecurity/trivy-java-db

关键不是换掉 NVD,而是承认 NVD 不再是完整数据源,在流程里加冗余

合规流程需要改什么

NVD 精简对合规报告的影响最容易被忽视,也最难修补。几条务实建议:

  1. 在漏洞策略里区分"已评分"和"未评分"两类 CVE。 未评分的不自动跳过,也不自动按中等处理,而是进入人工复审队列。复审标准可以简化:看漏洞描述是否涉及你使用的功能路径,看上游是否已发布补丁版本。

  2. 报告里新增一列"评分来源"。 标注 CVSS 来自 NVD、OSV 还是人工判定。审计师看到来源标注,比看到一堆空白字段更容易接受。

  3. 建立内部 CVE 评分缓存。 一旦你人工判定了一个 CVE 的严重性,把结果存进内部数据库(哪怕是一个 JSON 文件),下次同一 CVE 出现时直接复用,避免重复劳动。

# 最简方案:一个 Git 仓库存人工评分,CI 里拉取合并
mkdir -p internal-cve-ratings
echo '{"CVE-2024-1234": {"cvss": 8.1, "cwe": ["CWE-79"], "rationale": "XSS in admin panel, exploitable in our deployment"}}' \
  > internal-cve-ratings/CVE-2024-1234.json
git add . && git commit -m "add manual rating for CVE-2024-1234"
  1. 和上游社区同步。 Alpine、Debian、Red Hat 都维护自己的安全 advisory,往往比 NVD 更及时且有评分。容器镜像基于这些发行版时,优先查发行版自己的 CVE 数据库:
# Debian security tracker API 示例
curl -s "https://security-tracker.debian.org/tracker/status/release/stable" | \
  python3 -c "import sys,json; data=json.load(sys.stdin); print(json.dumps(data['packages']['openssl'], indent=2))"

该做的和不该做的

该做: - 扫描流水线里加多源富化层,NVD + OSV + 发行版 advisory - 漏洞策略里为"未评分 CVE"定义明确处理流程 - 合规报告标注评分来源,建立内部评分缓存 - 监控 NVD API 返回的缺失比例,设定预警阈值

不该做: - 不要假装 NVD 还会恢复全面评分——这是正式的政策转向,不是临时故障 - 不要把所有未评分 CVE 一律标为中等或一律忽略——两种做法都会在审计里出问题 - 不要等扫描器厂商替你解决——他们也在适应同样的数据缺口,你的流程必须有自己的冗余

NVD 精简不是世界末日,但它确实把"漏洞数据是公共基础设施"这个假设戳破了。容器安全程序从现在起要把自己当成数据聚合者,而不是单纯的 NVD 消费者。


相关推荐