為何是 CI
為什麼供應鏈安全應該在您的 CI 中落實
您出貨的軟體,就是 CI 建置在那個當下根據它所下載的一切產出的結果。這次建置是唯一能讓您看見放入了什麼、決定允許放入什麼,並記錄版本中包含什麼的地方。
更新日期
What software supply chain security means
演講中的實用定義:您的軟體就是您自己的程式碼,加上它所相依的一切;而供應鏈則是將這些組成部分納入版本發布的每一個步驟。
Petteri Pulkkinen和Erika Marttinen 於 Kubernetes Community Days Helsinki 2026,SBOMbastic: Getting Ready for Upcoming EU Cybersecurity Regulation in Software Supply Chains 的 2:35 至 4:28。
您的正式建置並不是筆電上的那一份
多數團隊以為在本機測試過的程式碼就是出貨的程式碼。原始碼相同,但軟體往往不同。一次建置是原始碼,加上它在執行當下下載的一切以及周遭的一切,而其中固定不變的部分少之又少。
未鎖定的相依套件每次解析結果都不同
相依套件使用版本範圍是常態。在 npm 中,^1.0.4 可接受之後任何 1.x 版本,因此同一份 package.json 在不同日子會安裝出不同版本。鎖定檔可以固定版本,但前提是它有被提交,而且建置確實遵循它:npm ci 會在鎖定檔與 package.json 不一致時失敗,而 npm install 則可能悄悄更新鎖定檔。許多專案根本沒有提交鎖定檔,廣泛使用的 Web 框架 Express 5.2.1 就沒有。
即使是鎖定的版本,也只是一個名稱,而不是一個檔案。pip 的文件說明,單靠鎖定版本並不能阻止套件索引為同一版本提供不同的檔案,這正是它提供雜湊檢查模式的原因。
時間會改變輸入
套件登錄庫在每次建置之間都會變動。新版本發布、舊版本被撤回,週一還安全解析的版本範圍,週二就可能抓到遭入侵的版本。容器標籤也會移動:Docker 自己的文件警告「映像檔標籤是可變的,發布者可以更新標籤,讓它指向新的映像檔」,因此除非以 digest 鎖定,否則 FROM node:20 每個月都不是同一個基礎映像檔。
時間也會進入產出之中。Reproducible Builds 專案稱時間戳記是「可重現性問題的最大來源」,因為許多工具都會把當下的日期與時間寫進它們建置的產物裡。
環境不同
- 作業系統與架構。macOS 與 ARM 的筆電,和 Linux 與 x86-64 的 CI 執行器,會為同一個套件安裝不同的預先編譯二進位檔。許多 npm 工具如今以包裝編譯後二進位檔的平台專屬套件形式發布。
- 工具鏈。除非每一個都鎖定版本,否則編譯器、執行環境與套件管理工具的版本會在不同機器之間產生落差。
- 快取。您的機器與您的 CI 會還原不同的快取,它們在不同時間、依不同的解析結果建立。
- 安裝腳本與建置步驟。Post-install hook、設定用的 action 與建置腳本會在建置時下載工具與資料,這些都不會出現在清單檔中。
- 設定。環境變數、建置旗標與機密只存在於 CI 中,其他地方都沒有。
Reproducible Builds 專案對可重現建置的定義是:「給定相同的原始碼、建置環境與建置指示,任何一方都能重新產生所有指定建置產物逐位元完全相同的副本。」這個定義顯示有多少條件必須保持不變。多數團隊的建置做不到這一點,而做得到的團隊也為此付出了大量心力。在您達到這個程度之前,對於出貨內容唯一可靠的說明,就是在那次出貨建置執行期間所記錄下來的內容。
A software supply chain, walked end to end
Petteri Pulkkinen和Erika Marttinen 於 Kubernetes Community Days Helsinki 2026,SBOMbastic: Getting Ready for Upcoming EU Cybersecurity Regulation in Software Supply Chains 的 15:20 至 16:38。
可以在哪裡檢查,以及每個位置看得到什麼
資安檢查可以在四個位置執行。每個位置各有其功能,但只有一個看得到實際產出您版本的那次建置。
| 功能 | 在 CI 建置中 | 開發人員機器 | 儲存庫掃描 | 建置產物掃描 |
|---|---|---|---|---|
| 看得到正式環境實際取得的版本 | 在下載時記錄 | 是另一次安裝 | 僅限鎖定檔有固定版本時 | 僅限它能辨識的部分 |
| 看得到遞移相依套件與從快取還原的套件 | 自己的快取,而非 CI 的快取 | 僅限鎖定檔 | ||
| 看得到安裝腳本下載的內容 | ||||
| 能阻止惡意的對外連線 | 執行器上的對外連線政策 | |||
| 在程式碼推送前發現問題 | 在 pull request 上 |
- 支援
- 部分支援
- 不支援
儲存庫掃描回報的是專案宣告的內容;完成品的建置產物掃描回報的是它能在其中辨識出的內容。兩者都沒有看到建置的過程。Express 5.2.1 沒有提交鎖定檔,因此將掃描器指向乾淨的 checkout 時,沒有任何專案的 npm 相依套件可以解析,只能回報它辨識出的其他內容。將同一個工具指向同一個儲存庫的已安裝副本,只要改變一個 cataloger 旗標,答案又會不同。回傳的結果取決於掃描前安裝了什麼,而這正是掃描無法掌控的部分。建置時 SBOM 頁面說明了掃描結果為何互相矛盾,以及記錄建置能帶來什麼改變。
管線本身就是攻擊目標
CI 是原始碼、相依套件與機密交會之處,因此值得攻擊者下手。
- tj-actions/changed-files(2025 年)。一個廣泛使用的 GitHub Action 遭入侵,並將 CI 機密輸出到建置日誌中。
- Codecov Bash Uploader(2021 年)。一個在客戶 CI 中執行的遭竄改腳本,將環境變數傳送到攻擊者的伺服器。
- Shai-Hulud npm 蠕蟲(2025 年)。遭入侵的套件在安裝時竊取憑證,並利用這些憑證發布更多遭入侵的套件。
- xz utils(2024 年)。後門藏在版本壓縮檔與建置流程本身之中,而不在可見的原始碼裡。
這些攻擊都是在建置或安裝期間執行程式碼。在它們之前執行的儲存庫掃描,或在它們之後執行的建置產物掃描,都看不到建置流程做了什麼。監看並限制作業可連線範圍的管控措施,必須位於作業執行的地方。這對內部與低調的應用程式同樣適用:這類攻擊針對的是管線以及每個應用程式共用的套件,而不是應用程式本身做了什麼。請參閱內部與低風險軟體的供應鏈安全。
程式碼更多、相依套件更多,閱讀它們的人更少
AI 程式設計代理撰寫與修改程式碼的速度,快到團隊來不及審查每一行,而且它們還會順手加入相依套件。它們也會指名不存在的套件:一項 USENIX Security 2025 研究分析了 576,000 個生成的程式碼範例,發現商用模型的幻覺套件比例至少為 5.2%,開源模型則為 21.7%。攻擊者只要註冊其中一個名稱,下一次信任它的建置就會安裝它。
每一項變更,無論由誰或由什麼撰寫,都會在前往正式環境的途中經過建置。這讓建置成為唯一不需仰賴有人看過的檢查點。
佐證資料必須描述實際出貨的內容
法規與客戶要求的是證明,而不是意圖。歐盟 Cyber Resilience Act(CRA)要求製造商識別並記錄其產品中的元件,並在產品的支援期間處理其中的漏洞。從清單檔產生的 SBOM 描述的是專案要求了什麼;從建置記錄的 SBOM 描述的是您必須負責的版本中實際包含了什麼。當新的漏洞公布時,後者能告訴您哪些版本受到影響。請閱讀 CRA 的要求。
「在 CI 中」應該代表什麼
把掃描器當作 CI 步驟執行是個開始,但它看的仍是檔案送達之後的狀態。真正做到「在 CI 中」,代表:
- 記錄,而非推斷。擷取作業實際下載的每個套件與每項下載內容,包括遞移相依套件與從快取還原的任何內容。
- 說明紀錄的完整程度。SBOM 應說明它是否涵蓋作業下載的一切,而不只是列出它找到的內容。
- 管控對外連線。決定作業可以連線到哪些主機,並在遭入侵的相依套件回傳資料之前阻擋其餘連線。
- 發布後持續監看。隨著新漏洞出現重新檢查已出貨的內容,並將發現轉化為修正。
這正是 CRACI 所做的事。它是您 GitHub Actions 作業的執行器,因此會記錄每次建置引入的內容、套用對外連線政策,並監控您已出貨的內容。搬移一個工作流程只需要一行:runs-on: craci。請參閱 CRACI 如何運作。
CI 無法取代的部分
將供應鏈安全放進建置,並不代表其他防護層可以省略。
- 開發人員機器仍然保存憑證並執行安裝腳本。也要保護它們;遭入侵的筆電可以在任何建置執行之前就推送程式碼。
- 程式碼審查與靜態分析能找出您自己程式碼中的缺陷,這是相依套件紀錄做不到的。
- 鎖定版本、鎖定檔與 digest 仍然是良好實務。它們能縮小建置之間的差距;而記錄建置則無論如何都能告訴您實際發生了什麼。
建置並不是唯一需要檢查的地方,卻是唯一看得到您實際出貨軟體的地方。