@soulbatical/tetra-dev-toolkit 1.18.1 → 1.20.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/README.md +76 -0
- package/bin/tetra-check-peers.js +22 -1
- package/bin/tetra-security-gate.js +293 -0
- package/bin/tetra-setup.js +172 -2
- package/bin/tetra-smoke.js +532 -0
- package/lib/checks/health/index.js +1 -0
- package/lib/checks/health/scanner.js +3 -1
- package/lib/checks/health/smoke-readiness.js +150 -0
- package/lib/checks/health/types.js +1 -1
- package/lib/checks/hygiene/stella-compliance.js +2 -2
- package/lib/checks/security/config-rls-alignment.js +10 -4
- package/lib/checks/security/direct-supabase-client.js +9 -0
- package/lib/checks/security/hardcoded-secrets.js +5 -2
- package/lib/checks/security/rls-live-audit.js +97 -6
- package/lib/runner.js +46 -8
- package/package.json +4 -2
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* @typedef {'plugins'|'mcps'|'git'|'tests'|'secrets'|'quality-toolkit'|'naming-conventions'|'rls-audit'|'rpc-param-mismatch'|'typescript-strict'|'prettier'|'coverage-thresholds'|'eslint-security'|'dependency-cruiser'|'conventional-commits'|'knip'|'dependency-automation'|'license-audit'|'sast'|'bundle-size'|'gitignore'|'repo-visibility'|'vincifox-widget'|'stella-integration'|'claude-md'|'doppler-compliance'|'infrastructure-yml'|'file-organization'|'security-layers'} HealthCheckType
|
|
8
|
+
* @typedef {'plugins'|'mcps'|'git'|'tests'|'secrets'|'quality-toolkit'|'naming-conventions'|'rls-audit'|'rpc-param-mismatch'|'typescript-strict'|'prettier'|'coverage-thresholds'|'eslint-security'|'dependency-cruiser'|'conventional-commits'|'knip'|'dependency-automation'|'license-audit'|'sast'|'bundle-size'|'gitignore'|'repo-visibility'|'vincifox-widget'|'stella-integration'|'claude-md'|'doppler-compliance'|'infrastructure-yml'|'file-organization'|'security-layers'|'smoke-readiness'} HealthCheckType
|
|
9
9
|
*
|
|
10
10
|
* @typedef {'ok'|'warning'|'error'} HealthStatus
|
|
11
11
|
*
|
|
@@ -29,7 +29,7 @@ const DUPLICATE_PATTERNS = [
|
|
|
29
29
|
{
|
|
30
30
|
pattern: /const QUESTIONS_DIR\s*=\s*join\(tmpdir\(\)/,
|
|
31
31
|
label: 'Local QUESTIONS_DIR (telegram question store)',
|
|
32
|
-
allowedIn: ['stella/src/telegram.ts']
|
|
32
|
+
allowedIn: ['stella/src/telegram.ts', 'backend/src/features/telegram/']
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
pattern: /function detectWorkspaceRef\(\)/,
|
|
@@ -49,7 +49,7 @@ const DUPLICATE_PATTERNS = [
|
|
|
49
49
|
{
|
|
50
50
|
pattern: /function splitMessage\(text:\s*string/,
|
|
51
51
|
label: 'Local splitMessage helper (for telegram)',
|
|
52
|
-
allowedIn: ['stella/src/telegram.ts', 'tools/helpers.ts']
|
|
52
|
+
allowedIn: ['stella/src/telegram.ts', 'tools/helpers.ts', 'backend/src/features/telegram/']
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
pattern: /function playMacAlert\(\)/,
|
|
@@ -29,7 +29,7 @@ export const meta = {
|
|
|
29
29
|
name: 'Config ↔ RLS Alignment',
|
|
30
30
|
category: 'security',
|
|
31
31
|
severity: 'critical',
|
|
32
|
-
description: 'Verifies that feature config accessLevel matches actual RLS policies on each table.
|
|
32
|
+
description: 'Verifies that feature config accessLevel matches actual RLS policies on each table. Uses live DB when available, falls back to migration parsing.'
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
@@ -390,17 +390,23 @@ function validateRlsClause(clause) {
|
|
|
390
390
|
return null
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
-
export async function run(config, projectRoot) {
|
|
393
|
+
export async function run(config, projectRoot, options = {}) {
|
|
394
394
|
const results = {
|
|
395
395
|
passed: true,
|
|
396
396
|
skipped: false,
|
|
397
397
|
findings: [],
|
|
398
398
|
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
|
|
399
|
-
details: { tablesChecked: 0, configsFound: 0, alignmentErrors: 0 }
|
|
399
|
+
details: { tablesChecked: 0, configsFound: 0, alignmentErrors: 0, source: 'migrations' }
|
|
400
400
|
}
|
|
401
401
|
|
|
402
402
|
const featureConfigs = parseFeatureConfigs(projectRoot)
|
|
403
|
-
|
|
403
|
+
|
|
404
|
+
// Use live DB state if available (passed from rls-live-audit via runner),
|
|
405
|
+
// otherwise fall back to migration file parsing
|
|
406
|
+
const rlsData = options.liveState || parseMigrations(projectRoot)
|
|
407
|
+
if (options.liveState) {
|
|
408
|
+
results.details.source = 'live-db'
|
|
409
|
+
}
|
|
404
410
|
|
|
405
411
|
results.details.configsFound = featureConfigs.size
|
|
406
412
|
|
|
@@ -63,6 +63,15 @@ const ALLOWED_FILES = [
|
|
|
63
63
|
// Domain middleware (sets RLS session vars — needs direct client)
|
|
64
64
|
/middleware\/domainOrganizationMiddleware\.ts$/,
|
|
65
65
|
|
|
66
|
+
// Auth routes that only use Supabase Auth API (not DB queries)
|
|
67
|
+
/routes\/auth\.ts$/,
|
|
68
|
+
|
|
69
|
+
// WebSocket auth verification (only uses auth.getUser for token validation)
|
|
70
|
+
/services\/terminalWebSocket\.ts$/,
|
|
71
|
+
|
|
72
|
+
// Frontend Supabase client (Vite apps — client-side auth only, no Tetra backend)
|
|
73
|
+
/frontend\/src\/lib\/supabase\.ts$/,
|
|
74
|
+
|
|
66
75
|
// Scripts (not production code)
|
|
67
76
|
/scripts\//,
|
|
68
77
|
]
|
|
@@ -34,10 +34,13 @@ export async function run(config, projectRoot) {
|
|
|
34
34
|
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 }
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// Get all source files
|
|
37
|
+
// Get all source files (always exclude node_modules, even nested ones)
|
|
38
38
|
const files = await glob('**/*.{ts,tsx,js,jsx,json}', {
|
|
39
39
|
cwd: projectRoot,
|
|
40
|
-
ignore:
|
|
40
|
+
ignore: [
|
|
41
|
+
'**/node_modules/**',
|
|
42
|
+
...config.ignore
|
|
43
|
+
]
|
|
41
44
|
})
|
|
42
45
|
|
|
43
46
|
for (const file of files) {
|
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* RLS Live DB Audit — queries pg_policies from the LIVE database
|
|
3
3
|
*
|
|
4
|
-
* The source of truth for RLS policy validation.
|
|
4
|
+
* The PRIMARY source of truth for RLS policy validation.
|
|
5
|
+
* When this check runs successfully, migration-based RLS checks
|
|
6
|
+
* (rls-policy-audit, config-rls-alignment) are skipped for current state
|
|
7
|
+
* and only validate unapplied migrations (delta).
|
|
8
|
+
*
|
|
9
|
+
* Migration file parsing can miss:
|
|
5
10
|
* - Policies applied directly via SQL (not in migration files)
|
|
6
11
|
* - PL/pgSQL dynamic policies (EXECUTE format)
|
|
7
12
|
* - Policies overridden by later manual changes
|
|
8
13
|
*
|
|
9
14
|
* Requires SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY in environment.
|
|
10
|
-
* Connects via @supabase/supabase-js and calls a lightweight RPC.
|
|
11
15
|
*
|
|
12
16
|
* Setup (one-time per project):
|
|
13
17
|
* CREATE OR REPLACE FUNCTION tetra_rls_audit()
|
|
14
|
-
* RETURNS TABLE(tablename text, policyname text, using_clause text, with_check_clause text)
|
|
18
|
+
* RETURNS TABLE(tablename text, policyname text, cmd text, using_clause text, with_check_clause text)
|
|
15
19
|
* LANGUAGE sql SECURITY DEFINER AS $$
|
|
16
|
-
* SELECT tablename::text, policyname::text, qual::text, with_check::text
|
|
20
|
+
* SELECT tablename::text, policyname::text, cmd::text, qual::text, with_check::text
|
|
17
21
|
* FROM pg_policies WHERE schemaname = 'public';
|
|
18
22
|
* $$;
|
|
23
|
+
*
|
|
24
|
+
* -- Optional: also return RLS enabled status per table
|
|
25
|
+
* CREATE OR REPLACE FUNCTION tetra_rls_tables()
|
|
26
|
+
* RETURNS TABLE(table_name text, rls_enabled boolean, rls_forced boolean)
|
|
27
|
+
* LANGUAGE sql SECURITY DEFINER AS $$
|
|
28
|
+
* SELECT c.relname::text, c.relrowsecurity, c.relforcerowsecurity
|
|
29
|
+
* FROM pg_class c
|
|
30
|
+
* JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
31
|
+
* WHERE n.nspname = 'public' AND c.relkind = 'r';
|
|
32
|
+
* $$;
|
|
19
33
|
*/
|
|
20
34
|
|
|
21
35
|
export const meta = {
|
|
@@ -70,13 +84,83 @@ function validateRlsClause(clause) {
|
|
|
70
84
|
return null
|
|
71
85
|
}
|
|
72
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Fetch data from a Supabase RPC endpoint.
|
|
89
|
+
* Returns null if the RPC doesn't exist or fails.
|
|
90
|
+
*/
|
|
91
|
+
async function callRpc(supabaseUrl, supabaseKey, rpcName) {
|
|
92
|
+
try {
|
|
93
|
+
const response = await fetch(`${supabaseUrl}/rest/v1/rpc/${rpcName}`, {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: {
|
|
96
|
+
'apikey': supabaseKey,
|
|
97
|
+
'Authorization': `Bearer ${supabaseKey}`,
|
|
98
|
+
'Content-Type': 'application/json',
|
|
99
|
+
},
|
|
100
|
+
body: '{}',
|
|
101
|
+
})
|
|
102
|
+
if (!response.ok) return null
|
|
103
|
+
return await response.json()
|
|
104
|
+
} catch {
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build a structured live DB state from raw policy + table data.
|
|
111
|
+
* This is the same format that config-rls-alignment uses from migration parsing,
|
|
112
|
+
* so it can transparently use either source.
|
|
113
|
+
*
|
|
114
|
+
* Returns: Map<tableName, { rlsEnabled, policies: [{ name, operation, using, withCheck }] }>
|
|
115
|
+
*/
|
|
116
|
+
export function buildLiveState(policies, tableStatus) {
|
|
117
|
+
const tables = new Map()
|
|
118
|
+
|
|
119
|
+
// Initialize from table status (if available)
|
|
120
|
+
if (Array.isArray(tableStatus)) {
|
|
121
|
+
for (const t of tableStatus) {
|
|
122
|
+
if (!t?.table_name) continue
|
|
123
|
+
tables.set(t.table_name, {
|
|
124
|
+
rlsEnabled: t.rls_enabled || false,
|
|
125
|
+
policies: [],
|
|
126
|
+
rpcFunctions: new Map(),
|
|
127
|
+
source: 'live-db'
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Add policies
|
|
133
|
+
for (const policy of policies) {
|
|
134
|
+
if (!policy) continue
|
|
135
|
+
const { tablename, policyname, cmd, using_clause, with_check_clause } = policy
|
|
136
|
+
if (!tablename) continue
|
|
137
|
+
|
|
138
|
+
if (!tables.has(tablename)) {
|
|
139
|
+
// Table exists in policies but not in table status — it has RLS (otherwise no policies)
|
|
140
|
+
tables.set(tablename, { rlsEnabled: true, policies: [], rpcFunctions: new Map(), source: 'live-db' })
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
tables.get(tablename).policies.push({
|
|
144
|
+
name: policyname,
|
|
145
|
+
operation: (cmd || 'ALL').toUpperCase(),
|
|
146
|
+
using: using_clause || '',
|
|
147
|
+
withCheck: with_check_clause || '',
|
|
148
|
+
file: 'LIVE DB'
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return tables
|
|
153
|
+
}
|
|
154
|
+
|
|
73
155
|
export async function run(config, projectRoot) {
|
|
74
156
|
const results = {
|
|
75
157
|
passed: true,
|
|
76
158
|
skipped: false,
|
|
77
159
|
findings: [],
|
|
78
160
|
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
|
|
79
|
-
details: { policiesChecked: 0, tablesChecked: 0, violations: 0 }
|
|
161
|
+
details: { policiesChecked: 0, tablesChecked: 0, violations: 0 },
|
|
162
|
+
// Expose live data so runner can pass it to other checks
|
|
163
|
+
_liveData: null
|
|
80
164
|
}
|
|
81
165
|
|
|
82
166
|
const supabaseUrl = process.env.SUPABASE_URL
|
|
@@ -105,7 +189,7 @@ export async function run(config, projectRoot) {
|
|
|
105
189
|
const status = response.status
|
|
106
190
|
if (status === 404) {
|
|
107
191
|
results.skipped = true
|
|
108
|
-
results.skipReason = 'RPC tetra_rls_audit() not found. Create it once:\n\n CREATE OR REPLACE FUNCTION tetra_rls_audit()\n RETURNS TABLE(tablename text, policyname text, using_clause text, with_check_clause text)\n LANGUAGE sql SECURITY DEFINER AS $$\n SELECT tablename::text, policyname::text, qual::text, with_check::text\n FROM pg_policies WHERE schemaname = \'public\';\n $$;'
|
|
192
|
+
results.skipReason = 'RPC tetra_rls_audit() not found. Create it once:\n\n CREATE OR REPLACE FUNCTION tetra_rls_audit()\n RETURNS TABLE(tablename text, policyname text, cmd text, using_clause text, with_check_clause text)\n LANGUAGE sql SECURITY DEFINER AS $$\n SELECT tablename::text, policyname::text, cmd::text, qual::text, with_check::text\n FROM pg_policies WHERE schemaname = \'public\';\n $$;'
|
|
109
193
|
} else {
|
|
110
194
|
results.skipped = true
|
|
111
195
|
results.skipReason = `RPC tetra_rls_audit() failed with status ${status}`
|
|
@@ -126,6 +210,13 @@ export async function run(config, projectRoot) {
|
|
|
126
210
|
return results
|
|
127
211
|
}
|
|
128
212
|
|
|
213
|
+
// Optionally fetch table-level RLS status
|
|
214
|
+
const tableStatus = await callRpc(supabaseUrl, supabaseKey, 'tetra_rls_tables')
|
|
215
|
+
|
|
216
|
+
// Build structured live state (shared with config-rls-alignment)
|
|
217
|
+
const liveState = buildLiveState(policies, tableStatus)
|
|
218
|
+
results._liveData = { policies, tableStatus, liveState }
|
|
219
|
+
|
|
129
220
|
const backendOnlyTables = config.supabase?.backendOnlyTables || []
|
|
130
221
|
const tablesChecked = new Set()
|
|
131
222
|
|
package/lib/runner.js
CHANGED
|
@@ -47,6 +47,7 @@ import { check as checkBundleSize } from './checks/health/bundle-size.js'
|
|
|
47
47
|
import { check as checkSast } from './checks/health/sast.js'
|
|
48
48
|
import { check as checkLicenseAudit } from './checks/health/license-audit.js'
|
|
49
49
|
import { check as checkSecurityLayers } from './checks/health/security-layers.js'
|
|
50
|
+
import { check as checkSmokeReadiness } from './checks/health/smoke-readiness.js'
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
53
|
* Adapt a health check (score-based) to the runner format (meta + run).
|
|
@@ -93,12 +94,12 @@ const ALL_CHECKS = {
|
|
|
93
94
|
frontendSupabaseQueries,
|
|
94
95
|
tetraCoreCompliance,
|
|
95
96
|
mixedDbUsage,
|
|
96
|
-
configRlsAlignment,
|
|
97
97
|
rpcSecurityMode,
|
|
98
98
|
systemdbWhitelist,
|
|
99
99
|
gitignoreValidation,
|
|
100
100
|
routeConfigAlignment,
|
|
101
|
-
rlsLiveAudit
|
|
101
|
+
rlsLiveAudit, // Must run BEFORE config-rls-alignment (provides live DB data)
|
|
102
|
+
configRlsAlignment, // Uses live DB data from rls-live-audit when available
|
|
102
103
|
],
|
|
103
104
|
stability: [
|
|
104
105
|
huskyHooks,
|
|
@@ -116,7 +117,8 @@ const ALL_CHECKS = {
|
|
|
116
117
|
adaptHealthCheck('bundle-size', 'Bundle Size Monitoring', 'low', checkBundleSize),
|
|
117
118
|
adaptHealthCheck('sast', 'Static Application Security Testing', 'medium', checkSast),
|
|
118
119
|
adaptHealthCheck('license-audit', 'License Compliance', 'low', checkLicenseAudit),
|
|
119
|
-
adaptHealthCheck('security-layers', 'Security Layer Coverage', 'high', checkSecurityLayers)
|
|
120
|
+
adaptHealthCheck('security-layers', 'Security Layer Coverage', 'high', checkSecurityLayers),
|
|
121
|
+
adaptHealthCheck('smoke-readiness', 'Smoke Test Readiness', 'medium', checkSmokeReadiness)
|
|
120
122
|
],
|
|
121
123
|
codeQuality: [
|
|
122
124
|
apiResponseFormat,
|
|
@@ -162,6 +164,10 @@ export async function runAllChecks(options = {}) {
|
|
|
162
164
|
}
|
|
163
165
|
}
|
|
164
166
|
|
|
167
|
+
// Track rls-live-audit result across suites so migration-based RLS checks can be skipped.
|
|
168
|
+
// rls-live-audit runs in 'security' suite, rls-policy-audit runs in 'supabase' suite.
|
|
169
|
+
let rlsLiveData = null
|
|
170
|
+
|
|
165
171
|
for (const suite of suites) {
|
|
166
172
|
if (!config.suites[suite]) {
|
|
167
173
|
continue
|
|
@@ -174,11 +180,43 @@ export async function runAllChecks(options = {}) {
|
|
|
174
180
|
}
|
|
175
181
|
|
|
176
182
|
for (const check of checks) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
183
|
+
let checkResult
|
|
184
|
+
|
|
185
|
+
// rls-policy-audit is pure migration parsing — skip when live DB is available
|
|
186
|
+
if (rlsLiveData && check.meta.id === 'rls-policy-audit') {
|
|
187
|
+
checkResult = {
|
|
188
|
+
id: check.meta.id,
|
|
189
|
+
name: `${check.meta.name} (skipped — live DB is source of truth)`,
|
|
190
|
+
severity: check.meta.severity,
|
|
191
|
+
passed: true,
|
|
192
|
+
skipped: true,
|
|
193
|
+
skipReason: 'Skipped: rls-live-audit succeeded — live DB is the source of truth for current RLS state.',
|
|
194
|
+
findings: [],
|
|
195
|
+
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// config-rls-alignment: always runs, but uses live DB data when available
|
|
199
|
+
else if (rlsLiveData && check.meta.id === 'config-rls-alignment') {
|
|
200
|
+
checkResult = {
|
|
201
|
+
id: check.meta.id,
|
|
202
|
+
name: `${check.meta.name} (live DB)`,
|
|
203
|
+
severity: check.meta.severity,
|
|
204
|
+
...await check.run(config, projectRoot, { liveState: rlsLiveData.liveState })
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
checkResult = {
|
|
209
|
+
id: check.meta.id,
|
|
210
|
+
name: check.meta.name,
|
|
211
|
+
severity: check.meta.severity,
|
|
212
|
+
...await check.run(config, projectRoot)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Capture live data from rls-live-audit for downstream checks
|
|
217
|
+
if (check.meta.id === 'rls-live-audit' && !checkResult.skipped && checkResult._liveData) {
|
|
218
|
+
rlsLiveData = checkResult._liveData
|
|
219
|
+
delete checkResult._liveData // Don't leak internal data into output
|
|
182
220
|
}
|
|
183
221
|
|
|
184
222
|
results.suites[suite].checks.push(checkResult)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soulbatical/tetra-dev-toolkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "restricted"
|
|
6
6
|
},
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
"tetra-check-rls": "./bin/tetra-check-rls.js",
|
|
33
33
|
"tetra-migration-lint": "./bin/tetra-migration-lint.js",
|
|
34
34
|
"tetra-db-push": "./bin/tetra-db-push.js",
|
|
35
|
-
"tetra-check-peers": "./bin/tetra-check-peers.js"
|
|
35
|
+
"tetra-check-peers": "./bin/tetra-check-peers.js",
|
|
36
|
+
"tetra-security-gate": "./bin/tetra-security-gate.js",
|
|
37
|
+
"tetra-smoke": "./bin/tetra-smoke.js"
|
|
36
38
|
},
|
|
37
39
|
"files": [
|
|
38
40
|
"bin/",
|