docs(16): create phase plan — 6 plans, 2 waves (dep audit, security checks, image hygiene)

SEC-01/02, DEP-01/02, IMG-01/02/03, CI-03. Wave 1: image-hygiene runtime guard (TDD), audit+outdated wrappers (TDD), eslint-plugin-security fold, gitleaks config+baseline+.dockerignore. Wave 2: ci.yml security job + gate wiring, publish.yml hygiene assertions + boot-smoke. esbuild GHSA-gv7w-rqvm-qjhr waivered in 16-02 before the gate goes live.
This commit is contained in:
Lucas Berger
2026-06-12 23:23:18 -04:00
parent bfc93584d7
commit e039c85a22
9 changed files with 1545 additions and 29 deletions
@@ -0,0 +1,163 @@
---
phase: 16-ci-dependency-audit-and-security-checks
plan: 04
type: execute
wave: 1
depends_on: []
files_modified:
- .gitleaks.toml
- scripts/gitleaks-baseline.json
- .dockerignore
autonomous: false
requirements: [SEC-01, IMG-02]
must_haves:
truths:
- "A gitleaks config exists that inherits the default ruleset and allowlists the known test-fixture VAPID keys + .env.example/.env.spike so they do not trip the gate"
- "A full-history/full-tree gitleaks baseline scan has been run and committed, suppressing any pre-existing findings so the first PR-diff scan starts from a clean known state"
- "A .dockerignore exists that keeps secrets, dev affordances, tests, and bulk out of the Docker build context WITHOUT excluding apps/api/src (the builder stage needs it)"
artifacts:
- path: ".gitleaks.toml"
provides: "gitleaks config: useDefault + allowlists for VAPID fixture / env templates"
contains: "useDefault"
- path: "scripts/gitleaks-baseline.json"
provides: "Committed full-history baseline scan output"
- path: ".dockerignore"
provides: "Build-context filter (secrets/dev/bulk), preserving apps/api/src + manifests"
contains: ".env"
key_links:
- from: ".gitleaks.toml"
to: "apps/api/tests/fixtures/vapid.ts"
via: "[[allowlists]] paths regex"
pattern: "vapid"
---
<objective>
Author the gitleaks configuration and the committed full-history baseline (D-02) and create the full `.dockerignore` (D-09 / IMG-02) — the static security-scan and image-hygiene artifacts the CI jobs in Wave 2 consume. This delivers the secret-scanning half of the D-01 security-check baseline (secret scanning + static security lint; Trivy/image CVE scanning is dropped per D-01).
Purpose: The app holds real family credentials (encryption key, OIDC secret, Fastmail app passwords), so secret scanning is core. A per-PR diff scan (wired in 16-05) needs a config that allowlists the known test-fixture VAPID keypair (`apps/api/tests/fixtures/vapid.ts`) and env templates, plus a one-time baseline so pre-existing findings do not block every future PR. Separately, today the entire repo root is sent to the Docker daemon as build context (no `.dockerignore` exists), so `.env`, dev seed scripts, tests, and `.planning/` are all shipped to the builder. The `.dockerignore` must exclude secrets/dev/bulk while preserving `apps/api/src` (the builder stage's `COPY apps/api ./apps/api` needs it) and the workspace manifests.
Output: `.gitleaks.toml`, `scripts/gitleaks-baseline.json`, `.dockerignore`. Consumed by 16-05 (gitleaks PR scan references the config + baseline) and 16-06 (static assertion greps the .dockerignore). The baseline-scan step is a human-verify checkpoint because it requires running gitleaks against the real repo history and confirming the only findings are the known test fixtures.
</objective>
<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/16-ci-dependency-audit-and-security-checks/16-CONTEXT.md
@.planning/phases/16-ci-dependency-audit-and-security-checks/16-RESEARCH.md
@.planning/phases/16-ci-dependency-audit-and-security-checks/16-PATTERNS.md
@.gitignore
</context>
<tasks>
<task type="auto">
<name>Task 1: Author .gitleaks.toml with default ruleset + fixture/env allowlists</name>
<read_first>
- .gitleaks.toml (file being created)
- .planning/phases/16-ci-dependency-audit-and-security-checks/16-RESEARCH.md (.gitleaks.toml Config section — exact content: title, [extend] useDefault=true, [[allowlists]] blocks with paths regex for apps/api/tests/fixtures/vapid.ts, .env.example, apps/api/.env.spike; the baseline caveat about the VAPID fixture)
- apps/api/tests/fixtures/vapid.ts (the real-looking VAPID keypair that WILL be flagged unless allowlisted)
- .gitignore (env-handling conventions: .env / .env.* ignored, .env.example kept)
</read_first>
<action>
Create .gitleaks.toml at repo root with: a `title`, an `[extend]` section with `useDefault = true` (inherit the built-in secret ruleset), and three `[[allowlists]]` blocks each with a `description` and a `paths` regex array allowlisting (1) apps/api/tests/fixtures/vapid.ts (documented test-only VAPID values), (2) `.env.example` (intentional placeholder template), and (3) apps/api/.env.spike (dev/spike values). Use the exact structure from RESEARCH.md. Do NOT add custom detection rules — only the default set plus allowlists. Commit: `chore(16-04): add gitleaks config with fixture + env allowlists`.
</action>
<verify>
<automated>grep -q "useDefault" .gitleaks.toml && grep -q "vapid" .gitleaks.toml && grep -q "env.example" .gitleaks.toml && echo OK</automated>
</verify>
<acceptance_criteria>
- .gitleaks.toml exists with `[extend] useDefault = true`
- Three `[[allowlists]]` blocks cover the VAPID fixture, .env.example, and .env.spike, each with a description
- No custom `[[rules]]` were added (default ruleset only)
</acceptance_criteria>
<done>The gitleaks config inherits the default ruleset and allowlists the three known-safe paths.</done>
</task>
<task type="auto">
<name>Task 2: Create the full .dockerignore (secrets/dev/bulk, preserving builder inputs)</name>
<read_first>
- .dockerignore (file being created)
- .planning/phases/16-ci-dependency-audit-and-security-checks/16-RESEARCH.md (Exact .dockerignore Line List section — the full recommended content; the critical insight that .dockerignore filters the build CONTEXT only, not COPY --from=stage; the "Items NOT excluded" list: apps/api/src, apps/pwa/src, pnpm-workspace.yaml, pnpm-lock.yaml, package.json, package.jsons, tsconfig.jsons)
- apps/api/Dockerfile (builder stage `COPY apps/api ./apps/api` at line 16 — proves apps/api/src MUST stay in context; pwa-builder `COPY apps/pwa` at line 32)
- .gitignore (section-header comment style to mirror)
</read_first>
<action>
Create .dockerignore at repo root mirroring the RESEARCH.md "Recommended .dockerignore" content with section-header comments (Secrets, VCS, Build artifacts, Dependencies, Tests, Playwright artifacts, Planning/docs, Editor/OS, CI config, SQL dumps). MUST exclude: .env, .env.* (with `!.env.example` un-ignore), apps/api/scripts/seed-credential.mjs, .git, **/dist/, **/node_modules/, apps/api/tests/, apps/api/test/, apps/pwa/e2e/, Playwright artifact dirs, .planning/, docs/, editor/OS files, .gitea/, and *.sql dumps. MUST NOT exclude apps/api/src, apps/pwa/src, pnpm-workspace.yaml, pnpm-lock.yaml, the package.json files, or the tsconfig.json files (the builder/pwa-builder stages need them). Add the explanatory NOTE comment from RESEARCH about migration .sql files traveling only in the builder stage. Commit: `chore(16-04): add .dockerignore (secrets/dev/bulk, preserve builder inputs)`.
</action>
<verify>
<automated>set -e; for p in ".env" "node_modules" "apps/api/scripts" ".git" ".planning" "apps/api/tests" "apps/pwa/e2e"; do grep -q "$p" .dockerignore || { echo "MISSING $p"; exit 1; }; done; grep -Eq '(^|/)apps/api/src( |/|$)' .dockerignore && { echo "ERROR: apps/api/src is excluded"; exit 1; }; echo OK</automated>
</verify>
<acceptance_criteria>
- .dockerignore exists and contains all forbidden patterns the 16-06 static assertion greps for (.env, node_modules, apps/api/scripts, .git, .planning, apps/api/tests, apps/pwa/e2e)
- .dockerignore does NOT exclude apps/api/src (verify grep finds no such line)
- `!.env.example` un-ignore is present so the template survives
</acceptance_criteria>
<done>The .dockerignore excludes secrets/dev/bulk from the build context while preserving builder-stage inputs.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking-human">
<name>Task 3: Run + commit the gitleaks full-history baseline; confirm only known fixtures are flagged</name>
<read_first>
- .planning/phases/16-ci-dependency-audit-and-security-checks/16-RESEARCH.md (Full-History Baseline Scan section — exact command `gitleaks git --config .gitleaks.toml --report-path scripts/gitleaks-baseline.json`; the VAPID fixture caveat; --baseline-path semantics)
- .gitleaks.toml (config authored in Task 1)
</read_first>
<what-built>
Tasks 1-2 created .gitleaks.toml and .dockerignore. This checkpoint runs the one-time full-history/full-tree gitleaks baseline scan locally and commits the result, so the PR-diff scan wired in 16-05 starts from a clean, reviewed known state. This must be human-verified because the scan reads the real repo history and the operator must confirm that the ONLY findings are the documented test fixtures (VAPID keys) — a real leaked credential surfacing here is a genuine security event, not noise.
Automated steps the executor performs first:
1. Install gitleaks v8.30.1 locally (single binary): download `gitleaks_8.30.1_linux_x64.tar.gz` from github.com/gitleaks/gitleaks releases, extract, chmod +x.
2. Run `gitleaks git --config .gitleaks.toml --report-path scripts/gitleaks-baseline.json` from repo root.
3. Inspect scripts/gitleaks-baseline.json — list every finding's file + rule.
</what-built>
<how-to-verify>
1. Review the executor's listing of baseline findings.
2. Confirm EVERY finding is one of: the test-fixture VAPID keys (apps/api/tests/fixtures/vapid.ts), .env.example placeholders, or .env.spike dev values — all of which Task 1 allowlisted (so ideally the baseline is empty/near-empty after allowlisting).
3. If ANY finding is a real credential (an actual OIDC secret, encryption key, or Fastmail app password committed to history) → STOP. Do not approve. This is a genuine leak requiring rotation + history rewrite, out of scope for this plan — flag it to the operator.
4. If all findings are the known fixtures (or none), approve. The executor then commits scripts/gitleaks-baseline.json with message `chore(16-04): commit gitleaks full-history baseline`.
</how-to-verify>
<verify>
<automated>test -f scripts/gitleaks-baseline.json && node -e "JSON.parse(require('fs').readFileSync('scripts/gitleaks-baseline.json','utf8')); console.log('valid JSON baseline')"</automated>
</verify>
<resume-signal>Type "approved" once you confirm the baseline contains only known test fixtures (or is empty), or describe any real credential found.</resume-signal>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| developer commit → git history | A secret committed to any branch enters history; gitleaks scans the git object, not just the working tree |
| repo working tree → Docker build context | Everything in the context is sent to the daemon and reachable by COPY; secrets/dev files must be filtered out |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-16-11 | Information Disclosure | A real OIDC secret / encryption key / Fastmail app password committed to git history | mitigate | D-02: gitleaks default ruleset + full-history baseline (Tasks 1, 3) surface any pre-existing leak; the human checkpoint blocks approval if a real credential is found |
| T-16-12 | Information Disclosure | Test-fixture keys mis-flagged, masking real findings in noise | mitigate | .gitleaks.toml allowlists the known fixture/template paths (Task 1) so the scan signal is real leaks only |
| T-16-13 | Information Disclosure | `.env`, seed-credential.mjs, .planning, or family data shipped in the Docker image | mitigate | D-09: .dockerignore (Task 2) filters secrets/dev/bulk from the build context; verified by 16-06 static assertion |
| T-16-14 | Tampering | .dockerignore accidentally excludes apps/api/src, breaking the build | accept | Task 2 verify explicitly asserts apps/api/src is NOT excluded; build failure is loud and caught at publish, residual risk nil |
</threat_model>
<verification>
- `.gitleaks.toml` has `useDefault = true` + the three fixture/env allowlists
- `scripts/gitleaks-baseline.json` exists, is valid JSON, and was human-confirmed to contain only known fixtures
- `.dockerignore` contains every forbidden pattern the 16-06 assertion checks AND does not exclude apps/api/src
</verification>
<success_criteria>
- gitleaks config inherits the default ruleset and allowlists known-safe paths
- Full-history baseline committed and confirmed free of real credentials (human checkpoint)
- .dockerignore excludes secrets/dev/bulk while preserving builder-stage inputs
</success_criteria>
<output>
Create `.planning/phases/16-ci-dependency-audit-and-security-checks/16-04-SUMMARY.md` when done.
</output>