連携

CI/CD での VulnCheck

VulnCheck CLI であらゆるビルドをスキャンします。すべてのプラットフォームガイドに共通するパターンと、それを実装したパイプライン例。

VulnCheck CLI を使えば、あらゆる CI プラットフォームを脆弱性ゲートに変えられます。リポジトリに対して vulncheck scan を実行すると、SBOM を生成し、すべてのコンポーネントを VulnCheck インテリジェンスと照合して、CVSS、Temporal スコア、EPSS、SSVC、VulnCheck KEV への該当情報を付加した CVE を返します。

GitHub では VulnCheck Action がこれらをすべて内包しています。それ以外の環境でも、シェル 5 ステップで同じ結果が得られます。インストール、認証、スキャン、ゲート、公開です。本ページがそのパターンであり、各プラットフォームのガイドはこれを適用したものです。

プラットフォーム別ガイド

プラットフォームガイド統合方法
GitHub ActionsVulnCheck in GitHub Actionsターンキー vulncheck-oss/action
GitLab CI/CDVulnCheck in GitLab CI/CDCLI
JenkinsVulnCheck in JenkinsCLI
Azure PipelinesVulnCheck in Azure PipelinesCLI
Bitbucket、CircleCI、Drone、Woodpecker、Buildkiteその他の CI プラットフォームCLI
どのガイドも同じ CLI を使用しているため、あるプラットフォームで記載されている内容は他のプラットフォームでも同様に機能します。お使いのプラットフォームが一覧にない場合は、その他の CI プラットフォーム に汎用的な手順があります。

前提条件

  • VulnCheck API トークン。作成方法は API トークン を参照してください。
  • ランナーから api.vulncheck.com および github.com(CLI のダウンロード用)への HTTPS 通信。
  • ジョブ内で利用できる curltarjq。スキャン結果を合否の判断に変換するのが jq です。

1. CLI をインストールする

バージョンを固定し、リリース tarball からインストールします。CI ではこの方法を推奨します。結果が再現可能で、GitHub API を呼び出さず、コンテナ内の root でも動作します。

VC_CLI_VERSION=1.1.0

ARCH="$(uname -m)"
case "$ARCH" in x86_64) ARCH=amd64 ;; aarch64) ARCH=arm64 ;; esac

curl -sSL "https://github.com/vulncheck-oss/cli/releases/download/v${VC_CLI_VERSION}/vulncheck_${VC_CLI_VERSION}_linux_${ARCH}.tar.gz" | tar -xz -C /tmp
install -m 0755 "/tmp/vulncheck_${VC_CLI_VERSION}_linux_${ARCH}/bin/vulncheck" /usr/local/bin/vulncheck

vulncheck version
VulnCheck CLI のインストール で説明している install.sh は開発端末向けのスクリプトであり、CI では次の 2 点が問題になります。
  • glibc を必要とします。Alpine などの musl 系イメージでは Unsupported operating system として終了します。
  • --sudosudo が存在することを前提にしています。root で実行しているコンテナでは sudo: command not found と表示した後に Installation complete! と報告し、何もインストールしないまま終了コード 0 を返します。問題が表面化するのは、後続の vulncheck: command not found の時点です。
また、最新リリースの判定に未認証の GitHub API を使用します。この API は IP 単位でレート制限され、ホストランナーではすべてのジョブで共有されます。

バイナリ自体は静的リンクされており、Alpine を含めどこでも動作します。glibc が必要なのはインストールスクリプトだけです。musl 系イメージでは上記の tarball を使用してください。

2. 認証する

トークンは CI プラットフォームのマスクされたシークレットとして保存し、VC_TOKEN として公開します。CLI はこれを自動的に読み取るため、CI で auth login を実行する必要はありません。

vulncheck auth status --json | jq -e '.authenticated'

VC_TOKEN は保存済みの設定ファイルより優先されます。また CLI は CI を自動検出し、CIBUILD_NUMBERRUN_ID のいずれかが設定されていれば --no-interactive として扱うため、プロンプトで停止することはありません。

3. スキャンする

vulncheck scan . --json > scan.json
jq -r '"\((.vulnerabilities // []) | length) vulnerabilities found"' scan.json

結果をファイルに書き出し、サマリーを自分で出力することで、ビルドログが読みやすくなります。--json モードでは、検出結果がない場合の出力は {"schema_version": 1} のみで、メッセージは一切表示されません。

結果は検出結果の配列で、ポリシー判定に必要な情報がすべて含まれます。

{
  "schema_version": 1,
  "vulnerabilities": [
    {
      "name": "requests",
      "version": "2.19.1",
      "cve": "CVE-2018-18074",
      "in_kev": false,
      "cvss_base_score": "7.5",
      "cvss_temporal_score": "7.1",
      "fixed_versions": "2.20.0",
      "metrics": { "epss": { "epss_score": 0.00182 }, "ssvc": [] }
    }
  ]
}

