mcpField
D
dependabot-triage

dependabot-triage

github.com/akshayrao14安全041 次安装6 周前更新
代码仓库

关于 dependabot-triage

面向 JavaScript/TypeScript 仓库的 Dependabot 告警分诊与修复技能,覆盖 npm、pnpm、yarn 和 bun。它结合暴露面评估、风险分级与锁文件版本校验,并在更改和创建 PR 前设置确认步骤。

主要功能

  1. 01标准与 Fast-Track 分诊模式
  2. 02检查本地及 CI 包管理器
  3. 03按影响、暴露面和 CVSS 排序
  4. 04分析导入位置与变更日志
  5. 05跨包管理器锁文件版本校验
  6. 06确认后创建修复 PR

适用场景

  1. 01确定多个 Dependabot 告警的处理优先级
  2. 02修复 JavaScript 或 TypeScript 依赖漏洞
  3. 03核对 CI 与本地包管理器版本一致性

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/akshayrao14/git-practices --skill dependabot-triage

安装后在 Claude Code 中输入/dependabot-triage确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

install.sh
LICENSE
README.md
SKILL.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> 中的所有仓库进行分流”
  • “运行组织范围的分流”

前置条件

  • 已通过身份验证且拥有仓库访问权限的 gh CLI(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)完全相同,且不可跳过。

  1. 检测正在使用的包管理器 — 获取警报前,先查明在所有场景(本地安装、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——请确认这是有意为之?”)。
  2. 获取警报:
    • 仓库 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 警报。
  3. 根据优先级框架进行排序(影响 × 暴露程度 × CVSS)。
  4. 阅读安全公告,确定首选项——提取 first_patched_version、vulnerable_version_range 和 cvss.score。对于漏洞簇,计算能够涵盖所有 CVE 修复版本的最小版本。
  5. 选择模式——建议快速通道或标准模式。 资格规则见“快速通道模式”部分。在排序摘要旁给出一句建议,例如:“这是一个开发依赖;要通过快速通道处理此修复吗?” 如果用户没有回应或意图不明确,默认使用标准模式。若 CI 使用的包管理器没有对应的已提交锁文件,也默认使用标准模式——每次 CI 构建都会重新解析传递依赖,覆盖字段是唯一的固定版本方式,而且这种情况不符合快速通道的条件(无法检查锁文件一致性)。
标准模式(步骤 6–8)— 完整防御流程
  1. 暴露面映射 — 在源代码中定位目标包的每个导入位置,并将其分类为 Public/API · Client-Bundle · Internal/Dev(参见“暴露面映射”部分)。输出数量和示例路径。
  2. 抓取变更日志 — 获取当前安装版本到目标版本之间的发行说明 / CHANGELOG.md。搜索 BREAKING、DEPRECATED、MIGRATION、removed、dropped support。逐字显示标记出的行。
  3. 安全联锁 — 暂停。 向用户显示暴露面摘要、变更日志标记和选定的目标版本。在进行任何文件编辑之前,等待用户明确确认(“yes”“go”“looks fine”等)。如果标记出 BREAKING / DEPRECATED / MIGRATION,则需要用户再次确认。
快速通道模式(步骤 6–8)— 低风险捷径
  1. 轻量暴露面检查 — 仅运行一次导入位置搜索,以确认主要暴露面类别,但不要枚举路径。输出:Exposure: Internal/Dev (8 sites)。到此为止;不按类别细分,也不列出路径。
  2. 跳过变更日志抓取。(快速通道明确以牺牲变更日志可见性换取速度;下方的资格规则会限制可能造成的影响范围。)
  3. 单次确认联锁 — 暂停。 输出:目标版本 + 主要暴露面类别 + CVSS + 快速通道资格原因。用户确认一次(“yes”“go”“ship it”)即可继续;任何含糊不清的回复都会退回标准模式。
