@soulbatical/tetra-dev-toolkit 1.16.3 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -280,6 +280,35 @@ function isWideOpen(using) {
280
280
  return using.trim() === 'true' || using.trim() === '(true)'
281
281
  }
282
282
 
283
+ /**
284
+ * Forbidden patterns in RLS policies — these bypass RLS and defeat the purpose.
285
+ * Valid patterns (from sparkbuddy-live):
286
+ * - organization_id IN (SELECT auth_admin_organizations())
287
+ * - user_id = auth.uid()
288
+ * - USING(true) for public tables only
289
+ * - Subquery to parent table with org/user check
290
+ *
291
+ * Everything else that grants blanket access is a security hole.
292
+ */
293
+ const FORBIDDEN_RLS_PATTERNS = [
294
+ { pattern: /service_role/i, label: 'service_role bypass' },
295
+ { pattern: /current_setting\s*\(\s*'role'/i, label: 'PostgreSQL role check bypass' },
296
+ { pattern: /current_setting\s*\(\s*'request\.jwt\.claims'/i, label: 'JWT claims role bypass' },
297
+ { pattern: /session_user\s*=\s*'postgres'/i, label: 'session_user postgres bypass' },
298
+ { pattern: /current_user\s*=\s*'postgres'/i, label: 'current_user postgres bypass' },
299
+ { pattern: /auth\.role\s*\(\)\s*=\s*'service_role'/i, label: 'auth.role() service_role bypass' },
300
+ { pattern: /pg_has_role/i, label: 'pg_has_role bypass' },
301
+ ]
302
+
303
+ /**
304
+ * Check if a USING/WITH CHECK clause contains forbidden bypass patterns
305
+ * Returns array of { label } for each forbidden pattern found
306
+ */
307
+ function findForbiddenPatterns(clause) {
308
+ if (!clause) return []
309
+ return FORBIDDEN_RLS_PATTERNS.filter(({ pattern }) => pattern.test(clause)).map(({ label }) => label)
310
+ }
311
+
283
312
  export async function run(config, projectRoot) {
284
313
  const results = {
285
314
  passed: true,
@@ -424,8 +453,12 @@ export async function run(config, projectRoot) {
424
453
  }
425
454
  }
426
455
 
427
- // CHECK 4: RPC functions must be SECURITY INVOKER
456
+ // CHECK 4: RPC functions must be SECURITY INVOKER (unless whitelisted)
457
+ const definerWhitelist = config?.supabase?.securityDefinerWhitelist || []
428
458
  for (const rpcName of cfg.rpcFunctions) {
459
+ // Skip if explicitly whitelisted in config
460
+ if (definerWhitelist.includes(rpcName)) continue
461
+
429
462
  const rpcInfo = tableRls.rpcFunctions.get(rpcName)
430
463
  if (rpcInfo && rpcInfo.securityMode === 'DEFINER') {
431
464
  results.passed = false
@@ -435,7 +468,28 @@ export async function run(config, projectRoot) {
435
468
  type: 'rpc-security-definer',
436
469
  severity: 'critical',
437
470
  message: `RPC function "${rpcName}" for table "${tableName}" uses SECURITY DEFINER — this BYPASSES RLS completely. Any authenticated user can see ALL data.`,
438
- fix: `Change to SECURITY INVOKER or remove the SECURITY DEFINER clause.`
471
+ fix: `Change to SECURITY INVOKER or add "${rpcName}" to supabase.securityDefinerWhitelist in .tetra-quality.json.`
472
+ })
473
+ results.summary.critical++
474
+ results.summary.total++
475
+ }
476
+ }
477
+
478
+ // CHECK 4b: No forbidden bypass patterns in any policy clause
479
+ for (const p of policies) {
480
+ const usingViolations = findForbiddenPatterns(p.using)
481
+ const checkViolations = findForbiddenPatterns(p.withCheck)
482
+ const allViolations = [...new Set([...usingViolations, ...checkViolations])]
483
+
484
+ if (allViolations.length > 0) {
485
+ results.passed = false
486
+ results.findings.push({
487
+ file: p.file,
488
+ line: 1,
489
+ type: 'rls-bypass-pattern',
490
+ severity: 'critical',
491
+ message: `Policy "${p.name}" on table "${tableName}" contains forbidden bypass pattern(s): ${allViolations.join(', ')}. RLS policies must ONLY use auth.uid(), auth_admin_organizations(), or parent-table subqueries. Service role bypasses RLS automatically — adding it to policies defeats the purpose.`,
492
+ fix: `Remove the bypass clause. Valid patterns: USING (organization_id IN (SELECT auth_admin_organizations())) or USING (user_id = auth.uid()).`
439
493
  })
440
494
  results.summary.critical++
441
495
  results.summary.total++
@@ -74,8 +74,8 @@ export async function run(config, projectRoot) {
74
74
  summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 }
75
75
  }
76
76
 
77
- // Build whitelist from hardcoded + config
78
- const configWhitelist = config?.supabase?.directSupabaseClientWhitelist || config?.directSupabaseClientWhitelist || []
77
+ // Build whitelist from hardcoded + config (canonical: security.directSupabaseClientWhitelist)
78
+ const configWhitelist = config?.security?.directSupabaseClientWhitelist || config?.directSupabaseClientWhitelist || []
79
79
  const extraPatterns = configWhitelist.map(p => new RegExp(p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
80
80
  const allAllowed = [...ALLOWED_FILES, ...extraPatterns]
81
81
 
@@ -77,7 +77,8 @@ export async function run(config, projectRoot) {
77
77
  }
78
78
 
79
79
  // Config-based ignore for controllers that legitimately mix DB helpers
80
- const mixedDbIgnore = config?.supabase?.mixedDbUsageIgnore || []
80
+ // Canonical: security.mixedDbWhitelist
81
+ const mixedDbIgnore = config?.security?.mixedDbWhitelist || config?.mixedDbWhitelist || []
81
82
 
82
83
  const files = await glob('**/*[Cc]ontroller*.ts', {
83
84
  cwd: projectRoot,
@@ -157,11 +158,12 @@ export async function run(config, projectRoot) {
157
158
 
158
159
  // HIGH: Multiple DB helper types in one controller
159
160
  if (usedLevels.length > 1) {
160
- // Allow: Superadmin controllers may legitimately use SUPERADMIN + ADMIN
161
+ // Allow: Superadmin controllers use superadminDB which internally calls systemDB
162
+ // So tetra sees SUPERADMIN + SYSTEM (or SUPERADMIN + ADMIN) — both are legitimate
161
163
  const isSuperadminMixingAdmin = expectedLevel === 'SUPERADMIN' &&
162
164
  usedLevels.length === 2 &&
163
165
  usedLevels.includes('SUPERADMIN') &&
164
- usedLevels.includes('ADMIN')
166
+ (usedLevels.includes('ADMIN') || usedLevels.includes('SYSTEM'))
165
167
 
166
168
  if (!isSuperadminMixingAdmin) {
167
169
  results.passed = false
@@ -102,6 +102,32 @@ function hasOrgAdminMiddleware(content) {
102
102
  return /requireOrganizationAdmin/.test(content)
103
103
  }
104
104
 
105
+ /**
106
+ * Check if admin routes are protected by a RouteManager group-level middleware.
107
+ * Many projects apply auth middleware at the route group level (e.g., all /api/admin/* routes)
108
+ * rather than in individual route files.
109
+ */
110
+ function hasRouteManagerGroupAuth(projectRoot) {
111
+ const candidates = [
112
+ join(projectRoot, 'backend/src/core/RouteManager.ts'),
113
+ join(projectRoot, 'src/core/RouteManager.ts'),
114
+ join(projectRoot, 'backend/src/routes/index.ts'),
115
+ join(projectRoot, 'src/routes/index.ts')
116
+ ]
117
+
118
+ for (const file of candidates) {
119
+ if (!existsSync(file)) continue
120
+ try {
121
+ const content = readFileSync(file, 'utf-8')
122
+ // Check for group middleware pattern: prefix '/api/admin' with authenticateToken
123
+ if (/\/api\/admin/.test(content) && /authenticateToken/.test(content)) {
124
+ return true
125
+ }
126
+ } catch { /* skip */ }
127
+ }
128
+ return false
129
+ }
130
+
105
131
  /**
106
132
  * Expected route filename for a given accessLevel
107
133
  */
@@ -123,8 +149,11 @@ export async function run(config, projectRoot) {
123
149
  details: { routesChecked: 0, violations: 0 }
124
150
  }
125
151
 
126
- // Config-based ignore for specific features/routes
127
- const routeIgnore = config?.supabase?.routeConfigAlignmentIgnore || []
152
+ // Canonical: security.routeConfigIgnore
153
+ const routeIgnore = config?.security?.routeConfigIgnore || config?.routeConfigAlignmentIgnore || []
154
+
155
+ // Detect if RouteManager applies group-level auth middleware to /api/admin/*
156
+ const routeManagerHasGroupAuth = hasRouteManagerGroupAuth(projectRoot)
128
157
 
129
158
  const featureConfigs = parseFeatureConfigs(projectRoot)
130
159
 
@@ -176,8 +205,8 @@ export async function run(config, projectRoot) {
176
205
  if (cfg.accessLevel === 'admin') {
177
206
  // Route name should be adminRoutes.ts
178
207
  if (routeName === 'adminRoutes.ts') {
179
- // CRITICAL: admin route MUST have authenticateToken
180
- if (!hasAuthMiddleware(content)) {
208
+ // CRITICAL: admin route MUST have authenticateToken (in file OR via RouteManager group)
209
+ if (!hasAuthMiddleware(content) && !routeManagerHasGroupAuth) {
181
210
  results.findings.push({
182
211
  file: relRouteFile,
183
212
  line: 1,
@@ -192,8 +221,8 @@ export async function run(config, projectRoot) {
192
221
  results.details.violations++
193
222
  }
194
223
 
195
- // CRITICAL: admin route MUST have requireOrganizationAdmin
196
- if (!hasOrgAdminMiddleware(content)) {
224
+ // CRITICAL: admin route MUST have requireOrganizationAdmin (in file OR via RouteManager group)
225
+ if (!hasOrgAdminMiddleware(content) && !routeManagerHasGroupAuth) {
197
226
  results.findings.push({
198
227
  file: relRouteFile,
199
228
  line: 1,
@@ -86,7 +86,8 @@ const FORBIDDEN_FILE_PATTERNS = [
86
86
  const DEFAULT_MAX_WHITELIST_SIZE = 35
87
87
 
88
88
  export async function run(config, projectRoot) {
89
- const MAX_WHITELIST_SIZE = config?.supabase?.maxWhitelistEntries || DEFAULT_MAX_WHITELIST_SIZE
89
+ // Canonical: security.systemDbMaxEntries
90
+ const MAX_WHITELIST_SIZE = config?.security?.systemDbMaxEntries || config?.systemDB?.maxWhitelistEntries || DEFAULT_MAX_WHITELIST_SIZE
90
91
  const results = {
91
92
  passed: true,
92
93
  findings: [],
package/lib/config.js CHANGED
@@ -42,7 +42,13 @@ export const DEFAULT_CONFIG = {
42
42
  // Code patterns
43
43
  checkSqlInjection: true,
44
44
  checkEvalUsage: true,
45
- checkCommandInjection: true
45
+ checkCommandInjection: true,
46
+
47
+ // Whitelists — project-specific overrides in .tetra-quality.json
48
+ directSupabaseClientWhitelist: [], // Files allowed to import createClient directly
49
+ mixedDbWhitelist: [], // Controllers allowed to mix DB helper types
50
+ routeConfigIgnore: [], // Tables to skip in route↔config alignment check
51
+ systemDbMaxEntries: 35 // Max systemDB whitelist entries before warning
46
52
  },
47
53
 
48
54
  // Stability checks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulbatical/tetra-dev-toolkit",
3
- "version": "1.16.3",
3
+ "version": "1.17.1",
4
4
  "publishConfig": {
5
5
  "access": "restricted"
6
6
  },