4. ビルドをゲートする

vulncheck scan脆弱性を検出したかどうかに関係なく終了コード 0 を返します。重大な CVE の検出はスキャンの成功であり、コマンドの失敗ではありません。したがって、どのパイプラインにも明示的なゲートが必要です。ゲートがなければ、ログに検出結果が残ったままジョブは成功します。

次のゲートは、CVSS 基本値がしきい値以上の検出結果、またはスコアを問わず VulnCheck KEV に該当する検出結果があればビルドを失敗させます。

VC_CVSS_THRESHOLD=7.0

jq -e --argjson max "$VC_CVSS_THRESHOLD" '
  [ (.vulnerabilities // [])[]
    | select((.cvss_base_score // 0 | tonumber? // 0) >= $max or .in_kev) ] | length == 0
' scan.json > /dev/null || {
  echo "Findings at or above CVSS ${VC_CVSS_THRESHOLD}, or in VulnCheck KEV:"
  jq -r --argjson max "$VC_CVSS_THRESHOLD" '
    (.vulnerabilities // [])[]
    | select((.cvss_base_score // 0 | tonumber? // 0) >= $max or .in_kev)
    | "  \(.cve)  \(.name)@\(.version)  CVSS \(.cvss_base_score)  KEV \(.in_kev)  fixed in \(.fixed_versions // "n/a")"
  ' scan.json
  exit 1
}

// [] は必須です。検出結果がない場合、スキャン結果には vulnerabilities キー自体が含まれません。ガードなしで反復処理すると jqCannot iterate over null として終了コード 5 を返し、何も問題のないリポジトリでビルドが失敗します。

cvss_base_score のガードも同様です。すべての検出結果に CVSS スコアが付いているとは限らず、値が欠落・null・空文字列の場合、tonumber.in_kev が評価される前に jq プログラム全体を同じ終了コード 5 で中断させます。.cvss_base_score // 0 | tonumber? // 0 と書けば、そうした検出結果はスコア 0 として扱われるため、スコアのない VulnCheck KEV 該当項目もゲートを壊すことなく in_kev 側の条件で捕捉できます。

しきい値を高く設定している場合でも、in_kev によるゲートは残す価値があります。既知の悪用が確認されている脆弱性は、スコアに関係なく重要です。ゲートに使用できるフィールドとしては、他に cvss_temporal_scoremetrics.epss.epss_scoremetrics.ssvc[].exploitation があります。

既存のコードベースでは、最初のパイプライン実行がすべてのマージを止めてしまわないよう、まず高いしきい値から始め、その後に厳しくしていきましょう。デフォルトブランチでは警告として実行し、プルリクエストでは失敗として扱う運用も多く見られます。

5. 結果を公開する

scan.json はビルドアーティファクトとして保存してください。ビルド時点で判明していた内容の記録になります。CLI は CycloneDX 1.7 形式の SBOM も出力できるため、あわせて公開することをおすすめします。

vulncheck scan . --sbom-only -o sbom.json

終了コード

スキャン結果によって終了コードが変わることはありませんが、失敗時には変わります。パイプラインが壊れた場合は次を確認してください。

コード意味CI でよくある原因
0成功脆弱性が検出された場合も含む
1内部エラーscan のパスを解決できない(下記参照)
2入力検証エラー引数やフラグの誤り
3認証エラーVC_TOKEN が未設定、期限切れ、またはジョブに渡っていない
4対象が見つからない該当するインデックスやリソースが存在しない
5レート制限複数のジョブが 1 つのトークンを共有している
6ネットワークエラーランナーから api.vulncheck.com に到達できない

--json モードでは、エラーは構造化されたエンベロープとして標準出力に返されるため、そのまま判定に使えます。

{
  "schema_version": 1,
  "error": {
    "code": "auth_invalid",
    "message": "unauthorized: token is missing or invalid",
    "http_status": 401
  }
}
スキャンパスを誤って指定すると、終了コード 1 で code: "internal" が返り、メッセージには snapdockerpodmancontainerd、OCI レジストリの解決失敗が列挙されます。scan はコンテナイメージの参照も受け付けるため、指定された値をイメージとして解決しようとするからです。プロバイダーの解決エラーが表示された場合は、まず指定したパスを確認してください。

閉域環境・オフラインのランナー

インターネットに接続できないランナーでは、ローカルにキャッシュしたインデックスに対して vulncheck scan --offline でスキャンできます。インデックスの同期とキャッシュについては オフラインモード、その元データについては オフラインバックアップ を参照してください。