VulnCheck は VulnCheck CLI を通じて GitLab CI/CD で動作します。インスタンス側に導入するものはなく、CI/CD カタログからコンポーネントを追加する必要もありません。.gitlab-ci.yml にジョブを 1 つ定義すれば、リポジトリをスキャンし、ポリシーに違反する検出結果があればパイプラインを失敗させ、結果をジョブのアーティファクトとして保存します。
API トークン から VulnCheck API トークンを作成し、プロジェクトに追加します。
VC_TOKEN を指定し、値にトークンを貼り付ける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 エンドポイントに限定されているため、既存のコメントは読めても新規作成はできません。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
artifacts:paths を使って sbom.json を通常のアーティファクトとして公開してください。いずれにせよこのレポート形式は Ultimate 限定ですが、artifacts:paths はすべてのプランで利用できます。api.vulncheck.com への HTTPS 通信と、CLI をダウンロードするための github.com への通信が必要です。外向き通信が制限されている場合は、リリース tarball を社内にミラーし、そこからインストールしてください。vulncheck scan --offline を使用します。オフラインモード を参照してください。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 つのトークンを共有している場合が多いです。 |
jq が Cannot iterate over null を返す | 検出結果がないスキャンには vulnerabilities キーが含まれません。上記のように (.vulnerabilities // [])[] を使用してください。 |
jq が null (null) cannot be parsed as a number を返す | cvss_base_score を持たない検出結果があります。上記のように .cvss_base_score // 0 | tonumber? // 0 を使用してください。 |
snap、docker、podman に言及したプロバイダー解決エラーが出る | スキャンパスを解決できず、scan がコンテナイメージの参照として扱おうとしています。パスを確認してください。 |