依存関係情報のデータモデル
概要
このドキュメントでは、依存関係スキャンとライセンススキャンの新機能を実現するためのセキュリティレポートとデータベーススキーマへの追加について検討します。
検討されているソリューションは次のとおりです:
- PostgreSQL プライマリデータベースにテーブルを追加する(スキーマ変更)
- 新しい PostgreSQL データベースを作成する(新しいスキーマ)
- オブジェクトストレージに新しいドキュメントを保存する
- ElasticSearch でデータをインデックス化する
- Redis でデータをキャッシュする
このドキュメントのほとんどのセクションは 3 つの部分で構成されています:
- 機能: ユーザーは何を必要としているか?
- 技術的な議論: 機能をどのように実装できるか?技術的な制約は何か?どのように満たすことができるか?
- 提案: 対応する DB スキーマまたはドキュメントスキーマは何か?
このドキュメントには、新機能を実装する際に使用すべき簡略化された提案が含まれています。 そのため、リレーショナル DB に追加する正確な外部キー制約など、実装の詳細が省略されている場合があります。
依存関係の検索
ユーザーはプロジェクトの依存関係を次のような方法で検索したいと考えています:
- 特定のパッケージ名(パッケージタイプとバージョン番号を指定して検索を絞り込む場合もある)
- 特定のライセンスを使用するパッケージ
- 特定の脆弱性の影響を受けるパッケージ
検索のスコープは次のいずれかになります:
- 単一プロジェクト
- プロジェクトグループ/ネームスペース
- GitLab インスタンス
検索結果は、クエリに一致するパッケージと、それらに依存するプロジェクトのリストです。
最初のバージョンでは、検索はデフォルトブランチに限定されます。 後に、すべての保護ブランチまたはすべてのプロジェクトリリースに拡張することができます。
パッケージの検索
ユーザーはプロジェクトが使用しているパッケージバージョンを確認したいと考えています。 次のような方法でプロジェクトの依存関係を検索する場合があります:
- パッケージ名
- パッケージ名とバージョン番号
プロジェクトの依存関係を検索する際、ユーザーは非標準のパッケージ名を知らない場合があります。
たとえば、django を検索する際に、Django がその Python パッケージの標準名であることに気づかない場合があります。
また、django は依存関係ファイルでプロジェクトが使用する依存関係名である場合もあります。
パッケージ名、タイプ、バージョンでプロジェクトの依存関係を効率的にクエリするための DB テーブルが必要です。
次のカラムを持つ project_dependencies テーブルを使用できます:
- パッケージタイプの
smallintカラム(整数値は Rails バックエンドが解釈する) - パッケージ名の
varcharカラム - パッケージバージョンの
varcharカラム - パフォーマンスのためにこれらすべてのカラムを組み合わせた複合インデックス
ただし、パッケージ名とバージョンはプロジェクトの依存関係全体で繰り返されます:
- 一部のパッケージは人気があり、多くのプロジェクトがそれらに依存しているため、名前が繰り返されます。
- これは特定のバージョンにも当てはまり、名前とバージョンの両方が繰り返されます。
- 将来的には、検索がデフォルトブランチに限定されなくなり、同じプロジェクトのブランチが名前とバージョンを繰り返す可能性が高くなります。
ストレージを節約するため、プロジェクトの依存関係として使用されるパッケージを 2 つの別々のテーブルで管理します:
packagesはパッケージをtypeと標準的なnameで追跡するpackage_versionsはバージョンをpackage_idとversionで追跡する
CREATE TABLE packages AS (
id integer NOT NULL,
type smallint NOT NULL,
name varchar NOT NULL
);
CREATE TABLE package_versions AS (
id integer NOT NULL,
package_id integer NOT NULL,
version varchar NOT NULL
);
ストレージを削減するため、パッケージや特定のバージョンがプロジェクトの依存関係として使用されなくなった場合、これらのテーブルをクリーンアップできます。 これはプロジェクトの依存関係を更新する際、または定期的に実行されるバッチジョブで行うことができます。
packages と package_versions のレコードはスキャンジョブから来ており、信頼できないため、
対応する ActiveRecord モデルのデフォルトスコープはオブジェクトを返すべきではありません。
将来的には、これらのスコープは信頼できる公開パッケージレジストリに記載されているものと一致するすべてのパッケージとバージョンを返すことができます。
packages テーブルはパッケージのソースを追跡しないため、
同じレコードが公開パッケージ(メインレジストリから取得)またはプライベートパッケージを参照する可能性があります。
この衝突は、目標が正確なパッケージ追跡ではなく、ストレージへの影響を最小限に抑えた効率的なクエリを可能にすることであるため、問題ではありません。
DB に保存される標準名は、依存関係ファイルで使用されるパッケージ名と異なる場合があります。
Rails バックエンドは packages テーブルに保存する前に名前を正規化します。
検索クエリを処理する際、バックエンドは指定されたパッケージ名を正規化して
DB に保存されているものと比較できるようにします。
また、利便性のために検索を大文字と小文字を区別しないようにする場合もあります。
必要に応じて、依存関係ファイルで使用される正確なパッケージ名を依存関係スキャンレポートから取得して
検索結果に表示することができます。
マルチプロジェクトリポジトリをサポートするため、プロジェクトの依存関係が見つかった依存関係ファイルへのパスを追跡し、
検索結果でこれらのファイルを報告する必要があります。これは、client/Gemfile と server/Gemfile のように
同じ GitLab プロジェクトの異なるディレクトリで同じパッケージマネージャーが使用されるケースをサポートします。
また、ファイルパスはプロジェクト間で繰り返される可能性が高いため、
ストレージを削減するために別の DB テーブルに保存する必要があります。
同じプロジェクト内および複数のプロジェクト間で依存関係のファイルパスの繰り返しを避けるため、 2 つのテーブルを使用してプロジェクトの依存関係を追跡します:
dependency_filesは依存関係ファイルのパスを追跡する。project_dependenciesは依存関係ファイルで見つかったプロジェクトのパッケージバージョンを追跡する。
CREATE TABLE dependency_files AS (
id integer NOT NULL,
package_type smallint NOT NULL,
path varchar NOT NULL
);
CREATE TABLE project_dependencies AS (
id integer NOT NULL,
created_at timestamp without time zone NOT NULL,
dependency_file_id integer NOT NULL,
package_version_id integer NOT NULL
);
実際には、パッケージマネージャーは通常、プロジェクトの依存関係を追跡するために 2 つのファイルを使用することに注意してください:
編集可能な依存関係ファイルと生成されたロックファイルですが、
依存関係スキャンのアナライザーとそれらが使用するレポートフォーマットは 1 つしか許可していません。
その結果、ユーザーはデータベースに保存されていないパスを検索して結果が得られない場合があります。
これに対処するため、Rails バックエンドは要求されたファイルパスを変換するか、追加のパスを検索することができます。
たとえば、ユーザーが client/Gemfile を検索している場合に client/Gemfile.lock を検索することができます。
将来的には dependency_file_id 外部キーをリレーションテーブルに置き換えて
依存関係のセットに貢献するすべてのファイルを追跡するか、
セットを表す dependency_file_sets テーブルを導入することができます。
dependency_files テーブルを活用することで、バックエンドは
特定のファイルパスまたはパッケージタイプに一致するプロジェクトの依存関係を効率的に
リストアップすることができます。
dependency_files テーブルは packages と同様に自動的にクリーンアップできます。
dependency_files のレコードはスキャンジョブから来ており、信頼できないため、
DependencyFile ActiveRecord モデルのデフォルトスコープは、
パスがルートディレクトリで見つかるサポートされている依存関係ファイルと一致するオブジェクトのみを返すべきです:
Gemfile、Gemfile.lock、package.json など。
カスタムファイル名やサブディレクトリにある依存関係ファイルはデフォルトスコープから除外されます。
プロジェクトの依存関係は、デフォルトブランチの新しい成功したパイプラインが 新しい依存関係スキャンレポートを生成するときにアップサートされます。 バックエンドは次の順序でデータベーステーブルにアップサートします:
packagespackage_versionsdependency_filesproject_dependencies
スキャンジョブが再試行された場合、バックエンドはデータの整合性を確保するために 対応するパイプラインを新しいものとして再処理します。 次の手順を実行します:
- パイプラインの完了したジョブをすべてリストアップする。再試行されたジョブは除外される。
- これらのジョブの依存関係スキャンレポートを収集する。
- これらのレポートを処理して、アップサートを実行する。
プロジェクトの依存関係を追跡するためのすべての DB テーブル
(packages、package_versions、dependency_files、project_dependencies)は
プライマリ DB に保存されており、PostgreSQL を使用して多数のプロジェクトを持つネームスペース全体で
特定のパッケージを検索する際に効率的に結合することができます。
プロジェクトの依存関係を別のデータベースに移動すると、
多くのプロジェクト ID でのインメモリ結合が必要になります。
保護ブランチとリリースでの検索
ユーザーはすべての保護ブランチとリリースにわたってプロジェクトの依存関係を検索したいと考えています。
保護ブランチとプロジェクトリリースは通常次に対応するため、特に重要です:
- 開発中またはメンテナンスモードのバージョンブランチ
- デプロイされてアクティブに使用されているリリース
保護ブランチの名前は protected_branches.name カラムに保存され、
プロジェクトリリースに対応する git タグは releases.tag に保存されています。
(これらのカラムは GitLab の DB スキーマにすでに存在しています。)
ブランチ名と git タグはどちらも git ref であるため、git タグまたはブランチのいずれかになる「ref」を保存します。
同じプロジェクトブランチまたはリリースに属するすべての依存関係で同じ git ref が繰り返されるのを避けるため、
ref でグループ化された依存関係を持つ新しい project_dependency_sets テーブルを導入します。
CREATE TABLE project_dependency_sets (
id integer NOT NULL,
project_id integer NOT NULL,
ref character varying NOT NULL
);
プロジェクトの依存関係は依存関係セットに属するようになります。
ALTER TABLE project_dependencies ADD COLUMN project_dependency_set_id NOT NULL;
プロジェクトの依存関係はプロジェクトを参照する必要がなくなり、
スペースを節約するために project_id カラムが削除されます。
ALTER TABLE project_dependencies DROP COLUMN project_id;
この変更を行う際、最初に既存のプロジェクトのデフォルトブランチの依存関係セットを作成し、
次に既存の依存関係をこれらのセットに追加します。
その後でのみ project_dependency_set_id カラムに NOT NULL 制約を追加できます。
これはカラム作成時にはできません。
プロジェクトの依存関係のアップサートは、依存関係セットもアップサートされるように変更されます。
スキャンジョブへのナビゲート
ユーザーは特定のプロジェクト依存関係を報告した CI パイプラインまたは CI ジョブを調べたいと考えています。
プロジェクトの依存関係が検出された CI パイプラインを追跡するため、
同じブランチまたはリリースで検出された依存関係をグループ化するために使用される project_dependency_sets テーブルを活用することができます。
これにより、多くのプロジェクトの依存関係にわたって同じパイプライン ID が繰り返されるのを避けることができます。
ALTER TABLE project_dependency_sets ADD COLUMN pipeline_id;
pipeline_id は、特定の git ref のプロジェクトの依存関係をアップサートする際に設定または更新されます。
あるいは、「依存関係セット」の概念を変更して、セットが単一の CI ジョブで検出された依存関係をグループ化するようにすることもできます。 これはより正確であり、ユーザーは依存関係が検出された正確なジョブにリダイレクトされることができます。 また、CI ジョブが再試行された場合や対応する依存関係が更新された場合のシナリオをより良くサポートします。 アップサートメカニズムは、同じ ref に対応する複数のセットをアップサートするように変更する必要があります。 これを達成するには 2 つの方法があります:
project_dependency_sets.job_nameでジョブ名を追跡し、プロジェクト ID、ref、ジョブ名に対応する依存関係をアップサートする。dependency_file_idをproject_dependenciesテーブルからproject_dependency_setsに移動し、プロジェクト ID、ref、ファイル ID に対応する依存関係をアップサートする。
後者は CI への結合度が低く、ジョブで見つかったファイルではなくファイルをアップサートするシンプルなロジックになります。
ブランチが保護対象になると、そのブランチの最後の依存関係スキャンアーティファクトセット (最新のパイプラインからのもの)が処理され、プロジェクトの依存関係がリレーショナル DB に追加されます。 これにより、新しいパイプラインを待つことなく、新たに保護されたブランチのプロジェクト依存関係をクエリできます。
開発用依存関係の除外
ユーザーはプロジェクトの依存関係を検索する際に開発用依存関係を除外したいと考えています。 これらの依存関係が属する正確なスコープ(Maven)またはグループ(Bundler)には関心がありません。
開発用依存関係とは、プロジェクトまたはその依存関係を開発する場合にのみ使用されるパッケージです。 これには、プロジェクトやそれが使用するライブラリのテスト、リント、デバッグのためだけに使用されるパッケージが含まれます。
パッケージマネージャーとビルドツールは、依存関係がどのように使用されるかを追跡するために異なる概念を使用しています:
- npm と yarn は開発用依存関係とその他を区別する。
- Maven は依存関係を 5 つのスコープに分類し、各スコープには明確に定義されたセマンティクスがある。
- Bundler はユーザー定義の依存関係グループをサポートする。これらは命名規則であり、特定のグループのセマンティクスは不明な場合がある。
検索の目的のため、正確な依存関係グループ/スコープはブール値に変換され、
project_dependencies テーブルの development カラムに保存されます。
ALTER TABLE project_dependencies ADD COLUMN development boolean;
project_dependencies.development は、これが開発用依存関係かどうかを確認できない場合は NULL になります:
- アナライザーが処理するファイルがその情報を提供しない。
- 依存関係グループのセマンティクスが不明。
正確な依存関係グループまたはスコープは、必要に応じてレポートやアーティファクトから取得することができます。 将来的に、ユーザーが依存関係グループでプロジェクトの依存関係をフィルタリングする必要がある場合、 次の変更が必要になります:
- 依存関係グループの名前を繰り返さずに追跡する DB テーブルを作成する。グループには名前とパッケージタイプがあり、パッケージタイプのコンテキストで名前を解釈できるようにする。
project_dependenciesテーブルにdependency_group_id外部キーを追加して、バックエンドがプロジェクト依存関係のグループを追跡できるようにする。情報が利用できない場合、このカラムは NULL になる。- 新しい
dependency_group_idカラムで冗長になるため、既存のdevelopmentカラムを削除する。
CREATE TABLE dependency_groups AS (
id integer NOT NULL,
package_type smallint NOT NULL,
name varchar NOT NULL
);
ALTER TABLE project_dependencies ADD COLUMN dependency_group_id integer;
ALTER TABLE project_dependencies DROP COLUMN development;
現在、Gemnasium アナライザーはパッケージが開発用依存関係かどうかを追跡しません。 それらが解析するロックファイルやグラフエクスポートのほとんどはその情報を提供せず、 この情報は依存関係スキャンレポートに保存することができません。
特定のライセンスを使用する依存関係の検索
ユーザーは特定のライセンスを使用するプロジェクトの依存関係をリストアップしたいと考えています。
プロジェクトの依存関係のライセンスはすでに software_licenses テーブルで追跡されています。
現在は software_license_policies と組み合わせてポリシーを設定し、
プロジェクトのコンテキストでライセンスを許可または拒否するために使用されています。
software_licenses はプロジェクトの依存関係で検出されたライセンスを追跡するために再利用できます。
パッケージバージョンには複数のライセンスがある可能性があるため、リレーションテーブルが必要です。
CREATE TABLE project_dependency_software_licenses AS (
project_dependency_id integer NOT NULL,
software_license_id integer NOT NULL
);
このモデルは複合ライセンスを正確に追跡しないことに注意してください。 たとえば、「MIT and BSD」と「MIT or BSD」の区別をしません。 ライセンスに一致する依存関係を検索する場合、この区別は必要ないと仮定しています。
project_dependencies、project_dependency_software_licenses、software_license_policies を結合することで、
未承認のライセンスを持つ依存関係を効率的にリストアップすることができます。
ストレージを削減するため、プロジェクトの依存関係やライセンスポリシーで参照されなくなったライセンスは削除すべきです。 これは依存関係やポリシーの更新または削除時、または定期的なジョブでバッチモードで行うことができます。
公開パッケージに関する情報(ユーザーパイプラインの外で)、使用するライセンスを含む情報を収集し始めると、
project_dependency_software_licenses が他のテーブルに保存された情報を繰り返す可能性があります。
ただし、この冗長性は依存関係が名前が公開パッケージと一致するプライベートパッケージである場合の衝突リスクを排除します。
その特定のケースでは、プロジェクト依存関係のライセンスが対応する公開パッケージのライセンスと異なる場合があります。
特定の脆弱性の影響を受ける依存関係の検索
ユーザーは特定の脆弱性の影響を受けるプロジェクトの依存関係をリストアップしたいと考えています。
プロジェクトの依存関係は複数の脆弱性の影響を受ける可能性がありますが、 脆弱性は依存関係に属します。
脆弱性は vulnerability_occurrences テーブルで追跡されているため、
脆弱性ファインディングをプロジェクト依存関係にリンクするための project_dependency_id 外部キーを追加することができます。
このカラムを追加することで、脆弱性に一致するプロジェクトの依存関係を効率的に検索し、
依存関係ごとに脆弱性を効率的にグループ化することができます。
project_dependency_id は、脆弱性が依存関係スキャンによって報告された場合は NULL になります。
ALTER TABLE vulnerability_occurrences ADD COLUMN project_dependency_id;
ただし、この変更は、すべての脆弱性に使用される汎用テーブルを依存関係スキャン固有のテーブルにリンクします。
問題がある場合は、プロジェクトの脆弱性とプロジェクトの依存関係の両方をメモリにロードし、
脆弱性の location を活用して、リクエストされた脆弱性に一致しない依存関係を削除することができます。
バックエンドはすでにプロジェクトの脆弱性とプロジェクトの依存関係の両方をメモリにロードし、 依存関係リストを提供する際にそれらを結合して依存関係ごとに脆弱性をグループ化しています。 ただし、デフォルトブランチのプロジェクトの依存関係は依存関係スキャンレポートのコレクションからではなく、 リレーショナル DB からフェッチされます。
新たに追加された依存関係の検索
ユーザーはデフォルトブランチまたは保護ブランチに最近追加されたプロジェクトの依存関係をリストアップしたいと考えています。
特定のブランチで検出されたプロジェクトの依存関係は project_dependencies テーブルにアップサートされます。
created_at カラムはアップサート時に更新されないため、最近追加された依存関係をフィルタリングするために使用できます。
依存関係ポリシー
ユーザーは特定のパッケージがプロジェクトの依存関係として追加されるのを防ぎたいと考えています。 特定のパッケージバージョンを除外したり、パッケージ全体を除外したりする場合があります。
パッケージポリシーは、プロジェクト ID、パッケージ ID、分類、バージョン範囲を組み合わせた
project_dependency_policies リレーションテーブルを導入することで追跡されます。
このテーブルは、ライセンスポリシーを保存するために使用される既存の software_license_policies テーブルと同様です。
CREATE TABLE project_dependency_policies (
id integer NOT NULL,
project_id integer NOT NULL,
package_id integer NOT NULL,
classification integer DEFAULT 0 NOT NULL,
version_range varchar DEFAULT '*'::varchar NOT NULL
);
project_id と package_id に基づいてリレーショナル DB を活用してプロジェクトの依存関係と対応するポリシーを JOIN することができます。
ただし、SQL クエリによって返されたポリシーが一致しない場合があり、
Rails バックエンドは version_range を評価して、ポリシーが依存関係として使用されるパッケージバージョンに適用されるかどうかを確認する必要があります。
project_dependency_policies は package_policies とも呼べます。
これは既存の software_license_policies テーブルとの一貫性が高まりますが、
パッケージがプロジェクトの依存関係として使用されることが伝わりにくくなります。
依存関係の監査
ユーザーは特定のパッケージがいつプロジェクトの依存関係として追加されたかを知りたいと考えています。
特定のブランチで検出されたプロジェクトの依存関係は project_dependencies テーブルにアップサートされます。
created_at カラムはアップサート時に更新されないため、依存関係が最初に検出されたときを反映します。
プロジェクトの依存関係が最初に導入された正確なコミットを知る必要がある場合は、
そのコミットの SHA を保存するための新しいカラムが project_dependencies テーブルに必要です。
ALTER TABLE project_dependencies ADD COLUMN first_commit_sha bytea NOT NULL;
最初のコミットは、プロジェクトの依存関係が最初に検出されたパイプラインの ID を保存することでも取得できます。ただし、CI データベースにアクセスする必要があり、パフォーマンス上の理由からそれは避けるべきです。
依存関係の変更を追跡する
MR をレビューする際、ユーザーはプロジェクトの依存関係の変更をレビューしたいと考えています。
Rails バックエンドはソースブランチとターゲットブランチ間のプロジェクトの依存関係を比較し、 MR ページに表示される差分を準備します。 これは、新たに導入された脆弱性と修正された脆弱性が MR でどのように表示されるかに類似しています。
比較されるプロジェクトの依存関係は異なるデータソースからフェッチすることができ、 Rails バックエンドは利用可能な最速のオプションを使用します:
- リレーショナル DB からフェッチする(プロジェクトの依存関係がある場合)
- それが利用できない場合は内部 Software of Bill of Materials(SBoM)からロードする
- それも利用できない場合は依存関係スキャンレポートのコレクションからロードする
プロジェクトの依存関係はすべて同じインターフェースに応答するため、データソースに関わらず同じ方法で比較できます。
内部 SBoM(サポートされている場合)は、差分を計算する際に中間キャッシュとして機能しながら動的に作成できます。
差分自体も、MR ページの読み込みを高速化するためにキャッシュすることができます。
依存関係の混同保護
ユーザーは依存関係の混同攻撃の可能性について警告を受けたいと考えています。 これらはプロジェクトのセキュリティダッシュボードにリストされた脆弱性ファインディングとして報告される必要があります。
パッケージチェックサムの変更を報告する
ユーザーはパッケージ名、パッケージバージョン、パッケージソース(追跡されている場合)が変更されていないにもかかわらず、 プロジェクトの依存関係のチェックサムが変更された場合に警告を受けたいと考えています。
パッケージのチェックサムは、依存関係スキャンレポートの一部として依存関係スキャンのアナライザーによって報告される必要があります。
チェックサムが Rails バックエンドで利用可能になったら、project_dependencies テーブルに保存できます。
ALTER TABLE project_dependencies ADD COLUMN checksum varchar;
CI パイプラインが完了したときにプロジェクトの依存関係をアップサートする前に、
バックエンドは project_dependencies テーブルから現在のチェックサムをフェッチし、
メモリ内にある(かつアップサートされようとしている)対応する依存関係のチェックサムと比較します。
次に、新しいチェックサムが古いものと一致しないプロジェクトの依存関係ごとに脆弱性ファインディングを作成し、
最終的にアップサートを続行します。
セキュリティチェックは、アップサートに影響を与えないように別のジョブで処理できます。
チェックサムは package_versions テーブルに保存できません:
- パッケージバージョンには複数のディストリビューションがあり、各ディストリビューションには複数のチェックサムがある。
- パッケージディストリビューションには、MD5 や SHA1 など異なるハッシュアルゴリズムに対応する複数のチェックサムがある。
packagesとpackages_versionsはパッケージソースを追跡しないため、公開パッケージとプライベートパッケージの衝突が発生する可能性がある。
バックエンドは単純にチェックサムを比較するため、project_dependencies がパッケージソースを追跡しないことは問題になりません。
パッケージソースの変更を報告する
ユーザーはプロジェクトの依存関係のソースが変更された場合に警告を受けたいと考えています。 特に、パッケージがプライベートレジストリからフェッチされていたものが、 公開レジストリからフェッチされるようになった場合に通知を受けたいと考えています。
パッケージのソースは、依存関係スキャンレポートの一部として依存関係スキャンのアナライザーによって報告される必要があります。
ソースが Rails バックエンドで利用可能になったら、新しい package_sources テーブルにその URL を保存して
プロジェクトの依存関係全体で再利用できるようにします。
CREATE TABLE package_sources AS (
id integer NOT NULL,
url varchar NOT NULL
);
依存関係がフェッチされたソースを追跡するための新しい package_source_id カラムが追加されます。
このカラムはアナライザーがパッケージソースを報告できない場合があるため NULL になる可能性があります。
たとえば、アナライザーがこの情報を提供しないロックファイルを解析する場合があります。
ALTER TABLE project_dependencies ADD COLUMN package_source_id;
パッケージやパッケージバージョンと同様に、パッケージソースは完了したパイプラインのプロジェクトの依存関係をアップサートする前にアップサートされます。
CI パイプラインが完了したときにプロジェクトの依存関係をアップサートする前に、
バックエンドは project_dependencies テーブルから現在のパッケージソース ID をフェッチし、
メモリ内の対応する依存関係のソース ID と比較します。
次に、ソースが変更されようとしているプロジェクトの依存関係ごとに脆弱性ファインディングを作成し、
最終的にアップサートを続行します。
パッケージチェックサムの不一致を報告する
ユーザーはプロジェクトの依存関係が公開パッケージと一致するが、 依存関係のチェックサムがその公開パッケージのいかなるディストリビューションとも一致しない場合に警告を受けたいと考えています。
パイプラインが完了した後にプロジェクトの依存関係をアップサートする際、 バックエンドはプロジェクトに追加された依存関係を収集し、 パッケージメタデータ DB を使用してこれらの依存関係に一致する公開パッケージバージョンのメタデータを収集します。 次に依存関係を反復処理し、依存関係のチェックサムが対応するパッケージバージョンのいかなるチェックサムとも一致しない場合に 脆弱性ファインディングを作成します。
バックエンドによって作成された新しい脆弱性ファインディングは、一部の場合に誤検知になります:
- 依存関係が名前とバージョンが公開パッケージと一致するプライベートパッケージである。
- 依存関係がパッケージメタデータ DB で追跡されていないパッケージディストリビューションまたは代替チェックサムを使用している。
パッケージソースが project_dependencies テーブルで追跡されている場合、
これを活用してチェックサムを比較する際にプライベートパッケージを除外できます。
パッケージメタデータ
パッケージメタデータには通常、パッケージ名、目的の説明、作者リスト、依存関係リストなどが含まれます。
パッケージメタデータはインストール可能なパッケージ自体に含まれるか、別のファイルに保持されます。 また、パッケージをホストしているレジストリによって繰り返される場合があり、レジストリ API を使用してフェッチできます。
パッケージレジストリが提供するもの:
- バージョンメタデータ: 特定のパッケージバージョンに固有のもの
- パッケージメタデータ: すべてのバージョンに適用されるもの
たとえば、rubygems.org API は gem メソッド と gem バージョンメソッド の両方を提供しています。
ほとんどの場合、バージョンメタデータは不変ですが、パッケージメタデータはそうではありません。 たとえば、パッケージの説明、作者、ライセンスは時間とともに変更される場合がありますが、 これらのメタデータは特定のパッケージバージョンについては変更されません。
トラッキングサービス
公開パッケージを追跡し、特定のパッケージメタデータを検索し、 これらのメタデータが変更された際に通知を投稿する新しいサービスを導入します。 パッケージレジストリに応じて、トラッキングサービスはレジストリ API をポーリングするか、イベントフィードをリッスンします。
シンプルな設計では、サービスは関連するメタデータフィールドをリレーショナル DB で追跡できます。
- パッケージが初めて確認されると、追跡されているフィールドが DB テーブルに保存されます。
- 同じパッケージの新しいメタデータが利用可能になると、これらのメタデータが DB のものと比較されます。
- 新しい値が古い値と一致しない場合、通知が投稿され、DB レコードがアップサートされます。
新しいフィールドを追跡することを決定した場合、パッケージが既に DB で追跡されていても、 そのフィールドの変更を検出するために 2 つのメタデータ更新が必要になります。 この制限を解消するため、サービスはパッケージレジストリからの生のメタデータをドキュメントデータベース(オブジェクトストレージ)に保存することができます。 新しいフィールドを追跡することを決定したら、ドキュメント DB に保存された既存のメタデータを処理して、 そのフィールドに対応する新しい DB カラムを埋めることができます。
サービスは、パッケージメタデータとバージョンメタデータの変化を追跡するためにそれぞれ 2 つの DB テーブルを使用する場合があります。
これらのテーブルは GitLab バックエンドの packages と package_versions テーブルに類似したものになります。
ただし、バックエンドはプロジェクトの依存関係として使用されるパッケージとバージョンのみを追跡し、
特定の依存関係を検索するために必要な情報のみを保存します。
GitLab バックエンドはトラッキングサービスによって投稿された通知をリッスンし、 更新されたパッケージに依存するプロジェクトに対して何らかのアクションを実行して反応します。 たとえば、更新されたパッケージを使用するすべてのプロジェクトに脆弱性ファインディングを作成することで反応する場合があります。
トラッキングサービスはまた、パッケージメタデータから抽出するフィールドをフェッチするための API エンドポイントを公開し、 追跡するパッケージレジストリのプロキシとして機能します。
パイプライン外でのライセンススキャン
ユーザーは CI パイプラインを実行せずにプロジェクトの依存関係が使用するライセンスを追跡したいと考えています。 パイプライン外でのライセンススキャンには、プライベートパッケージが使用するライセンスを追跡できないなどの制限があることを理解しています。
パイプライン外でライセンススキャンを実行するため、
バックエンドはプロジェクトの依存関係からパッケージタイプ、名前、バージョンを抽出し、
パッケージトラッキングサービス API を使用して対応するライセンスを収集します。
次に software_licenses にライセンスをアップサートし、
見つかったライセンスを反映するために project_dependency_licenses にレコードを挿入します。
パイプライン外でのライセンススキャンは、プロジェクトの依存関係がトラッキングサービスによって追跡される公開パッケージと タイプ、名前、バージョンが一致するプライベートパッケージである場合、不正確になる可能性があります。 プロジェクトの依存関係のパッケージソースがリレーショナル DB で追跡されている場合、 バックエンドはこの情報を活用してスキャンからプライベートパッケージを除外できます。
バックエンドは、スキャンされているブランチが project_dependencies テーブルで追跡されている場合、
リレーショナル DB を活用してプロジェクトの依存関係をリストアップすることができます。
そうでない場合は、スキャンされているコミットのプロジェクトの依存関係をリストアップする
依存関係スキャンレポートや他のドキュメントを使用します。
パッケージ更新に関する通知
ユーザーは自分のプロジェクトが依存するパッケージに関する重要なイベントについて通知を受けたいと考えています。 特に、次のイベントについて通知を受けたいと考えています:
- 新しいメジャーバージョンが公開された。
- 使用しているメジャーバージョンの新しいマイナーバージョンが公開された。
- 使用しているマイナーバージョンの新しいパッチバージョンが公開された。
- 使用しているメジャーバージョンがサポート終了になった。
- ソフトウェアライセンスが変更された。
- パッケージのメンテナーが変更された。
- アップストリームプロジェクトのメンテナーが変更された。
これらのイベントのいずれかが発生すると、トラッキングサービスは GitLab バックエンドに通知し、 バックエンドは更新されたパッケージを使用するすべてのプロジェクトをリストアップして、 これらのユーザー通知を作成します。
イベントに応じて、バックエンドはパッケージ更新の影響を受けないプロジェクトを除外する場合があります。 たとえば、プロジェクトがパッケージのバージョン 2.0.0 を使用している場合、 新しいマイナーバージョン 1.2.3 の通知を作成しません。
更新されたパッケージに依存するすべてのプロジェクトの通知を作成するのは長いプロセスであるため、 このタスクは複数の非同期ジョブに分割される場合があります。 たとえば、ネームスペースごとおよび/またはプロジェクトごとに 1 つのジョブがある場合があります。
通知メッセージとユーザーへの配信方法は今後定義されます。
ヤンクされたパッケージに関する警告
ユーザーは自分のプロジェクトが依存するパッケージがパッケージレジストリからヤンクされた場合に警告を受けたいと考えています。 この警告はセキュリティダッシュボードにリストされた新しい脆弱性ファインディングとして表示されます。
パッケージがパッケージレジストリからヤンクされると、トラッキングサービスは GitLab バックエンドに通知し、 バックエンドは同じパッケージタイプと名前に依存するすべてのプロジェクトに脆弱性ファインディングを作成します。 同様に、ヤンクされたパッケージバージョンについて通知を受けた場合、 バックエンドはまったく同じパッケージタイプ、名前、バージョンに依存するプロジェクトに脆弱性ファインディングを作成します。
ヤンクされたパッケージに依存するすべてのプロジェクトの脆弱性ファインディングを作成するのは長いプロセスであるため、 このタスクは複数の非同期ジョブに分割される場合があります。 たとえば、ネームスペースごとおよび/またはプロジェクトごとに 1 つのジョブがある場合があります。
バックエンドによって作成された脆弱性ファインディングは、 公開レジストリからヤンクされたパッケージまたはバージョンが プロジェクトが依存するプライベートパッケージと一致する場合、誤検知になる可能性があります。 ただし、プライベートパッケージは公開レジストリをミラーリングするレジストリからフェッチされる場合があるため、 ファインディングはまだ関連性がある場合があります。
パッケージスコア
一部のサービスは、パッケージの品質と健全性を測定するスコアなど、公開パッケージのメトリクスを提供しています。 パッケージレジストリと同様に、トラッキングサービスを使用して追跡できるパッケージメタデータを提供します。
GitLab は既存のパッケージスコアリングサービスに依存するか、独自のスコアリングサービスを作成することができます。 どちらの場合も、トラッキングサービスはまったく同じ方法でバックエンドにスコアの変化を通知します。
スコアが下がった場合の警告
ユーザーはプロジェクトの依存関係として使用するパッケージのスコアが下がった場合に通知を受けたいと考えています。
パッケージスコアが下がると(たとえば B から C へ)、トラッキングサービスは GitLab バックエンドに通知し、 バックエンドは更新されたパッケージを使用するすべてのプロジェクトをリストアップして、 これらのユーザー通知を作成します。 これはパッケージレジストリでパッケージのメタデータが変更された際の通知送信に類似しています。
セキュリティアドバイザリー
セキュリティアドバイザリーは、特定のバージョンのパッケージに適用される脆弱性を説明します。 通常のパッケージメタデータとは異なり、特定のパッケージバージョンに関連するセキュリティアドバイザリーは変更されます:
- 新しいセキュリティアドバイザリーはいつでもパッケージバージョンに影響を与える可能性があります。
- 既存のアドバイザリーが更新される場合があります。たとえば、CVSS スコアが変更される場合があります。
- 既存のアドバイザリーが削除される場合があります(そのバージョンに影響しなくなったため)。
セキュリティアドバイザリーは、GitLab が管理する脆弱性データベースである GitLab Advisory Database や、 ruby-advisory-db などの他の脆弱性データベースによって提供されます。
脆弱性データベースの追跡
複数の脆弱性データベースをサポートするため、 これらのデータベースを追跡し、2 種類のイベントについてリスナーに通知する新しいサービスを導入します:
- セキュリティアドバイザリーが脆弱性データベースに追加された。
- 既存のアドバイザリーの重要なフィールドが変更された。
トラッキングサービスはまた、脆弱性データベースのプロキシとして機能します: タイプ、名前、バージョンで識別されるパッケージのセキュリティアドバイザリーを返す API エンドポイントを公開します。
GitLab が自社の脆弱性データベースのみを使用する場合、2 つの可能なアーキテクチャがあります:
- トラッキングサービスは gemnasium-db git リポジトリの上に構築され、 YAML ファイルが追加または変更されたときにリアルタイムで反応するためにウェブフックを使用します。
- gemnasium-db はセキュリティアドバイザリーをホストするサービスに置き換えられます。 それはトラッキングサービスのように動作し、新しいアドバイザリーとアドバイザリーの更新についてリスナーに通知します。
パイプライン外での依存関係スキャン
ユーザーは CI パイプラインを実行せずにプロジェクトの依存関係をスキャンしたいと考えています。 ロックファイルがないプロジェクトをスキャンできないなどの制限があることを理解しています。
パイプライン外で実行する場合、依存関係スキャンはプロジェクトの依存関係をリストアップするレポートに依存します。 これらのレポートは既存の依存関係スキャンレポートまたは何らかの SBoM アーティファクトになる可能性があります。 将来的には、Rails バックエンドがロックファイルや依存関係エクスポートを直接処理して、 プロジェクトの依存関係をリストアップする CI ジョブの必要性を排除できます。
パイプライン外で依存関係スキャンを実行するため、バックエンドは 4 つの手順を踏みます:
- スキャンされるプロジェクトの依存関係からパッケージタイプ、名前、バージョンを抽出する
- アドバイザリーデータベース API またはトラッキングサービス API に接続して、パッケージタイプと名前に一致するアドバイザリーを収集する
- 各アドバイザリーの影響を受けるバージョン範囲を評価する。範囲外のバージョンを使用する依存関係は影響を受けない
- 影響を受ける各依存関係の脆弱性ファインディングを作成する
脆弱性ファインディングを作成する際、バックエンドは次のものを組み合わせます:
- 影響を受ける依存関係に固有の情報(パッケージバージョンや依存関係が宣言されているファイルなど)
- セキュリティアドバイザリーからの汎用情報(セキュリティ上の欠陥の説明、CVE ID、CVSS ベクターなど)
バックエンドと CI ジョブの両方で依存関係スキャンを実行できる混在環境では、
バックエンドは脆弱性ファインディングのフィンガープリントを設定して、
スキャンジョブによって報告された同じ脆弱性のフィンガープリントと一致するようにする必要があります。
フィンガープリントが一致しない場合、依存関係スキャンジョブを含むパイプラインが完了した後に、
バックエンドによって検出された脆弱性が no longer detected としてマークされます。
パイプライン外で依存関係スキャンを実行すると、プロジェクトの依存関係が セキュリティアドバイザリーが存在する公開パッケージとタイプと名前が一致するプライベートパッケージである場合、 誤検知が発生する可能性があります。 ただし、このプライベートパッケージは公開レジストリをミラーリングするレジストリからフェッチされる場合があるため、 脆弱性ファインディングはまだ関連性がある場合があります。
CI での実行時、依存関係スキャンはポスト分析ツールの恩恵を受けることができます。 これらはクロスチェックの依存関係スキャンレポートと他のレポートを照合して誤検知にフラグを立てます。 その場合、パイプライン外で実行する際には依存関係スキャンはこれらのポスト分析ツールの恩恵を受けられず、 誤検知を報告する可能性があります。
バックエンドは、スキャンされているブランチが project_dependencies テーブルで追跡されている場合、
リレーショナル DB を活用してプロジェクトの依存関係をリストアップすることができます。
そうでない場合は、スキャンされているコミットのプロジェクトの依存関係をリストアップする
依存関係スキャンレポートや他のドキュメントを使用します。
パイプライン外で実行する場合、依存関係スキャンは CI での実行時にサポートするすべてのフィルターをサポートする必要があります。 特に、依存関係スキャン CI ジョブでサポートされている場合は、スキャンから開発用依存関係を除外できる必要があります。 今日のスキャンフィルターは CI 変数を使用して設定されており、これらは Rails バックエンドで直接利用できないため、 スキャンフィルター設定をプロジェクト設定に移動する可能性があります。
新しいセキュリティアドバイザリーに関する警告
ユーザーは新しい CI パイプラインを実行せずに、プロジェクトの依存関係に影響する新しいセキュリティアドバイザリーについて警告を受けたいと考えています。 通知を受け取り、セキュリティダッシュボードに新しい脆弱性ファインディングが表示されます。 このファインディングは依存関係スキャン CI ジョブによって報告されるものほど正確でない場合があり、 誤検知の可能性があることを理解しています。
新しいセキュリティアドバイザリーが脆弱性データベースに追加されると、GitLab バックエンドに通知され、4 つの手順で対応します:
- パッケージタイプと名前に基づいて、影響を受けるパッケージを依存関係として使用するすべてのプロジェクトをリストアップする。
- 影響を受けるバージョンの範囲を評価し、影響を受けないパッケージバージョンを使用するプロジェクトを除外する。
- 残りのすべてのプロジェクトに脆弱性ファインディングを作成する。
- 最後に、これらの新しい脆弱性に対するユーザー通知を作成する。
影響を受けるすべてのプロジェクトの脆弱性ファインディングとユーザー通知を作成するのは長いプロセスであるため、 このタスクは複数の非同期ジョブに分割される場合があります。 たとえば、影響を受けるパッケージに依存するプロジェクトごとに 1 つのジョブがある場合があります。 ジョブは次の手順に従います:
- 影響を受けるバージョン範囲を評価し、プロジェクトが影響を受けないバージョンに依存している場合は終了する
- 影響を受けるバージョン範囲に一致するすべてのプロジェクト依存関係の脆弱性ファインディングを作成する
- そのプロジェクトの通知が有効なすべてのユーザーの通知を作成する
バックエンドはリレーショナル DB を使用して、新たに影響を受けるパッケージに一致するすべてのプロジェクト依存関係を効率的にリストアップします。
更新されたセキュリティアドバイザリーに関する通知
ユーザーはプロジェクトの依存関係に影響するセキュリティアドバイザリーが変更された場合に通知を受けたいと考えています。 特に、CVSS スコアが変更された場合や新しい修正が利用可能になった場合を知りたいと考えています。
新しいセキュリティアドバイザリーが更新されると、GitLab バックエンドはその変更について通知を受け、 セキュリティアドバイザリーの ID と一致する主要な脆弱性識別子を持つすべてのプロジェクトをリストアップします。 次に、一致するすべてのプロジェクトにこの変更に関するユーザー通知を作成します。
依存関係スキャンの脆弱性ファインディングの主要識別子は、CVE ID などのセキュリティアドバイザリーの ID であると仮定しています。
この識別子は vulnerability_occurrences テーブルの既存の primary_identifier_id カラムで追跡されています。
更新されたアドバイザリーの影響を受けるプロジェクトを見つけるため、
バックエンドは projects、vulnerability_occurrences、vulnerability_identifiers を結合する SQL クエリを実行します。
後者の 2 つのテーブルは vulnerability_occurrences.primary_identifier_id で結合されます。
アドバイザリーに CVE ID がある場合、クエリは vulnerability_identifiers.external_type が cve の識別子と
vulnerability_identifiers.external_type が CVE ID と等しい識別子でフィルタリングされます。
一致するプロジェクトをリストアップするクエリは vulnerability_occurrences.primary_identifier の既存のインデックスを使用します。
効率的にするため、vulnerability_identifiers.external_type と vulnerability_identifiers.external_name の複合インデックスも必要です。
ソフトウェア部品表
Software Bill of Materials(SBoM)は、ソフトウェアの構成要素をリストアップするドキュメントです。 構成要素は URL、座標のセット、またはその他の種類の ID を使用して識別されます。 構成要素には、作者リスト、URL、ライセンス、セキュリティアドバイザリーなどの追加メタデータがある場合があります。 ドキュメントには通常、SBoM が作成されたソフトウェアと SBoM の作成に使用されたツールの両方が記述されています。
このドキュメントでは、SBoM がコンテナスキャンによって検出されたシステムレベルのパッケージや 手動で追跡されスキャナーによって検出されないコンポーネントもリストアップできますが、 依存関係スキャンとライセンススキャンによって検出されたアプリケーションレベルのパッケージに焦点を当てています。
SBoM にはさまざまなフォーマットがありますが、このドキュメントは CycloneDX に焦点を当てています。 これは包括的で、拡張可能であり、オープンソースライブラリと自動化ツールによってよくサポートされているためです。
CycloneDX
CycloneDX はプロジェクトの依存関係として使用されるコンポーネントのインベントリに使用できます。 CycloneDX コンポーネントは SWID、CPE、または Package URL(PURL)を使用して識別できます。 gemnasium-db 脆弱性データベースのパッケージスラグと同様に、 PURL はパッケージタイプ、完全なパッケージ名、パッケージバージョンを組み合わせています。 PURL は非デフォルトのパッケージレジストリやリポジトリもサポートしています。
コンポーネントをリンクして依存関係グラフを記述することができます。 依存関係は推移的(コンポーネントの依存関係)または直接的(ソフトウェア自体の依存関係)のいずれかになります。 依存関係にはメタデータがなく、開発用依存関係と本番用依存関係を区別することはできません。 ただし、CycloneDX フォーマットを拡張することで依存関係メタデータをサポートできます。
コンポーネントは複数のライセンスを持つことができます。 ライセンスは名前、URL、SPDX 識別子、ライセンス自体を引用することで記述できます。 SPDX 式もサポートされています。
コンポーネントは外部 URL を持つことができ、 各 URL は複数のチェックサムまたは「ハッシュ」を提供できます。 これはパッケージディストリビューションの署名を保存するために使用できます。
SBoM メタデータは、インベントリが作成されたソフトウェアを記述することができます。 Gradle マルチプロジェクトビルドのような複数のサブプロジェクトで構成される複合ソフトウェアを記述することはできないようです。 ただし、CycloneDX を使用して親プロジェクトとそのサブプロジェクトを記述する方法は少なくとも 2 つあります:
- サブプロジェクトを親プロジェクトの「コンポーネント」として記述する
- 複数の CycloneDX ドキュメントを作成する
CycloneDX は、インベントリを作成するために使用される依存関係ファイルを追跡しません。 その結果、SBoM はインベントリにリストされたコンポーネントを導入するファイルへのユーザーリダイレクトに使用できません。 ただし、この制限を回避する方法があります:
- グローバルメタデータ プロパティ を使用して、依存関係が検出された依存関係ファイル(オプションのロックファイル)を追跡する。ただし、同じタイプの複数のファイルを追跡することはできません。
- コンポーネント プロパティ を使用して、各依存関係が検出された依存関係ファイル(オプションのロックファイル)を追跡する。これは冗長性を導入しますが、より柔軟です。
- 依存関係ファイルを正確に記述し、それらがコンポーネントとどのように関係するかを記述する CycloneDX 拡張を作成する。
脆弱性拡張は、 SBoM に脆弱性を埋め込み、コンポーネントにリンクするために使用できます。 脆弱性は次のものを持つことができます:
- CVE 識別子
- 説明
- ソース名(NVD など)と URL
- CVSS ベクター、ベーススコアなど
- CWE 識別子
- アドバイザリー URL
- 修正
脆弱性は 1 つの識別子しか持てず、これは CVE ID でなければならないようです。要確認。 また、脆弱性の短い説明を提供することはできないようです。
内部 SBoM
内部 SBoM(ISBoM)は、プロジェクトのコミットのすべての依存関係を記述するドキュメントまたはドキュメントのセットです。 SBoM エクスポートの生成、依存関係リストのレンダリング、依存関係の比較、および古いコミットでの新しい依存関係スキャンの実行に必要なすべてのデータを含む必要があります。
ISBoM の主な目的は、包括的で拡張可能なファイルフォーマットを導入することでメンテナンスコストを削減することです。 これにより、さまざまなフォーマットとフォーマットバージョンのアダプターを維持する必要がなくなります。 新しいバージョンの Rails バックエンドは古いバージョンの ISBoM との後方互換性を維持し、 新しい CI パイプラインを実行せずに古い git コミットのための新しい SBoM をエクスポートすることが可能になります。 その目標を達成するため、ISBoM は多くの情報を表しているため、リレーショナル DB ではなくオブジェクトストレージにファイル(またはファイル)として保存されます。 また、ファイルパスまたは URL は予測可能であるべきで、リレーショナル DB に大量の情報を保存せずに ISBoM を取得できるようにします。 そのため、ISBoM は次のいずれかになります:
- 複数の依存関係ファイルを追跡し、マルチプロジェクトリポジトリを記述できる単一の SBoM ドキュメント
- アーカイブにまとめられた SBoM ドキュメントのコレクション
副次的な目標は次のとおりです:
- 高速な解析: プロジェクトの依存関係は、グラフ情報の有無にかかわらず ISBoM から効率的に抽出されます。ISBoM 全体を解析してメモリにロードせずに、特定の依存関係ファイルに対応するプロジェクトの依存関係を抽出できる必要があります。
- 効率的なストレージ: ISBoM は、アクセスされる可能性が低くなった場合に圧縮されます。
長続きするドキュメントであるため、ISBoM には不変のデータのみを含める必要があります。 特に、ライセンス情報を含めることができますが、プロジェクトの依存関係で検出された脆弱性は含めるべきではありません。 時間とともに脆弱性は変化し、新しい脆弱性が追加される可能性があり、結果として脆弱性リストが不正確または不完全になります。
ISBoM は主に SBoM エクスポートの作成に使用されますが、プロジェクトの依存関係がリレーショナル DB で追跡されていない場合に 他の機能をサポートするためにも使用できます:
- MR のソースブランチとターゲットブランチ間の依存関係の比較
- 依存関係リストのレンダリング
- 依存関係リストでの依存関係パスまたは依存関係グラフの表示
- 新しい脆弱性の発見
プロジェクトの依存関係に関する基本情報のみが必要な機能の場合、 ISBoM は処理されているコミットのリレーショナル DB でプロジェクトの依存関係が利用できない場合のフォールバックとして使用できます。 (DB テーブルからこれらの情報を取得するほうが速いと仮定されています。) この場合、ISBoM はこれらのテーブルをキャッシュとして後で使用できるように DB テーブルを埋めるために使用できます。
スキャンジョブが SBoM のエクスポートに必要な情報を提供しない場合、 この情報は外部サービスから取得して ISBoM に追加する必要があります。 たとえば、SBoM エクスポートにライセンス情報や作者リストが必要な場合、 直接またはトラッキングサービス経由でパッケージをホストするレジストリまたはリポジトリからフェッチする必要があります。 これは ISBoM の作成を複雑にしますが、ISBoM が完全であり直接使用できることを保証します。
SBoM をライセンススキャンアーティファクトとして使用する
CycloneDX はライセンススキャン(LS)レポートフォーマットのスーパーセットです:
- パッケージマネージャー、パッケージ名、バージョン(LS)を組み合わせると Package URL(CycloneDX)に変換できます。
- ライセンス ID(LS)は SPDX ライセンスリスト と一致する場合、SPDX ID(CycloneDX)として使用できます。
- ライセンスは両方のフォーマットで名前と URL を持つことができます。
CycloneDX と同様に、LS レポートはプロジェクトの依存関係を導入する依存関係ファイルや マルチプロジェクトビルド/リポジトリのサブプロジェクトを追跡しません。
その結果、既存のライセンススキャンジョブを変更して CycloneDX ドキュメントを生成することができます。 逆に、Rails バックエンドを変更して既存の LS レポートの代わりに CycloneDX ドキュメントを使用するか、 両方のフォーマットをサポートすることができます。 ただし、バックエンドがライセンススキャン機能を実装するために必要なフィールドは CycloneDX ではオプションであり、 バックエンドは不完全なドキュメントを処理できる必要があります。
SBoM を依存関係スキャンアーティファクトとして使用する
CycloneDX は依存関係スキャン(DS)レポートフォーマットのスーパーセットではありません:
- 依存関係を導入する依存関係ファイルを直接または間接的に追跡できない。
- マルチプロジェクト Gradle ビルドのような複合プロジェクトを記述できない。
- 脆弱性は短い説明(脆弱性名)を持てない。
- 脆弱性は代替識別子を持てず、CVE ID のみがサポートされているようです。
ただし、CycloneDX を拡張することでこれらの制限を取り除くことができます。 (マルチプロジェクトビルドとリポジトリを正確に記述するために複数の CycloneDX ドキュメントを生成する必要がある場合があります。)
その結果、既存の依存関係スキャンジョブを変更して CycloneDX ドキュメントを生成することができますが、 ギャップを埋める CycloneDX 拡張を書くまで、これらのドキュメントは元の DS レポートほど正確ではありません。
Rails バックエンドを変更して既存の DS レポートの代わりに CycloneDX ドキュメントを使用することができますが、 CycloneDX を拡張して SBoM が依存関係ファイルを正確に追跡するようになるまでは変更できません。 短い説明と代替識別子がない脆弱性をサポートするようにバックエンドを変更する場合もあります。これは手頃なように思えます。 ただし、バックエンドは依存関係リストのレンダリングと脆弱性の追跡において依存関係ファイルパスに大きく依存しています。
SBoM エクスポート
ユーザーは CycloneDX などの自分が使用するフォーマットでプロジェクトの SBoM をエクスポートしたいと考えています。 プロジェクトグループ、ワークスペース、またはインスタンスのすべてのプロジェクトをカバーする SBoM が必要な場合があります。 SBoM の生成に大幅な時間がかかる場合、ユーザーは準備ができたときに通知を受ける必要があります。 プロジェクト設定とグループ設定に応じて、SBoM エクスポートは次のタイミングで作成されます:
- ユーザーのリクエストにより、任意のプロジェクトコミットに対して
- プロジェクトリリースが作成されるたびに
- 定期的に、すべての関連ブランチに対して
技術的に、プロジェクトのコミットの SBoM エクスポートは次のいずれかから作成できます:
- 最新の成功したパイプラインのすべての依存関係スキャンとライセンススキャンレポート
- コミットに対応する内部 SBoM
ISBoM を入力として使用することには複数の利点があります:
- CI データベースに CI アーティファクトをリストアップするためのクエリが不要です。
- オブジェクトストレージから取得するファイルが 1 つだけです。
- ISBoM フォーマットは、それを構築するために使用されるレポートと比較してより安定しています。
- ISBoM は SBoM エクスポートに必要なパッケージメタデータをすでに提供しています。
- 特に多数のプロジェクトを処理する場合、全体的に高速です。
SBoM エクスポートは、アーカイブに組み合わされた複数の SBoM ドキュメントで構成される場合があります。
- エクスポートは、プロジェクトグループまたはワークスペースのすべてのプロジェクトをカバーする場合があり、複数の SBoM ドキュメントが生成されます。
- GitLab プロジェクトはマルチプロジェクトリポジトリである場合があり、SBoM フォーマットによっては完全なインベントリを作成するために複数の SBoM ドキュメントを作成する必要があります。
エクスポートが複数の SBoM ドキュメントで構成される場合、 プロジェクト設定またはグループ設定に応じて、tgz アーカイブまたは zip アーカイブとして利用可能です。 圧縮アーカイブはダウンロードに便利であり、ストレージへのプラスの影響もあります。
シンプルさのために、オンデマンドエクスポートは単一のプロジェクトに限定して エクスポートの生成に数秒しかかからないようにすることができます。 十分に速い場合、このタスクは同期的に処理され、 UI が簡素化され、専用の Rails ワーカーが不要になります。
プロジェクトグループまたはワークスペースの SBoM エクスポートは同期的なオンデマンドリクエストで実行するには時間がかかりすぎると仮定されており、 これらのタスクは専用の Rails ワーカーによって処理される必要があります。 定期的な SBoM エクスポートは定期的な Rails ジョブによって実行されます。
非同期に作成された SBoM エクスポートはオブジェクトストレージに保存されます。 ユーザーは対応するコードが公開された後かなり経ってから SBoM エクスポートが必要な場合があります。
- SBoM エクスポートが CI アーティファクトから作成される場合、特定の保持ポリシーを持ち、後で期限切れになる必要があります。
- SBoM エクスポートが長続きする ISBoM から作成される場合、スペースを節約するために安全に削除できます。
リリースアセットとしての SBoM
1 つまたは複数の SBoM エクスポートは、プロジェクト設定に応じてプロジェクトリリースが作成されるときにリンクされます。 SBoM エクスポートはオブジェクトストレージに保存され、アセットリンクを使用してリリースにリンクされます。 理想的には、この機能は 3 つのシナリオをサポートします:
- SBoM エクスポートはリリース作成時にすでに利用可能である。
- SBoM エクスポートは進行中であり、利用可能になったときにリンクされる。
- SBoM エクスポートがない。エクスポートをトリガーして、利用可能になったときに SBoM をリンクする必要があります。
グラフを使用した依存関係の説明
ユーザーは、明示的に要求されていないにもかかわらず特定のパッケージにプロジェクトが依存している理由を知りたいと考えています。 つまり、推移的依存関係と、自分たちが管理する依存関係ファイルで導入された依存関係との関係を知りたいと考えています。 この関係はさまざまな方法で提示できます:
- 推移的依存関係と導入された依存関係を接続するすべてのパスをリストアップする
- 完全な依存関係グラフを表示し、推移的依存関係と導入された依存関係を接続するノードとエッジをハイライトする
- 推移的依存関係とそれに接続された導入された依存関係のみを含むサブグラフを表示する
単一のプロジェクトには複数の依存関係グラフがある場合があり、1 つまたは複数のスキャンジョブによって生成された複数のアーティファクトに保存されている場合があります。
- 使用されているレポートフォーマットによっては、マルチプロジェクトリポジトリまたはビルドのすべての依存関係を記述するために複数のドキュメントが必要な場合があります。
- リポジトリが複数のパッケージマネージャーを使用する場合、複数のスキャンジョブによって処理され、複数の CI アーティファクトが生成されます。
とは言え、ユーザーは特定の推移的依存関係が導入された依存関係にどのように接続されているかを知りたいだけであり、 この質問に答えるには単一の依存関係グラフで十分です。 情報は単一の依存関係スキャンレポートまたは SBoM アーティファクトから取得できます。
実装は UI に表示されるものに関わらずほぼ同じです。
- フロントエンドはパッケージ名、パッケージバージョン、依存関係ファイルパス、コミット SHA、プロジェクト ID のサブグラフを要求します。
- サブグラフがグラフ内でハイライトされる場合、フロントエンドはファイル、コミット、プロジェクトの完全なグラフも要求します。
- バックエンドはファイル、コミット、プロジェクトに対応する CI アーティファクトをフェッチします。
- バックエンドは完全な依存関係グラフを構築し、サブグラフを計算します。
- バックエンドはサブグラフ、パスのリスト、またはフロントエンドが推移的依存関係を説明するために必要なものを返します。
フロントエンドは GraphQL を使用してグラフをクエリできます。
ユーザーが特定の推移的依存関係を探索すると、同じファイル、コミット、プロジェクトの他の推移的依存関係を探索する可能性が高くなります。 最適化として、バックエンドは対応する依存関係グラフをキャッシュして、これらの手順が繰り返されないようにする必要があります:
- CI アーティファクトをリストアップして、依存関係ファイル、コミット、プロジェクトに対応するものを見つける。
- 見つかった CI アーティファクトを解析してグラフ情報を抽出する。
あるいは、依存関係グラフ情報を ISBoM からフェッチすることもできます。 その場合、ISBoM フォーマットは ISBoM 全体を解析してメモリにロードせずに、特定の依存関係ファイルのグラフを効率的に取得するために最適化される必要があります。
導入された依存関係ごとの脆弱性のグループ化
ユーザーは、自分たちが管理する依存関係ファイルで導入されたどの依存関係が検出された脆弱性の原因かを知りたいと考えています。 これに対処するため、既存の依存関係リストは導入された依存関係ごとに脆弱性をグループ化する必要があります。 導入された依存関係は、それが直接影響を受けるか、影響を受けるパッケージ(推移的依存関係)に依存する場合、脆弱性に責任があります。
推移的依存関係は複数の導入された依存関係によって共有される場合があるため、 導入された依存関係と脆弱性の間には N-N の関係があります:
- 導入された依存関係には複数の脆弱性がある場合があります。
- 脆弱性は複数の導入された依存関係に接続されている場合があります。
導入された依存関係ごとの脆弱性のグループ化は、すべての依存関係、 つまり依存関係を持つ推移的依存関係にも拡張できます。 たとえば、導入された依存関係 A が B に依存し、推移的依存関係 B が C と D に依存している場合、次のようにグループ化できます:
- A に関連する脆弱性: A、B、C、または D に直接影響する脆弱性
- B に関連する脆弱性: B、C、または D に直接影響する脆弱性
影響を受ける依存関係とその祖先との関係は依存関係グラフによって記述されます。 依存関係グラフは現時点では依存関係スキャンレポートの一部ではありませんが、 既存のレポートはグラフを提供するために置き換え、拡張、または補完されます。
リレーショナル DB から導入された依存関係を効率的にフェッチするため、
project_dependencies テーブルに新しい introduced ブール値カラムを追加し、
追跡されているブランチのプロジェクトの依存関係をアップサートするときに設定することができます。
バックエンドは依存関係グラフをフェッチして処理し、依存関係が導入されたものか推移的なものかを確認します。
このカラムはグラフが利用できない場合は NULL になります。これはアナライザーの制限や解析する依存関係ファイルの制限によるものかもしれません。
ALTER TABLE project_dependencies ADD COLUMN introduced boolean;
すべての関連する脆弱性を含む導入された依存関係のリストを準備するため、 バックエンドは 4 つの手順を踏みます:
- リレーショナル DB から導入されたプロジェクトの依存関係をフェッチする(おそらく
project_dependencies.introducedを使用して) - オブジェクトストレージから対応する依存関係グラフをロードする
- 同じプロジェクトブランチの脆弱性ファインディングをフェッチする
- 影響を受ける依存関係を導入された依存関係に接続する依存関係マップを構築する
- 依存関係マップを使用して、導入された依存関係ごとに脆弱性ファインディングをグループ化する
依存関係リストは api/Gemfile のような特定のファイルで導入された依存関係に限定できます。
これによりバックエンドがより効率的になります:
- リレーショナル DB をクエリするときに、そのファイルに一致するプロジェクトの依存関係をフェッチします。
- プロジェクトブランチのすべての脆弱性ファインディングをフェッチしますが、
locationがファイルと一致しないファインディングはメモリに保持しません。 - ファイルに対応する依存関係グラフのみをロードして処理します。
技術的に、api/Gemfile は project_dependency_files テーブルで追跡する唯一のファイルがある場合、api/Gemfile.lock に解決される場合があります。
影響を受ける推移的依存関係を導入された依存関係に解決するために使用される依存関係マップは、パフォーマンスのためにキャッシュできます。 キャッシュのスコープは、プロジェクトのコミットと、マップが生成された依存関係ファイルのセットになります。
