Template

Baseline Sweep Starter Kit (Docker + cron + Slack + Tickets)

Copy‑paste templates to schedule a weekly OWASP ZAP Baseline sweep with Docker + cron, send color‑coded Slack alerts, and optionally open Jira/Linear tickets. Built for solo operators—safe, passive, and fast to ship.

Copy these templates, fill in the [BRACKETS], and you’ll have a weekly, production‑safe OWASP ZAP Baseline sweep running via Docker + cron with Slack notifications and optional ticket creation (Jira or Linear). Everything here assumes passive Baseline scans only, mounting /zap/wrk so artifacts persist. Start on staging, then move to production once your OUTOFSCOPE and rate‑limits are set.

How to use this kit

  • Duplicate this kit into a repo or ops folder.
  • Create a local ./zap directory for artifacts and configs.
  • Fill out .env, endpoint_inventory.csv, and (optionally) zap.context.
  • Test one endpoint with docker compose run --rm zap-baseline.
  • Add a cron entry to run weekly; pipe JSON to Slack and, if desired, create tickets for [SEVERITY_THRESHOLD].

Docker Compose (copy-paste)

Place this at your project root. It mounts ./zap to /zap/wrk so reports/configs persist. Default image tag is stable; set [ZAP_TAG] to weekly if you prefer fresher add-ons. Adjust minutes -m to control the spider duration.

# docker-compose.yml
services:
  zap-baseline:
    image: ghcr.io/zaproxy/zaproxy:${ZAP_TAG:-stable}
    working_dir: /zap/wrk
    volumes:
      - ./zap:/zap/wrk:rw
    # Uncomment if you have a context for authenticated areas
    # - ./zap/zap.context:/zap/wrk/zap.context:ro
    environment:
      - TZ=${TZ:-UTC}
    command: >-
      zap-baseline.py
      -t ${TARGET_URL}
      -m ${SPIDER_MINS:-2}
      -r reports/${REPORT_BASENAME:-baseline}.html
      -x reports/${REPORT_BASENAME:-baseline}.xml
      -J reports/${REPORT_BASENAME:-baseline}.json
      -w reports/${REPORT_BASENAME:-baseline}.md
      -c ${CONFIG_FILE:-zap-baseline.conf}
      # Optional, for SPAs only and when safe
      # -j
      # Optional, scan as context user (requires zap.context)
      # -n zap.context -U ${CONTEXT_USER:-}
    # Prevents container from hanging in the background
    restart: "no"

.env (fill this)

Create a .env next to your docker-compose.yml and fill these:

# .env (example)
TARGET_URL=https://[YOUR_DOMAIN_OR_ENDPOINT]
SPIDER_MINS=2
ZAP_TAG=stable
REPORT_BASENAME=baseline-[SHORT_SERVICE_NAME]
CONFIG_FILE=zap-baseline.conf
TZ=UTC

# Optional for authenticated baseline with a context
CONTEXT_USER=[LOW_PRIV_TEST_USERNAME]

# Slack + ticketing endpoints (optional)
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/[XXX]/[YYY]/[ZZZ]
JIRA_BASE_URL=https://[YOUR_SUBDOMAIN].atlassian.net
JIRA_EMAIL=[YOU@YOURDOMAIN.COM]
JIRA_API_TOKEN=[JIRA_API_TOKEN]
JIRA_PROJECT_KEY=[KEY]
LINEAR_API_KEY=[LINEAR_API_KEY]
LINEAR_TEAM_ID=[LINEAR_TEAM_ID]
LINEAR_LABEL_IDS=[LABEL_ID1],[LABEL_ID2]
SEVERITY_THRESHOLD=[FAIL|WARN]

cron examples (weekly sweep)

Run weekly on Monday at 07:00. Use flock to avoid overlaps. Update the absolute path.

# m h dom mon dow  command
0 7 * * 1  cd /[PATH]/to/your/kit && \
  /usr/bin/flock -n /tmp/zap-baseline.lock \
  /usr/bin/docker compose run --rm zap-baseline && \
  /usr/bin/docker compose rm -f

Tip: log outputs for quick forensics.

0 7 * * 1  cd /[PATH]/your/kit && \
  /usr/bin/flock -n /tmp/zap-baseline.lock \
  /usr/bin/docker compose run --rm zap-baseline >> zap/cron.log 2>&1 && \
  /usr/bin/docker compose rm -f >> zap/cron.log 2>&1

Endpoint inventory CSV (copy and fill)

Track the surfaces you care about and throttle responsibly. Import this CSV into Notion/Sheets if you prefer.

# zap/endpoint_inventory.csv
url,method,auth_scope,rate_limit_rpm,owner,notes,last_checked_at
https://[YOUR_DOMAIN]/webhook/order,POST,public,60,[OWNER_NAME],"Accepts HMAC header X-Example-Signature",[YYYY-MM-DD]
https://[YOUR_DOMAIN]/portal/login,GET,anonymous,120,[OWNER_NAME],"SPA entry; consider -j when safe",[YYYY-MM-DD]
https://api.[YOUR_DOMAIN]/v1/forms/contact,POST,low-priv-user,30,[OWNER_NAME],"Context user required for auth flow",[YYYY-MM-DD]

