JavaLYG

Dependabot告警缺少仓库依赖版本:接入version_requirement并保留未知状态

把 Dependabot 告警同步进工单系统,表里有包名、有漏洞范围,却不知道仓库记录的依赖版本;再去拆锁文件,接入成本一下就上来了。GitHub 在 2026 年 10 月 20 日新增了 REST 字段 version_requirement。下面讲清它能补什么、哪些值仍要当作未知,并给出可直接运行的离线兼容检查。

一、先分清两种“版本”

告警里的 security_vulnerability.vulnerable_version_range 描述漏洞影响范围;dependency.version_requirement 描述 Dependabot 为这条告警最近记录的依赖版本或约束。两者分别回答“哪些版本受影响”和“仓库依赖记录是什么”,不能互相填空。

例如漏洞范围是一段区间,仓库记录也可能是一段约束。看着都带大于号、小于号,却不是同一张清单。不要用公告范围填进“已安装版本”,更不要因为新增字段有值,就标记为已经修复。官方更新减少的是额外取数,不是把风险判断自动做完。

二、同一字段有三种来源

官方说明:由锁文件发现时,值是解析后的版本;只有清单、没有锁文件时,是清单声明的版本或版本约束;通过 Dependency submission API 发现时,是构建工具提交的解析版本。接入时最好在内部资产记录里另存已知来源,不能仅凭字符串外观猜来源。

字段本身不是“发现方式”的枚举,也不提供完整安装树。一个没有比较符的字符串,不足以证明生产环境确实安装了它;构建工具提交的结果也仍需对应具体构建与部署产物。扫描快照、代码当前分支、容器里实际依赖,可能不是同一时刻。

Dependabot三种发现方式与版本记录字段的关系
图为来源语义示意,不表示自动修复流程。配图由 DM-Fox 生成并经等比例尺寸规范化,技术关系与可读性经复核。

三、空值不是“没有漏洞”

取不到值时,字段可以为 null。如果同一告警涉及一个依赖的多个易受攻击版本,字段只保留一个已记录值,不一定代表全部版本;过长的约束还可能截断。这三个限制都不能靠客户端补零解决。

推荐保留原文、采样时间和告警身份,将缺失/null/空白显示为“未知”。缺失与 null 在原始响应里仍有差别,本例只在展示状态上归并;保存原始响应时不要删掉这个差别。不要把未知归成已修复,也别拼接一个假版本让报表好看。

该字段已用于单条告警、仓库/组织/企业告警列表,以及 dependabot_alert webhook。官方公告说明 github.com 和带数据驻留的 GitHub Enterprise Cloud 已可用,GitHub Enterprise Server 将在 3.24 包含它。不要把云端可用推成所有自托管版本立即都有。

四、先接只读端点,再接展示

仓库列表入口是 GET /repos/{owner}/{repo}/dependabot/alerts,可显式传 state=open 与 per_page=100。使用限定到目标仓库、具有“Dependabot alerts: read”权限的 fine-grained token;组织、企业端点的角色和令牌支持不同,不能照搬仓库规则。

REST 文档还列出 classic token 的 security_events scope;只用于公开仓库时可用 public_repo。无论令牌属于哪类,仍须确认实际身份、仓库访问和告警功能条件。本文不调用真实仓库,不验证这些权限,令牌也不该出现在工单正文或截图里。

拿到 HTTP 200 后,检查顶层是否为数组、每条 dependency 是否为对象,再读取新增字段。列表要沿 Link 响应头里的 next 继续取,per_page=100 只是页大小,不是“最多就这些”。同步到一半失败应标为不完整,不能把已拿到的第一页冒充全部。

五、用完整离线代码检查兼容边界

将下面完整代码保存为 demo.py,用 Python 3 运行。它只做字段提取与类型门禁,不联网、不判断各生态版本大小,也不声称验证了锁文件、真实 API 或漏洞修复。读取部分兼容缺字段;False、0 等不能因为是假值就被悄悄归成未知。

import copy
import json

def 提取(告警):
    if not isinstance(告警, dict):
        raise ValueError('告警必须是对象')
    依赖 = 告警.get('dependency')
    if not isinstance(依赖, dict):
        raise ValueError('缺少依赖对象')
    值 = 依赖.get('version_requirement')
    if 值 is not None and not isinstance(值, str):
        raise ValueError('版本记录类型错误')
    状态 = '有记录,仍需复核' if 值 is not None and 值.strip() else '未知'
    return {'版本原文': 值, '记录状态': 状态}

# 以下全部是自造离线样本,不是真实仓库响应。
样本 = {'dependency': {'version_requirement': '>=1.0, <2.0'}}
原件 = copy.deepcopy(样本)
assert 提取(样本)['版本原文'] == '>=1.0, <2.0'
assert 提取(样本)['记录状态'] == '有记录,仍需复核'
assert 样本 == 原件
for 依赖 in [{}, {'version_requirement': None},
             {'version_requirement': ''}, {'version_requirement': '  '}]:
    assert 提取({'dependency': 依赖})['记录状态'] == '未知'
for 值 in [False, 0, [], {}]:
    try:
        提取({'dependency': {'version_requirement': 值}})
    except ValueError:
        continue
    raise AssertionError('不应接受此类型')
print(json.dumps(提取(样本), ensure_ascii=False))
print('离线字段兼容检查通过;未调用真实 GitHub API')

运行命令为 python3 demo.py。输出保留原始约束,并提示“有记录,仍需复核”。这里不使用字符串大小比较,也不拿一条约束当确定安装版本。需要判定是否受影响时,应使用对应包生态的版本语义,并复核依赖图与完整约束;本文没有交付通用版本解析器。

六、失败分支不要只剩下重试

认证错误先检查凭据是否有效;403 要结合响应与限流信息核对权限、角色或限制;404 也可能涉及资源不可见,不能立即断言仓库不存在。记录脱敏状态码和必要错误信息,不把带认证头的请求原样贴进日志。

现象排查与处理
字段缺失或 null保留未知;核对服务版本和扫描记录,不回填公告范围
值是约束而非单版本核对清单与锁文件来源,不标成确定安装版本
一条告警涉及多个版本复核完整依赖图,不用单值证明覆盖全部
分页中断保留失败页与不完整状态,恢复时防重复
补丁版本存在继续核对升级、构建、部署和重新扫描,不能直接关单

七、验收分成三层

第一层是接口:身份正确、目标正确、分页完整,保留采样时刻与必要的原始数据。第二层是字段:新增字段保持原文,缺失/null/空白可见,非法类型明确报错;不要将“有记录”写成“已核实”。

第三层才是业务:用锁文件、构建依赖清单与部署产物核对实际版本,升级后再扫描,并检查告警状态。代码提交了、接口返回 200、页面出现一个版本,这三件事都不能单独代替修复验收。

本文实际执行的是自造样本的内存隔离检查,以及最终文章中这段完整代码;没有拉取任何真实私有仓库告警,也没有执行远程升级或关单。可复现范围写窄一点,反而更值得信任。

  • 确认字段路径,不与漏洞范围互填。
  • 保留来源、原文、采样时间与未知状态。
  • 确认分页完整,并对同步失败做可见标记。
  • 修复结果由构建、部署与重新扫描单独验收。

八、官方参考资料

以下三份资料分别支撑变更时间与语义、端点权限、分页处理;本文按 2026 年 10 月 21 日读取的官方内容说明,不把未来服务端行为当作永久保证。

🔕 评论已关闭