应用(步骤 9–13)— 两种模式完全相同,不可协商
  1. 确认分支策略和基分支。 使用 gh repo view --json defaultBranchRef 检测默认分支;绝不硬编码 main。默认策略:从最新的 origin/<detected-base> 为每个包创建一个 PR。
  2. 编辑 package.json — 为步骤 1 中检测到的每个 PM 写入 override 字段(npm overrides、pnpm pnpm.overrides、yarn resolutions、bun overrides;参见“Override pattern”)。两种模式都必须执行。
  3. 重新生成每个相关锁文件 — 对步骤 1 中检测到的每个 PM 运行其安装命令。对于没有已提交锁文件的 PM,生成临时锁文件用于一致性检查,并在提交前删除它(参见“验证(一致性检查)”)。不存在“主要”锁文件。
  4. 一致性检查 — 对步骤 1 中检测到的每个 PM,读取其锁文件(或 <pm> why 输出),并断言每个 PM 解析到的目标包版本都相同。版本不一致 → 两种模式下都中止 PR。 后续操作参见“版本不一致 / 构建失败时自动还原”。
  5. 在分支上提交并创建 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>。要我在标准模式下重新运行吗 — 拉取变更日志并进行完整的暴露映射,以便查看实际变更内容?”

同一个包失败后,不要悄悄重试快速通道。应升级到标准模式或停止并询问。对失败的升级反复重试快速通道,正是此模式旨在避免的陷阱。

优先级框架

按以下三个维度排序:

  1. 影响 — RCE > 数据窃取/SSRF > DoS > 逻辑错误
  2. 暴露面 — 见下文的暴露面映射。这不是对漏洞是否可触达的启发式猜测,而是对软件包被导入位置的分类,并向用户呈现为攻击面摘要。
  3. 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 合并后:

  1. git fetch origin <base>
  2. git checkout <next-branch> && git rebase origin/<base>
  3. 解决冲突时,保留所有现有的 override 键并添加新的键。
  4. 使用 PM 的仅更新锁文件安装命令重新生成锁文件(参见此技能中的 Override pattern 部分)。
  5. git add <manifest> <lockfile> && git rebase --continue
  6. git push --force-with-lease origin <next-branch>

不要使用 --force(不带 lease)——它会跳过安全检查,而该检查会在远程分支的进度超出你已获取的内容时中止操作。

每条告警的处理流程

假设已完成“检测正在使用的包管理器”(工作流步骤 1)——下面的覆盖配置 + 一致性矩阵依赖于了解哪些包管理器会操作此代码库。

  1. 去重:GitHub 会为每个已提交的锁文件分别生成一条告警。两个已提交的锁文件(例如 package-lock.json + pnpm-lock.yaml)= 2 条告警、1 个漏洞。只需修复一次该包。

  2. 获取安全公告:gh api repos/<org>/<repo>/dependabot/alerts/<n> — 提取 first_patched_version 和 vulnerable_version_range。列表端点不会显示这些信息。对于告警簇,请获取簇中的每条告警。

  3. 定位:直接依赖(在 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);"
    
  4. 暴露面映射:参见“暴露面映射”一节。对每个导入位置进行分类(Public/API · Client-Bundle · Internal/Dev),并为用户汇总暴露面。

  5. 选择版本(防御性最小修复版本):

    • 单条告警:选择 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。
    • 除非用户批准升级主版本,否则限制在当前安装版本的同一主版本内。
  6. 抓取变更日志 — 获取当前版本到目标版本之间的发布说明。优先尝试 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,则将安全确认提升为“需要二次确认”,再继续操作。

    **传递依赖限定说明:**要求二次确认前,请检查标记出的包在源代码中是否有直接导入位置(参见暴露面映射步骤)。如果没有直接导入,该包就是纯传递依赖——请在标记旁明确说明:“此标记出现在一个不会被源代码直接导入的包中。除非您的代码会解析其错误消息或接入其内部实现,否则此破坏性变更不太可能影响您。” 如果该标记显然与调用代码无关,这样就可以将两轮确认合并为一轮。

  7. **安全确认 — 暂停。**向用户展示:

    • 选定的目标版本 + 原因(防御性最低版本还是最新版本,以及原因)
    • 暴露面(每个类别的数量、示例路径)
    • 变更日志标记(逐字引用的行;若无则写“无标记”)

    等待用户明确同意。如果出现变更日志标记,必须得到明确无误的“是,继续” / “我已处理”——未回复或含糊回复都意味着停止。

  8. 用户同意后:编辑 package.json、重新生成两个锁文件、运行 Parity Check、提交,并在推送前征求同意。