Use this file only as an inventory; Baseline scans use the -t [TARGET_URL] you pass. Rotate through key URLs on a schedule or focus on your main surface.

zap-baseline.conf (starter overrides + OUTOFSCOPE)

Place this in ./zap/zap-baseline.conf. Start strict and tune down noise over time. Use OUTOFSCOPE to exclude third‑party assets or noisy paths. Rule overrides are per rule ID — leave placeholders until you confirm the exact IDs in your reports.

# zap/zap-baseline.conf
# Action can be: IGNORE, WARN, FAIL (case-insensitive)
# Format: [ACTION] [RULE_ID] [URL_REGEX_OPTIONAL]
# Examples (fill real RULE_IDs from your report):
FAIL [RULE_ID_FOR_HSTS]
WARN [RULE_ID_FOR_CSP]
WARN [RULE_ID_FOR_X_CONTENT_TYPE_OPTIONS]
IGNORE [RULE_ID_FOR_COOKIE_SECURE_FLAG] https://[YOUR_DOMAIN]/legacy/.*

# OUTOFSCOPE patterns (regex). One per line.
OUTOFSCOPE https?://(www\.)?google-analytics\.com/.*
OUTOFSCOPE https?://cdn\.[YOUR_DOMAIN]/.*\.(png|jpg|gif|css|js)$
OUTOFSCOPE https?://static\.example-cdn\.com/.*

Quick rule: only IGNORE when you have a written rationale; prefer WARN with a ticket to follow up.

Slack webhook payload (severity color-coding)

Post a succinct summary to Slack. Choose colors and mapping that fit your workflow. Replace [BRACKETS] and send with curl.

{
  "username": "ZAP Baseline",
  "icon_emoji": ":mag_right:",
  "blocks": [
    {"type":"header","text":{"type":"plain_text","text":"Baseline Sweep: [SHORT_SERVICE_NAME] → [ALERT_SEVERITY]"}},
    {"type":"section","fields":[
      {"type":"mrkdwn","text":"*Target:* [TARGET_URL]"},
      {"type":"mrkdwn","text":"*Run:* [RUN_ID]"},
      {"type":"mrkdwn","text":"*Highest:* [ALERT_SEVERITY]"},
      {"type":"mrkdwn","text":"*Findings:* FAIL=[FAIL_CT] • WARN=[WARN_CT] • INFO=[INFO_CT]"}
    ]},
    {"type":"section","text":{"type":"mrkdwn","text":"Top rule: *[ALERT_TITLE]* ([ALERT_RULE_ID])\n[ALERT_SUMMARY]\n<${REPORT_URL}|View full HTML report>"}},
    {"type":"context","elements":[{"type":"mrkdwn","text":"ZAP exit code: [EXIT_CODE]"}]}
  ],
  "attachments": [
    {"color": "[SEVERITY_COLOR]"}
  ]
}

Send it:

curl -X POST -H 'Content-type: application/json' \
  --data @zap/slack-payload.json \
  "${SLACK_WEBHOOK_URL}"

Suggested mapping (edit to taste): FAIL → danger, WARN → warning, INFO/PASS → good.

Jira Cloud: minimal issue create (REST v3)

Create a minimal issue for high‑priority findings. Replace [BRACKETS]. Description links to your saved HTML report.

{
  "fields": {
    "project": { "key": "[JIRA_PROJECT_KEY]" },
    "summary": "ZAP Baseline: [ALERT_SEVERITY] – [ALERT_TITLE] @ [SHORT_SERVICE_NAME]",
    "description": "Rule: [ALERT_RULE_ID] – [ALERT_TITLE]\nSeverity: [ALERT_SEVERITY]\nTarget: [TARGET_URL]\nTop URL: [ALERT_URL]\nRun: [RUN_ID]\nReport: [REPORT_URL]\nNotes: [OPTIONAL_NOTES]",
    "issuetype": { "name": "Bug" },
    "labels": ["zap","baseline","[SHORT_SERVICE_NAME]","[ALERT_SEVERITY]"],
    "priority": { "name": "[JIRA_PRIORITY_NAME]" }
  }
}

Send it:

curl -s -X POST \
  -H "Content-Type: application/json" \
  -u "${JIRA_EMAIL}:${JIRA_API_TOKEN}" \
  --data @zap/jira-issue.json \
  "${JIRA_BASE_URL}/rest/api/3/issue"

Tip: only open tickets at or above [SEVERITY_THRESHOLD] to avoid noise.

Linear: minimal issue create (GraphQL)

Linear’s API is GraphQL. Create a simple Issue with variables. Replace [BRACKETS].

