Responding to a security finding
Every push runs five security jobs: pip-audit, npm audit, bandit, gitleaks, and (manual for now) OWASP ZAP. This doc tells you what to do when one of them fires.
Severity → action
| Severity | Action |
|---|---|
| CRITICAL | Block merge. File issue with severity:critical + area:security. Fix before any other work ships. |
| HIGH | Block merge. File issue with severity:high. Fix in this PR or in a directly follow-on PR (link it). |
| MEDIUM | Do not block. File issue with severity:medium-low. Triage within 1 week. |
| LOW | Accumulate in the monthly security digest issue. |
Where findings come from
pip-audit (Python deps)
- Action:
pip install --upgrade <package>to a non-vulnerable version, or pin a patched range. - If no fix available: document in issue + consider swapping the library.
npm audit (frontend deps)
- Action:
npm audit fixusually works for transitive. For direct deps, bump explicitly. - If a dev-only vuln is flagged and impact is genuinely nil: add to ignore list with comment in
.npmauditrc(create if missing).
bandit (Python SAST)
- Common hits: hardcoded passwords (B105),
eval/execusage (B307),pickle.loads(B301), weak crypto (B303). - Action: refactor to avoid the pattern. Bandit suppressions (
# nosec) require a one-line justification comment on the same line.
gitleaks (secret scan)
- Always critical. If a real secret was committed:
1. Rotate the secret immediately (Alpaca keys, Heroku keys, whatever)
2. Scrub the secret from git history (
git filter-repoor BFG) 3. Force-push requires--force-with-lease, approval from repo owner 4. File an incident issue taggedarea:security severity:critical - Add the pattern to
.gitleaksignoreonly for known-safe false positives (e.g. a fixture that happens to look like a token).
OWASP ZAP baseline (automated — PR + weekly schedule)
The ZAP scan runs automatically via .github/workflows/security-zap.yml:
- PR trigger: Targets the Antlers branch preview URL when
pr-preview.ymlhas deployed one. Skips silently if the preview is unreachable (backend-only PRs, docs PRs). Posts a sticky comment on the PR with the outcome. - Weekly trigger: Runs every Monday at 09:07 UTC against
https://raxx-app.pages.dev(Antlers staging) andhttps://raxx-api-staging.herokuapp.com/(API staging). - DO NOT scan production hosts (
app.raxx.app,raxx-console-prod, etc.). Scanning prod can trigger WAF lockouts and creates synthetic load.
Triage flow for ZAP findings:
| ZAP risk level | Workflow action | Human action |
|---|---|---|
| HIGH (riskcode 3) | zap_file_issues.py auto-files a GitHub issue with severity:high + area:security + needs-grooming |
sre-agent triages; routes to feature-developer for code fixes |
| MEDIUM (riskcode 2) | Workflow posts summary in PR comment / step summary | Review manually; file issue if exploitable in this threat model |
| LOW / INFO | Logged in artifact only | No action required; review during monthly digest |
Artifact retention: ZAP HTML + JSON reports are uploaded as run artifacts for 30 days. Download from the Actions run page.
False-positive suppression: Rule overrides live in .zap/rules.tsv. Each
IGNORE entry requires a justification comment. Add new ignores via PR — do not
disable the scan job.
Rule override rationale (see .zap/rules.tsv for full comments):
- 10202 IGNORE — CSRF tokens not applicable to stateless JSON API with header auth
- 10015 IGNORE — Cache-control on CF Pages CDN previews is CDN-controlled, not app-controllable
- 10016 IGNORE — X-XSS-Protection is deprecated in modern browsers; CSP is the correct defense
- 10035 IGNORE — HSTS belongs on origin server config, not on CF Pages-hosted SPA
Hardening follow-ups tracked under Epic #81.
When the CI scan is noisy
Tune the ruleset, don't disable the job. Adjust bandit config, semgrep rules, or audit ignore lists with a short comment explaining why. The first iteration of CI will catch false positives — fixing the ruleset is the job.
When findings pile up faster than you can fix
Escalate to the parent epic (#81) as a blocker. Velocity ≠ value if we ship vulnerable code.