CI/CD パイプライン

Azure Pipelines

Azure DevOps のパイプラインに VulnCheck の脆弱性ゲートを追加し、検出結果をプルリクエストに投稿します。

VulnCheck は VulnCheck CLI を通じて Azure Pipelines で動作します。Marketplace から拡張機能をインストールする必要はありません。以下のパイプラインは、バージョンを固定した CLI をインストールし、リポジトリをスキャンし、ポリシーに違反する検出結果があれば実行を失敗させ、結果をパイプラインのアーティファクトとして公開します。

トークンを保存する

API トークン から VulnCheck API トークンを作成し、シークレット変数として追加します。パイプライン自体に設定する方法(Edit > Variables > New variableKeep this value secret をオンにする)と、複数のパイプラインで共有するために Pipelines > Library の変数グループに設定する方法があります。

Azure Pipelines は、通常の変数と異なり、シークレット変数を自動的には環境変数に展開しません。トークンを必要とするタスクごとに、以下のように env: ブロックで明示的にマッピングする必要があります。これを忘れると、変数を設定しているにもかかわらず CLI は認証エラーで終了コード 3 を返します。

すべての実行でスキャンする

trigger:
  - main

pr:
  - main

pool:
  vmImage: ubuntu-latest

variables:
  VC_CLI_VERSION: '1.1.0'
  VC_CVSS_THRESHOLD: '7.0'

steps:
  - bash: |
      set -euo pipefail
      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
      sudo install -m 0755 "/tmp/vulncheck_${VC_CLI_VERSION}_linux_${ARCH}/bin/vulncheck" /usr/local/bin/vulncheck
      vulncheck version
    displayName: Install the VulnCheck CLI

  - bash: |
      set -euo pipefail
      vulncheck scan . --json > "$(Build.ArtifactStagingDirectory)/scan.json"
      jq -r '"\((.vulnerabilities // []) | length) vulnerabilities found"' "$(Build.ArtifactStagingDirectory)/scan.json"
    displayName: Scan with VulnCheck
    env:
      VC_TOKEN: $(VC_TOKEN)

  - bash: |
      SCAN="$(Build.ArtifactStagingDirectory)/scan.json"
      jq -e --argjson max "$VC_CVSS_THRESHOLD" '
        [ (.vulnerabilities // [])[]
          | select((.cvss_base_score // 0 | tonumber? // 0) >= $max or .in_kev) ] | length == 0
      ' "$SCAN" > /dev/null || {
        echo "##vso[task.logissue type=error]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"
        exit 1
      }
    displayName: Gate on findings

  - task: PublishPipelineArtifact@1
    condition: always()
    inputs:
      targetPath: $(Build.ArtifactStagingDirectory)/scan.json
      artifact: vulncheck-scan
    displayName: Publish scan results

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

  • ゲートを別ステップにしているのは意図的です。 vulncheck scan は重大な脆弱性を検出した場合でも終了コード 0 を返すため、スキャンのステップは常に成功し、ポリシー判定は実行サマリー上で明確に読み取れます。CI/CD での VulnCheck を参照してください。
  • condition: always() により、ゲートが失敗した場合でもアーティファクトが公開されます。証跡が最も必要になるのは、その実行です。
  • curljq はプリインストールされています。 Microsoft ホステッドの Ubuntu イメージでの話です。セルフホステッドエージェントでは、ホストに一度インストールしてください。
  • VC_CLI_VERSION を固定してください。 これにより、どの実行も既知のバージョンの CLI でスキャンされ、install.sh が最新リリースの判定に行う未認証の GitHub API 呼び出しも避けられます。

プルリクエストにコメントする

Azure Repos のプルリクエストでは、ビルド自身の OAuth トークンを使って REST API 経由でコメントスレッドを作成できます。

  - bash: |
      if [ "$BUILD_REASON" != "PullRequest" ]; then exit 0; fi
      SCAN="$(Build.ArtifactStagingDirectory)/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")
      jq -n --arg body "$BODY" '{comments: [{parentCommentId: 0, content: $body, commentType: 1}], status: 1}' > thread.json
      curl -sS --fail-with-body \
        --header "Authorization: Bearer ${SYSTEM_ACCESSTOKEN}" \
        --header "Content-Type: application/json" \
        --data @thread.json \
        "${SYSTEM_COLLECTIONURI}${SYSTEM_TEAMPROJECT}/_apis/git/repositories/${BUILD_REPOSITORY_ID}/pullRequests/${SYSTEM_PULLREQUEST_PULLREQUESTID}/threads?api-version=7.1"
    displayName: Comment on the pull request
    condition: always()
    env:
      SYSTEM_ACCESSTOKEN: $(System.AccessToken)
System.AccessToken は、マッピングしない限りスクリプトには渡されません。上記の env: ブロックがその役割を果たしています。さらに、その背後にある ID(Project Collection Build Service またはプロジェクトのビルドサービスアカウント)には、対象リポジトリに対する Contribute to pull requests の権限が必要です。権限がない場合、API は 403 を返します。

失敗させずに警告する

実行を止めずに検出結果を可視化したい場合は、警告をログに出力してステップは成功させます。

  - bash: |
      SCAN="$(Build.ArtifactStagingDirectory)/scan.json"
      jq -e --argjson max "$VC_CVSS_THRESHOLD" '
        [ (.vulnerabilities // [])[]
          | select((.cvss_base_score // 0 | tonumber? // 0) >= $max or .in_kev) ] | length == 0
      ' "$SCAN" > /dev/null || echo "##vso[task.logissue type=warning]VulnCheck policy breach — see the vulncheck-scan artifact"
    displayName: Report findings

よく使われる構成は、プルリクエストの実行では確実に失敗させ、ブランチのビルドでは警告に留める方法です。これにより、新たに公開された CVE が無関係な作業を止めることはありません。両者は condition: eq(variables['Build.Reason'], 'PullRequest') で切り分けます。

Windows とセルフホステッドエージェント

Windows エージェントでは、windows_amd64.zip のリリースをダウンロードし、展開した bin ディレクトリを PATH に追加したうえで、同じ vulncheck scanjq のステップを PowerShell から実行します。セルフホステッドエージェントには api.vulncheck.com への HTTPS 通信と、CLI をダウンロードするための github.com への通信が必要です。外向き通信が制限されている場合は、リリースアーカイブを社内にミラーしてください。完全な閉域環境のエージェントについては オフラインモード を参照してください。

トラブルシューティング

症状原因
シークレット変数を設定しているのに終了コード 3 になるタスクに env: のマッピングがない。Azure Pipelines はシークレットを自動的に環境変数へ展開しません。
プルリクエストの実行だけ終了コード 3 になるフォークからのプルリクエストのビルドにはシークレット変数が渡されません。condition: eq(variables['System.PullRequest.IsFork'], 'False') でスキャンをフォーク以外の実行に限定し、マージ後の状態はブランチのビルドでカバーしてください。
プルリクエストの threads API が 403 を返すビルドサービスの ID に、対象リポジトリの Contribute to pull requests 権限がありません。
終了コード 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 を使用してください。