# zap/linear-create-issue.graphql
mutation CreateIssue($input: IssueCreateInput!) {
  issueCreate(input: $input) { success issue { id identifier url } }
}
{
  "query": "mutation CreateIssue($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { id identifier url } } }",
  "variables": {
    "input": {
      "teamId": "[LINEAR_TEAM_ID]",
      "title": "ZAP Baseline: [ALERT_SEVERITY] – [ALERT_TITLE] @ [SHORT_SERVICE_NAME]",
      "description": "Rule: [ALERT_RULE_ID] – [ALERT_TITLE]\nSeverity: [ALERT_SEVERITY]\nTarget: [TARGET_URL]\nTop URL: [ALERT_URL]\nRun: [RUN_ID]\nReport: [REPORT_URL]",
      "labelIds": [ [LABEL_ID1], [LABEL_ID2] ]
    }
  }
}

Send it:

curl -s -X POST https://api.linear.app/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: ${LINEAR_API_KEY}" \
  --data @zap/linear-issue.json

Safe defaults + ethics checklist

What Baseline does here: 1–2 minute spider, then passive rules only. It won’t send active attacks. Use this checklist before you point at production.

  • You control the target: set [TARGET_URL] and confirm it’s your asset or you have written permission.
  • Keep it passive: do not switch to Full Scan on production.
  • Rate‑limit awareness: set rate_limit_rpm in endpoint_inventory.csv and schedule off‑peak.
  • Scope tightly: add OUTOFSCOPE for third‑party hosts and static assets.
  • Start on staging: verify configs and Slack/ticket routing.
  • Ticket hygiene: create issues only for [SEVERITY_THRESHOLD] and above.
  • Paper trail: keep a version log (see last section) and save reports for 90 days.
  • Privacy: scrub tokens/PII from logs and payloads.

Artifacts, exit codes, and paths

Artifacts persist under ./zap/reports. Exit codes help your automation decide what to do next.

  • Reports written per run:
    • HTML: zap/reports/[REPORT_BASENAME].html
    • XML: zap/reports/[REPORT_BASENAME].xml
    • JSON: zap/reports/[REPORT_BASENAME].json
    • MD: zap/reports/[REPORT_BASENAME].md
  • Common exit codes (Baseline):
    • 0 = No FAILs.
    • 1 = ≥1 FAIL.
    • 2 = WARNs only (no FAILs).
    • 3 = Other failure (container, network, etc.).

Recommendation: route Slack on any non‑0; open tickets on 1 or per‑rule exceptions you care about.

Optional: authenticated Baseline via context

Authenticated Baseline still stays passive but reaches more surface area. Steps:

  1. Export or craft a ZAP context file named zap.context with:

    • Include regex for in‑scope URLs: https://[YOUR_DOMAIN]/(app|api)/.*
    • Exclude regex for out‑of‑scope paths and third‑parties.
    • Authentication mechanism for your app.
    • A low‑priv test user [CONTEXT_USER] defined in the context.
  2. Mount the context and scan as that user (uncomment in Compose):

# command additions
-n zap.context -U ${CONTEXT_USER}
  1. If your app is a heavy SPA and safe to crawl, consider -j to enable the Ajax spider. Keep it off by default on production.

Keep a separate staging context for deeper coverage.

Version log (keep this updated)

Use this to record changes so your future self (or client) knows what changed and why.

# Baseline Sweep – Version Log

## [YYYY-MM-DD]
- Change: [WHAT_CHANGED]
- Why: [REASON]
- Risk policy: [E.G., FAIL certain rules, WARN others]
- Evidence: [LINK_TO_REPORT]

## [YYYY-MM-DD]
- Change: Added OUTOFSCOPE for https://static.[YOUR_DOMAIN]
- Why: Noise from CDN assets with no security impact
- Risk policy: unchanged
- Evidence: reports/baseline-2026-05-29.html

Optional: quick JSON helpers (jq)

Optional helper examples to wire results to Slack/tickets using the JSON report. Tailor to your report structure and naming.

Highest severity and counts (generic jq):

# Derive top severity, counts, and first alert title from JSON
JSON=zap/reports/${REPORT_BASENAME:-baseline}.json
FAIL_CT=$(jq '[..|objects|select(.risk?=="High" or .risk?=="Medium" or .riskdesc?=="High" or .riskdesc?=="Medium")] | length' "$JSON")
WARN_CT=$(jq '[..|objects|select(.risk?=="Low" or .riskdesc?=="Low")] | length' "$JSON")
INFO_CT=$(jq '[..|objects|select(.risk?=="Informational" or .riskdesc?=="Informational")] | length' "$JSON")
TOP_TITLE=$(jq -r '..|objects|select(has("name"))|.name|select(.!=null)|. ' "$JSON" | head -n1)

# Decide severity threshold (simple example)
if [ "$FAIL_CT" -gt 0 ]; then
  ALERT_SEVERITY=FAIL; SEVERITY_COLOR=danger
elif [ "$WARN_CT" -gt 0 ]; then
  ALERT_SEVERITY=WARN; SEVERITY_COLOR=warning
else
  ALERT_SEVERITY=INFO; SEVERITY_COLOR=good
fi

Note: ZAP JSON structures can vary by version; validate keys (risk vs riskdesc) in your reports.