MOOSSin
選單

全部文章 · 開源工具

pip-tools、CycloneDX 與 pip-licenses:替一個 Python 專案留下供應鏈紀錄

三個開源工具,一支批次檔,產出帶雜湊的鎖定檔、SBOM 和第三方授權聲明,而且每次執行都跟版本控制裡的比對。平常沒人看這些檔案,某個套件爆出漏洞那天你會想要。先從一份食材成分表講起。

你有沒有仔細看過手搖飲杯子上那張成分標示?大部分人沒有。你買的是「珍珠奶茶」,不是「水、砂糖、樹薯粉、紅茶、奶精、焦糖色素」。但那張標示存在,而且是法律規定要有的,因為總有一天某個成分會出事,那時候需要有人能立刻回答「這杯有沒有用到那個」。

軟體專案也有成分表,只是大多數專案沒有寫。一個 Python 專案的直接相依可能只有八個套件,解出來裝進環境的是三十一個。哪一版、從哪來、什麼授權、有沒有被換過內容,這些問題平常沒有人問,在某個套件爆出漏洞、或者你想把專案交給別人的那天,會一起被問。三個開源工具可以把答案預先寫好:pip-tools、cyclonedx-bom、pip-licenses。

工具住在自己的環境裡

第一條規矩:這三個工具不裝進執行期的 venv。它們有自己的一個小環境,版本釘死,連 pip 的版本也釘。

理由很單純,如果工具跟專案住在一起,工具自己的相依也會被算進專案的成分表。那就像成分標示上出現了「秤重用的秤」。它們住隔壁,需要的時候過來量一下,量完回去。

從一次乾跑開始

pip 22.2 起有 pip install --dry-run --report report.json:不安裝任何東西,只解析出這個平台上會裝哪些套件、哪些版本、每個 wheel 的雜湊,輸出成 JSON。從這份報告產生鎖定檔,每個套件一行版本、一行 --hash=sha256:…。pip-tools 的 pip-compile --generate-hashes 做的是同一件事,差別在它從 requirements.in 解析。

接著建一個用完即丟的驗證環境,用 pip install --require-hashes -r requirements.lock.txt 真的裝一次,再跑 pip check。這一步證明鎖定檔在這個平台上裝得起來,不只是解析出來看起來合理。這跟食譜一樣,寫得再漂亮,沒有真的煮過一次都不算數。

SBOM 和授權清單從同一個環境長出來

cyclonedx-bom 提供 cyclonedx-py environment,對一個 Python 環境產生 CycloneDX 格式的 SBOM,目前的規格版本是 1.7,加上 --output-reproducible 可以讓同樣的環境產生逐位元組相同的檔案。SBOM 是 Software Bill of Materials 的縮寫,翻成中文大概就是「軟體物料清單」,也就是那張成分標示的正式名稱。

pip-licenses 對同一個環境列出每個套件的授權,--with-license-file 連授權原文一起帶出來。兩份東西正規化後進版本控制。

我會再從授權清單產生一份人看的第三方聲明:套件、版本、直接或間接相依、SPDX 授權、審查結果、來源網址。大多數是 MIT、BSD、Apache-2.0,自動通過;MPL-2.0 這種帶條件的授權標成人工審查,理由寫旁邊。第一次跑出這份清單的時候我嚇了一跳,因為裡面有一半的套件我根本不知道自己有裝,它們是某個套件的某個套件的某個套件帶進來的。就像你以為自己只點了一杯奶茶,成分表上卻有七種你沒聽過的東西。

檢查模式和更新模式

批次檔有兩種跑法。預設是檢查:重新產生全部產物,跟版本控制裡的比對,任何差異都讓它失敗。這讓「有人手改了鎖定檔」或「解析結果變了」在提交前就被抓到。加 --update 才是更新:重寫鎖定檔、SBOM 和聲明。兩種模式最後都跑一次專門的回歸測試。

「預設是檢查、要更新得明講」這個設計我很喜歡。它讓「不小心改到」變成不可能,你得很清楚地告訴它「我現在就是要改」。

映像那一端也要求雜湊

正式映像安裝套件時帶 --require-hashes --only-binary=:all:。前者要求鎖定檔裡每個 wheel 的雜湊都對得上,後者禁止現場從原始碼編譯。跨平台要各自一份鎖定檔,Windows 上解出來的 wheel 跟 Linux 上的不同。

