In the previous lesson we walked through BazarNube's dynamic scanning by hand: explore, passive and active scan, interpret alerts, and generate a report. It works, but it doesn't scale: no one is going to open ZAP's GUI every time Marc does a push. Security that only happens "when someone remembers" isn't security. In this final lesson of the module we take ZAP into the CI/CD pipeline: we turn the scan into an automatic step that runs on every change, publishes the report as an artifact, and breaks the build when vulnerabilities appear above the agreed threshold. This is DAST inside a DevSecOps flow.

Ethical and legal note (still in force). Automating doesn't change the rules: the active scan attacks for real. The pipeline must always point at BazarNube's own, ephemeral staging (https://staging.bazarnube.local), never at production or third-party services. Automating an attack against a system that isn't yours makes it more dangerous, not less.

Contents

  1. Why automate DAST
  2. The packaged scripts: baseline scan and full scan with Docker
  3. The rules file for managing false positives
  4. The Automation Framework (YAML with jobs)
  5. ZAP's API and daemon mode
  6. Integration with GitHub Actions
  7. Integration with GitLab CI
  8. Thresholds, build failure, and reports as artifacts
  9. A complete DAST pipeline for BazarNube
  10. Common mistakes and tips
  11. Exercises
  12. Conclusion

Why automate DAST

Automating the dynamic scan pursues three goals:

  • Repeatability: the same scan, with the same policy, every time. No forgotten steps.
  • Early detection: a vulnerability found in the pipeline costs much less than one found in production.
  • Governance: the pipeline can block a deployment if something serious appears, applying an objective policy instead of individual judgment.
flowchart LR
    A[Push / MR to BazarNube] --> B[Build and deploy to staging]
    B --> C[ZAP DAST in a container]
    C --> D{Alerts > threshold?}
    D -->|Yes| E[Build FAIL, blocks deploy]
    D -->|No| F[Build OK, publishes report]
    E --> G[Report artifact]
    F --> G

The packaged scripts: baseline scan and full scan with Docker

ZAP ships CI-ready scripts inside the zaproxy/zap-stable image (formerly owasp/zap2docker-stable). The two that matter:

Script What it does Actively attacks Duration When to use it
zap-baseline.py Spider + passive scan; reports headers, cookies, leaks No Short (minutes) On every PR/commit, quick gate
zap-full-scan.py Spider + AJAX Spider + full active scan Yes Long (tens of min) Nightly or pre-release
zap-api-scan.py Scan guided by OpenAPI/SOAP/GraphQL definition Yes Variable APIs (BazarNube's)

Strategy for BazarNube: zap-baseline.py on every Pull Request (fast, non-invasive, suitable as a gate) and zap-full-scan.py in a nightly job against dedicated staging (slow and aggressive, but off the developers' critical path).

Example of a baseline against staging, mounting a volume to collect reports:

docker run --rm -v "$(pwd)/zap-out:/zap/wrk/:rw" \
  -t zaproxy/zap-stable zap-baseline.py \
  -t https://staging.bazarnube.local \
  -r baseline-report.html \
  -J baseline-report.json \
  -w baseline-report.md \
  -c bazarnube-rules.conf

Key flags:

  • -t target URL (always your own staging).
  • -r/-J/-w reports in HTML, JSON, and Markdown.
  • -c rules file (false positives, see below).
  • -I don't fail the build on Info-level alerts (only return an error on WARN/FAIL).
  • -l minimum level that triggers failure (PASS, IGNORE, INFO, WARN, FAIL).

The full scan is identical in form, changing the script:

docker run --rm -v "$(pwd)/zap-out:/zap/wrk/:rw" \
  -t zaproxy/zap-stable zap-full-scan.py \
  -t https://staging.bazarnube.local \
  -r full-report.html -J full-report.json \
  -c bazarnube-rules.conf

The rules file for managing false positives

In 06-03 we did manual triage by marking false positives in the GUI. In CI that won't do: we need the decision to be versioned code. The rules file (-c) lets you fix the handling of each rule by its ID. Format: ID<TAB>action<TAB>URL-regex(optional).

# bazarnube-rules.conf
# ID	Action	URL (optional)
10096	IGNORE	# Timestamp Disclosure: noise, doesn't apply to BazarNube
10021	IGNORE	# X-Content-Type-Options on static CDN assets
10035	WARN	# Strict-Transport-Security: warn, don't break the build yet
40012	FAIL	# Reflected XSS: always breaks the build
40018	FAIL	# SQL Injection: always breaks the build

Possible actions: IGNORE (don't report), WARN (report without failing the build), and FAIL (report and break the build). This way the handling of false positives lives in the repository, is reviewed in a PR, and stays audited: no invisible decisions.

The Automation Framework (YAML with jobs)

The scripts are convenient but rigid. For scans with a context, authentication, and tailored policy (like the ones we defined in 06-02 and 06-03), ZAP offers the Automation Framework: a single YAML file that describes the scan as a list of chained jobs. It's the recommended approach for serious cases.

# zap-bazarnube.yaml
env:
  contexts:
    - name: BazarNube
      urls:
        - https://staging.bazarnube.local
      includePaths:
        - "https://staging.bazarnube.local.*"
      excludePaths:
        - "https://staging.bazarnube.local/logout.*"
        - ".*pay\\.tercero\\.com.*"
      authentication:
        method: form
        parameters:
          loginPageUrl: https://staging.bazarnube.local/login
          loginRequestUrl: https://staging.bazarnube.local/api/auth/login
          loginRequestBody: "username={%username%}&password={%password%}"
      users:
        - name: seller
          credentials:
            username: [email protected]
            password: ${BZN_TEST_PASS}
  parameters:
    failOnError: true

jobs:
  - type: spider
    parameters:
      context: BazarNube
      user: seller
  - type: spiderAjax
    parameters:
      context: BazarNube
      user: seller
  - type: passiveScan-wait
  - type: activeScan
    parameters:
      context: BazarNube
      user: seller
      policy: BazarNube-Web
  - type: report
    parameters:
      template: traditional-html
      reportFile: zap-bazarnube.html
  - type: report
    parameters:
      template: traditional-json
      reportFile: zap-bazarnube.json

Run it with:

docker run --rm -v "$(pwd):/zap/wrk/:rw" \
  -t zaproxy/zap-stable zap.sh -cmd -autorun /zap/wrk/zap-bazarnube.yaml

Note that the test password arrives via an environment variable (${BZN_TEST_PASS}), injected from the CI secrets manager: never credentials in the YAML.

ZAP's API and daemon mode

For full control (integrating ZAP with your own scripts, orchestrating from Python/Node), you start ZAP as a daemon without a GUI and talk to it via its REST API:

# Headless ZAP listening on 8080, with a mandatory API key
docker run -u zap -p 8080:8080 -d --name zap zaproxy/zap-stable \
  zap.sh -daemon -host 0.0.0.0 -port 8080 \
  -config api.key=$ZAP_KEY \
  -config api.addrs.addr.name=.* -config api.addrs.addr.regex=true
  • -daemon starts without a graphical interface.
  • -config api.key=... protects the API with a key (mandatory; don't leave it empty).
  • REST endpoints under /JSON/... to launch the spider, active scan, read alerts, and generate reports (we already used them in 06-03).

Many teams use the official client library instead of raw curl:

from zapv2 import ZAPv2
zap = ZAPv2(apikey='KEY', proxies={'http': 'http://localhost:8080'})
zap.spider.scan('https://staging.bazarnube.local')
zap.ascan.scan('https://staging.bazarnube.local', scanpolicyname='BazarNube-Web')
alerts = zap.core.alerts(baseurl='https://staging.bazarnube.local')

Integration with GitHub Actions

In GitHub, the typical flow is to bring up staging, run the baseline on every PR, and upload the report as an artifact. There's an official zaproxy/action-baseline action:

# .github/workflows/dast.yml
name: DAST BazarNube
on: [pull_request]

jobs:
  zap-baseline:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Bring up BazarNube staging
        run: docker compose -f docker-compose.staging.yml up -d --wait

      - name: ZAP Baseline Scan
        uses: zaproxy/[email protected]
        with:
          target: 'https://staging.bazarnube.local'
          rules_file_name: 'bazarnube-rules.conf'
          cmd_options: '-J baseline-report.json -w baseline-report.md'
          fail_action: true

      - name: Publish report as an artifact
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: zap-report
          path: |
            report_html.html
            baseline-report.json
            baseline-report.md

fail_action: true makes alerts marked as FAIL break the job (and therefore block the merge if the check is required). The upload-artifact step with if: always() guarantees the report is saved even if the build fails, which is exactly when you need it most.

Integration with GitLab CI

In GitLab the pattern is equivalent, using the ZAP image as the job image:

# .gitlab-ci.yml
dast_baseline:
  stage: test
  image: zaproxy/zap-stable
  variables:
    TARGET: "https://staging.bazarnube.local"
  script:
    - mkdir -p zap-out
    - zap-baseline.py -t "$TARGET"
        -c bazarnube-rules.conf
        -r zap-out/report.html -J zap-out/report.json
        || EXIT=$?
    - echo "ZAP exit code $EXIT"
    - '[ "${EXIT:-0}" -lt 2 ] || exit 1'   # 2 = WARN, 3 = FAIL -> breaks the build
  artifacts:
    when: always
    paths:
      - zap-out/
    expire_in: 30 days

Here we handle the exit code by hand: ZAP returns 0 (all OK), 1 (internal error), 2 (there are WARNs), and 3 (there are FAILs). We decide the threshold in the job's own logic. artifacts: when: always serves the same purpose as in GitHub: keep the report even if the job fails.

Thresholds, build failure, and reports as artifacts

The policy for "when to break the build" is a team decision, not a technical one. A healthy progression for BazarNube:

Adoption phase What breaks the build Goal
Introduction Nothing (WARN on everything) Gain visibility without frustrating the team
Consolidation Only confirmed High (SQLi, XSS) Block the serious stuff, tolerate the noise
Maturity High and Medium per rules Raise the bar progressively

Script exit codes, for configuring the gate:

Code Meaning Recommended CI action
0 No findings above the threshold Continue
1 ZAP execution error Fail and investigate
2 At least one WARN Per policy (warn or fail)
3 At least one FAIL Fail the build

And always publish the reports (HTML for humans, JSON to compare across builds, Markdown to paste into the ticket) as artifacts with if: always() / when: always.

A complete DAST pipeline for BazarNube

Putting it all together, BazarNube's strategy combines two gates of different cost:

flowchart TD
    A[PR opened] --> B[Ephemeral deploy to staging]
    B --> C[zap-baseline.py with rules.conf]
    C --> D{FAIL?}
    D -->|Yes| E[Blocks merge + artifact]
    D -->|No| F[Merge allowed + artifact]
    G[Nightly cron] --> H[Deploy stable staging]
    H --> I[Authenticated zap-full-scan.py]
    I --> J[Report to the backlog: BZN-xxx]
  • Per PR: zap-baseline.py (fast, non-invasive) as a required gate. It only breaks the build on FAIL (confirmed SQLi/XSS).
  • Nightly: zap-full-scan.py or the authenticated Automation Framework (slow, aggressive) against dedicated staging. It doesn't block anyone; its findings enter as BZN-xxx tickets in the backlog, already mapped to the Top Ten (06-03).

This way the developer gets fast feedback on their PR and the deep scan happens without slowing the workflow. The SRE maintains the ephemeral staging; Lucía and Marc receive actionable tickets instead of a huge PDF every quarter.

Common Mistakes and Tips

  • Scanning a staging that isn't ready. If the DAST starts before the app responds, it explores nothing. Use --wait/healthchecks before launching ZAP.
  • Leaving the API without a key. An empty -config api.key= exposes the daemon. Generate a key and pass it via a CI secret.
  • Credentials or keys in the YAML. Every secret (test password, ZAP_KEY) goes through CI variables/secrets, never versioned.
  • Making the full scan a gate on every PR. It's too slow and aggressive; it frustrates the team and slows the merge. Baseline on PR, full scan nightly.
  • Not publishing the report when the build fails. Without if: always()/when: always you lose exactly the report of the failure.
  • Managing false positives "by eye" every run. Put them in the versioned rules file; have them reviewed in a PR.
  • Setting the bar to the maximum from day one. If everything breaks the build at once, the team will disable the scan. Raise the threshold in phases.

Exercises

Exercise 1. Marc proposes making zap-full-scan.py a required gate on every BazarNube Pull Request. Explain why this isn't a good idea and what two-tier strategy you'd propose instead.

Exercise 2. The baseline persistently reports alert 10096 (Timestamp Disclosure), which the team has already verified as a false positive. Write the line of the bazarnube-rules.conf file to silence it and explain why this is better than marking it in the GUI.

Exercise 3. In GitLab, your ZAP job ends with exit code 3 but the pipeline shows green. What's happening and how do you fix it so that case breaks the build and keeps the report?

Solutions

Solution 1. The full scan performs a complete active scan with the Spider and AJAX Spider: it takes tens of minutes and attacks for real, so as a gate on every PR it would slow down development and create friction. Recommended strategy: baseline on every PR (fast, passive, breaks the build only on FAIL) and a nightly full scan against dedicated staging, whose findings enter the backlog as tickets without blocking anyone.

Solution 2. Line of the rules file:

10096	IGNORE	# Timestamp Disclosure: false positive verified by the team

It's better than the GUI because the decision stays versioned in the repository: it's reviewed in a PR, it has an author and a date, it's applied identically on every CI run, and it's auditable. A Mark as False Positive in the GUI lives only in one person's local session and doesn't propagate to the pipeline.

Solution 3. The script returns 3 (there are FAIL alerts), but the job doesn't propagate that code, probably because it was captured with || true or isn't translated into exit 1. You have to evaluate the exit code and force the failure, while always keeping the artifacts:

script:
  - zap-baseline.py -t "$TARGET" -c bazarnube-rules.conf -r zap-out/report.html || EXIT=$?
  - '[ "${EXIT:-0}" -lt 2 ] || exit 1'
artifacts:
  when: always
  paths: [ zap-out/ ]

With [ "${EXIT:-0}" -lt 2 ] || exit 1, any code >= 2 (WARN/FAIL) breaks the build, and when: always guarantees the report is uploaded even on failure.

Conclusion

With this lesson you close out Module 6. You've gone from running ZAP by hand to integrating it into a DevSecOps pipeline: baseline and full scan with Docker, the Automation Framework for tailored authenticated scans, the API in daemon mode, real pipelines in GitHub Actions and GitLab CI, management of thresholds and build failure, reports as artifacts, and a versioned rules file for false positives. The result is a DAST that runs on its own, on every change, and protects BazarNube without depending on someone remembering.

This is also the point where the whole course converges. ZAP doesn't live in isolation: it's one piece within a secure SDLC. In Module 7 we make that leap: the secure development lifecycle, threat modeling to decide what to protect before writing code, and DevSecOps as the philosophy that ties together everything you've learned. There you'll fit the pieces: the Top Ten 2021 (M3) as the catalog of risks, the ASVS (M4) as verifiable requirements, SAMM (M5) as the program's maturity model, and ZAP (M6) as the automated dynamic verification inside the pipeline. Security stops being a final phase and becomes a continuous property of the process.

OWASP Course: Guidelines and Standards for Web Application Security

Module 1: Introduction to OWASP

Module 2: Main OWASP Projects

Module 3: OWASP Top Ten 2021 in Depth

Module 4: OWASP ASVS (Application Security Verification Standard)

Module 5: OWASP SAMM (Software Assurance Maturity Model)

Module 6: OWASP ZAP (Zed Attack Proxy)

Module 7: Best Practices and Recommendations

Module 8: Practical Exercises and Case Studies

Module 9: Assessment and Certification

© Copyright 2026. All rights reserved