GitLab シークレット検出 ADR 008: 統合スキャナー

背景

現在、シークレット検出スキャンは、ソースコード(パイプラインSD経由)、Gitコミット(プッシュ保護)、Issue説明/コメント(クライアントサイドSD)、そしてさらに追加予定のさまざまなスキャン対象タイプに対して実行されています。上記のすべてのスキャン対象タイプは異なるスキャンエンジン(Gitleaks、チームが管理するもの、Webフロントエンドによるもの)を持っており、複雑さをさらに増すことに、GitLabの一部のチームは独自バージョンのシークレット検出を維持しています。これにより複数の問題(以下に概説)が発生し、顧客にとって一貫性のない体験になります:

  • 複数のエンジンにわたって機能パリティを維持する大きな負担が生じます。
  • パフォーマンスと効率のためにコアスキャンロジックを最適化することが制限されます。
  • すべてのスキャンエンジンに対してルールセットの品質(精度とリコール)を確保することが難しくなります。
  • 一般的に特定の正規表現エンジンに対して行われるルールパターンの最適化も難しくなります。
  • エンジンの互換性に基づいたインテリジェントなスキャン機能の統合が一貫しなくなります。

提案

提案はすべてのスキャン対象タイプに使用される統合シークレット検出スキャナーを構築することです。コアの責任は、提供されたスキャン設定に対して与えられたペイロードのセットに対してシークレット検出スキャンを実行することです。スキャナーは1つ以上の基盤となるスキャンエンジンを含み、正確な検出結果を得るために複数のスキャンエンジンを使用して同時にスキャンを実行する可能性があります。

スキャナーは内部的に、スキャン前後に実行されるプロセッサーを使用して、スキャンプロセスを促進したり検出結果を強化したりします(詳細はこちら)。このアプローチにより、コアスキャンロジックをすべてのスキャン対象実装にわたって再利用できます。

ドメイン固有の前処理(スキャン可能な形式にデータを収集・構造化するなど)とポスト処理ステップ(有効性チェックを含む)はスキャナーモジュールのスコープ外であり、呼び出し元のアプリケーションが処理します。

特性

  • スキャナーはポータブルです。これは、GitLab CI のような分離された環境で実行される一部のスキャン対象タイプ(ソースコード/アーティファクト)を考慮すると重要です。
  • スキャナーはトランザクションまたはストリーミングベースのスキャンを実行できます。
  • スキャナーはクロスプラットフォーム互換です。
  • スキャナーは本質的にステートレスです。
  • スキャナーはリソース制約のある環境(クライアントIDEや小さなCI環境など)で実行できるほど高度にリソース効率が良いです。
  • スキャナーはユーザー入力(デフォルト値付き)を受け入れることでスキャン動作のカスタマイズを許可します:
    • 除外(ルール/パス/値)
    • カスタムルールセット
    • ルールセットバージョン
    • タイムアウト制約
    • ペイロードサイズ制限
    • ファイルパス/ディレクトリパス/stdin経由で受け入れ可能なペイロード(バイナリ実行ファイル)
    • 設定可能なリソース(メモリ/CPUコア)制限(バイナリ実行ファイル)
  • スキャナーはエアギャップ環境でバイナリ実行ファイルとして実行できます。

上記の特性の一部はスキャナークライアントを通じて実現されます(詳細は以下)。

前提条件

統合スキャナーのアイデアは、効率的かつポータブルなスキャンエンジンがある場合にのみ実現可能です。決定に従い、VectorscanベースのGoスキャンエンジンを実装する必要があります。

スキャナーの使用モード

