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.
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.token is the only required input.
| Input | Description | Default |
|---|---|---|
token | VulnCheck API token | — |
command | Command to run | scan |
scan-path | Path to scan | ./ |
scan-cvss-base-threshold | CVSS base score threshold | — |
scan-cvss-temporal-threshold | CVSS temporal score threshold | — |
scan-cve-details | Annotate every finding with package type, cataloger and locations | false |
scan-cve-npm-rel | Use npm to trace a CVE package to its owner | false |
disable-pr-comment | Skip posting the scan result as a pull request comment | false |
github-token | Token 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
Unlike the CLI, the action decides the outcome for you:
| Configuration | The action fails the run when |
|---|---|
| No threshold set | Any vulnerability is found, at any severity |
scan-cvss-base-threshold and/or scan-cvss-temporal-threshold set | A 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.
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.| Output | Description |
|---|---|
scan-output | Results of the scan |
scan-count | Number of vulnerabilities found |
scan-signature | SHA256 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.
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.
@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.
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.