--- phase: 16-ci-dependency-audit-and-security-checks plan: 03 type: execute wave: 1 depends_on: [] files_modified: - package.json - pnpm-lock.yaml - eslint.config.js autonomous: true requirements: [SEC-02] must_haves: truths: - "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" artifacts: - path: "eslint.config.js" provides: "eslint-plugin-security recommended config folded in before prettierConfig" contains: "eslint-plugin-security" - path: "package.json" provides: "eslint-plugin-security added to root devDependencies" contains: "eslint-plugin-security" key_links: - from: "eslint.config.js" to: "eslint-plugin-security" via: "import pluginSecurity + spread configs.recommended" pattern: "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). @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.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. ## 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 | - `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 - 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 Create `.planning/phases/16-ci-dependency-audit-and-security-checks/16-03-SUMMARY.md` when done.