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
- Why automate DAST
- The packaged scripts: baseline scan and full scan with Docker
- The rules file for managing false positives
- The Automation Framework (YAML with jobs)
- ZAP's API and daemon mode
- Integration with GitHub Actions
- Integration with GitLab CI
- Thresholds, build failure, and reports as artifacts
- A complete DAST pipeline for BazarNube
- Common mistakes and tips
- Exercises
- 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.confKey flags:
-ttarget URL (always your own staging).-r/-J/-wreports in HTML, JSON, and Markdown.-crules file (false positives, see below).-Idon't fail the build on Info-level alerts (only return an error on WARN/FAIL).-lminimum 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.confThe 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 buildPossible 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.jsonRun it with:
docker run --rm -v "$(pwd):/zap/wrk/:rw" \
-t zaproxy/zap-stable zap.sh -cmd -autorun /zap/wrk/zap-bazarnube.yamlNote 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-daemonstarts 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.mdfail_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 daysHere 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 onFAIL(confirmed SQLi/XSS). - Nightly:
zap-full-scan.pyor the authenticated Automation Framework (slow, aggressive) against dedicated staging. It doesn't block anyone; its findings enter asBZN-xxxtickets 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: alwaysyou 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:
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
- OWASP Top Ten
- OWASP ASVS (Application Security Verification Standard)
- OWASP SAMM (Software Assurance Maturity Model)
- OWASP ZAP (Zed Attack Proxy)
- Other Key Projects: WSTG, Cheat Sheets and Dependency-Check
Module 3: OWASP Top Ten 2021 in Depth
- A01:2021 – Broken Access Control
- A02:2021 – Cryptographic Failures and Sensitive Data Exposure
- A03:2021 – Injection
- Cross-Site Scripting (XSS) in Depth
- A04:2021 – Insecure Design
- A05:2021 – Security Misconfiguration
- XML External Entities (XXE)
- A06:2021 – Vulnerable and Outdated Components
- A07:2021 – Identification and Authentication Failures
- A08:2021 – Software and Data Integrity Failures (Insecure Deserialization)
- A09:2021 – Security Logging and Monitoring Failures
- A10:2021 – Server-Side Request Forgery (SSRF)
Module 4: OWASP ASVS (Application Security Verification Standard)
Module 5: OWASP SAMM (Software Assurance Maturity Model)
Module 6: OWASP ZAP (Zed Attack Proxy)
- Introduction to ZAP
- Installation and Configuration
- Vulnerability Scanning
- Automating Security Testing
Module 7: Best Practices and Recommendations
- Secure Software Development Life Cycle (SDLC)
- Threat Modeling
- Integrating Security into DevOps (DevSecOps)
- Security Training and Awareness
- Additional Tools and Resources
Module 8: Practical Exercises and Case Studies
- Exercise 1: Identifying Vulnerabilities
- Exercise 2: Implementing Security Controls
- Case Study 1: Analyzing a Security Incident
- Case Study 2: Improving the Security of a Web Application
