Files
familysync/.planning/phases/16-ci-dependency-audit-and-security-checks/16-VERIFICATION.md
T

14 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

Both items were CONFIRMED on 2026-06-13 against the live Gitea Actions logs for PR #15 (merge commit 06238a9). The Gitea runner could not be reached during planning; it was verified after merge.

  1. Gitleaks PR diff scan + gate wiring CONFIRMED (CI run #51, security job 148)

    • Log evidence: gitleaks git --config .gitleaks.toml --baseline-path scripts/gitleaks-baseline.json ran → 40 commits scannedno leaks found. check-audit.mjsAudit PASS — no unwaived High/Critical advisories (esbuild GHSA waived); check-outdated.mjs advisory-only (OUTDATED-WITH-ADVISORY / ROUTINE-DRIFT), did not gate.
    • Blocking chain: gitleaks exits non-zero on a finding → security fails → gate checks needs.security.result == success. The gate-fails-on-a-red-job behavior is independently demonstrated by run #49, where fast-checks: failure produced gate: failure. Not separately re-tested with a planted secret (the PR carried none), but the enforcement path is proven end-to-end.
  2. Boot-smoke PASS on a freshly-built production image CONFIRMED (publish run #52, publish job 150, post-merge)

    • Log evidence: docker build --target productionStatic image hygiene assertions PASSED. → boot-smoke ran the image with NODE_ENV=production DEV_AUTH_BYPASS=true → image logged [FATAL] DEV_AUTH_BYPASS=true is set in a production environment. ... Refusing to start.PASS: Production image refused to start with DEV_AUTH_BYPASS=true (exit 1)docker push (v1.1-06238a9 + latest, digest sha256:aa6f845…). The D-08 guard fired in the actual shipped image, matched via the FATAL marker (WR-02 fix, not a false-pass), and the image published only after the gates passed.

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). The two live-CI items were CONFIRMED post-merge against the Gitea Actions logs (PR #15 run #51 security + publish run #52 boot-smoke) — see "Human Verification Required" above. Phase fully verified.


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