关于 dependabot-triage
面向 JavaScript/TypeScript 仓库的 Dependabot 告警分诊与修复技能,覆盖 npm、pnpm、yarn 和 bun。它结合暴露面评估、风险分级与锁文件版本校验,并在更改和创建 PR 前设置确认步骤。
主要功能
- 01标准与 Fast-Track 分诊模式
- 02检查本地及 CI 包管理器
- 03按影响、暴露面和 CVSS 排序
- 04分析导入位置与变更日志
- 05跨包管理器锁文件版本校验
- 06确认后创建修复 PR
适用场景
- 01确定多个 Dependabot 告警的处理优先级
- 02修复 JavaScript 或 TypeScript 依赖漏洞
- 03核对 CI 与本地包管理器版本一致性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ akshayrao14/ git-practices --skill dependabot-triage安装后在 Claude Code 中输入/dependabot-triage确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
install.shLICENSEREADME.mdSKILL.md.tmpl详细介绍
Dependabot 分流与修复(v2.1)
v2.1 亮点
- 两种模式——标准模式和快速通道。 标准模式是防御性工作流(完整的暴露面映射、变更日志抓取、安全联锁)。快速通道适用于低风险升级(内部/开发类别、CVSS < 7,或用户主动选择)——跳过变更日志和详细的暴露面枚举,采用单次确认联锁。两种模式都必须进行一致性检查并双写,不容妥协。
- CI 工作流检查——一级检测步骤。 仅凭锁文件无法判断 CI 中实际运行的内容。CI 可能使用与已提交锁文件不同的包管理器(例如,提交了 yarn.lock,但 CI 运行
npm install),也可能运行<pm> install,而非严格依据锁文件的<pm> ci。检测结果决定覆盖规则和一致性矩阵。 - 防御性版本选择——选择能修复所有集群内 CVE 的最低修复版本,而不是“同一主版本中的最新版本”。
- 暴露面映射取代启发式可达性分析——对每个导入位置进行分类(公开/API · 客户端包 · 内部/开发),向用户展示影响范围,而不是试图证明代码不可达。
- 强制锁文件一致性检查——每个修改清单文件的包管理器都必须将目标包解析为同一版本;不一致则中止 PR。之所以必须执行,是因为 CI/CD 经常使用与本地健全性检查不同的包管理器。
- 抓取变更日志并设置安全联锁(标准模式)——获取当前版本至目标版本之间的发行说明/CHANGELOG,标记
BREAKING/DEPRECATED/MIGRATION关键词,并在应用升级前暂停,等待用户明确确认。 - 自动回退——如果快速通道的一致性检查失败,或升级后构建失败,该技能会建议切换回标准模式进行深入分析,而不是反复重试。
- 仅在用户主动选择时进行组织级扇出——绝不自动触发;枚举组织会消耗 API 速率限制额度,还会发现非 JS 生态的噪声。
此技能的适用范围:npm、pnpm、yarn(classic/berry)、bun。
范围
仅适用于 JavaScript / TypeScript 仓库——npm、pnpm、yarn、bun 生态。涵盖后端 Node.js 服务、前端包(CSR)、服务端渲染应用(SSR)以及混合 SSR+CSR 框架(Next.js、Nuxt、Remix、SvelteKit)。
尚未涵盖:Python(pip / poetry / uv)、Go modules、Java(Maven / Gradle)、Ruby(bundler)、Rust(cargo)、.NET(NuGet)。如果用户分享的 Dependabot URL 指向一个告警属于非 JS 生态的仓库(检查 .dependency.package.ecosystem),请明确说明并停止——不要将此技能的操作方法用于这些生态。
组织级扇出会发现非 JS 告警(例如 Python pillow、Java 包)。请将这些告警从排名表中筛除,或标注为“此技能不适用范围——需单独处理”。
如何安装(一次性操作,每位工程师各自安装)
npx skills add akshayrao14/dependabot-triage
该命令会将技能安装到适合你的 Agent 的技能目录(Codex ~/.codex/skills、Claude Code ~/.claude/skills,或开放标准目录 ~/.agents/skills)。之后请重启 Agent 会话——该技能应会显示在 /skills(Claude Code)中,或通过 Agent 的技能发现功能找到。
如何调用
触发短语(使用其中任意一句即可让 Agent 调用此技能):
- “对
<repo>中的 Dependabot 告警进行分流” - “我应该先修复哪个漏洞?<github.com/.../security/dependabot URL>”
- “修复此仓库中的 Dependabot 告警 #”
- “将
<pkg>升级到非易受攻击版本”
请提供以下任意一项:
- 仓库级 Dependabot 安全 URL(
github.com/<org>/<repo>/security/dependabot),或 - 仓库路径 + 告警编号,或
- 如果你已知漏洞,只提供包名。
组织级扇出仅在用户主动选择时执行。 即使用户粘贴了组织 URL(github.com/orgs/<org>/security/alerts/dependabot),也不要自动枚举整个组织。请先确认:“此操作会分页检查组织中每个仓库的告警,可能会大量消耗 API 速率限制额度。是否继续?”只有得到 yes 后才运行。明确触发组织级扇出的短语:
- “在整个组织中进行扇出”
- “对
<org>中的所有仓库进行分流” - “运行组织范围的分流”
前置条件
- 已通过身份验证且拥有仓库访问权限的
ghCLI(gh auth status)。 - 目标仓库的本地克隆(Agent 需要读取
package.json和锁文件)。 - 已安装
node、pnpm和/或npm,用于重新生成锁文件。 - 拥有推送分支并创建 PR 的写入权限(或者接受 Agent 在提交步骤停止)。
Claude 将执行的操作(v2.1 工作流)
Detect PMs → Fetch + Rank → Mode Selection → { Standard | Fast-Track } → Apply + Parity Check → PR
两种模式的前置步骤(1–4)相同。中间步骤根据模式有所不同。收尾步骤(重新生成 + 一致性检查 + PR)完全相同,且不可跳过。
- 检测正在使用的包管理器 — 获取警报前,先查明在所有场景(本地安装、CI 安装、容器构建)中,哪些包管理器实际用于此仓库。下方的覆盖 + 一致性矩阵取决于此结果。
- 已提交的锁文件:
ls package-lock.json pnpm-lock.yaml yarn.lock bun.lock* 2>/dev/null。 - CI/容器安装命令 — 搜索 CI 配置和 Dockerfile,查找实际的安装命令:
rg -tg '*.yml' -tg '*.yaml' \ '\b(npm (install|ci)|yarn install|pnpm install|bun install)\b' \ .github .gitlab-ci.yml Dockerfile* docker-compose*.yml circle.yml .circleci 2>/dev/null - 对比已提交的锁文件与 CI 安装命令。如果 CI 使用的包管理器与已提交锁文件对应的包管理器不同,或者 CI 使用的包管理器没有对应的已提交锁文件,则相应扩展覆盖 + 一致性矩阵(参见“覆盖模式”和“验证(一致性检查)”)。
- 不要一开始就让用户说明包管理器的配置。 用户通常只了解部分情况(本地环境,而非 CI;或反之)。应根据锁文件和 CI 搜索结果进行检测,展示发现的情况,并仅在发现异常时请求确认(例如:“我看到已提交了 yarn.lock,但 CI 运行的是
npm install——请确认这是有意为之?”)。
- 已提交的锁文件:
- 获取警报:
- 仓库 URL →
gh api "repos/<org>/<repo>/dependabot/alerts?state=open&per_page=100" - 组织 URL → 仅在用户明确确认后,运行
gh api "orgs/<org>/dependabot/alerts?state=open&per_page=100"(会对所有仓库分页获取)。 - 对于此技能,仅筛选
.dependency.package.ecosystem == "npm";将非 JS 警报标记为范围外。 - 按
(repo, package)去重。 - 包含多种生态系统的仓库(Python + npm、Go + npm 等): 在展示排序表格之前,先按生态系统对警报分组。将范围外的生态系统列为顶部的独立阻塞部分——例如:“⛔ 此技能不处理(请单独处理):pillow × 10(pip)、urllib3 × 2(pip)。”用户需要看到哪些内容未涵盖,以便在其他地方处理。绝不要悄悄忽略非 JS 警报。
- 仓库 URL →
- 根据优先级框架进行排序(影响 × 暴露程度 × CVSS)。
- 阅读安全公告,确定首选项——提取
first_patched_version、vulnerable_version_range和cvss.score。对于漏洞簇,计算能够涵盖所有 CVE 修复版本的最小版本。 - 选择模式——建议快速通道或标准模式。 资格规则见“快速通道模式”部分。在排序摘要旁给出一句建议,例如:“这是一个开发依赖;要通过快速通道处理此修复吗?” 如果用户没有回应或意图不明确,默认使用标准模式。若 CI 使用的包管理器没有对应的已提交锁文件,也默认使用标准模式——每次 CI 构建都会重新解析传递依赖,覆盖字段是唯一的固定版本方式,而且这种情况不符合快速通道的条件(无法检查锁文件一致性)。
标准模式(步骤 6–8)— 完整防御流程
- 暴露面映射 — 在源代码中定位目标包的每个导入位置,并将其分类为 Public/API · Client-Bundle · Internal/Dev(参见“暴露面映射”部分)。输出数量和示例路径。
- 抓取变更日志 — 获取当前安装版本到目标版本之间的发行说明 /
CHANGELOG.md。搜索BREAKING、DEPRECATED、MIGRATION、removed、dropped support。逐字显示标记出的行。 - 安全联锁 — 暂停。 向用户显示暴露面摘要、变更日志标记和选定的目标版本。在进行任何文件编辑之前,等待用户明确确认(“yes”“go”“looks fine”等)。如果标记出
BREAKING/DEPRECATED/MIGRATION,则需要用户再次确认。
快速通道模式(步骤 6–8)— 低风险捷径
- 轻量暴露面检查 — 仅运行一次导入位置搜索,以确认主要暴露面类别,但不要枚举路径。输出:
Exposure: Internal/Dev (8 sites)。到此为止;不按类别细分,也不列出路径。 - 跳过变更日志抓取。(快速通道明确以牺牲变更日志可见性换取速度;下方的资格规则会限制可能造成的影响范围。)
- 单次确认联锁 — 暂停。 输出:目标版本 + 主要暴露面类别 + CVSS + 快速通道资格原因。用户确认一次(“yes”“go”“ship it”)即可继续;任何含糊不清的回复都会退回标准模式。
应用(步骤 9–13)— 两种模式完全相同,不可协商
- 确认分支策略和基分支。 使用
gh repo view --json defaultBranchRef检测默认分支;绝不硬编码main。默认策略:从最新的origin/<detected-base>为每个包创建一个 PR。 - 编辑
package.json— 为步骤 1 中检测到的每个 PM 写入 override 字段(npmoverrides、pnpmpnpm.overrides、yarnresolutions、bunoverrides;参见“Override pattern”)。两种模式都必须执行。 - 重新生成每个相关锁文件 — 对步骤 1 中检测到的每个 PM 运行其安装命令。对于没有已提交锁文件的 PM,生成临时锁文件用于一致性检查,并在提交前删除它(参见“验证(一致性检查)”)。不存在“主要”锁文件。
- 一致性检查 — 对步骤 1 中检测到的每个 PM,读取其锁文件(或
<pm> why输出),并断言每个 PM 解析到的目标包版本都相同。版本不一致 → 两种模式下都中止 PR。 后续操作参见“版本不一致 / 构建失败时自动还原”。 - 在分支上提交并创建 PR — 推送前必须获得用户明确确认。
Claude 未经确认不会执行的操作
- 将直接依赖升级到更高主版本(可能造成破坏性变更)。
- 变更日志抓取标记出
BREAKING/DEPRECATED/MIGRATION时,未经用户再次确认就应用升级。 - 锁文件一致性检查失败时创建 PR(标准模式或快速通道模式均如此)。
- 跳过双重写入 override(两种模式下都必须执行)。
- 在明确不符合资格的情况下使用快速通道 —
Public/API导入位置同时满足 CVSS ≥ 7.0、advisory 严重性为critical,或需要跨主版本升级。无论用户如何要求,都退回标准模式;此安全底线不可覆盖。 - 用户只粘贴了单个仓库 URL(或反之)时,在整个组织范围内自动批量执行。
- 删除或替换包。
- 推送到
main/ 合并 PR。 - 跳过提交前钩子。
快速通道模式
适用于低风险升级的精简工作流分支。以牺牲变更日志可见性和详细的暴露面枚举换取速度。不会牺牲锁文件完整性 — 双重写入 override 和一致性检查仍为必需步骤。
适用条件(满足任意一项即可)
- 仅开发环境暴露 — 轻量级导入点检查显示,
Internal/Dev(配置、脚本、测试、工具)中的导入点占比 ≥ 90%,且Public/API中没有导入点。 - CVSS 较低 —
security_advisory.cvss.score< 7.0 且Public/API中没有导入点。 - 用户主动选择 — 用户明确表示
"fast-track"、"just bump it"、"don't bother with the changelog"或类似内容。
如果都不满足,则默认使用标准模式。
不适用条件(不可覆盖 — 始终使用标准模式)
出现以下情况时,将拒绝快速通道 — 回退到标准模式并用一句话说明原因:
- 包存在任何
Public/API导入点,且 CVSS ≥ 7.0。 - 通告严重性为
critical(无论 CVSS 如何)。 - 需要跨主版本升级(防御性最低版本位于下一个主版本中)。
- CI 运行包管理器的宽松安装命令(而非锁文件严格模式下的等效命令),且没有为该包管理器提交锁文件。每次 CI 构建都会重新解析传递依赖 — 该侧唯一的固定项就是覆盖字段。风险更高;应当采用标准模式,完整披露信息。也不符合对等性检查条件:没有已提交的锁文件可供比对。
这些条件不可覆盖。如果用户在上述任一情况下明确要求使用快速通道,应礼貌拒绝并运行标准模式 — 安全底线不可由用户配置。
快速通道跳过的内容
| 步骤 | 标准模式 | 快速通道 |
|---|---|---|
| 暴露映射(逐个列举导入点) | 是 — 按类别列出完整路径 | 跳过 — 仅用一行列出主要类别 |
| 变更日志抓取 | 是 — 发行说明 + CHANGELOG.md,并标记 BREAKING/DEPRECATED/MIGRATION | 跳过 |
| 安全互锁 | 检测到破坏性变更关键词时需两次确认 | 需一次确认 |
快速通道保留的内容(不可妥协)
| 步骤 | 两种模式 |
|---|---|
| 防御性最低修补版本 | 是 |
| 双写覆盖(参见覆盖模式) | 是 |
| 重新生成两个锁文件 | 是 |
| 锁文件对等性检查 + 不匹配时中止 | 是 |
| 仅在用户明确同意后提交 + 推送 | 是 |
推荐提示
在第 5 步展示排序摘要时,添加“模式”列或一行推荐:
Rank | Repo | Package | CVSS | Exposure (dominant) | Recommended mode
1 | webhook-ternity | @types/node | 5.3 | Internal/Dev | Fast-Track (Internal/Dev + low CVSS)
2 | webhook-ternity | axios | 7.5 | Public/API | Standard (hard-ineligible: Public/API + CVSS ≥ 7)
3 | webhook-ternity | follow-redirects | 6.5 | Public/API | Standard (Public/API exposure, no auto-rule fires; opt-in available)
适当时可逐字使用以下提示:“#1 是仅用于开发环境的 @types 升级,CVSS 为 5.3 — 你想通过快速通道修复吗?”
注意:Client-Bundle 中 CVSS < 7.0 的 XSS 消毒器包(例如 dompurify、sanitize-html)将根据适用条件表触发自动快速通道规则。展示推荐时请标明这一点:“这是 Client-Bundle 中的消毒器类包 — 根据 CVSS 符合快速通道条件,但你可能希望使用标准模式查看变更日志。由你决定。” 以用户选择为准。
对等性检查 / 构建失败时自动回退
快速通道会跳过分析,但不会掩盖失败。如果升级后出现以下情况:
- 对等性检查失败(npm 和 pnpm 解析到了不同版本),或者
- 用户运行
pnpm build/npm test并报告失败,或者 - 提交前钩子(lint、类型检查)失败
…则提出为同一个包切换回标准模式:
“快速通道遇到了
<failure>。要我在标准模式下重新运行吗 — 拉取变更日志并进行完整的暴露映射,以便查看实际变更内容?”
同一个包失败后,不要悄悄重试快速通道。应升级到标准模式或停止并询问。对失败的升级反复重试快速通道,正是此模式旨在避免的陷阱。
优先级框架
按以下三个维度排序:
- 影响 — RCE > 数据窃取/SSRF > DoS > 逻辑错误
- 暴露面 — 见下文的暴露面映射。这不是对漏洞是否可触达的启发式猜测,而是对软件包被导入位置的分类,并向用户呈现为攻击面摘要。
- CVSS — 仅用作平局时的判定依据。脱离暴露面上下文的原始分数会产生误导(例如,axios 中的 SSRF 显示为 4.8,但如果 axios 位于 Public/API 中,影响就更大)。
CVSS=0 时的回退处理。 如果公告尚未评分(新发布的 GHSA 中很常见),Dependabot 会返回
security_advisory.cvss.score: 0(或null)。不要让未评分的警报在按 CVSS 排序的排名中垫底;应回退使用security_advisory.severity(该字段始终有值)。分数的大致映射:critical≈ 9.5,high≈ 7.5,medium≈ 5.0,low≈ 2.0。在排序表中明确标出回退结果,例如写成cvss: — (high),而不是cvss: 0,让用户了解为什么高严重性警报排在低 CVSS 警报之前。
暴露面映射
此模型取代了此前的启发式“可触达性”模型。目标是不再尝试证明漏洞不可触达,因为漏报很危险。改为枚举目标软件包的所有导入位置,并将其分别归类。用户根据攻击面最终判断风险。
类别
| 类别 | 定义 | 文件路径示例 |
|---|---|---|
| Public/API | 处理来自信任边界外部的 HTTP/RPC/webhook 流量的代码。攻击者可控的输入会流入此处。 | handlers/、routes/、controllers/、app/api/**/route.{ts,js}、pages/api/**、middleware.{ts,js}、+server.{ts,js}、+page.server.{ts,js}、Lambda 入口处理程序、Express/Fastify 路由定义 |
| Client-Bundle | 会打包到浏览器中的代码。可通过 XSS/DOM 注入/渲染到浏览器的恶意用户内容触达。 | src/components/**、src/app/**/{page,layout,loading,error}.{ts,tsx}(无 .server. 后缀)、src/pages/**(Next.js 传统目录)、Vue/Svelte 组件文件,以及任何导入到通过 <script> 发布的入口中的内容。 |
| Internal/Dev | 构建、工具、脚本、测试和配置。不在生产请求路径中运行,也不会发送到浏览器。 | vite.config.*、next.config.*、*.config.{js,ts,mjs}、scripts/、tests/、**/*.test.*、**/*.spec.*、.husky/、.cicd/、tools/、bin/(CLI 脚本) |
如何枚举导入位置
优先使用 ripgrep(速度快,并遵循 .gitignore):
# Find every import site of <pkg>
rg -t js -t ts "from ['\"]<pkg>(/|['\"])|require\(['\"]<pkg>(/|['\"])" -l | sort -u
备选方案是 grep:
grep -rE "from ['\"]<pkg>['\"]|require\(['\"]<pkg>['\"]\)" \
--include='*.{js,jsx,ts,tsx,mjs,cjs,mts,cts,vue,svelte}' -l | grep -v node_modules
然后根据上表匹配路径并逐一归类。无法明确归类的路径 → 询问用户,不要猜测。
如何向用户呈现
输出攻击面摘要,而非结论:
Exposure surface for axios in webhook-ternity:
Public/API : 12 sites (e.g. handlers/getAllCandidates.js, handlers/public/idMetadata.js, ...)
Client-Bundle : 0 sites
Internal/Dev : 3 sites (e.g. tests/util.test.js, scripts/migrate.js, ...)
如果单个导入位置横跨多个类别(例如,某个 util 同时由路由处理程序和客户端组件重新导出),则归入其中风险最高的类别。
用户查看攻击面后,自行决定升级是否值得承担风险——Claude 不会说“此漏洞可触达”或“此漏洞不可触达”。
跨仓库 / 全组织排名(选择启用)
仅在用户明确要求跨组织分发时运行。在影响 × 暴露度 × CVSS 的基础上应用:
- 先按
(repo, package)分组。 对同一(repo, package)组合,如果同一个依赖有 N 个 CVE,GitHub 会创建 N 条警报。将它们合并——一次升级即可修复这一组问题。 - 集群加成。 在 Public/API 仓库中,警报很多的
(repo, package)组合是 ROI 最高的选择,即使单个 CVSS 评分一般也是如此。例如:webhook 服务中的 30 条 HTTP-client 警报 > 内部 CLI 工具中的 1 条 CVSS-9 警报——一个 PR 就能关闭 30 条警报,而且暴露度高。 - 仓库特征仅供参考,不是定论。 后端 / webhook 仓库往往包含 HTTP 库的 Public/API 导入点;前端仓库往往包含 XSS 清理器的 Client-Bundle 导入点。应通过暴露度映射确认,而不是想当然。
输出排名表:排名、仓库、包、暴露度摘要(Public/API / Client-Bundle / Internal/Dev 数量)、合并的警报数。让用户选择,或接受默认项(排名第 1)。
分支策略
默认:每个包一个分支,每个分支一个 PR。 更易于审查;如果单个覆盖项导致下游问题,也更容易回滚。
动态确定基分支
不要将 main 硬编码。基分支可能是 main、master、pre-release、develop 等。运行时检测:
# Repo's configured default branch (preferred)
gh repo view --json defaultBranchRef --jq .defaultBranchRef.name
# Fallback if gh unavailable
git symbolic-ref refs/remotes/origin/HEAD --short | sed 's@^origin/@@'
如果用户采用不同的发布流程(例如 PR 先合入 pre-release,再提升至 main),请询问这个 PR 应以哪个分支为目标。不要自行假设。
始终从最新的远程基分支创建分支,而不是从本地分支
BASE=<detected-or-confirmed-base>
git fetch origin "$BASE"
git checkout -b <new-branch> "origin/$BASE"
本地的基分支副本可能已过时;从它创建分支会让 PR 差异混入因遗漏上游提交而产生的偏差。
在多 PR 会话中,每次创建新分支前都要运行 git fetch origin <base>——不要重复使用本次会话早些时候的 fetch 结果。同一会话中已创建的 PR 可能已经合并(自动合并、快速审查),这意味着 origin/<base> 自上次 fetch 后可能已有更新。使用过时的 fetch 会造成原本通过重新获取最新内容所要避免的合并冲突问题。
策略选项——询问用户
开始前,询问用户这批更改希望采用哪种策略:
- (a) 每个包一个 PR(默认)——原子化,回滚干净
- (b) 多个包共用一个 PR——待审查的 PR 更少,但回滚时会相互牵连
- (c) 基于现有分支 / 已开启的 PR 继续提交——适用于之前的 PR 仍处于开启状态,且用户希望将更改合并打包的情况
如果用户说“怎样更清晰就怎样”,选择 (a)。
同一个包出现在多个清单中
如果同一个易受攻击的包出现在多个清单中(例如根清单和子包清单),默认采用每个包一个 PR,并修改所有清单——一个分支修改每个受影响的清单,并重新生成每个受影响的锁文件。这样可以减少 PR 数量,并确保修复是原子化的。
只有当不同清单中的暴露类别差异显著(例如根清单中是 Public/API,子清单中是 Internal/Dev),且用户明确要求针对每种暴露级别分别设置审查关卡时,才按清单拆分为多个 PR。
多个 PR 会话:连续变基冲突模式
当用户选择策略 (a) — 每个软件包提交一个 PR — 且多个 PR 的目标是同一个清单时,后续每个 PR 都会在清单的同一个插入位置(overrides 块)发生冲突。这是可预见的,并非错误。解决方法始终是增量添加:保留 HEAD 中的 overrides,并追加传入 PR 的键。
每个 PR 合并后:
git fetch origin <base>git checkout <next-branch> && git rebase origin/<base>- 解决冲突时,保留所有现有的 override 键并添加新的键。
- 使用 PM 的仅更新锁文件安装命令重新生成锁文件(参见此技能中的 Override pattern 部分)。
git add <manifest> <lockfile> && git rebase --continuegit push --force-with-lease origin <next-branch>
不要使用 --force(不带 lease)——它会跳过安全检查,而该检查会在远程分支的进度超出你已获取的内容时中止操作。
每条告警的处理流程
假设已完成“检测正在使用的包管理器”(工作流步骤 1)——下面的覆盖配置 + 一致性矩阵依赖于了解哪些包管理器会操作此代码库。
-
去重:GitHub 会为每个已提交的锁文件分别生成一条告警。两个已提交的锁文件(例如
package-lock.json+pnpm-lock.yaml)= 2 条告警、1 个漏洞。只需修复一次该包。 -
获取安全公告:
gh api repos/<org>/<repo>/dependabot/alerts/<n>— 提取first_patched_version和vulnerable_version_range。列表端点不会显示这些信息。对于告警簇,请获取簇中的每条告警。 -
定位:直接依赖(在
package.json中)还是传递依赖?查找依赖它的包:node -e "const l=require('./package-lock.json'); for(const[k,v]of Object.entries(l.packages||{}))if(v.dependencies?.['<pkg>']||v.devDependencies?.['<pkg>'])console.log(k,v.version);" -
暴露面映射:参见“暴露面映射”一节。对每个导入位置进行分类(Public/API · Client-Bundle · Internal/Dev),并为用户汇总暴露面。
-
选择版本(防御性最小修复版本):
- 单条告警:选择
first_patched_version对应的^X.Y.Z。 - 告警簇:选择满足
X.Y.Z =簇中所有 CVE 的first_patched_version最大值的^X.Y.Z—— 也就是涵盖所有修复的最小版本。通过以下命令计算:for n in <alert-numbers>; do gh api repos/<org>/<repo>/dependabot/alerts/$n \ --jq '.security_advisory.vulnerabilities[0].first_patched_version.identifier' done | sort -V | tail -1 - 不要默认选择“同一主版本中的最新版本”。 最新版本会扩大变更范围,并有破坏传递依赖的风险。仅在以下情况覆盖防御性最低版本:(a) 用户明确要求,或 (b) 最低版本已停止维护 / 已撤回 / 会引入新的 CVE。
- 除非用户批准升级主版本,否则限制在当前安装版本的同一主版本内。
- 单条告警:选择
-
抓取变更日志 — 获取当前版本到目标版本之间的发布说明。优先尝试 GitHub release API;失败时从包 tarball 中获取
CHANGELOG.md:# 1. Find the upstream repo PKG_REPO=$(npm view <pkg> repository.url | sed -E 's|.*github\.com[/:]([^.]+)\.git|\1|') # 2. Pull release notes for the target tag (try with and without leading "v") gh release view "v<target>" --repo "$PKG_REPO" --json tagName,body,createdAt 2>/dev/null \ || gh release view "<target>" --repo "$PKG_REPO" --json tagName,body,createdAt # 3. Fallback: CHANGELOG from the published tarball npm pack <pkg>@<target> >/dev/null tar -xf <pkg>-<target>.tgz -C /tmp/<pkg> grep -inE 'BREAKING|DEPRECATED|MIGRATION|removed|drop(ped)? support' /tmp/<pkg>/package/CHANGELOG.md | head -50将标记出的行原样展示给用户——不要改述。如果出现
BREAKING/DEPRECATED/MIGRATION,则将安全确认提升为“需要二次确认”,再继续操作。**传递依赖限定说明:**要求二次确认前,请检查标记出的包在源代码中是否有直接导入位置(参见暴露面映射步骤)。如果没有直接导入,该包就是纯传递依赖——请在标记旁明确说明:“此标记出现在一个不会被源代码直接导入的包中。除非您的代码会解析其错误消息或接入其内部实现,否则此破坏性变更不太可能影响您。” 如果该标记显然与调用代码无关,这样就可以将两轮确认合并为一轮。
-
**安全确认 — 暂停。**向用户展示:
- 选定的目标版本 + 原因(防御性最低版本还是最新版本,以及原因)
- 暴露面(每个类别的数量、示例路径)
- 变更日志标记(逐字引用的行;若无则写“无标记”)
等待用户明确同意。如果出现变更日志标记,必须得到明确无误的“是,继续” / “我已处理”——未回复或含糊回复都意味着停止。
-
用户同意后:编辑
package.json、重新生成两个锁文件、运行 Parity Check、提交,并在推送前征求同意。
覆盖配置模式(Node.js)
以下情况使用覆盖配置:
- 传递依赖漏洞 — 此包不在
dependencies/devDependencies中。若不升级父依赖,覆盖配置是唯一的修复方式。 - 直接依赖且传递依赖也要求使用此包 — 升级
dependencies.<pkg>并不能保证传递依赖使用同一版本(不同的 semver 范围可能会分别提升安装)。仍需添加覆盖配置作为纵深防御。
双重写入是强制要求
在任何情境(本地安装、CI 安装、容器构建)下,都要为步骤 1 中检测到的每个 PM写入覆盖字段。不同 PM 使用不同的覆盖字段:
| PM | package.json 中的覆盖字段 |
|---|---|
| npm | overrides |
| pnpm | pnpm.overrides |
| yarn classic (v1) | resolutions |
| yarn berry (v2+) | resolutions |
| bun | overrides |
当涉及多个 PM 时,只写入其中一个字段会导致环境漂移:开发者在本地看到一个解析版本,CI 发布的却是另一个。这正是此技能要防止的错误类型。每次都要将覆盖项写入每个相关字段,并使用相同的目标范围,即使当前只提交了一个锁文件也是如此。
本组织中一种常见模式是 npm-CI + pnpm-local;任何 PM 不匹配的情况(yarn-local + npm-CI、bun-local + npm-CI 等)都会导致同类漂移。规范的 npm + pnpm 双重写入如下:
{
"pnpm": {
"overrides": {
"<pkg>": "^X.Y.Z"
}
},
"overrides": {
"<pkg>": "^X.Y.Z"
}
}
对于 yarn + npm 组合,应同时写入 resolutions 和 overrides;对于 yarn + pnpm 组合,应同时写入 resolutions 和 pnpm.overrides。请根据上表进行替换。
然后重新生成每个相关锁文件。规范的 pnpm + npm 组合如下:
pnpm install --no-frozen-lockfile
npm install --package-lock-only
对于其他 PM,重新生成命令见“前端代码库 → 各包管理器的锁文件与覆盖语法”表格。
工作流末尾的 Parity Check 会断言每个 PM 生成的解析版本都相同,详见“验证(Parity Check)”。
npm EOVERRIDE:覆盖项必须包含在直接依赖范围内
当 overrides.<pkg> 不在 dependencies / devDependencies 中声明的范围内时,npm 会报错:
EOVERRIDE: Override for <pkg>@<range> conflicts with direct dependency
两种规范的修复方式:
- 收紧直接依赖范围,使其与覆盖项一致或比覆盖项更窄。例如,直接依赖
"postcss": "^8"搭配overrides.postcss: "^8.5.10"会报错;将直接依赖改为"postcss": "^8.5.10",覆盖项即可正常生效。 - 如果不应更改直接依赖范围,则通过
"$<pkg>"自引用:"overrides": { "postcss": "$postcss" }— npm 会重复使用已解析的直接依赖版本。如果直接依赖已经是修补后的版本,而你只需要强制传递依赖使用该版本,此方法很有用。
Yarn resolutions 没有这项限制 — 只有 npm overrides 有。
前端代码库(浏览器包 + SSR)
核心工作流可直接应用于 React、Next.js、Vite、Vue、Svelte、Angular 代码库。前端特定说明:
各包管理器的锁文件与覆盖语法
| 包管理器 | 锁文件 | package.json 中的覆盖字段 | 重新生成命令 |
|---|---|---|---|
| npm | package-lock.json | "overrides": { ... } | npm install --package-lock-only |
| pnpm | pnpm-lock.yaml | "pnpm": { "overrides": { ... } } | pnpm install --no-frozen-lockfile |
| yarn (v1) | yarn.lock | "resolutions": { ... } | yarn install |
| yarn (berry, v2+) | yarn.lock | "resolutions": { ... } | yarn install |
| bun | bun.lockb(或 bun.lock) | "overrides": { ... } | bun install |
如果多个锁文件并存(常见于迁移期间或 npm-CI / pnpm-local 配置中),请双重写入覆盖项,并重新生成每个锁文件。Parity Check 仍然适用。通过告警负载中的 .dependency.manifest_path 检测告警所引用的锁文件。
CSR + SSR 混合场景的暴露面映射
现代前端仓库很少是纯 CSR。Next.js、Nuxt、Remix、SvelteKit,甚至一些 Vite 配置,都会随 bundle 一起部署服务器运行时。同一个依赖可能同时属于多个暴露类别。
前端仓库中涉及三个类别:
- 公共/API — 处理外部 HTTP/RPC 流量的服务器运行时。例如:Next.js
app/**/route.{ts,js}、pages/api/**、middleware.{ts,js}、Remixloader/action、Nuxt 服务器路由、SvelteKit+page.server.{ts,js}和+server.{ts,js}。与后端公共/API 导入完全同样处理。 - 客户端 Bundle — 会包含在浏览器 bundle 中的代码。例如:
src/components/**、不带.server.后缀的src/app/**/{page,layout,loading,error}.{ts,tsx}、src/pages/**(Next.js 经典默认导出)、Vue/Svelte 组件文件,以及由<script>加载的入口所导入的任何代码。 - 内部/开发 — 配置、构建、脚本、测试。例如:
vite.config.*、next.config.*、*.config.{js,ts,mjs}、scripts/、tests/、**/*.test.*、**/*.spec.*。
如果同一个依赖同时导入自 src/components/Foo.tsx(客户端 Bundle)和 src/app/api/foo/route.ts(公共/API),则优先级应按公共/API 类别确定——同时向用户展示这两处,以便他们了解影响范围并非仅限于单一平面。
路径含义不明确时(例如,共享工具 lib/http.ts 被重新导出到两个平面),请询问用户;不要猜测。
前端特定的影响权重
影响维度仍然优先,但当导入位置仅属于客户端 Bundle 或仅属于公共/API 时,权重背景会有所不同:
- 仅客户端 Bundle 导入 — 高优先级:XSS 清理器绕过(dompurify、sanitize-html)、客户端状态库中的原型污染(lodash.merge、immer 9 之前的版本)、i18n 库中的模板注入、路由器/路径匹配器中的 ReDoS。低优先级:SSRF、标头注入、NO_PROXY 绕过(浏览器不会发起出站 HTTP 请求)。
- 公共/API 导入(任何在服务器端运行的平面) — 按完整的服务器端优先级处理:SSRF、标头注入、HTTP 客户端中的原型污染都再次成为重要问题。
如果 CVSS-9 SSRF 漏洞只出现在客户端 Bundle 导入位置,通常优先级较低;同一漏洞出现在公共/API 导入位置则属于严重问题。暴露面映射的输出会告诉你属于哪种情况。
框架版本锁定 — 谨慎处理
React/Next/Angular/Vue 通常会通过 peer deps 或一方元包锁定传递依赖版本。升级前:
- 检查父框架(
next、react、@angular/core)的peerDependencies。 - 项目进行到一半时升级
@types/react、react-dom、next的主版本,往往会连带影响 30 多个包——请标记为“需要单独的 PR + 手动冒烟测试”,切勿将其合并到安全升级 PR 中。 - 升级
core-js、regenerator-runtime、tslib通常是安全的;升级任何以-loader或-plugin结尾的依赖(webpack/rollup/vite 生态)则经常会破坏构建——提交 PR 前,请运行pnpm build(或等效命令)进行验证。Changelog Scrape 步骤会通过BREAKING/removed标记显示其中的大部分问题。
版本范围规则
- 默认采用防御性最小修复版本。 使用
^X.Y.Z,其中X.Y.Z是能够涵盖所有适用 CVE 修复版本的最低版本(单个警报:first_patched_version;一组警报:该组中first_patched_version的最大值)。仅在主动选择时才使用当前主版本中的最新版本——更大的改动范围会增加升级本身引发问题的可能性。 - 使用
^X.Y.Z,而不是>=X.Y.Z。两者的下限相同,但前者上限为<next_major,可避免意外升级到新的主版本。 - 绝不使用
>X.Y.Z——这会排除修复版本本身,强制升级到X.Y.Z+1,毫无益处。 - 对于 pnpm 条件覆盖(仅在某些范围内存在漏洞),使用
<pkg>@<range>: <fix>语法,例如"axios@<1.12.0": ">=1.12.0"。 - 条件覆盖会过时。 如果已有条件覆盖(例如
"axios@<1.12.0": ">=1.12.0"),而新的 CVE 范围超出先前的上限,则将该条目替换为宽泛的"axios": "^<minimal-patched>"。不要叠加条件覆盖。每批新的 CVE 都会提高最低版本要求。 - 如果防御性最低版本已停止维护、已撤回,或自身又触发了新的 CVE,则向用户升级处理——提供下一个较新的候选版本及其变更日志抓取结果,让用户选择。
验证(一致性检查)
需要通过两项检查。打开 PR 前,两项都必须通过。对步骤 1 中检测到的每个包管理器都运行检查,而不仅仅是标准的 npm + pnpm 组合。
1. 修复版本解析
对于每个检测到的包管理器,确认其解析的目标包版本为 >= minimal_patched_version。直接询问包管理器(或直接读取锁文件)是最可靠的方法——不受锁文件格式变更影响:
# npm: read package-lock.json directly
NPM_VER=$(node -e "const l=require('./package-lock.json'); console.log(l.packages['node_modules/<pkg>']?.version || '');")
# pnpm: ask pnpm itself for the resolved root version
PNPM_VER=$(pnpm why <pkg> --depth=0 --json 2>/dev/null \
| node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{const j=JSON.parse(d);const v=j[0]?.dependencies?.["<pkg>"]?.version || j[0]?.devDependencies?.["<pkg>"]?.version;console.log(v||"")})')
# yarn classic (v1)
YARN_VER=$(yarn why <pkg> 2>/dev/null | grep -oE 'Found "<pkg>@[^"]+"' | head -1 | sed -E 's/.*@//; s/"$//')
# yarn berry (v2+)
YARN_VER=$(yarn why <pkg> --json 2>/dev/null | head -1 \
| node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{try{const j=JSON.parse(d);const k=Object.keys(j.children||{})[0];console.log(j.children?.[k]?.locator?.split("@npm:")[1]||"")}catch{}})')
# bun
BUN_VER=$(bun pm ls --all 2>/dev/null | grep -E '\b<pkg>@' | head -1 | sed -E 's/.*@//')
echo "npm=$NPM_VER pnpm=$PNPM_VER yarn=$YARN_VER bun=$BUN_VER"
仅使用实际涉及的包管理器对应的变量;忽略其余变量。所有结果都必须显示修复版本(或更高版本)。
为没有已提交锁文件的包管理器生成临时锁文件
如果 CI 使用的包管理器没有已提交的锁文件(例如已提交 yarn.lock,但 CI 运行 npm install),则仅为一致性检查生成临时锁文件,然后在提交前将其删除:
| 包管理器 | 仅重新生成锁文件 | 生成的文件 |
|---|---|---|
| npm | npm install --package-lock-only | package-lock.json |
| pnpm | pnpm install --lockfile-only | pnpm-lock.yaml |
| yarn classic | yarn install --mode=update-lockfile | yarn.lock |
| yarn berry | yarn install --mode=update-lockfile | yarn.lock |
| bun | bun install --frozen-lockfile=false | bun.lock |
⚠ 切勿提交来自尚未有已提交锁文件的包管理器所生成的锁文件。双锁文件漂移正是这项技能要防止的错误类型——在权威锁文件
pnpm-lock.yaml旁生成yarn.lock,会在下次运行yarn install时悄然产生偏差;你扩大了影响范围,而不是将其减半。
如果检测到的 CI 包管理器没有已提交的锁文件,还要明确断言:
package.json中存在该包管理器对应的覆盖字段(参见“覆盖模式 → 双写是强制要求”中的包管理器表)。- 目标包的直接依赖范围与覆盖项一致(避免
EOVERRIDE;参见“npmEOVERRIDE”)。
2. 锁文件一致性(强制要求——不一致时中止 PR)
# Collect non-empty resolved versions across all detected PMs
VERS=$(printf '%s\n' "$NPM_VER" "$PNPM_VER" "$YARN_VER" "$BUN_VER" | grep -v '^$' | sort -u)
COUNT=$(echo "$VERS" | grep -c .)
if [ "$COUNT" -eq 0 ]; then
echo "PARITY ABORT: could not resolve <pkg> in any lockfile"
exit 1
fi
if [ "$COUNT" -gt 1 ]; then
echo "PARITY ABORT: PMs disagree:"
echo "$VERS"
exit 1
fi
echo "PARITY OK: all PMs resolved <pkg>@$VERS"
如果任意一对包管理器解析出的目标包版本不同,请中止 PR。不要推送,不要创建 PR,也不要提交只修复了一半的状态。向用户指出不一致,并列出所有版本和可能原因:
- 某个覆盖机制中缺少覆盖规则(最常见)——重新编辑
package.json,将覆盖规则写入所有相关字段,重新生成锁文件并再次检查。 - 条件式 pnpm 覆盖规则(
pkg@<range>)不受 npm 支持——改为无条件覆盖。 - pnpm 提升边界导致传递依赖副本的版本不同——可能需要检查
pnpm-lock.yaml(pnpm why <pkg>会显示多个版本)。 - npm 的
EOVERRIDE阻止了覆盖规则,可能没有提示,也可能报错——检查直接依赖的版本范围(参见“npmEOVERRIDE”)。 - 对等依赖不匹配导致传递依赖解析结果不同——强制覆盖前先调查。
开发环境与 CI/CD 之间的环境漂移(任意包管理器组合:npm-CI + pnpm-local、npm-CI + yarn-local、bun-local + npm-CI 等)正是此技能旨在防止的问题类型。一致性检查是不可妥协的。
不要轻信安装命令的标准输出
即使覆盖规则强制重新解析且锁文件实际上发生了变化,npm install --package-lock-only 也可能输出 up to date。在某些边缘情况下,pnpm、yarn 和 bun 也一样。务必运行上述验证命令——它们会直接读取锁文件(或包管理器自身的解析图)。
提交/PR 格式
- 标题:
security: bump <pkg> to ^X.Y.Z (CVE-XXXX-YYYYY) - 正文:链接 Dependabot 提醒编号,注明公告(GHSA + CVE),写明 CVSS,说明依赖链,并注明如果仅修改覆盖规则则没有更改源代码。
合并后——UI 延迟
GitHub 的 Dependabot UI 计数通常会比实际修复滞后 5–30 分钟。如果用户在合并后不久报告“只关闭了 N 条提醒,而你预测会关闭 M 条”,不要以为修复不完整。先通过 API 验证:
# Open axios alerts in this repo right now
gh api "repos/<org>/<repo>/dependabot/alerts?state=open&per_page=100" --jq '[.[] | select(.dependency.package.name=="<pkg>")] | length'
# Fixed-today list (sanity check the merge actually closed them)
gh api "repos/<org>/<repo>/dependabot/alerts?state=fixed&per_page=100" --jq '[.[] | select(.dependency.package.name=="<pkg>" and (.fixed_at | startswith("'"$(date -u +%Y-%m-%d)"'")))] | length'
如果 API 确认已修复,但 UI 仍显示旧计数,请告知用户这是 UI 延迟,并非回归问题。API 是事实来源。锁文件推送后,UI 通常会在一小时内更新。
平台说明
- Windows + OneDrive:
yarn install/npm install/pnpm install可能会因 OneDrive 在同步过程中短暂锁定文件,而在.node原生二进制文件上遇到EPERM。通常重试一次即可解决。不要终止进程,也不要升级为--force——只需重试一次。