ship-safe 4.2.0 → 5.0.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.
@@ -0,0 +1,204 @@
1
+ /**
2
+ * RAG Security Agent
3
+ * ====================
4
+ *
5
+ * Detects security vulnerabilities in Retrieval-Augmented Generation
6
+ * (RAG) implementations. RAG poisoning attacks are a proven threat
7
+ * vector — attackers corrupt knowledge bases to manipulate LLM outputs.
8
+ *
9
+ * Checks: unvalidated document ingestion, missing chunk isolation,
10
+ * embedding exposure, metadata leakage, no retrieval filtering,
11
+ * excessive context stuffing.
12
+ *
13
+ * Maps to: OWASP LLM08 (Vector & Embedding Weaknesses),
14
+ * OWASP Agentic AI ASI04 (Memory Poisoning)
15
+ */
16
+
17
+ import path from 'path';
18
+ import { BaseAgent } from './base-agent.js';
19
+
20
+ // =============================================================================
21
+ // RAG SECURITY PATTERNS
22
+ // =============================================================================
23
+
24
+ const PATTERNS = [
25
+ // ── Document Ingestion ───────────────────────────────────────────────────
26
+ {
27
+ rule: 'RAG_UNSANITIZED_INGESTION',
28
+ title: 'RAG: Documents Ingested Without Sanitization',
29
+ regex: /(?:addDocuments|add_documents|upsert|insert|index\.add|from_documents)\s*\(\s*(?:documents|docs|chunks|texts|pages|uploads|files|userDocs|userFiles)/g,
30
+ severity: 'high',
31
+ cwe: 'CWE-20',
32
+ owasp: 'A03:2021',
33
+ description: 'Documents added to vector store without sanitization. Attackers can embed malicious instructions in documents that get retrieved and injected into LLM context.',
34
+ fix: 'Sanitize document content before ingestion: strip HTML/script tags, detect prompt injection patterns, validate content type.',
35
+ },
36
+ {
37
+ rule: 'RAG_USER_UPLOAD_TO_VECTORDB',
38
+ title: 'RAG: User Upload Directly to Vector Store',
39
+ regex: /(?:upload|multer|formidable|busboy|req\.file|req\.files)[\s\S]{0,500}(?:addDocuments|add_documents|upsert|vectorStore|index\.add|embed|from_documents)/g,
40
+ severity: 'critical',
41
+ cwe: 'CWE-434',
42
+ owasp: 'A03:2021',
43
+ description: 'User file uploads are fed directly into a vector database without review. This enables RAG poisoning — attackers upload documents containing hidden instructions.',
44
+ fix: 'Add a review/approval pipeline between user uploads and vector store ingestion. Scan uploads for prompt injection patterns.',
45
+ },
46
+ {
47
+ rule: 'RAG_NO_CONTENT_FILTER',
48
+ title: 'RAG: No Content Filtering on Ingestion',
49
+ regex: /(?:TextLoader|PDFLoader|CSVLoader|DirectoryLoader|WebBaseLoader|UnstructuredLoader|load_and_split|loadDocuments)\s*\((?![\s\S]{0,300}(?:filter|sanitize|clean|validate|strip|remove|moderate))/g,
50
+ severity: 'medium',
51
+ cwe: 'CWE-20',
52
+ owasp: 'A03:2021',
53
+ confidence: 'medium',
54
+ description: 'Document loaders used without content filtering. Loaded content goes directly to embedding/vector store.',
55
+ fix: 'Add content filtering after loading documents: remove scripts, detect injection patterns, validate format.',
56
+ },
57
+
58
+ // ── Chunk Isolation ──────────────────────────────────────────────────────
59
+ {
60
+ rule: 'RAG_NO_TENANT_ISOLATION',
61
+ title: 'RAG: Vector Store Without Tenant Isolation',
62
+ regex: /(?:vectorStore|pinecone|chroma|weaviate|qdrant|milvus|pgvector)[\s\S]{0,300}(?:add|upsert|insert|index)(?![\s\S]{0,200}(?:namespace|tenant|partition|collection|filter|user_id|org_id|metadata.*(?:user|tenant|org)))/g,
63
+ severity: 'high',
64
+ cwe: 'CWE-284',
65
+ owasp: 'A01:2021',
66
+ confidence: 'medium',
67
+ description: 'Documents stored in vector database without tenant/user isolation. Users can retrieve other users\' private documents.',
68
+ fix: 'Use namespaces, collections, or metadata filters to isolate documents by tenant/user.',
69
+ },
70
+ {
71
+ rule: 'RAG_SYSTEM_DOCS_WITH_USER_DOCS',
72
+ title: 'RAG: System Documents Mixed With User Documents',
73
+ regex: /(?:addDocuments|add_documents|upsert)[\s\S]{0,200}(?:system|internal|private|admin)[\s\S]{0,200}(?:user|public|external|upload)/g,
74
+ severity: 'medium',
75
+ cwe: 'CWE-668',
76
+ owasp: 'A01:2021',
77
+ confidence: 'low',
78
+ description: 'System/internal documents mixed with user documents in the same collection. User queries could retrieve sensitive internal docs.',
79
+ fix: 'Separate system documents from user documents in different collections or namespaces.',
80
+ },
81
+
82
+ // ── Retrieval & Query Safety ─────────────────────────────────────────────
83
+ {
84
+ rule: 'RAG_NO_RETRIEVAL_FILTER',
85
+ title: 'RAG: Retrieved Chunks Used Without Filtering',
86
+ regex: /(?:similarity_search|similaritySearch|query|retrieve|get_relevant|asRetriever)[\s\S]{0,300}(?:page_content|pageContent|text|content|document)[\s\S]{0,200}(?:prompt|messages|content|system|user)/g,
87
+ severity: 'high',
88
+ cwe: 'CWE-74',
89
+ owasp: 'A03:2021',
90
+ confidence: 'medium',
91
+ description: 'Retrieved document chunks injected into LLM prompt without filtering. Poisoned documents can override system instructions.',
92
+ fix: 'Filter retrieved chunks: remove instruction-like content, apply relevance score thresholds, limit number of chunks.',
93
+ },
94
+ {
95
+ rule: 'RAG_NO_RELEVANCE_THRESHOLD',
96
+ title: 'RAG: No Relevance Score Threshold on Retrieval',
97
+ regex: /(?:similarity_search|similaritySearch|asRetriever)\s*\(\s*(?:query|question|input)(?![\s\S]{0,200}(?:score_threshold|scoreThreshold|threshold|minScore|min_score|filter))/g,
98
+ severity: 'medium',
99
+ cwe: 'CWE-20',
100
+ owasp: 'A03:2021',
101
+ confidence: 'low',
102
+ description: 'Vector similarity search without relevance threshold. Low-relevance chunks may contain poisoned content designed to be retrieved for many queries.',
103
+ fix: 'Set a minimum relevance score threshold to filter out low-quality matches.',
104
+ },
105
+ {
106
+ rule: 'RAG_EXCESSIVE_CONTEXT',
107
+ title: 'RAG: Excessive Retrieved Context',
108
+ regex: /(?:k\s*[:=]\s*(?:[5-9]\d|\d{3,})|top_k\s*[:=]\s*(?:[5-9]\d|\d{3,})|search_kwargs.*k.*(?:[5-9]\d|\d{3,}))/g,
109
+ severity: 'medium',
110
+ cwe: 'CWE-400',
111
+ owasp: 'A04:2021',
112
+ confidence: 'low',
113
+ description: 'Retrieving a very large number of chunks (50+). Increases the attack surface for prompt injection via poisoned documents.',
114
+ fix: 'Limit retrieved chunks to the minimum needed (typically 3-10). More chunks = more injection surface.',
115
+ },
116
+
117
+ // ── Embedding & Data Exposure ────────────────────────────────────────────
118
+ {
119
+ rule: 'RAG_EMBEDDING_API_EXPOSED',
120
+ title: 'RAG: Embedding Endpoint Exposed Without Auth',
121
+ regex: /(?:app\.|router\.|api\.)(?:get|post)\s*\(\s*['"].*(?:embed|embedding|vectorize|encode)['"](?![\s\S]{0,300}(?:auth|middleware|protect|guard|jwt|bearer|session))/g,
122
+ severity: 'high',
123
+ cwe: 'CWE-306',
124
+ owasp: 'A07:2021',
125
+ confidence: 'medium',
126
+ description: 'Embedding API endpoint exposed without authentication. Attackers can enumerate embeddings or generate vectors for injection attacks.',
127
+ fix: 'Add authentication middleware to embedding endpoints.',
128
+ },
129
+ {
130
+ rule: 'RAG_METADATA_IN_RESPONSE',
131
+ title: 'RAG: Internal Metadata Exposed in Response',
132
+ regex: /(?:metadata|source|file_path|filePath|url|author|timestamp)[\s\S]{0,100}(?:res\.json|res\.send|response\.json|return\s*\{|jsonify)/g,
133
+ severity: 'medium',
134
+ cwe: 'CWE-200',
135
+ owasp: 'A01:2021',
136
+ confidence: 'low',
137
+ description: 'Internal document metadata (file paths, sources, authors) returned in API responses. Leaks information about the knowledge base structure.',
138
+ fix: 'Strip internal metadata before returning responses. Only expose necessary fields to the client.',
139
+ },
140
+ {
141
+ rule: 'RAG_EMBEDDING_IN_RESPONSE',
142
+ title: 'RAG: Raw Embeddings Returned to Client',
143
+ regex: /(?:embedding|vector|dense_vector)[\s\S]{0,100}(?:res\.json|res\.send|response|return|output)/g,
144
+ severity: 'medium',
145
+ cwe: 'CWE-200',
146
+ owasp: 'A01:2021',
147
+ confidence: 'low',
148
+ description: 'Raw embedding vectors returned in API responses. Research shows embeddings can be inverted to recover original text, leaking private data.',
149
+ fix: 'Never return raw embedding vectors in API responses. Embeddings should remain server-side only.',
150
+ },
151
+
152
+ // ── Model & Pipeline Safety ──────────────────────────────────────────────
153
+ {
154
+ rule: 'RAG_PICKLE_EMBEDDING_MODEL',
155
+ title: 'RAG: Embedding Model Loaded via Pickle',
156
+ regex: /(?:torch\.load|pickle\.load|joblib\.load)\s*\(\s*(?!.*safetensors)(?!.*weights_only\s*=\s*True)/g,
157
+ severity: 'critical',
158
+ cwe: 'CWE-502',
159
+ owasp: 'A08:2021',
160
+ description: 'ML model loaded from pickle format. Pickle files can contain arbitrary code that executes on load.',
161
+ fix: 'Use safetensors format for model loading. If using torch.load(), set weights_only=True.',
162
+ },
163
+ {
164
+ rule: 'RAG_TRUST_REMOTE_CODE',
165
+ title: 'RAG: Model Loaded With trust_remote_code=True',
166
+ regex: /trust_remote_code\s*=\s*True/g,
167
+ severity: 'high',
168
+ cwe: 'CWE-94',
169
+ owasp: 'A08:2021',
170
+ description: 'Embedding or LLM model loaded with trust_remote_code=True. Allows execution of arbitrary code from the model repository.',
171
+ fix: 'Set trust_remote_code=False. Audit model code before enabling remote code execution.',
172
+ },
173
+ ];
174
+
175
+ // =============================================================================
176
+ // RAG SECURITY AGENT
177
+ // =============================================================================
178
+
179
+ export class RAGSecurityAgent extends BaseAgent {
180
+ constructor() {
181
+ super(
182
+ 'RAGSecurityAgent',
183
+ 'Detect RAG security vulnerabilities — poisoning, embedding exposure, tenant isolation gaps',
184
+ 'llm'
185
+ );
186
+ }
187
+
188
+ async analyze(context) {
189
+ const { files } = context;
190
+
191
+ const codeFiles = files.filter(f => {
192
+ const ext = path.extname(f).toLowerCase();
193
+ return ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.rb', '.go', '.java'].includes(ext);
194
+ });
195
+
196
+ let findings = [];
197
+ for (const file of codeFiles) {
198
+ findings = findings.concat(this.scanFileWithPatterns(file, PATTERNS));
199
+ }
200
+ return findings;
201
+ }
202
+ }
203
+
204
+ export default RAGSecurityAgent;
@@ -108,7 +108,11 @@ export class SBOMGenerator {
108
108
  } catch { /* skip */ }
109
109
  }
110
110
 
111
- // ── Build CycloneDX BOM ───────────────────────────────────────────────────
111
+ // ── Detect licenses from lock files ─────────────────────────────────────
112
+ const licenses = this._detectLicenses(rootPath);
113
+
114
+ // ── Build CycloneDX BOM (CRA-enhanced) ──────────────────────────────────
115
+ const projectMeta = this.getProjectMetadata(rootPath);
112
116
  const bom = {
113
117
  bomFormat: 'CycloneDX',
114
118
  specVersion: '1.5',
@@ -119,23 +123,57 @@ export class SBOMGenerator {
119
123
  tools: [{
120
124
  vendor: 'ship-safe',
121
125
  name: 'ship-safe',
122
- version: '4.0.0',
126
+ version: '5.0.0',
123
127
  }],
124
- component: this.getProjectMetadata(rootPath),
128
+ component: projectMeta,
129
+ // EU CRA: supplier identification
130
+ supplier: this._getSupplier(rootPath),
131
+ // EU CRA: lifecycle phase
132
+ lifecycles: [{ phase: 'build' }],
125
133
  },
126
- components: components.map((c, i) => ({
127
- 'bom-ref': `component-${i}`,
128
- type: c.type,
129
- name: c.name,
130
- version: c.version,
131
- purl: c.purl,
132
- scope: c.scope,
133
- })),
134
+ components: components.map((c, i) => {
135
+ const comp = {
136
+ 'bom-ref': `component-${i}`,
137
+ type: c.type,
138
+ name: c.name,
139
+ version: c.version,
140
+ purl: c.purl,
141
+ scope: c.scope,
142
+ };
143
+ // EU CRA: attach license if known
144
+ const lic = licenses[c.name];
145
+ if (lic) {
146
+ comp.licenses = [{ license: { id: lic } }];
147
+ }
148
+ return comp;
149
+ }),
150
+ // EU CRA: vulnerability disclosure info
151
+ vulnerabilities: [],
134
152
  };
135
153
 
136
154
  return bom;
137
155
  }
138
156
 
157
+ /**
158
+ * Attach known vulnerabilities to the SBOM (CRA requirement).
159
+ */
160
+ attachVulnerabilities(bom, depVulns = []) {
161
+ bom.vulnerabilities = depVulns.map((v, i) => ({
162
+ 'bom-ref': `vuln-${i}`,
163
+ id: v.id || v.package || `VULN-${i}`,
164
+ source: { name: 'ship-safe' },
165
+ ratings: [{
166
+ severity: v.severity || 'unknown',
167
+ method: 'other',
168
+ }],
169
+ description: v.description || '',
170
+ affects: [{
171
+ ref: v.package || 'unknown',
172
+ }],
173
+ }));
174
+ return bom;
175
+ }
176
+
139
177
  /**
140
178
  * Generate SBOM and write to file.
141
179
  */
@@ -165,6 +203,57 @@ export class SBOMGenerator {
165
203
  };
166
204
  }
167
205
 
206
+ /**
207
+ * EU CRA: Extract supplier info from package.json.
208
+ */
209
+ _getSupplier(rootPath) {
210
+ const pkgPath = path.join(rootPath, 'package.json');
211
+ try {
212
+ if (fs.existsSync(pkgPath)) {
213
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
214
+ const author = typeof pkg.author === 'string' ? pkg.author
215
+ : pkg.author?.name || pkg.author?.email || null;
216
+ if (author) {
217
+ return { name: author, url: [pkg.homepage || pkg.repository?.url || ''].filter(Boolean) };
218
+ }
219
+ }
220
+ } catch { /* skip */ }
221
+ return { name: 'Unknown' };
222
+ }
223
+
224
+ /**
225
+ * Detect licenses from node_modules (best-effort).
226
+ * Returns { packageName: 'MIT' | 'ISC' | ... }
227
+ */
228
+ _detectLicenses(rootPath) {
229
+ const licenses = {};
230
+ const nodeModules = path.join(rootPath, 'node_modules');
231
+ const pkgPath = path.join(rootPath, 'package.json');
232
+
233
+ if (!fs.existsSync(pkgPath)) return licenses;
234
+
235
+ try {
236
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
237
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
238
+
239
+ for (const name of Object.keys(allDeps)) {
240
+ const depPkgPath = path.join(nodeModules, name, 'package.json');
241
+ try {
242
+ if (fs.existsSync(depPkgPath)) {
243
+ const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf-8'));
244
+ if (depPkg.license) {
245
+ licenses[name] = typeof depPkg.license === 'string'
246
+ ? depPkg.license
247
+ : depPkg.license.type || 'UNKNOWN';
248
+ }
249
+ }
250
+ } catch { /* skip */ }
251
+ }
252
+ } catch { /* skip */ }
253
+
254
+ return licenses;
255
+ }
256
+
168
257
  uuid() {
169
258
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
170
259
  const r = Math.random() * 16 | 0;
@@ -35,6 +35,10 @@ const FALLBACK_CATEGORY_MAP = {
35
35
  'history': 'secrets',
36
36
  'cicd': 'config',
37
37
  'mobile': 'injection',
38
+ 'privacy': 'config',
39
+ 'mcp': 'llm',
40
+ 'agentic': 'llm',
41
+ 'rag': 'llm',
38
42
  'recon': null, // skip recon findings
39
43
  };
40
44
 
@@ -0,0 +1,154 @@
1
+ /**
2
+ * SupabaseRLSAgent
3
+ * =================
4
+ *
5
+ * Detects missing or weak Row Level Security (RLS) in Supabase projects.
6
+ * Checks SQL migrations, client-side service_role key usage,
7
+ * unprotected storage operations, and anon-key data mutations.
8
+ */
9
+
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import { BaseAgent, createFinding } from './base-agent.js';
13
+
14
+ // Patterns for client-side code
15
+ const CLIENT_PATTERNS = [
16
+ {
17
+ rule: 'SUPABASE_SERVICE_KEY_CLIENT',
18
+ title: 'Supabase: Service Role Key in Client Code',
19
+ regex: /SUPABASE_SERVICE_ROLE_KEY|service_role_key|serviceRoleKey|supabaseAdmin/g,
20
+ severity: 'critical',
21
+ cwe: 'CWE-798',
22
+ owasp: 'A07:2021',
23
+ description: 'Service role key bypasses RLS entirely. Never expose it in client-side code.',
24
+ fix: 'Use the anon key on the client. Move service_role operations to a backend/edge function.',
25
+ },
26
+ {
27
+ rule: 'SUPABASE_RLS_DISABLED',
28
+ title: 'Supabase: RLS Bypass via .rpc() or Admin Client',
29
+ regex: /\.rpc\s*\(\s*['"][^'"]+['"]/g,
30
+ severity: 'high',
31
+ cwe: 'CWE-284',
32
+ owasp: 'A01:2021',
33
+ confidence: 'medium',
34
+ description: 'Supabase .rpc() calls execute database functions that may bypass RLS policies.',
35
+ fix: 'Ensure the underlying SQL function uses SECURITY DEFINER carefully, or set search_path.',
36
+ },
37
+ {
38
+ rule: 'SUPABASE_PUBLIC_ANON_INSERT',
39
+ title: 'Supabase: Unguarded Insert/Update/Delete',
40
+ regex: /supabase\s*\.from\s*\(\s*['"][^'"]+['"]\s*\)\s*\.(?:insert|update|delete|upsert)\s*\(/g,
41
+ severity: 'high',
42
+ cwe: 'CWE-284',
43
+ owasp: 'A01:2021',
44
+ confidence: 'medium',
45
+ description: 'Supabase data mutation without visible auth check. Ensure RLS policies protect this table.',
46
+ fix: 'Verify RLS is enabled on the table and policies restrict mutations to authenticated users.',
47
+ },
48
+ {
49
+ rule: 'SUPABASE_UNPROTECTED_STORAGE',
50
+ title: 'Supabase: Storage Operation Without Auth',
51
+ regex: /supabase\s*\.storage\s*\.from\s*\(\s*['"][^'"]+['"]\s*\)\s*\.(?:upload|remove|move|createSignedUrl|list)\s*\(/g,
52
+ severity: 'medium',
53
+ cwe: 'CWE-284',
54
+ owasp: 'A01:2021',
55
+ confidence: 'medium',
56
+ description: 'Supabase storage operation detected. Ensure storage policies restrict access.',
57
+ fix: 'Configure storage bucket policies to require authentication.',
58
+ },
59
+ ];
60
+
61
+ // Client-side directories (findings here are more severe)
62
+ const CLIENT_DIRS = /(?:^|[/\\])(?:src|pages|app|components|hooks|lib|utils)[/\\]/i;
63
+
64
+ export class SupabaseRLSAgent extends BaseAgent {
65
+ constructor() {
66
+ super('SupabaseRLSAgent', 'Supabase Row Level Security audit', 'auth');
67
+ }
68
+
69
+ shouldRun(recon) {
70
+ return recon?.databases?.includes('supabase') ||
71
+ recon?.authPatterns?.includes('supabase-auth') ||
72
+ false;
73
+ }
74
+
75
+ async analyze(context) {
76
+ const { rootPath, files } = context;
77
+ let findings = [];
78
+
79
+ // ── 1. Scan client-side code for Supabase security issues ─────────────────
80
+ const codeFiles = files.filter(f => {
81
+ const ext = path.extname(f).toLowerCase();
82
+ return ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.vue', '.svelte'].includes(ext);
83
+ });
84
+
85
+ for (const file of codeFiles) {
86
+ const fileFindings = this.scanFileWithPatterns(file, CLIENT_PATTERNS);
87
+ // Elevate severity for findings in client-side directories
88
+ const relPath = path.relative(rootPath, file).replace(/\\/g, '/');
89
+ if (CLIENT_DIRS.test(relPath)) {
90
+ for (const f of fileFindings) {
91
+ if (f.rule === 'SUPABASE_SERVICE_KEY_CLIENT') {
92
+ f.severity = 'critical';
93
+ }
94
+ }
95
+ }
96
+ findings = findings.concat(fileFindings);
97
+ }
98
+
99
+ // ── 2. Scan SQL migrations for missing RLS ────────────────────────────────
100
+ const sqlFiles = files.filter(f => path.extname(f).toLowerCase() === '.sql');
101
+ const tablesWithRLS = new Set();
102
+ const tablesWithoutRLS = [];
103
+
104
+ for (const file of sqlFiles) {
105
+ const content = this.readFile(file);
106
+ if (!content) continue;
107
+
108
+ // Find tables that have RLS enabled
109
+ const rlsMatches = content.matchAll(/ALTER\s+TABLE\s+(?:(?:public|auth|storage)\.)?["']?(\w+)["']?\s+ENABLE\s+ROW\s+LEVEL\s+SECURITY/gi);
110
+ for (const m of rlsMatches) {
111
+ tablesWithRLS.add(m[1].toLowerCase());
112
+ }
113
+
114
+ // Find CREATE TABLE statements
115
+ const createMatches = content.matchAll(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:(?:public|auth|storage)\.)?["']?(\w+)["']?/gi);
116
+ for (const m of createMatches) {
117
+ const tableName = m[1].toLowerCase();
118
+ // Skip Supabase internal tables
119
+ if (['_prisma_migrations', 'schema_migrations', 'knex_migrations'].includes(tableName)) continue;
120
+
121
+ // Check if RLS is enabled in the same file
122
+ const rlsInFile = new RegExp(
123
+ `ALTER\\s+TABLE\\s+(?:(?:public|auth|storage)\\.)?["']?${tableName}["']?\\s+ENABLE\\s+ROW\\s+LEVEL\\s+SECURITY`,
124
+ 'gi'
125
+ ).test(content);
126
+
127
+ if (!rlsInFile && !tablesWithRLS.has(tableName)) {
128
+ tablesWithoutRLS.push({ table: tableName, file });
129
+ }
130
+ }
131
+ }
132
+
133
+ // Report tables missing RLS
134
+ for (const { table, file } of tablesWithoutRLS) {
135
+ // Double-check across all SQL files
136
+ if (tablesWithRLS.has(table)) continue;
137
+ findings.push(createFinding({
138
+ file,
139
+ line: 0,
140
+ severity: 'critical',
141
+ category: 'auth',
142
+ rule: 'SUPABASE_NO_RLS_POLICY',
143
+ title: `Supabase: Table "${table}" Missing RLS`,
144
+ description: `Table "${table}" is created without enabling Row Level Security. Any user with the anon key can read/write all rows.`,
145
+ matched: `CREATE TABLE ${table}`,
146
+ fix: `Add: ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY;\nThen create appropriate policies with CREATE POLICY.`,
147
+ }));
148
+ }
149
+
150
+ return findings;
151
+ }
152
+ }
153
+
154
+ export default SupabaseRLSAgent;