How to Set Up Jenkins for Security Scanning with OWASP ZAP

How to Set Up Jenkins for Security Scanning with OWASP ZAP

Security testing used to be something I bolted on right before a release, usually as a manual pentest a week before launch. That’s a terrible way to catch vulnerabilities — by the time you find them, the code has already moved three sprints ahead. Adding OWASP ZAP (Zed Attack Proxy) into a Jenkins pipeline changes that entirely: every build gets an automated security scan, and vulnerabilities show up as build artifacts right next to your test results.

What is OWASP ZAP

OWASP ZAP is a free, open-source dynamic application security testing (DAST) tool maintained by the OWASP Foundation. Unlike static analysis tools that read source code, ZAP actively attacks a running application — probing for SQL injection, XSS, broken authentication, insecure headers, and dozens of other issues — the same way an attacker would.

Jenkins Architecture Refresher

For a security pipeline, it helps to think in terms of stages that build up context:

  • Controller: Orchestrates the pipeline and stores scan reports as build artifacts.
  • Agent: Where the application is deployed to a test environment and where ZAP actually runs its scan.
  • Pipeline: Deploys the app, runs the scan, evaluates results against a risk threshold, and fails the build if critical vulnerabilities are found.

This “deploy then attack” model means ZAP needs a live, reachable instance of your application — typically a staging or ephemeral test environment spun up specifically for the scan.

Prerequisites

  • Jenkins (2.4+ LTS) with Docker available on the agent (the easiest way to run ZAP)
  • A deployable build of your application (container image or a runnable artifact)
  • Basic understanding of HTTP and web application structure
  • Docker installed on the Jenkins agent, or ZAP installed natively

Step 1: Install Docker on the Jenkins Agent

sudo apt update
sudo apt install -y docker.io
sudo usermod -aG docker jenkins
sudo systemctl restart jenkins

Running ZAP via its official Docker image (zaproxy/zap-stable) avoids manual installation and keeps the scanner version consistent across builds.

Step 2: Pull the ZAP Docker Image

docker pull zaproxy/zap-stable

Step 3: Install Supporting Jenkins Plugins

  • HTML Publisher Plugin — to publish the ZAP HTML report as a build artifact
  • Docker Pipeline Plugin — to run ZAP as a container step directly from the Jenkinsfile

Step 4: Write the Jenkinsfile

Here’s a pipeline that deploys the app to a test environment, runs a ZAP baseline scan, and publishes the results:

pipeline {
    agent any

    environment {
        TARGET_URL = 'http://staging.myapp.internal:8080'
        ZAP_REPORT_DIR = "${WORKSPACE}/zap-report"
    }

    stages {
        stage('Checkout') {
            steps {
                git branch: 'main', url: 'https://github.com/yourorg/webapp.git'
            }
        }

        stage('Build') {
            steps {
                sh 'docker build -t myapp:latest .'
            }
        }

        stage('Deploy to Test Environment') {
            steps {
                sh '''
                    docker rm -f myapp-test || true
                    docker run -d --name myapp-test -p 8080:8080 myapp:latest
                    sleep 15
                '''
            }
        }

        stage('ZAP Baseline Scan') {
            steps {
                sh '''
                    mkdir -p $ZAP_REPORT_DIR
                    docker run --network host -v $ZAP_REPORT_DIR:/zap/wrk/:rw \
                      zaproxy/zap-stable zap-baseline.py \
                      -t $TARGET_URL \
                      -r zap_report.html \
                      -J zap_report.json \
                      -I
                '''
            }
        }

        stage('Evaluate Results') {
            steps {
                script {
                    def report = readJSON file: "${ZAP_REPORT_DIR}/zap_report.json"
                    def highRiskCount = report.site[0].alerts.findAll { it.riskcode.toInteger() >= 3 }.size()
                    if (highRiskCount > 0) {
                        error "ZAP found ${highRiskCount} high-risk vulnerabilities. Failing build."
                    }
                }
            }
        }
    }

    post {
        always {
            publishHTML(target: [
                reportDir: 'zap-report',
                reportFiles: 'zap_report.html',
                reportName: 'OWASP ZAP Security Report'
            ])
            sh 'docker rm -f myapp-test || true'
        }
    }
}

The -I flag tells ZAP to continue rather than exit non-zero on warnings, letting the pipeline’s own Evaluate Results stage decide the failure threshold — this gives you more control than relying purely on ZAP’s exit code.

Baseline Scan vs Full Active Scan

  • Baseline scan (zap-baseline.py): Passive scan, spiders the app briefly, doesn’t send attack payloads. Fast, safe to run against any environment, good for every build.
  • Full scan (zap-full-scan.py): Active scan that sends real attack payloads (SQLi, XSS attempts, etc.). More thorough but slower and should only run against isolated, disposable test environments — never production.

A common pattern is running the baseline scan on every commit and scheduling the full active scan nightly or before a release, on a dedicated ephemeral environment.

stage('Nightly Full Scan') {
    when {
        triggeredBy 'TimerTrigger'
    }
    steps {
        sh '''
            docker run --network host -v $ZAP_REPORT_DIR:/zap/wrk/:rw \
              zaproxy/zap-stable zap-full-scan.py \
              -t $TARGET_URL -r zap_full_report.html -I
        '''
    }
}

Authenticated Scanning

Most real applications sit behind a login. ZAP supports authenticated scans via a context file that describes the login form and session handling:

