@soulbatical/tetra-dev-toolkit 1.11.0 → 1.12.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/mixed-db-usage.js +180 -0
- package/lib/runner.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mixed Database Usage Detection — HARD BLOCK
|
|
3
|
+
*
|
|
4
|
+
* Controllers MUST use only ONE type of database helper.
|
|
5
|
+
* The DB helper must match the controller naming convention:
|
|
6
|
+
*
|
|
7
|
+
* AdminXxxController → adminDB(req) only
|
|
8
|
+
* UserXxxController → userDB(req) only
|
|
9
|
+
* PublicXxxController → publicDB() only
|
|
10
|
+
* SystemXxxController → systemDB(context) only
|
|
11
|
+
* SuperadminXxxController → superadminDB(req) only
|
|
12
|
+
* WebhookXxxController → systemDB(context) only (webhooks have no user session)
|
|
13
|
+
*
|
|
14
|
+
* Violations:
|
|
15
|
+
* - CRITICAL: Controller uses systemDB when user context is available (should be adminDB/userDB)
|
|
16
|
+
* - HIGH: Controller mixes multiple DB helper types
|
|
17
|
+
* - MEDIUM: Controller DB helper doesn't match naming convention
|
|
18
|
+
*
|
|
19
|
+
* Reference: stella_howto_get slug="tetra-architecture-guide"
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readFileSync, existsSync } from 'fs'
|
|
23
|
+
import { join, basename, dirname } from 'path'
|
|
24
|
+
import { glob } from 'glob'
|
|
25
|
+
|
|
26
|
+
const DB_PATTERNS = {
|
|
27
|
+
systemDB: { level: 'SYSTEM', pattern: /systemDB\s*\(/g, desc: 'System-level (cron, webhooks)' },
|
|
28
|
+
adminDB: { level: 'ADMIN', pattern: /adminDB\s*\(/g, desc: 'Admin operations (org-scoped)' },
|
|
29
|
+
userDB: { level: 'USER', pattern: /userDB\s*\(/g, desc: 'User-specific operations' },
|
|
30
|
+
publicDB: { level: 'PUBLIC', pattern: /publicDB\s*\(/g, desc: 'Public/unauthenticated' },
|
|
31
|
+
superadminDB: { level: 'SUPERADMIN', pattern: /superadminDB\s*\(/g, desc: 'Cross-org superadmin' }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Determine expected DB level from controller filename
|
|
36
|
+
*/
|
|
37
|
+
function getExpectedLevel(fileName) {
|
|
38
|
+
const lower = fileName.toLowerCase()
|
|
39
|
+
// Order matters — check compound names first
|
|
40
|
+
if (lower.includes('system')) return 'SYSTEM'
|
|
41
|
+
if (lower.includes('superadmin')) return 'SUPERADMIN'
|
|
42
|
+
if (lower.includes('webhook')) return 'SYSTEM' // webhooks have no user session
|
|
43
|
+
if (lower.includes('cron')) return 'SYSTEM'
|
|
44
|
+
if (lower.includes('admin')) return 'ADMIN'
|
|
45
|
+
if (lower.includes('user')) return 'USER'
|
|
46
|
+
if (lower.includes('public')) return 'PUBLIC'
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if file has user authentication context available
|
|
52
|
+
*/
|
|
53
|
+
function hasUserContext(content) {
|
|
54
|
+
return content.includes('AuthenticatedRequest') ||
|
|
55
|
+
content.includes('req.userToken') ||
|
|
56
|
+
content.includes('req.user') ||
|
|
57
|
+
content.includes('authenticateToken')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function check(projectRoot, config = {}) {
|
|
61
|
+
const results = {
|
|
62
|
+
name: 'Mixed DB Usage',
|
|
63
|
+
slug: 'mixed-db-usage',
|
|
64
|
+
passed: true,
|
|
65
|
+
skipped: false,
|
|
66
|
+
findings: [],
|
|
67
|
+
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
|
|
68
|
+
details: { controllersChecked: 0, violations: 0 }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const files = await glob('**/*[Cc]ontroller*.ts', {
|
|
72
|
+
cwd: projectRoot,
|
|
73
|
+
ignore: [
|
|
74
|
+
'**/node_modules/**',
|
|
75
|
+
'**/.next/**',
|
|
76
|
+
'**/dist/**',
|
|
77
|
+
'**/build/**',
|
|
78
|
+
...(config.ignore || []),
|
|
79
|
+
'**/*.test.ts',
|
|
80
|
+
'**/*.spec.ts',
|
|
81
|
+
'**/*.d.ts'
|
|
82
|
+
]
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if (files.length === 0) {
|
|
86
|
+
results.skipped = true
|
|
87
|
+
results.skipReason = 'No controller files found'
|
|
88
|
+
return results
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
const fullPath = join(projectRoot, file)
|
|
93
|
+
let content
|
|
94
|
+
try { content = readFileSync(fullPath, 'utf-8') } catch { continue }
|
|
95
|
+
|
|
96
|
+
results.details.controllersChecked++
|
|
97
|
+
const fileName = basename(file)
|
|
98
|
+
|
|
99
|
+
// Detect all DB usages in this file
|
|
100
|
+
const dbUsages = new Map() // level -> [{type, line, code}]
|
|
101
|
+
const lines = content.split('\n')
|
|
102
|
+
|
|
103
|
+
for (const [dbType, cfg] of Object.entries(DB_PATTERNS)) {
|
|
104
|
+
lines.forEach((line, idx) => {
|
|
105
|
+
// Reset regex lastIndex
|
|
106
|
+
cfg.pattern.lastIndex = 0
|
|
107
|
+
if (cfg.pattern.test(line)) {
|
|
108
|
+
const level = cfg.level
|
|
109
|
+
if (!dbUsages.has(level)) dbUsages.set(level, [])
|
|
110
|
+
dbUsages.get(level).push({
|
|
111
|
+
type: dbType,
|
|
112
|
+
line: idx + 1,
|
|
113
|
+
code: line.trim().substring(0, 120)
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (dbUsages.size === 0) continue
|
|
120
|
+
|
|
121
|
+
const expectedLevel = getExpectedLevel(fileName)
|
|
122
|
+
const usedLevels = [...dbUsages.keys()]
|
|
123
|
+
const userContextAvailable = hasUserContext(content)
|
|
124
|
+
|
|
125
|
+
// CRITICAL: systemDB used when user context is available and it's not a system controller
|
|
126
|
+
if (dbUsages.has('SYSTEM') && userContextAvailable && expectedLevel !== 'SYSTEM') {
|
|
127
|
+
results.passed = false
|
|
128
|
+
const locs = dbUsages.get('SYSTEM')
|
|
129
|
+
for (const loc of locs) {
|
|
130
|
+
results.findings.push({
|
|
131
|
+
file,
|
|
132
|
+
line: loc.line,
|
|
133
|
+
type: 'systemDB with user context',
|
|
134
|
+
severity: 'critical',
|
|
135
|
+
message: `${fileName} uses systemDB() but has user authentication context → MUST use ${expectedLevel === 'ADMIN' ? 'adminDB(req)' : expectedLevel === 'USER' ? 'userDB(req)' : 'adminDB(req) or userDB(req)'}`,
|
|
136
|
+
snippet: loc.code,
|
|
137
|
+
fix: `Replace systemDB('context') with ${expectedLevel === 'ADMIN' ? 'adminDB(req)' : 'userDB(req)'}. systemDB bypasses RLS — user operations MUST go through RLS. See: stella_howto_get slug="tetra-architecture-guide"`
|
|
138
|
+
})
|
|
139
|
+
results.summary.critical++
|
|
140
|
+
results.summary.total++
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// HIGH: Multiple DB helper types in one controller
|
|
145
|
+
if (usedLevels.length > 1) {
|
|
146
|
+
results.passed = false
|
|
147
|
+
results.findings.push({
|
|
148
|
+
file,
|
|
149
|
+
line: 1,
|
|
150
|
+
type: 'mixed db usage',
|
|
151
|
+
severity: 'high',
|
|
152
|
+
message: `${fileName} mixes ${usedLevels.join(' + ')} database helpers. Controllers MUST use exactly ONE DB helper type.`,
|
|
153
|
+
fix: `Split into separate controllers per DB level, or refactor services to accept injected supabase client. Admin + System mix → move system ops to a separate SystemXxxController.`
|
|
154
|
+
})
|
|
155
|
+
results.summary.high++
|
|
156
|
+
results.summary.total++
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// MEDIUM: DB helper doesn't match naming convention
|
|
160
|
+
if (expectedLevel && usedLevels.length === 1 && !usedLevels.includes(expectedLevel)) {
|
|
161
|
+
const actualLevel = usedLevels[0]
|
|
162
|
+
// Don't double-report if already caught as critical
|
|
163
|
+
if (!(actualLevel === 'SYSTEM' && userContextAvailable)) {
|
|
164
|
+
results.findings.push({
|
|
165
|
+
file,
|
|
166
|
+
line: 1,
|
|
167
|
+
type: 'naming mismatch',
|
|
168
|
+
severity: 'medium',
|
|
169
|
+
message: `${fileName} implies ${expectedLevel} but uses ${actualLevel}. Either rename the controller or change the DB helper.`,
|
|
170
|
+
fix: `Rename to match DB level, or change DB helper to match controller naming convention.`
|
|
171
|
+
})
|
|
172
|
+
results.summary.medium++
|
|
173
|
+
results.summary.total++
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
results.details.violations = results.findings.length
|
|
179
|
+
return results
|
|
180
|
+
}
|
package/lib/runner.js
CHANGED
|
@@ -13,6 +13,7 @@ import * as deprecatedSupabaseAdmin from './checks/security/deprecated-supabase-
|
|
|
13
13
|
import * as directSupabaseClient from './checks/security/direct-supabase-client.js'
|
|
14
14
|
import * as frontendSupabaseQueries from './checks/security/frontend-supabase-queries.js'
|
|
15
15
|
import * as tetraCoreCompliance from './checks/security/tetra-core-compliance.js'
|
|
16
|
+
import * as mixedDbUsage from './checks/security/mixed-db-usage.js'
|
|
16
17
|
import * as systemdbWhitelist from './checks/security/systemdb-whitelist.js'
|
|
17
18
|
import * as huskyHooks from './checks/stability/husky-hooks.js'
|
|
18
19
|
import * as ciPipeline from './checks/stability/ci-pipeline.js'
|
|
@@ -37,6 +38,7 @@ const ALL_CHECKS = {
|
|
|
37
38
|
directSupabaseClient,
|
|
38
39
|
frontendSupabaseQueries,
|
|
39
40
|
tetraCoreCompliance,
|
|
41
|
+
mixedDbUsage,
|
|
40
42
|
systemdbWhitelist,
|
|
41
43
|
gitignoreValidation
|
|
42
44
|
],
|