范围与边界
这是一篇“资料研读与合成示例”,整理/更新于 。示例只读取本地样本,不安装依赖、不执行项目脚本,也不对任何第三方目标发起测试。真实项目应先确认代码、依赖和构建环境的授权范围,并为自动化任务设置网络出口与凭据边界。
本文讨论的是“已知漏洞是否与解析出的版本匹配”。它不是渗透测试,也不是对业务代码的可达性证明。扫描报告应作为变更评审的证据之一,而不是单独的发布结论。
为什么从 Lockfile 开始
版本范围(例如 urllib3>=2.0)描述的是允许安装什么;解析后的 lockfile 才记录了某次安装实际选择了什么。npm 官方文档把 package-lock.json 的 lockfileVersion 分为 v1、v2、v3,并在 packages 映射中记录安装位置与版本。把这些字段保存下来,报告才能回答“哪个文件、哪个路径、哪个版本触发了告警”。
| 输入 | 能确认什么 | 仍需补充什么 |
|---|---|---|
| npm package-lock v1–v3 | 解析后的 npm 版本、嵌套路径、部分 dev 标记 | registry、安装平台、peer/optional 选择 |
| requirements.txt | 精确 pin 或原始版本约束 | 未 pin 的最终解析、环境 marker、私有索引 |
| poetry.lock | 包名、解析版本、main/dev 分类 | 部署组装方式、运行时是否真的加载 |
因此先保存原文件哈希和工具版本,再归一化生态坐标(npm、PyPI 等),最后做漏洞匹配。不要把“无法映射到数据库”改写成“安全”,而应保留为 unknown。
用 lockwatch 复现一次离线扫描
lockwatch 是本站配套的只读示例工具,当前实现支持 package-lock.json、requirements.txt 和 poetry.lock。离线模式默认不联网;只有显式加入 --osv 才会查询 OSV。下面的样本位于博客目录的 samples/supply-chain/,便于在没有业务源码的情况下复核解析逻辑。
cd E:\moban2\lockwatch
$env:PYTHONPATH = "src"
python -m lockwatch scan E:\moban2\secx-security-blog\samples\supply-chain\package-lock.json --format json --pretty
python -m lockwatch scan E:\moban2\secx-security-blog\samples\supply-chain\requirements-pinned.txt --format json --pretty
第一条命令应列出 lodash、minimist 与一个 scoped 包的解析版本;第二条命令会同时展示一个精确 pin 和一个版本范围。输出中的 metadata.osv_queried 应为 false,这正是本地演练不泄露包名的安全默认值。
需要联网时,再明确记录端点、超时和缓存位置:
python -m lockwatch scan E:\moban2\secx-security-blog\samples\supply-chain\package-lock.json `
--osv --timeout 5 --cache .lockwatch-cache.json --format sarif `
--output .\artifacts\lockwatch.sarif
CI 中可以把 SARIF 作为构建产物,但应把缓存视作不可信输入,避免在特权步骤直接执行缓存内容。--fail-on high 适合表达组织门槛;升级前仍应看 advisory 的修复版本、依赖路径和实际部署范围。
OSV 查询与隐私边界
OSV 的 POST /v1/query 接受生态、包名和版本,/v1/querybatch 可批量查询。它匹配的是公开漏洞记录中的受影响范围,不需要上传源代码。即便如此,私有包名、内部版本号和查询时间仍可能构成元数据;团队可在网络代理处做域名白名单、脱敏日志和速率控制,也可以只在受控夜间任务中开启联网。
建议把一次查询的四类证据一起保存:lockfile 的内容哈希、工具和规则版本、OSV 响应中的 advisory ID/修改时间、查询时间与端点。OSV 数据是持续更新的,同一版本在不同日期重新查询可能多出或少一条记录,报告必须带时间而不是只写“扫描通过”。查询失败要显示 error,不能降级为 clean。
如果项目使用私有 registry,先确认包名是否会被发送到公共服务;必要时在内部镜像、离线数据库或经审批的出口执行。不要把 token、完整仓库 URL 或源码片段放入自定义诊断字段。
对外部服务的最小暴露还包括请求节奏和错误日志:固定超时、限制重试、避免把完整响应写进公开构建日志,并给缓存设置过期时间。若项目要求离线交付,可把经过审批的 OSV 数据快照作为版本化输入,报告中注明快照日期和覆盖范围。
版本匹配不等于运行时可达
一个匹配到的 advisory 说明“记录的包版本落在受影响范围内”。它没有回答漏洞函数是否被业务调用、危险配置是否开启、包是否只存在于开发阶段,也没有覆盖镜像里另一个未提交的安装结果。反过来,没有匹配也不等于安全:数据库可能延迟、包名映射可能失败,或漏洞来自自研补丁。
- 先按直接依赖和 transitive 路径分组:直接依赖优先修复,传递依赖要找到能改变解析结果的上游约束。
- 再看修复版本和兼容性:选择明确的 fixed version,跑单元、集成和构建测试,不用“升到 latest”掩盖变更范围。
- 最后判断暴露面:区分生产、构建、测试和文档环境;对 dev-only 结果可设有期限的例外,而不是永久忽略。
可审计的结果不是“零告警”,而是每一个版本都有一条能被复盘的接受或修复理由。
误报、遗漏与限制
- 范围版本:lockwatch 会保留未精确 pin 的约束,但不会为
>=或^自动猜一个版本查询 OSV;应先生成可信 lockfile。 - 生态元数据:同名包、大小写、私有 fork、替换源和 git 依赖可能无法映射到公共坐标。
- 运行时差异:平台 marker、optional/peer 依赖、容器层和构建脚本会改变最终安装集,源码仓库扫描不能代替镜像扫描。
- 可达性:工具没有做调用图、配置展开或动态加载分析;高严重度只代表优先级信号,不是影响已被利用的证据。
例外应写明 advisory ID、理由、负责人和到期时间。供应商发布新版本或 OSV 记录变更后,要重新打开例外评审;没有期限的 ignore 文件会把风险变成遗忘。
修复与验证清单
| 阶段 | 验证动作 | 留存证据 |
|---|---|---|
| 解析前 | 锁定输入路径、哈希、工具版本;确认扫描范围已授权 | 提交 SHA、文件哈希、运行命令 |
| 匹配后 | 核对 advisory 受影响范围、fixed version、直接/传递路径 | OSV ID、修改时间、依赖树片段 |
| 修复后 | 更新 manifest 与 lockfile,离线复扫,再做构建和回归 | 新旧报告、测试结果、变更评审 |
| 发布前 | 扫描实际镜像/部署产物,确认 dev 依赖未进入运行层 | 镜像摘要、SBOM、发布门禁日志 |
本地样本和命令只验证解析路径,不代表任何真实项目的安全状态。若要把它接入 GitHub Actions,建议在低权限 job 运行离线解析,把联网查询和 SARIF 上传拆成审查过的步骤,并固定 action 版本。