CI/CD Pipelines

Jenkins

Add a VulnCheck vulnerability gate to a Jenkins declarative pipeline.

VulnCheck runs in Jenkins through the VulnCheck CLI no plugin to install. The pipeline below installs a pinned CLI, scans the workspace, fails the build on findings that breach your policy, and archives the results.

Store your token

Create a VulnCheck API token from API Tokens, then add it to Jenkins as a Secret text credential:

The pipeline binds that credential to VC_TOKEN with withCredentials, so the token is masked in the build log and never lands in the environment of unrelated steps.

Declarative pipeline

pipeline {
    agent {
        docker {
            image 'debian:bookworm-slim'
            args '-u root'
        }
    }

    environment {
        VC_CLI_VERSION    = '1.1.0'
        VC_CVSS_THRESHOLD = '7.0'
    }

    stages {
        stage('Install the VulnCheck CLI') {
            steps {
                sh '''
                    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
                '''
            }
        }

        stage('Scan') {
            steps {
                withCredentials([string(credentialsId: 'vulncheck-api-token', variable: 'VC_TOKEN')]) {
                    sh '''
                        vulncheck scan . --json > scan.json
                        jq -r '"\\((.vulnerabilities // []) | length) vulnerabilities found"' scan.json
                    '''
                }
            }
        }

        stage('Gate on findings') {
            steps {
                sh '''
                    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
                    }
                '''
            }
        }
    }

    post {
        always {
            archiveArtifacts artifacts: 'scan.json', allowEmptyArchive: true
        }
    }
}
Note the doubled backslashes in the jq programs — \\( rather than \(. Jenkins processes escape sequences even inside single-quoted Groovy strings, so an unescaped \( is rejected as an illegal escape character before the shell ever sees it.

Other details worth knowing:

  • The gate is a separate stage on purpose. vulncheck scan exits 0 even when it finds critical vulnerabilities, so the scan stage always succeeds and the policy decision stays visible as its own stage in the build. See VulnCheck in CI/CD.
  • args '-u root' lets the install step write to /usr/local/bin. Without it, install to a writable location and add it to PATH instead.
  • archiveArtifacts in post { always { … } } keeps scan.json from failed builds, the runs you most want the evidence from.
  • Pin VC_CLI_VERSION. Every build then scans with a known CLI, and you avoid the unauthenticated GitHub API call that install.sh makes to resolve the latest release.

Warn instead of failing

To surface findings without blocking the build, mark it unstable rather than failed:

        stage('Gate on findings') {
            steps {
                script {
                    def breaches = sh(returnStatus: true, script: '''
                        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
                    ''')
                    if (breaches != 0) {
                        unstable('VulnCheck policy breach — see scan.json')
                    }
                }
            }
        }

A common arrangement is to fail on change requests and only mark the branch build unstable, so a newly published CVE never blocks unrelated work:

                    if (breaches != 0) {
                        if (env.CHANGE_ID) {
                            error('VulnCheck policy breach on this change request')
                        } else {
                            unstable('VulnCheck policy breach on the branch build')
                        }
                    }

Agents without Docker

On a plain agent, install the CLI once on the host through your configuration management, or a one-off admin step and drop the install stage entirely:

pipeline {
    agent { label 'linux' }

    environment {
        VC_CVSS_THRESHOLD = '7.0'
    }

    stages {
        stage('Scan') {
            steps {
                withCredentials([string(credentialsId: 'vulncheck-api-token', variable: 'VC_TOKEN')]) {
                    sh 'vulncheck version'
                    sh 'vulncheck scan . --json > scan.json'
                }
            }
        }
    }
}

Keep vulncheck version in the build so the log records which CLI produced the results.

Troubleshooting

SymptomCause
illegal escape character when the pipeline loadsA jq string interpolation written as \( inside a Groovy string. Use \\(.
vulncheck: command not found after a successful install stageinstall.sh --sudo in a container running as root: sudo does not exist, and the script reports success anyway. Use the tarball install above.
Build fails with exit 3The credential is missing or the binding did not apply — check that the sh step using vulncheck is inside the withCredentials block.
Build fails with exit 5Rate limited, usually many executors sharing one token.
Cannot iterate over null from jqThe vulnerabilities key is absent on a clean scan. Use (.vulnerabilities // [])[], as above.
null (null) cannot be parsed as a number from jqA finding with no cvss_base_score. Use .cvss_base_score // 0 | tonumber? // 0, as above.