yarn audit实战指南教你高效修复依赖漏洞
优化前:卡得不行
上周项目上线前例行做一次依赖安全扫描,顺手跑了下 yarn audit,结果我直接懵了。这命令跑起来跟死机一样,终端卡住五六秒没反应,然后一堆警告刷屏,最后等它结束——整整 42 秒。你没看错,42 秒就为了扫个依赖漏洞。
更离谱的是,在 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.lock 和 node_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 阶段。最后发现最简单的分流策略才是最有效的。
这个方案不是最优的,但最简单,也最容易维护。毕竟我们不是安全团队,没必要为一个辅助功能投入太多工程成本。

暂无评论