Files
familysync/.planning/milestones/v1.1-phases/16-ci-dependency-audit-and-security-checks/16-03-PLAN.md
T
2026-06-18 22:21:38 -04:00

9.8 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
16-ci-dependency-audit-and-security-checks 03 execute 1
package.json
pnpm-lock.yaml
eslint.config.js
true
SEC-02
truths artifacts key_links
eslint-plugin-security runs as part of the existing pnpm lint gate, as blocking errors (not warnings)
pnpm lint passes green across both apps with the security plugin active — existing detect-object-injection / fs-filename noise is triaged (rule-tuned or targeted eslint-disable with justification), not left red
path provides contains
eslint.config.js eslint-plugin-security recommended config folded in before prettierConfig eslint-plugin-security
path provides contains
package.json eslint-plugin-security added to root devDependencies eslint-plugin-security
from to via pattern
eslint.config.js eslint-plugin-security import pluginSecurity + spread configs.recommended pluginSecurity
Fold eslint-plugin-security into the existing root flat ESLint config (D-03) so its rules run as blocking ERRORS inside the current `pnpm lint` step, and triage the resulting violations across the existing codebase so the gate goes green.

Purpose: The phase 13 ESLint gate already runs on every PR in fast-checks. Adding a static security lint here costs nothing extra in CI (same install, same step). The plugin is heuristic and noisy — detect-object-injection fires on every obj[key] (pervasive in Drizzle ORM and TS generics) and detect-non-literal-fs-filename can fire on dynamic path construction. The user explicitly chose error over warn, accepting that triage of existing code is expected work, not a blocker.

