auditai-scan 0.7.4 → 0.7.5

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.
@@ -7916,6 +7916,31 @@ function readsOnlyPublicRows(fn2, tables, fromMigrationsOnly) {
7916
7916
  });
7917
7917
  }
7918
7918
  var API_ROLES2 = /* @__PURE__ */ new Set(["anon", "authenticated", "public"]);
7919
+ var NAMES_A_PERSON = /(^|_)(user|uid|profile|member|person|owner|customer|employee|student|patient|email)(_?id)?$/i;
7920
+ var CARRIES_CREDENTIAL = /(^|_)(pin|password|passwd|secret|token|otp|code|hash)$/i;
7921
+ function isCatalogRelation(name) {
7922
+ return name.startsWith("information_schema.") || name.startsWith("pg_catalog.") || /^pg_[a-z_]+$/.test(name);
7923
+ }
7924
+ function writesNothing(fn2, byName, seen = /* @__PURE__ */ new Set()) {
7925
+ if ((fn2.writes?.length ?? 0) > 0) return false;
7926
+ if (seen.has(fn2.name)) return true;
7927
+ seen.add(fn2.name);
7928
+ return (fn2.calls ?? []).every((c) => {
7929
+ const callee = byName.get(c);
7930
+ return callee !== void 0 && writesNothing(callee, byName, seen);
7931
+ });
7932
+ }
7933
+ function narrowResult(fn2, byName) {
7934
+ if (!writesNothing(fn2, byName)) return null;
7935
+ if (fn2.returns === "boolean") {
7936
+ if (fn2.params === void 0 && fn2.args !== "") return null;
7937
+ const about = (fn2.params ?? []).map((p) => p.name);
7938
+ return about.some((n) => NAMES_A_PERSON.test(n) || CARRIES_CREDENTIAL.test(n)) ? null : "boolean";
7939
+ }
7940
+ const read = fn2.tables ?? [];
7941
+ if (fn2.returns !== "void" && read.length > 0 && read.every(isCatalogRelation)) return "catalog";
7942
+ return null;
7943
+ }
7919
7944
  var securityDefinerFunctionWithoutCallerCheck = {
7920
7945
  id: "supabase.security-definer-function-without-caller-check",
7921
7946
  title: "SECURITY DEFINER function without a caller check",
@@ -7933,6 +7958,7 @@ var securityDefinerFunctionWithoutCallerCheck = {
7933
7958
  }
7934
7959
  const tables = new Map(ctx.model.tables.map((t) => [t.table, t]));
7935
7960
  const hasMigrations = ctx.model.files.some((f) => MIGRATION_SQL.test(f));
7961
+ const byName = new Map(fns.map((f) => [f.name, f]));
7936
7962
  const out = [];
7937
7963
  for (const fn2 of fns) {
7938
7964
  if (!fn2.securityDefiner || fn2.checksCaller) continue;
@@ -7941,6 +7967,8 @@ var securityDefinerFunctionWithoutCallerCheck = {
7941
7967
  const roles = fn2.grantedTo.filter((r) => API_ROLES2.has(r));
7942
7968
  if (roles.length === 0) continue;
7943
7969
  if (readsOnlyPublicRows(fn2, tables, hasMigrations)) continue;
7970
+ const narrow = narrowResult(fn2, byName);
7971
+ const narrowNote = narrow === "boolean" ? " It changes nothing and returns only true or false, so it is reported as medium: a yes/no answer about ids the caller supplies can still confirm that a record exists or that a user holds a role." : narrow === "catalog" ? " It changes nothing and reads only the system catalog, so it is reported as medium: it describes the schema, not anyone's rows." : "";
7944
7972
  const anonymous = roles.includes("anon") || roles.includes("public");
7945
7973
  const sites = rpcByName.get(fn2.name) ?? [];
7946
7974
  const entries = unique(sites.map((s) => s.handlerData.entry));
@@ -7969,7 +7997,7 @@ var securityDefinerFunctionWithoutCallerCheck = {
7969
7997
  evidence: [
7970
7998
  {
7971
7999
  kind: "rule",
7972
- 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.`,
8000
+ 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}${narrowNote} Filter by auth.uid() inside the function, make it SECURITY INVOKER, or revoke EXECUTE from public, anon and authenticated.`,
7973
8001
  locations: locations3(
7974
8002
  fn2.location,
7975
8003
  ...sites.flatMap((s) => [s.handler.location, s.query.location])
@@ -7978,13 +8006,14 @@ var securityDefinerFunctionWithoutCallerCheck = {
7978
8006
  deterministic: false,
7979
8007
  ruleId: this.id,
7980
8008
  function: fn2.name,
7981
- grantedTo: [...fn2.grantedTo]
8009
+ grantedTo: [...fn2.grantedTo],
8010
+ ...narrow ? { narrowResult: narrow } : {}
7982
8011
  }
7983
8012
  },
7984
8013
  { kind: "trace", summary: path.join(" -> ") }
7985
8014
  ]
7986
8015
  },
7987
- anonymous ? "critical" : "high"
8016
+ narrow ? "medium" : anonymous ? "critical" : "high"
7988
8017
  )
7989
8018
  );
7990
8019
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditai-scan",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
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",