前言

最近供應鏈攻擊(Supply Chain Attack)的事件頻傳,所以搞得人心惶惶,各家廠商最近應該收到不少來自於mend.io的開源碼掃描報告,又或者是被要求要提交軟體中所參考的各種套件及所用版本,方便那些資安單位(?)可以檢視評估是否有可能已經遭受到攻擊。

果不其然收到了要求提交的需求,這裡就簡單記錄一下如何快速的盤點目前專案中所使用的套件與版本。

環境

實際上專案的後端是使用dotnet core(nuget),前端是使用angular和react(npm)

後端(Dotnet)套件盤點

以往來說,dotnet系列的程式碼,紀錄所使用套件的位置都是在各個專案檔(.csproj)上,會在專案檔中看到類似以下的內容,表示這些套件在這個專案中被可能有被參考使用。
更古老跟著專案走的 packages.config 就先不提了…

1
2
3
4
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NLog" Version="4.6.3" />
</ItemGroup>

在dotnet core統一使用sdk格式的專案檔的時候,就可以大量看到這樣子的內容

也就是說,如果要盤點出方案中,dotnet使用的套件,只要找出每一個csproj檔案中的PackageReference元素就可以了。 (誤)

即使是啟用了中央套件管理的方式,這種盤點方式也沒有毛病

如果只是這樣的話,那這一篇大概也沒什麼好寫了

在這種AI當道的時代,我想直接開個AI Agent在專案根目錄,給個promt說要盤點第三方套件也能有差不多的效果吧~XD

dotnet cli中,其實早就有這麼一個列出所有參考套件的指令 dotnet list package

※ 但如果是dotnet 10,可能會改成名詞優先,會變成 dotnet package list

透過 --help參數輸出如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Description:
列出專案或解決方案的所有套件參考。

使用方式:
dotnet list [<PROJECT | SOLUTION>] package [options]

引數:
<PROJECT | SOLUTION> 要操作的專案或解決方案。若未指定檔案,命令就會在目前的目錄中搜尋一個檔案。 [default: {當前位置}]

選項:
-v, --verbosity <LEVEL> 設定 MSBuild 的詳細程度層級。允許的值為 q[uiet]、m[inimal]、n[ormal]、d[etailed] 及 diag[nostic]。
--outdated 列出有更新版本的套件。無法與 '--deprecated' 或 '--vulnerable' 選項一併使用。
--deprecated 列出已淘汰的套件。無法與 '--vulnerable' 或 '--outdated' 選項一併使用。
--vulnerable 列出有已知弱點的套件。無法與 '--deprecated' 或 '--outdated' 選項一併使用。
-f, --framework <FRAMEWORK | FRAMEWORK\RID> 選擇架構以顯示其套件。若有多個架構,則重複使用該選項。
--include-transitive 列出可轉移和頂層套件。
--include-prerelease 在搜尋較新套件時,建議搜尋具有發行前版本的套件。需要使用 '--outdated' 選項。
--highest-patch 在搜尋較新套件時,建議只搜尋主要和次要版本號碼相符的套件。需要使用 '--outdated’ 選項。
--highest-minor 在搜尋較新的套件時,建議只搜尋主要版本號碼相符的套件。需要使用 '--outdated' 選項。
--config, --configfile <CONFIG_FILE> 要使用的 NuGet 組態檔路徑。需要 '--outdated'、'--deprecated' 或 '--vulnerable' 選項。
-s, --source <SOURCE> 搜尋較新的套件時,所使用的 NuGet 來源。需要 '--outdated'、'--deprecated' 或 '--vulnerable' 選項。
--interactive 允許命令停止並等候使用者輸入或動作 (例如: 完成驗證)。
--format <console|json> 指定清單套件命令的輸出格式類型。
--output-version <output-version> 指定機器可讀取輸出的版本。需要 '--format json' 選項。
-?, -h, --help 顯示命令列說明。

結果大概長得像這樣子(隨便拿個做來玩玩的專案試,請不要真的找這個路徑~XD)
dotnet列出套件