Output: eslint-plugin-security in root devDependencies (pinned), the flat-config block, and whatever targeted suppressions / rule-tunes are needed to make pnpm lint green. Consumed by 16-05 (no new ci.yml step — the existing lint step now enforces it; 16-05 only documents the fold).

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/16-ci-dependency-audit-and-security-checks/16-CONTEXT.md @.planning/phases/16-ci-dependency-audit-and-security-checks/16-RESEARCH.md @.planning/phases/16-ci-dependency-audit-and-security-checks/16-PATTERNS.md @eslint.config.js Task 1: Install eslint-plugin-security and fold it into the flat config - package.json (root devDependencies block — lines 18-27; existing pinned eslint tooling versions) - eslint.config.js (the whole file — section 5 prettierConfig MUST remain last; the existing `files: ['apps/**/*.{ts,tsx}']` block pattern at lines 27-43) - .planning/phases/16-ci-dependency-audit-and-security-checks/16-PATTERNS.md (eslint.config.js section — exact import + spread placement before prettierConfig; detect-object-injection guidance) - .planning/phases/16-ci-dependency-audit-and-security-checks/16-RESEARCH.md (eslint-plugin-security Integration section — version 4.0.1 (or 3.0.1), flat-config wiring, ESLint 9.39.4 compatibility, the 15-rule table) Add eslint-plugin-security to root devDependencies pinned to an EXACT version (4.0.1; or 3.0.1 if the executor prefers more bake time — both are flat-config compatible with the pinned ESLint 9.39.4). Use `pnpm add -D -w eslint-plugin-security@` so pnpm-lock.yaml updates. Do NOT upgrade ESLint. In eslint.config.js: add `import pluginSecurity from 'eslint-plugin-security';` to the import block (lines 7-11), and insert a NEW config block with `files: ['apps/**/*.{ts,tsx}']` that spreads `...pluginSecurity.configs.recommended` and its `...pluginSecurity.configs.recommended.rules` — placed AFTER section 4 (disableTypeChecked) and BEFORE `prettierConfig` (which must stay the last element). Add a section-header comment ("eslint-plugin-security: blocking errors per D-03"). Do not yet add per-rule overrides — Task 2 decides those after measuring noise. Commit: `chore(16-03): add eslint-plugin-security to root flat config (D-03)`. node -e "const p=require('./package.json'); if(!p.devDependencies['eslint-plugin-security']) throw new Error('not in devDependencies'); console.log('dep OK')" && grep -q "pluginSecurity" eslint.config.js && grep -nq "prettierConfig" eslint.config.js && echo CONFIG-OK - eslint-plugin-security present in root devDependencies at an exact pinned version; pnpm-lock.yaml updated - eslint.config.js imports pluginSecurity and spreads configs.recommended in an `apps/**/*.{ts,tsx}` block placed before prettierConfig - prettierConfig remains the final element of the exported config array - ESLint version unchanged (still 9.39.4) The security plugin is installed and registered in the flat config, before prettier, without touching the ESLint pin. Task 2: Triage security-rule violations until pnpm lint is green - eslint.config.js (the security block added in Task 1) - .planning/phases/16-ci-dependency-audit-and-security-checks/16-RESEARCH.md (Triage Strategy section — Option A: disable detect-object-injection globally with a justification comment + inline disable at true risk sites; Option B: keep error + annotate each site; other high-noise candidates: detect-non-literal-fs-filename, detect-possible-timing-attacks) - apps/api/src (Drizzle ORM bracket-access and generic patterns that trigger detect-object-injection) - apps/pwa/src (any dynamic bracket access / fs-like patterns) Run `pnpm lint` and capture every security/* violation grouped by rule. For each rule decide: (a) genuine risk → fix the code; (b) whole-codebase false positive (e.g. security/detect-object-injection on Drizzle/TS-generic bracket access where the key is schema-derived or zod-validated, not user-controlled) → disable that single rule in the eslint.config.js security block with an inline comment justifying why (note that real user-controlled key risks are guarded by zod validation); (c) a small number of site-specific false positives → add `// eslint-disable-next-line security/ -- ` at each site. Prefer the minimal change that keeps the maximum number of rules at error: disable only the rules that are pervasively false-positive (likely just detect-object-injection, possibly detect-non-literal-fs-filename), and annotate individual sites for the rest. Re-run `pnpm lint` until it is green with `--max-warnings 0`. Do NOT introduce blanket `/* eslint-disable */` file headers. Commit: `chore(16-03): triage eslint-plugin-security findings to green`. pnpm lint - `pnpm lint` exits 0 across both apps with the security plugin active - Any globally disabled security rule has an inline justification comment in eslint.config.js (no silent `off`) - No blanket file-level `/* eslint-disable */` headers were added; suppressions are rule-specific with `-- justification` - The majority of the 15 security rules remain at error (only pervasively-false-positive rules are disabled) pnpm lint is green with eslint-plugin-security enforcing as errors; suppressions are minimal and justified.

<threat_model>

Trust Boundaries

Boundary Description
developer source → committed code Static security lint inspects source at code-analysis time, before it ships
user-controlled input → object/property access detect-object-injection targets this; real risk only when the key is attacker-controlled and unvalidated

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-16-08 Tampering / Information Disclosure Insecure code patterns (eval, child_process with variables, unsafe regex/ReDoS, pseudo-random crypto) introduced in source mitigate D-03: eslint-plugin-security rules run as blocking errors in the lint gate (Tasks 1-2), failing the PR on flagged patterns
T-16-09 Elevation of Privilege Prototype-pollution / object-injection via user-controlled bracket keys mitigate detect-object-injection findings triaged: real user-controlled sites are zod-validated; the rule is disabled globally ONLY because the remaining hits are schema-derived keys (Task 2 justification), not because the risk is ignored
T-16-10 Repudiation Suppression comments hide a genuine vulnerability without accountability accept Every suppression carries a -- justification; no blanket file disables (Task 2); residual risk is the reviewer trusting the justification, accepted for a two-person repo with PR review
</threat_model>
- `pnpm lint` → exit 0 (both apps, --max-warnings 0) - `grep -n "security/detect-object-injection" eslint.config.js` → if present, an adjacent justification comment exists - eslint-plugin-security pinned in package.json devDependencies; pnpm-lock.yaml reflects it

<success_criteria>

  • eslint-plugin-security folded into the existing flat config as blocking errors, prettierConfig still last
  • pnpm lint green with the plugin active
  • Suppressions are rule-specific and justified; the ESLint pin is untouched </success_criteria>
Create `.planning/phases/16-ci-dependency-audit-and-security-checks/16-03-SUMMARY.md` when done.