auditai-scan 0.6.1 → 0.7.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.
Files changed (2) hide show
  1. package/dist/auditai-scan.mjs +454 -117
  2. package/package.json +1 -1
@@ -138,6 +138,9 @@ function where(f) {
138
138
  for (const e of f.evidence) for (const l of e.locations ?? []) seen.add(`${l.file}:${l.line}`);
139
139
  return [...seen].join(", ");
140
140
  }
141
+ function fixBody(diff) {
142
+ return diff.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).map((l) => l.slice(1));
143
+ }
141
144
  function formatFinding(f) {
142
145
  const why = f.evidence.find((e) => e.kind === "rule")?.summary ?? "";
143
146
  const lines = [
@@ -152,6 +155,10 @@ function formatFinding(f) {
152
155
  const s = [...f.evidence].reverse().find((e) => e.data?.suppressed === true);
153
156
  if (s) lines.push(` Ignored ${s.summary}`);
154
157
  }
158
+ if (f.fix) {
159
+ lines.push(` Fix ${f.fix.summary} (${f.fix.touchedFiles.join(", ")})`);
160
+ for (const line of fixBody(f.fix.diff)) lines.push(` ${line}`);
161
+ }
155
162
  return lines.join("\n");
156
163
  }
157
164
  function formatScanText(r) {
@@ -190,6 +197,222 @@ function formatScanText(r) {
190
197
  // packages/scanner/src/scan.ts
191
198
  import { statSync as statSync2 } from "node:fs";
192
199
 
200
+ // packages/fixes/dist/sql-fixes.js
201
+ var OWNER_COLUMNS = [
202
+ "user_id",
203
+ "owner_id",
204
+ "profile_id",
205
+ "author_id",
206
+ "created_by",
207
+ "account_id",
208
+ "tenant_id",
209
+ "organization_id",
210
+ "org_id",
211
+ "workspace_id",
212
+ "team_id"
213
+ ];
214
+ var API_ROLES = "public, anon, authenticated";
215
+ function table(model, name) {
216
+ if (!name)
217
+ return void 0;
218
+ const key = name.toLowerCase();
219
+ return model.tables.find((t) => t.table.toLowerCase() === key);
220
+ }
221
+ function ownerColumn(t) {
222
+ if (!t)
223
+ return null;
224
+ const cols = t.columns.map((c) => c.toLowerCase());
225
+ for (const candidate of OWNER_COLUMNS)
226
+ if (cols.includes(candidate))
227
+ return candidate;
228
+ return null;
229
+ }
230
+ function qualified(name) {
231
+ return name.includes(".") ? name : `public.${name}`;
232
+ }
233
+ function fn(model, name) {
234
+ if (!name)
235
+ return void 0;
236
+ const key = name.toLowerCase();
237
+ return (model.sqlFunctions ?? []).find((f) => f.name.toLowerCase() === key);
238
+ }
239
+ function signatureOf(f) {
240
+ const name = f.name.includes(".") ? f.name : `public.${f.name}`;
241
+ return f.args === void 0 ? `${name}(...)` : `${name}(${f.args})`;
242
+ }
243
+ var HEADER = (title) => `-- ${title}
244
+ -- Proposed by Audit AI. Read it, then apply it with the rest of your migrations.
245
+ `;
246
+ function sqlFunctionFix(finding4, model) {
247
+ const name = finding4.evidence[0]?.data?.function;
248
+ const f = fn(model, typeof name === "string" ? name : void 0);
249
+ if (!f)
250
+ return null;
251
+ const sig = signatureOf(f);
252
+ const ambiguous = sig.endsWith("(...)");
253
+ const body = [
254
+ HEADER(`Stop anon and authenticated from calling ${f.name} directly`),
255
+ ambiguous ? "-- The argument types could not be read from the migrations; put the real signature in\n-- place of (...) before applying. `\\df public.*` in psql prints it.\n" : "",
256
+ `revoke execute on function ${sig} from ${API_ROLES};
257
+ `,
258
+ "-- Leave this line out if nothing calls the function with the service role.\n",
259
+ `grant execute on function ${sig} to service_role;
260
+ `
261
+ ].join("");
262
+ return {
263
+ file: `fix_revoke_execute_${f.name.replace(/[^a-z0-9]+/gi, "_").toLowerCase()}.sql`,
264
+ sql: body,
265
+ summary: `Revoke execute on ${f.name} from the API roles`,
266
+ rationale: "The function runs as its creator, so row level security does not apply inside it, and Supabase grants execute on new functions in schema public to anon and authenticated directly. Revoking from PUBLIC alone does not remove those grants, which is why a function that looks locked down is still callable with the public key. If a browser is supposed to call it, do not apply this: add a caller check inside the body instead (`where owner_id = (select auth.uid())`)."
267
+ };
268
+ }
269
+ function enableRlsFix(finding4, model, withPolicy) {
270
+ const name = finding4.evidence[0]?.data?.table;
271
+ if (typeof name !== "string")
272
+ return null;
273
+ const t = table(model, name);
274
+ const full = qualified(name);
275
+ const owner = ownerColumn(t);
276
+ const lines = [HEADER(`Turn on row level security for ${full}`)];
277
+ lines.push(`alter table ${full} enable row level security;
278
+ `);
279
+ if (withPolicy) {
280
+ if (owner) {
281
+ lines.push(`
282
+ create policy "${name}: owner reads" on ${full}
283
+ for select to authenticated
284
+ using (${owner} = (select auth.uid()));
285
+ `, `
286
+ create policy "${name}: owner writes" on ${full}
287
+ for all to authenticated
288
+ using (${owner} = (select auth.uid()))
289
+ with check (${owner} = (select auth.uid()));
290
+ `);
291
+ } else {
292
+ lines.push(`
293
+ -- No column of ${full} ties a row to a person (looked for ${OWNER_COLUMNS.slice(0, 4).join(", ")}\u2026),
294
+ -- so no policy is proposed: with RLS on and no policy the table is readable only with the
295
+ -- service role, which is the safe default. Add a policy once you decide who owns a row.
296
+ `);
297
+ }
298
+ } else {
299
+ lines.push(`
300
+ -- The policies this table already has start applying the moment row level security is on;
301
+ -- read them once before applying, because until now they have never run.
302
+ `);
303
+ }
304
+ return {
305
+ file: `fix_enable_rls_${name.replace(/[^a-z0-9]+/gi, "_").toLowerCase()}.sql`,
306
+ sql: lines.join(""),
307
+ summary: `Enable row level security on ${full}`,
308
+ rationale: withPolicy ? "Without row level security the anon key reads and writes every row of the table through PostgREST. Turning it on denies everything by default, so apply this together with a policy, and check that your own server code uses the service role where it needs full access." : "The table already carries policies, which is what makes this a defect rather than a choice: they have no effect until row level security is on. Read them once before applying \u2014 they have never run, so they may not say what their author believed."
309
+ };
310
+ }
311
+ function anonWriteFix(finding4, model) {
312
+ const data = finding4.evidence[0]?.data ?? {};
313
+ const name = data.table;
314
+ const policy = data.policy;
315
+ const command = data.command;
316
+ if (typeof name !== "string" || typeof policy !== "string")
317
+ return null;
318
+ const full = qualified(name);
319
+ const owner = ownerColumn(table(model, name));
320
+ const cmd = typeof command === "string" ? command : "all";
321
+ const safeName = policy.replace(/"/g, '""');
322
+ const lines = [HEADER(`Close the open write policy "${policy}" on ${full}`)];
323
+ if (owner) {
324
+ lines.push(`drop policy "${safeName}" on ${full};
325
+ `, `
326
+ create policy "${safeName}" on ${full}
327
+ for ${cmd} to authenticated
328
+ `, cmd === "insert" ? ` with check (${owner} = (select auth.uid()));
329
+ ` : ` using (${owner} = (select auth.uid()))${cmd === "all" ? `
330
+ with check (${owner} = (select auth.uid()))` : ""};
331
+ `);
332
+ } else {
333
+ lines.push(`-- No column of ${full} ties a row to a person, so there is nothing to compare the caller
334
+ -- with. Either add one, or take the policy away and let your server write the table with the
335
+ -- service role after it has checked the caller itself.
336
+ `, `drop policy "${safeName}" on ${full};
337
+ `);
338
+ }
339
+ return {
340
+ file: `fix_policy_${name.replace(/[^a-z0-9]+/gi, "_").toLowerCase()}_${cmd}.sql`,
341
+ sql: lines.join(""),
342
+ summary: `Tie the write policy on ${full} to the caller`,
343
+ rationale: owner ? `The policy decides with a tautology and is open to anon, so anyone holding the public key can write ${full} straight through PostgREST. The replacement keeps the same command and ties the row to the signed-in caller through ${owner}. If this table is meant to accept rows from strangers (a contact form, a newsletter), keep the insert open but give it a predicate on the row's shape and a rate limit.` : `The policy decides with a tautology and is open to anon, so anyone holding the public key can write ${full} straight through PostgREST. Nothing in the table identifies an owner, so the honest fix is to remove the policy and write the table from your server after it has checked the caller.`
344
+ };
345
+ }
346
+ function userMetadataFix(finding4) {
347
+ const data = finding4.evidence[0]?.data ?? {};
348
+ const name = data.table;
349
+ const policy = data.policy;
350
+ if (typeof name !== "string" || typeof policy !== "string")
351
+ return null;
352
+ const full = qualified(name);
353
+ return {
354
+ file: `fix_policy_${name.replace(/[^a-z0-9]+/gi, "_").toLowerCase()}_app_metadata.sql`,
355
+ sql: [
356
+ HEADER(`Stop the policy "${policy}" on ${full} from trusting user_metadata`),
357
+ "-- Re-create the policy with the claim read from app_metadata, which only the service role\n",
358
+ "-- writes. Copy the predicate from your own migration and change the one word: replace\n",
359
+ "-- (select auth.jwt()) -> 'user_metadata' ->> '<claim>'\n",
360
+ "-- with\n",
361
+ "-- (select auth.jwt()) -> 'app_metadata' ->> '<claim>'\n",
362
+ `--
363
+ -- drop policy "${policy.replace(/"/g, '""')}" on ${full};
364
+ `,
365
+ `-- create policy "${policy.replace(/"/g, '""')}" on ${full} ... using (...);
366
+ `,
367
+ "\n-- Then set the claim where the user cannot reach it, from a server with the service role:\n",
368
+ "-- await admin.auth.admin.updateUserById(id, { app_metadata: { is_admin: true } });\n"
369
+ ].join(""),
370
+ summary: `Move the claim behind "${policy}" from user_metadata to app_metadata`,
371
+ rationale: "user_metadata is written by the user themselves with supabase.auth.updateUser({ data }), and it is copied into their next access token without review, so a policy that reads it grants itself. app_metadata can only be set with the service role. This fix is a template rather than a finished statement: the predicate belongs to your policy, and moving the claim only helps once something server-side actually sets app_metadata."
372
+ };
373
+ }
374
+ function sqlFixFor(finding4, model) {
375
+ switch (finding4.ruleId) {
376
+ case "supabase.security-definer-function-without-caller-check":
377
+ return sqlFunctionFix(finding4, model);
378
+ case "supabase.table-without-rls":
379
+ return enableRlsFix(finding4, model, true);
380
+ case "supabase.policies-without-rls-enabled":
381
+ return enableRlsFix(finding4, model, false);
382
+ case "supabase.anon-write-policy":
383
+ return anonWriteFix(finding4, model);
384
+ case "supabase.rls-policy-trusts-user-metadata":
385
+ return userMetadataFix(finding4);
386
+ default:
387
+ return null;
388
+ }
389
+ }
390
+ function addFileDiff(path, body) {
391
+ const lines = body.replace(/\n$/, "").split("\n");
392
+ return [
393
+ `diff --git a/${path} b/${path}`,
394
+ "new file mode 100644",
395
+ "--- /dev/null",
396
+ `+++ b/${path}`,
397
+ `@@ -0,0 +1,${lines.length} @@`,
398
+ ...lines.map((l) => `+${l}`),
399
+ ""
400
+ ].join("\n");
401
+ }
402
+ function deterministicFix(finding4, model, migrationsDir = "supabase/migrations") {
403
+ const fix = sqlFixFor(finding4, model);
404
+ if (!fix)
405
+ return null;
406
+ const stamp = (finding4.createdAt ?? "").replace(/\D/g, "").slice(0, 14) || "00000000000000";
407
+ const path = `${migrationsDir}/${stamp}_${fix.file}`;
408
+ return {
409
+ summary: fix.summary,
410
+ diff: addFileDiff(path, fix.sql),
411
+ touchedFiles: [path],
412
+ rationale: fix.rationale
413
+ };
414
+ }
415
+
193
416
  // packages/graph/src/graph.ts
194
417
  var SecurityGraph = class {
195
418
  nodes = /* @__PURE__ */ new Map();
@@ -221,10 +444,10 @@ var SecurityGraph = class {
221
444
  function buildGraph(model) {
222
445
  const g = new SecurityGraph();
223
446
  const tableInfo = new Map(model.tables.map((t) => [t.table, t]));
224
- const tableNode = (table) => {
225
- const info = tableInfo.get(table.toLowerCase());
447
+ const tableNode = (table2) => {
448
+ const info = tableInfo.get(table2.toLowerCase());
226
449
  const data = {
227
- table,
450
+ table: table2,
228
451
  known: info !== void 0,
229
452
  rlsEnabled: info?.rlsEnabled ?? false,
230
453
  policies: info?.policies ?? [],
@@ -233,19 +456,19 @@ function buildGraph(model) {
233
456
  };
234
457
  const node = g.addNode(
235
458
  info ? {
236
- id: `table:${table}`,
459
+ id: `table:${table2}`,
237
460
  kind: "Table",
238
- label: `public.${table}`,
461
+ label: `public.${table2}`,
239
462
  data: { ...data },
240
463
  location: info.location
241
- } : { id: `table:${table}`, kind: "Table", label: `public.${table}`, data: { ...data } }
464
+ } : { id: `table:${table2}`, kind: "Table", label: `public.${table2}`, data: { ...data } }
242
465
  );
243
466
  for (const p of data.policies) {
244
467
  const pn = g.addNode({
245
- id: `policy:${table}:${p}`,
468
+ id: `policy:${table2}:${p}`,
246
469
  kind: "RLSPolicy",
247
470
  label: p,
248
- data: { table, name: p }
471
+ data: { table: table2, name: p }
249
472
  });
250
473
  g.addEdge(node.id, pn.id, "GUARDED_BY");
251
474
  }
@@ -746,11 +969,11 @@ function walkOwn(body, visit) {
746
969
  };
747
970
  go(body);
748
971
  }
749
- function ownReturns(fn) {
750
- if (!fn.body) return [];
751
- if (!ts.isBlock(fn.body)) return [fn.body];
972
+ function ownReturns(fn2) {
973
+ if (!fn2.body) return [];
974
+ if (!ts.isBlock(fn2.body)) return [fn2.body];
752
975
  const out = [];
753
- walkOwn(fn.body, (n) => {
976
+ walkOwn(fn2.body, (n) => {
754
977
  if (ts.isReturnStatement(n) && n.expression) out.push(n.expression);
755
978
  });
756
979
  return out;
@@ -921,10 +1144,10 @@ function growNames(decls, scope) {
921
1144
  if (scope.names.size === before) break;
922
1145
  }
923
1146
  }
924
- function ownDeclarations(fn) {
1147
+ function ownDeclarations(fn2) {
925
1148
  const out = [];
926
- if (fn.body) {
927
- walkOwn(fn.body, (n) => {
1149
+ if (fn2.body) {
1150
+ walkOwn(fn2.body, (n) => {
928
1151
  if (ts3.isVariableDeclaration(n)) out.push(n);
929
1152
  });
930
1153
  }
@@ -953,10 +1176,10 @@ function moduleSecretScope(sf) {
953
1176
  moduleScopes.set(sf, scope);
954
1177
  return scope;
955
1178
  }
956
- function secretScopeFor(fn, sf) {
1179
+ function secretScopeFor(fn2, sf) {
957
1180
  const mod = moduleSecretScope(sf);
958
1181
  const scope = { names: new Set(mod.names), fns: mod.fns };
959
- growNames(ownDeclarations(fn), scope);
1182
+ growNames(ownDeclarations(fn2), scope);
960
1183
  return scope;
961
1184
  }
962
1185
  var STORED_SECRET_FIELD = /(^|_)(secret|signing_key|api_key|key_hash|token_hash|hmac_key)$/;
@@ -1077,10 +1300,10 @@ function gatesByThrow(node, fnBody) {
1077
1300
  }
1078
1301
  return false;
1079
1302
  }
1080
- function secretChecksIn(fn, sf) {
1081
- if (!fn.body) return [];
1082
- const body = fn.body;
1083
- const secrets = secretScopeFor(fn, sf);
1303
+ function secretChecksIn(fn2, sf) {
1304
+ if (!fn2.body) return [];
1305
+ const body = fn2.body;
1306
+ const secrets = secretScopeFor(fn2, sf);
1084
1307
  const secretish = (e) => isSecretish(e, secrets);
1085
1308
  const builtFromSecret = instancesBuiltFromSecret(body, secretish);
1086
1309
  const out = [];
@@ -1193,8 +1416,8 @@ function mentions(cond, names) {
1193
1416
  return false;
1194
1417
  }
1195
1418
  function ifsAfter(node) {
1196
- const fn = enclosingFunction(node);
1197
- const scope = fn ? fn.body : node.getSourceFile();
1419
+ const fn2 = enclosingFunction(node);
1420
+ const scope = fn2 ? fn2.body : node.getSourceFile();
1198
1421
  const out = [];
1199
1422
  if (!scope) return out;
1200
1423
  walkOwn(scope, (n) => {
@@ -2461,7 +2684,9 @@ function applyCreateFunction(reg, stmt, file) {
2461
2684
  if (!isWord(tk[i], "function")) return;
2462
2685
  const q = readQualifiedName(tk, i + 1);
2463
2686
  if (!q || !isPunct(tk[q.next], "(")) return;
2464
- i = groupEnd(tk, q.next) + 1;
2687
+ const argsEnd = groupEnd(tk, q.next);
2688
+ const args = argumentTypes(stmt, tk, q.next, argsEnd);
2689
+ i = argsEnd + 1;
2465
2690
  let securityDefiner = false;
2466
2691
  let returns = null;
2467
2692
  let body = "";
@@ -2494,17 +2719,122 @@ function applyCreateFunction(reg, stmt, file) {
2494
2719
  }
2495
2720
  const key = qualifiedKey(q);
2496
2721
  const code = maskSqlComments(body);
2497
- const fn = {
2722
+ const fn2 = {
2498
2723
  schema: q.schema,
2499
2724
  name: q.name,
2500
2725
  securityDefiner,
2501
2726
  returns,
2727
+ args,
2502
2728
  body: code,
2503
2729
  directCheck: CALLER_CHECKS.some((re) => re.test(code)),
2504
2730
  acl: reg.byKey.get(key)?.acl ?? initialAcl(reg, q.schema),
2505
2731
  location: { file, line: stmt.line }
2506
2732
  };
2507
- reg.byKey.set(key, fn);
2733
+ reg.byKey.set(key, fn2);
2734
+ }
2735
+ var TYPE_WORD = /* @__PURE__ */ new Set([
2736
+ "anyarray",
2737
+ "anyelement",
2738
+ "bigint",
2739
+ "bigserial",
2740
+ "bit",
2741
+ "bool",
2742
+ "boolean",
2743
+ "box",
2744
+ "bytea",
2745
+ "char",
2746
+ "character",
2747
+ "cidr",
2748
+ "circle",
2749
+ "date",
2750
+ "decimal",
2751
+ "double",
2752
+ "float",
2753
+ "float4",
2754
+ "float8",
2755
+ "inet",
2756
+ "int",
2757
+ "int2",
2758
+ "int4",
2759
+ "int8",
2760
+ "integer",
2761
+ "interval",
2762
+ "json",
2763
+ "jsonb",
2764
+ "line",
2765
+ "lseg",
2766
+ "macaddr",
2767
+ "money",
2768
+ "name",
2769
+ "numeric",
2770
+ "oid",
2771
+ "path",
2772
+ "point",
2773
+ "polygon",
2774
+ "real",
2775
+ "record",
2776
+ "regclass",
2777
+ "serial",
2778
+ "smallint",
2779
+ "smallserial",
2780
+ "text",
2781
+ "time",
2782
+ "timestamp",
2783
+ "timestamptz",
2784
+ "timetz",
2785
+ "trigger",
2786
+ "tsquery",
2787
+ "tsvector",
2788
+ "uuid",
2789
+ "varbit",
2790
+ "varchar",
2791
+ "void",
2792
+ "xml"
2793
+ ]);
2794
+ function argumentTypes(stmt, tokens, open, close) {
2795
+ if (close <= open + 1) return "";
2796
+ const parts = [];
2797
+ let depth = 0;
2798
+ let start = open + 1;
2799
+ const pieces = [];
2800
+ for (let i = open + 1; i < close; i++) {
2801
+ const t = tokens[i];
2802
+ if (isPunct(t, "(") || isPunct(t, "[")) depth += 1;
2803
+ else if (isPunct(t, ")") || isPunct(t, "]")) depth -= 1;
2804
+ else if (depth === 0 && isPunct(t, ",")) {
2805
+ pieces.push([start, i]);
2806
+ start = i + 1;
2807
+ }
2808
+ }
2809
+ pieces.push([start, close]);
2810
+ for (const [from, to] of pieces) {
2811
+ const words = [];
2812
+ for (let i = from; i < to; i++) {
2813
+ const t = tokens[i];
2814
+ if (!t) continue;
2815
+ if (isWord(t, "default")) break;
2816
+ if (t.kind === "punct" && t.value === "=") break;
2817
+ words.push(t);
2818
+ }
2819
+ if (words.length === 0) return null;
2820
+ let k = 0;
2821
+ if (isWord(words[k], "in") || isWord(words[k], "out") || isWord(words[k], "inout")) {
2822
+ if (isWord(words[k], "out")) continue;
2823
+ k += 1;
2824
+ } else if (isWord(words[k], "variadic")) {
2825
+ k += 1;
2826
+ }
2827
+ const firstWord = words[k];
2828
+ const named = firstWord !== void 0 && firstWord.kind === "word" && !TYPE_WORD.has(firstWord.value.toLowerCase()) && words.slice(k + 1).some((w) => w.kind === "word");
2829
+ const typeStart = named ? k + 1 : k;
2830
+ const first = words[typeStart];
2831
+ const last = words[words.length - 1];
2832
+ if (!first || !last) return null;
2833
+ parts.push(
2834
+ stmt.text.slice(first.start - stmt.start, last.end - stmt.start).replace(/\s+/g, " ").trim().toLowerCase()
2835
+ );
2836
+ }
2837
+ return parts.join(", ");
2508
2838
  }
2509
2839
  function readFunctionList(tokens, from) {
2510
2840
  const keys = [];
@@ -2530,28 +2860,28 @@ function applyAlterFunction(reg, stmt) {
2530
2860
  const tk = stmt.tokens;
2531
2861
  const list = readFunctionList(tk, 2);
2532
2862
  const key = list.keys[0];
2533
- const fn = key === void 0 ? void 0 : reg.byKey.get(key);
2534
- if (!fn || key === void 0) return;
2863
+ const fn2 = key === void 0 ? void 0 : reg.byKey.get(key);
2864
+ if (!fn2 || key === void 0) return;
2535
2865
  let i = list.next;
2536
2866
  if (isWord(tk[i], "external")) i += 1;
2537
2867
  if (isWord(tk[i], "security")) {
2538
- if (isWord(tk[i + 1], "definer")) fn.securityDefiner = true;
2539
- else if (isWord(tk[i + 1], "invoker")) fn.securityDefiner = false;
2868
+ if (isWord(tk[i + 1], "definer")) fn2.securityDefiner = true;
2869
+ else if (isWord(tk[i + 1], "invoker")) fn2.securityDefiner = false;
2540
2870
  return;
2541
2871
  }
2542
2872
  let moved = null;
2543
2873
  if (isWord(tk[i], "rename") && isWord(tk[i + 1], "to")) {
2544
2874
  const name = identOf(tk[i + 2]);
2545
- if (name) moved = { schema: fn.schema, name, next: 0 };
2875
+ if (name) moved = { schema: fn2.schema, name, next: 0 };
2546
2876
  } else if (isWord(tk[i], "set") && isWord(tk[i + 1], "schema")) {
2547
2877
  const schema = identOf(tk[i + 2]);
2548
- if (schema) moved = { schema, name: fn.name, next: 0 };
2878
+ if (schema) moved = { schema, name: fn2.name, next: 0 };
2549
2879
  }
2550
2880
  if (!moved) return;
2551
2881
  reg.byKey.delete(key);
2552
- fn.schema = moved.schema;
2553
- fn.name = moved.name;
2554
- reg.byKey.set(qualifiedKey(moved), fn);
2882
+ fn2.schema = moved.schema;
2883
+ fn2.name = moved.name;
2884
+ reg.byKey.set(qualifiedKey(moved), fn2);
2555
2885
  }
2556
2886
  function readRoles(tokens, from) {
2557
2887
  const roles = [];
@@ -2606,8 +2936,8 @@ function applyGrantRevoke(reg, stmt) {
2606
2936
  if (isWord(tk[i], "function") || isWord(tk[i], "routine")) {
2607
2937
  const list = readFunctionList(tk, i + 1);
2608
2938
  targets = list.keys.flatMap((k) => {
2609
- const fn = reg.byKey.get(k);
2610
- return fn ? [fn] : [];
2939
+ const fn2 = reg.byKey.get(k);
2940
+ return fn2 ? [fn2] : [];
2611
2941
  });
2612
2942
  i = list.next;
2613
2943
  } else if (isWord(tk[i], "all") && (isWord(tk[i + 1], "functions") || isWord(tk[i + 1], "routines")) && isWord(tk[i + 2], "in") && isWord(tk[i + 3], "schema")) {
@@ -2622,7 +2952,7 @@ function applyGrantRevoke(reg, stmt) {
2622
2952
  i += 1;
2623
2953
  }
2624
2954
  targets = [...reg.byKey.values()].filter(
2625
- (fn) => schemas.includes((fn.schema ?? "public").toLowerCase())
2955
+ (fn2) => schemas.includes((fn2.schema ?? "public").toLowerCase())
2626
2956
  );
2627
2957
  } else {
2628
2958
  return;
@@ -2630,7 +2960,7 @@ function applyGrantRevoke(reg, stmt) {
2630
2960
  const [start, end] = roleClause(tk, i, grant ? "to" : "from");
2631
2961
  if (start < 0) return;
2632
2962
  const roles = readRoles(tk.slice(0, end), start);
2633
- for (const fn of targets) applyToAcl(fn.acl, grant, roles);
2963
+ for (const fn2 of targets) applyToAcl(fn2.acl, grant, roles);
2634
2964
  }
2635
2965
  function applyDefaultPrivileges(reg, stmt) {
2636
2966
  const tk = stmt.tokens;
@@ -2727,6 +3057,7 @@ function finishFunctions(reg) {
2727
3057
  location: f.location
2728
3058
  };
2729
3059
  if (f.returns !== null) info.returns = f.returns;
3060
+ if (f.args !== null) info.args = f.args;
2730
3061
  return info;
2731
3062
  });
2732
3063
  }
@@ -2926,9 +3257,9 @@ function sync(state, key) {
2926
3257
  function findColumn(meta, name) {
2927
3258
  return meta.columns.find((c) => c.name === name);
2928
3259
  }
2929
- function pkOf(state, table) {
2930
- if (table === "auth.users") return ["id"];
2931
- return state.meta.get(table)?.pk ?? [];
3260
+ function pkOf(state, table2) {
3261
+ if (table2 === "auth.users") return ["id"];
3262
+ return state.meta.get(table2)?.pk ?? [];
2932
3263
  }
2933
3264
  function resolveRefs(state, meta) {
2934
3265
  for (const col of meta.columns) {
@@ -2937,12 +3268,12 @@ function resolveRefs(state, meta) {
2937
3268
  }
2938
3269
  }
2939
3270
  }
2940
- function clearRefsTo(state, table, column) {
3271
+ function clearRefsTo(state, table2, column) {
2941
3272
  for (const [key, meta] of state.meta) {
2942
3273
  let touched = false;
2943
3274
  for (const col of meta.columns) {
2944
3275
  const r = col.references;
2945
- if (!r || r.table !== table || column !== void 0 && r.column !== column) continue;
3276
+ if (!r || r.table !== table2 || column !== void 0 && r.column !== column) continue;
2946
3277
  col.references = null;
2947
3278
  col.fkName = null;
2948
3279
  touched = true;
@@ -3792,9 +4123,9 @@ function analyzeModule(rel, sf) {
3792
4123
  if (!ts9.isCallExpression(init) && !ts9.isNewExpression(init)) continue;
3793
4124
  if (ts9.isCallExpression(init)) {
3794
4125
  const callee = init.expression;
3795
- const fn = ts9.isIdentifier(callee) ? callee.text : ts9.isPropertyAccessExpression(callee) ? callee.name.text : "";
4126
+ const fn2 = ts9.isIdentifier(callee) ? callee.text : ts9.isPropertyAccessExpression(callee) ? callee.name.text : "";
3796
4127
  const tableName = stringLiteralValue(init.arguments[0]);
3797
- if ((DRIZZLE_TABLE_FNS.has(fn) || fn === "table") && tableName !== null) {
4128
+ if ((DRIZZLE_TABLE_FNS.has(fn2) || fn2 === "table") && tableName !== null) {
3798
4129
  drizzleTables.set(d.name.text, tableName);
3799
4130
  continue;
3800
4131
  }
@@ -3996,12 +4327,12 @@ var NO_ARG = {
3996
4327
  whole: false,
3997
4328
  isRequest: false
3998
4329
  };
3999
- function ownFunctionSym(facts, name, fn) {
4330
+ function ownFunctionSym(facts, name, fn2) {
4000
4331
  const factory = facts.clientFactories.find((c) => c.name === name);
4001
4332
  if (factory) return { kind: "factory", factory };
4002
4333
  const helper = facts.authHelpers.find((a) => a.name === name);
4003
- if (helper) return { kind: "auth", helper, fn, facts };
4004
- return { kind: "function", name, fn, facts };
4334
+ if (helper) return { kind: "auth", helper, fn: fn2, facts };
4335
+ return { kind: "function", name, fn: fn2, facts };
4005
4336
  }
4006
4337
  function exportedSym(p, tf, name, depth) {
4007
4338
  if (depth > 5) return null;
@@ -4048,7 +4379,7 @@ function scopeOf(p, facts) {
4048
4379
  for (const [name, v] of facts.moduleVars) {
4049
4380
  scope.set(name, { kind: "var", name, init: v.init, facts });
4050
4381
  }
4051
- for (const [name, table] of facts.drizzleTables) scope.set(name, { kind: "table", table });
4382
+ for (const [name, table2] of facts.drizzleTables) scope.set(name, { kind: "table", table: table2 });
4052
4383
  for (const [name, v] of facts.authVars) scope.set(name, { kind: "auth", helper: v.helper });
4053
4384
  for (const [local, ref] of facts.imports) {
4054
4385
  const target = p.resolver.resolve(ref.spec, facts.file);
@@ -4074,9 +4405,9 @@ function scopeOf(p, facts) {
4074
4405
  }
4075
4406
  if (!/^[.~]|^@\//.test(ref.spec)) continue;
4076
4407
  for (const f of p.registry.values()) {
4077
- const fn = f.functions.get(ref.imported);
4078
- if (!fn?.exported) continue;
4079
- const sym = ownFunctionSym(f, ref.imported, fn.fn);
4408
+ const fn2 = f.functions.get(ref.imported);
4409
+ if (!fn2?.exported) continue;
4410
+ const sym = ownFunctionSym(f, ref.imported, fn2.fn);
4080
4411
  if (sym.kind === "factory" || sym.kind === "auth") {
4081
4412
  scope.set(local, sym);
4082
4413
  p.warnings.push(
@@ -4096,10 +4427,10 @@ function symOfCallee(p, callee, scope) {
4096
4427
  }
4097
4428
  return void 0;
4098
4429
  }
4099
- function returnedExpressions(fn) {
4100
- if (!fn.body) return [];
4101
- if (!ts11.isBlock(fn.body)) return [fn.body];
4102
- return collect(fn.body, ts11.isReturnStatement).map((r) => r.expression).filter((e) => e !== void 0);
4430
+ function returnedExpressions(fn2) {
4431
+ if (!fn2.body) return [];
4432
+ if (!ts11.isBlock(fn2.body)) return [fn2.body];
4433
+ return collect(fn2.body, ts11.isReturnStatement).map((r) => r.expression).filter((e) => e !== void 0);
4103
4434
  }
4104
4435
  function factoryOfFunction(p, sym, depth) {
4105
4436
  const key = `${sym.facts.file}#${sym.name}`;
@@ -4477,10 +4808,10 @@ function callTarget(p, call, frame, scope) {
4477
4808
  return null;
4478
4809
  }
4479
4810
  function bindDeclarations(p, frame, acc) {
4480
- const { rel, sf, fn } = frame;
4811
+ const { rel, sf, fn: fn2 } = frame;
4481
4812
  const scope = scopeOf(p, frame.facts);
4482
4813
  const loc2 = (n) => ({ file: rel, line: lineOf(sf, n) });
4483
- const body = fn.body ?? fn;
4814
+ const body = fn2.body ?? fn2;
4484
4815
  for (const [name, sym] of scope) {
4485
4816
  if (sym.kind !== "var" || frame.clients.has(name) || frame.instances.has(name)) continue;
4486
4817
  const vb = varBinding(p, sym);
@@ -4605,10 +4936,10 @@ function bindDeclarations(p, frame, acc) {
4605
4936
  }
4606
4937
  }
4607
4938
  function analyzeFrame(p, frame, acc) {
4608
- const { rel, sf, fn } = frame;
4939
+ const { rel, sf, fn: fn2 } = frame;
4609
4940
  const scope = scopeOf(p, frame.facts);
4610
4941
  const loc2 = (n) => ({ file: rel, line: lineOf(sf, n) });
4611
- const body = fn.body ?? fn;
4942
+ const body = fn2.body ?? fn2;
4612
4943
  bindDeclarations(p, frame, acc);
4613
4944
  for (const call of collect(body, ts11.isCallExpression)) {
4614
4945
  const callee = call.expression;
@@ -4616,8 +4947,8 @@ function analyzeFrame(p, frame, acc) {
4616
4947
  continue;
4617
4948
  if (!ts11.isIdentifier(callee.expression)) continue;
4618
4949
  const outer = frame.clients.get(callee.expression.text);
4619
- const fn2 = call.arguments.map((a) => unwrap(a)).find((a) => ts11.isArrowFunction(a) || ts11.isFunctionExpression(a));
4620
- const param = fn2 && (ts11.isArrowFunction(fn2) || ts11.isFunctionExpression(fn2)) ? fn2.parameters[0] : void 0;
4950
+ const fn3 = call.arguments.map((a) => unwrap(a)).find((a) => ts11.isArrowFunction(a) || ts11.isFunctionExpression(a));
4951
+ const param = fn3 && (ts11.isArrowFunction(fn3) || ts11.isFunctionExpression(fn3)) ? fn3.parameters[0] : void 0;
4621
4952
  if (outer && param && ts11.isIdentifier(param.name)) frame.clients.set(param.name.text, outer);
4622
4953
  }
4623
4954
  const isSessionCall = (call) => /\.auth\.(getUser|getSession|getClaims)$/.test(call.expression.getText(sf)) || symOfCallee(p, call.expression, scope)?.kind === "auth";
@@ -4632,7 +4963,7 @@ function analyzeFrame(p, frame, acc) {
4632
4963
  text: gate.node.expression.getText(sf).replace(/\s+/g, " ").slice(0, 160)
4633
4964
  });
4634
4965
  }
4635
- for (const check of secretChecksIn(fn, sf)) {
4966
+ for (const check of secretChecksIn(fn2, sf)) {
4636
4967
  acc.authChecks.push({ ...loc2(check.node), kind: "secret" });
4637
4968
  }
4638
4969
  for (const pa of collect(body, ts11.isPropertyAccessExpression)) {
@@ -5137,16 +5468,16 @@ function enclosingCondition(node, body) {
5137
5468
  return null;
5138
5469
  }
5139
5470
  var normalizeColumn = (c) => (c ?? "").toLowerCase().replace(/_/g, "");
5140
- function singular(table) {
5141
- if (table.endsWith("ies")) return `${table.slice(0, -3)}y`;
5142
- if (table.endsWith("ses") || table.endsWith("xes")) return table.slice(0, -2);
5143
- return table.endsWith("s") ? table.slice(0, -1) : table;
5471
+ function singular(table2) {
5472
+ if (table2.endsWith("ies")) return `${table2.slice(0, -3)}y`;
5473
+ if (table2.endsWith("ses") || table2.endsWith("xes")) return table2.slice(0, -2);
5474
+ return table2.endsWith("s") ? table2.slice(0, -1) : table2;
5144
5475
  }
5145
- function columnNamesTable(column, table) {
5476
+ function columnNamesTable(column, table2) {
5146
5477
  const c = normalizeColumn(column);
5147
5478
  if (!c.endsWith("id") || c === "id") return false;
5148
5479
  const stem = c.slice(0, -2);
5149
- const t = normalizeColumn(table);
5480
+ const t = normalizeColumn(table2);
5150
5481
  return stem === t || stem === singular(t);
5151
5482
  }
5152
5483
  function guardMatch(q, g, tables) {
@@ -5256,8 +5587,8 @@ function layoutGuardsFor(p, dir, cache) {
5256
5587
  const sf = p.sources.get(rel);
5257
5588
  const facts = p.registry.get(rel);
5258
5589
  if (!sf || !facts || isClientComponentFile(sf)) continue;
5259
- const fn = pageHandlerIn(sf);
5260
- if (!fn) continue;
5590
+ const fn2 = pageHandlerIn(sf);
5591
+ if (!fn2) continue;
5261
5592
  try {
5262
5593
  const analysed = analyzeHandler(p, {
5263
5594
  rel,
@@ -5266,9 +5597,9 @@ function layoutGuardsFor(p, dir, cache) {
5266
5597
  kind: "page",
5267
5598
  route: dir,
5268
5599
  method: "PAGE",
5269
- fn: fn.fn,
5270
- node: fn.node,
5271
- wrapper: fn.wrapper
5600
+ fn: fn2.fn,
5601
+ node: fn2.node,
5602
+ wrapper: fn2.wrapper
5272
5603
  });
5273
5604
  guards = { authChecks: analysed.authChecks, roleChecks: analysed.roleChecks ?? [] };
5274
5605
  } catch {
@@ -5280,7 +5611,7 @@ function layoutGuardsFor(p, dir, cache) {
5280
5611
  return guards;
5281
5612
  }
5282
5613
  function analyzeHandler(p, h) {
5283
- const { rel, sf, fn } = h;
5614
+ const { rel, sf, fn: fn2 } = h;
5284
5615
  const loc2 = (n) => ({ file: rel, line: lineOf(sf, n) });
5285
5616
  const acc = {
5286
5617
  inputs: [],
@@ -5295,7 +5626,7 @@ function analyzeHandler(p, h) {
5295
5626
  rel,
5296
5627
  sf,
5297
5628
  facts: h.facts,
5298
- fn,
5629
+ fn: fn2,
5299
5630
  depth: 0,
5300
5631
  via: [],
5301
5632
  inputNames: /* @__PURE__ */ new Set(["params", "searchParams"]),
@@ -5313,13 +5644,13 @@ function analyzeHandler(p, h) {
5313
5644
  pathPos: [],
5314
5645
  exitPropagates: true
5315
5646
  };
5316
- const first = fn.parameters[0];
5647
+ const first = fn2.parameters[0];
5317
5648
  if (h.kind === "route" && first) {
5318
5649
  if (ts11.isIdentifier(first.name)) frame.reqNames.add(first.name.text);
5319
5650
  else for (const nm of boundNames(first.name)) if (REQUEST_NAME.test(nm)) frame.reqNames.add(nm);
5320
5651
  }
5321
5652
  if (h.kind === "server_action") {
5322
- const params = h.wrapper ? fn.parameters.slice(0, 1) : fn.parameters;
5653
+ const params = h.wrapper ? fn2.parameters.slice(0, 1) : fn2.parameters;
5323
5654
  for (const prm of params) {
5324
5655
  for (const nm of boundNames(prm.name)) {
5325
5656
  if (!acc.inputs.some((i) => i.kind === "action_arg" && i.name === nm)) {
@@ -5578,7 +5909,7 @@ function callerCheckingFunctions(model) {
5578
5909
  );
5579
5910
  }
5580
5911
  var CALL2 = /(?:"?([A-Za-z_][A-Za-z0-9_$]*)"?\s*\.\s*)?"?([A-Za-z_][A-Za-z0-9_$]*)"?\s*\(/g;
5581
- function qualified(schema, name) {
5912
+ function qualified2(schema, name) {
5582
5913
  const s = (schema ?? "public").toLowerCase();
5583
5914
  const n = (name ?? "").toLowerCase();
5584
5915
  return s === "public" ? n : `${s}.${n}`;
@@ -5586,7 +5917,7 @@ function qualified(schema, name) {
5586
5917
  function callsFunctionIn(expr, names) {
5587
5918
  if (names.size === 0) return false;
5588
5919
  for (const c of expr.replace(/'(?:[^']|'')*'/g, "''").matchAll(CALL2)) {
5589
- if (names.has(qualified(c[1], c[2]))) return true;
5920
+ if (names.has(qualified2(c[1], c[2]))) return true;
5590
5921
  }
5591
5922
  return false;
5592
5923
  }
@@ -5654,14 +5985,14 @@ function handlerViews(ctx) {
5654
5985
  function queryViews(ctx, handler) {
5655
5986
  return ctx.graph.out(handler.id, "CALLS").map((query) => {
5656
5987
  const client = ctx.graph.out(query.id, "USES_CLIENT")[0];
5657
- const table = ctx.graph.out(query.id, "TARGETS")[0];
5988
+ const table2 = ctx.graph.out(query.id, "TARGETS")[0];
5658
5989
  return {
5659
5990
  query,
5660
5991
  data: query.data,
5661
5992
  client,
5662
5993
  clientData: client?.data,
5663
- table,
5664
- tableData: table?.data
5994
+ table: table2,
5995
+ tableData: table2?.data
5665
5996
  };
5666
5997
  });
5667
5998
  }
@@ -5696,16 +6027,16 @@ function anonReadPolicy(t) {
5696
6027
  (p) => (p.command === "select" || p.command === "all") && (p.using ?? "").replace(/[\s()]/g, "").toLowerCase() === "true" && (p.roles.length === 0 || p.roles.some((r) => r === "anon" || r === "public"))
5697
6028
  );
5698
6029
  }
5699
- function publicReadNote(p, table) {
5700
- return ` public.${table} is readable by the anon role through RLS policy "${p.name}" (${p.location.file}:${p.location.line}), so this read leaks nothing beyond what the anon key already returns; the repository may not have intended that policy, so the finding stays at medium.`;
6030
+ function publicReadNote(p, table2) {
6031
+ return ` public.${table2} is readable by the anon role through RLS policy "${p.name}" (${p.location.file}:${p.location.line}), so this read leaks nothing beyond what the anon key already returns; the repository may not have intended that policy, so the finding stays at medium.`;
5701
6032
  }
5702
6033
  function adminOnly(ctx, h, t) {
5703
6034
  const check = h.roleChecks[0];
5704
6035
  if (!check || !t?.known || !singleTenantTable(ctx, t.table)) return void 0;
5705
6036
  return check;
5706
6037
  }
5707
- function singleTenantTable(ctx, table) {
5708
- const info = ctx.model.tables.find((x) => x.table === table.toLowerCase());
6038
+ function singleTenantTable(ctx, table2) {
6039
+ const info = ctx.model.tables.find((x) => x.table === table2.toLowerCase());
5709
6040
  if (!info || info.columns.some((c) => isScopeColumn(c))) return false;
5710
6041
  for (const col of info.columnInfo ?? []) {
5711
6042
  if (!col.references || col.nullable) continue;
@@ -5714,16 +6045,16 @@ function singleTenantTable(ctx, table) {
5714
6045
  }
5715
6046
  return true;
5716
6047
  }
5717
- function adminOnlyNote(check, table) {
5718
- return ` Admin-only: the handler stops unless ${check.source} passes the role check at ${check.file}:${check.line} (${check.text}), and public.${table} has no tenant or owner column, so the row is shared site content rather than a tenant's; verify the admin check cannot be self-granted.`;
6048
+ function adminOnlyNote(check, table2) {
6049
+ return ` Admin-only: the handler stops unless ${check.source} passes the role check at ${check.file}:${check.line} (${check.text}), and public.${table2} has no tenant or owner column, so the row is shared site content rather than a tenant's; verify the admin check cannot be self-granted.`;
5719
6050
  }
5720
- function tableDataOf(ctx, table) {
5721
- return ctx.graph.nodes.get(`table:${table}`)?.data;
6051
+ function tableDataOf(ctx, table2) {
6052
+ return ctx.graph.nodes.get(`table:${table2}`)?.data;
5722
6053
  }
5723
6054
  function callerCheck(checks) {
5724
6055
  return checks?.find((c) => isScopeColumn(c.column) && !c.inputDerived);
5725
6056
  }
5726
- function guardTiesRowToCaller(guard, table, callerFns) {
6057
+ function guardTiesRowToCaller(guard, table2, callerFns) {
5727
6058
  const callerFilter = guard.filters.find((f) => isScopeColumn(f.column) && !f.inputDerived);
5728
6059
  if (callerFilter) {
5729
6060
  return {
@@ -5747,10 +6078,10 @@ function guardTiesRowToCaller(guard, table, callerFns) {
5747
6078
  why: `the read runs with a ${guard.client === "anon" ? "public anon" : "privileged"} client and filters by no owner column, so it returns the row for anyone`
5748
6079
  };
5749
6080
  }
5750
- const reads = (table?.policyDetails ?? []).filter(
6081
+ const reads = (table2?.policyDetails ?? []).filter(
5751
6082
  (p) => p.command === "select" || p.command === "all"
5752
6083
  );
5753
- const scoped = table?.known === true && table.rlsEnabled && reads.length > 0 && reads.every((p) => policyScopesToCaller(p.using) || callsFunctionIn(p.using ?? "", callerFns));
6084
+ const scoped = table2?.known === true && table2.rlsEnabled && reads.length > 0 && reads.every((p) => policyScopesToCaller(p.using) || callsFunctionIn(p.using ?? "", callerFns));
5754
6085
  return {
5755
6086
  tied: scoped,
5756
6087
  how: `through ${guard.clientName ?? "a user-scoped client"}, so RLS applied`,
@@ -5868,7 +6199,7 @@ var tableWithoutRls = {
5868
6199
  ],
5869
6200
  summary: `public.${t.table} has no "enable row level security" in migrations but is queried with a ${c.kind} client. Anyone holding the public anon key can read every row directly through PostgREST.`,
5870
6201
  title: `Table "${t.table}" is exposed without RLS`,
5871
- data: { deterministic: true, ruleId: this.id },
6202
+ data: { deterministic: true, ruleId: this.id, table: t.table },
5872
6203
  tail: locations(v.table?.location)
5873
6204
  }));
5874
6205
  addReach(g, h, v, `supabase.${v.data.operation}:public.${t.table}`);
@@ -6712,7 +7043,7 @@ var storagePolicyWithoutOwnerCheck = {
6712
7043
  return out;
6713
7044
  }
6714
7045
  };
6715
- var API_ROLES = /* @__PURE__ */ new Set(["anon", "authenticated", "public"]);
7046
+ var API_ROLES2 = /* @__PURE__ */ new Set(["anon", "authenticated", "public"]);
6716
7047
  var securityDefinerFunctionWithoutCallerCheck = {
6717
7048
  id: "supabase.security-definer-function-without-caller-check",
6718
7049
  title: "SECURITY DEFINER function without a caller check",
@@ -6729,22 +7060,22 @@ var securityDefinerFunctionWithoutCallerCheck = {
6729
7060
  rpcByName.set(key, [...rpcByName.get(key) ?? [], r]);
6730
7061
  }
6731
7062
  const out = [];
6732
- for (const fn of fns) {
6733
- if (!fn.securityDefiner || fn.checksCaller) continue;
6734
- if (fn.name.includes(".")) continue;
6735
- if (fn.returns === "trigger" || fn.returns === "event_trigger") continue;
6736
- const roles = fn.grantedTo.filter((r) => API_ROLES.has(r));
7063
+ for (const fn2 of fns) {
7064
+ if (!fn2.securityDefiner || fn2.checksCaller) continue;
7065
+ if (fn2.name.includes(".")) continue;
7066
+ if (fn2.returns === "trigger" || fn2.returns === "event_trigger") continue;
7067
+ const roles = fn2.grantedTo.filter((r) => API_ROLES2.has(r));
6737
7068
  if (roles.length === 0) continue;
6738
7069
  const anonymous = roles.includes("anon") || roles.includes("public");
6739
- const sites = rpcByName.get(fn.name) ?? [];
7070
+ const sites = rpcByName.get(fn2.name) ?? [];
6740
7071
  const entries = unique(sites.map((s) => s.handlerData.entry));
6741
- const endpoint = `POST /rest/v1/rpc/${fn.name}`;
7072
+ const endpoint = `POST /rest/v1/rpc/${fn2.name}`;
6742
7073
  const who = anonymous ? "Anonymous visitors can call it with the public anon key" : "Any signed-in user can call it";
6743
- const callNote = entries.length > 0 ? ` The app calls it with supabase.rpc("${fn.name}") from ${entries.join(", ")}.` : "";
7074
+ const callNote = entries.length > 0 ? ` The app calls it with supabase.rpc("${fn2.name}") from ${entries.join(", ")}.` : "";
6744
7075
  const path = [
6745
7076
  entries[0] ?? endpoint,
6746
- `supabase.rpc("${fn.name}") (EXECUTE: ${roles.join(", ")})`,
6747
- `public.${fn.name}() SECURITY DEFINER (runs as its owner, RLS does not apply)`,
7077
+ `supabase.rpc("${fn2.name}") (EXECUTE: ${roles.join(", ")})`,
7078
+ `public.${fn2.name}() SECURITY DEFINER (runs as its owner, RLS does not apply)`,
6748
7079
  "no auth.uid() / auth.jwt() check"
6749
7080
  ];
6750
7081
  out.push(
@@ -6752,27 +7083,27 @@ var securityDefinerFunctionWithoutCallerCheck = {
6752
7083
  ctx,
6753
7084
  this,
6754
7085
  {
6755
- title: `${anonymous ? "Anonymous-callable " : ""}SECURITY DEFINER function "${fn.name}" without a caller check`,
7086
+ title: `${anonymous ? "Anonymous-callable " : ""}SECURITY DEFINER function "${fn2.name}" without a caller check`,
6756
7087
  entrypoints: [...entries, endpoint],
6757
7088
  sources: unique([
6758
7089
  ...sites.flatMap((s) => s.inputs.map((i) => `${i.kind}:${i.name}`)),
6759
7090
  "rpc arguments"
6760
7091
  ]),
6761
- sinks: [`postgres.function:public.${fn.name}`],
7092
+ sinks: [`postgres.function:public.${fn2.name}`],
6762
7093
  path,
6763
7094
  evidence: [
6764
7095
  {
6765
7096
  kind: "rule",
6766
- summary: `public.${fn.name}() is SECURITY DEFINER: it runs with the rights of its owner and Row Level Security does not apply inside it. Its body never reads the caller's identity (auth.uid(), auth.jwt(), auth.email() or the request JWT), so whatever it returns or changes is available to every role that can execute it: ${roles.join(", ")}. ${who} at ${endpoint}.${callNote} Filter by auth.uid() inside the function, make it SECURITY INVOKER, or revoke EXECUTE from public, anon and authenticated.`,
7097
+ summary: `public.${fn2.name}() is SECURITY DEFINER: it runs with the rights of its owner and Row Level Security does not apply inside it. Its body never reads the caller's identity (auth.uid(), auth.jwt(), auth.email() or the request JWT), so whatever it returns or changes is available to every role that can execute it: ${roles.join(", ")}. ${who} at ${endpoint}.${callNote} Filter by auth.uid() inside the function, make it SECURITY INVOKER, or revoke EXECUTE from public, anon and authenticated.`,
6767
7098
  locations: locations3(
6768
- fn.location,
7099
+ fn2.location,
6769
7100
  ...sites.flatMap((s) => [s.handler.location, s.query.location])
6770
7101
  ),
6771
7102
  data: {
6772
7103
  deterministic: false,
6773
7104
  ruleId: this.id,
6774
- function: fn.name,
6775
- grantedTo: [...fn.grantedTo]
7105
+ function: fn2.name,
7106
+ grantedTo: [...fn2.grantedTo]
6776
7107
  }
6777
7108
  },
6778
7109
  { kind: "trace", summary: path.join(" -> ") }
@@ -6825,9 +7156,9 @@ function applyPublicTables(findings, publicTables2, now) {
6825
7156
  if (f.sinks.length === 0) return f;
6826
7157
  const tables = [];
6827
7158
  for (const sink of f.sinks) {
6828
- const table = READ_SINK.exec(sink)?.[1];
6829
- if (table === void 0 || !declared.has(table.toLowerCase())) return f;
6830
- if (!tables.includes(table)) tables.push(table);
7159
+ const table2 = READ_SINK.exec(sink)?.[1];
7160
+ if (table2 === void 0 || !declared.has(table2.toLowerCase())) return f;
7161
+ if (!tables.includes(table2)) tables.push(table2);
6831
7162
  }
6832
7163
  const command = f.evidence[0]?.data?.command;
6833
7164
  if (f.ruleId === "supabase.rls-policy-without-caller-predicate" && command !== "select") {
@@ -7050,7 +7381,13 @@ function runScan(path, opts = {}) {
7050
7381
  const graph = buildGraph(model);
7051
7382
  const publicTables2 = cfg.config.publicTables ?? [];
7052
7383
  const runOpts = { ...opts.now === void 0 ? {} : { now: opts.now }, publicTables: publicTables2 };
7053
- const findings = runRules(defaultRules, model, graph, runOpts);
7384
+ const rulesFindings = runRules(defaultRules, model, graph, runOpts);
7385
+ const migrationsDir = repoDirs.dirs[0] ?? "supabase/migrations";
7386
+ const findings = rulesFindings.map((f) => {
7387
+ if (f.status === "suppressed") return f;
7388
+ const fix = deterministicFix(f, model, migrationsDir);
7389
+ return fix ? { ...f, fix } : f;
7390
+ });
7054
7391
  const coverage = summarizeCoverage(findings);
7055
7392
  const summary = summarize(model, defaultRules.length, publicTables2);
7056
7393
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditai-scan",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Deterministic security scanner for Next.js + Supabase apps: cross-tenant reads, RLS gaps, service-role misuse, mass assignment. No account, no model, seconds.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",