提案されたスキャナーのミニマリスティックなスコープとステートレスな性質により、_ポータビリティ_の利点が開きます。これは特定のスキャン対象タイプ(CI環境で実行されるソースコードやジョブアーティファクト)にとって必要不可欠です。そのため、スキャン対象タイプの性質(トラフィック、サイズなど)に応じて、以下の3つのモードのいずれかでスキャンエンジンを採用できます:

  • 分散サービスとして: スキャナーはスキャンAPIエンドポイントを持つREST/gRPCレイヤーでラップされます。呼び出し元はネットワーク経由でスキャンリクエストを行います。このモードは軽量ペイロード(1MB未満)を持つ高トラフィックのSD機能に適しています。例: シークレット検出サービス経由でのワークアイテムのスキャン

  • 組み込みモジュールとして: このモードのスキャナーは呼び出し元アプリケーションと同じホストに_組み込まれています_。呼び出し元はスキャン可能なペイロードに対してスキャンを呼び出します。このモードは本質的にトランザクション型です。すでにプッシュ保護機能でこのモードを使用しており、エンジンはRuby GemとしてRailsモノリスに組み込まれています。Railsモノリスはスキャンリクエスト(スキャン可能なペイロードとしてgit diffデータを含む)をGemに送信します。

組み込みモード

  • バッチプロセッサーとして: これは、シークレット検出プログラム(+エンジン)がデータが存在する場所で実行されるインストレージ処理をサポートするための特殊なケースです。この逆アプローチは、データストレージとスキャンサーバー間のデータ転送コストを避けるために、ソースコードやジョブアーティファクトのような大きなデータサイズを持つスキャン対象タイプに適しています。組み込みモードとの主な違いは、呼び出し元がスキャンリクエスト内にスキャン可能なペイロードを含めるのに対し、バッチモードでは呼び出し元がターゲットホスト(プログラムとデータが存在する場所)で利用可能なスキャン可能なペイロードを指定する点です(例: リクエストとともにファイルパスを渡す)。

バッチモード

これらのモードはスキャナークライアントとして実装されています。スキャナークライアントは呼び出し元がスキャナーモジュールを呼び出すためのラッパーインターフェースです。スキャナークライアントはgRPCサービス(分散処理向け)またはCLIアプリケーション(組み込み/バッチ処理向け)のいずれかです。

コンポーネント

統合スキャナーアーキテクチャには以下のコンポーネントが導入されます:

  • スキャナー: シークレット検出スキャンをオーケストレーションするコアコンポーネント。スキャナークライアントからスキャン可能なペイロードをスキャン設定とともに受け取り、前処理を適用し、基盤となるスキャンエンジンを呼び出し、検出結果に後処理を実行します。ポータブルでステートレスになるよう設計されています。

  • スキャンエンジン: シークレット検出ロジックを実行する実際のエンジン。正規表現ベースのエンジン(Vectorscanなど)、AIベースのエンジン、または複数のエンジンの組み合わせである可能性があります。スキャナーはエンジン固有の詳細を抽象化し、異なるエンジンをシームレスにプラグインできるようにします。

  • プロセッサー: コアスキャン操作の前(プリプロセッサー)または後(ポストプロセッサー)に実行するモジュールコンポーネントで、スキャンプロセスを強化したり検出結果を改良したりします。

    • プリプロセッサー: スキャンエンジンのために入力ペイロードを準備したり追加のメタデータを提供したりします(例: ASTの生成、インライン除外のためのペイロードデータのサニタイズ)。
    • ポストプロセッサー: 内部エンジンによって検出された結果を改良します(例: AIベースの偽陽性削減、エントロピーマッチング)。
  • スキャナークライアント: 呼び出し元アプリケーションがスキャナーモジュールと対話できるようにするスキャナーラッパー。gRPCサービスやCLIアプリケーションなどがあります。

  • アナライザー: スキャナーモジュールを呼び出してペイロードをスキャンするアプリケーション。ペイロードをスキャン可能な形式に変換し、スキャン設定を提供します。各アナライザーは特定のスキャン対象タイプ(プッシュ保護、ソースコードCIなど)を担当します。

  • 設定: 除外、カスタムルール、タイムアウト制約を含むスキャナーの動作パラメーターを定義します。アナライザーからスキャナークライアントを通じてスキャナーに提供されます。

