@soulbatical/tetra-dev-toolkit 1.16.2 → 1.16.3
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/README.md +27 -2
- package/lib/checks/security/config-rls-alignment.js +43 -2
- package/lib/runner.js +64 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ LAYER 8: DB HELPERS adminDB/userDB/publicDB/systemDB enforce correct
|
|
|
58
58
|
| `tetra-audit` | Run quality/security/hygiene checks |
|
|
59
59
|
| `tetra-audit quick` | Quick critical checks (pre-commit) |
|
|
60
60
|
| `tetra-audit security` | Full security suite (12 checks) |
|
|
61
|
-
| `tetra-audit stability` | Stability suite (
|
|
61
|
+
| `tetra-audit stability` | Stability suite (16 checks) |
|
|
62
62
|
| `tetra-audit codeQuality` | Code quality suite (4 checks) |
|
|
63
63
|
| `tetra-audit supabase` | Supabase suite (3 checks) |
|
|
64
64
|
| `tetra-audit hygiene` | Repo hygiene suite (2 checks) |
|
|
@@ -119,13 +119,26 @@ Exit codes: `0` = passed, `1` = failed (CRITICAL/HIGH), `2` = error. No middle g
|
|
|
119
119
|
| `rpc-param-mismatch` | critical | TypeScript `.rpc()` calls with wrong parameter names vs SQL |
|
|
120
120
|
| `rpc-generator-origin` | high | RPC functions not generated by Tetra SQL Generator |
|
|
121
121
|
|
|
122
|
-
### Stability (
|
|
122
|
+
### Stability (16 checks)
|
|
123
123
|
|
|
124
124
|
| Check | Severity | What it catches |
|
|
125
125
|
|-------|----------|-----------------|
|
|
126
126
|
| `husky-hooks` | medium | Missing pre-commit/pre-push hooks |
|
|
127
127
|
| `ci-pipeline` | medium | Missing or incomplete CI config |
|
|
128
128
|
| `npm-audit` | high | Known vulnerabilities in dependencies |
|
|
129
|
+
| `tests` | high | Missing test framework, test files, or test scripts |
|
|
130
|
+
| `eslint-security` | high | Missing ESLint config, security plugin, or SonarJS plugin |
|
|
131
|
+
| `typescript-strict` | medium | TypeScript strict mode not enabled |
|
|
132
|
+
| `coverage-thresholds` | medium | No coverage thresholds configured |
|
|
133
|
+
| `knip` | medium | Dead code: unused files, dependencies, exports |
|
|
134
|
+
| `dependency-cruiser` | medium | Circular dependencies, architecture violations |
|
|
135
|
+
| `dependency-automation` | medium | No Dependabot or Renovate configured |
|
|
136
|
+
| `prettier` | low | No code formatter configured |
|
|
137
|
+
| `conventional-commits` | low | No commit message convention enforced |
|
|
138
|
+
| `bundle-size` | low | No bundle size monitoring |
|
|
139
|
+
| `sast` | medium | No static application security testing |
|
|
140
|
+
| `license-audit` | low | No license compliance checking |
|
|
141
|
+
| `security-layers` | high | Missing security layers (auth, rate limiting, CORS, etc.) |
|
|
129
142
|
|
|
130
143
|
### Code Quality (4 checks)
|
|
131
144
|
|
|
@@ -220,6 +233,18 @@ If any step fails, fix it before writing code. No exceptions.
|
|
|
220
233
|
|
|
221
234
|
## Changelog
|
|
222
235
|
|
|
236
|
+
### 1.16.0
|
|
237
|
+
|
|
238
|
+
**New: Full Stability Suite (16 checks)**
|
|
239
|
+
- Stability suite expanded from 3 → 16 checks via health check adapter
|
|
240
|
+
- New checks: tests, eslint-security, typescript-strict, coverage-thresholds, knip, dependency-cruiser, dependency-automation, prettier, conventional-commits, bundle-size, sast, license-audit, security-layers
|
|
241
|
+
- `tetra-audit stability` now catches missing test infrastructure, dead code, formatting, and security layer gaps
|
|
242
|
+
- Health checks (score-based) automatically adapted to runner format (pass/fail)
|
|
243
|
+
- RPC generator: SECURITY DEFINER → SECURITY INVOKER for all data RPCs
|
|
244
|
+
- Migration lint: expanded whitelist for legitimate DEFINER functions
|
|
245
|
+
- Mixed DB checker: fixed regex that matched `superadminDB` as `adminDB`
|
|
246
|
+
- Route config checker: support `@tetra-audit-ignore` directive
|
|
247
|
+
|
|
223
248
|
### 1.15.0
|
|
224
249
|
|
|
225
250
|
**New: Migration Lint + DB Push Guard**
|
|
@@ -184,6 +184,43 @@ function parseMigrations(projectRoot) {
|
|
|
184
184
|
})
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
// Find PL/pgSQL loop-based CREATE POLICY patterns (DO blocks with EXECUTE format)
|
|
188
|
+
// Handles two patterns:
|
|
189
|
+
// Pattern A: FOR t IN SELECT unnest(ARRAY['table1','table2']) LOOP ... EXECUTE format('CREATE POLICY ...')
|
|
190
|
+
// Pattern B: tables TEXT[] := ARRAY['table1','table2']; FOREACH tbl IN ARRAY tables LOOP ... EXECUTE format('CREATE POLICY ...')
|
|
191
|
+
if (/DO\s+\$/.test(content) && /EXECUTE\s+format\s*\(\s*'CREATE\s+POLICY/i.test(content)) {
|
|
192
|
+
// Try unnest pattern first, then FOREACH/variable pattern
|
|
193
|
+
const arrayMatch = content.match(/unnest\s*\(\s*ARRAY\s*\[\s*'([^[\]]+)'\s*\]/i)
|
|
194
|
+
|| content.match(/(?:TEXT\[\]|text\[\])\s*:=\s*ARRAY\s*\[\s*'([^[\]]+)'\s*\]/i)
|
|
195
|
+
if (arrayMatch) {
|
|
196
|
+
const loopTables = arrayMatch[1].split(/'\s*,\s*'/).map(t => t.trim())
|
|
197
|
+
const execLines = [...content.matchAll(/EXECUTE\s+format\s*\(\s*'(CREATE\s+POLICY\s+.*?)'\s*,/gi)]
|
|
198
|
+
for (const exec of execLines) {
|
|
199
|
+
const stmt = exec[1]
|
|
200
|
+
const forOp = stmt.match(/FOR\s+(SELECT|INSERT|UPDATE|DELETE|ALL)/i)
|
|
201
|
+
const operation = forOp ? forOp[1].toUpperCase() : 'ALL'
|
|
202
|
+
const hasUsing = /\bUSING\b/i.test(stmt)
|
|
203
|
+
const hasWithCheck = /WITH\s+CHECK/i.test(stmt)
|
|
204
|
+
const condMatch = stmt.match(/(?:USING|WITH\s+CHECK)\s*\(\s*(.*?)\s*\)/i)
|
|
205
|
+
const condition = condMatch ? condMatch[1] : ''
|
|
206
|
+
|
|
207
|
+
for (const table of loopTables) {
|
|
208
|
+
if (!tables.has(table)) tables.set(table, { rlsEnabled: false, policies: [], rpcFunctions: new Map() })
|
|
209
|
+
const policyName = `${table}_${operation.toLowerCase()}_org`
|
|
210
|
+
if (!tables.get(table).policies.find(p => p.name === policyName)) {
|
|
211
|
+
tables.get(table).policies.push({
|
|
212
|
+
name: policyName,
|
|
213
|
+
operation,
|
|
214
|
+
using: hasUsing ? condition : '',
|
|
215
|
+
withCheck: hasWithCheck ? condition : '',
|
|
216
|
+
file: relFile
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
187
224
|
// Find RPC functions and their security mode
|
|
188
225
|
const funcRegex = /CREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+(?:public\.)?(\w+)\s*\(([\s\S]*?)\)\s*RETURNS\s+([\s\S]*?)(?:LANGUAGE|AS)/gi
|
|
189
226
|
for (const m of content.matchAll(funcRegex)) {
|
|
@@ -204,6 +241,7 @@ function parseMigrations(projectRoot) {
|
|
|
204
241
|
}
|
|
205
242
|
}
|
|
206
243
|
}
|
|
244
|
+
|
|
207
245
|
}
|
|
208
246
|
|
|
209
247
|
return tables
|
|
@@ -221,8 +259,10 @@ function findFiles(projectRoot, pattern) {
|
|
|
221
259
|
* Check if a USING clause enforces org isolation
|
|
222
260
|
*/
|
|
223
261
|
function isOrgIsolation(using) {
|
|
224
|
-
|
|
225
|
-
|
|
262
|
+
const hasOrgFunction = /auth_org_id\(\)|auth_admin_organizations\(\)/i.test(using)
|
|
263
|
+
// Legacy pattern: auth.jwt() -> 'app_metadata' ->> 'organization_id'
|
|
264
|
+
const hasLegacyJwtOrg = /auth\.jwt\(\)\s*->\s*'app_metadata'\s*->>\s*'organization_id'/i.test(using)
|
|
265
|
+
return (hasOrgFunction || hasLegacyJwtOrg) && /organization_id/i.test(using)
|
|
226
266
|
}
|
|
227
267
|
|
|
228
268
|
/**
|
|
@@ -287,6 +327,7 @@ export async function run(config, projectRoot) {
|
|
|
287
327
|
const updatePolicies = policies.filter(p => p.operation === 'UPDATE' || p.operation === 'ALL')
|
|
288
328
|
const deletePolicies = policies.filter(p => p.operation === 'DELETE' || p.operation === 'ALL')
|
|
289
329
|
|
|
330
|
+
|
|
290
331
|
// CHECK 2: Must have policies for all operations
|
|
291
332
|
const missingOps = []
|
|
292
333
|
if (selectPolicies.length === 0) missingOps.push('SELECT')
|
package/lib/runner.js
CHANGED
|
@@ -32,6 +32,56 @@ import * as rpcGeneratorOrigin from './checks/supabase/rpc-generator-origin.js'
|
|
|
32
32
|
import * as fileOrganization from './checks/hygiene/file-organization.js'
|
|
33
33
|
import * as stellaCompliance from './checks/hygiene/stella-compliance.js'
|
|
34
34
|
|
|
35
|
+
// Health checks (score-based) — wrapped as runner checks via adapter
|
|
36
|
+
import { check as checkTests } from './checks/health/tests.js'
|
|
37
|
+
import { check as checkEslintSecurity } from './checks/health/eslint-security.js'
|
|
38
|
+
import { check as checkTypescriptStrict } from './checks/health/typescript-strict.js'
|
|
39
|
+
import { check as checkCoverageThresholds } from './checks/health/coverage-thresholds.js'
|
|
40
|
+
import { check as checkKnip } from './checks/health/knip.js'
|
|
41
|
+
import { check as checkDependencyCruiser } from './checks/health/dependency-cruiser.js'
|
|
42
|
+
import { check as checkDependencyAutomation } from './checks/health/dependency-automation.js'
|
|
43
|
+
import { check as checkPrettier } from './checks/health/prettier.js'
|
|
44
|
+
import { check as checkConventionalCommits } from './checks/health/conventional-commits.js'
|
|
45
|
+
import { check as checkBundleSize } from './checks/health/bundle-size.js'
|
|
46
|
+
import { check as checkSast } from './checks/health/sast.js'
|
|
47
|
+
import { check as checkLicenseAudit } from './checks/health/license-audit.js'
|
|
48
|
+
import { check as checkSecurityLayers } from './checks/health/security-layers.js'
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Adapt a health check (score-based) to the runner format (meta + run).
|
|
52
|
+
* A health check passes if it scores > 0 (has at least some infrastructure).
|
|
53
|
+
*/
|
|
54
|
+
function adaptHealthCheck(id, name, severity, healthCheckFn) {
|
|
55
|
+
return {
|
|
56
|
+
meta: { id, name, severity },
|
|
57
|
+
async run(config, projectRoot) {
|
|
58
|
+
const result = await healthCheckFn(projectRoot)
|
|
59
|
+
const passed = result.score > 0
|
|
60
|
+
const findings = []
|
|
61
|
+
|
|
62
|
+
if (!passed && result.details?.message) {
|
|
63
|
+
findings.push({
|
|
64
|
+
file: 'project',
|
|
65
|
+
line: 0,
|
|
66
|
+
severity,
|
|
67
|
+
message: result.details.message
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
passed,
|
|
73
|
+
findings,
|
|
74
|
+
summary: {
|
|
75
|
+
total: 1,
|
|
76
|
+
[severity]: passed ? 0 : 1,
|
|
77
|
+
score: result.score,
|
|
78
|
+
maxScore: result.maxScore
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
35
85
|
// Register all checks
|
|
36
86
|
const ALL_CHECKS = {
|
|
37
87
|
security: [
|
|
@@ -51,7 +101,20 @@ const ALL_CHECKS = {
|
|
|
51
101
|
stability: [
|
|
52
102
|
huskyHooks,
|
|
53
103
|
ciPipeline,
|
|
54
|
-
npmAudit
|
|
104
|
+
npmAudit,
|
|
105
|
+
adaptHealthCheck('tests', 'Test Infrastructure', 'high', checkTests),
|
|
106
|
+
adaptHealthCheck('eslint-security', 'ESLint Security Plugins', 'high', checkEslintSecurity),
|
|
107
|
+
adaptHealthCheck('typescript-strict', 'TypeScript Strictness', 'medium', checkTypescriptStrict),
|
|
108
|
+
adaptHealthCheck('coverage-thresholds', 'Test Coverage Thresholds', 'medium', checkCoverageThresholds),
|
|
109
|
+
adaptHealthCheck('knip', 'Dead Code Detection (Knip)', 'medium', checkKnip),
|
|
110
|
+
adaptHealthCheck('dependency-cruiser', 'Dependency Architecture', 'medium', checkDependencyCruiser),
|
|
111
|
+
adaptHealthCheck('dependency-automation', 'Dependency Updates (Dependabot/Renovate)', 'medium', checkDependencyAutomation),
|
|
112
|
+
adaptHealthCheck('prettier', 'Code Formatting (Prettier)', 'low', checkPrettier),
|
|
113
|
+
adaptHealthCheck('conventional-commits', 'Conventional Commits', 'low', checkConventionalCommits),
|
|
114
|
+
adaptHealthCheck('bundle-size', 'Bundle Size Monitoring', 'low', checkBundleSize),
|
|
115
|
+
adaptHealthCheck('sast', 'Static Application Security Testing', 'medium', checkSast),
|
|
116
|
+
adaptHealthCheck('license-audit', 'License Compliance', 'low', checkLicenseAudit),
|
|
117
|
+
adaptHealthCheck('security-layers', 'Security Layer Coverage', 'high', checkSecurityLayers)
|
|
55
118
|
],
|
|
56
119
|
codeQuality: [
|
|
57
120
|
apiResponseFormat,
|