CI/CD Pipelines

GitHub Actions

Scan pull requests with the VulnCheck Action, and post findings straight into the review.

The VulnCheck Action is our turnkey CI integration. It wraps the VulnCheck CLI, scans your repository on every pull request, fails the run on findings, and posts them as a pull request comment so reviewers see them without leaving the review.

Quickstart

Store your VulnCheck API token as a repository or organization secret named VC_TOKEN in the examples below, then add the workflow:

name: Scan with VulnCheck

on:
  pull_request:
    branches:
      - main

permissions: write-all

jobs:
  scan:
    name: Scan with VulnCheck
    runs-on: ubuntu-latest
    steps:
      - uses: vulncheck-oss/action@v1
        with:
          command: scan
          token: ${{ secrets.VC_TOKEN }}

That is the whole integration. The action checks out nothing of its own, so add actions/checkout first if your workflow needs the working tree for other steps.

token is your VulnCheck API token create one from API Tokens and store it as a secret. Never inline it in the workflow file.

Inputs

token is the only required input.

InputDescriptionDefault
tokenVulnCheck API token
commandCommand to runscan
scan-pathPath to scan./
scan-cvss-base-thresholdCVSS base score threshold
scan-cvss-temporal-thresholdCVSS temporal score threshold
scan-cve-detailsAnnotate every finding with package type, cataloger and locationsfalse
scan-cve-npm-relUse npm to trace a CVE package to its ownerfalse
disable-pr-commentSkip posting the scan result as a pull request commentfalse
github-tokenToken used to create the authenticated GitHub client${{ github.token }}

With scan-cve-details enabled, each finding also arrives as a workflow annotation:

Notice: CVE-2021-23337 found in npm package lodash in /package-lock.json using javascript-lock-cataloger
Notice: CVE-2021-44906 found in npm package minimist in /package-lock.json using javascript-lock-cataloger

Failing the build

Unlike the CLI, the action decides the outcome for you:

ConfigurationThe action fails the run when
No threshold setAny vulnerability is found, at any severity
scan-cvss-base-threshold and/or scan-cvss-temporal-threshold setA finding is at or above one of the thresholds

The default is strict: a single low-severity finding fails the run. Set a threshold to narrow that to what actually breaches your policy.

- uses: vulncheck-oss/action@v1
  with:
    command: scan
    token: ${{ secrets.VC_TOKEN }}
    scan-cvss-base-threshold: '7.0'

Setting either threshold also changes the pull request comment: findings are split into those above the threshold and those below, rather than presented as one list.

This is the one place the action departs from every other platform in this section. vulncheck scan on its own always exits 0, which is why the CLI recipes add an explicit jq gate. The action applies the policy in its own JavaScript, so do not add a gate of your own on top of it.

Outputs

OutputDescription
scan-outputResults of the scan
scan-countNumber of vulnerabilities found
scan-signatureSHA256 hash of the results, for detecting change between runs

These are for consuming the results, not for gating on them: the action has already failed the run by the time a later step could read them, so any step that uses an output needs if: always() to run at all.

- uses: vulncheck-oss/action@v1
  id: vulncheck
  with:
    command: scan
    token: ${{ secrets.VC_TOKEN }}
    scan-cvss-base-threshold: '7.0'

- name: Save the scan results
  if: always()
  env:
    SCAN_COUNT: ${{ steps.vulncheck.outputs.scan-count }}
    SCAN_OUTPUT: ${{ steps.vulncheck.outputs.scan-output }}
  run: |
    echo "$SCAN_COUNT vulnerabilities found"
    printf '%s' "$SCAN_OUTPUT" > scan.json

- uses: actions/upload-artifact@v4
  if: always()
  with:
    name: vulncheck-scan
    path: scan.json

Reading the outputs through env: rather than interpolating them straight into the run: script keeps a value that came out of a scan from being parsed as shell.

Permissions

Posting pull request comments needs write access:

permissions: write-all

If you would rather not grant that, set disable-pr-comment: true. The run still fails on findings — the comment is presentation, not the gate — you just lose the summary in the review.

Pull requests from forks cannot be scanned. GitHub withholds repository secrets from pull_request runs raised from a fork, so secrets.VC_TOKEN arrives empty and the action fails on authentication. Skip those runs with if: github.event.pull_request.head.repo.full_name == github.repository on the job, and let a scan on your default branch cover the code once it merges.

On repeat runs of the same pull request the action compares the new scan-signature against its previous comment: an unchanged result is not commented again, and a changed one is posted as the change.

Pinning

@v1 tracks the latest v1 release. To pin exactly, use a full release tag such as vulncheck-oss/action@v1.1.5, or a commit SHA if your organization requires immutable references.

Self-hosted runners and GitHub Enterprise Server

The action runs on node24 and needs no additional tooling on the runner. Where third-party actions are blocked by policy, or on a GitHub Enterprise Server instance without access to the public marketplace, drive the CLI directly - same result, no marketplace dependency:

name: Scan with VulnCheck CLI

on:
  pull_request:

jobs:
  scan:
    runs-on: ubuntu-latest
    env:
      VC_TOKEN: ${{ secrets.VC_TOKEN }}
      VC_CLI_VERSION: 1.1.0
      VC_CVSS_THRESHOLD: '7.0'
    steps:
      - uses: actions/checkout@v6

      - name: Install the VulnCheck CLI
        run: |
          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

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

      - name: Gate on findings
        run: |
          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 "::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.json
            exit 1
          }

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: vulncheck-scan
          path: scan.json

See VulnCheck in CI/CD for what each of those steps is doing, and why the gate is a separate step.