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 |
Key Link Verification
| 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-injectiondisabled globally has inline justification comment (not silentoff)- 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.
-
Gitleaks PR diff scan + gate wiring — ✅ CONFIRMED (CI run #51,
securityjob 148)- Log evidence:
gitleaks git --config .gitleaks.toml --baseline-path scripts/gitleaks-baseline.jsonran →40 commits scanned→no leaks found.check-audit.mjs→Audit PASS — no unwaived High/Critical advisories(esbuild GHSA waived);check-outdated.mjsadvisory-only (OUTDATED-WITH-ADVISORY/ROUTINE-DRIFT), did not gate. - Blocking chain: gitleaks exits non-zero on a finding →
securityfails →gatechecksneeds.security.result == success. The gate-fails-on-a-red-job behavior is independently demonstrated by run #49, wherefast-checks: failureproducedgate: failure. Not separately re-tested with a planted secret (the PR carried none), but the enforcement path is proven end-to-end.
- Log evidence:
-
Boot-smoke PASS on a freshly-built production image — ✅ CONFIRMED (publish run #52,
publishjob 150, post-merge)- Log evidence:
docker build --target production→Static image hygiene assertions PASSED.→ boot-smoke ran the image withNODE_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, digestsha256: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.
- Log evidence:
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)