Every control in this course can be tested, and untested controls erode: a refactor drops a middleware, a new route skips the role check, a dependency update reintroduces a known flaw. Security testing puts those checks into the pipeline so regressions are caught like any broken unit test. No single tool covers everything, so layer several cheap ones.
In this lesson you will learn what each testing category finds, how to run static analysis on Node.js code, how to run an OWASP ZAP scan against a running application, how to write security assertions as ordinary tests, and how to triage results.
| Layer | What it examines | Finds | Typical tools |
|---|---|---|---|
| SAST (static) | Source code, without running it | Injection sinks, unsafe APIs, hard-coded secrets | Semgrep, CodeQL, ESLint security plugins |
| SCA (composition) | Dependency manifests | Known vulnerable packages | npm audit, Dependabot, Snyk |
| Secret scanning | Repository contents and history | Committed credentials | gitleaks, host push protection |
| DAST (dynamic) | The running application over HTTP | Missing headers, reflected input, exposed endpoints | OWASP ZAP |
| Security unit tests | Your own controls | Regressions in authz, validation, headers | Jest or Vitest with supertest |
Static tools run in seconds on every pull request; dynamic tools run against staging; periodic manual review and penetration testing find the logic flaws tools cannot.
Semgrep matches code patterns, and its community rule packs for Node.js and Express cover the classic sinks:
# Run the Node.js and Express rule packs, fail on findings, output SARIF for code-scanning UIs
semgrep scan --config p/nodejs --config p/express --error --sarif --output semgrep.sarifA typical finding points at an exec() call built from a variable and links to the fix. Add project-specific rules for your own conventions, such as "every router under /admin must call requireRole". Findings that block a merge get fixed; weekly reports get ignored.
ZAP is a free proxy and scanner. Its baseline scan spiders the target and runs passive checks only, so it is safe against staging in every pipeline:
docker run --rm -v "$PWD":/zap/wrk:rw ghcr.io/zaproxy/zaproxy:stable \
zap-baseline.py -t https://staging.example.com -r zap-report.html -c zap.confThe report lists missing security headers, cookies without flags, information leakage and reflected parameters. The full scan (zap-full-scan.py) adds active attacks such as injection probes; run it only against environments you own, with written authorization, never against production or third parties. For APIs, zap-api-scan.py takes an OpenAPI definition so every documented endpoint is exercised.
The most durable checks live next to the feature tests. The sample file asserts three controls: the header baseline, ownership on a data route, and type validation on a login body. Write one such test for every control you add: 403 for the wrong role, 413 for an oversized body, 429 after the limit, 401 for an expired token. They run in milliseconds and fail the build on exactly the regression a refactor would introduce.
# .github/workflows/security.yml (excerpt)
- run: npm ci --ignore-scripts
- run: npm audit --audit-level=high
- run: npx gitleaks git --no-banner
- run: semgrep scan --config p/nodejs --config p/express --error
- run: npm test # includes tests/security.test.js
# after deploy to staging:
- run: docker run --rm ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t ${{ vars.STAGING_URL }}Triage each finding by reachability with untrusted input and by impact; fix at the root, suppress only with a comment explaining why, and review suppressions quarterly.
What is the main difference between SAST and DAST?
Next lesson: Cloud Security Basics: IAM, Storage and Network — the settings that decide whether a cloud account is a fortress or a public folder.