返回 2026-06-26
🔒 安全

Scrutineer:在不淹没维护者的情况下扫描开源代码Scrutineer: scanning open source without flooding maintainers

nesbitt.io·2026-06-25

开源代码安全审计中,发现漏洞其实是最简单的环节,真正的挑战在于如何合理地报告和修复它们而不给维护者带来过载。Scrutineer 工具致力于在扫描开源项目时,自动过滤无效或低危问题,避免向维护者发送海量无意义的安全警报。这种机制在保障软件供应链安全的同时,极大减轻了开源社区的维护负担。合理的警报分发策略是实现可持续开源安全治理的关键。

Andrew Nesbitt

Scrutineer 会扫描开源代码仓库以查找安全漏洞,并负责后续的所有工作:验证每一个漏洞、找出联系人、起草修复方案,并持续跟踪直到发布安全公告。过去几个月,我一直在为 Alpha-Omega 开发这个工具。

大语言模型让发现开源代码中的漏洞变得容易得多。将其指向一个代码库,它就能比以前的模糊测试工具和扫描器更快、成本更低地找出真实的 bug,尽管也会夹杂一些虚构的 bug,但瓶颈并未随之改变。每一条发现仍需由维护者去阅读、确认和修复,而他们的时间和注意力是整个生态系统赖以生存的有限资源。试图用机器生成的报告对维护者进行狂轰滥炸来保障一切安全,只会耗尽这些中坚力量。

今年五月,当我用几个 AI 扫描器扫描 curl 时,大部分输出结果都不符合该项目自身的披露政策,而真正有价值的发现却被淹没在其余的报告中。Scrutineer 的设计初衷就是确保模型生成的大量报告永远不会直接落到维护者手中。

你只需通过 URL 添加一个代码库,它就会针对代码运行一系列技能流水线,并在 Web UI 中呈现结果以供分类处理。它已经交到了生态系统安全工程师以及 Alpha-Omega 资助的几个团队手中,在我们的共同努力下,借助它已经发现、报告、修复并发布了相当数量的漏洞。

扫描的运行方式

每次扫描都是磁盘上的一个技能:包含一个 SKILL.md 文件、一个用于输出的 JSON schema,以及它所需的任何脚本。当你添加代码库时,分类(triage)技能会首先运行,并并行排入流水线中的其余部分。返回的结果是一组结构化的发现,每个发现都带有严重程度、CWE、链接回源代码行的确切位置、受影响的版本,以及说明如何触发该漏洞的六步追踪记录。

由于技能只是目录中的文件,因此更改运行内容只需编辑 Markdown,而无需重新编译扫描器。默认的技能集合位于 skills/ 目录中,分类技能的 SKILL.md 列出了要触发的技能,而放入一个新目录即可增加一种扫描类型,完全不需要修改代码。

技能

