yarn audit实战指南教你高效修复依赖漏洞

Good“雨童 安全 阅读 2,687
赞 20 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

上周项目上线前例行做一次依赖安全扫描,顺手跑了下 yarn audit,结果我直接懵了。这命令跑起来跟死机一样,终端卡住五六秒没反应,然后一堆警告刷屏,最后等它结束——整整 42 秒。你没看错,42 秒就为了扫个依赖漏洞。

yarn audit实战指南教你高效修复依赖漏洞

更离谱的是,在 CI/CD 流水线里跑的时候,因为超时设置是 30 秒,直接挂了。我当时第一反应是:不至于吧,一个静态分析能这么耗性能?但事实摆在眼前,本地都卡成这样,CI 更扛不住。

最要命的是,我们项目也没多大,yarn.lock 才 2800 行,算不上巨型项目。我一开始以为是网络问题,怀疑它在后台疯狂请求 npm registry 或安全数据库。后来发现不是,是它自己内部处理逻辑太重,解析、比对、树重建全堆一块儿,CPU 直接拉满。

找到病根了!

我先用 time 命令测了真实耗时:

time yarn audit --json > audit.log

输出结果:

yarn audit --json > audit.log  8.32s user 3.11s system 115% cpu 9.937s total

等等,这和我之前感受的 40 多秒对不上?意识到一个问题:我平时用的是全局安装的 yarn,而项目里其实有本地 node_modules/.bin/yarn。赶紧切到本地执行:

time ./node_modules/.bin/yarn audit --json > audit.log

这次结果吓人:**user 32.6s, total 超过 38s**。终于对上了。

接着我用 --verbose 看日志,发现大量时间花在“Resolving packages”阶段,尤其是重复解析相同版本包的不同路径。比如一个包被七八个不同路径引入,yarn audit 居然挨个去 resolve,而不是缓存中间结果。

我又试了 yarn why lodash,发现整个 dependency tree 构建本身就慢。结合之前经验,基本判断问题是出在:yarn v1(也就是 Classic)的依赖解析机制太笨重,特别是在扁平化不彻底或存在大量 peer conflict 的时候。

试了几种方案

第一反应是升级 yarn v2+ 或 pnpm。但项目依赖太多,升级成本太高,光兼容性问题就能折腾一周。pass。

第二想法是加缓存。查了一圈,yarn audit 本身不支持缓存输出,而且每次都是实时解析 yarn.locknode_modules 结构。不过我发现个关键点:如果我们能在 CI 中复用已有的 node_modules,是不是能省掉一部分重建开销?

于是我在 CI 配置里加了缓存策略:

- name: Cache node_modules
  uses: actions/cache@v3
  with:
    path: node_modules
    key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
    restore-keys: |
      ${{ runner.os }}-yarn-

这招有点用,节省了安装时间,但 audit 本身还是慢。说明瓶颈不在 install,而在 audit 的分析过程。

第三种方案:能不能不让它扫全量?查文档发现 audit 支持 --level 参数:

yarn audit --level low    # 只扫 high 以上

我改成 --level high,结果耗时只降了不到 5 秒,意义不大。因为不管级别如何,它都得先把整个依赖图建出来,过滤只是最后一步。

核心突破:预生成 + 异步分流

最后我想到一个取巧办法:既然 audit 必须分析完整依赖结构,那能不能把这个过程拆出去,不放在主构建流程里?

做法是:把 yarn audit 改成异步任务,只在 nightly build 或 PR review 后触发,而不是每个 commit 都跑。同时生成一份 JSON 报告存档,前端展示页面可以读这个报告,而不是实时跑命令。

具体实现是在 GitHub Actions 中新增一个定时 workflow:

name: Security Audit (Nightly)
on:
  schedule:
    - cron: '0 2 * * *'  # 每天凌晨两点
  workflow_dispatch:     # 也支持手动触发

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: yarn

      - name: Cache node_modules
        uses: actions/cache@v3
        with:
          path: node_modules
          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}

      - run: yarn install --frozen-lockfile

      - name: Run yarn audit and save report
        run: |
          yarn audit --json > audit-report.json || true
          # 提取关键信息汇总
          echo "::group::Audit Summary"
          cat audit-report.json | grep '"type": "auditAdvisory"' | wc -l
          echo "::endgroup::"

      - name: Upload report as artifact
        uses: actions/upload-artifact@v3
        with:
          name: security-audit
          path: audit-report.json

这样一来,主 CI 不再阻塞于 audit,而是通过另一个通道获取安全状态。我们在内部 dashboard 上读取这份 report 并展示高危数量,开发同学也能随时下载查看。

同时,我还写了个轻量脚本,只检查关键依赖是否有已知严重漏洞,用于主流程快速校验:

// quick-audit.js
const fs = require('fs');
const { execSync } = require('child_process');

const CRITICAL_DEPS = ['lodash', 'axios', 'debug', 'handlebars', 'jquery'];

try {
  const result = execSync('yarn list --json', { encoding: 'utf-8' });
  const lines = result.split('n').filter(Boolean);
  const data = JSON.parse(lines[lines.length - 1]);

  if (!data?.data?.trees) process.exit(0);

  const found = data.data.trees.filter(t =>
    CRITICAL_DEPS.includes(t.name.replace(/@.*$/, ''))
  );

  const hasVulnVersion = found.some(pkg => {
    const version = pkg.name.match(/@(.+)$/)?.[1];
    // 这里简单判断版本号是否低于某个阈值(示例)
    return pkg.name.includes('lodash@') && version && /^1.(1[5-9]|[0-9])./.test(version);
  });

  if (hasVulnVersion) {
    console.error('Found potentially vulnerable core packages');
    process.exit(1);
  }
} catch (err) {
  console.error('Quick audit failed:', err.message);
  process.exit(0); // 不阻断构建
}

这个脚本跑一遍只要 1.2 秒左右,作为主流程的“轻量哨兵”完全够用。

优化后:流畅多了

现在我们的 CI 主流程从原来平均 3 分钟(含 audit)降到 1 分 10 秒,失败率归零。nightly audit 报告每天准时生成,有问题会发 Slack 告警。

更重要的是,开发体验好了太多。以前提个 PR 动不动卡在 audit 那儿,现在提交完立马看到结果,不用干等。

性能数据对比

  • 优化前:每次 CI 执行 yarn audit,平均耗时 38~42 秒,CPU 占用峰值 95%
  • 优化后
    • 主流程使用轻量检查脚本,平均耗时 1.3 秒
    • 完整 audit 移入 nightly job,平均耗时 40 秒但不影响主流程
    • 整体 CI 成功率从 82% 提升至 99.6%

这里注意我踩过好几次坑:一开始想用 yarn audit --groups dependencies 来缩小范围,结果发现还是会扫描所有子依赖;还试过用 Docker 缓存 node_modules,但体积太大反而拖慢了 pull 阶段。最后发现最简单的分流策略才是最有效的。

这个方案不是最优的,但最简单,也最容易维护。毕竟我们不是安全团队,没必要为一个辅助功能投入太多工程成本。

以上是我的优化经验,有更优的实现方式欢迎评论区交流

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论