docker run --network host -v $ZAP_REPORT_DIR:/zap/wrk/:rw \
  zaproxy/zap-stable zap-full-scan.py \
  -t $TARGET_URL \
  -z "-config replacer.full_list(0).description=auth \
      -config replacer.full_list(0).enabled=true \
      -config replacer.full_list(0).matchtype=REQ_HEADER \
      -config replacer.full_list(0).matchstr=Authorization \
      -config replacer.full_list(0).replacement='Bearer $API_TOKEN'"

For form-based login, ZAP’s context file (exported from the ZAP desktop UI) can be mounted into the container and passed with -n /zap/wrk/context.context.

Integrating with Git and Pull Requests

Fail fast by wiring ZAP results into pull request checks. If your pipeline runs on a PR trigger, publish a summarized comment back to GitHub using the GitHub API or a plugin like GitHub Checks API, so reviewers see security findings without leaving the PR.

Real-World Workflow

  1. Every push to a feature branch triggers a build, deploy to an ephemeral container, and a ZAP baseline scan (fast feedback, a few minutes).
  2. Merge to main triggers deployment to staging and a scheduled nightly full active scan.
  3. Release candidates require a clean full scan report as a manual gate before promotion to production.
  4. All reports are archived as build artifacts for audit and compliance purposes.

Security and Operational Best Practices

  • Never run an active scan against production — it sends real attack traffic and can cause data corruption or outages.
  • Isolate the scan target on its own network segment to avoid collateral scanning of unrelated services.
  • Set risk thresholds thoughtfully; failing every build on low-risk informational findings creates alert fatigue and gets the check disabled.
  • Keep the ZAP Docker image updated regularly — vulnerability signatures improve with each release.
  • Store scan history (not just the latest report) to track whether security debt is trending up or down over time.

Troubleshooting

  • ZAP can’t reach the target URL: Confirm the container network mode; --network host is often needed so ZAP inside Docker can reach a locally deployed app.
  • Scan takes too long: Narrow the spider’s scope with -m (max scan duration) or exclude irrelevant URL patterns with a context file.
  • False positives on every build: Add specific alert IDs to a ZAP “ignore” rules file rather than lowering the overall risk threshold globally.

Combining ZAP with Other Security Tools

A single DAST scan is one layer in a much larger security posture, and Jenkins is a natural place to string these layers together into one pipeline:

stage('Dependency Vulnerability Scan') {
    steps {
        sh 'npm audit --audit-level=high'
    }
}

stage('SAST with Semgrep') {
    steps {
        sh 'semgrep --config=auto --json --output=semgrep-report.json .'
    }
}

stage('Container Image Scan with Trivy') {
    steps {
        sh 'trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:latest'
    }
}

stage('ZAP Baseline Scan') {
    steps {
        sh '''
            docker run --network host -v $ZAP_REPORT_DIR:/zap/wrk/:rw \
              zaproxy/zap-stable zap-baseline.py -t $TARGET_URL -r zap_report.html -I
        '''
    }
}

Running dependency scanning, static analysis, container scanning, and dynamic scanning together gives you coverage across the full software supply chain: known-vulnerable libraries, insecure code patterns, vulnerable base images, and runtime behavior — each catching classes of issues the others can’t see.

Tracking Security Debt Over Time

Rather than treating every ZAP finding as a build-breaker, many teams route medium and low-risk findings into a tracked backlog while only failing builds on high/critical issues. A simple pattern is to export ZAP’s JSON report and push a summarized count to a tracking system or dashboard on every run:

stage('Track Security Metrics') {
    steps {
        script {
            def report = readJSON file: "${ZAP_REPORT_DIR}/zap_report.json"
            def counts = [high: 0, medium: 0, low: 0, info: 0]
            report.site[0].alerts.each { alert ->
                def risk = alert.riskcode.toInteger()
                if (risk == 3) counts.high++
                else if (risk == 2) counts.medium++
                else if (risk == 1) counts.low++
                else counts.info++
            }
            echo "Security findings — High: ${counts.high}, Medium: ${counts.medium}, Low: ${counts.low}, Info: ${counts.info}"
        }
    }
}

Trending this over weeks tells you whether the team’s security debt is actually shrinking, which is a far more useful signal than a single pass/fail per build.

FAQs

Does ZAP replace static code analysis (SAST) tools? No — ZAP is dynamic (DAST) and tests the running application; pair it with a SAST tool like SonarQube for source-level vulnerability detection.

Can I run ZAP scans against a Kubernetes-deployed staging environment? Yes, point the -t target at the service’s ingress or NodePort URL; just ensure the Jenkins agent has network access to the cluster.

Is the baseline scan safe to run against a shared staging environment used by other teams? Generally yes, since it’s passive, but always confirm with your security team — even passive scans generate noticeable traffic.

How do I keep security findings from blocking every single deploy? Set the failure threshold to high/critical findings only, and route medium/low findings to a tracked backlog instead of failing the build outright.

Summary

Bolting OWASP ZAP onto a Jenkins pipeline turns security testing from a pre-release scramble into a routine, automated part of every deployment. Start with fast baseline scans on every commit, layer in scheduled full active scans against isolated environments, and use Jenkins’ own scripting to decide what severity actually blocks a release. The result is vulnerabilities caught in minutes instead of months.

References

Total
1
Shares

Leave a Reply

Previous Post
How to Set Up Jenkins for Continuous Deployment to Heroku

How to Set Up Jenkins for Continuous Deployment to Heroku

Next Post
How to Use Jenkins with Splunk for Log Analysis

How to Use Jenkins with Splunk for Log Analysis

Related Posts