DependabotnpmGitHub供應鏈安全自動化開源

49 個 repo 的 Dependabot 警報一晚盤完:盤點、批次修、驗證的流程

· · 18 分鐘閱讀
目錄
  1. 盤點時看到什麼?
  2. 先修哪些?
  3. 每個 repo 怎麼修?
  4. npm audit fix 修不掉的怎麼辦?
  5. 怎麼驗證修完沒弄壞?
  6. 合併之後還要看什麼?
  7. 為什麼總數幾乎沒降?

一個帳號底下 49 個 repo 的 Dependabot 警報,我們一晚盤完,流程是:用 gh api 把每個 repo 的 open alert 抓下來;回 403 的代表掃描沒開,先打開;按嚴重度和套件分組,這一輪只修 critical 和 high;每個 repo 用乾淨 clone 開分支,修前先跑一次 build 和測試留底;npm audit fix 不加 --force,npm 要你降版的改用 overrides;修後逐支對照測試、實際載入有動到的原生套件、用瀏覽器確認頁面真的有畫出來;合併後等 5 分鐘再抓一次警報。

結果先講:5 個公開 repo 合併後,GitHub 上的 critical 和 high 都歸零;另外 4 個動手的 repo 用本機 npm audit 看,3 個當晚歸零,1 個剩 6 條 high,隔天另開一輪清掉。全帳號總數則只從 1,230 降到 1,205,原因在最後一段。

盤點時看到什麼?

11 月 2 日晚上盤的是一個 GitHub 帳號底下 49 個非 fork 的 repo,open alert 一共 1,230 條:critical 29、high 534。

第一個意外是 403。15 個私有 repo 的 Dependabot alerts 根本沒開,API 直接回 403。我們用一行 PUT 打開,回 204 就是開了。封存(archived)的 repo GitHub 不掃,跳過。

第二個意外是「剛開掃描的 repo 顯示 0」不可信。有一個 repo 的相依清單(SBOM)有 1,552 個套件,開了約兩小時後仍然是 0;另外三個晚一點掃完,一口氣補進 178 條,其中一個從原本只顯示 high 15 條補到 critical 3、high 63,另一個從 0 變成 critical 1、high 27。工具回 0 不代表乾淨,這跟我們之前寫的工具靜默回空是同一種坑。

第三個意外是分支。Dependabot 只掃預設分支。我們有一個 repo 的主開發線不是預設分支,預設分支停在 6 月,所以 GitHub 上那 170 條 critical/high 反映的是 6 月的程式碼。這種 repo 要在實際上線的分支自己跑 npm audit --json 才準。

盤點腳本(Git Bash,需要 gh 已登入):

OWNER=your-account
mkdir -p raw
gh repo list "$OWNER" --source --no-archived --limit 200 --json name --jq '.[].name' > repos.txt
while read -r r; do
  if ! gh api "repos/$OWNER/$r/dependabot/alerts?state=open&per_page=100" \
       --paginate --slurp > "raw/$r.json" 2> "raw/$r.err"; then
    echo "$r: $(head -c 100 "raw/$r.err")"      # 403 多半是掃描沒開
    rm -f "raw/$r.json"
  fi
done < repos.txt

# 打開某個 repo 的掃描(回 204 就是開了)
# gh api -X PUT "repos/$OWNER/<repo>/vulnerability-alerts"