也能輸出成json格式(參數--format json),更方便直接餵給AI或者一些程式閱讀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"version": 1,
"parameters": "",
"projects": [
{
"path": "MockProxy/MockProxy.csproj",
"frameworks": [
{
"framework": "net8.0",
"topLevelPackages": [
{
"id": "PuppeteerSharp",
"requestedVersion": "20.1.3",
"resolvedVersion": "20.1.3"
}
]
}
]
}
]
}

前端套件(npm)盤點

我不專精的前端就更不用說了,早在dotnet framework時期,就已經是模組跟著專案跑的形狀了,這種基本的東西肯定是有的

指令為 npm ls,透過讀取package.json檔案,來知道當前前端專案使用到哪裡套件

※ 當前資料夾應該是要在package.json檔案的位置,如果不是,應該可以透過 <package-spec> 的參數調整

一樣放個 --help的輸出結果,有興趣可以試試各種參數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
List installed packages

Usage:
npm ls <package-spec>

Options:
[-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth <depth>]
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]]
[--include <prod|dev|optional|peer> [--include <prod|dev|optional|peer> ...]]
[--link] [--package-lock-only] [--no-unicode]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces] [--include-workspace-root] [--install-links]

alias: list

輸出結果範本(路徑塗掉了,不然在專案那一層還有列出機器上的路徑)
npm-ls

一樣有json版本的輸出結果,方便後續可能還想要什麼程式處理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
{
"version": "0.0.0",
"name": "hexo-site",
"dependencies": {
"hexo-generator-archive": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hexo-generator-archive/-/hexo-generator-archive-2.0.0.tgz",
"overridden": false
},
"hexo-generator-category": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hexo-generator-category/-/hexo-generator-category-2.0.0.tgz",
"overridden": false
},
"hexo-generator-index": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/hexo-generator-index/-/hexo-generator-index-4.0.0.tgz",
"overridden": false
},
"hexo-generator-sitemap": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/hexo-generator-sitemap/-/hexo-generator-sitemap-3.0.1.tgz",
"overridden": false
},
"hexo-generator-tag": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hexo-generator-tag/-/hexo-generator-tag-2.0.0.tgz",
"overridden": false
},
"hexo-renderer-ejs": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hexo-renderer-ejs/-/hexo-renderer-ejs-2.0.0.tgz",
"overridden": false
},
"hexo-renderer-marked": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/hexo-renderer-marked/-/hexo-renderer-marked-7.0.1.tgz",
"overridden": false
},
"hexo-renderer-stylus": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/hexo-renderer-stylus/-/hexo-renderer-stylus-3.0.1.tgz",
"overridden": false
},
"hexo-server": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hexo-server/-/hexo-server-3.0.0.tgz",
"overridden": false
},
"hexo-theme-landscape": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/hexo-theme-landscape/-/hexo-theme-landscape-1.1.0.tgz",
"overridden": false
},
"hexo-theme-next": {
"version": "8.25.0",
"resolved": "https://registry.npmjs.org/hexo-theme-next/-/hexo-theme-next-8.25.0.tgz",
"overridden": false
},
"hexo": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/hexo/-/hexo-7.3.0.tgz",
"overridden": false
}
}
}

SBOM(Software Bill of Materials)

實際上不管是前端或者後端,最終目的應該都是要提交一份軟體料件清單,方便我們可以知道有沒有所謂的供應鏈攻擊或者使用版本有沒有資安風險,雖然需求只有到交出參考了什麼套件以及套件是什麼版本這樣,但還想要再進一步說一下由OWASP主導的CycloneDX專案。

上一次碰到要做SBOM的時候,只有純後端,所以簡單透過微軟自己的SBOM Tool就出去了,這次本來想要依樣畫葫蘆再做一次,可是碰上了專案屬於前後端分離的狀況,那SBOM Tool就不適用了,當然需求本身只要透過dotnet list packagenpm ls就可以解決,但是其實產出來的json格式不一,懶人如我只想透過json格式輸出,再寫個dotnet的小程式讀取內容後,取出套件名稱和版本,輸出到表格中再貼上要提交文件中(萬能Excel…),這時候就看到 CycloneDX,輸出格式應該不管前後端產出都會是定義好的…吧

