The principle lives in CLAUDE.md §"Security is a main concern" and the DoD checklist in DoD.md §6.2. Both are runtime-agnostic. This file holds the recipes — concrete patterns per runtime so projects don't reinvent the wheel.
Pick one when you fill in project_config_overview.md §"Security stack".
Combinations are fine (e.g. an AWS-hosted backend that also ships a
desktop companion app).
Why these four capabilities aren't optional. Quality of working software degrades to zero the moment something is exploited in production. The recipes below all deliver the same four capabilities; they just differ in tooling and where the gate fires: keep secrets out of git → catch OWASP patterns before push → watch dependencies + infra for CVEs continuously → fix-before-deploy for everything that touches a customer surface.
The struct2flow default per STACK_DEFAULTS.md: Lambda + DynamoDB + Amplify Hosting + CDK + CodePipeline.
.githooks/pre-push runs gitleaks protect --staged before any push.
Blocks on detection. Catches AWS keys, OpenAI tokens, JWT secrets,
arbitrary high-entropy strings, etc.
For accidental commits that already happened: gitleaks detect over
the full history, then rewrite with git filter-repo and rotate the
leaked credential immediately. The credential is compromised the
moment it lands on origin — assume it's been scraped within
minutes.
Secrets at runtime live in AWS Secrets Manager / Parameter Store,
fetched at Lambda cold-start. Never in .env files committed to the
repo, never in CDK string literals.
Semgrep runs in pre-push with --config=auto (mostly community
OWASP rules + struct2flow-specific patterns). Typical PR cost: 5-10s.
Blocks on any finding ≥ WARNING severity unless suppressed inline
with a justification comment.
ESLint carries eslint-plugin-security +
eslint-plugin-no-unsanitized in the workspace's eslint config.
Already part of the lint gate (§4.2 in DoD), so cost is zero
incremental.
CI runs the deeper Semgrep pack
(--config=p/owasp-top-ten --config=p/r2c-security-audit) — slower,
not pre-push budget.
Pre-push: osv-scanner --lockfile=backend/package-lock.json --lockfile=frontend/package-lock.json.
Fails on any CVE rated High or Critical. Below that → tracked
in docs/config/findings.md as a planned upgrade.
CI nightly: same scan across the full repo; if a new CVE just dropped in something we already use, it fires an alert (same SNS → Slack pipe as MALT).
CI step before any deploy: cdk synth then trivy config cdk.out/.
Catches public S3 buckets, missing SSE, over-permissive IAM
statements, security-group 0.0.0.0/0 ingress, unencrypted RDS, etc.
Severity policy: High or Critical → blocks the deploy.
Medium → allowed but recorded in findings.md for cleanup.
CI pipeline (CodePipeline): after the preview env is healthy, run ZAP baseline scan against the API Gateway URL. Passive only, ~5 min. Findings annotate the build but only High or Critical block promotion to prod.
Nightly: full active scan against the prod URL (read-paths only — nothing that mutates state). Results land in S3, summarized in the same Slack channel as MALT alerts.
Each project carries a project_config_security.md (sibling to
project_config_dod.md) listing:
- Trust boundaries — what's the public surface, what's intra-VPC, what's per-tenant.
- Auth surfaces — Cognito / Auth0 / custom; token lifetime; refresh policy.
- Sensitive data classes — PII, payment, credentials; storage
- transport + retention rules per class.
- Adversary assumptions — what we defend against, what we don't (out-of-scope by design).
- Incident playbook — first 10 minutes, escalation, comms.
The threat model is reviewed at every major feature plan — same gate as the §2 major bug consensus.
Apps with no deployed HTTP surface (Electron / Tauri desktop, CLIs, local-first utilities).
Same gitleaks pre-push hook as Recipe A. Plus: any local config the
app writes (token cache, keystore) must live under the OS keychain
or an OS-protected user dir (~/Library/Keychains on macOS,
Credential Manager on Windows, libsecret on Linux). Never plain
JSON on disk.
Same as Recipe A.
Same as Recipe A. Critical for desktop apps because users install the shipped artifact directly — a vulnerable dependency travels into every install.
Distribution artifacts (DMG / MSI / AppImage / npm tarball) are
signed and the public key + SHAs are published with the release.
For Electron / Tauri: codesign on macOS, signtool on Windows. For
CLIs distributed via npm: enable npm provenance.
Skip ZAP / Nuclei. There's nothing remote to scan. The threat model shifts to: malicious update server, compromised lockfile, local privilege escalation. Cover via §1 + §3 + §4.
Adapted for desktop:
- Trust boundaries — user filesystem, OS keychain, update server.
- Auth surfaces — none, or device-bound tokens.
- Sensitive data classes — what the app stores locally, encrypted
at rest (
age/libsodium). - Update channel — signed, fetched over HTTPS, public key pinned.
- Adversary assumptions — same machine, malicious dependencies, spoofed update server.
For services packaged as containers (struct2flow's fallback when Lambda's 15-min ceiling or package size bite).
Same gitleaks pre-push. At runtime, secrets come from the
orchestrator's secret store (ECS task definition secrets,
Kubernetes Secret, etc.). Never in the image, never in env-vars
baked into the Dockerfile.
CI step right after docker build:
trivy image --severity HIGH,CRITICAL --exit-code 1 <image>:<tag>.
Blocks the push to ECR / GHCR on any High/Critical CVE in the
base image or any layer. Forces base-image refresh discipline.
Catches the same drift class as Recipe A.4 in the orchestrator's own config language.
Same shape as Recipe A.5. If the service is internal-only (no public ingress), skip the prod scan but keep the preview-env baseline scan.
Container-specific additions:
- Image provenance — base image source, signed?
- Runtime sandboxing — read-only root FS, dropped capabilities, no privileged mode, seccomp profile.
- Network egress — does the container need outbound internet? If not, deny by default at the orchestrator level.
These bind every project regardless of stack.
Per the blueprint pre-push hook (DoD §4 enriched in §4.7):
gitleaks protect --staged— fails on any secretsemgrep --config=auto— fails onWARNING+severityosv-scanner --lockfile=…— fails onHIGH+CVE- (lint security plugins ride inside
npm run lint— already wired)
Budget: all four must fit inside the §3.7 pre-push ≤30 s ceiling. If they don't, move the offender to CI (Semgrep deep packs are the usual offender, not gitleaks).
| Step | Tool | Blocks on | Where the finding lives |
|---|---|---|---|
| Deep SAST | semgrep --config=p/owasp-top-ten --config=p/r2c-security-audit |
Any HIGH | PR comment + findings.md |
| Container scan | trivy image (Recipe C only) |
HIGH+ CVE | Blocks ECR push |
| IaC scan | trivy config |
HIGH+ misconfig | Blocks deploy |
| DAST baseline | zap-baseline.py |
HIGH+ alert | Blocks promote to prod |
osv-scannerover all lockfiles — catches new CVEs in deps we already use. Alert routes through the same MALT pipe.- ZAP full active scan against prod (read-paths only).
- Nuclei against deployed targets — template-based, complementary to ZAP.
docs/config/findings.md is the canonical place where every
security finding lives until fixed or accepted. Same register as
Codex review findings — security findings carry a [SEC] tag.
A finding marked Status: Accepted has a sign-off line naming
who accepted the risk and why.
When a security finding hits production:
- Stop the bleed — disable the affected route / rotate the leaked secret / pin the vulnerable dep at a known-good version. Speed beats elegance here.
- Write the bug —
BUG-XXXindocs/doing/BUGS.mdwith the[SEC]tag. Two-commit pattern still applies: reproducer first, fix second. - Notify per project policy — if PII / payment / credentials
were exposed, the
project_config_security.md§"Incident playbook" names who gets paged and the regulatory clock. - Postmortem —
docs/done/INCIDENT-YYYY-MM-DD.mdwith timeline, root cause, what the gate missed, what changes (new rule? new scanner? new pre-push step?). The DoD evolves from real incidents, not hypotheticals.
- Hard-coded secrets, even "just for local dev" — that file will be committed eventually.
// eslint-disable-next-lineor# nosec/// nosemgrepwithout a justification comment naming the threat model entry that makes the suppression safe.- A new public route without a corresponding ZAP baseline run.
- A dep upgrade that introduces a new HIGH+ CVE without an immediate rollback or pin.
- A
findings.mdentry left untriaged across two consecutive grooming passes — either fix, defer with a date, or markStatus: Acceptedwith a sign-off.