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
./zapdirectory 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_rpminendpoint_inventory.csvand schedule off‑peak. - Scope tightly: add
OUTOFSCOPEfor 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
- HTML:
- 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:
Export or craft a ZAP context file named
zap.contextwith:- 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.
- Include regex for in‑scope URLs:
Mount the context and scan as that user (uncomment in Compose):
# command additions
-n zap.context -U ${CONTEXT_USER}
- If your app is a heavy SPA and safe to crawl, consider
-jto 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.