---
phase: 16
slug: ci-dependency-audit-and-security-checks
status: draft
nyquist_compliant: true
wave_0_complete: false
created: 2026-06-12
---

# Phase 16 — Validation Strategy

> Per-phase validation contract for feedback sampling during execution.

This phase is mostly CI/Docker/security wiring. Only two artifacts carry unit-testable
pure logic — the boot guard (`assertNotDevBypassInProduction()`) and the audit-wrapper
filter (`check-audit.mjs`). Everything else is verified by file-assertion, `pnpm lint`,
or a CI-run / boot-smoke that is exercised after merge.

---

## Test Infrastructure

| Property | Value |
|----------|-------|
| **Framework** | Vitest (apps/api) for the boot guard; `node --test` for the root-level audit wrapper |
| **Config file** | `apps/api/vitest.config.ts`; root scripts use no config (`node --test`) |
| **Quick run command** | `pnpm --filter @familysync/api test -- --run tests/lib/bootGuards.test.ts` |
| **Full suite command** | `pnpm --filter @familysync/api test && node --test scripts/__tests__/check-audit.test.mjs` |
| **Estimated runtime** | ~15 seconds |

---

## Sampling Rate

- **After every task commit:** Run the relevant quick command (boot guard unit test, or `node --test` for the audit wrapper, or `pnpm lint` for the eslint fold)
- **After every plan wave:** Run the full suite command
- **Before `/gsd-verify-work`:** Full API suite green + `pnpm lint` green + (post-merge) publish boot-smoke PASS
- **Max feedback latency:** 60 seconds

---

## Per-Task Verification Map

| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
| 16-01-01 | 01 | 1 | IMG-01 | T-16-02 | Failing test pins guard exit(1) on prod+bypass | unit | `pnpm --filter @familysync/api test -- --run tests/lib/bootGuards.test.ts` | ❌ W0 | ⬜ pending |
| 16-01-02 | 01 | 1 | IMG-01 | T-16-02 | Guard exits 1 on NODE_ENV=production + DEV_AUTH_BYPASS=true; inert otherwise | unit | `pnpm --filter @familysync/api test -- --run tests/lib/bootGuards.test.ts` | ❌ W0 | ⬜ pending |
| 16-01-03 | 01 | 1 | IMG-01 | T-16-01 | Production image bakes NODE_ENV=production | file-assert | `grep -c "ENV NODE_ENV=production" apps/api/Dockerfile` (==1, in production stage) | ✅ | ⬜ pending |
| 16-02-01 | 02 | 1 | DEP-01 | T-16-04/T-16-05 | esbuild High advisory waived with reason+reviewer before gate goes live | file-assert | `node -e "require('./scripts/audit-allowlist.json')['GHSA-gv7w-rqvm-qjhr']"` | ❌ W0 | ⬜ pending |
| 16-02-02 | 02 | 1 | DEP-01 | T-16-04 | Wrapper blocks unwaived High+Critical, honors allowlist | unit | `node --test scripts/__tests__/check-audit.test.mjs` | ❌ W0 | ⬜ pending |
| 16-02-03 | 02 | 1 | DEP-02 | T-16-06 | Outdated report tiered, pin-aware, always exit 0 | behavior | `node scripts/check-outdated.mjs; test $? -eq 0` | ❌ W0 | ⬜ pending |
| 16-03-01 | 03 | 1 | SEC-02 | T-16-08 | eslint-plugin-security registered as blocking errors | file-assert | `grep -q pluginSecurity eslint.config.js` | ✅ | ⬜ pending |
| 16-03-02 | 03 | 1 | SEC-02 | T-16-08/T-16-09 | Lint green with security rules active; suppressions justified | lint | `pnpm lint` | ✅ | ⬜ pending |
| 16-04-01 | 04 | 1 | SEC-01 | T-16-12 | gitleaks config inherits default ruleset + fixture/env allowlists | file-assert | `grep -q useDefault .gitleaks.toml && grep -q vapid .gitleaks.toml` | ✅ | ⬜ pending |
| 16-04-02 | 04 | 1 | IMG-02 | T-16-13/T-16-14 | .dockerignore excludes secrets/dev/bulk, preserves apps/api/src | file-assert | `grep -q "apps/api/tests" .dockerignore` and apps/api/src NOT excluded | ✅ | ⬜ pending |
| 16-04-03 | 04 | 1 | SEC-01 | T-16-11 | Full-history baseline committed, only known fixtures flagged | human-verify + file-assert | `test -f scripts/gitleaks-baseline.json` + operator confirms findings | ✅ | ⬜ pending |
| 16-05-01 | 05 | 2 | CI-03/SEC-01/DEP-01/DEP-02 | T-16-15/T-16-16/T-16-17 | security job: gitleaks always, audit/outdated code-gated, base.sha probed | yaml-parse | `python3 -c "import yaml;yaml.safe_load(open('.gitea/workflows/ci.yml'))['jobs']['security']"` | ✅ | ⬜ pending |
| 16-05-02 | 05 | 2 | CI-03 | T-16-15 | gate requires security success (individual result check) | yaml-parse | `grep -q needs.security.result .gitea/workflows/ci.yml` | ✅ | ⬜ pending |
| 16-06-01 | 06 | 2 | IMG-03 | T-16-20 | build and push are separate steps (assertion seam) | yaml-parse | `python3` build/push split assertion | ✅ | ⬜ pending |
| 16-06-02 | 06 | 2 | IMG-03 | T-16-18/T-16-19/T-16-21 | static + boot-smoke assertions between build and push | yaml-parse | `python3` assertions-between-build-and-push assertion | ✅ | ⬜ pending |

