SBOM差分検知ツール：sbomdiffの使い方説明

- [コマンドの説明について](#コマンドの説明について)
- [実際にSBOMの差分を検出してみる](#実際にsbomの差分を検出してみる)
  - [仮想環境の構築](#仮想環境の構築)
  - [パッケージに対する変更とSBOMの生成](#パッケージに対する変更とsbomの生成)
    - [初期：パッケージのインストールとSBOM生成](#初期パッケージのインストールとsbom生成)
    - [変更1：パッケージのアップデートと追加、SBOM生成](#変更1パッケージのアップデートと追加sbom生成)
    - [変更2：パッケージの削除、SBOM生成](#変更2パッケージの削除sbom生成)
  - [SBOMの差分を検出](#sbomの差分を検出)
    - [パッケージのバージョンやライセンス変更、新規追加パッケージの検出](#パッケージのバージョンやライセンス変更新規追加パッケージの検出)
    - [削除されたパッケージの検知](#削除されたパッケージの検知)
- [sbomdiffをshellスクリプトで利用するときの注意点とその対策](#sbomdiffをshellスクリプトで利用するときの注意点とその対策)
  - [txt形式で出力する場合にバグがある](#txt形式で出力する場合にバグがある)
  - [差分を検知するとエラーになる](#差分を検知するとエラーになる)
      - [解決策1: ||trueをつけて、終了ステータスを0に上書きする](#解決策1-trueをつけて終了ステータスを0に上書きする)
      - [解決策2: 終了ステータスに1を返す場合も実行されるようにスクリプトを書く](#解決策2-終了ステータスに1を返す場合も実行されるようにスクリプトを書く)
- [sbomdiffの活用先について](#sbomdiffの活用先について)


[sbomdiff](https://github.com/anthonyharrison/sbomdiff/tree/main)は異なる2つのSBOMファイルから差分を抽出するライブラリです。このツールが対応するSBOMの形式は[SPDX](https://www.spdx.org/) と [CycloneDX](https://www.cyclonedx.org/) の２種類です。

また、sbomdiffはpythonで作成されたツールであり、`pip`を使い、インストールすることが可能です。

sbomdiffはSBOMの４つの変更を検知することができます。

- パッケージのバージョン変更
- パッケージのライセンス変更
- 取り除かれたパッケージ
- 新たに追加されたパッケージ

これらの変更を検知できるため、sbomdiffは、あるソフトウェアに変更を加える、または加えられたときにその差分を検知することができます。

本記事では、実際に、SBOMに人工的に変更を加えていき、sbomdiffがどのように差分を検出するのかを見ていくことが目的です。


# コマンドの説明について

```bash
sbomdiff [-h]\
				 [--sbom {auto,spdx,cyclonedx}]\
				 [--exclude-license]\
				 [-d]\
				 [-o OUTPUT_FILE]\
				 [-f {text,json,yaml}]\
				 [-V]\
				 FILE1 FILE2
```

各引数の説明です。

比較する２種類のファイルを指定します。

- `FILE1`: ここでは、変更前のSBOMを指定します
- `FILE2`: ここでは、変更後のSBOMを指定します

sbomdiffに関する情報の確認

- `-h, --help`: ヘルプオプション、各オプションの簡易的な説明を表示
- `-v, --version`: バージョンを確認

入力に関するオプション
- `--sbom {auto,spdx,cyclonedx}`: 比較するSBOMの形式を指定、defultではautoになっている
- `--exclude-license`: パッケージのライセンスの変更情報を除外する

出力に関するオプション
- `-d, --debug`: デバッグの詳細情報について出力する
- `-o OUTPUT_FILE, --output-file OUTPUT_FILE` OUTPUT_FILEにファイル名を代入することで、そのファイルに結果を出力できる defaultでは結果が標準出力される
-  -f {text,json,yaml}, --format {text,json,yaml}`: 出力先フォーマットを選ぶことができる、ファイルのフォーマットはtext,json,yamlの中から選ぶことができ、defaultではtext形式で出力される

もしも、ソフトウェア構成情報に変更を加える前のSBOM：`sbom_prev.json`と変更後のSBOM：`sbom_post.json`を比較し、その結果をjson形式でsbom_diff.jsonというファイルに出力したい場合は、以下のようにコマンドをたたきます。
```bash
sbomdiff --sbom=cyclonedx -o sbom_diff.json -f=json sbom_prev.json sbom_post.json
```

# 実際にSBOMの差分を検出してみる
それでは、Pythonとpipを使って、パッケージのバージョンアップやアンインストールを行い、その変更前後でソフトウェア構成情報をSBOMに記録し、変更前後で生成したSBOMの差分を検出していきます。

初めに、作業用directoryを作成しておきます。
```bash
mkdir sbomdiff_test
```

## 仮想環境の構築
まず，venvでpythonの仮想環境を構築し、環境をアクティベートします。

```bash
python3 -m venv sbomdiff_venv
cd sbomdiff_venv
source bin/activate
```

## パッケージに対する変更とSBOMの生成
パッケージに対して変更を加え、その都度、SBOMを生成していきます。今回作成たSBOMは3種類あり、それらを以下表にまとめておきます。

| 段階  | SBOMの名前      | 加えた変更                            | 
| ----- | --------------- | ------------------------------------- | 
| 初期  | sbom_prev.json  | Flask1.1.4をインストール              | 
| 変更1 | sbom_post.json  | Flaskをアップデート、matplotlibを追加 | 
| 変更2 | sbom_post2.json | matplotlibとnumpyを削除               |

以降の節では、上で示したSBOMの作成手順を説明します。

### 初期：パッケージのインストールとSBOM生成
`pip install`でパッケージをインストールし、その後、pythonのパッケージのメタデータが存在する仮想環境ディレクトリごと、`syft`でスキャンします。

今回は、Flaskのバージョン1.1.4をインストールします。そして、`sbom_prev.json`という名前でSBOMを生成します。

sbomは`sbomdiff_test`ディレクトリに配置しておきたいので、`cd`でそこに移動してください。

```bash
pip install Flask==1.1.4
syft dir:~/sbomdiff_test/sbomdiff_venv -o cyclonedx-json=sbom_prev.json
```

### 変更1：パッケージのアップデートと追加、SBOM生成
次に、Flaskをアップデート、さらにmatplotlibを新たにインストールし追加します。そして、`sbom_post.json`という名前でSBOMを生成します。


```bash
pip install -U Flask
pip install matplotlib
syft dir:~/sbomdiff_test/sbomdiff_venv -o cyclonedx-json=sbom_post.json
```

### 変更2：パッケージの削除、SBOM生成
最後に、パッケージ`matplotlib`、`numpy`を削除し、`sbom_post2.json`という名前でSBOMを生成します。
```bash
pip uninstall matplotlib
pip uninstall numpy
syft dir:~/sbomdiff_test/sbomdiff_venv -o cyclonedx-json=sbom_post2.json
```

## SBOMの差分を検出
上で生成した3種類のSBOMから差分を検出します。どのSBOMをそれぞれ比較するかを以下の表にまとめておきます。

| sbom差分ファイル名 | 変更前のsbomファイル | 変更後sbomファイル | 検知が期待される差分                                               | 
| ------------------ | -------------------- | ------------------ | ------------------------------------------------------------------ | 
| sbom_diff1.json    | sbom_prev.json       | sbom_post.json     | パッケージのバージョンやライセンス変更、新たに追加されるパッケージ | 
| sbom_diff2.json    | sbom_prev.json       | sbom_post2.json    | 削除されたパッケージ                                               | 

### パッケージのバージョンやライセンス変更、新規追加パッケージの検出
`sbom_prev.json`と`sbom_post.json`の差分を検出します。以下のコマンドを実行し、jsonとtxt、両方の出力結果を見てみます。
```bash
sbomdiff --sbom=cyclonedx -o sbom_diff1.json -f=json sbom_prev.json sbom_post.json
sbomdiff --sbom=cyclonedx -f=text sbom_prev.json sbom_post.json > sbom_diff1.txt
```
実際に、パッケージに加えられた変更を検知することができました。以下、結果の詳細です。

こちらは、txt形式の出力結果です。それぞれパッケージの変更情報が直接書かれています。
- [VERSION]：バージョンの変更
- [LICENSE]：ライセンスの変更
- [ADDED  ]：追加されたパッケージの情報
- [REMOVED]：削除されたパッケージの情報

最後に、Summaryとして、それぞれの合計が記載されています。
```txt: sbom_diff1.txt
[VERSION] Flask: Version changed from 1.1.4 to 3.0.3
[LICENSE] Flask: License changed from BSD-3-Clause to NOT FOUND
[VERSION] Jinja2: Version changed from 2.11.3 to 3.1.4
[LICENSE] Jinja2: License changed from BSD-3-Clause to NOT FOUND
[VERSION] Werkzeug: Version changed from 1.0.1 to 3.0.3
[LICENSE] Werkzeug: License changed from BSD-3-Clause to NOT FOUND
[VERSION] click: Version changed from 7.1.2 to 8.1.7
[VERSION] itsdangerous: Version changed from 1.1.0 to 2.2.0
[LICENSE] itsdangerous: License changed from BSD to NOT FOUND
[ADDED  ] blinker: (Version 1.8.2) (License NOT FOUND)
___________
___________
...........
___________
Summary
Version changes:  5
License changes:  12
Removed packages: 0
New packages:     4
```

json形式ではtxt形式とは異なり、パッケージ名とそのステータス、どのように変更があったかが、key:value形式で表されています。変更数が多い場合に、jsonファイルを加工してトリアージする場合はjson形式の方が使い勝手が良さそうです。
```json:sbom_diff1.json
{
  "tool": {
    "name": "sbomdiff",
    "version": "0.5.3"
  },
  "file_1": "sbom_prev.json",
  "file_2": "sbom_post.json",
  "differences": [
    {
      "package": "Flask",
      "status": "change",
      "version": {
        "from": "1.1.4",
        "to": "3.0.3"
      },
      "license": {
        "from": "BSD-3-Clause",
        "to": "NOT FOUND"
      }
    },
    {
      "package": "Jinja2",
      "status": "change",
      "version": {
        "from": "2.11.3",
        "to": "3.1.4"
      },
      "license": {
        "from": "BSD-3-Clause",
        "to": "NOT FOUND"
      }
    },
    {
      "package": "Werkzeug",
      "status": "change",
      "version": {
        "from": "1.0.1",
        "to": "3.0.3"
      },
      "license": {
        "from": "BSD-3-Clause",
        "to": "NOT FOUND"
      }
    },
    {
      "package": "click",
      "status": "change",
      "version": {
        "from": "7.1.2",
        "to": "8.1.7"
      }
    },
    {
      "package": "itsdangerous",
      "status": "change",
      "version": {
        "from": "1.1.0",
        "to": "2.2.0"
      },
      "license": {
        "from": "BSD",
        "to": "NOT FOUND"
      }
    },
    {
      "package": "blinker",
      "status": "add",
      "version": {
        "from": "1.8.2"
      },
      "license": {
        "to": "NOT FOUND"
      }
    },
    
    
    {
      "package": "six",
      "status": "add",
      "version": {
        "from": "1.16.0"
      },
      "license": {
        "to": "MIT"
      }
    }
  ],
    ___________
    ___________
    ...........
    ...........
    ___________
  "summary": {
    "version_changes": 5,
    "new_packages": 12,
    "removed_packages": 0,
    "license_changes": 4
  }
}
```


### 削除されたパッケージの検知
削除されたパッケージの検知についても同様です。
```bash
sbomdiff --sbom=cyclonedx -o sbom_diff2.json -f=json sbom_prev.json sbom_post2.json
sbomdiff --sbom=cyclonedx -f=text sbom_prev.json sbom_post2.json > sbom_diff2.txt
```

txt形式
```txt: sbom_diff2.txt
[REMOVED] matplotlib: (Version 3.8.4)
[REMOVED] numpy: (Version 1.26.4)

Summary

Version changes:  0
License changes:  0
Removed packages: 2
New packages:     0
```

json形式
```json:sbom_diff2.json
{
  "tool": {
    "name": "sbomdiff",
    "version": "0.5.3"
  },
  "file_1": "sbom_post.json",
  "file_2": "sbom_post2.json",
  "differences": [
    {
      "package": "matplotlib",
      "status": "remove",
      "version": {
        "from": "3.8.4"
      }
    },
    {
      "package": "numpy",
      "status": "remove",
      "version": {
        "from": "1.26.4"
      }
    }
  ],
  "summary": {
    "version_changes": 0,
    "new_packages": 0,
    "removed_packages": 2,
    "license_changes": 0
  }
}
```

# sbomdiffをshellスクリプトで利用するときの注意点とその対策
sbomdiffを使う際に以下2つの注意点に配慮する必要があります。
- txt形式で出力する際にSummaryがでないこと
- 差分を検知した場合、修了ステータスが1となること
です。

## txt形式で出力する場合にバグがある
sbomdiffでは、ドキュメントが推奨する以下のようなコマンドの実行を行うとSummaryがtxtへ出力されません。
```bash
sbomdiff --sbom=cyclonedx -o sbom_diff1.txt -f=text sbom_prev.json sbom_post.json
```
そこで、txtへSummaryを出力させるために、以下のようにコマンドを実行します。
```bash
sbomdiff --sbom=cyclonedx -f=text sbom_prev.json sbom_post.json > sbom_diff1.txt
```

## 差分を検知するとエラーになる
sbomdiffはdiffコマンドと似た仕様になっています。実際にソースコードを確認してみると、
https://github.com/anthonyharrison/sbomdiff/blob/3e095b3972a99b6c9fa0fe30ac795060f9b02382/sbomdiff/cli.py#L247
```python
    # Return code indicates if any differences have been detected
    if (version_changes or license_changes or removed_packages or new_packages) != 0:
        return 1

    return 0


if __name__ == "__main__":
    sys.exit(main())
```
ファイルの差分を検知した場合、終了ステータスに1を返すことがわかります。そのため、スクリプトなどで後続する処理がある場合はそこでエラー終了してしまいます。

この仕様により、CI/CDなどでsbomdiffを利用する場合、パイプラインが止まってしまいます。

そこで、スクリプトに書いた一連の処理でsbomdiffを利用する場合は、差分があってもエラーとして扱われないよう対策する必要があります。

#### 解決策1: ||trueをつけて、終了ステータスを0に上書きする
コマンドの後に、`||true`をつけて、終了ステータスを0に上書きすることで、スクリプトが止まるのを防ぎます。
```bash
sbomdiff --sbom=cyclonedx -o sbom_diff1.txt -f=text sbom_prev.json sbom_post.json
```

#### 解決策2: 終了ステータスに1を返す場合も実行されるようにスクリプトを書く
```bash
sbomdiff --sbom=cyclonedx -o sbom_diff1.txt -f=text sbom_prev.json sbom_post.json
# 直前のコマンドの終了ステータスを取得
exit_code=$?
echo ${exit_code}
# 終了コードが1の場合はスキップする
if [ $exit_code -eq 0 ]; then
    echo "can't detect the differences of two sbom files."
        #exit $exit_code
elif [ $exit_code -eq 1 ]; then
    echo "detect the differences of two sbom files."
fi
```


# sbomdiffの活用先について
ここまでの説明を読んで、sbomdiffの使い方はわかったと思います。しかし、実際にどう活用したらよいのかわからない方も多いのではないのでしょうか？

そこで、次回はsbomdiffの活用方法について少し解説していきたいと思っています。