覆盖配置模式(Node.js)

以下情况使用覆盖配置:

  • 传递依赖漏洞 — 此包不在 dependencies / devDependencies 中。若不升级父依赖,覆盖配置是唯一的修复方式。
  • 直接依赖且传递依赖也要求使用此包 — 升级 dependencies.<pkg> 并不能保证传递依赖使用同一版本(不同的 semver 范围可能会分别提升安装)。仍需添加覆盖配置作为纵深防御。
双重写入是强制要求

在任何情境(本地安装、CI 安装、容器构建)下,都要为步骤 1 中检测到的每个 PM写入覆盖字段。不同 PM 使用不同的覆盖字段:

PMpackage.json 中的覆盖字段
npmoverrides
pnpmpnpm.overrides
yarn classic (v1)resolutions
yarn berry (v2+)resolutions
bunoverrides

当涉及多个 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 中的覆盖字段重新生成命令
npmpackage-lock.json"overrides": { ... }npm install --package-lock-only
pnpmpnpm-lock.yaml"pnpm": { "overrides": { ... } }pnpm install --no-frozen-lockfile
yarn (v1)yarn.lock"resolutions": { ... }yarn install
yarn (berry, v2+)yarn.lock"resolutions": { ... }yarn install
bunbun.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 一起部署服务器运行时。同一个依赖可能同时属于多个暴露类别。

前端仓库中涉及三个类别:

  1. 公共/API — 处理外部 HTTP/RPC 流量的服务器运行时。例如:Next.js app/**/route.{ts,js}、pages/api/**、middleware.{ts,js}、Remix loader/action、Nuxt 服务器路由、SvelteKit +page.server.{ts,js} 和 +server.{ts,js}。与后端公共/API 导入完全同样处理。
  2. 客户端 Bundle — 会包含在浏览器 bundle 中的代码。例如:src/components/**、不带 .server. 后缀的 src/app/**/{page,layout,loading,error}.{ts,tsx}、src/pages/**(Next.js 经典默认导出)、Vue/Svelte 组件文件,以及由 <script> 加载的入口所导入的任何代码。
  3. 内部/开发 — 配置、构建、脚本、测试。例如: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),则仅为一致性检查生成临时锁文件,然后在提交前将其删除:

包管理器仅重新生成锁文件生成的文件
npmnpm install --package-lock-onlypackage-lock.json
pnpmpnpm install --lockfile-onlypnpm-lock.yaml
yarn classicyarn install --mode=update-lockfileyarn.lock
yarn berryyarn install --mode=update-lockfileyarn.lock
bunbun install --frozen-lockfile=falsebun.lock

⚠ 切勿提交来自尚未有已提交锁文件的包管理器所生成的锁文件。双锁文件漂移正是这项技能要防止的错误类型——在权威锁文件 pnpm-lock.yaml 旁生成 yarn.lock,会在下次运行 yarn install 时悄然产生偏差;你扩大了影响范围,而不是将其减半。

如果检测到的 CI 包管理器没有已提交的锁文件,还要明确断言:

  • package.json 中存在该包管理器对应的覆盖字段(参见“覆盖模式 → 双写是强制要求”中的包管理器表)。
  • 目标包的直接依赖范围与覆盖项一致(避免 EOVERRIDE;参见“npm EOVERRIDE”)。
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 阻止了覆盖规则,可能没有提示,也可能报错——检查直接依赖的版本范围(参见“npm EOVERRIDE”)。
  • 对等依赖不匹配导致传递依赖解析结果不同——强制覆盖前先调查。

开发环境与 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——只需重试一次。

评论

支持 Markdown