コンポーネント階層

これらのコンポーネントがどのように相互作用するかの概要:

コンポーネント階層

高レベル設計

異なるスキャン対象タイプごとに異なるアナライザーを持つ統合シークレット検出スキャナーの包括的な図:

統合シークレット検出スキャナーの高レベル設計

スキャナーの内部構造

追加のスキャンエンジン(AIベース(内部)やポータビリティのためのRE2 WASMベースなど)を導入してシークレットを検出する計画があるため、スキャナーAPIは単一の基盤スキャンエンジンから自身を分離する必要があります。これにより、ユースケースに応じて正規表現ベース、AIベース、または複数のスキャンエンジンをプラグインできます。

プロセッサー

スキャンエンジンコンポーネントには、内部エンジンが実行するスキャン操作の前(プリプロセッサー)と後(ポストプロセッサー)に介入するプロセッサーが含まれています。

  • プリプロセッサー: 入力ペイロードはプリプロセッサーを通過して特定の操作を実行します。変更されたペイロードまたはスキャンエンジンに役立つ追加のメタデータが付いた元のペイロードのいずれかを返します。例として、ペイロードのAST生成、インライン除外のためのペイロードデータのサニタイズなどがあります。

  • ポストプロセッサーは内部エンジンによって検出された結果を処理し、偽陽性のために特定の検出を破棄するヒントを返すか、またはドメイン固有の基準を実行して新しいメタデータを生成します。例として、AIベースの偽陽性削減、エントロピーマッチングなどがあります。

プロセッサーは汎用(すべてのスキャンで実行)または条件付き(エンジンタイプ、顧客層などの特定の基準によってトリガー)のいずれかです。1つのプロセッサーの出力が別のプロセッサーの入力になるチェーンパイプラインで動作することも、独立して機能することもできます。いずれの場合でも、スキャナーAPIはすべてのプロセッサーの結果を収集して最終的な出力を決定します。

スキャナーの内部コンポーネントを表す図:

スキャナーの内部構造

配布

スキャンエンジンのさまざまな使用モードと、Vectorscan正規表現エンジンがプラットフォーム依存であることを考えると、GitLabアプリケーション内でエンジンをアクセス可能にする方法を特定することが重要です。

  • エンジンを分散(grpc)サービスとして使用する場合: grpcサービスはRunwayを使用してデプロイされ、GitLab.com内部アプリケーションからアクセス可能になります。クライアントはgrpcエンドポイント経由でサービスにリモートスキャンリクエストを送信できます。

  • エンジンを組み込みモジュールまたはバッチ処理として使用する場合: クライアントアプリケーションに応じて、組み込みの性質が異なります。エンジンはGoベースのアプリケーション向けにはGoモジュールとしてインポートできますが、非Goアプリケーション(GitLab RailsのほとんどはRuby)向けには2つの選択肢があります:

    1. GoのスキャンエンジンソースからJasonの静的ライブラリのためのRuby C拡張を書く
    2. OmnibusでGo バイナリ実行ファイルをGitLab Railsにパッケージ化する

VectorScanエンジンが現在のアプローチ(行単位)ではなくデータのチャンク単位でスキャンすることを考えると、高いヒープ使用量の問題はもはや適用されません。さらに、VectorscanエンジンはSpreadシート事前割り当てられたスクラッチスペース上で動作するため、スキャン全体のメモリ消費量の低下に貢献します。C関数経由でGoメソッドを呼び出すスループットは、Ruby <-> Goプロセス間のIPC(プロセス間通信)と比較してほとんどオーバーヘッドを追加しません。以上のことから、決定はRuby C拡張を書く方向に傾いています。ただし、データ駆動型のエビデンスでこの決定を確認するスパイクをまだ実施していません。