@soulbatical/tetra-dev-toolkit 1.16.3 → 1.17.0
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.
- package/lib/checks/security/config-rls-alignment.js +6 -2
- package/lib/checks/security/direct-supabase-client.js +2 -2
- package/lib/checks/security/mixed-db-usage.js +5 -3
- package/lib/checks/security/route-config-alignment.js +35 -6
- package/lib/checks/security/systemdb-whitelist.js +2 -1
- package/lib/config.js +7 -1
- package/package.json +1 -1
|
@@ -424,8 +424,12 @@ export async function run(config, projectRoot) {
|
|
|
424
424
|
}
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
-
// CHECK 4: RPC functions must be SECURITY INVOKER
|
|
427
|
+
// CHECK 4: RPC functions must be SECURITY INVOKER (unless whitelisted)
|
|
428
|
+
const definerWhitelist = config?.supabase?.securityDefinerWhitelist || []
|
|
428
429
|
for (const rpcName of cfg.rpcFunctions) {
|
|
430
|
+
// Skip if explicitly whitelisted in config
|
|
431
|
+
if (definerWhitelist.includes(rpcName)) continue
|
|
432
|
+
|
|
429
433
|
const rpcInfo = tableRls.rpcFunctions.get(rpcName)
|
|
430
434
|
if (rpcInfo && rpcInfo.securityMode === 'DEFINER') {
|
|
431
435
|
results.passed = false
|
|
@@ -435,7 +439,7 @@ export async function run(config, projectRoot) {
|
|
|
435
439
|
type: 'rpc-security-definer',
|
|
436
440
|
severity: 'critical',
|
|
437
441
|
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
|
|
442
|
+
fix: `Change to SECURITY INVOKER or add "${rpcName}" to supabase.securityDefinerWhitelist in .tetra-quality.json.`
|
|
439
443
|
})
|
|
440
444
|
results.summary.critical++
|
|
441
445
|
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?.
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
127
|
-
const routeIgnore = config?.
|
|
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
|
-
|
|
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
|