auditai-scan 0.7.1 → 0.7.2

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.
@@ -253,6 +253,19 @@ var COMMANDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete",
253
253
  var HEADER = (title) => `-- ${title}
254
254
  -- Proposed by Audit AI. Read it, then apply it with the rest of your migrations.
255
255
  `;
256
+ function policedTables(f, model) {
257
+ const names = f.tables ?? [];
258
+ if (names.length === 0)
259
+ return null;
260
+ const out = [];
261
+ for (const n of names) {
262
+ const t = table(model, n);
263
+ if (!t || !t.rlsEnabled || t.policies.length === 0)
264
+ return null;
265
+ out.push(qualified(t.table));
266
+ }
267
+ return out;
268
+ }
256
269
  function sqlFunctionFix(finding4, model) {
257
270
  const name = finding4.evidence[0]?.data?.function;
258
271
  const f = fn(model, typeof name === "string" ? name : void 0);
@@ -260,6 +273,21 @@ function sqlFunctionFix(finding4, model) {
260
273
  return null;
261
274
  const sig = signatureOf(f);
262
275
  const ambiguous = sig.endsWith("(...)");
276
+ const policed = f.returns !== void 0 && f.returns !== "void" ? policedTables(f, model) : null;
277
+ if (policed && !ambiguous) {
278
+ return {
279
+ file: `fix_security_invoker_${f.name.replace(/[^a-z0-9]+/gi, "_").toLowerCase()}.sql`,
280
+ sql: [
281
+ HEADER(`Run ${f.name} with the caller's rights, so row level security applies inside it`),
282
+ `-- It reads ${policed.join(", ")}, and each of them has row level security with policies.
283
+ `,
284
+ `alter function ${sig} security invoker;
285
+ `
286
+ ].join(""),
287
+ summary: `Run ${f.name} with the caller's rights (security invoker)`,
288
+ rationale: `${f.name} only reads tables that already have row level security with policies (${policed.join(", ")}), so running it with the caller's rights lets those policies decide what it returns, and calls from your app keep working. If it must keep its owner's rights on purpose (for example it counts rows a caller may not see), revoke execute from public, anon and authenticated instead, or add a caller check inside the body.`
289
+ };
290
+ }
263
291
  const body = [
264
292
  HEADER(`Stop anon and authenticated from calling ${f.name} directly`),
265
293
  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" : "",
@@ -2709,7 +2737,8 @@ function applyCreateFunction(reg, stmt, file) {
2709
2737
  const q = readQualifiedName(tk, i + 1);
2710
2738
  if (!q || !isPunct(tk[q.next], "(")) return;
2711
2739
  const argsEnd = groupEnd(tk, q.next);
2712
- const args = argumentTypes(stmt, tk, q.next, argsEnd);
2740
+ const params = readParams(stmt, tk, q.next, argsEnd);
2741
+ const args = params === null ? null : params.map((p) => p.type).join(", ");
2713
2742
  i = argsEnd + 1;
2714
2743
  let securityDefiner = false;
2715
2744
  let returns = null;
@@ -2749,6 +2778,7 @@ function applyCreateFunction(reg, stmt, file) {
2749
2778
  securityDefiner,
2750
2779
  returns,
2751
2780
  args,
2781
+ params,
2752
2782
  body: code,
2753
2783
  directCheck: CALLER_CHECKS.some((re) => re.test(code)),
2754
2784
  acl: reg.byKey.get(key)?.acl ?? initialAcl(reg, q.schema),
@@ -2815,8 +2845,8 @@ var TYPE_WORD = /* @__PURE__ */ new Set([
2815
2845
  "void",
2816
2846
  "xml"
2817
2847
  ]);
2818
- function argumentTypes(stmt, tokens, open, close) {
2819
- if (close <= open + 1) return "";
2848
+ function readParams(stmt, tokens, open, close) {
2849
+ if (close <= open + 1) return [];
2820
2850
  const parts = [];
2821
2851
  let depth = 0;
2822
2852
  let start = open + 1;
@@ -2854,11 +2884,12 @@ function argumentTypes(stmt, tokens, open, close) {
2854
2884
  const first = words[typeStart];
2855
2885
  const last = words[words.length - 1];
2856
2886
  if (!first || !last) return null;
2857
- parts.push(
2858
- stmt.text.slice(first.start - stmt.start, last.end - stmt.start).replace(/\s+/g, " ").trim().toLowerCase()
2859
- );
2887
+ parts.push({
2888
+ name: named && firstWord ? firstWord.value.toLowerCase() : null,
2889
+ type: stmt.text.slice(first.start - stmt.start, last.end - stmt.start).replace(/\s+/g, " ").trim().toLowerCase()
2890
+ });
2860
2891
  }
2861
- return parts.join(", ");
2892
+ return parts;
2862
2893
  }
2863
2894
  function readFunctionList(tokens, from) {
2864
2895
  const keys = [];
@@ -3043,6 +3074,18 @@ function callees(reg, f, byName) {
3043
3074
  }
3044
3075
  return out;
3045
3076
  }
3077
+ var RELATION = /(?<![A-Za-z0-9_$])(?:from|join)\s+(?:only\s+)?(?:"?([A-Za-z_][A-Za-z0-9_$]{0,62})"?\s*\.\s*)?"?([A-Za-z_][A-Za-z0-9_$]{0,62})"?(?![A-Za-z0-9_$"]|\s*[.(])/gi;
3078
+ function relationsOf(body) {
3079
+ const out = [];
3080
+ for (const m of body.matchAll(RELATION)) {
3081
+ const schema = m[1]?.toLowerCase();
3082
+ const name = m[2]?.toLowerCase();
3083
+ if (!name || name === "only" || name === "lateral") continue;
3084
+ const key = schema === void 0 || schema === "public" ? name : `${schema}.${name}`;
3085
+ if (!out.includes(key)) out.push(key);
3086
+ }
3087
+ return out;
3088
+ }
3046
3089
  function effectiveRoles(acl) {
3047
3090
  const out = [];
3048
3091
  if (acl.publicExec || acl.roles.includes("anon")) out.push("anon");
@@ -3082,6 +3125,10 @@ function finishFunctions(reg) {
3082
3125
  };
3083
3126
  if (f.returns !== null) info.returns = f.returns;
3084
3127
  if (f.args !== null) info.args = f.args;
3128
+ if (f.params?.every((x) => x.name !== null))
3129
+ info.params = f.params.map((x) => ({ name: x.name ?? "", type: x.type }));
3130
+ const tables = relationsOf(f.body);
3131
+ if (tables.length > 0) info.tables = tables;
3085
3132
  return info;
3086
3133
  });
3087
3134
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditai-scan",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
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",