面向 AI Agent 的依赖漏洞扫描 API:一次调用拿到 OSV.dev 公告、CVSS 分数与修复版本

OSV.dev 是开源领域最好的免费漏洞数据库,它的 API 几乎给了你一切,唯独少了你做决策时最需要的那个字段:分数。本文讲怎么把漏洞公告变成一个可以直接用来判断的阈值。
一句话总结
- •OSV.dev 把 GitHub Advisory Database、Go 漏洞库、RustSec、PyPA 与各发行版安全跟踪库统一到同一套 CC-BY-4.0 schema 中——这是开源依赖数据最合适的来源。
- •OSV 只提供 CVSS 向量串,不给分数。而构建卡点需要的是数字,所以本接口按官方公式从向量算出 CVSS v3.1 基础分。
- •一次 POST 可检查最多 50 个包、覆盖 13 个生态,固定 5 积分——批满时每个包 0.1 积分,且只在成功时计费。
- •请从 lockfile 提交确切的已安装版本,不要提交范围:OSV 是拿具体版本去比对受影响范围的,因此范围本身没有唯一答案。
- •版本更新并不等于干净。lodash 4.17.21 仍带有只在 4.18.0 中修复的公告——这恰恰说明为什么要查,而不是假设。
每份漏洞公告里都缺的那个字段
OSV.dev 是开源漏洞数据这些年来最好的一件事。Google 做它是为了解决一个实实在在的烂摊子:GitHub Advisory Database、Go 漏洞库、RustSec、PyPA,以及各大 Linux 发行版的安全跟踪库,都在用各自的 schema、各自的版本语义描述同一类问题。OSV 把这些全部归一化,以 CC-BY-4.0 发布,无需 API key,毫秒级响应。
它同样也不会告诉你事情有多严重。一条 OSV 公告带有 CVSS 向量串——CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H——但不带这个向量算出来的分数。于是每个想定下「高于 7.0 就中断构建」这类规则的团队,最后要么自己实现一遍 CVSS 规范,要么放弃,改成匹配严重程度字符串。
这是个小缺口,后果却不小,因为严重程度标签并不是阈值。「HIGH」不是一个你能拿去做比较的数字。
这个接口返回什么
依赖漏洞扫描一次 POST 接收最多 50 个包,按包返回每一条未撤回的公告,含 CVE 编号、计算得出的 CVSS v3.1 基础分、CWE 弱点分类,以及能够解决它的确切版本——外加整批按严重程度汇总的计数。
import httpx, os
API = "https://www.apipick.com/api"
HEADERS = {"x-api-key": os.environ["APIPICK_KEY"]}
def scan(packages):
r = httpx.post(f"{API}/scan-dependencies",
headers=HEADERS, json={"packages": packages})
r.raise_for_status()
return r.json()
report = scan([
{"ecosystem": "npm", "name": "lodash", "version": "4.17.15"},
{"ecosystem": "PyPI", "name": "requests", "version": "2.19.0"},
])
print(report["summary"])
# {'packages_scanned': 2, 'vulnerable_packages': 2,
# 'total_vulnerabilities': 16, 'highest_cvss_score': 8.1,
# 'by_severity': {'critical': 0, 'high': 5, 'medium': 9, ...}}用一个数字来卡发布
因为分数是数值型的,策略就是一次比较。注意其中对 None 的显式处理:没有 v3 向量的公告分数为 null,把它强制当成 0,正是未评分的严重漏洞溜过卡点的方式。
THRESHOLD = 7.0
blocking = [
(pkg["name"], v)
for pkg in report["results"]
for v in pkg["vulnerabilities"]
if v["cvss_score"] is not None and v["cvss_score"] >= THRESHOLD
or v["cvss_score"] is None and v["severity"] in ("HIGH", "CRITICAL")
]
if blocking:
for name, v in blocking:
fix = ", ".join(v["fixed_versions"]) or "no fix published"
print(f"{name}: {v['id']} {v['severity']} {v['cvss_score']} -> {fix}")
raise SystemExit(1)编码 Agent 这个场景
更有意思的使用方并不是 CI,而是一开始就在写依赖清单的那个 Agent。能调用这个接口的编码 Agent,会在写下某个版本之前先核查,并在同一轮回答里直接给出修复版本,而不是先产出一个依赖升级、一小时后再被 CI 里的扫描器打回。
工具定义就是一个含三个字段的对象,schema 以 OpenAI 和 Anthropic 两种格式托管在 GET /api/scan-dependencies/tool-schema,可以直接拉取而不必手写。当 Agent 需要了解某个刚发现的 CVE 的更完整背景——KEV 状态、EPSS 概率、厂商公告——可以搭配 Cybersecurity Search 一起用。
版本更新不等于版本干净
人们很容易把「升级到最新」当成修复然后翻篇。但去扫一下 lodash@4.17.21——大多数人心目中那个安全版本——公告依然会返回,而且只在一个尚不存在的 4.18.0 中修复。这不是数据出错,这正是数据在尽职。值得养成的习惯是:查你真正要发布的那个版本,而不是你以为没问题的那个版本。
自己接,还是直接调
| 自己对接 OSV | API Pick | |
|---|---|---|
| 批量行为 | querybatch 只返回 id——每条公告还要再跑一趟 | 一次调用,返回完整公告内容 |
| CVSS 分数 | 自己实现 v3.1 规范 | 已计算并返回 |
| 修复版本 | 按包过滤受影响范围矩阵 | 按包解析好 |
| 部分失败 | 一个坏包拖垮整批 | 就地报告,其余照常返回 |
| 生态名称 | 区分大小写:PyPI、crates.io,不是 pypi | 常见别名已归一化 |
| 成本 | 免费,外加你要维护的代码 | 5 积分/次,只在成功时计 |
数据从哪来
公告来自 OSV.dev,以 CC-BY-4.0 发布,覆盖 npm、PyPI、Go、Maven、NuGet、RubyGems、crates.io、Packagist、Hex、Pub、CRAN、SwiftURL 与 ConanCenter。我们补上的是分数计算、按包汇总,以及容错:单个查询失败的包会带上 error 字段返回,其余包一切照常,同时响应会设置 packages_failed,让你知道这份汇总是不完整,而不是干净。
免费 key 附带 100 积分、无需绑卡,相当于二十次装满 50 个包的扫描。把一个 CI 任务或一个 Agent 指过来,看看你的 lockfile 里一直带着什么。
常见问题
它和 npm audit 或 GitHub Dependabot 告警有什么不同?
那两者都绑定在单一生态和单一工作流上。npm audit 只懂 npm,Dependabot 只会对它监听的仓库提 PR。而这是一个普通的 HTTP 接口,可以在任何地方回答关于 13 个生态中任意包的问题——CI 脚本、对话中的编码 Agent、供应商评审表格、Slack 机器人都行。它不会替你开 PR、也不管理你的仓库;它只回答「这个确切版本是否受影响、什么版本能修」,剩下的交给你自己的工具去决定。
为什么要自己算 CVSS 分数,而不是直接返回 OSV 给的内容?
OSV 公告带的是 CVSS 向量串,例如 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H,但不带算出来的数字。而「高于 7.0 就中断构建」这类策略需要的正是数字,于是每个使用方最后都在重复实现同一份规范。我们按官方公式从向量算出 CVSS v3.1 基础分,以 cvss_score 返回,让你的卡点是一次数值比较,而不是对严重程度标签做字符串匹配。
如果某条公告没有 CVSS v3 向量会怎样?
有些公告只带 v4 向量,有些生态公告则完全不带向量。这种情况下 cvss_score 为 null,severity 回退到发布方自己的定性评级,若也没有则为 UNKNOWN。请在卡点逻辑中显式处理 null,而不要把它强制当成 0,否则一条未评分的严重公告就会一路放行。
可以提交 ^4.17.0 这样的版本范围吗?
不行。请按 lockfile 中的写法提交确切的已安装版本。OSV 判断某个版本是否受影响,靠的是拿它去比对公告中的受影响范围,所以需要一个具体的点来测试。范围本身没有唯一答案——4.17.0 和 4.17.21 完全可能落在同一条公告的两侧。
扫描一个大型项目要花多少钱?
无论一次带多少个包,单次调用都是 5 积分,上限 50 个包。一份 200 个包的 lockfile 就是 4 次调用,合计 20 积分。积分只在响应成功时扣除,因此 OSV 故障或请求格式错误都不花钱。
本文涉及的 API
Sarah Choy 是 API Pick 的 CEO,专注于为 AI Agent 与 LLM 工作流构建可用于生产的 API。