在 CI 安装依赖前拦截供应链风险
教程面向 PHP、npm 和 PyPI 构建依赖安全过滤器,在依赖进入构建缓存前结合威胁情报与规则进行检查。内容覆盖清单解析、仿冒包名和依赖混淆检测,以及 CI 集成、测试与生产加固。
AI 深度解读
依赖安全检查放在安装之后,可能已经晚了一步:恶意包的安装脚本有机会先执行,再等扫描器报警。原文提出把检查前移,在 CI 安装依赖前读取锁文件,结合漏洞情报和本地规则决定是否放行,覆盖 npm、PyPI 和 PHP 的 Composer 生态。
这个方向值得借鉴,但代码不能直接当成生产工具。输入在检测引擎部分截断,前面的解析器和情报查询也存在明显缺口。对前端和全栈团队,更有用的是理解这道检查该放在哪里,以及哪些检测结果还不足以证明依赖安全。
检查前移,关键是赶在安装脚本之前
原文以 2024 年 10 月的「Black October」攻击为切入点,描述攻击者向 npm、PyPI 和 Packagist 发布仿冒内部依赖的恶意包。不过,输入没有提供事件证据或攻击规模,不能据此判断具体影响。
作者的方案是一个独立 Go 二进制,要求 Go 1.22+,可以通过 go version 检查环境。它作为 GitHub Actions 或 GitLab CI 的一个步骤,读取依赖锁文件,并行查询威胁情报,再应用策略;发现违规时,以非零退出码阻断构建。
真正有价值的是执行顺序:先检查,再安装依赖。
如果把过滤器放在 npm ci 之后,它即使成功报警,也没有实现作者要的安装前防护。同样,要兑现「可疑包进入构建缓存前就被拦截」,CI 的缓存恢复和写入顺序也要配合,单靠扫描程序无法保证。
检测分成两路。OSV.dev、npm audit、PyPI Advisories 和 Packagist Security 用于查询已有情报;本地规则则检查内部包名、近似热门包名和可疑安装脚本。前者依赖情报收录,后者依赖规则质量,两者都不能单独证明一个包没有恶意行为。
统一数据结构容易,正确读出依赖才是难点
作者先把不同生态归一成 Lockfile、Package、Finding、Evidence 和 PolicyConfig。包名、版本、下载地址、完整性信息进入统一模型,命中规则后再记录来源、证据与修复建议。
策略通过 FailOnSeverity 决定哪些等级阻断构建,等级包括 CRITICAL、HIGH、MEDIUM、LOW、INFO;还预留了 IgnoreDevDeps、AllowlistPath 和 CustomRulesPath。
解析器接口让新增生态有了固定入口:
type Parser interface {
Parse(path string) (*model.Lockfile, error)
Ecosystem() model.Ecosystem
LockfileNames() []string
}
要关注 Parse 这一行:后面的检测再完整,也只能检查它成功读出的依赖。
原文列出的文件包括:
- npm 的
package-lock.json、yarn.lock、pnpm-lock.yaml。 - PyPI 的
poetry.lock、requirements.txt、uv.lock、Pipfile.lock。 - Composer 的
composer.lock,并区分packages与packages-dev。
但「列出文件名」不等于「已经支持」。Yarn、pnpm、uv 和 Pipfile 的关键实现没有给出;npm 解析器声称兼容 lockfile v1、v2,却直接对 raw["packages"] 做类型断言。字段不存在时,程序会先发生 panic,无法走到预期的 v1 回退逻辑。
Python 部分的问题更直观。注释声称处理 pkg==1.2.3、pkg>=1.2.3 和 pkg @ url,实际只按 == 拆分,其余形式会被跳过。poetry.lock 则先走 YAML 解析,失败后才回退 TOML,这条路径没有展示可靠性验证。
此外,展示的 Package 没有 Ecosystem 字段,后续却多次访问 pkg.Ecosystem;部分文件缺少必要导入,Composer 的匿名结构体类型也存在不匹配。当前代码首先需要修到可编译,再谈覆盖率。
情报加规则能扩大覆盖,也会扩大误报
情报提供者统一实现 Name()、Query()、RateLimit(),便于调度。OSV 使用 https://api.osv.dev/v1/querybatch,HTTP 超时设为 10 秒;npm 提供者设为 15 秒。代码声明的速率分别是每分钟 100、60 次,本地规则为每分钟 10000 次。
这些数字只是配置和接口返回值。原文没有展示限流器,OSV 注释虽然写了每批最多 100 个包,代码也没有实际分批;可选 API key 的更高额度同样没有给出验证依据。
下面这段负责把 OSV 严重性映射到构建策略:
func mapOSVSeverity(sev []struct{Type, Score string}) model.Severity {
for _, s := range sev {
if s.Type == "CVSS_V3" {
switch {
case s.Score >= "9.0": return model.SeverityCritical
case s.Score >= "7.0": return model.SeverityHigh
case s.Score >= "4.0": return model.SeverityMedium
default: return model.SeverityLow
}
}
}
return model.SeverityMedium
}
要检查的是 s.Score >= "9.0":这里比较的是字符串,没有解析 CVSS 分数或向量,不能据此可靠分级。查询响应的结构和后续详情获取也需要核对,原文没有展示完整验证。
npm 示例逐包请求 advisory 路径,注释建议生产环境使用 https://registry.npmjs.org/-/npm/v1/security/advisories/bulk。但当前实现没有展示受影响版本匹配,查询失败或返回 404 时还会直接继续。这样得到的「没有发现」,可能只是「没有查成功」。
本地规则也有三处需要分清:
第一,内部包名命中就报 DEP-CONFUSION,等级为 CRITICAL。代码没有核验包是否来自公共仓库,合法私有包也可能触发。依赖混淆要查的是名称与实际来源是否冲突。
第二,拼写仿冒规则把包名与各生态前 10000 个热门包比较,编辑距离不超过 2、包名长度大于 3 就报 HIGH。编辑距离就是把一个名字改成另一个名字需要多少次字符操作。但代码遇到第一个匹配就退出,并没有实现注释所称的「最近匹配」。
第三,可疑脚本规则生成 SUSPICIOUS-SCRIPT,报告里将脚本文本截到 100 个字符,但 isSuspiciousScript 的实现已经截断。锁文件能否提供足够的脚本内容,输入也没有证明。
我的判断
我认可把锁文件检查放到依赖安装之前,也认可让每条告警带上证据。这比安装完成后才发现问题,更符合阻止安装阶段攻击的目标。
但这份输入是一份架构草稿。章节目录出现了测试和生产加固,实际内容没有展开;检测引擎仅剩开头,无法确认并发、去重、错误处理和退出策略如何实现。作者没有给出实测拦截率、误报率、扫描耗时或对照数据,「快」以及检测效果都不能当成经过验证的结论。
相比现有生态工具,自建过滤器的价值主要在跨生态策略、内部仓库约束和统一证据输出。代价是自己维护解析器、API 兼容性和规则误报。普通团队如果只是查已知漏洞,未必值得先重写整套查询逻辑。
IgnoreDevDeps 也不能因为名字里有 Dev 就直接打开。前端构建工具本来就运行在 CI 中,开发依赖同样可能参与攻击。更合理的判断依据是它会在哪里执行、能访问哪些凭据。
能拿来做什么
- 在前端或 Node 项目中,把锁文件检查安排在
npm ci之前,并明确检查失败与情报服务不可用时各自如何处理,避免查询失败被当成安全通过。 - 先支持团队实际使用的一种锁文件,用真实项目核对解析出的依赖;遇到无法识别的版本、依赖形式或来源时,输出明确错误。
- 对内部 npm 包同时检查名称、scope 和实际下载来源,把
@corp/pkg、私有仓库配置与ResolvedURL放在一起判断。 - 把拼写相似度和脚本模式先作为人工复核线索,保留规则、来源、版本和原始证据,积累误报情况后再决定是否自动阻断。