--paginate 一定要配 --slurp,不然多頁的陣列會直接黏成 ][,JSON 解析不了。另外 --slurp 不能跟 --jq 一起用,所以彙總另外寫一支 node(Node 21 以上才有 Object.groupBy):

// summarize.mjs:每個 repo 各嚴重度幾條,再列 critical/high 明細
import fs from 'node:fs'
const order = ['critical', 'high', 'medium', 'low']
const rows = []
for (const f of fs.readdirSync('raw').filter(f => f.endsWith('.json'))) {
  const pages = JSON.parse(fs.readFileSync(`raw/${f}`, 'utf8') || '[]')
  for (const a of pages.flat()) rows.push({
    repo: f.slice(0, -5),
    sev: a.security_vulnerability.severity,
    pkg: a.dependency.package.name,
    path: a.dependency.manifest_path,
    dep: `${a.dependency.relationship}/${a.dependency.scope}`,
    fix: a.security_vulnerability.first_patched_version?.identifier ?? '-',
  })
}
for (const [repo, rs] of Object.entries(Object.groupBy(rows, r => r.repo))) {
  console.log(repo, order.map(s => `${s}=${rs.filter(r => r.sev === s).length}`).join(' '))
}
for (const r of rows.filter(r => r.sev === 'critical' || r.sev === 'high')) {
  console.log([r.repo, r.sev, r.pkg, r.path, r.dep, `>=${r.fix}`].join('\t'))
}

明細裡最有用的兩欄是 relationship(direct 或 transitive)和 scope(runtime 或 development)。直接依賴的 runtime 漏洞先修;只在開發工具鏈裡的,排後面。

先修哪些?

這一輪只修 critical 和 high。medium 和 low 大多卡在大版本升級(例如 firebase-admin、eslint 的整條鏈),一晚做不完,硬做只會把驗證做草。

排序之後會發現很多 repo 卡的是同一批套件:undici 和 path-to-regexp(被部署工具的套件帶進來)、@grpc/grpc-js、protobufjs、tar、websocket-driver。同一個套件在不同 repo 的修法幾乎一樣,第一個 repo 驗過的做法,後面直接套。

這一晚動手的是 9 個:3 個主力 repo、5 個公開的小工具、1 個在非預設分支上線的產品 repo。其他有 critical/high 的 repo 列在清單上排隊,沒有假裝修完。

每個 repo 怎麼修?

每個 repo 都跑同一支腳本,重點是修之前先留底:

#!/usr/bin/env bash
# fixrepo.sh <repo>:乾淨 clone、留底、audit fix、再跑一次
r=$1; OWNER=your-account
W=/c/work/wt-dep-$r; L=/c/work/dep-logs; mkdir -p "$L"
rm -rf "$W" && gh repo clone "$OWNER/$r" "$W" -- -q || exit 1
cd "$W" && git checkout -q -b "fix/deps-$(date +%F)"

npm ci --no-audit --no-fund > "$L/$r-ci.log" 2>&1
npm run build --if-present > "$L/$r-build-before.log" 2>&1; echo "build before rc=$?"
npm test --if-present      > "$L/$r-test-before.log"  2>&1; echo "test before rc=$?"
git checkout -q -- .        # build 可能改到被追蹤的檔,還原再修

npm audit fix --no-fund > "$L/$r-fix.log" 2>&1
npm run build --if-present > "$L/$r-build-after.log" 2>&1; echo "build after rc=$?"
npm test --if-present      > "$L/$r-test-after.log"  2>&1; echo "test after rc=$?"
npm audit --json > "$L/$r-audit.json"
git status --short

為什麼一定要留底?因為「修完測試有紅」不代表是你弄壞的。這一晚我們碰到三次:一個 repo 的主分支在修之前就有 1 個測試失敗;一個 repo 的 eslint 修前修後都是同樣 5 個錯;那個產品 repo 有 14 支測試修前就失敗(多半要模擬器才能跑)。有修前的 log 對照,這些都能一眼排除,不會誤判,也不會順手去「修」不相干的東西。

npm audit fix 修不掉的怎麼辦?

先講結論:不加 --force。npm audit 給的建議有時候是降版,我們這一晚就看到它建議把部署工具的套件降回舊的大版本、把 PWA 外掛降版,只為了換掉底下一個子套件。

改用 overrides,只把有漏洞的子套件指到修補版:

"overrides": {
  "@vercel/node": {
    "undici": "^6.29.1",
    "path-to-regexp": "6.3.0"
  },
  "@grpc/grpc-js": "^1.14.7"
}

改 overrides 之前,先把 lock 裡那幾個舊套件的項目刪掉再 npm install,然後 npm ls 看有沒有 invalid。

真的需要升大版本的,先看相依套件接受的範圍。例如一個 MCP server 要把 zod 從 3 升到 4,我們先確認 MCP SDK 接受 ^3.25 || ^4,而且原始碼沒有直接 import zod,才升。另外遇到一個 npm 本身的坑:npm 10.9.4 裝 vitest 4 時會崩,錯誤是 Cannot read properties of null (reading 'edgesOut'),改用 npx -y npm@11 install 就好。

也有刻意不做的。一個 repo 剩 6 條 high 全在 puppeteer 24 這條開發依賴上,要升 25 才清得掉,而 25 是大版本、需要 Node 22.12 以上,會牽動 20 多支截圖和渲染腳本。全在開發依賴、不進線上 bundle,這一晚沒動。隔天另開一輪升級,實測 puppeteer 能正常啟動、渲染頁面,6 條 high 才清掉。

怎麼驗證修完沒弄壞?

build 過不算數。我們用四層:

  1. 測試逐支對照。不是只看總數,而是修前修後每支測試的結果比一次。產品 repo 那 60 支測試,修前修後結果完全一樣。
  2. 實際載入有動到的原生套件。sharp 從 0.34 升到 0.35,就真的拿它做一次 PNG 轉 JPEG、裁切、放大,再用它渲染一張分享圖,看得到輸出才算。
  3. 用瀏覽器確認頁面真的畫出來。HTTP 200 不代表 app 有掛載,前端套件升版最常見的壞法是白頁。
  4. CI 看 conclusion,不看 status。status=completed 只代表跑完了,conclusion=success 才是過了。

第 3 層的最小檢查:

// mount-check.mjs:HTTP 200 不算數,#root 裡真的有東西才算
import puppeteer from 'puppeteer'
const url = process.argv[2]
const browser = await puppeteer.launch()
const page = await browser.newPage()
const errors = []
page.on('pageerror', e => errors.push(e.message))
await page.goto(url, { waitUntil: 'networkidle0', timeout: 60000 })
const kids = await page.$eval('#root', el => el.children.length).catch(() => 0)
await browser.close()
console.log(kids > 0 ? `MOUNTED (${kids} children)` : 'EMPTY', errors.length ? `pageerror x${errors.length}` : '')
process.exit(kids > 0 ? 0 : 1)

MCP server 沒有頁面可看,就用 stdio 跑一次 initialize 和 tools/list,工具清單一個不少才算。

合併之後還要看什麼?

先看合併會不會直接上線。5 個公開 repo 裡有 1 個接了 Vercel 的 git 整合,推上預設分支就是正式部署。合併前我們先確認 fix 分支的 preview 建置成功,合併後再確認正式網址指到新的部署、頁面正常畫出來。

然後等。我們在合併後 4 分 43 秒和 5 分 15 秒各抓一次,結果一樣(更早沒測過,所以抓 5 分鐘當保守的等待時間):5 個公開 repo 從 205 條降到 2 條,剩 1 條 low、1 條 medium,都是開發依賴,要等上游升版。合併後 Dependabot 還自己開了一張 PR 要升剩下那個套件。

另外,本機 npm audit 和 GitHub 的數字會對不上:同一個 repo,本機報 moderate 12 條,GitHub 只算 1 條。兩邊比對的資料庫和方式不同,以 GitHub 為準、本機當參考。

為什麼總數幾乎沒降?

因為同一晚有兩件事在抵銷。動手的 repo 關掉 200 多條,但剛開掃描的三個 repo 補進 178 條。還有三個主力 repo 的修補當晚還在分支上或還沒推,要等合併進 GitHub 掃的預設分支、重掃之後才會反映出來。

所以我們最後的報告是一個 repo 一行:修前、修後、還剩什麼、為什麼剩。全帳號總數只拿來看趨勢,不拿來判斷做完沒有。

這一輪的批次修,也是好幾個 agent 同時跑的。同時跑幾個才不會把電腦塞死,我們寫在另一篇。


修依賴是補供應鏈這一端的洞;另一端是 AI 應用自己的系統提示詞防不防得住注入。UltraProbe 是我們開源的掃描器,貼上提示詞就能看缺哪幾項防禦。

常見問題

用 API 抓 Dependabot 警報回 403,是權限不夠嗎?

多半不是。私有 repo 的 Dependabot alerts 預設可能是關的,這時候 GET /repos/{owner}/{repo}/dependabot/alerts 會回 403。用 gh api -X PUT repos/{owner}/{repo}/vulnerability-alerts 打開,回 204 就是開了。剛開的 repo 可能好幾個小時都顯示 0 條,那是還沒掃完,不是乾淨。

npm audit fix 建議降版或要加 --force,要照做嗎?

不要。--force 會照它的建議直接換大版本,有時候還是降版,例如把框架降回舊版只為了換掉底下一個子套件。比較安全的做法是在 package.json 用 overrides 只把有漏洞的子套件指到修補版,改完跑 npm ls 確認沒有 invalid,再跑建置和測試。

修完合併了,GitHub 上的警報為什麼沒有關?

常見三個原因:一、Dependabot 只掃預設分支,修在別的分支不算;二、合併後要等它重掃,我們在合併後約 5 分鐘抓了兩次,結果一致,更早沒測過,所以以 5 分鐘當保守的等待時間;三、你看的是全帳號總數,同一時間其他剛開掃描的 repo 可能正在補進新的警報。要一個 repo 一個 repo看。

每週 AI 自動化實戰筆記

不廢話,只有能直接用的東西。Prompt 模板、自動化 SOP、技術拆解。

加入一人公司實驗室

免費資源包、每日建造日誌、可以對話的 AI Agent。一群用 AI 武裝自己的獨立開發者社群。

想自己動手試試?

UltraProbe 免費、免註冊,掃一次網站就知道 Google 與 AI 找不找得到你。