雜湊對不上會發生什麼?安裝直接停止。這聽起來很不方便,但想想它在防什麼:有人在你和 PyPI 之間把套件換掉了。這種事真的發生過,不是都市傳說。多一個對不上就停的檢查,比事後在新聞上看到自己用的套件被植入東西好得多。

小劇場:這些東西真的有人看嗎

「你說的這個 SBOM,你自己多久打開一次?」

「幾乎不會。」

「那為什麼要做?」

「因為它的價值不在我打開它的時候,在我需要它的那一天。某個套件爆漏洞,我可以在一分鐘內回答『我有沒有用到、哪一版、直接還是間接』。沒有它,我要花一個下午。」

「所以它像保險。」

「像保險,而且保費是一支批次檔,一次寫好就不用再付。」

「那如果永遠沒有那一天呢?」

「那我就永遠不用打開它,這是最好的結果。」

值得說的缺口

這套流程比對的是套件,不是你自己寫在 SBOM 中繼資料裡的東西。我有一份 SBOM 的專案名稱欄位寫錯過,工具沒抓到,因為它只看套件清單。人寫的欄位還是要人看。成分表可以自動產生,但杯子上的店名還是要有人記得改。

為什麼這幾年大家突然在講 SBOM

成分表這個概念在軟體業存在很久了,但真正變成顯學是 2020 年之後。那年底 SolarWinds 事件爆發,一間公司的更新機制被植入後門,幾千個客戶跟著中招,包括好幾個政府單位。2021 年美國發了一道行政命令,要求賣給聯邦政府的軟體要附 SBOM。從那之後,SBOM 從「有做很好」變成「沒做會被問」。

2024 年的 xz-utils 事件更嚇人。一個幾乎所有 Linux 都會用到的壓縮函式庫,被一個花了兩年時間取得維護者信任的帳號植入後門,差一點就進到主流發行版的正式版本裡。被發現是因為一個工程師覺得 SSH 登入慢了半秒,去查為什麼。半秒。這件事讓所有人意識到,你相依的東西有多少是你根本不認識的人在維護。

SBOM 的兩大格式是 CycloneDX 和 SPDX,前者來自 OWASP,後者來自 Linux 基金會,兩個都是開放標準。這篇用的是 CycloneDX,沒有特別理由,只是 Python 生態的工具支援比較順手。

常見的三個誤會

第一個誤會是「鎖定檔就是 requirements.txt 寫死版本」。寫死版本只保證版本號一樣,不保證內容一樣。同一個版本號的套件可以被重新上傳(PyPI 現在禁止了,但其他來源不一定),只有雜湊能保證你裝的是同一個檔案。

第二個誤會是「SBOM 產一次就好」。每次相依變動都要重產,不然它描述的是一個已經不存在的環境。這是為什麼要放進批次檔、放進檢查流程,讓它跟著程式碼一起變。

第三個誤會是「授權清單是法務的事」。法務看得懂 MIT 和 GPL 的差別,但法務不知道你的專案是不是把套件交付給了使用者,而那件事決定了義務有沒有被觸發。工程師和法務各知道一半,清單是讓兩邊對話的共同語言。

一個週末可以試的小練習

找一個你自己的 Python 專案,只要有 requirements.txt 就行。建一個乾淨的 venv,pip install --dry-run --report report.json -r requirements.txt,打開那個 JSON 看一下。數一數 install 陣列裡有幾個套件,跟你 requirements.txt 裡的行數比一下。

然後在另一個 venv 裡 pip install pip-licenses,對你的專案環境跑 pip-licenses --format=markdown。你會得到一張表,看一下有沒有你沒聽過的授權名稱。大部分人第一次做這件事都會發現至少一個「這是什麼」。那個「這是什麼」就是這整套流程存在的理由。

後記

手搖飲的成分表,我後來真的開始看了。看了之後買的東西沒有變,只是知道自己在喝什麼。軟體的成分表大概也是這樣,它不會讓你的程式變好,只是讓你知道自己在跑什麼。而在一個什麼都能 pip install 的年代,知道自己在跑什麼,已經是一種少見的清醒。

專案頁面:github.com/jazzband/pip-tools、github.com/CycloneDX/cyclonedx-python、github.com/raimon49/pip-licenses

閱讀偏好

返回全部文章作者與內容更正

留言(0)

還沒有留言。讀完有想法,歡迎留下第一則。

留言經站長審核後才會顯示。暱稱和留言會公開,請不要寫下電話、地址等個人資料;資料怎麼處理,請見隱私權政策。