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.
- package/dist/auditai-scan.mjs +54 -7
- package/package.json +1 -1
package/dist/auditai-scan.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|