Integrations

VulnCheck in CI/CD

Scan every build with the VulnCheck CLI — the pattern behind every platform guide, and the pipeline examples that implement it.

The VulnCheck CLI turns any CI platform into a vulnerability gate. Point vulncheck scan at a repository, and it builds an SBOM, matches every component against VulnCheck intelligence, and returns CVEs enriched with CVSS, temporal scores, EPSS, SSVC and VulnCheck KEV membership.

On GitHub, the VulnCheck Action wraps all of this for you. Everywhere else, the same result is five lines of shell: install, authenticate, scan, gate, publish. This page is that pattern; the platform guides apply it.

Platform guides

PlatformGuideIntegration
GitHub ActionsVulnCheck in GitHub ActionsTurnkey vulncheck-oss/action
GitLab CI/CDVulnCheck in GitLab CI/CDCLI
JenkinsVulnCheck in JenkinsCLI
Azure PipelinesVulnCheck in Azure PipelinesCLI
Bitbucket, CircleCI, Drone, Woodpecker, BuildkiteOther CI platformsCLI
Every guide uses the same CLI, so anything documented on one platform works on the others. If your platform is not listed, Other CI platforms has the generic recipe.

Prerequisites

  • A VulnCheck API token. See API Tokens to create one.
  • Outbound HTTPS access from your runners to api.vulncheck.com and github.com (the latter to download the CLI).
  • curl, tar and jq available in the job. jq is what turns scan results into a pass/fail decision.

1. Install the CLI

Pin a version and install the release tarball. This is the recommended approach for CI: it is deterministic, it does not call the GitHub API, and it works as root inside a container.

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
The install.sh convenience script documented in Installing the VulnCheck CLI is built for workstations, and has two sharp edges in CI:
  • It requires glibc. On Alpine and other musl images it exits with Unsupported operating system.
  • --sudo assumes sudo exists. In a container running as root it prints sudo: command not found, then reports Installation complete! and exits 0 without installing anything the failure only surfaces later as vulncheck: command not found.
It also resolves the latest release through the unauthenticated GitHub API, which is rate limited per IP and shared across every job on a hosted runner.

The binary itself is statically linked and runs anywhere, Alpine included. It is only the install script that is glibc-only. Use the tarball above on musl images.

2. Authenticate

Store your token as a masked secret in your CI platform and expose it as VC_TOKEN. The CLI reads it automatically; there is no auth login step in CI.

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

VC_TOKEN takes precedence over any saved config file. The CLI also detects CI automatically — CI, BUILD_NUMBER or RUN_ID being set implies --no-interactive, so it never blocks on a prompt.

3. Scan

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

Writing to a file and printing your own summary keeps the build log readable. In --json mode a clean scan prints only {"schema_version": 1}, with no message at all.

Results are an array of findings, each carrying everything you need for a policy decision:

{
  "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. Gate the build

vulncheck scan exits 0 whether or not it finds vulnerabilities. Finding a critical CVE is a successful scan, not a failed command. Every pipeline therefore needs an explicit gate — without one, your job goes green with findings in the log.

This gate fails the build on any finding at or above a CVSS base score, or in VulnCheck KEV at any score:

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
}

The // [] is load-bearing. A clean scan omits the vulnerabilities key entirely, and jq exits 5 with Cannot iterate over null if you iterate it unguarded failing your build on a repository with nothing wrong with it.

So is the guard around cvss_base_score. Not every finding carries a CVSS score, and tonumber on a missing, null or empty value aborts the whole jq program with the same exit 5 before .in_kev is ever evaluated. Writing it as .cvss_base_score // 0 | tonumber? // 0 scores those findings 0, so a scoreless VulnCheck KEV entry is still caught by the in_kev half of the test rather than crashing the gate.

Gating on in_kev is worth keeping even when your threshold is high: known-exploited vulnerabilities matter regardless of score. Other fields you can gate on are cvss_temporal_score, metrics.epss.epss_score, and metrics.ssvc[].exploitation.

Start with a high threshold on an existing codebase so the first pipeline run does not block every merge, then tighten it. Many teams run the gate as a warning on their default branch and as a hard failure on pull requests.

5. Publish results

Keep scan.json as a build artifact it is the record of what was known at build time. The CLI also emits a CycloneDX 1.7 SBOM, which is worth publishing alongside it:

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

Exit codes

The scan result never changes the exit code, but failures do. Match on these when a pipeline breaks:

CodeMeaningUsual cause in CI
0SuccessIncluding scans that found vulnerabilities
1Internal errorA scan path that cannot be resolved — see below
2Validation failureBad arguments or flags
3Auth failureVC_TOKEN missing, expired, or not exposed to the job
4Not foundNo such index or resource
5Rate limitedConcurrent jobs sharing one token
6Network failureRunner cannot reach api.vulncheck.com

In --json mode, errors arrive on stdout as a structured envelope you can match on:

{
  "schema_version": 1,
  "error": {
    "code": "auth_invalid",
    "message": "unauthorized: token is missing or invalid",
    "http_status": 401
  }
}
A mistyped scan path returns code: "internal" at exit 1, and the message is a list of failed snap, docker, podman, containerd and OCI registry lookups. scan also accepts container image references, so it tries to resolve yours as one. If you see provider resolution errors, check the path you passed before anything else.

Air-gapped and offline runners

Runners without internet access can scan against locally cached indices with vulncheck scan --offline. See Offline mode for syncing and caching indices, and Offline Backups for the underlying data.