SBOM 有了,還是答不出「誰在用」:四個實測
上一篇講 OpenShift AI 驗收時全綠但四件事是錯的。這篇換個對象:SBOM。
起點是一個很具體的問題:
出了一個 CVE,我要在幾分鐘內回答「本行哪些系統在用這個套件、哪一版、部署到哪」。
有 SBOM 不等於答得出來。我花了兩天把一條完整的鏈接起來—— pipeline 產成品 → 算 digest → SCA 掃描(Mend SaaS)→ 依賴樹入帳 → 用 PURL 反查—— 每一步都跑成功。然後逐條驗,四件事是錯的。其中兩件是我自己算錯的,那兩件反而更值得寫。
一、能寫、能讀,就是不能查
要回答「誰在用」,前提是成品身分(image digest)要能綁在掃描紀錄上。 Mend 支援 project tag,於是我掃描時帶上:
mend dep --update --tags artifactDigest:<sha256>
(順帶一提,分隔符是 : 不是 =,而且 --tags 與 --local 互斥——
要打標籤就必須上傳,純本機掃描拿不到標籤。)
寫入成功。讀回來也成功:
[{"key": "artifactDigest", "value": "50cd47b9…"}]
到這裡我差點就寫「可行」。幸好多做了一步——拿一個不存在的值去查:
| 查詢 | 回傳專案數 |
|---|---|
| 不帶任何參數(基準) | N |
?tag=artifactDigest:<真實值> |
N |
?tag=artifactDigest:ZZZZ_NOT_EXIST |
N |
?tag=@@@@(語法根本是錯的) |
N |
四個一模一樣,連語法錯誤都不報錯。
(測的是 Platform API v2.0 GET /orgs/{org}/projects 的 tag/tags 參數,2026-08 實測。
不排除另有查詢途徑,但至少不是最明顯的那一條。)
這不是「查不到」,是根本沒在查——參數被整個忽略了。 只測真實值那一次,會百分之百誤判成成功,因為它確實回了一堆資料。
教訓:任何「可以用 X 查詢」的宣稱,驗收方式不是「用真的 X 查一次看有沒有結果」, 是用一個保證不存在的 X 查一次,看結果有沒有變。沒變就是假的。
對這個案子的結論很硬:tag 是註記,不是鍵。 digest 寫得進去、看得到,但沒辦法拿它反查,那它就只是個顯示欄位。
二、direct 和 transitive,我用錯規則分了一遍
帳本要分「直接相依」和「被別人帶進來的」,因為兩者的處置完全不同: 直接相依你改得動 manifest,遞移相依你只能等上游或換套件。
我自己寫的判斷規則是:在依賴樹裡曾經當過別人的 parent,就算遞移。 跑出來 direct 28、transitive 23。
同一個專案,Mend API 給的答案是 direct 2、transitive 49。
差了一個數量級。錯的是我:direct 的定義不是「在樹的哪一層」, 是「有沒有寫在 manifest 的頂層宣告裡」。 一個套件可以同時是頂層宣告、又是別人的 parent,我的規則把它判成遞移。
教訓:同一件事有兩個來源時,先對帳再相信自己算的那個。 我如果沒去拉 API 交叉比對,這個數字會一路帶到報表上,而且看起來完全合理。
三、誠實欄位算錯,比沒有誠實欄位更糟
我在全量匯出的 metadata 裡放了幾個「這份資料自己的品質」欄位, 本意是不要讓人以為涵蓋率 100%:
artifacts=36 artifactsWithoutDigest=6 artifactsWithoutDepGraph=28
第一版跑出來是 artifactsWithoutDepGraph=36——36 之 36,全部都沒有依賴圖。
但我明明親手把 80 條依賴邊灌進去過。去看程式碼,錯在我拿成品的 artifact_id
去比對一個裝 PURL 的集合——兩種東西永遠比不中,所以答案恆等於「全部都沒有」。
這個 bug 的性質值得說清楚:它不會讓資料變少,它會讓一個宣稱自己誠實的欄位說謊。 而且方向是「把自己講得更爛」,所以看起來像是保守、不像是錯——最不容易被抓的那種。
教訓:誠實欄位本身也要測。 一個沒被測試過的品質指標,跟沒有品質指標是兩回事——後者只是缺,前者是錯誤資訊。
四、build_ref 是空的,而且空在最該有的地方
帳本每一筆成品都記來源:intake(A=pipeline 自動、B=工具匯入、C=人工填),
以及 build_ref(哪一次 build、哪個 commit)。
我抽查時發現,有幾筆 intake=A 的紀錄,build_ref 是空字串。
intake=A 的意思是「這筆是 pipeline 自動入帳的,信心最高」。
而 build_ref 空,代表這筆最可信的紀錄,追不回是哪次 build、哪個 commit 產生的。
七段稽核鏈(成品 → 品質關卡 → 建置來源 → 原始碼)就斷在這裡。
更麻煩的是它不會報錯。信心等級照樣算成 high,因為我當初的規則是
「intake=A 且有 digest → high」——沒有把 build_ref 算進去。
教訓:信心等級不能是「填進來的」,必須是從實際欄位推導出來的; 而推導規則要涵蓋所有讓它不可信的欄位,漏一個就會出現「高信心的斷鏈」。
那怎麼驗才有用
這四條抽出來是四個可以直接套用的動作:
| 要驗什麼 | 沒用的驗法 | 有用的驗法 |
|---|---|---|
| 「可以用 X 查詢」 | 用真的 X 查一次 | 用不存在的 X 查一次,看結果變不變 |
| 自己算的分類 | 看數字合不合理 | 找第二個來源算同一件事,對帳 |
| 涵蓋率/品質欄位 | 相信它 | 餵一筆已知答案進去,看它報得對不對 |
| 「這筆資料可信」 | 看信心等級 | 看信心是怎麼推導的,推導有沒有漏欄位 |
共同點是一樣的:成功的那次不構成證據,失敗的那次才構成證據。 一個永遠回傳結果的查詢,跟一個真的在過濾的查詢,長得一模一樣—— 除非你去問它一個它應該答不出來的問題。
本文所有數字來自我自己建的示範專案(一個 Vue 前端 + 一條 Azure Pipelines), 在筆電與雲端 CI 上都可重現。表一的 N 是同一個實際數字,與結論無關故略。 工具版本以 2026 年 8 月實測為準,廠商日後修正不在本文追蹤範圍。