diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index c9901f7..6921f46 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -330,7 +330,7 @@ Plans: - [x] 16-01-PLAN.md — Image-hygiene runtime: bake NODE_ENV=production + boot-time refuse-to-boot guard (IMG-01) - [x] 16-02-PLAN.md — pnpm audit gate + waiver allowlist + advisory-only tiered outdated report (DEP-01, DEP-02) -- [ ] 16-03-PLAN.md — Fold eslint-plugin-security into the lint gate as blocking errors + triage (SEC-02) +- [x] 16-03-PLAN.md — Fold eslint-plugin-security into the lint gate as blocking errors + triage (SEC-02) - [ ] 16-04-PLAN.md — gitleaks config + full-history baseline + .dockerignore (SEC-01, IMG-02) **Wave 2** *(blocked on Wave 1 completion)* @@ -359,7 +359,7 @@ Plans: | 13. Real Lint Gate (ESLint) | v1.1 | 3/3 | Complete | 2026-06-12 | | 14. Desktop E2E Coverage | v1.1 | 1/1 | Complete | 2026-06-12 | | 15. Doc-Only CI Skip + MD Lint | v1.1 | 3/3 | Complete | 2026-06-12 | -| 16. CI Dep Audit, Sec & Img Hyg | v1.1 | 2/6 | In Progress| | +| 16. CI Dep Audit, Sec & Img Hyg | v1.1 | 3/6 | In Progress| | ## Backlog @@ -367,7 +367,7 @@ Plans: **Goal:** [Captured for future planning] Abstract the calendar backend behind a provider interface so Fastmail/CalDAV is one implementation among potentially many. Shipping with a single provider is fine, but the broker, sync, and event-expansion layers should be structured so additional providers (e.g. other CalDAV hosts, Google Calendar, generic ICS feeds) can be added without rework. Captures the "provider" seam as an explicit architectural concern. **Requirements:** TBD -**Plans:** 2/6 plans executed +**Plans:** 3/6 plans executed Plans: diff --git a/.planning/STATE.md b/.planning/STATE.md index e84bd7a..94065a6 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,14 +3,14 @@ gsd_state_version: 1.0 milestone: v1.1 milestone_name: Operability & Polish status: executing -stopped_at: Completed 16-01-PLAN.md -last_updated: "2026-06-13T09:20:23.640Z" +stopped_at: Completed 16-03-PLAN.md +last_updated: "2026-06-13T09:26:02.327Z" last_activity: 2026-06-13 -- Phase 16 execution started progress: total_phases: 19 completed_phases: 6 total_plans: 23 - completed_plans: 19 + completed_plans: 20 percent: 32 --- @@ -26,7 +26,7 @@ See: .planning/PROJECT.md (updated 2026-06-10) ## Current Position Phase: 16 (ci-dependency-audit-and-security-checks) — EXECUTING -Plan: 3 of 6 +Plan: 4 of 6 Status: Ready to execute Last activity: 2026-06-13 -- Phase 16 execution started @@ -105,6 +105,7 @@ _Updated after each plan completion_ | Phase 09-faster-write-back P01 | 341 | 3 tasks | 3 files | | Phase 16 P01 | 188 | 3 tasks | 4 files | | Phase 16-ci-dependency-audit-and-security-checks P02 | 25 | 3 tasks | 5 files | +| Phase 16-ci-dependency-audit-and-security-checks P03 | 2 | 2 tasks | 5 files | ## Accumulated Context @@ -168,6 +169,8 @@ Recent decisions affecting current work: - [Phase ?]: D-13-08: Prettier reformat committed as isolated mechanical diff; CI Format check step added to fast-checks job - [Phase ?]: D-07 (16-01): ENV NODE_ENV=production baked into production Dockerfile stage — engages devBypass.ts hard guard so DEV_AUTH_BYPASS never injects in production - [Phase ?]: D-08 (16-01): assertNotDevBypassInProduction() boot guard — first statement in isMainModule(), exits non-zero when NODE_ENV=production AND DEV_AUTH_BYPASS=true; unit-tested +- [Phase ?]: D-03-SEC-VERSION: Pinned eslint-plugin-security@3.0.1 over 4.0.1 — stable, flat-config compatible with ESLint 9.39.4, more bake time +- [Phase ?]: D-03-OBJ-INJECT: detect-object-injection disabled globally in eslint-plugin-security block — all hits were numeric loop indices / schema-derived keys; zod guards real API input; 14 of 15 rules remain at error ### Roadmap Evolution @@ -232,8 +235,8 @@ Recent decisions affecting current work: ## Session Continuity -Last session: 2026-06-13T09:20:23.627Z -Stopped at: Completed 16-01-PLAN.md +Last session: 2026-06-13T09:26:02.314Z +Stopped at: Completed 16-03-PLAN.md Resume file: None ## Operator Next Steps diff --git a/.planning/phases/16-ci-dependency-audit-and-security-checks/16-03-SUMMARY.md b/.planning/phases/16-ci-dependency-audit-and-security-checks/16-03-SUMMARY.md new file mode 100644 index 0000000..e3e80c3 --- /dev/null +++ b/.planning/phases/16-ci-dependency-audit-and-security-checks/16-03-SUMMARY.md @@ -0,0 +1,116 @@ +--- +phase: 16-ci-dependency-audit-and-security-checks +plan: "03" +subsystem: infra +tags: [eslint, security, eslint-plugin-security, static-analysis, ci] + +# Dependency graph +requires: + - phase: 13-real-lint-gate-eslint + provides: root flat ESLint config (eslint.config.js) that this plan extends +provides: + - eslint-plugin-security folded into the existing pnpm lint gate as blocking errors (D-03) + - 14 of 15 security rules active; detect-object-injection disabled globally with justification + - Targeted inline suppressions at 2 detect-non-literal-fs-filename false-positive sites +affects: + - 16-05 (documents the lint gate fold; no new ci.yml step needed — lint already enforces it) + +# Tech tracking +tech-stack: + added: + - eslint-plugin-security@3.0.1 (root devDependencies, pinned exact) + patterns: + - Security rules folded into existing lint step: no extra CI install cost, same pnpm lint gate + - High-FP rules disabled globally with inline justification comment; site-specific FPs get eslint-disable-next-line with rationale + +key-files: + created: [] + modified: + - eslint.config.js + - package.json + - pnpm-lock.yaml + - apps/api/src/index.ts + - apps/api/tests/broker/expand.test.ts + +key-decisions: + - "D-03-SEC-VERSION: Pinned eslint-plugin-security@3.0.1 (not 4.0.1) — 3.0.1 has more bake time; both are flat-config compatible" + - "D-03-OBJ-INJECT: detect-object-injection disabled globally — all hits were numeric loop indices (arr[i]) and schema-derived keys, not user-controlled input; remaining 14 rules enforced at error" + - "D-03-FS-FILENAME: detect-non-literal-fs-filename suppressed at 2 sites (realpathSync(process.argv[1]) and test fixture readFileSync) — both are runtime/test-controlled paths, not user input" + +patterns-established: + - "Security lint fold: add security plugin block before prettierConfig (must stay last); disable only pervasively-FP rules globally with justification" + - "Inline suppression format: // eslint-disable-next-line security/ -- " + +requirements-completed: [SEC-02] + +# Metrics +duration: 2min +completed: 2026-06-13 +--- + +# Phase 16 Plan 03: eslint-plugin-security Static Lint Gate Summary + +**eslint-plugin-security@3.0.1 folded into the existing pnpm lint gate as 14 blocking error-level rules; detect-object-injection disabled globally for Drizzle/TS-generic FPs; pnpm lint green** + +## Performance + +- **Duration:** 2 min +- **Started:** 2026-06-13T09:21:47Z +- **Completed:** 2026-06-13T09:24:29Z +- **Tasks:** 2 +- **Files modified:** 5 + +## Accomplishments + +- eslint-plugin-security@3.0.1 installed to root devDependencies (exact pin) +- Flat config extended: new security block (`files: apps/**/*.{ts,tsx}`) with `...pluginSecurity.configs.recommended` spread, placed before `prettierConfig` (which stays last) +- Triaged 4 total violations: 2 detect-non-literal-fs-filename (inline suppressions with justification), 2 detect-object-injection (globally disabled with justification comment) +- `pnpm lint` exits 0 with `--max-warnings 0` across both apps; ESLint pin unchanged at 9.39.4 + +## Task Commits + +1. **Task 1: Install eslint-plugin-security and fold it into the flat config** - `826a23a` (chore) +2. **Task 2: Triage security-rule violations until pnpm lint is green** - `59e49ec` (chore) + +## Files Created/Modified + +- `eslint.config.js` — added `pluginSecurity` import + security config block (section 5, before prettierConfig); detect-object-injection globally disabled with justification +- `package.json` — eslint-plugin-security@3.0.1 added to root devDependencies +- `pnpm-lock.yaml` — lockfile updated to reflect new package +- `apps/api/src/index.ts` — inline `eslint-disable-next-line` for `detect-non-literal-fs-filename` on `realpathSync(process.argv[1])` +- `apps/api/tests/broker/expand.test.ts` — inline `eslint-disable-next-line` for `detect-non-literal-fs-filename` on test-fixture `readFileSync` + +## Decisions Made + +- **Version choice:** Pinned eslint-plugin-security@3.0.1 (not 4.0.1) — 4.0.1 was published the same day as phase research (freshness concern); 3.0.1 is stable and flat-config compatible with ESLint 9.39.4. +- **detect-object-injection disabled globally:** After running lint and auditing all 2 hits: both were `ranks[i] > ranks[i - 1]` numeric loop index comparisons in tests — not user-controlled keys. Disabling the single highest-noise rule globally while keeping the remaining 14 rules at error. Matches RESEARCH triage Option A recommendation. +- **detect-non-literal-fs-filename: inline suppressions at 2 sites:** Not disabled globally because only 2 hits exist and both are clearly false positives. Site-level suppression is the minimal-change approach that keeps the rule active for any future truly dynamic `fs.*` calls. + +## Deviations from Plan + +None — plan executed exactly as written. Triage decision to disable detect-object-injection globally vs. annotating sites (Option A vs. B per RESEARCH) was explicitly delegated to the executor; Option A was chosen after confirming all hits were numeric loop indices. + +## Issues Encountered + +None. Only 4 lint violations found (2 rules, 2 sites each), far fewer than the "dozens" anticipated for Drizzle ORM bracket access — the codebase does not have heavy obj[key] usage in API source files. + +## Threat Surface Scan + +No new network endpoints, auth paths, file access patterns, or schema changes introduced. This plan adds only dev-tooling configuration. + +## Known Stubs + +None. + +## User Setup Required + +None — no external service configuration required. The security lint fold is automatic via `pnpm lint` (existing CI step). + +## Next Phase Readiness + +- Plan 16-04 (gitleaks secret scanning) is ready to proceed +- Plan 16-05 (CI documentation) will reference this plan's D-03 fold — the lint step already enforces it; no new ci.yml job step needed for the security lint + +--- +*Phase: 16-ci-dependency-audit-and-security-checks* +*Completed: 2026-06-13*