Featured image of post Enforcing Dynamic Security Testing in CI with OWASP ZAP

Enforcing Dynamic Security Testing in CI with OWASP ZAP

Integrate OWASP ZAP into your CI pipeline to enforce dynamic security testing before merge. This article demonstrates how to scan a live preview of your Hugo site and block pull requests with high-risk vulnerabilities or missing security headers.

This post is part of the DevSecOps Principles series.

Introduction

In the first article, we built a modern CI pipeline to lint, build, and preview a Hugo site using GitHub Actions. Now, we’ll take a step forwardin the DevSecOps direction.

Indeed, traditional pipelines often focus on code quality and deployment readiness. However, that’s no longer enough. A single missing HTTP header or exposed debug page could compromise your entire application. And in enterprise-grade CI/CD pipelines, security testing isn’t an afterthought, it’s a gate.

That’s why in this second article, we’ll introduce Automated Dynamic Security Testing (DAST) using OWASP ZAP and demonstrate how to integrate it into our GitHub Actions pipeline before merging changes into main. By automating the scan of a live preview of our Hugo site each time a pull request is made, we:

  • Detect runtime security issues (unlike SAST which only scans code)

  • Prevent risky code from reaching production

  • Establish a habit of security-first development

  • simulate a real-world DevSecOps enforcement layer used in production environments


If high-severity vulnerabilities or missing security headers are detected, the merge is blocked automatically, just like it would be in an enterprise CI/CD setup.

This enhancement helps enforce security compliance early and automatically, aligning with the core DevSecOps principle of shifting security left.

As said before, this article continues from our previous guide on setting up a CI pipeline with GitHub Actions for a Hugo site. Make sure your initial pipeline is in place before proceeding. No additional installation of ZAP is required for this setup.

đź”’ Security Checks Enforced

Here’s what the new security check does:

Check Type Description
High Severity Threshold Blocks the merge if the ZAP scan finds any vulnerability with a severity score of 7.0 or higher
Security Headers Check Verifies the presence of essential HTTP headers like Strict-Transport-Security andX-Content-Type-Options
No debug or Sensitive Information Ensure the page does not expose stack traces, debug messages, or server metadata that could aid attackers

All checks run automatically after a live preview of the Hugo site is built inside a container. If anything fails, the pipeline halts, protecting main from risky changes.

Alerting and Reporting Features

To simulate a more complete DevSecOps setup:

Feature Description
Upload ZAP Scan Report Automatically saves the full ZAP scan report (.html or .json) as a GitHub Actions artifact. Your team can download and review it directly from the UI.
Auto-create GitHub Issues When allow_issue_writing: true is enabled in the ZAP GitHub Action, issues are automatically opened or updated with the vulnerability findings. This lets you track and manage security issues just like technical debt, in your existing GitHub backlog.

These features allow your team to stay informed, track vulnerabilities over time, and ensure no security warning goes unnoticed — even after a successful build.

âť— Note that GitHub doesn’t allow Actions to create issues unless:

  • The GitHub token has proper permissions, AND
  • It’s not running from a fork or GitHub Actions from a PR created by a fork (for security reasons).

For that reason, we’re going to leave this to false value here to avoid risk and complexity.

Integrating OWASP ZAP into GitHub Actions

We’ll add a new job in our ci.yml workflow after the smoke test, using zaproxy/action-full-scan to scan the live Hugo site.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
  owasp-zap:
    name: Run OWASP ZAP Scan
    runs-on: ubuntu-latest
    needs: http-smoke-test  # Waits for successful site build and HTTPie test

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Serve Hugo site with Nginx
        run: |
          docker run -d --rm -p 8080:80 -v $(pwd)/public:/usr/share/nginx/html nginx
          sleep 5  # Give Nginx time to serve the site

      - name: Run OWASP ZAP scan
        uses: zaproxy/action-full-scan@v0.10.0
        with:
          target: 'http://localhost:8080'
          fail_action: true                                                 #  Fails job if vulnerabilities are found
          allow_issue_writing: false                                        #  Open GitHub issues automatically (set to true to enable)
          cmd_options: '-a -j zap-report.json -r zap-report.html -l WARN'   #  Fail on medium+ severity, save report as JSON and HTML

      - name: Upload ZAP JSON Report
        if: always() # Always upload, even if scan fails
        uses: actions/upload-artifact@v4
        with:
          name: zap-report-json
          path: report_json.json

      - name: Upload ZAP HTML Report
        if: always()  # Always upload, even if scan fails
        uses: actions/upload-artifact@v4
        with:
          name: zap-report-html
          path: report_html.html
  • needs: http-smoke-test

    • Ensures this job runs only after Hugo is built and successfully served/tested by the previous job.
    • Reuses the output (/public) from the Hugo build
  • docker run -v $(pwd)/public:/usr/share/nginx/html nginx

    • Serves the static Hugo site at http://localhost:8080 using Nginx.
    • This mimics a real server environment for ZAP to scan.
  • zaproxy/action-full-scan@v0.10.0

    • Launches a full DAST scan with the ZAP CLI against the preview site.
    • fail_action: true ensures the job fails if critical issues (e.g., severity ≥ WARN) are found.
    • cmd_options:
      • -a: aggressive scan mode
      • -j zap-report.json: generate a JSON report
      • -r zap-report.html: generate a HTML report
      • -l WARN: sets minimum alert level to WARN
  • upload-artifact

    • Saves the ZAP scan report as a downloadable artifact in GitHub Actions
      • JSON is for automation (issue creation, pipelines)
      • HTML is for humans to browse the full report
    • Useful for auditing, debugging, or sharing with the team.
  • allow_issue_writing: true

    • Auto-create GitHub Issues on Vulnerabilities

⚠️ Also don’t forget to add owasp-zap job in the following line of your auto-merge job:

1
needs: [lint-markdown, hugo-preview, http-smoke-test, owasp-zap]

Final Pipeline Overview

Here’s what your complete DevSecOps-enhanced CI pipeline now looks like:

  1. Markdown Linting

  2. Hugo Preview Build in Container

  3. HTTPie Smoke Test (via local server)

  4. OWASP ZAP Security Scan

  5. Auto-Merge if All Stages Pass

Begins to look like something isn't it? :)

CI pipeline New Final Look


How to test

To trigger the full pipeline, simply add a modification in a feature branch then commit and push:

1
2
3
4
5
git checkout -b test-feature
echo "# Trigger Test" >> content/post/<your-post>index.md
git add .
git commit -m "Trigger CI pipeline"
git push origin test-feature

Then just open a Pull Request into main. GitHub Actions will run all stages, including OWASP ZAP, and block the merge if critical issues are found.

Conclusion

I really encourage you to try integrating OWASP ZAP into your CI pipeline. It’s surprisingly simple to automate and adds a strong layer of defense to your workflow.

By catching high-severity vulnerabilities before code reaches production, you bring your pipeline closer to what real-world security gates look like in enterprise environments.

Thanks for following along! I hope this helps make your DevOps pipeline not just smarter, but safer.