*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*

---

## Wave 0 Requirements

These are the test/scaffold assets that do not yet exist and must be created by their owning
task as the FIRST step (RED) before implementation:

- [ ] `apps/api/tests/lib/bootGuards.test.ts` — unit tests for `assertNotDevBypassInProduction()` (created by 16-01 Task 1, RED)
- [ ] `apps/api/src/lib/bootGuards.ts` — exported guard function (created by 16-01 Task 2, GREEN)
- [ ] `scripts/__tests__/check-audit.test.mjs` — unit tests for the audit-wrapper filter logic (created by 16-02 Task 2)
- [ ] `scripts/check-audit.mjs` — audit wrapper (16-02 Task 2)
- [ ] `scripts/check-outdated.mjs` — outdated wrapper (16-02 Task 3)
- [ ] `scripts/audit-allowlist.json` — seeded with `GHSA-gv7w-rqvm-qjhr` (16-02 Task 1)
- [ ] `scripts/outdated-pins.json` — intentional-pin reasons (16-02 Task 1)
- [ ] `.gitleaks.toml` — config + allowlists (16-04 Task 1)
- [ ] `scripts/gitleaks-baseline.json` — full-history scan output, committed (16-04 Task 3)
- [ ] `.dockerignore` — root-level build-context filter (16-04 Task 2)

The boot-guard unit test (`bootGuards.test.ts`) is the primary Wave 0 test asset. The audit-wrapper
test is the secondary. All other artifacts are config/wiring verified by file-assertion, lint, or CI-run.

---

## Manual-Only Verifications

| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| Full-history gitleaks baseline contains only known test fixtures (no real leaked credential) | SEC-01 | Reading repo history for real secrets is a judgment call; a real finding is a security event needing rotation, not auto-approval | 16-04 Task 3 checkpoint: review baseline findings; approve only if every finding is the VAPID fixture / env template |
| A PR with a deliberately-planted fake secret in the diff fails `CI / gate` via the security job | SEC-01 | Requires opening a throwaway PR against the live Gitea runner | After merge: push a throwaway branch adding a fake AWS-key-shaped string to a tracked file; open PR; confirm gate fails on the security job; close PR |
| A doc-only PR still runs gitleaks but skips audit/outdated | SEC-01 / DEP-01 | Requires a live runner PR to observe step skip behavior | After merge: open a doc-only PR; confirm the security job runs gitleaks (visible in log) and the audit/outdated steps are skipped |
| The published production image refuses to boot with DEV_AUTH_BYPASS=true (boot-smoke PASS in a real publish run) | IMG-03 | Only runs on push-to-main publish against the built image | After this branch merges, watch the Publish workflow run; confirm the boot-smoke step prints PASS and the image publishes |
| base.sha is available on the Gitea runner (Assumption A2 / OQ-1) | SEC-01 | Gitea event-context parity is not probe-confirmed for this field | The 16-05 probe step echoes base.sha/head.sha in the first PR's security-job log; confirm BASE_SHA resolves (event context or merge-base fallback) |

---

## Validation Sign-Off

- [x] All tasks have `<automated>` verify or Wave 0 dependencies
- [x] Sampling continuity: no 3 consecutive tasks without automated verify
- [x] Wave 0 covers all MISSING references
- [x] No watch-mode flags (uses `--run` / `node --test`, never `vitest` watch)
- [x] Feedback latency < 60s
- [x] `nyquist_compliant: true` set in frontmatter

**Approval:** approved 2026-06-13
