Integrations

Jenkins Security Scan for Docker Images: Full Pipeline

Published July 25, 2026 · 9 min read

A Jenkins security scan stage for your Docker images is straightforward to add and easy to get wrong in the details — credentials handling, agent choice, and what actually fails the build. This guide builds a complete declarative Jenkinsfile that builds, scans, archives the report, and gates on severity, and explains each stage.

Scanning Docker images in a Jenkins pipeline

Why run a Jenkins security scan in the pipeline

Jenkins is often the system of record for what actually got built and deployed, which makes it a natural place to attach a security gate: the scan result lives next to the build number and the commit that produced it, and a failed stage is a signal every downstream job (deploy, promote, release) can check before proceeding.

The pipeline below follows the same build-artifact pattern used in our general image-scanning guide and in the GitHub Actions version of this workflow: build the image, export it to a tar with docker save, and scan the artifact you actually produced rather than a registry copy.

The complete Jenkinsfile

Save this as Jenkinsfile at the root of your repository. It requires an agent with Docker available and assumes an NVD_API_KEY Secret text credential has been configured in Jenkins:

pipeline {
  agent { label 'docker' }

  environment {
    NVD_API_KEY = credentials('nvd-api-key')
    IMAGE_TAR   = 'myapp.tar'
  }

  stages {
    stage('Build image') {
      steps {
        sh 'docker build -t myapp:ci .'
        sh "docker save myapp:ci -o ${IMAGE_TAR}"
      }
    }

    stage('Install ScanRook') {
      steps {
        sh 'curl -fsSL https://scanrook.sh/install | sh'
      }
    }

    stage('Scan image') {
      steps {
        sh """
          scanrook scan \
            --file ${IMAGE_TAR} \
            --mode deep \
            --format json \
            --out report.json
        """
      }
    }

    stage('Archive report') {
      steps {
        archiveArtifacts artifacts: 'report.json', fingerprint: true
      }
    }

    stage('Fail on critical or high CVEs') {
      steps {
        script {
          def critical = sh(script: "jq '.summary.critical // 0' report.json", returnStdout: true).trim()
          def high = sh(script: "jq '.summary.high // 0' report.json", returnStdout: true).trim()
          echo "Critical: ${critical}, High: ${high}"
          if (critical.toInteger() > 0 || high.toInteger() > 0) {
            sh "jq '.findings[] | select(.severity == \"CRITICAL\" or .severity == \"HIGH\") | {cve, package: .package.name, version: .package.version, severity}' report.json"
            error("Failing build: ${critical} critical and ${high} high severity vulnerabilities found")
          }
        }
      }
    }
  }

  post {
    always {
      archiveArtifacts artifacts: 'report.json', allowEmptyArchive: true
    }
  }
}

What each stage does

  • agent { label 'docker' }. The pipeline needs an agent with the Docker daemon and CLI available, since ScanRook scans a saved tar file rather than a running container. Label your Docker-capable agents accordingly in Jenkins node configuration.
  • Credentials binding. credentials('nvd-api-key') pulls the key from the Jenkins credential store into the NVD_API_KEY environment variable at runtime. Jenkins automatically masks the value in console output, which a plain environment variable in the Jenkinsfile would not do.
  • Build and save. Exporting the freshly built image with docker save guarantees the scan covers exactly what this build produced, not a tag that may have been overwritten by a concurrent build on a shared agent.
  • Scripted gate. The final stage uses a script {...} block because declarative syntax alone cannot easily branch on a shell command's output. error() fails the stage (and the build) with a readable message in the Jenkins UI.
  • Double archive. The explicit Archive report stage runs before the gate so the report is attached even on success, and the post { always { ... } } block guarantees it is also archived when the gate stage fails and aborts the run.

Failing the build on severity — and keeping it useful

The same tuning that applies to any CI gate applies here. Start with critical.toInteger() > 0 alone and let high-severity findings appear in the console log without failing the build; a base image can carry a large inherited backlog that has nothing to do with the current change — see how to reduce CVEs in Docker images before tightening the gate to include highs.

For multibranch pipeline jobs, mark this Jenkinsfile stage as a required check in your source control platform's branch protection settings (GitHub, GitLab, or Bitbucket) — Jenkins reporting a failed build is only enforced if the platform is configured to block merges on it.

To gate on newly introduced findings rather than the full inherited set, archive the report from the last successful build on the target branch and diff against it in the scripted stage, failing only when the pull request build introduces CVEs the target branch does not already have.

Scanning images you did not build in this job

The same stages extend to images that already exist — a vendor base image, or the tag currently running in production. Pull and save it instead of building:

stage('Scan deployed image') {
  steps {
    sh 'docker pull registry.example.com/myapp:prod'
    sh 'docker save registry.example.com/myapp:prod -o deployed.tar'
    sh 'scanrook scan --file deployed.tar --format json --out deployed.json'
    archiveArtifacts artifacts: 'deployed.json'
  }
}

Add this as a stage in a separate, cron-triggered Jenkins pipeline job rather than the per-commit build job. Comparing its report against the latest per-commit build tells you whether what is running in production has drifted from what the repository would ship today.

Operational notes

  • Always reference credentials through Jenkins Credentials Binding; never paste an API key directly into a Jenkinsfile, even one stored in a private repository.
  • For agents provisioned per build (Kubernetes or ephemeral cloud agents), there is no persistent cache directory by default — mount a shared volume or accept the warm-up cost on every run.
  • For organizations building many images, move the scan stages into a Shared Library so every Jenkinsfile calls one maintained pipeline function instead of duplicating the scripted gate logic.
  • Retain archived report.json artifacts according to your Jenkins job's discard-old-builds policy; several compliance frameworks expect a retrievable scan history tied to build numbers.

A maintained reference pipeline, including a Shared Library variant, is documented in the ScanRook docs.

Frequently asked questions

How do I scan a Docker image in Jenkins?

Build the image, export it with docker save, install the scanner CLI, and scan the tar. A scripted stage reads the JSON report and calls error() past your severity threshold.

Should scans run on every branch?

Pull request builds catch what you are introducing; a separate cron-triggered job on the main branch catches advisories published after merge.

How do I store the NVD API key securely?

As a Secret text credential in Jenkins, referenced with credentials() in the environment block — never inline in the Jenkinsfile.

Can builds cache the vulnerability database?

Yes, if the agent has a persistent path outside the ephemeral workspace. Ephemeral per-build agents will re-warm the cache on every run.

Add ScanRook to your Jenkins pipeline

Drop the Jenkinsfile above into your repository and every image your pipeline builds is checked against OSV, NVD, and vendor advisory data before it ships — with JSON reports you can archive, gate on, and diff between builds.

Related Posts

More on this topic.