Swift Digest
SE-0509 | Swift Evolution

Software Bill of Materials (SBOM) Generation for Swift Package Manager

Proposal
SE-0509
Authors
Ev Cheng
Review Manager
Franz Busch
Status
Implemented (Swift 6.4)

01 何が問題だったのか

SBOM(Software Bill of Materials)は、成果物(アーティファクト)に含まれているソフトウェアコンポーネントの明細を記述したものです。依存しているパッケージに脆弱性があるかを調べるサプライチェーンセキュリティの観点や、規制・監査の要件から、ビルドした成果物に対してSBOMを生成したいというニーズが高まっています。SBOMの代表的な仕様としては CycloneDXSPDX の2つがあります。

これまでSwift Package Manager(SwiftPM)にはSBOMを生成する機能がなく、サードパーティ製のツールに頼る必要がありました。しかしそれらのツールは基本的に Package.swiftPackage.resolved だけを見て依存関係を解析するため、「どの product がどの依存を使っているか」といったビルドグラフレベルの情報は欠落してしまいます。結果として、ランタイムに実際にリンクされる product と、パッケージグラフ上に現れる依存とのずれを反映できず、必要以上にノイズの多いSBOMしか作れないという課題がありました。

02 どのように解決されるのか

SwiftPMにCycloneDXとSPDXのSBOMを生成する機能を組み込みます。生成方法は2つ用意され、ビルドに付随して作る方法と、ビルドとは独立に呼び出すサブコマンドから作る方法のいずれかを選べます。対応する仕様は各メジャーバージョンの最新マイナーのみで、JSON形式のみをサポートします(現時点ではCycloneDX 1.7とSPDX 3.0)。

swift build への統合

swift build--sbom-spec フラグが追加されます。cyclonedx または spdx、あるいはその両方(フラグを2回指定)を渡すと、ビルド完了後にSBOMが生成されます。ビルドが失敗したときやSBOM生成が失敗したときはビルド全体がエラーになります(挙動を警告に弱めたい場合は --sbom-warning-only を使えます)。

# パッケージ全体のCycloneDX SBOMをビルドグラフ込みで生成
$ swift build --build-system swiftbuild --sbom-spec cyclonedx

# パッケージ全体のSPDX SBOMを生成
$ swift build --build-system swiftbuild --sbom-spec spdx

# CycloneDXとSPDXの両方を、パッケージグラフのみから生成
$ swift build --sbom-spec cyclonedx --sbom-spec spdx

# 特定のproductに絞って生成
$ swift build --build-system swiftbuild --product MyProduct1 --sbom-spec cyclonedx

# 出力先ディレクトリを指定
$ swift build --build-system swiftbuild --sbom-spec cyclonedx --sbom-spec spdx --sbom-output-dir /MyDirectory

--sbom-spec は、--product--target も付けない(パッケージ全体に対するSBOM)か、--product と組み合わせる(特定productに対するSBOM)かのどちらかでのみ使えます。--target と組み合わせた場合はビルドがエラーになります。

--build-system swiftbuild を指定しない場合、SwiftBuildが提供するビルド依存グラフが使えないため、パッケージグラフのみから生成した旨の警告が最終行に出力されます。ビルドグラフがある場合は、実際にビルドで使われたコンポーネント・依存関係のみがSBOMに含まれ、条件付き依存(OS別など)も反映されます。ビルドグラフがない場合は、解決済みパッケージグラフ上のすべての依存関係がそのまま含まれます。なお trait による依存の取捨はパッケージ解決の段階で反映されるため、どちらのケースでも正しく扱われます。

インクリメンタルビルドの場合でも、ビルドが成功すればその時点のグラフ状態を元にSBOMは毎回生成されます。SBOMの有無はビルドがフルになるかインクリメンタルになるかには影響しません。同じ構成で複数回ビルドした場合、以前のSBOMは上書きされずにタイムスタンプ付きで併存します(同じproductを異なるフラグでビルドするユースケースを想定したものです)。

バージョン指定は次のように行えます。数字なしの cyclonedx / spdx は常にSwiftPMがサポートする最新メジャーの最新マイナーを指します。マイナーバージョンが新しくなった時点で、直前のマイナーはサポート対象から外れます(メンテナンスコストを現実的に保つため)。

--sbom-spec <spec>
    cyclonedx   - SwiftPMが対応する最新のCycloneDX(現在: 1.7)
    spdx        - SwiftPMが対応する最新のSPDX(現在: 3.0)
    cyclonedx1  - CycloneDX v1 系の最新マイナー
    spdx3       - SPDX v3 系の最新マイナー

その他に --sbom-output-dir で出力先ディレクトリ、--sbom-filter でSBOMに含める情報の粒度を指定できます。--sbom-filter は以下の3値のいずれかです。

  • all(既定): パッケージ・product の両方を網羅した詳細なSBOM。
  • package: パッケージとパッケージ間の依存のみ。CVEなどパッケージ単位で開示される脆弱性の対処に向いています。
  • product: product と product 間の依存のみ。ランタイムに実際に含まれるコンポーネントを把握するのに向いています。

環境変数による設定

CIなどで swift build の呼び出し側を書き換えられない場合に備えて、同等のフラグを環境変数でも指定できます。対応するフラグが同時に渡された場合は常にフラグ側が優先されます。

  • SWIFTPM_BUILD_SBOM_SPEC: cyclonedx / spdx / cyclonedx,spdx のいずれかで、自動的にSBOM生成を有効化します。
  • SWIFTPM_BUILD_SBOM_OUTPUT_DIR: 出力先ディレクトリ。
  • SWIFTPM_BUILD_SBOM_FILTER: all / package / product(既定は all)。
  • SWIFTPM_BUILD_SBOM_WARNING_ONLY: true にすると、SBOM生成失敗をビルドエラーではなく警告に格下げします。

これにより、インフラやセキュリティチームが配下のすべてのSwiftプロジェクトに対してSBOMを強制しつつ、既存のビルド結果には影響を与えない、といった運用ができます。

swift package generate-sbom サブコマンド

ビルドを伴わずにSBOMだけを生成したい場合のために、swift package generate-sbom サブコマンドも追加されます。フラグは swift build と同じものを共有しますが、このサブコマンドはビルドを実行せず、SwiftBuildのビルド依存グラフも使いません。そのため生成されるSBOMはパッケージグラフのみを元にしたものになり、コマンドの最終行にその旨の警告が必ず出力されます。

# パッケージグラフのみからCycloneDX SBOMを生成
$ swift package generate-sbom --sbom-spec cyclonedx

# 特定productに絞って生成
$ swift package generate-sbom --product MyProduct1 --sbom-spec cyclonedx

Package.resolved が古いまま暗黙的に再解決されてしまうのを避けたい場合は、--disable-automatic-resolution を付けて、解決済みでなければ失敗させることもできます。このサブコマンドは、ビルドが済んだあとのパイプラインや、ビルド時とは別のツールチェーン上でSBOMを作りたいケースを想定しています。ビルド後に依存グラフが変わった状態で呼ぶと、成果物の実状とSBOMの内容がずれる可能性がある点には注意が必要です。

SBOMに含まれる情報

いずれの仕様でも、SBOMには次の情報が含まれます。

  • コンポーネント: 各パッケージ/product の種別(executableを含むパッケージ・product は application、それ以外は library)、名前、バージョンタグまたはコミットSHA、Package URL(PURL)、ソースリポジトリ情報(コミットSHAとURL)、Swiftパッケージかproductかの区別、テストコンポーネントか否かを示すスコープ(test / required)。
  • 依存関係: パッケージ → パッケージ、パッケージ → product、product → product の3種類の依存を表現します。product → product はサイクルを避けるため、同一 root パッケージ内の product 間は追跡しません。
  • メタデータ: 生成に使ったSwiftPMのバージョン、生成時刻、仕様バージョン。

たとえばCycloneDXでは各コンポーネントが次のような形で表現されます。

{
    "bom-ref" : "swift-asn1:SwiftASN1",
    "name" : "SwiftASN1",
    "purl" : "pkg:swift/github.com/apple/swift-asn1:SwiftASN1@1.5.0",
    "scope" : "required",
    "type" : "library",
    "version" : "1.5.0",
    "properties" : [
        { "name" : "swift-entity", "value" : "swift-product" }
    ]
}

SBOM生成は内部的に、パッケージグラフ(および任意でSwiftBuildのビルド依存グラフ)から情報を抽出するExtractor層、それをCycloneDX / SPDXの形式に変換するConverter層、生成結果を組み込みJSONスキーマで検証するValidator層の3層で行われます。swift-package-manager 程度の規模(約60コンポーネント、350依存)であれば、両方のSBOMを生成しても数秒で完了する想定です。

今後の展望

今回のスコープ外として、ライセンスの自動推定、--target 単位のSBOM、XML/YAMLなど追加フォーマット、CycloneDX 2 / SPDX 4 への対応、複数成果物のSBOMのマージ、SBOMへの署名やハッシュの追加、ビルド済みバイナリへのSBOM埋め込みなどが今後の拡張候補として挙げられています(いずれもspeculativeなもので、実現を約束するものではありません)。