進行 .NET NuGet 套件稽核時,套件數量不是越少越好,也不是看到弱點警告就盲目升到最新版。真正要控制的是可解釋的相依圖、可驗證的弱點處置,以及移除後仍成立的建置與執行契約。一個專案可能沒有直接呼叫某個套件,卻透過上層套件需要它;也可能直接宣告了已由框架或其他套件提供的元件。若只搜尋 using 就刪除,會漏掉反射、設定繫結與執行期載入;若完全不整理,又會擴大更新面、部署輸出與稽核噪音。
本文把工作拆成「盤點、追因、處置、驗證」四層。範例使用隔離的 .NET 10 測試專案,不連接真實帳號、客戶資料或正式環境。重點不是追求漂亮的套件數,而是讓每一個直接參考都有理由、每一個傳遞參考都有來源,且安全更新或移除都能由固定指令重現。
NuGet 套件稽核與瘦身的直接答案
.NET NuGet 套件稽核的安全做法是先鎖定 SDK 與還原來源,列出直接及傳遞相依,再用 dotnet nuget why 追出引入路徑;弱點應依公告與實際使用面處置,移除則必須通過還原、建置、測試及發佈輸出檢查。
「套件瘦身」不等於刪除所有未出現在原始碼文字中的 PackageReference。直接套件可能只在 MSBuild、來源產生器、設定檔或執行期使用;傳遞套件則通常不該被誤認為可自由刪除的直接宣告。第一個決策問題應是:這個套件為何存在? 第二個問題才是:哪一層應負責它的版本?
建議把證據分成四份。第一份是專案檔與集中版本檔中的直接宣告;第二份是 project.assets.json 或 CLI 列出的已解析圖;第三份是弱點資料來源在本次還原時回報的公告;第四份是程式及部署輸出真正需要的組件。這四份證據回答不同問題,不能互相替代。
在 .NET 10,名詞優先命令為 dotnet package list;.NET 9 或更早版本使用 dotnet list package。官方文件也指出 --include-transitive 不是預設,因此只看一般清單會遺漏傳遞套件。自動化腳本應先確認 SDK 版本,再選擇正確命令形式,不要把語法差異誤判成專案問題。
先區分直接相依、傳遞相依與執行期檔案
直接相依是專案明確宣告的 PackageReference;傳遞相依是直接套件為完成自己的契約而帶入的元件;發佈輸出則是特定目標框架與執行識別碼解析後真正交付的檔案集合。三者有關,但不是同一張清單。
例如套件 A 依賴記錄抽象層,記錄抽象層又依賴注入元件。專案可能完全沒有直接宣告注入元件,還原後它仍會出現在傳遞清單。這不是「多餘 DLL」的證據,而是相依圖的結果。dotnet nuget why 會顯示從頂層套件到目標套件的路徑,適合回答「誰把它帶進來」。
相反地,若專案直接宣告某個套件,但 API 搜尋、編譯器參考、設定檔與測試都找不到用途,而移除後輸出契約仍通過,它才是較可信的清理候選。搜尋 using 只能當初篩,因為完整型別名稱、反射字串、序列化轉換器、MSBuild targets、原始碼產生器與設定驅動載入都可能沒有一般 using。可靠流程要讓編譯器、測試與發佈輸出共同作證。
還要避免把傳遞相依直接升格為永久的頂層參考。遇到傳遞弱點時,NuGet 官方建議先更新最靠近直接參考的套件;若仍無法解除,再評估更新更深層套件或暫時釘選修正版。直接釘選雖能控制解析結果,也會讓團隊承擔其生命週期,因此應記錄原因與移除條件。
對可插拔或獨立發佈元件,建置成功也不代表輸出完整。主程式提供的共用契約通常不應重複部署,但新版本套件新增的傳遞組件可能必須隨附。驗證時應檢查隔離輸出目錄,並從乾淨程序啟動測試,而不是因開發機全域快取或其他專案剛好存在同名 DLL 就誤判成功。
用 NuGet Audit 處理弱點,而不是隱藏警告
NuGet Audit 在還原期間比對相依套件與弱點資料。官方對應碼為 NU1901 低、NU1902 中、NU1903 高、NU1904 嚴重;NU1900 表示取得弱點資訊時發生通訊問題,NU1905 則與無法提供弱點資料的稽核來源有關。警告碼不同,處置也不同:網路失敗不能被解讀為「沒有弱點」。
NuGetAuditMode 可設為 direct 或 all。官方文件指出,目標為 net10.0 或更新版本時預設為 all,較舊目標預設為 direct;多目標專案只要有一個目標選擇 all,稽核會對所有目標使用該模式。為了讓 CI 意圖清楚,建議在儲存庫層明確設定模式與最低嚴重度,而不是只依賴隨 SDK 改變的預設。
以下為屬性設定片段,完整可執行專案見重現章節。
net10.0
true
all
moderate
發現直接套件弱點時,先閱讀公告影響條件與修正版本,再更新直接參考並跑回歸測試。發現傳遞弱點時,先用 dotnet nuget why 找路徑,再更新最靠近頂層的套件。不要因警告妨礙建置就全面加入 NoWarn。官方提供 NuGetAuditSuppress,但也明確把抑制視為最後手段;若風險經評估不適用,抑制項目仍應綁定特定公告、決策理由、負責人與複查日期。
「本次沒有列出弱點」只代表這次相依圖與當時資料來源沒有已知命中,不代表套件永久安全。公告資料會更新,來源也可能暫時失敗。因此 CI 應保留還原與稽核紀錄,並把 NU1900、NU1905 與空報告區分。安全結論必須包含時間、SDK、來源狀態及解析版本。
安全移除套件的決策流程
套件清理應以小批次變更進行。一次刪除大量參考,即使最後建置失敗,也很難知道哪一個移除破壞了契約。每個候選最好有一張簡短決策卡:直接或傳遞、目前引入路徑、原始碼用途、建置用途、執行期用途、輸出差異、弱點狀態,以及回復方式。
- 建立基準。 固定 SDK、目標框架與 NuGet 設定,執行乾淨還原、Release 建置、測試及發佈,保存套件圖與輸出檔名清單。
- 追出原因。 用
dotnet package list --include-transitive區分頂層與傳遞套件,對疑問套件執行dotnet nuget why,再檢查程式、專案檔、targets、設定與測試。 - 一次處理一個候選。 若是弱點,優先更新上層直接套件;若是冗餘直接參考,移除一筆後重新還原。不要同時升級框架、改部署方式與刪套件。
- 比較可觀察結果。 要求建置與測試成功,必要組件仍在輸出中,不必要組件確實消失,且沒有新增 NU190x 警告。對插件或工具再以乾淨程序做啟動測試。
- 記錄與回復。 提交套件圖前後差異、理由、公告連結與回復提交。若無法證明安全,保留套件並建立待查工作,不要用猜測換取較小的數字。
這個流程也能避免兩種常見誤判。第一是把目標框架已提供的功能與 NuGet 套件混為一談;是否需要明確參考要由目標框架、編譯結果與部署模式決定。第二是認為相同功能只能存在一個組件名稱;版本升級可能改用不同套件或新傳遞相依,應看相依圖與實際載入,不可只比較檔名相似度。
對 CI 而言,建議將清單輸出為 JSON,讓檢查器穩定讀取,而不是解析在地化主控台文字。--format json --output-version 1 可固定目前結構版本。報告應至少保留 resolved version、top-level/transitive 分類與弱點結果;但不要把內部來源網址或敏感授權資料寫進公開產物。
驗證與重現:在隔離 .NET 10 專案檢查相依圖
以下測試已於排程環境使用 .NET SDK 10.0.400 實際執行,僅建立本機沙盒專案並從公開 NuGet 來源還原,不觸碰既有應用資料。實測確認 JSON 可分開列出頂層與傳遞套件、弱點查詢可完成,且 dotnet nuget why 能顯示完整引入鏈。
以下四個檔案放在同一個空白資料夾。global.json 固定 SDK,NuGet.Config 明確指定公開套件與稽核來源;需要已安裝 .NET SDK 10.0.400 並可連線 NuGet。
NuGetAuditLab.csproj
Exe
net10.0
true
all
moderate
Program.cs
System.Console.WriteLine("NuGetAuditLab ready");
global.json
{"sdk":{"version":"10.0.400","rollForward":"disable"}}
NuGet.Config
- 建立上述四個完整檔案;不要把 PackageReference 直接放在 PropertyGroup 中,它必須位於 ItemGroup。
- 執行固定版本檢查與乾淨還原:
dotnet --version,接著執行dotnet restore NuGetAuditLab.csproj --force --no-cache。若 SDK 不是 10.0.400、還原失敗或出現未處置的 NU190x,測試失敗。 - 執行
dotnet package list --project NuGetAuditLab.csproj --include-transitive --format json --output-version 1。通過條件是 JSON 的頂層含Microsoft.Extensions.Http10.0.0,傳遞集合含Microsoft.Extensions.DependencyInjection10.0.0。 - 執行
dotnet package list --project NuGetAuditLab.csproj --include-transitive --vulnerable --format json --output-version 1。本次實測結束碼為 0,專案沒有列出弱點套件;若來源錯誤、JSON 無法解析、出現弱點項目或結束碼非零,則失敗。 - 執行
dotnet nuget why NuGetAuditLab.csproj Microsoft.Extensions.DependencyInjection。通過條件是路徑依序顯示Microsoft.Extensions.Http、Microsoft.Extensions.Logging、Microsoft.Extensions.DependencyInjection,證明傳遞套件不是無來源的殘留。
dotnet --version
dotnet restore NuGetAuditLab.csproj --force --no-cache
dotnet package list --project NuGetAuditLab.csproj --include-transitive --format json --output-version 1
dotnet package list --project NuGetAuditLab.csproj --include-transitive --vulnerable --format json --output-version 1
dotnet nuget why NuGetAuditLab.csproj Microsoft.Extensions.DependencyInjection
實測只驗證命令行為與這個合成相依圖,沒有宣稱任何其他專案安全,也沒有提供效能數字。讀者在自己的測試專案重現時,應保留完整輸出並檢查資料來源狀態;若環境使用 .NET 9 或更早版本,依官方文件改用 dotnet list package,不要直接複製 .NET 10 的命令順序。
結論:把套件數量轉成可治理的相依契約
有效的 NuGet 治理不是定期把所有套件升到最新,也不是追求最少的 PackageReference。它要求團隊知道每個頂層套件解決什麼問題、每個傳遞套件由誰帶入、每個弱點警告如何判讀,以及移除後哪些可觀察條件必須維持。
最實用的起點是建立一次可重現基準:固定 SDK,輸出 JSON 相依圖,對疑問套件執行 dotnet nuget why,啟用涵蓋傳遞相依的 NuGet Audit,再讓每次升級或刪除只改一個決策。如此一來,套件瘦身不再是憑名稱猜測,而是以還原、建置、測試與輸出證據共同約束的工程變更。
**2026-09-27 編輯複驗:**以下為檢查結果摘要,不是 CLI 原始輸出的逐字複製。另執行 dotnet build NuGetAuditLab.csproj -c Release --no-restore 與 dotnet run --project NuGetAuditLab.csproj -c Release --no-build,要求結束碼皆為 0 且程式印出固定文字。
SDK: 10.0.400
Top-level: Microsoft.Extensions.Http 10.0.0
Transitive: Microsoft.Extensions.DependencyInjection 10.0.0
Path: Microsoft.Extensions.Http -> Microsoft.Extensions.Logging -> Microsoft.Extensions.DependencyInjection
Vulnerable-package entries: none in this run
Program output: NuGetAuditLab ready
官方參考文件