Using OWASP ZAP for CI/CD: Integrating Security into Your Pipeline

Using OWASP ZAP for CI/CD: Integrating Security into Your Pipeline

Integrating security testing into CI/CD ensures vulnerabilities are found early, reducing risk and remediation cost. OWASP ZAP (Zed Attack Proxy) is a free, open-source web application security scanner designed to be automation-friendly, making it a solid choice for embedding dynamic application security testing (DAST) into your pipeline. This article shows a practical, prescriptive approach to add ZAP to a CI/CD workflow.

Why add ZAP to CI/CD

  • Shift-left security: catch runtime issues before release.
  • Automatable: CLI, Docker, REST API and CI-friendly reports.
  • Actionable output: alerts mapped to risk levels to guide fixes.
  • Cost-effective: open-source tool with active community and add-ons.

High-level strategy

  1. Run ZAP against a deployable test instance (staging or ephemeral environment).
  2. Use ZAP in two modes: lightweight quick scans for every commit and deeper scans for nightly builds or pre-release gates.
  3. Fail builds on configurable thresholds (e.g., any High or >X Medium alerts).
  4. Collect and publish reports (HTML, JSON, JUnit) for developers and security teams.
  5. Triage alerts, add suppressions for false positives, and track real fixes in issue trackers.

Pipeline placement and environment

  • Run ZAP after application is deployed to a test environment and before release.
  • Prefer ephemeral environments spun up per branch or pull request (preview environments) to avoid interference and ensure reproducible scans.
  • Ensure test instance has representative data and authentication flows if required.

Modes of use

  • Passive scanning: observe traffic (no active attacks) — safe and fast for PR checks.
  • Active scanning: probes for vulnerabilities — use in isolated test environments only.
  • Spidering / Crawling: discover endpoints to test.
  • Authentication handling: use ZAP’s authentication mechanisms (form-based, token, OAuth) or replay recorded authenticated sessions.

