Files
familysync/.planning/phases/16-ci-dependency-audit-and-security-checks/16-VERIFICATION.md
T
Lucas Berger c72e013a7b
CI / changes (pull_request) Successful in 3s
CI / fast-checks (pull_request) Failing after 57s
CI / api (pull_request) Successful in 58s
CI / harness (pull_request) Successful in 3m59s
CI / security (pull_request) Successful in 40s
CI / gate (pull_request) Failing after 1s
docs(phase-16): complete phase execution
2026-06-13 09:00:12 -04:00

13 KiB

phase, verified, status, score, overrides_applied
phase verified status score overrides_applied
16-ci-dependency-audit-and-security-checks 2026-06-13T12:56:26Z passed 8/8 0

Phase 16: CI Dependency Audit, Security Checks & Image Hygiene — Verification Report

Phase Goal: Extend Gitea CI with outdated-dependency reporting + vulnerability audit + a baseline of additional security checks, and enforce the dev/prod image boundary so no dev-bypass, secret, or family data ships in published images. Verified: 2026-06-13T12:56:26Z Status: passed Re-verification: No — initial verification

Goal Achievement

Observable Truths

# Truth Status Evidence
1 A production image with DEV_AUTH_BYPASS=true refuses to boot (process exits non-zero) VERIFIED assertNotDevBypassInProduction() in bootGuards.ts calls process.exit(1) when NODE_ENV==='production' && DEV_AUTH_BYPASS==='true'; 3/3 unit tests pass
2 The production Docker stage bakes NODE_ENV=production so the devBypass hard guard is engaged VERIFIED ENV NODE_ENV=production at line 45 of apps/api/Dockerfile, inside FROM base AS production stage only
3 The boot guard is a unit-tested exported function, called first in isMainModule() VERIFIED bootGuards.ts exports assertNotDevBypassInProduction; called at index.ts:116 as the first statement inside if (isMainModule()) {, before VAPID config (line 121), workers (line 141), and serve (line 149)
4 The audit wrapper blocks unwaived High+Critical advisories; esbuild GHSA waived with expiry VERIFIED check-audit.mjs exports selectBlocking/partitionAdvisories/isWaived; no --audit-level flag; esbuild GHSA-gv7w-rqvm-qjhr waived in audit-allowlist.json with reviewer, reason, and future expiry 2026-09-01; 9/9 unit tests pass (including expiry + fail-closed on malformed date)
5 The outdated wrapper is advisory-only (always exits 0), tiered, and pin-aware VERIFIED check-outdated.mjs unconditionally calls process.exit(0) at line 189; no reachable process.exit(1); four tiers (OUTDATED-WITH-ADVISORY / MAJOR-BEHIND-INTENTIONAL / MAJOR-BEHIND-UNPINNED / ROUTINE-DRIFT); reads outdated-pins.json with four pin reasons (eslint, @eslint/js, zod, @types/node)
6 eslint-plugin-security runs as blocking errors in pnpm lint, baseline is green VERIFIED eslint-plugin-security@3.0.1 in root devDependencies; folded into eslint.config.js section 5 before prettierConfig; detect-object-injection disabled globally with inline justification comment; pnpm lint exits 0 with --max-warnings 0; pnpm typecheck passes
7 A gitleaks config with useDefault + fixture allowlists exists; clean baseline committed VERIFIED .gitleaks.toml has [extend] useDefault = true and 4 [[allowlists]] blocks (VAPID fixture, .env.example, .env.spike, crypto test); scripts/gitleaks-baseline.json is valid JSON [] (empty — no pre-existing findings); .dockerignore covers all 7 forbidden patterns and does NOT exclude apps/api/src
8 security job (gitleaks always; audit/outdated code-gated) wired into gate as strict success VERIFIED ci.yml has security: job with needs: [changes], if: pull_request; checkout has fetch-depth: 0; gitleaks steps have no if:; audit/outdated steps have if: needs.changes.outputs.code == 'true'; gate needs: includes security; gate script checks needs.security.result != 'success' as an individual non-skippable check (not in the success-or-skipped loop)

Score: 8/8 truths verified

Required Artifacts

Artifact Expected Status Details
apps/api/src/lib/bootGuards.ts Exports assertNotDevBypassInProduction() VERIFIED Exists, exports function, correct logic
apps/api/tests/lib/bootGuards.test.ts 3 unit test cases VERIFIED 3 cases present; 3/3 pass via vitest
apps/api/Dockerfile ENV NODE_ENV=production in production stage VERIFIED Line 45, inside FROM base AS production only
apps/api/src/index.ts Imports and calls guard first in isMainModule VERIFIED Import at line 18; call at line 116, first statement in block
scripts/check-audit.mjs Blocking wrapper with pure filter exports VERIFIED Exports selectBlocking, partitionAdvisories, isWaived; no --audit-level
scripts/audit-allowlist.json GHSA-gv7w-rqvm-qjhr waiver with reason+reviewer+expires VERIFIED Valid JSON; all fields present; expiry 2026-09-01 (future)
scripts/check-outdated.mjs Advisory-only tiered report; always exits 0 VERIFIED process.exit(0) at end; no reachable exit(1) on report path
scripts/outdated-pins.json 4 pin reasons (eslint, @eslint/js, zod, @types/node) VERIFIED All 4 present with justification text
scripts/__tests__/check-audit.test.mjs 9 test cases (4 plan-required + 5 expiry/edge cases) VERIFIED 9/9 pass via node --test
eslint.config.js eslint-plugin-security before prettierConfig; detect-object-injection off with justification VERIFIED Section 5; inline comment on disabled rule
package.json eslint-plugin-security in devDependencies VERIFIED 3.0.1
.gitleaks.toml useDefault + 3 allowlists (VAPID, .env.example, .env.spike) VERIFIED Present; 4 allowlists (plan called for 3; crypto.test.ts is a bonus)
scripts/gitleaks-baseline.json Valid JSON, confirmed clean VERIFIED [] — no findings
.dockerignore Forbidden patterns present; apps/api/src NOT excluded VERIFIED All 7 required patterns found; apps/api/src does not appear as an exclusion
.gitea/workflows/ci.yml security job + updated gate VERIFIED Job present with correct conditional structure and gate wiring
.gitea/workflows/publish.yml Static assertion + boot-smoke between build and push VERIFIED Step order: Build → static assertions → boot-smoke → Push
From To Via Status Details
apps/api/src/index.ts apps/api/src/lib/bootGuards.ts import { assertNotDevBypassInProduction } + call as first statement in isMainModule() WIRED Line 18 import; line 116 call; verified order before VAPID/workers/serve
apps/api/Dockerfile production stage apps/api/src/lib/bootGuards.ts (via baked env) ENV NODE_ENV=production engages NODE_ENV check in guard WIRED Line 45 in production stage only
scripts/check-audit.mjs scripts/audit-allowlist.json readFileSync + filter by github_advisory_id WIRED allowlistPath = resolve(__dirname, 'audit-allowlist.json') at line 103
scripts/check-outdated.mjs scripts/outdated-pins.json readFileSync + pin-reason lookup WIRED pinsPath = resolve(__dirname, 'outdated-pins.json') at line 59
eslint.config.js eslint-plugin-security import pluginSecurity + spread configs.recommended WIRED Lines 11, 117-121
.gitleaks.toml apps/api/tests/fixtures/vapid.ts [[allowlists]] paths regex WIRED Path regex apps/api/tests/fixtures/vapid\.ts in first allowlist block
.gitea/workflows/ci.yml security job scripts/check-audit.mjs node scripts/check-audit.mjs step (code-gated) WIRED Line 430
.gitea/workflows/ci.yml gate security job needs.security.result == 'success' individual check WIRED Gate needs [fast-checks, changes, api, harness, security]; individual check at line 452
.gitea/workflows/publish.yml boot-smoke apps/api/src/lib/bootGuards.ts (via built image) docker run --env NODE_ENV=production --env DEV_AUTH_BYPASS=true; assert non-zero exit + guard message WIRED Step 5 ("Image hygiene — boot-smoke"); greps for DEV_AUTH_BYPASS=true is set in a production environment

Behavioral Spot-Checks

Behavior Command Result Status
boot guard exits 1 when NODE_ENV=production and DEV_AUTH_BYPASS=true vitest run tests/lib/bootGuards.test.ts 3/3 tests pass PASS
check-audit unit tests — blocking/waiving/expiry logic node --test scripts/__tests__/check-audit.test.mjs 9/9 pass PASS
pnpm lint green with eslint-plugin-security active pnpm lint exits 0 PASS
pnpm typecheck clean pnpm typecheck exits 0 (both apps) PASS
ENV NODE_ENV=production in production Dockerfile stage awk + grep on Dockerfile Found at line 45 in production stage PASS
.dockerignore covers all 7 forbidden patterns; does not exclude apps/api/src grep loop All 7 OK; apps/api/src absent PASS
gitleaks baseline is valid JSON node -e JSON.parse(...) [] — 0 findings, valid JSON PASS
security job parses, has correct structure python3 yaml parse security needs:[changes], gitleaks steps always, audit/outdated code-gated PASS
publish.yml step order: build → assertions → smoke → push python3 yaml parse Steps [3]=Build, [4]=static, [5]=smoke, [6]=Push PASS
check-outdated always exits 0 (no process.exit(1) on report path) grep Only process.exit(0) at line 189 PASS

Probe Execution

Step 7c skipped — no probe scripts declared or expected for this phase (CI workflow verification; no probe-*.sh files present).

Requirements Coverage

Phase 16 requirement IDs are defined in PLAN frontmatter and ROADMAP.md; they do not appear in REQUIREMENTS.md (which tracks only v1.1 functional requirements up to CI-01/CI-02). This is expected — REQUIREMENTS.md ends its traceability table at CI-02 and notes that CI, TEST, ADMIN, SETUP categories are tracked there. The Phase 16 operational/security requirement IDs (SEC-, DEP-, IMG-*, CI-03) are roadmap-internal tracking identifiers, not v1.1 product requirements.

REQ-ID Plan What was verified Status
IMG-01 16-01 bootGuards.ts exported guard; index.ts wiring as first call in isMainModule; Dockerfile ENV NODE_ENV=production in production stage only SATISFIED
DEP-01 16-02 check-audit.mjs blocks unwaived High+Critical; audit-allowlist.json with esbuild GHSA waiver pre-seeded; time-boxed expiry enforced SATISFIED
DEP-02 16-02 check-outdated.mjs always exits 0; four tiers including intentional-pin; outdated-pins.json with 4 reasons SATISFIED
SEC-02 16-03 eslint-plugin-security@3.0.1 in root devDeps; folded into flat config before prettierConfig; pnpm lint exits 0 SATISFIED
SEC-01 16-04 .gitleaks.toml with useDefault=true + fixture/env allowlists; gitleaks-baseline.json = []; human checkpoint completed (baseline clean) SATISFIED
IMG-02 16-04 .dockerignore covers all 7 required forbidden patterns; does NOT exclude apps/api/src SATISFIED
CI-03 16-05 security job in ci.yml; gitleaks always-runs; audit/outdated code-gated; gate wires security via individual strict success check SATISFIED
IMG-03 16-06 publish.yml: Build → static assertions (grep .dockerignore + --target production) → boot-smoke (assert non-zero exit + guard message) → Push SATISFIED

Anti-Patterns Found

None. Scan of all 15 phase-16-modified files found no TBD/FIXME/XXX markers, no placeholder returns, no blanket /* eslint-disable */ headers, no hardcoded empty data structures in rendering paths.

Notable good patterns observed:

  • detect-object-injection disabled globally has inline justification comment (not silent off)
  • Audit wrapper expiry check fails closed on unparseable date strings (malformed → not waived)
  • Boot-smoke matches guard output text as belt-and-suspenders (non-zero exit alone is insufficient)

Human Verification Required

Two items are structurally verified (YAML + logic) but require a live CI run to observe the end-to-end behavior. Per phase 16 RESEARCH.md and the verifier instruction notes, this is an expected/accepted constraint — the Gitea runner is not reachable locally, and a code-review + auto-fix loop was already completed.

  1. Gitleaks PR diff scan blocks a real secret

    • Test: Open a PR that introduces a dummy secret string matching a gitleaks default rule (e.g. a fake GITHUB_TOKEN=ghp_... pattern in a test file not covered by allowlists)
    • Expected: The security job fails; the gate job fails; the PR is blocked from merging
    • Why human: Cannot drive the Gitea CI runner locally
  2. Boot-smoke PASS on a freshly-built production image

    • Test: Merge a commit to main; observe the publish workflow run; verify the "Image hygiene — boot-smoke" step logs PASS: Production image refused to start with DEV_AUTH_BYPASS=true
    • Expected: Step passes; Push image runs; image is published
    • Why human: Cannot build and run the Docker image in this environment (no Docker daemon); boot-smoke requires the actual built image artifact

Gaps Summary

No gaps. All 8 observable truths are verified against the codebase, all 16 required artifacts exist and are substantive, all key links are wired. Behavioral spot-checks pass (9/9 unit tests, lint, typecheck, structural YAML parsing). Two human verification items remain for live CI observation, which is the expected end-state per the phase boundary (no Docker daemon, no Gitea runner locally).


Verified: 2026-06-13T12:56:26Z Verifier: Claude (gsd-verifier)