Semgrep and ZAP
Two open-source scanners — one reads the code, one attacks the running app — both configured in the repo.
Why two scanners
They catch different mistakes. Semgrep reads the source and finds patterns, like a raw SQL string or a secret read in the wrong file. OWASP ZAP never sees the source. It sends real requests to a running app and reports what comes back.
Both are free, run in Docker, and need no account. Their config lives in scripts/security, so a rule changes in the same pull request as the code it covers.
Semgrep
Bones doesn't use Semgrep's public rule packs. It has 15 rules of its own in scripts/security/semgrep/bones.yml, each one a line of SECURITY_REVIEW.md turned into a pattern:
- Secrets — dumping the environment, reading a secret outside its module, or inlining one into a client build.
- Database —
sql.raw, SQL built from strings, andlikewithout escaping. - Input — unbounded strings and arrays, and loose schemas.
- Auth — reading the session outside
auth.ts, or trustingX-Forwarded-For. - Rendering — HTML injected into the page.
bones.ts and bones.tsx beside the rules hold good and bad examples. CI tests the rules against them before scanning, so a rule that stops matching fails loudly instead of passing everything.
ZAP
ZAP runs two scans against a throwaway copy of the stack:
- A passive baseline of the web app. It crawls the pages and flags missing headers, weak cookies, and leaked information.
- An active scan of the backend. It sends SQL injection, open-redirect, and path probes to every signed-out route in
scripts/security/zap/backend-openapi.yaml. A local run sent 823 requests and got no server errors.
Every ZAP rule fails the scan unless scripts/security/zap/*.conf lowers it to a warning, with the reason on the same line.
Alongside them
Two checks are small scripts, not tools:
csp-check.mjsloads every page in Chromium through Playwright and fails on any Content Security Policy violation.client-secrets.mjsfails when a client reads an unlisted env var, or when a built bundle contains a secret.
GitHub's Dependabot alerts cover dependencies.