Getting started: quick setup (Docker)

  1. Ensure your app is deployed at a reachable URL (e.g., http://app-test.local).
  2. Run ZAP Docker image for an unauthenticated quick scan:

    Code

    docker run –rm -v \((pwd)/zap_reports:/zap/reports owasp/zap2docker-stable zap-baseline.py -t http://app-test.local -r zap_report.html </span></code></div></div></pre> <ul> <li>zap-baseline.py runs an automated baseline scan (passive + limited active checks).</li> <li>Output HTML is saved to ./zap_reports/zap_report.html.</li> </ul> </li> </ol> <h3>Deeper scans and authenticated flows</h3> <ul> <li>Use zap-full-scan.py for deeper active scans: <pre><div class="XG2rBS5V967VhGTCEN1k"><div class="nHykNMmtaaTJMjgzStID"><div class="HsT0RHFbNELC00WicOi8"><i><svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M15.434 7.51c.137.137.212.311.212.49a.694.694 0 0 1-.212.5l-3.54 3.5a.893.893 0 0 1-.277.18 1.024 1.024 0 0 1-.684.038.945.945 0 0 1-.302-.148.787.787 0 0 1-.213-.234.652.652 0 0 1-.045-.58.74.74 0 0 1 .175-.256l3.045-3-3.045-3a.69.69 0 0 1-.22-.55.723.723 0 0 1 .303-.52 1 1 0 0 1 .648-.186.962.962 0 0 1 .614.256l3.541 3.51Zm-12.281 0A.695.695 0 0 0 2.94 8a.694.694 0 0 0 .213.5l3.54 3.5a.893.893 0 0 0 .277.18 1.024 1.024 0 0 0 .684.038.945.945 0 0 0 .302-.148.788.788 0 0 0 .213-.234.651.651 0 0 0 .045-.58.74.74 0 0 0-.175-.256L4.994 8l3.045-3a.69.69 0 0 0 .22-.55.723.723 0 0 0-.303-.52 1 1 0 0 0-.648-.186.962.962 0 0 0-.615.256l-3.54 3.51Z"></path></svg></i><p class="li3asHIMe05JPmtJCytG wZ4JdaHxSAhGy1HoNVja cPy9QU4brI7VQXFNPEvF">Code</p></div><div class="CF2lgtGWtYUYmTULoX44"><button type="button" class="st68fcLUUT0dNcuLLB2_ ffON2NH02oMAcqyoh2UU MQCbz04ET5EljRmK3YpQ CPXAhl7VTkj2dHDyAYAf" data-copycode="true" role="button" aria-label="Copy Code"><svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M9.975 1h.09a3.2 3.2 0 0 1 3.202 3.201v1.924a.754.754 0 0 1-.017.16l1.23 1.353A2 2 0 0 1 15 8.983V14a2 2 0 0 1-2 2H8a2 2 0 0 1-1.733-1H4.183a3.201 3.201 0 0 1-3.2-3.201V4.201a3.2 3.2 0 0 1 3.04-3.197A1.25 1.25 0 0 1 5.25 0h3.5c.604 0 1.109.43 1.225 1ZM4.249 2.5h-.066a1.7 1.7 0 0 0-1.7 1.701v7.598c0 .94.761 1.701 1.7 1.701H6V7a2 2 0 0 1 2-2h3.197c.195 0 .387.028.57.083v-.882A1.7 1.7 0 0 0 10.066 2.5H9.75c-.228.304-.591.5-1 .5h-3.5c-.41 0-.772-.196-1-.5ZM5 1.75v-.5A.25.25 0 0 1 5.25 1h3.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-3.5A.25.25 0 0 1 5 1.75ZM7.5 7a.5.5 0 0 1 .5-.5h3V9a1 1 0 0 0 1 1h1.5v4a.5.5 0 0 1-.5.5H8a.5.5 0 0 1-.5-.5V7Zm6 2v-.017a.5.5 0 0 0-.13-.336L12 7.14V9h1.5Z"></path></svg>Copy Code</button><button type="button" class="st68fcLUUT0dNcuLLB2_ WtfzoAXPoZC2mMqcexgL ffON2NH02oMAcqyoh2UU MQCbz04ET5EljRmK3YpQ GnLX_jUB3Jn3idluie7R"><svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" fill-rule="evenodd" d="M20.618 4.214a1 1 0 0 1 .168 1.404l-11 14a1 1 0 0 1-1.554.022l-5-6a1 1 0 0 1 1.536-1.28l4.21 5.05L19.213 4.382a1 1 0 0 1 1.404-.168Z" clip-rule="evenodd"></path></svg>Copied</button></div></div><div class="mtDfw7oSa1WexjXyzs9y" style="color: var(--sds-color-text-01); font-family: var(--sds-font-family-monospace); direction: ltr; text-align: left; white-space: pre; word-spacing: normal; word-break: normal; font-size: var(--sds-font-size-label); line-height: 1.2em; tab-size: 4; hyphens: none; padding: var(--sds-space-x02, 8px) var(--sds-space-x04, 16px) var(--sds-space-x04, 16px); margin: 0px; overflow: auto; border: none; background: transparent;"><code class="language-text" style="color: rgb(57, 58, 52); font-family: Consolas, "Bitstream Vera Sans Mono", "Courier New", Courier, monospace; direction: ltr; text-align: left; white-space: pre; word-spacing: normal; word-break: normal; font-size: 0.9em; line-height: 1.2em; tab-size: 4; hyphens: none;"><span>docker run --rm -v \)(pwd)/zap_reports:/zap/reports owasp/zap2docker-stable zap-full-scan.py -t http://app-test.local -r zap_full_report.html
  3. For authenticated apps, either:
    • Configure ZAP context, users, and authentication via the ZAP UI and export a session/API script; or
    • Use the ZAP API/CLI to set context and authentication programmatically before scanning:
      • Start ZAP in daemon mode, use the REST API to define context, login script, and user, then run scans.
  4. Example: CI job (GitHub Actions)

    • Quick baseline scan on pull requests, fail only on High alerts:

      ”` name: zap-scan

      on: [pull_request]

      jobs: zap: runs-on: ubuntu-latest steps:

      • name: Checkout uses: actions/checkout@v4

        Code

        - name: Start test environment run: | # start app in background or ensure reachable URL docker-compose -f docker-compose.test.yml up -d

        • name: Run ZAP baseline run: | docker run –network host –rm -v ${{ github.workspace }}/zap_reports:/zap/reports owasp/zap2docker-stable zap-baseline.py -t http://localhost:8080 -r zap_report.html -J zap_report.json

        • name: Fail on high alerts run: | jq ‘.site[].alerts[] | select(.risk == “High”)’ zap_reports/zap_report.json | grep -q ‘.’ && (echo “High risk

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *