Ravindra BagaleCourses & study guides मराठी

Chapter 29: Scan Pipeline in Jenkins

29.5 ZAP baseline

Why: the problem

Suppose we are testing a Swiggy-style order page. The code is scanned, the libraries are scanned, the image is scanned. But some problems only show up when the app is running: a missing security header, a cookie without the Secure flag, a server that tells everyone its version number. To find these, someone must talk to the running app like a browser does. OWASP ZAP does this. Its baseline scan is a gentle, passive scan: it crawls the pages and reads the replies, but it does not attack. That makes it safe for a lab or test URL in CI. One firm rule: you only scan apps you own, at your own lab URL. Never point ZAP at someone else's site.

What we will build

A pipeline that starts the badge API in a Docker network, runs the ZAP baseline scan against it, reads the exit code, and fails on FAIL-level rules. You will scan a Node app, fix missing headers with helmet, scan a Python app with a rules file, and scan a Java app behind nginx.

What: every term in plain English

  • DAST — Dynamic Application Security Testing: testing the running app from outside.
  • ZAP — Zed Attack Proxy, a free OWASP tool for web app security testing.
  • Baseline scan — A short spider plus passive checks only. It does not send attacks.
  • Passive rule — A rule that only reads responses, e.g. "header X-Content-Type-Options missing".
  • Rules file (-c) — A tab-separated file that sets each rule ID to IGNORE, WARN or FAIL.
  • Exit codes — 0 = pass, 1 = at least one FAIL, 2 = at least one WARN, 3 = other error.
  • Target — The URL you scan, e.g. http://badge:3000. Only your own lab.
Create a lab network Start the badge app Check it answers Pull ZAP Run the baseline scan Make a rules file Gate on FAIL only
Start app, ZAP baseline, read exit code, clean up

How: step by step

  1. Create a lab network

    docker network create lab
    

    What it does: A private Docker network so ZAP can reach the app by name.

    Output

    8d2f1c0e3b7a...
    
  2. Start the badge app

    docker run -d --name badge --network lab badge-api:1.1
    

    What it does: Runs the app with DNS name badge on network lab.

    Output

    4a91c2...
    
  3. Check it answers

    docker run --rm --network lab curlimages/curl -s http://badge:3000/health
    

    What it does: Calls the app from inside the same network.

    Output

    {"status":"ok"}
    
  4. Pull ZAP

    docker pull ghcr.io/zaproxy/zaproxy:stable
    

    What it does: Gets the official ZAP image.

    Output

    Status: Downloaded newer image for ghcr.io/zaproxy/zaproxy:stable
    
  5. Run the baseline scan

    mkdir -p reports && setfacl -m u:1000:rwx reports
    docker run --rm --network lab -v "$PWD/reports:/zap/wrk:rw" ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://badge:3000 -r zap.html -J zap.json; echo "exit=$?"
    

    What it does: Spiders the app for 1 minute, runs passive rules, writes HTML and JSON reports. reports must be writable by the zap user (uid 1000) inside the container; setfacl gives only that user write access.

    Output

    Total of 4 URLs
    WARN-NEW: X-Content-Type-Options Header Missing [10021] x 3
    WARN-NEW: Missing Anti-clickjacking Header [10020] x 1
    FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 3 WARN-INPROG: 0 INFO: 4 IGNORE: 0 PASS: 52
    exit=2
    
  6. Make a rules file

    docker run --rm -v "$PWD:/zap/wrk:rw" ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://badge:3000 -g zap.conf --network lab || true
    

    What it does: -g writes a file with every rule set to WARN. You then change the ones you care about to FAIL.

    Output

    (writes zap.conf)
    
  7. Gate on FAIL only

    Use -c zap.conf and treat exit 1 as red, exit 2 as warning
    

    What it does: In the pipeline, rules you set to FAIL stop the build.

    Output

    FAIL-NEW: 1 ... exit=1
    

Example 1: Node

Stack. Node.js, helmet, ZAP in Docker

Goal. Node.js: ZAP finds missing headers; helmet fixes them.

app/server.js (top, after the fix)

const express = require("express");
const helmet = require("helmet");
const app = express();
app.use(helmet());          // adds X-Content-Type-Options, X-Frame-Options, CSP and more
app.disable("x-powered-by"); // do not tell the world we use Express

Jenkinsfile

pipeline {
  agent { label 'scan' }
  options { timestamps(); timeout(time: 30, unit: 'MINUTES') }
  stages {
    stage('Checkout') { steps { checkout scm } }
    stage('Start app for DAST') {
      steps {
        sh 'docker network create lab-${BUILD_NUMBER}'
        sh 'docker run -d --name badge-${BUILD_NUMBER} --network lab-${BUILD_NUMBER} --network-alias badge badge-api:${GIT_COMMIT}'
        sh 'sleep 5'
      }
    }
    stage('ZAP baseline') {
      steps {
        sh '''
          mkdir -p reports && setfacl -m u:1000:rwx reports
          set +e
          docker run --rm --network lab-${BUILD_NUMBER} -v "$PWD/reports:/zap/wrk:rw" \
            ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://badge:3000 -c zap.conf -r zap.html
          rc=$?
          set -e
          if [ $rc -eq 1 ] || [ $rc -eq 3 ]; then echo "ZAP gate failed (rc=$rc)"; exit 1; fi
          echo "ZAP rc=$rc (0 pass, 2 warnings only)"
        '''
      }
      post {
        always {
          archiveArtifacts artifacts: 'reports/zap.html', allowEmptyArchive: true
          sh 'docker rm -f badge-${BUILD_NUMBER} || true; docker network rm lab-${BUILD_NUMBER} || true'
        }
      }
    }
    stage('Live (lab)') {
      steps { echo 'All gates green: deploying badge-api to the lab' }
    }
  }
}

What each line does

  • app.use(helmet()) — Adds a set of safe HTTP security headers to every reply.
  • app.disable("x-powered-by") — Removes the header that reveals Express.
  • lab-${BUILD_NUMBER} — A new network per build, so parallel builds do not clash.
  • --network-alias badge — The app is reachable as http://badge:3000 inside that network.
  • set +e ... rc=$? — Lets us read ZAP's exit code instead of failing at once, so we can treat 2 (warnings) differently from 1 (fail).
  • if [ $rc -eq 1 ] || [ $rc -eq 3 ] — FAIL rules or a ZAP error stop the build.
  • post { always { ... docker rm -f ... } } — Always removes the test container and network, even when red.

Green output

PASS: X-Content-Type-Options Header Missing [10021]
PASS: Missing Anti-clickjacking Header [10020]
PASS: Server Leaks Information via "X-Powered-By" [10037]
FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 0 WARN-INPROG: 0 INFO: 3 IGNORE: 0 PASS: 55
ZAP rc=0 (0 pass, 2 warnings only)
Finished: SUCCESS

Expected result. Before helmet: 3 warnings. After: all pass. The report is archived.

Example 2: Python: a rules file decides what fails

Stack. Python, Flask, ZAP rules file

Goal. Python: a rules file decides what fails.

zap.conf (part)

# rule-id   action  (name)
10020   FAIL    (Anti-clickjacking Header)
10021   FAIL    (X-Content-Type-Options Header Missing)
10038   WARN    (Content Security Policy Header Not Set)
10036   IGNORE  (Server Leaks Version Information via "Server")

app.py (add headers)

@app.after_request
def security_headers(resp):
    resp.headers["X-Content-Type-Options"] = "nosniff"
    resp.headers["X-Frame-Options"] = "DENY"
    return resp

Command

docker run --rm --network lab -v "$PWD:/zap/wrk:rw" ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://badge-py:8000 -c zap.conf -r zap.html; echo "exit=$?"

What each line does

  • 10020 FAIL — Missing clickjacking header becomes a build failure.
  • 10038 WARN — CSP is wanted but only a warning for now.
  • 10036 IGNORE — Accepted: the lab nginx Server header, with a comment in the team wiki.
  • @app.after_request — Flask runs this on every reply and adds the headers.

Green output

(before the fix)
FAIL-NEW: X-Content-Type-Options Header Missing [10021] x 2
FAIL-NEW: 2 ... exit=1
(after the fix)
WARN-NEW: Content Security Policy (CSP) Header Not Set [10038] x 2
FAIL-NEW: 0 WARN-NEW: 1 ... exit=2

Expected result. Red before, then exit 2 (only a warning) after. The pipeline treats 2 as pass with warnings.

Example 3: Java behind nginx: scan the URL users really hit

Stack. Java Spring Boot, nginx, ZAP

Goal. Java behind nginx: scan the URL users really hit.

nginx.conf (server part)

server {
  listen 80;
  server_tokens off;                                  # hide nginx version
  add_header X-Content-Type-Options nosniff always;
  add_header X-Frame-Options DENY always;
  location / { proxy_pass http://badge-java:8080; }
}

Command

docker run --rm --network lab -v "$PWD:/zap/wrk:rw" ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t http://edge:80 -m 2 -c zap.conf -r zap.html

What each line does

  • server_tokens off — nginx stops printing its version.
  • add_header ... always — Adds the header to error replies too.
  • -t http://edge:80 — Scans the proxy, because that is what users reach.
  • -m 2 — Spider for 2 minutes instead of the default 1.

Green output

PASS: Server Leaks Version Information via "Server" HTTP Response Header Field [10036]
FAIL-NEW: 0 WARN-NEW: 0 PASS: 56
exit=0

Expected result. Clean scan through the real front door.

Verify: did it work?

  1. Open the HTML report

    xdg-open reports/zap.html
    

    What it does: Shows each alert, the URL and how to fix it.

    Output

    (browser) ZAP Scanning Report - Summary of Alerts
    
  2. Count alerts from JSON

    jq "[.site[].alerts[]] | length" reports/zap.json
    

    What it does: Number of alert types found.

    Output

    0
    
  3. Confirm cleanup

    docker ps -a --filter name=badge- --format "{{.Names}}"
    

    What it does: No leftover test containers.

    Output

    (empty)
    

When it is red 1

docker: ... open /zap/wrk/zap.html: permission denied

Cause. The zap user inside the container cannot write to the mounted folder.

Fix, step by step

  1. mkdir -p reports && setfacl -m u:1000:rwx reports before the run.
  2. Or create the folder owned by uid 1000 on the agent once.
  3. Rerun.

When it is red 2

[Errno -2] Name or service not known: badge
ERROR Failed to access the target URL

Cause. ZAP is not on the same Docker network as the app, or the alias is wrong.

Fix, step by step

  1. Add --network lab-${BUILD_NUMBER} to the ZAP run.
  2. Check docker network inspect lab-... lists the app.
  3. Wait until /health answers before scanning.

When it is red 3

ZAP rc=2
ERROR: script returned exit code 2

Cause. The shell treated warnings (2) as failure because the docker line was not wrapped.

Fix, step by step

  1. Use set +e and read rc=$? as in Example 1.
  2. Decide in writing: 1 and 3 fail, 2 warns.
  3. Or pass -I to zap-baseline.py to ignore warnings.

Cleanup

  1. Remove lab container and network

    docker rm -f badge && docker network rm lab
    

    What it does: Stops the test app and removes the network.

    Output

    badge
    lab
    
  2. Remove the ZAP image (optional)

    docker rmi ghcr.io/zaproxy/zaproxy:stable
    

    What it does: Frees about 1.5 GB if you do not need it.

    Output

    Untagged: ghcr.io/zaproxy/zaproxy:stable
    

Ravindra Bagale's Tip

ZAP fakt aaplya lab URL var. Dusryachya site var kadhihi nahi.

Practice tasks

Easy Run ZAP on your laptop

Why: See what alerts look like.

What to do: Run the baseline on the badge app in a lab network.

How:
  1. docker network create lab
  2. Start badge.
  3. Run step 5.

Check: You see WARN lines and an HTML report.

Medium Fix headers in Node

Why: Most baseline alerts are headers.

What to do: Add helmet and rescan.

How:
  1. npm install helmet
  2. app.use(helmet())
  3. Rescan.

Check: Warnings 10020 and 10021 become PASS.

Medium Write a rules file

Why: Teams choose what fails.

What to do: Generate and edit zap.conf.

How:
  1. Run with -g zap.conf.
  2. Set two rules to FAIL.
  3. Run with -c zap.conf.

Check: A missing header gives exit 1.

Hard Exit code handling

Why: 2 should not break the build, 1 should.

What to do: Copy the set +e logic into your Jenkinsfile and test all cases.

How:
  1. Remove helmet (expect 1 with FAIL rules).
  2. Keep only CSP warning (expect 2).
  3. Full fix (expect 0).

Check: Build red only for 1 and 3.

Easy Run ZAP baseline by hand once

Why: Running a tool by hand first shows you what Jenkins will see.

What to do: Run ZAP baseline on your laptop against the Node badge app, before Jenkins.

How:
  1. Open a terminal in the badge-api folder.
  2. Run the ZAP baseline command from step-by-step.
  3. Read the summary line.

Check: You can say how many findings there are and what the exit code was (echo $?).

Medium Make ZAP baseline go red on purpose

Why: A gate you have never seen fail is a gate you do not trust.

What to do: Add one bad thing that ZAP baseline should catch and run the build.

How:
  1. Create a branch called break-scan.
  2. Add the bad change from the "When it is red" section.
  3. Push and watch the build.

Check: The scan stage is red and the Live (lab) stage is skipped.

Medium Archive the ZAP baseline report

Why: Reviewers and auditors need the report file, not only the console.

What to do: Save the report as a build artifact in Jenkins.

How:
  1. Write the report to reports/ in the scan command.
  2. Add post { always { archiveArtifacts artifacts: 'reports/**', allowEmptyArchive: true } }.
  3. Run the build and open the Artifacts link.

Check: The report file is downloadable from the build page.

Hard Explain ZAP baseline in 60 seconds

Why: Interviewers ask "what does this stage do and why".

What to do: Record yourself explaining the stage to a fresher.

How:
  1. Say the problem it solves.
  2. Say the command and the exit code rule.
  3. Say what you do when it is red.

Check: A friend can repeat back when the build should go red.

Homework: try at home

Try at home

Generate zap.conf with -g, set 10020 and 10021 to FAIL, run against the badge app without helmet (red), then with helmet (green).

Recap

  • ZAP baseline is a safe passive DAST scan of a running app.
  • Run app and ZAP in a per-build Docker network; scan only your lab.
  • Exit codes: 0 pass, 1 fail, 2 warn, 3 error.
  • A rules file (-c) sets IGNORE/WARN/FAIL per rule.
  • Always archive the report and clean up containers.

FAQ

How long does it take?

About 1-3 minutes for a small app.

Can it log in?

Baseline does not handle login well; the full scan with contexts can, later.

Is it safe for production?

Baseline is passive, but still scan test or lab environments in CI.

Why does it find nothing on an API?

Spiders follow links. For APIs use zap-api-scan.py with an OpenAPI file.

Where is the report?

In the mounted folder, then archived by Jenkins.

Interview questions

  1. SAST vs DAST?

    SAST reads source code without running it; DAST tests the running app from outside, like ZAP.

  2. How do you run ZAP in Jenkins?

    Start the app in a Docker network, run zap-baseline.py container against it, handle exit codes, archive the report, clean up.

  3. Why is the baseline scan safe in CI?

    It only spiders and applies passive rules; no attack payloads.

  4. How do you tune ZAP rules?

    Generate a config with -g, set rules to FAIL/WARN/IGNORE, pass with -c.

  5. How do you scan a REST API?

    zap-api-scan.py with the OpenAPI definition.

Quick quiz

Q1. Does baseline attack the app?

No, it is passive: spider plus reading responses.

Q2. What does exit 2 mean?

Only warnings were found.

Q3. Which flag writes a rules file?

-g zap.conf.

Q4. Why a network per build?

So parallel builds do not clash.

Q5. Which URLs may you scan?

Only your own lab URLs.

Samajla ka?

Ravi Sir's Cards for this lesson

मराठी cards