OWASP ZAP in CI without breaking every build

Dynamic scanning in a pipeline fails for predictable reasons: no authentication, no seeded data, a scan that takes forty minutes and a threshold that fails builds on informational alerts. How to configure ZAP so the result is trusted.

Putting a dynamic application security scanner in the pipeline sounds obviously right and usually ends one of two ways. Either it fails builds for cookie flags on a static asset until somebody sets it to never fail, or it runs unauthenticated against a login page and reports nothing for a year while everyone assumes they are covered.

OWASP ZAP works well in CI, but only with four things configured deliberately.

Scan a deployed environment, not a container you just started

ZAP needs an application with data in it. A freshly started container with an empty database has no objects to enumerate, no forms with realistic state and no authenticated area worth exploring. The scan finds the login page and the health endpoint.

Run it against the ephemeral preview environment or the staging deployment, after the seed data step. If your pipeline does not create one, that is the first thing to build; everything below depends on it.

Authenticate, and prove you stayed authenticated

The automation framework handles this in a YAML plan rather than the old command-line options:

env:
  contexts:
    - name: app
      urls: [ "https://staging.example.com/" ]
      includePaths: [ "https://staging.example.com/.*" ]
      excludePaths: [ ".*/logout.*", ".*/admin/danger.*" ]
      authentication:
        method: browser
        parameters:
          loginPageUrl: "https://staging.example.com/login"
      sessionManagement: { method: headers }
      users:
        - name: tester
          credentials: { username: "${ZAP_USER}", password: "${ZAP_PASS}" }
jobs:
  - type: spiderAjax
    parameters: { context: app, user: tester, maxDuration: 5 }
  - type: activeScan
    parameters: { context: app, user: tester, maxRuleDurationInMins: 3 }
  - type: report
    parameters: { template: sarif-json, reportFile: zap.sarif }

The excludePaths for logout is not optional. Neither is a verification regex that tells ZAP what a logged-in page looks like, so it re-authenticates instead of silently scanning as an anonymous user. Check the run's statistics for the number of authenticated requests; if it is near zero, the scan was decorative.

Spider a single-page application with the AJAX spider

The traditional spider follows links in returned HTML. A React or Vue application returns an empty div and a bundle, so the traditional spider finds one URL. The AJAX spider drives a real browser and follows what the application actually does.

Even so, it will miss routes that need a specific state to reach. Import the API definition directly — ZAP reads OpenAPI, GraphQL and SOAP — so the scanner sees every endpoint rather than the ones a crawler stumbled into. In an API-first product this import matters far more than the spider does.

Fail on what is worth failing on

The default alert thresholds will fail your build on X-Content-Type-Options missing from a PNG. Do this instead:

  • Maintain an alert filter file that downgrades known-accepted alerts to informational, with a comment explaining why each one is accepted and who decided.
  • Fail the build only on High confidence, High or Medium risk alerts.
  • Write everything else to the report and publish it as a build artefact.

If your platform ingests SARIF — GitHub code scanning does — output SARIF and let the code scanning UI handle the triage, deduplication and "new since last run" logic. That is much better than a pass/fail gate, because it puts the finding next to the code without blocking a release on a false positive.

Keep the scan inside the pipeline's time budget

A full active scan of a large application takes hours. Nobody waits hours for a pull request. Split it:

  • On every pull request: the baseline scan (passive only, no attacks) plus the API import. Two or three minutes, catches header and cookie regressions and any new unauthenticated endpoint.
  • Nightly against staging: the full authenticated active scan, with the results going to the security backlog rather than to a build status.

That split is the difference between a scanner that developers tolerate and one they disable.

What it finds and what it does not

ZAP is reliable on the injection classes, on missing security headers, on cookie attributes, on mixed content, on directory listings and on some SQL and command injection cases where the response differs measurably. Those are real findings and they are cheap to catch continuously.

It does not find broken authorisation, which is the flaw most likely to actually hurt you, because it has no model of which objects belong to which user. It does not find business logic bugs. It does not understand that the endpoint it just called ten thousand times sent ten thousand emails.

So treat ZAP the way you treat a linter: it catches the regressions a human already knows about, cheaply and constantly, and frees the human to look for what a scanner cannot see. Both belong in the programme; neither replaces the other.

ConsultorIA

Want this done on your cloud?

A ten-day read-only assessment is free, and Skyline lets you see your estate on a map before you write to us.

Related articles