雖然在盤點的時候,因為時程關係沒能來得及研究,所以就是直接裝在本機電腦上,做成全域的cli工具,但現在我想試試看更適用於開發者的docker版本,感覺更適合用於三不五時存成一個docker-compose.yml或者Dockerfile,想用再開起來就好了

Dotnet Module

Docker部分的指令是使用docker run
-v .:/app為掛載volumn,將指令執行的當前目錄,掛載到container中的app目錄.
--rm,執行完之後,就把container刪掉
cyclonedx/cyclonedx-dotnet則是docker hub上的image的名稱

再來進入到cyclonedx的選項
-F json,指定輸出為json格式(預設應該是xml)
-tfm "net8.0",測試的專案是用dotnet 8的(但是image裡面是使用dotnet 9,如果有什麼問題,可能改別的image看看)
-o app/sbom,因為前面的docker指令將Host的目錄掛載到container的app資料夾中,因此就加上輸出位置到app資料夾中

1
docker run --rm -v .:/app cyclonedx/cyclonedx-dotnet -F json -tfm "net8.0" -o app "app/MockProxy.sln"

上面指令就會產出 MockProxy.sln,這個方案檔中的軟體料件清單到bom.json中
dotnet的SBOM

NPM Module

進到 cyclonedx-node-npm的github repo,發現竟然沒有無腦docker版本,沒關係,那就自己把這個image做出來

※ 但就npm來說,因為跟著專案走,其實沒什麼必要這樣做就是了

有了dotnet module的經驗,大概可以想像得到流程是

  1. 裝好node.js
  2. 安裝npm module
  3. 把指令串進container

簡單做個dockerfile

1
2
3
4
5
6
7
8
9
10
FROM node:lts

RUN apt-get update && \
npm install --global @cyclonedx/cyclonedx-npm && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

WORKDIR /app

ENTRYPOINT ["cyclonedx-npm"]

產生一個image,方便後面重複使用

1
docker build -t cyclonedx-npm .

所以執行的方式為

1
docker run --rm -v .:/app cyclonedx-npm -v -o sbom.json

上面指令就會產出,軟體料件清單到bom.json中,由於看起來好像什麼都沒發生,加個-v把執行過程輸出一下
npm的SBOM

結語

本篇的目標只有找出dotnet和node所使用的套件清單,雖然有點陽春,但是都可以靠開發依賴的cli工具完成。

但是出於好奇心,順手用用看CycloneDX來產出對應的SBOM內容,結果打開npm版本的時候不如預期,並沒有像dotnet版本一樣有給個docker懶人包(雖然dotnet版本也沒有特別提到需要掛volumn就是了,需要有一點使用過docker經驗的再嘗試會更好),有找到在docker hub上的npm版本,但是最後更新已經是三年前,索性一不做二不休,順便練習一下怎麼包image,實際做出對應npm版的CycloneDX image,並且可以仿照dotnet版的做類似的使用方式。

結論來說,實際上到底使用了什麼套件,可能也不會到太準確,因為開發的時候,很多套件都是抓下來用用看,但最後成品可能沒有去特別移除這些沒用到的部分,又或者套件本身用了一個全包的基底層,也有分割出來對應相依程式的版本,例如:dotnet的FluentMigrator.Runner,為了支援各式各樣的資料庫,這個Runner會從nuget上抓下非常非常多的版本,但實際上最後只用到少數幾個,就會產生很多不必要的參考套件,而且最後的發佈成品也會包括這些內容,美其名是可能可以支援各式各樣的資料庫執行Migration,但現實面就是只會用一到兩種的資料庫就足夠使用,最後的結果就是充滿了無用程式碼的應用程式,在每一個bit可能都會收費的雲端中,變成了一種隱形的成本。

說是這麼說,但不知道實際執行會不會用到的情況下,應該還是保持一貫的態度,能動的程式就別再動了,免得出事~XD。

參考

  1. dotnet 套件列表
  2. npm ls
  3. SBOM Tool
  4. CycloneDX module for .NET
  5. CycloneDX SBOM for npm