依存関係
推移的依存関係とは?
推移的依存関係とは、別のパッケージが必要とするために、ソフトウェアが使用することになるパッケージです。自分で宣言したものではありませんが、出荷するものに含まれ、独自の脆弱性とライセンスを持っています。推移的依存関係を把握し、修正し、各リリースにどれが入ったかを証明する方法を解説します。
更新日
直接依存関係と推移的依存関係
直接依存関係とは、マニフェストで自分が宣言するものです。推移的依存関係とは、依存関係が宣言し、さらにその依存関係が宣言しているもので、ツリーの最深部まで続きます。パッケージマネージャーはインストール時にツリー全体を解決し、通常はその結果をロックファイルに記録します。
パッケージを1つだけ、expressを宣言したアプリを例にとります。npm ls --allを実行すると、その1行が何を連れてくるかがわかります。
my-app
└─┬ express ← 直接:自分で宣言
├─┬ body-parser ← 推移的、深さ1
│ └── raw-body ← 推移的、深さ2
├── cookie ← 推移的、深さ1
└─┬ send ← 推移的、深さ1
└── mime-types ← 推移的、深さ2 実際のツリーはもっと長くなります。一般的なアプリケーションのパッケージの大半はこのようにして入ってくるため、出荷するコードの大部分は、自分ではなくパッケージマネージャーが選んだものということになります。
| 直接依存関係 | 推移的依存関係 | |
|---|---|---|
| 選んだのは誰か | 自社チーム(マニフェストで) | パッケージマネージャー(依存関係のマニフェストから) |
| 記載場所 | マニフェストとロックファイル | ロックファイル(ある場合) |
| バージョンを決めるのは誰か | 自分 | 上書きしない限り、親パッケージのバージョン範囲 |
| 脆弱性の修正方法 | アップグレードする | 親をアップグレードするか、バージョンを上書きする |
推移的依存関係が重要な理由
推移的依存関係は、直接依存関係と同じリスクを抱えながら、可視性も制御も劣ります。
- 脆弱性。log4jのゼロデイであるLog4Shellが公表されたとき、GoogleのOpen Source Insightsチームは、影響を受けるlog4jのコードに依存するMaven Centralの成果物を35,863件見つけました。直接依存していたのは約7,000件で、大半は推移的依存関係として取り込んでおり、その多くは間にあるパッケージが先に修正を出すまで修正できませんでした。
- サプライチェーン攻撃。xz utilsのバックドア(CVE-2024-3094)は、OpenSSHがxzに依存していないにもかかわらずOpenSSHに到達しました。いくつかのLinuxディストリビューションはsystemdの通知に対応するためにOpenSSHにパッチを当てており、libsystemdはxzが提供するライブラリであるliblzmaに依存しているためです。
- ライセンス。推移的なパッケージにもそれぞれ独自のライセンスがあります。3階層下にあるコピーレフトライセンスも、自分で追加したものと同じように拘束力を持ちます。
- 破壊的変更。親パッケージがバージョン範囲を許容していると、マニフェストを何も変えていなくても、次のクリーンインストールで推移的依存関係の新しいリリースが入り込みます。
推移的依存関係を一覧表示する方法
主要なパッケージマネージャーはいずれも、解決済みのツリーを表示できます。
| エコシステム | コマンド |
|---|---|
| npm | npm ls --all |
| pnpm | pnpm list --depth Infinity |
| Python(uv) | uv tree |
| Python(pip) | pipdeptree |
| Go | go mod graph |
| Rust | cargo tree |
| Maven | mvn dependency:tree |
| Gradle | gradle dependencies |
特定のパッケージがなぜ含まれているかを知るには、そこに至るパスを確認します。npm explain <package>、pnpm why、yarn why、cargo tree -i <crate>を使います。パスを表示するだけでなく活用したい場合は、依存関係グラフをご覧ください。
脆弱な推移的依存関係を修正する方法
- 親をアップグレードする。そのパッケージを取り込んでいる直接依存関係を見つけ、修正済みバージョンを要求するリリースに移行します。親のメンテナーがテスト済みであるため、最もクリーンな修正方法です。
- ロックファイルを更新する。親のバージョン範囲がすでに修正済みバージョンを許容している場合は、
cargo update -p <crate>やnpm updateの再実行などでロックファイルのエントリを更新するだけで済みます。 - バージョンを上書きする。親が修正をリリースしていない場合は、自分でバージョンを強制します。npmとpnpmでは
overrides、Yarnではresolutions、pipでは制約ファイル、Goではgo.modでより高いバージョンを要求します。親はそのバージョンでリリースされたことがないため、必ずテストしてください。 - 親を置き換えるか削除する。どちらもうまくいかない場合、そのパッケージを持ち込んでいる依存関係は、もう使い続ける価値がないかもしれません。
一部のソフトウェアコンポジション解析ツールは、自社のコードが脆弱な関数をそもそも呼び出せるかを確認する到達可能性分析を備えており、これらの修正のどれを先に行うかを判断できます。選択肢についてはSCAツールの比較をご覧ください。
ロックファイルだけではわからない理由
ほとんどのツールは、ロックファイルから推移的依存関係を把握します。これは、ロックファイルが存在し、最新で、ビルドがそこからのみインストールする場合には有効です。しかしCIでは、そうでないことがよくあります。
- ロックファイルがない。バージョン範囲を指定した
requirements.txtや、ロックファイルをコミットしていないプロジェクトは、ビルドのたびに新たに解決されるため、推移的依存関係のバージョンがビルド間で変わることがあります。 - キャッシュ。CIキャッシュはパッケージを取得する代わりに以前の実行から復元し、その内容は現在のロックファイルの記載と一致するとは限りません。GitHub Actionsのキャッシュが隠すものをご覧ください。
- ビルドツール。リンター、コンパイラー、コードジェネレーター、セットアップのステップは、それぞれ独自の依存関係ツリーをビルドに持ち込みます。アプリケーションのマニフェストには載っていませんが、ソースコードやシークレットにアクセスできる状態で実行されます。
- インストールスクリプトとダウンロード。パッケージのインストールスクリプトがさらにコードを取得したり、ビルドのステップがバイナリを直接ダウンロードしたりすることがあります。どちらもロックファイルにエントリを残しません。
リリースにどの推移的依存関係が入ったかの完全な記録は、そのリリースのビルド中に取得したものだけです。ビルドを記録することと、後からファイルをスキャンすることの違いは、ビルド時SBOMで解説しています。
推移的依存関係とCRA
EUのCyber Resilience Actは、製造業者に「少なくとも製品の最上位の依存関係」を網羅したSBOMの作成を求めています(附属書I・第II部・第1項)。推移的依存関係は明記されていないため、法的な最低ラインは多くのガイドが示すよりも低くなっています。しかしCRAは、製品に含まれるコンポーネントの脆弱性を特定して対応することも求めており、脆弱な推移的パッケージもそのコンポーネントの1つです。最初の階層で止まっているSBOMでは、依存関係ツリーの大部分がそのプロセスから漏れてしまいます。詳しくはCRAのSBOM要件をご覧ください。
CRACIによる推移的依存関係のトラッキング
CRACIは、ビルドをスキャンするのではなく記録するGitHub Actionsのランナーです。各ジョブの実行中、パッケージを認識するプロキシが、CIキャッシュから復元されたパッケージも含め、ツリーのあらゆる階層でジョブが取得するすべてのパッケージを記録します。これにより、スキャナー型のツールよりも完全に、使用中の依存関係を把握できます。
- すべての階層を、正直に。各ジョブのSBOMには完全性の状態があるため、記録ですべてを捉えられなかった場合にそれがわかります。
- ツリーを可視化。依存関係グラフで、SBOMの依存関係の階層を複数のビューで確認できます。
- 推移的依存関係を含むエクスポート。CycloneDXとSPDXのエクスポートには推移的依存関係が含まれ、それぞれに宣言ライセンスが付きます。
- 影響を受けたビルドの特定。新たな脆弱性が公開されると、ビルド履歴から、直接か推移的かを問わず、脆弱なパッケージを含んでいた過去のビルドがわかるため、それを出荷していた期間を把握できます(CVEの影響を受けているかをご覧ください)。履歴は保持期間の設定に従い、デフォルトは180日です。
- ポリシーゲート。特定のCVEを含むビルドを、出荷前にブロックできます。
CRACIは到達可能性分析を行いません。自社のコードが脆弱な関数を呼び出しているかを知る必要がある場合は、それに対応したSCAツールと組み合わせてください。CRACIのSBOM生成をご覧いただくか、デモを予約して自社のビルドを記録してみてください。
推移的依存関係に関するよくあるご質問
推移的依存関係とは何ですか?
推移的依存関係とは、別のパッケージが必要とするために、ソフトウェアが使用することになるパッケージです。アプリがライブラリAに依存し、AがライブラリBに依存している場合、Bはアプリの推移的(間接的)な依存関係です。自分で宣言したものではありませんが、ソフトウェアと一緒に出荷されます。
直接依存関係と推移的依存関係の違いは何ですか?
直接依存関係とは、package.json、pyproject.toml、go.modなどのマニフェストで自分が宣言するものです。推移的依存関係とは、直接依存関係が宣言するもので、深さを問いません。直接依存関係は自分で選び、推移的依存関係はパッケージマネージャーがツリーを解決する際に選びます。
transient dependencyはtransitive dependency(推移的依存関係)と同じですか?
実際上は同じです。「transient dependency」は「transitive dependency」のよくある言い間違いです。正しい用語はtransitive(推移的)で、推移律に由来します。AがBに依存し、BがCに依存しているなら、AはCに依存しているということです。
推移的依存関係の脆弱性はどのように修正しますか?
まず、修正済みバージョンを含むリリースがあれば、そのパッケージを取り込んでいる直接依存関係をアップグレードします。なければ、npmのoverrides、Yarnのresolutions、pipの制約ファイルなど、パッケージマネージャーの上書きの仕組みで修正済みバージョンを強制し、親パッケージがそのバージョンでも動作することをテストします。
すべての推移的依存関係を一覧表示するにはどうすればよいですか?
どのパッケージマネージャーも、解決済みのツリーを表示できます。npm ls --all、pnpm list --depth Infinity、uv treeまたはpipdeptree、go mod graph、cargo tree、mvn dependency:tree、gradle dependenciesです。本番ビルドで実際に何がインストールされたかを知るには、ビルドそのものを記録してください。
EUのCyber Resilience Actは、SBOMに推移的依存関係を含めることを求めていますか?
規則が定める最低ラインは「少なくとも製品の最上位の依存関係」です(附属書I・第II部・第1項)。推移的依存関係は明記されていませんが、脆弱な推移的パッケージも製品に含まれており、CRAは出荷するコンポーネントの脆弱性に対応することを求めています。
ビルドが取り込んだすべての依存関係を確認
GitHub ActionsのワークフローをひとつCRACIで実行し、依存関係ツリーのあらゆる階層で取得されたすべてのパッケージを記録したSBOMを入手してください。
デモを予約