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 | Guide | Integration |
|---|---|---|
| GitHub Actions | VulnCheck in GitHub Actions | Turnkey vulncheck-oss/action |
| GitLab CI/CD | VulnCheck in GitLab CI/CD | CLI |
| Jenkins | VulnCheck in Jenkins | CLI |
| Azure Pipelines | VulnCheck in Azure Pipelines | CLI |
| Bitbucket, CircleCI, Drone, Woodpecker, Buildkite | Other CI platforms | CLI |
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.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
install.sh convenience script documented in Installing the VulnCheck CLI is built for workstations, and has two sharp edges in CI: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.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.
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.
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": [] }
}
]
}
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.
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
The scan result never changes the exit code, but failures do. Match on these when a pipeline breaks:
| Code | Meaning | Usual cause in CI |
|---|---|---|
| 0 | Success | Including scans that found vulnerabilities |
| 1 | Internal error | A scan path that cannot be resolved — see below |
| 2 | Validation failure | Bad arguments or flags |
| 3 | Auth failure | VC_TOKEN missing, expired, or not exposed to the job |
| 4 | Not found | No such index or resource |
| 5 | Rate limited | Concurrent jobs sharing one token |
| 6 | Network failure | Runner 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
}
}
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.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.