每个技能都是 GitHub 上 skills 文件夹中的一个目录。triage(分类)技能首先运行,收集供审计使用所需的上下文,而静态分析、去重和导出等辅助技能则在外围提供支持。真正塑造该工具运作方式的核心技能如下:

  • security-deep-dive 是由模型支持的审计技能,负责生成发现结果,也是目前最重要的技能;其他所有技能要么为其提供上下文,要么处理其返回的结果。它分两个阶段运行。第一阶段会建立代码库中所有接收器(sink)的清单,即每一个执行代码、调用 shell 或触及可能存在敌意的路径的地方,但此时尚不对它们进行任何评判。第二阶段则逐个遍历该清单,将每个接收器追溯到信任边界,并判断恶意输入是否能触达该处。该清单是报告的一部分,而非临时草稿,因此对同一个 commit 进行两次运行会得到完全相同的列表。它审计的是项目自身的代码,而不是其依赖项的已知 CVE:只有当存在漏洞的逻辑位于当前代码库中时,该发现才算数。
  • threat-model 在任何审计开始之前,先推导出项目的安全契约:它对调用方做了哪些假设,在这些假设下保证哪些属性,哪些部分留给集成方处理,以及哪些代码不在范围内。每个声明都被标记为已记录(附有文件和行号或已关闭的 issue)或推断(基于代码推理并标记待人工确认)。它会原样提取 SECURITY.md 中已声明的范围,因此模型是项目自身既定立场的超集,而非与之竞争的替代品。深度分析阶段会加载这些内容,而不是每次运行都重新推导边界,从而将注意力集中在项目声称要保护的代码部分上。
  • maintainers 负责确定漏洞披露的联系人,并将找到的人员归类为活跃负责人、常规维护者、一次性贡献者和机器人。它从 ecosyste.ms 拉取提交历史、issue 和 PR 活动记录以及注册表所有权信息,并读取 SECURITY.md 和 CODEOWNERS 来查找具名的安全联系人,而不是简单地给 git log 中排在最前面的人发邮件。输出结果会为每个人附上相应的披露渠道:仓库启用了私有漏洞报告的走私有报告通道,有公开联系方式的走已发布的联系方式。
  • patch 针对已确认的发现,以对所扫描 ref 的 unified diff 形式提出修复方案。它遵循在 sink 处就地最小修改的原则,匹配现有代码风格,复用项目已有的 sanitizer 或 validator,并在测试套件可行时附带回归测试。如果无法判断危险路径与正常使用的分歧点在哪里,它会拒绝猜测而非强行给出方案。一个能正确解析、目标文件存在且通过 git apply --check 的 diff 会被存储在该发现上作为建议修复,并可以 .patch 文件形式下载;不会推送任何内容,因此由分析师审查后手动提交 PR。
  • breaking-change 会读取该建议修复,并判断发布后是否会破坏库的顶级依赖方。它将 diff 对公共 API 表面的变更归类为纯新增、收紧的输入契约、签名变更或同形态的行为变更,然后检查被依赖最多的包是否可能调用了受影响的符号。这是对 diff 和依赖元数据的静态分析,从不运行任何人的代码;当仅凭包名无法做出判断时,它返回 unknown 而非给出一个看似确定实则错误的结论。这个判定往往决定了一个修复能否顺利发布,还是只能搁置一旁。
  • release-watch 负责处理补丁合入后容易被忽略的环节。一个发现在提交合并到上游后状态变为 fixed,但使用者无法 pin 到某个 commit,他们需要带 tag 的发布版本。因此,一旦发现被修复,该技能会轮询上游的发布记录,将每个 tag 映射回对应的 commit,并检查该修复是否能从该版本中获取。当包含修复的发布出现时,它会将 tag、URL 和时间戳记录到该发现上;在此之前,它会报告最新发布版本并在下次运行时再次检查。
  • 默认捆绑了三十多个技能,随着我们遇到现有技能无法覆盖的场景,这个列表还在持续增长。

    底层数据

    在阅读任何代码之前,scrutineer 获取的关于项目的大量信息都来自于 ecosyste.ms。元数据、包、公告和依赖项等技能都会查询它的 API:仓库元数据、每个已发布的包及其下载量和依赖数、已提交的已知公告,以及一旦出现漏洞会受影响的下游项目。维护者分析则依赖于来自同一来源的注册表所有权数据。依赖项信息是在本地读取而非远程获取的:依赖项和 sbom 技能在检出的代码上运行 git-pkgs,以索引代码树中的每个清单文件并生成 CycloneDX SBOM,从而使依赖图反映出仓库所声明的内容。这样一来,发现的漏洞就会附带上下文信息,比如该包的使用范围以及谁依赖它,而不是仅仅给出一个干瘪的行号。

    披露前的验证

    贯穿这些技能的核心原则是排除发现项(finding),而不是收集它们。深度分析只有在能够追踪到恶意输入时,才会保留一个汇聚点(sink)。breaking-change 和 patch 技能会返回未知或直接拒绝,而不是做出错误的判断;threat-model 会记录项目文档中声明的不属于问题的内容,这样它们一开始就不会被当作漏洞提出来。将举证责任加诸于发现项之上,可以将模型产生的大部分噪音留在工具内部,而不是进入某个人的收件箱。而且,如果项目在其 SECURITY.md 或威胁模型中记录了哪些情况不属于漏洞,这就为任何扫描器(不仅仅是这个工具)提供了从源头直接丢弃这些报告的依据。

    scrutineer 之所以拥有一整套工作流而不是仅仅一个“报告”按钮,是因为模型直接生成的发现项并不适合直接发送给任何人。每个发现项都从新建状态开始,经过验证、分类、披露草稿和报告等环节,且每个步骤都设有人工审核关卡。对于高危和严重级别的发现项,在进行任何高成本工作之前,会首先自动将其加入一个低成本的只读分类器队列,将它们分类为真阳性、假阳性、已修复或不确定。如果严重级别的发现项被判定为真阳性,随后将触发一个独立的验证流程。在人工进行审查之前,任何信息都不会发送给维护者;而当信息最终送达时,它是通过 GitHub 的私密漏洞报告功能,作为一个已验证的漏洞并附带了建议的补丁,而不是另一个听起来似乎有理的“可能”,让维护者不得不牺牲周末时间去反驳它。

    发现项的导入与导出

    发现项不必源于 scrutineer 也能通过其工作流进行处理。向它 POST 另一个扫描器的输出或渗透测试报告,无论是 SARIF、CSV、markdown 还是最简形式的 JSON,每一项都会进入与原生发现项相同的分类和披露流程,并通过内容指纹与已有内容进行去重。上传的 CycloneDX 或 SPDX SBOM 会将每个组件解析到对应的源代码仓库,并将其排入扫描队列。输出的格式则是协调员或注册表能够接受的:发现项可以导出为 OSV 记录或 CSAF 2.0 公告,而披露包会将 OSV、CSAF、markdown 报告和补丁打包成一个 tarball,以备不通过 GitHub 私密报告渠道时使用。

    在你的整个生态系统中尝试它

    scrutineer 采用 MIT 许可证,代码托管在 GitHub 上。它在本地运行,扫描默认在一个临时的 Docker 容器中进行,该容器具有只读的源代码挂载和出站白名单。你可以通过 Claude Code 订阅令牌或 Anthropic API 密钥将其指向 Claude。

    我现在最希望的是,大家能拿它去测试一下那些我不常接触的生态系统中的代码仓库。该流水线依赖于 ecosyste.ms 和各大包注册中心,因此已经覆盖了 npm、PyPI、RubyGems、crates.io、Go、Packagist、Hex 和 NuGet,但这些技能是基于我们碰巧最先扫描的项目而塑造的。如果你所在的生态系统有其专属的规范,且默认技能不够用,通常只需添加一个新的技能文件即可解决,这算是开源社区里要求的最小贡献了,欢迎在 issue 追踪器里向我反馈遇到了什么问题。

    需要完整排版与评论请前往来源站点阅读。