CI/CD パイプライン

Jenkins

Jenkins の Declarative Pipeline に VulnCheck の脆弱性ゲートを追加します。

VulnCheck は VulnCheck CLI を通じて Jenkins で動作します。プラグインのインストールは不要です。以下のパイプラインは、バージョンを固定した CLI をインストールし、ワークスペースをスキャンし、ポリシーに違反する検出結果があればビルドを失敗させ、結果をアーカイブします。

トークンを保存する

API トークン から VulnCheck API トークンを作成し、Secret text の資格情報として Jenkins に追加します。

パイプラインは withCredentials でこの資格情報を VC_TOKEN にバインドします。これによりトークンはビルドログ上でマスクされ、無関係なステップの環境に残ることもありません。

Declarative Pipeline

pipeline {
    agent {
        docker {
            image 'debian:bookworm-slim'
            args '-u root'
        }
    }

    environment {
        VC_CLI_VERSION    = '1.1.0'
        VC_CVSS_THRESHOLD = '7.0'
    }

    stages {
        stage('Install the VulnCheck CLI') {
            steps {
                sh '''
                    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
                '''
            }
        }

        stage('Scan') {
            steps {
                withCredentials([string(credentialsId: 'vulncheck-api-token', variable: 'VC_TOKEN')]) {
                    sh '''
                        vulncheck scan . --json > scan.json
                        jq -r '"\\((.vulnerabilities // []) | length) vulnerabilities found"' scan.json
                    '''
                }
            }
        }

        stage('Gate on findings') {
            steps {
                sh '''
                    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
                    }
                '''
            }
        }
    }

    post {
        always {
            archiveArtifacts artifacts: 'scan.json', allowEmptyArchive: true
        }
    }
}
jq のプログラム内でバックスラッシュを二重にしている点に注意してください。\( ではなく \\( と書きます。Jenkins はシングルクォートの Groovy 文字列でもエスケープシーケンスを解釈するため、エスケープしていない \( はシェルに渡る前に不正なエスケープ文字として拒否されます。

その他に押さえておきたい点があります。

  • ゲートを別ステージにしているのは意図的です。 vulncheck scan は重大な脆弱性を検出した場合でも終了コード 0 を返すため、スキャンのステージは常に成功し、ポリシー判定はビルド内の独立したステージとして見えるようになります。CI/CD での VulnCheck を参照してください。
  • args '-u root' により、インストールのステップが /usr/local/bin に書き込めるようになります。指定しない場合は、書き込み可能な場所にインストールして PATH に追加してください。
  • post { always { … } } 内の archiveArtifacts により、失敗したビルドでも scan.json が保持されます。証跡が最も必要になるのは、その実行です。
  • VC_CLI_VERSION を固定してください。 これにより、どのビルドも既知のバージョンの CLI でスキャンされ、install.sh が最新リリースの判定に行う未認証の GitHub API 呼び出しも避けられます。

失敗させずに警告する

ビルドを止めずに検出結果を可視化したい場合は、失敗ではなく unstable として扱います。

        stage('Gate on findings') {
            steps {
                script {
                    def breaches = sh(returnStatus: true, script: '''
                        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
                    ''')
                    if (breaches != 0) {
                        unstable('VulnCheck policy breach — see scan.json')
                    }
                }
            }
        }

よく使われる構成は、変更リクエストでは失敗させ、ブランチのビルドでは unstable に留める方法です。これにより、新たに公開された CVE が無関係な作業を止めることはありません。

                    if (breaches != 0) {
                        if (env.CHANGE_ID) {
                            error('VulnCheck policy breach on this change request')
                        } else {
                            unstable('VulnCheck policy breach on the branch build')
                        }
                    }

Docker を使わないエージェント

通常のエージェントでは、構成管理または一度きりの管理作業としてホストに CLI をインストールし、インストールのステージを完全に削除します。

pipeline {
    agent { label 'linux' }

    environment {
        VC_CVSS_THRESHOLD = '7.0'
    }

    stages {
        stage('Scan') {
            steps {
                withCredentials([string(credentialsId: 'vulncheck-api-token', variable: 'VC_TOKEN')]) {
                    sh 'vulncheck version'
                    sh 'vulncheck scan . --json > scan.json'
                }
            }
        }
    }
}

どのバージョンの CLI が結果を生成したかがログに残るよう、ビルド内に vulncheck version を残しておいてください。

トラブルシューティング

症状原因
パイプラインの読み込み時に illegal escape character が出るGroovy 文字列の中で jq の文字列補間を \( と書いている。\\( のようにエスケープしてください。
インストールのステージが成功したのに vulncheck: command not found になるroot で実行しているコンテナで install.sh --sudo を使用している。sudo が存在しないにもかかわらず、スクリプトは成功として報告します。上記の tarball インストールを使用してください。
ビルドが終了コード 3 で失敗する資格情報が存在しないか、バインドが適用されていない。vulncheck を実行する sh ステップが withCredentials ブロックの内側にあるか確認してください。
ビルドが終了コード 5 で失敗するレート制限。多数の Executor が 1 つのトークンを共有している場合が多いです。
jqCannot iterate over null を返す検出結果がないスキャンには vulnerabilities キーが含まれません。上記のように (.vulnerabilities // [])[] を使用してください。
jqnull (null) cannot be parsed as a number を返すcvss_base_score を持たない検出結果があります。上記のように .cvss_base_score // 0 | tonumber? // 0 を使用してください。