CI/CD パイプライン

GitLab CI/CD

GitLab.com でもセルフマネージド環境でも、GitLab パイプラインに VulnCheck の脆弱性ゲートを追加します。

VulnCheck は VulnCheck CLI を通じて GitLab CI/CD で動作します。インスタンス側に導入するものはなく、CI/CD カタログからコンポーネントを追加する必要もありません。.gitlab-ci.yml にジョブを 1 つ定義すれば、リポジトリをスキャンし、ポリシーに違反する検出結果があればパイプラインを失敗させ、結果をジョブのアーティファクトとして保存します。

トークンを保存する

API トークン から VulnCheck API トークンを作成し、プロジェクトに追加します。

CLI は環境変数から VC_TOKEN を自動的に読み取ります。グループ単位で設定すれば、1 つのトークンをすべてのプロジェクトで共有できます。

すべてのパイプラインでスキャンする

stages:
  - security

vulncheck-scan:
  stage: security
  image: debian:bookworm-slim
  variables:
    VC_CLI_VERSION: '1.1.0'
    VC_CVSS_THRESHOLD: '7.0'
  before_script:
    - apt-get update -qq && apt-get install -y -qq --no-install-recommends ca-certificates curl jq
    - |
      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
  script:
    - vulncheck scan . --json > scan.json
    - jq -r '"\((.vulnerabilities // []) | length) vulnerabilities found"' scan.json
    - |
      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
      }
  artifacts:
    when: always
    paths:
      - scan.json
    expire_in: 30 days

押さえておきたい点がいくつかあります。

  • ゲートは省略できません。 vulncheck scan は重大な脆弱性を検出した場合でも終了コード 0 を返すため、jq -e によるチェックがなければ、ログに検出結果が残ったままジョブは成功します。詳しい説明とゲートに使えるその他のフィールドは CI/CD での VulnCheck を参照してください。
  • when: always を指定すると、ジョブが失敗した場合でも scan.json が保持されます。アーティファクトが最も必要になるのは、まさにその実行です。
  • VC_CLI_VERSION を固定してください。 install.sh は最新リリースの判定に未認証の GitHub API を使用します。この API は IP 単位でレート制限され、GitLab.com のホストランナーではすべてのジョブで共有されます。さらに glibc 専用であるため、Alpine イメージでは失敗します。上記の tarball を使えばどちらの問題も回避でき、バイナリ自体は Alpine でも問題なく動作します。

マージリクエストのみをスキャンする

すべてのブランチのすべてのコミットをスキャンするのは、多くの場合過剰です。上記のジョブ名を .vulncheck に変更してください。先頭のドットにより、単独では実行されない隠しテンプレートになります。あとは必要な数だけジョブを派生させます。

vulncheck-merge-request:
  extends: .vulncheck
  rules:
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'

vulncheck-default-branch:
  extends: .vulncheck
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  allow_failure: true

マージリクエストでは確実に失敗させ、デフォルトブランチでは警告に留めるのが良い既定値です。新たなポリシー違反を含んだままマージされることはなくなり、一方で、すでにリリース済みの依存関係に対して新たに公開された CVE が無関係な作業を止めることもありません。

マージリクエストにコメントする

検出結果は、レビュー担当者の目の前にあるときに最も役立ちます。GitLab の Notes API を使えば、マージリクエストのコメントとして投稿できます。

  script:
    - vulncheck scan . --json > scan.json
    - |
      BODY=$(jq -r '
        "## VulnCheck scan\n\n" +
        (if ((.vulnerabilities // []) | length) == 0
         then "No vulnerabilities found."
         else "| CVE | Package | CVSS | KEV | Fixed in |\n|---|---|---|---|---|\n" +
              ([ (.vulnerabilities // [])[]
                 | "| \(.cve) | \(.name)@\(.version) | \(.cvss_base_score) | \(if .in_kev then "Yes" else "No" end) | \(.fixed_versions // "n/a") |" ]
               | join("\n"))
         end)' scan.json)
    - |
      if [ -n "$CI_MERGE_REQUEST_IID" ]; then
        curl -sS --fail-with-body \
          --header "PRIVATE-TOKEN: ${GITLAB_COMMENT_TOKEN}" \
          --data-urlencode "body=${BODY}" \
          "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/notes"
      fi
これには api スコープを持つプロジェクトまたはグループのアクセストークンが必要で、マスクされた変数(上記の GITLAB_COMMENT_TOKEN)として保存します。組み込みの CI_JOB_TOKEN では実現できません。Notes API では GET エンドポイントに限定されているため、既存のコメントは読めても新規作成はできません。

SBOM を公開する

CLI は CycloneDX 形式の SBOM を生成します。スキャン結果とあわせて保存する価値があります。

  script:
    - vulncheck scan . --sbom-only -o sbom.json
  artifacts:
    paths:
      - sbom.json

GitLab Ultimate では、artifacts:reports:cyclonedx によって SBOM を GitLab 自体に渡し、プロジェクトの依存関係一覧に反映できます。

  artifacts:
    reports:
      cyclonedx: sbom.json
GitLab はこのレポートを CycloneDX 1.4 仕様に基づいて説明していますが、CLI が出力するのは 1.7 です。お使いのインスタンスがレポートを受け付けるか確認してください。依存関係一覧が生成されない場合は、artifacts:paths を使って sbom.json を通常のアーティファクトとして公開してください。いずれにせよこのレポート形式は Ultimate 限定ですが、artifacts:paths はすべてのプランで利用できます。

セルフマネージドのランナー

  • ランナーには api.vulncheck.com への HTTPS 通信と、CLI をダウンロードするための github.com への通信が必要です。外向き通信が制限されている場合は、リリース tarball を社内にミラーし、そこからインストールしてください。
  • 完全な閉域環境のランナーでは、キャッシュ済みのインデックスに対して vulncheck scan --offline を使用します。オフラインモード を参照してください。
  • Shell Executor では、before_script ではなくホストに一度 CLI をインストールしてください。あわせて、どのビルドが結果を生成したかがログに残るよう、ジョブ内に vulncheck version を残しておきます。

トラブルシューティング

症状原因
インストールのステップが成功したのに vulncheck: command not found になるroot で実行しているコンテナで install.sh --sudo を使用している。sudo が存在しないにもかかわらず、スクリプトは成功として報告します。上記の tarball インストールを使用してください。
インストール中に Unsupported operating system と表示されるAlpine などの musl 系イメージで install.sh を実行している。tarball インストールなら動作します。
ジョブが終了コード 3 で失敗するVC_TOKEN が未設定、期限切れ、またはこのジョブに渡っていない。実行中のブランチに対して Protect variable の設定を確認してください。
ジョブが終了コード 5 で失敗するレート制限。多数の並列ジョブが 1 つのトークンを共有している場合が多いです。
jqCannot iterate over null を返す検出結果がないスキャンには vulnerabilities キーが含まれません。上記のように (.vulnerabilities // [])[] を使用してください。
jqnull (null) cannot be parsed as a number を返すcvss_base_score を持たない検出結果があります。上記のように .cvss_base_score // 0 | tonumber? // 0 を使用してください。
snapdockerpodman に言及したプロバイダー解決エラーが出るスキャンパスを解決できず、scan がコンテナイメージの参照として扱おうとしています。パスを確認してください。