Featured project 14 · Security testing · OWASP ZAP
Security Testing with OWASP ZAP
Security checks a QA team can run on every change: OWASP ZAP baseline and full scans in Docker with clear pass/fail rules, and API tests for the vulnerabilities a scanner cannot see. The app comes in a hardened and a deliberately vulnerable build, and every planted vulnerability must be caught.
Purpose
A scan that has only ever been clean has not shown that it can find anything. This project proves its setup: ten vulnerabilities are planted in a second build of the same app, each mapped to the tool that must report it, and a verdict job fails the build if one is missed or if the hardened build has a new warning.
Technology stack
| Area | Tools |
|---|---|
| App | NoteVault, Node 24 running TypeScript directly, no dependencies; hardened and vulnerable build from one code base |
| DAST | OWASP ZAP 2.17.0 in Docker: baseline scan (passive) and full scan (active attacks), rules file with FAIL, WARN and IGNORE |
| Tests | node:test API tests for IDOR, mass assignment, brute force, CSRF on login, session fixation, user enumeration; unit tests for the verdict logic |
| Isolation | both builds and ZAP on a private Docker network, no ports published, repository mounted read-only |
| CI | GitHub Actions: checks, one scan job per build in parallel, a verdict job that writes the result table into the job summary |
Architecture overview
docker network notevault-scan
notevault-hardened ──┐
notevault-vulnerable ┴─◄ ZAP baseline + full scan ──► JSON reports
verify.ts: hardened = no FAIL / WARN · vulnerable = every planted defect as its rule id
npm test: IDOR · mass assignment · brute force (what ZAP cannot know)Key engineering decisions
- Defects are switches in one code base (has("defect-id")), so the two builds cannot drift apart; a unit test fails if one disappears
- The rules file is the policy: FAIL for the planted defects and classic high-impact problems, IGNORE only with a written reason
- A small verdict script decides from the JSON reports instead of relying on ZAP exit codes
- Access control is tested with knowledge of who owns what, because a scanner cannot know it
- The vulnerable build never leaves a private Docker network
Implemented features
- Six header and cookie defects caught by the baseline scan, reflected XSS by the full scan, three access-control defects by API tests
- The scan found two real weaknesses in the hardened build (a CSRF error page with an empty token, ambiguous paths accepted) and one false positive, all documented
- Hardened build: Content-Security-Policy, frame protection, nosniff, cross-origin isolation, HttpOnly/Secure/SameSite cookie, scrypt hashes, rate limit, new session id at sign-in
- Verdict table in the GitHub job summary; ZAP HTML and JSON reports as artifacts
- Design notes and 66 interview questions on security testing with ZAP, linked to the code (Markdown + PDF)
Setup and execution
npm ci
npm test :: API security tests, no Docker
npm run scan:full :: ZAP baseline + full scan of both builds (Docker)
npm run verify -- --full :: the verdictSample test results

CI workflow
On every push: static checks and the API security tests; then the baseline and full scan of each build in parallel jobs; then the verdict, written into the job summary. The whole run takes under four minutes.
Known limitations and future improvements
Limitations
- Unauthenticated scans: signed-in pages are covered by the API tests, not by ZAP
- HTTP on a private network: HSTS and TLS need a scan of a deployed environment; DAST does not replace SAST, dependency scanning or a penetration test
Next steps
- An authenticated ZAP context
- An API scan from an OpenAPI definition
