auditai-scan 0.1.0 → 0.2.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.
- package/dist/auditai-scan.mjs +994 -269
- package/package.json +1 -1
package/dist/auditai-scan.mjs
CHANGED
|
@@ -173,8 +173,8 @@ function formatScanText(r) {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
// packages/scanner/src/scan.ts
|
|
176
|
-
import { existsSync, readFileSync as
|
|
177
|
-
import { join as
|
|
176
|
+
import { existsSync, readFileSync as readFileSync3 } from "node:fs";
|
|
177
|
+
import { join as join4 } from "node:path";
|
|
178
178
|
|
|
179
179
|
// packages/graph/src/graph.ts
|
|
180
180
|
var SecurityGraph = class {
|
|
@@ -286,7 +286,8 @@ function buildGraph(model) {
|
|
|
286
286
|
filters: q.filters,
|
|
287
287
|
payload: q.payload,
|
|
288
288
|
text: q.text,
|
|
289
|
-
table: q.table
|
|
289
|
+
table: q.table,
|
|
290
|
+
...q.via && q.via.length > 0 ? { via: q.via } : {}
|
|
290
291
|
};
|
|
291
292
|
const qn = g.addNode({
|
|
292
293
|
id: `query:${q.location.file}:${q.location.line}:${i}`,
|
|
@@ -367,6 +368,8 @@ function isIgnored(rel, patterns) {
|
|
|
367
368
|
function discoverFiles(root, extraSqlDirs = [], ignoreGlobs = []) {
|
|
368
369
|
const source = [];
|
|
369
370
|
const sql = [];
|
|
371
|
+
const manifests = [];
|
|
372
|
+
const tsconfigs = [];
|
|
370
373
|
const ignore = ignoreGlobs.map(globToRegExp);
|
|
371
374
|
const walk2 = (dir, sqlOnly = false) => {
|
|
372
375
|
let entries;
|
|
@@ -396,14 +399,18 @@ function discoverFiles(root, extraSqlDirs = [], ignoreGlobs = []) {
|
|
|
396
399
|
continue;
|
|
397
400
|
}
|
|
398
401
|
if (sqlOnly || name.endsWith(".d.ts")) continue;
|
|
399
|
-
if (
|
|
402
|
+
if (name === "package.json") manifests.push(rel);
|
|
403
|
+
else if (/^tsconfig(\..+)?\.json$/.test(name)) tsconfigs.push(rel);
|
|
404
|
+
else if (SOURCE_EXT.test(name)) source.push(rel);
|
|
400
405
|
}
|
|
401
406
|
};
|
|
402
407
|
walk2(root);
|
|
403
408
|
for (const extra of extraSqlDirs) walk2(resolve(root, extra), true);
|
|
404
409
|
source.sort();
|
|
405
410
|
sql.sort();
|
|
406
|
-
|
|
411
|
+
manifests.sort();
|
|
412
|
+
tsconfigs.sort();
|
|
413
|
+
return { source, sql, manifests, tsconfigs };
|
|
407
414
|
}
|
|
408
415
|
|
|
409
416
|
// packages/parser/src/model.ts
|
|
@@ -486,21 +493,80 @@ function hasExportModifier(node) {
|
|
|
486
493
|
const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
|
|
487
494
|
return mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
488
495
|
}
|
|
489
|
-
function
|
|
496
|
+
function functionOfInitializer(init, sf) {
|
|
497
|
+
const u = unwrap(init);
|
|
498
|
+
if (ts.isArrowFunction(u) || ts.isFunctionExpression(u)) return { fn: u };
|
|
499
|
+
if (ts.isCallExpression(u)) {
|
|
500
|
+
for (const a of u.arguments) {
|
|
501
|
+
const ua = unwrap(a);
|
|
502
|
+
if (ts.isArrowFunction(ua) || ts.isFunctionExpression(ua)) {
|
|
503
|
+
return { fn: ua, wrapper: u.expression.getText(sf).replace(/\s+/g, "") };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
function topLevelFunctions(sf) {
|
|
490
510
|
const out = [];
|
|
491
511
|
for (const stmt of sf.statements) {
|
|
492
|
-
if (ts.isFunctionDeclaration(stmt) && stmt.name
|
|
493
|
-
out.push({ name: stmt.name.text, fn: stmt, node: stmt });
|
|
494
|
-
} else if (ts.isVariableStatement(stmt)
|
|
512
|
+
if (ts.isFunctionDeclaration(stmt) && stmt.name) {
|
|
513
|
+
out.push({ name: stmt.name.text, fn: stmt, node: stmt, exported: hasExportModifier(stmt) });
|
|
514
|
+
} else if (ts.isVariableStatement(stmt)) {
|
|
515
|
+
const exported = hasExportModifier(stmt);
|
|
495
516
|
for (const d of stmt.declarationList.declarations) {
|
|
496
|
-
if (ts.isIdentifier(d.name)
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
517
|
+
if (!ts.isIdentifier(d.name) || !d.initializer) continue;
|
|
518
|
+
const f = functionOfInitializer(d.initializer, sf);
|
|
519
|
+
if (!f) continue;
|
|
520
|
+
out.push(
|
|
521
|
+
f.wrapper === void 0 ? { name: d.name.text, fn: f.fn, node: d, exported } : { name: d.name.text, fn: f.fn, node: d, exported, wrapper: f.wrapper }
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return out;
|
|
527
|
+
}
|
|
528
|
+
function exportedFunctions(sf) {
|
|
529
|
+
return topLevelFunctions(sf).filter((f) => f.exported);
|
|
530
|
+
}
|
|
531
|
+
function classesIn(sf) {
|
|
532
|
+
const out = [];
|
|
533
|
+
for (const stmt of sf.statements) {
|
|
534
|
+
if (!ts.isClassDeclaration(stmt) || !stmt.name) continue;
|
|
535
|
+
const ctorParams = [];
|
|
536
|
+
const propFromParam = /* @__PURE__ */ new Map();
|
|
537
|
+
const methods = /* @__PURE__ */ new Map();
|
|
538
|
+
for (const member of stmt.members) {
|
|
539
|
+
if (ts.isConstructorDeclaration(member)) {
|
|
540
|
+
member.parameters.forEach((p, i) => {
|
|
541
|
+
const name = ts.isIdentifier(p.name) ? p.name.text : `arg${i}`;
|
|
542
|
+
ctorParams.push(name);
|
|
543
|
+
const mods = ts.getModifiers(p) ?? [];
|
|
544
|
+
if (mods.some(
|
|
545
|
+
(m) => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.PublicKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword || m.kind === ts.SyntaxKind.ReadonlyKeyword
|
|
546
|
+
)) {
|
|
547
|
+
propFromParam.set(name, i);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
if (member.body) {
|
|
551
|
+
for (const bin of collect(member.body, ts.isBinaryExpression)) {
|
|
552
|
+
if (bin.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(bin.left) && bin.left.expression.kind === ts.SyntaxKind.ThisKeyword && ts.isIdentifier(bin.right)) {
|
|
553
|
+
const idx = ctorParams.indexOf(bin.right.text);
|
|
554
|
+
if (idx >= 0) propFromParam.set(bin.left.name.text, idx);
|
|
555
|
+
}
|
|
500
556
|
}
|
|
501
557
|
}
|
|
558
|
+
} else if (ts.isMethodDeclaration(member) && ts.isIdentifier(member.name)) {
|
|
559
|
+
methods.set(member.name.text, member);
|
|
502
560
|
}
|
|
503
561
|
}
|
|
562
|
+
out.push({
|
|
563
|
+
name: stmt.name.text,
|
|
564
|
+
node: stmt,
|
|
565
|
+
exported: hasExportModifier(stmt),
|
|
566
|
+
ctorParams,
|
|
567
|
+
propFromParam,
|
|
568
|
+
methods
|
|
569
|
+
});
|
|
504
570
|
}
|
|
505
571
|
return out;
|
|
506
572
|
}
|
|
@@ -546,6 +612,11 @@ function enclosingStatement(node) {
|
|
|
546
612
|
|
|
547
613
|
// packages/parser/src/nextjs.ts
|
|
548
614
|
var ROUTE_FILE = /^(?:(.*?)\/)?(?:src\/)?app\/(.*?)\/?route\.(ts|tsx|js|jsx|mjs)$/;
|
|
615
|
+
var PAGE_FILE = /^(?:(.*?)\/)?(?:src\/)?app\/(.*?)\/?page\.(tsx|ts|jsx|js)$/;
|
|
616
|
+
function routePath(dir) {
|
|
617
|
+
const parts = dir.split("/").filter((p) => p.length > 0 && !p.startsWith("(") && !p.startsWith("@"));
|
|
618
|
+
return `/${parts.join("/")}`;
|
|
619
|
+
}
|
|
549
620
|
function appRootOf(rel) {
|
|
550
621
|
const m = /^(?:(.*?)\/)?(?:src\/)?app\//.exec(rel);
|
|
551
622
|
if (!m) return null;
|
|
@@ -555,9 +626,31 @@ function appRootOf(rel) {
|
|
|
555
626
|
function routeFromFile(rel) {
|
|
556
627
|
const m = ROUTE_FILE.exec(rel);
|
|
557
628
|
if (!m) return null;
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
629
|
+
return routePath(m[2] ?? "");
|
|
630
|
+
}
|
|
631
|
+
function pageFromFile(rel) {
|
|
632
|
+
const m = PAGE_FILE.exec(rel);
|
|
633
|
+
if (!m) return null;
|
|
634
|
+
return routePath(m[2] ?? "");
|
|
635
|
+
}
|
|
636
|
+
function pageHandlerIn(sf) {
|
|
637
|
+
for (const stmt of sf.statements) {
|
|
638
|
+
if (ts2.isFunctionDeclaration(stmt)) {
|
|
639
|
+
const mods = ts2.getModifiers(stmt) ?? [];
|
|
640
|
+
if (mods.some((m) => m.kind === ts2.SyntaxKind.DefaultKeyword)) {
|
|
641
|
+
return { name: stmt.name?.text ?? "Page", fn: stmt, node: stmt, exported: true };
|
|
642
|
+
}
|
|
643
|
+
} else if (ts2.isExportAssignment(stmt) && !stmt.isExportEquals) {
|
|
644
|
+
const e = unwrap(stmt.expression);
|
|
645
|
+
if (ts2.isIdentifier(e)) {
|
|
646
|
+
const f = topLevelFunctions(sf).find((t) => t.name === e.text);
|
|
647
|
+
if (f) return { ...f, exported: true };
|
|
648
|
+
} else if (ts2.isArrowFunction(e) || ts2.isFunctionExpression(e)) {
|
|
649
|
+
return { name: "Page", fn: e, node: stmt, exported: true };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return null;
|
|
561
654
|
}
|
|
562
655
|
function routeHandlersIn(sf) {
|
|
563
656
|
const out = [];
|
|
@@ -582,9 +675,172 @@ function isClientComponentFile(sf) {
|
|
|
582
675
|
}
|
|
583
676
|
|
|
584
677
|
// packages/parser/src/parse-project.ts
|
|
678
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
679
|
+
import { join as join3, resolve as resolve2 } from "node:path";
|
|
680
|
+
import ts5 from "typescript";
|
|
681
|
+
|
|
682
|
+
// packages/parser/src/resolve.ts
|
|
585
683
|
import { readFileSync } from "node:fs";
|
|
586
|
-
import { join as join2, posix
|
|
587
|
-
import
|
|
684
|
+
import { join as join2, posix } from "node:path";
|
|
685
|
+
import ts3 from "typescript";
|
|
686
|
+
var EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
|
|
687
|
+
var CONDITIONS = ["import", "default", "require", "node", "types", "module"];
|
|
688
|
+
function dirOf(rel) {
|
|
689
|
+
const d = posix.dirname(rel);
|
|
690
|
+
return d === "." ? "" : d;
|
|
691
|
+
}
|
|
692
|
+
function under(dir, rel) {
|
|
693
|
+
return posix.normalize(dir ? posix.join(dir, rel) : rel).replace(/^\.\//, "");
|
|
694
|
+
}
|
|
695
|
+
function matchPattern(pattern, spec) {
|
|
696
|
+
const i = pattern.indexOf("*");
|
|
697
|
+
if (i < 0) return pattern === spec ? "" : null;
|
|
698
|
+
const pre = pattern.slice(0, i);
|
|
699
|
+
const suf = pattern.slice(i + 1);
|
|
700
|
+
if (spec.length >= pre.length + suf.length && spec.startsWith(pre) && spec.endsWith(suf)) {
|
|
701
|
+
return spec.slice(pre.length, spec.length - suf.length);
|
|
702
|
+
}
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
function pickCondition(v) {
|
|
706
|
+
if (typeof v === "string") return [v];
|
|
707
|
+
if (Array.isArray(v)) return v.flatMap(pickCondition);
|
|
708
|
+
if (v && typeof v === "object") {
|
|
709
|
+
const o = v;
|
|
710
|
+
for (const c of CONDITIONS) {
|
|
711
|
+
if (c in o) {
|
|
712
|
+
const r = pickCondition(o[c]);
|
|
713
|
+
if (r.length > 0) return r;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
for (const val of Object.values(o)) {
|
|
717
|
+
const r = pickCondition(val);
|
|
718
|
+
if (r.length > 0) return r;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return [];
|
|
722
|
+
}
|
|
723
|
+
function resolveExports(exports, subpath) {
|
|
724
|
+
if (exports === null || exports === void 0) return [];
|
|
725
|
+
if (typeof exports === "string") return subpath === "." ? [exports] : [];
|
|
726
|
+
if (Array.isArray(exports)) return exports.flatMap((e) => resolveExports(e, subpath));
|
|
727
|
+
if (typeof exports !== "object") return [];
|
|
728
|
+
const obj = exports;
|
|
729
|
+
const keys = Object.keys(obj);
|
|
730
|
+
if (!keys.some((k) => k.startsWith("."))) return subpath === "." ? pickCondition(obj) : [];
|
|
731
|
+
const exact = obj[subpath];
|
|
732
|
+
if (exact !== void 0) return pickCondition(exact);
|
|
733
|
+
for (const k of keys) {
|
|
734
|
+
if (!k.includes("*")) continue;
|
|
735
|
+
const m = matchPattern(k, subpath);
|
|
736
|
+
if (m === null) continue;
|
|
737
|
+
return pickCondition(obj[k]).map((v) => v.replace("*", m));
|
|
738
|
+
}
|
|
739
|
+
return [];
|
|
740
|
+
}
|
|
741
|
+
var Resolver = class {
|
|
742
|
+
constructor(root, files, manifests, tsconfigs, warnings) {
|
|
743
|
+
this.files = files;
|
|
744
|
+
for (const rel of manifests) {
|
|
745
|
+
try {
|
|
746
|
+
const j = JSON.parse(readFileSync(join2(root, rel), "utf8"));
|
|
747
|
+
if (typeof j.name !== "string") continue;
|
|
748
|
+
const main2 = [j.main, j.module, j.types].find((v) => typeof v === "string");
|
|
749
|
+
this.packages.push({
|
|
750
|
+
name: j.name,
|
|
751
|
+
dir: dirOf(rel),
|
|
752
|
+
exports: j.exports ?? null,
|
|
753
|
+
main: main2 ?? null
|
|
754
|
+
});
|
|
755
|
+
} catch (e) {
|
|
756
|
+
warnings.push(`could not read ${rel}: ${e instanceof Error ? e.message : String(e)}`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
this.packages.sort((a, b) => b.name.length - a.name.length);
|
|
760
|
+
for (const rel of tsconfigs) {
|
|
761
|
+
const read = ts3.readConfigFile(join2(root, rel), (p) => readFileSync(p, "utf8"));
|
|
762
|
+
const config = read.config;
|
|
763
|
+
if (read.error || !config) continue;
|
|
764
|
+
const co = config.compilerOptions ?? {};
|
|
765
|
+
const paths = co.paths;
|
|
766
|
+
if (!paths || typeof paths !== "object") continue;
|
|
767
|
+
const dir = dirOf(rel);
|
|
768
|
+
const base = typeof co.baseUrl === "string" ? under(dir, co.baseUrl).replace(/^\.$/, "") : dir;
|
|
769
|
+
const mappings = [];
|
|
770
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
771
|
+
if (!Array.isArray(targets)) continue;
|
|
772
|
+
mappings.push({
|
|
773
|
+
pattern,
|
|
774
|
+
targets: targets.filter((t) => typeof t === "string")
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
this.tsconfigs.push({ dir, base, paths: mappings });
|
|
778
|
+
}
|
|
779
|
+
this.tsconfigs.sort((a, b) => b.dir.length - a.dir.length);
|
|
780
|
+
}
|
|
781
|
+
packages = [];
|
|
782
|
+
tsconfigs = [];
|
|
783
|
+
cache = /* @__PURE__ */ new Map();
|
|
784
|
+
/** Project-relative path of the module `spec` refers to from `fromRel`, or null when it is outside the project. */
|
|
785
|
+
resolve(spec, fromRel) {
|
|
786
|
+
const key = `${fromRel}\0${spec}`;
|
|
787
|
+
const cached = this.cache.get(key);
|
|
788
|
+
if (cached !== void 0) return cached;
|
|
789
|
+
let hit = null;
|
|
790
|
+
for (const base of this.candidates(spec, fromRel)) {
|
|
791
|
+
hit = this.firstExisting(base);
|
|
792
|
+
if (hit) break;
|
|
793
|
+
}
|
|
794
|
+
this.cache.set(key, hit);
|
|
795
|
+
return hit;
|
|
796
|
+
}
|
|
797
|
+
candidates(spec, fromRel) {
|
|
798
|
+
const out = [];
|
|
799
|
+
if (spec.startsWith(".")) {
|
|
800
|
+
out.push(posix.normalize(posix.join(posix.dirname(fromRel), spec)));
|
|
801
|
+
return out;
|
|
802
|
+
}
|
|
803
|
+
const cfg = this.tsconfigs.find((c) => c.dir === "" || fromRel.startsWith(`${c.dir}/`));
|
|
804
|
+
if (cfg) {
|
|
805
|
+
for (const { pattern, targets } of cfg.paths) {
|
|
806
|
+
const m = matchPattern(pattern, spec);
|
|
807
|
+
if (m === null) continue;
|
|
808
|
+
for (const t of targets) out.push(under(cfg.base, t.replace("*", m)));
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (spec.startsWith("@/") || spec.startsWith("~/")) {
|
|
812
|
+
const rest = spec.slice(2);
|
|
813
|
+
const appRoot = appRootOf(fromRel);
|
|
814
|
+
if (appRoot) out.push(posix.join(appRoot, rest), posix.join(appRoot, "src", rest));
|
|
815
|
+
out.push(rest, `src/${rest}`);
|
|
816
|
+
}
|
|
817
|
+
const pkg = this.packages.find((p) => spec === p.name || spec.startsWith(`${p.name}/`));
|
|
818
|
+
if (pkg) {
|
|
819
|
+
const sub = spec === pkg.name ? "." : `.${spec.slice(pkg.name.length)}`;
|
|
820
|
+
for (const t of resolveExports(pkg.exports, sub)) out.push(under(pkg.dir, t));
|
|
821
|
+
if (sub === ".") {
|
|
822
|
+
if (pkg.main) out.push(under(pkg.dir, pkg.main));
|
|
823
|
+
out.push(under(pkg.dir, "src/index"), under(pkg.dir, "index"));
|
|
824
|
+
} else {
|
|
825
|
+
out.push(under(pkg.dir, sub), under(pkg.dir, `src/${sub.slice(2)}`));
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return out;
|
|
829
|
+
}
|
|
830
|
+
firstExisting(base) {
|
|
831
|
+
const b = base.replace(/^\.\//, "");
|
|
832
|
+
const stems = [b];
|
|
833
|
+
const m = /\.(js|jsx|mjs|cjs)$/.exec(b);
|
|
834
|
+
if (m) stems.push(b.slice(0, -m[0].length));
|
|
835
|
+
for (const s of stems) {
|
|
836
|
+
if (this.files.has(s)) return s;
|
|
837
|
+
for (const ext of EXTENSIONS) if (this.files.has(s + ext)) return s + ext;
|
|
838
|
+
for (const ext of EXTENSIONS)
|
|
839
|
+
if (this.files.has(`${s}/index${ext}`)) return `${s}/index${ext}`;
|
|
840
|
+
}
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
};
|
|
588
844
|
|
|
589
845
|
// packages/parser/src/rls.ts
|
|
590
846
|
var CONSTRAINT_WORDS = /* @__PURE__ */ new Set([
|
|
@@ -629,7 +885,7 @@ function splitTopLevel(text) {
|
|
|
629
885
|
if (cur.trim()) out.push(cur);
|
|
630
886
|
return out.map((s) => s.trim()).filter((s) => s.length > 0);
|
|
631
887
|
}
|
|
632
|
-
var IDENT = String.raw`(?:public
|
|
888
|
+
var IDENT = String.raw`(?:"?public"?\.)?"?([A-Za-z_][A-Za-z0-9_]*)"?`;
|
|
633
889
|
var CREATE_TABLE = new RegExp(
|
|
634
890
|
String.raw`^create\s+table\s+(?:if\s+not\s+exists\s+)?${IDENT}\s*\(`,
|
|
635
891
|
"i"
|
|
@@ -717,22 +973,22 @@ function parseSqlForRls(rel, text, into) {
|
|
|
717
973
|
}
|
|
718
974
|
|
|
719
975
|
// packages/parser/src/supabase.ts
|
|
720
|
-
import
|
|
976
|
+
import ts4 from "typescript";
|
|
721
977
|
var CREATE_CLIENT_CALLEES = /^(createClient|createServerClient|createBrowserClient)$/;
|
|
722
978
|
function isCreateClientCall(call, sf) {
|
|
723
979
|
const callee = call.expression;
|
|
724
|
-
if (
|
|
725
|
-
if (
|
|
980
|
+
if (ts4.isIdentifier(callee)) return CREATE_CLIENT_CALLEES.test(callee.text);
|
|
981
|
+
if (ts4.isPropertyAccessExpression(callee)) return CREATE_CLIENT_CALLEES.test(callee.name.text);
|
|
726
982
|
return CREATE_CLIENT_CALLEES.test(callee.getText(sf));
|
|
727
983
|
}
|
|
728
984
|
function resolveArgText(expr, sf) {
|
|
729
985
|
const u = unwrap(expr);
|
|
730
|
-
if (!
|
|
986
|
+
if (!ts4.isIdentifier(u)) return expr.getText(sf);
|
|
731
987
|
let scope = expr.parent;
|
|
732
988
|
while (scope) {
|
|
733
|
-
if (
|
|
734
|
-
const decl = collect(scope,
|
|
735
|
-
(d) =>
|
|
989
|
+
if (ts4.isFunctionLike(scope) || ts4.isBlock(scope) || ts4.isSourceFile(scope)) {
|
|
990
|
+
const decl = collect(scope, ts4.isVariableDeclaration).find(
|
|
991
|
+
(d) => ts4.isIdentifier(d.name) && d.name.text === u.text && d.initializer
|
|
736
992
|
);
|
|
737
993
|
if (decl?.initializer) return `${u.text} = ${decl.initializer.getText(sf)}`;
|
|
738
994
|
}
|
|
@@ -751,13 +1007,13 @@ function classifyCreateClientCall(call, sf) {
|
|
|
751
1007
|
evidence: `${callee}() from @supabase/ssr acts as the signed-in user; RLS applies`
|
|
752
1008
|
};
|
|
753
1009
|
}
|
|
754
|
-
if (/
|
|
1010
|
+
if (/SERVICE_?ROLE|SECRET_KEY|SB_SECRET/i.test(keyArg)) {
|
|
755
1011
|
return {
|
|
756
1012
|
kind: "service_role",
|
|
757
1013
|
evidence: `key ${keyArg} is a service-role secret; RLS is bypassed`
|
|
758
1014
|
};
|
|
759
1015
|
}
|
|
760
|
-
const anonKey = /ANON|PUBLISHABLE
|
|
1016
|
+
const anonKey = /ANON|PUBLISHABLE/i.test(keyArg);
|
|
761
1017
|
if (anonKey && /Authorization|headers/i.test(optArg)) {
|
|
762
1018
|
return {
|
|
763
1019
|
kind: "user_scoped",
|
|
@@ -773,39 +1029,101 @@ function classifyCreateClientCall(call, sf) {
|
|
|
773
1029
|
return { kind: "unknown", evidence: `could not classify key argument ${keyArg || "(none)"}` };
|
|
774
1030
|
}
|
|
775
1031
|
var AUTH_CALL = /\.auth\.(getUser|getSession|getClaims)\s*\(/;
|
|
1032
|
+
function hasModifier(node, kind) {
|
|
1033
|
+
const mods = ts4.canHaveModifiers(node) ? ts4.getModifiers(node) : void 0;
|
|
1034
|
+
return mods?.some((m) => m.kind === kind) ?? false;
|
|
1035
|
+
}
|
|
776
1036
|
function analyzeModule(rel, sf) {
|
|
777
1037
|
const imports = /* @__PURE__ */ new Map();
|
|
1038
|
+
const reexports = [];
|
|
1039
|
+
const exportedNames = /* @__PURE__ */ new Set();
|
|
1040
|
+
let defaultExport = null;
|
|
1041
|
+
for (const stmt of sf.statements) {
|
|
1042
|
+
if (ts4.isImportDeclaration(stmt) && ts4.isStringLiteral(stmt.moduleSpecifier)) {
|
|
1043
|
+
const spec = stmt.moduleSpecifier.text;
|
|
1044
|
+
const clause = stmt.importClause;
|
|
1045
|
+
if (!clause) continue;
|
|
1046
|
+
if (clause.name) imports.set(clause.name.text, { spec, imported: "default" });
|
|
1047
|
+
const nb = clause.namedBindings;
|
|
1048
|
+
if (nb && ts4.isNamedImports(nb)) {
|
|
1049
|
+
for (const el of nb.elements) {
|
|
1050
|
+
imports.set(el.name.text, { spec, imported: (el.propertyName ?? el.name).text });
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (nb && ts4.isNamespaceImport(nb)) imports.set(nb.name.text, { spec, imported: "*" });
|
|
1054
|
+
} else if (ts4.isExportDeclaration(stmt)) {
|
|
1055
|
+
const spec = stmt.moduleSpecifier && ts4.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : null;
|
|
1056
|
+
const clause = stmt.exportClause;
|
|
1057
|
+
if (spec && !clause) reexports.push({ star: true, spec });
|
|
1058
|
+
else if (clause && ts4.isNamedExports(clause)) {
|
|
1059
|
+
for (const el of clause.elements) {
|
|
1060
|
+
const name = (el.propertyName ?? el.name).text;
|
|
1061
|
+
if (spec) reexports.push({ star: false, name, alias: el.name.text, spec });
|
|
1062
|
+
else exportedNames.add(name);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
} else if (ts4.isExportAssignment(stmt) && !stmt.isExportEquals) {
|
|
1066
|
+
const e = unwrap(stmt.expression);
|
|
1067
|
+
if (ts4.isIdentifier(e)) defaultExport = e.text;
|
|
1068
|
+
} else if (ts4.isFunctionDeclaration(stmt) && stmt.name && hasModifier(stmt, ts4.SyntaxKind.DefaultKeyword)) {
|
|
1069
|
+
defaultExport = stmt.name.text;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
const functions = /* @__PURE__ */ new Map();
|
|
1073
|
+
for (const f of topLevelFunctions(sf)) {
|
|
1074
|
+
functions.set(f.name, exportedNames.has(f.name) ? { ...f, exported: true } : f);
|
|
1075
|
+
}
|
|
1076
|
+
const classes = /* @__PURE__ */ new Map();
|
|
1077
|
+
for (const c of classesIn(sf)) {
|
|
1078
|
+
classes.set(c.name, exportedNames.has(c.name) ? { ...c, exported: true } : c);
|
|
1079
|
+
}
|
|
1080
|
+
const moduleVars = /* @__PURE__ */ new Map();
|
|
778
1081
|
for (const stmt of sf.statements) {
|
|
779
|
-
if (!
|
|
780
|
-
const
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1082
|
+
if (!ts4.isVariableStatement(stmt)) continue;
|
|
1083
|
+
const exported = hasModifier(stmt, ts4.SyntaxKind.ExportKeyword);
|
|
1084
|
+
for (const d of stmt.declarationList.declarations) {
|
|
1085
|
+
if (!ts4.isIdentifier(d.name) || !d.initializer || functions.has(d.name.text)) continue;
|
|
1086
|
+
const init = unwrap(d.initializer);
|
|
1087
|
+
if (!ts4.isCallExpression(init) && !ts4.isNewExpression(init)) continue;
|
|
1088
|
+
moduleVars.set(d.name.text, {
|
|
1089
|
+
name: d.name.text,
|
|
1090
|
+
init,
|
|
1091
|
+
exported: exported || exportedNames.has(d.name.text),
|
|
1092
|
+
node: d
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
787
1095
|
}
|
|
788
1096
|
const clientFactories = [];
|
|
789
1097
|
const authHelpers = [];
|
|
790
|
-
for (const
|
|
791
|
-
const text =
|
|
792
|
-
const location = { file: rel, line: lineOf(sf,
|
|
1098
|
+
for (const f of functions.values()) {
|
|
1099
|
+
const text = f.fn.getText(sf);
|
|
1100
|
+
const location = { file: rel, line: lineOf(sf, f.node) };
|
|
793
1101
|
if (AUTH_CALL.test(text)) {
|
|
794
1102
|
authHelpers.push({
|
|
795
|
-
name:
|
|
1103
|
+
name: f.name,
|
|
796
1104
|
location,
|
|
797
1105
|
evidence: "calls supabase auth.getUser/getSession/getClaims"
|
|
798
1106
|
});
|
|
799
1107
|
continue;
|
|
800
1108
|
}
|
|
801
|
-
const creates = collect(
|
|
1109
|
+
const creates = collect(f.fn, ts4.isCallExpression).filter((c) => isCreateClientCall(c, sf));
|
|
802
1110
|
const first = creates[0];
|
|
803
1111
|
if (first) {
|
|
804
1112
|
const { kind, evidence } = classifyCreateClientCall(first, sf);
|
|
805
|
-
clientFactories.push({ name:
|
|
1113
|
+
clientFactories.push({ name: f.name, kind, location, evidence });
|
|
806
1114
|
}
|
|
807
1115
|
}
|
|
808
|
-
return {
|
|
1116
|
+
return {
|
|
1117
|
+
file: rel,
|
|
1118
|
+
clientFactories,
|
|
1119
|
+
authHelpers,
|
|
1120
|
+
imports,
|
|
1121
|
+
functions,
|
|
1122
|
+
classes,
|
|
1123
|
+
moduleVars,
|
|
1124
|
+
reexports,
|
|
1125
|
+
defaultExport
|
|
1126
|
+
};
|
|
809
1127
|
}
|
|
810
1128
|
|
|
811
1129
|
// packages/parser/src/parse-project.ts
|
|
@@ -831,151 +1149,411 @@ var FILTER_METHODS = /* @__PURE__ */ new Set([
|
|
|
831
1149
|
var WRITE_OPERATIONS = /* @__PURE__ */ new Set(["insert", "update", "upsert"]);
|
|
832
1150
|
var OPERATIONS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "upsert"]);
|
|
833
1151
|
var PUBLIC_SECRET_ENV = /process\.env\.NEXT_PUBLIC_[A-Z0-9_]*(?:SERVICE_ROLE|SECRET)[A-Z0-9_]*/g;
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1152
|
+
var MAX_DEPTH = 3;
|
|
1153
|
+
var WRAPPER_AUTH = /enhance(Action|RouteHandler)|auth|protected|session|guard|require|withUser/i;
|
|
1154
|
+
var REQUEST_NAME = /^(req|request)$/;
|
|
1155
|
+
var NO_ARG = { client: null, instance: null, tainted: false, isRequest: false };
|
|
1156
|
+
function ownFunctionSym(facts, name, fn) {
|
|
1157
|
+
const factory = facts.clientFactories.find((c) => c.name === name);
|
|
1158
|
+
if (factory) return { kind: "factory", factory };
|
|
1159
|
+
const helper = facts.authHelpers.find((a) => a.name === name);
|
|
1160
|
+
if (helper) return { kind: "auth", helper };
|
|
1161
|
+
return { kind: "function", name, fn, facts };
|
|
1162
|
+
}
|
|
1163
|
+
function exportedSym(p, tf, name, depth) {
|
|
1164
|
+
if (depth > 5) return null;
|
|
1165
|
+
const f = tf.functions.get(name);
|
|
1166
|
+
if (f?.exported) return ownFunctionSym(tf, name, f.fn);
|
|
1167
|
+
const c = tf.classes.get(name);
|
|
1168
|
+
if (c?.exported) return { kind: "class", cls: c, facts: tf };
|
|
1169
|
+
const v = tf.moduleVars.get(name);
|
|
1170
|
+
if (v?.exported) return { kind: "var", name, init: v.init, facts: tf };
|
|
1171
|
+
if (name === "default" && tf.defaultExport) {
|
|
1172
|
+
const local = tf.defaultExport;
|
|
1173
|
+
const lf = tf.functions.get(local);
|
|
1174
|
+
if (lf) return ownFunctionSym(tf, local, lf.fn);
|
|
1175
|
+
const lc = tf.classes.get(local);
|
|
1176
|
+
if (lc) return { kind: "class", cls: lc, facts: tf };
|
|
1177
|
+
const lv = tf.moduleVars.get(local);
|
|
1178
|
+
if (lv) return { kind: "var", name: local, init: lv.init, facts: tf };
|
|
1179
|
+
}
|
|
1180
|
+
for (const r of tf.reexports) {
|
|
1181
|
+
const target = p.resolver.resolve(r.spec, tf.file);
|
|
1182
|
+
const t = target ? p.registry.get(target) : void 0;
|
|
1183
|
+
if (!t) continue;
|
|
1184
|
+
if (r.star) {
|
|
1185
|
+
const s = exportedSym(p, t, name, depth + 1);
|
|
1186
|
+
if (s) return s;
|
|
1187
|
+
} else if (r.alias === name) {
|
|
1188
|
+
const s = exportedSym(p, t, r.name, depth + 1);
|
|
1189
|
+
if (s) return s;
|
|
857
1190
|
}
|
|
858
1191
|
}
|
|
859
1192
|
return null;
|
|
860
1193
|
}
|
|
861
|
-
function
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
for (const [
|
|
867
|
-
|
|
868
|
-
|
|
1194
|
+
function scopeOf(p, facts) {
|
|
1195
|
+
const cached = p.scopes.get(facts.file);
|
|
1196
|
+
if (cached) return cached;
|
|
1197
|
+
const scope = /* @__PURE__ */ new Map();
|
|
1198
|
+
p.scopes.set(facts.file, scope);
|
|
1199
|
+
for (const [name, f] of facts.functions) scope.set(name, ownFunctionSym(facts, name, f.fn));
|
|
1200
|
+
for (const [name, c] of facts.classes) scope.set(name, { kind: "class", cls: c, facts });
|
|
1201
|
+
for (const [name, v] of facts.moduleVars) {
|
|
1202
|
+
scope.set(name, { kind: "var", name, init: v.init, facts });
|
|
1203
|
+
}
|
|
1204
|
+
for (const [local, ref] of facts.imports) {
|
|
1205
|
+
const target = p.resolver.resolve(ref.spec, facts.file);
|
|
1206
|
+
const tf = target ? p.registry.get(target) : void 0;
|
|
869
1207
|
if (tf) {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
matched
|
|
886
|
-
|
|
1208
|
+
if (ref.imported === "*") scope.set(local, { kind: "namespace", facts: tf });
|
|
1209
|
+
else {
|
|
1210
|
+
const sym = exportedSym(p, tf, ref.imported, 0);
|
|
1211
|
+
if (sym) scope.set(local, sym);
|
|
1212
|
+
}
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
if (!/^[.~]|^@\//.test(ref.spec)) continue;
|
|
1216
|
+
for (const f of p.registry.values()) {
|
|
1217
|
+
const fn = f.functions.get(ref.imported);
|
|
1218
|
+
if (!fn?.exported) continue;
|
|
1219
|
+
const sym = ownFunctionSym(f, ref.imported, fn.fn);
|
|
1220
|
+
if (sym.kind === "factory" || sym.kind === "auth") {
|
|
1221
|
+
scope.set(local, sym);
|
|
1222
|
+
p.warnings.push(
|
|
1223
|
+
`unresolved import "${ref.spec}" in ${facts.file}; matched "${ref.imported}" by name`
|
|
1224
|
+
);
|
|
1225
|
+
break;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
return scope;
|
|
1230
|
+
}
|
|
1231
|
+
function symOfCallee(p, callee, scope) {
|
|
1232
|
+
if (ts5.isIdentifier(callee)) return scope.get(callee.text);
|
|
1233
|
+
if (ts5.isPropertyAccessExpression(callee) && ts5.isIdentifier(callee.expression)) {
|
|
1234
|
+
const ns = scope.get(callee.expression.text);
|
|
1235
|
+
if (ns?.kind === "namespace") return exportedSym(p, ns.facts, callee.name.text, 0) ?? void 0;
|
|
1236
|
+
}
|
|
1237
|
+
return void 0;
|
|
1238
|
+
}
|
|
1239
|
+
function returnedExpressions(fn) {
|
|
1240
|
+
if (!fn.body) return [];
|
|
1241
|
+
if (!ts5.isBlock(fn.body)) return [fn.body];
|
|
1242
|
+
return collect(fn.body, ts5.isReturnStatement).map((r) => r.expression).filter((e) => e !== void 0);
|
|
1243
|
+
}
|
|
1244
|
+
function factoryOfFunction(p, sym, depth) {
|
|
1245
|
+
const key = `${sym.facts.file}#${sym.name}`;
|
|
1246
|
+
if (p.factoryOfFn.has(key)) return p.factoryOfFn.get(key) ?? null;
|
|
1247
|
+
p.factoryOfFn.set(key, null);
|
|
1248
|
+
const sf = p.sources.get(sym.facts.file);
|
|
1249
|
+
if (!sf || depth > 2) return null;
|
|
1250
|
+
const scope = scopeOf(p, sym.facts);
|
|
1251
|
+
let found = null;
|
|
1252
|
+
for (const ret of returnedExpressions(sym.fn)) {
|
|
1253
|
+
const u = unwrap(ret);
|
|
1254
|
+
if (!ts5.isCallExpression(u)) continue;
|
|
1255
|
+
found = classifyCall(p, u, sf, scope, null, depth + 1);
|
|
1256
|
+
if (found) break;
|
|
1257
|
+
}
|
|
1258
|
+
p.factoryOfFn.set(key, found);
|
|
1259
|
+
return found;
|
|
1260
|
+
}
|
|
1261
|
+
function classifyCall(p, call, sf, scope, _frame, depth) {
|
|
1262
|
+
const sym = symOfCallee(p, call.expression, scope);
|
|
1263
|
+
if (sym?.kind === "factory") {
|
|
1264
|
+
return { kind: sym.factory.kind, name: sym.factory.name, location: sym.factory.location };
|
|
1265
|
+
}
|
|
1266
|
+
if (sym?.kind === "function") {
|
|
1267
|
+
const b = factoryOfFunction(p, sym, depth);
|
|
1268
|
+
if (b) return b;
|
|
1269
|
+
}
|
|
1270
|
+
if (isCreateClientCall(call, sf)) {
|
|
1271
|
+
const c = classifyCreateClientCall(call, sf);
|
|
1272
|
+
return {
|
|
1273
|
+
kind: c.kind,
|
|
1274
|
+
name: call.expression.getText(sf),
|
|
1275
|
+
location: { file: sf.fileName, line: lineOf(sf, call) }
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
return null;
|
|
1279
|
+
}
|
|
1280
|
+
function instanceOfCall(p, call, frame, scope) {
|
|
1281
|
+
const sym = symOfCallee(p, call.expression, scope);
|
|
1282
|
+
if (sym?.kind !== "function") return null;
|
|
1283
|
+
const fscope = scopeOf(p, sym.facts);
|
|
1284
|
+
const params = sym.fn.parameters.map((pp) => ts5.isIdentifier(pp.name) ? pp.name.text : null);
|
|
1285
|
+
for (const ret of returnedExpressions(sym.fn)) {
|
|
1286
|
+
const u = unwrap(ret);
|
|
1287
|
+
if (!ts5.isNewExpression(u) || !ts5.isIdentifier(u.expression)) continue;
|
|
1288
|
+
const cs = fscope.get(u.expression.text);
|
|
1289
|
+
if (cs?.kind !== "class") continue;
|
|
1290
|
+
const ctorArgs = (u.arguments ?? []).map((a) => {
|
|
1291
|
+
const ua = unwrap(a);
|
|
1292
|
+
if (ts5.isIdentifier(ua)) {
|
|
1293
|
+
const j = params.indexOf(ua.text);
|
|
1294
|
+
if (j >= 0 && frame) return argBinding(p, call.arguments[j], frame);
|
|
1295
|
+
}
|
|
1296
|
+
return NO_ARG;
|
|
1297
|
+
});
|
|
1298
|
+
return { cls: cs.cls, facts: cs.facts, ctorArgs };
|
|
1299
|
+
}
|
|
1300
|
+
return null;
|
|
1301
|
+
}
|
|
1302
|
+
function varBinding(p, sym) {
|
|
1303
|
+
const key = `${sym.facts.file}#${sym.name}`;
|
|
1304
|
+
const cached = p.varBindings.get(key);
|
|
1305
|
+
if (cached) return cached;
|
|
1306
|
+
p.varBindings.set(key, NO_ARG);
|
|
1307
|
+
const sf = p.sources.get(sym.facts.file);
|
|
1308
|
+
if (!sf) return NO_ARG;
|
|
1309
|
+
const scope = scopeOf(p, sym.facts);
|
|
1310
|
+
const init = unwrap(sym.init);
|
|
1311
|
+
let out = NO_ARG;
|
|
1312
|
+
if (ts5.isCallExpression(init)) {
|
|
1313
|
+
const found = classifyCall(p, init, sf, scope, null, 0);
|
|
1314
|
+
const client = found ? {
|
|
1315
|
+
kind: found.kind,
|
|
1316
|
+
name: sym.name,
|
|
1317
|
+
location: { file: sym.facts.file, line: lineOf(sf, init) }
|
|
1318
|
+
} : null;
|
|
1319
|
+
const instance = client ? null : instanceOfCall(p, init, null, scope);
|
|
1320
|
+
out = { client, instance, tainted: false, isRequest: false };
|
|
1321
|
+
} else if (ts5.isNewExpression(init) && ts5.isIdentifier(init.expression)) {
|
|
1322
|
+
const cs = scope.get(init.expression.text);
|
|
1323
|
+
if (cs?.kind === "class") {
|
|
1324
|
+
out = {
|
|
1325
|
+
client: null,
|
|
1326
|
+
instance: { cls: cs.cls, facts: cs.facts, ctorArgs: [] },
|
|
1327
|
+
tainted: false,
|
|
1328
|
+
isRequest: false
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
p.varBindings.set(key, out);
|
|
1333
|
+
return out;
|
|
1334
|
+
}
|
|
1335
|
+
function isQueryChain(call) {
|
|
1336
|
+
return flattenChain(call).segments.some((s) => s.name === "from" || s.name === "rpc");
|
|
1337
|
+
}
|
|
1338
|
+
function derivedIn(frame, e) {
|
|
1339
|
+
for (const id of identifiersIn(e)) if (frame.inputNames.has(id)) return true;
|
|
1340
|
+
return frame.depth === 0 && /^(params|body|query|searchParams)\b/.test(e.getText(frame.sf));
|
|
1341
|
+
}
|
|
1342
|
+
function argBinding(p, arg, frame) {
|
|
1343
|
+
if (!arg) return NO_ARG;
|
|
1344
|
+
const u = unwrap(arg);
|
|
1345
|
+
const scope = scopeOf(p, frame.facts);
|
|
1346
|
+
let client = null;
|
|
1347
|
+
let instance = null;
|
|
1348
|
+
let isRequest = false;
|
|
1349
|
+
if (ts5.isIdentifier(u)) {
|
|
1350
|
+
client = frame.clients.get(u.text) ?? null;
|
|
1351
|
+
instance = frame.instances.get(u.text) ?? null;
|
|
1352
|
+
isRequest = frame.reqNames.has(u.text);
|
|
1353
|
+
if (!client && !instance) {
|
|
1354
|
+
const sym = scope.get(u.text);
|
|
1355
|
+
if (sym?.kind === "var") {
|
|
1356
|
+
const vb = varBinding(p, sym);
|
|
1357
|
+
client = vb.client;
|
|
1358
|
+
instance = vb.instance;
|
|
887
1359
|
}
|
|
888
|
-
|
|
889
|
-
|
|
1360
|
+
}
|
|
1361
|
+
} else if (ts5.isPropertyAccessExpression(u) && u.expression.kind === ts5.SyntaxKind.ThisKeyword) {
|
|
1362
|
+
const tp = frame.thisProps.get(u.name.text);
|
|
1363
|
+
if (tp) {
|
|
1364
|
+
client = tp.client;
|
|
1365
|
+
instance = tp.instance;
|
|
1366
|
+
isRequest = tp.isRequest;
|
|
1367
|
+
}
|
|
1368
|
+
} else if (ts5.isCallExpression(u)) {
|
|
1369
|
+
client = classifyCall(p, u, frame.sf, scope, frame, 0);
|
|
1370
|
+
if (!client) instance = instanceOfCall(p, u, frame, scope);
|
|
1371
|
+
} else if (ts5.isNewExpression(u) && ts5.isIdentifier(u.expression)) {
|
|
1372
|
+
const cs = scope.get(u.expression.text);
|
|
1373
|
+
if (cs?.kind === "class") {
|
|
1374
|
+
instance = {
|
|
1375
|
+
cls: cs.cls,
|
|
1376
|
+
facts: cs.facts,
|
|
1377
|
+
ctorArgs: (u.arguments ?? []).map((a) => argBinding(p, a, frame))
|
|
1378
|
+
};
|
|
890
1379
|
}
|
|
891
1380
|
}
|
|
892
|
-
|
|
893
|
-
|
|
1381
|
+
return { client, instance, tainted: derivedIn(frame, arg), isRequest };
|
|
1382
|
+
}
|
|
1383
|
+
function methodTarget(inst, method) {
|
|
1384
|
+
const m = inst.cls.methods.get(method);
|
|
1385
|
+
if (!m) return null;
|
|
1386
|
+
const thisProps = /* @__PURE__ */ new Map();
|
|
1387
|
+
for (const [prop, idx] of inst.cls.propFromParam)
|
|
1388
|
+
thisProps.set(prop, inst.ctorArgs[idx] ?? NO_ARG);
|
|
1389
|
+
return { fn: m, facts: inst.facts, name: `${inst.cls.name}.${method}`, cls: inst.cls, thisProps };
|
|
1390
|
+
}
|
|
1391
|
+
function callTarget(p, call, frame, scope) {
|
|
1392
|
+
const callee = call.expression;
|
|
1393
|
+
if (ts5.isIdentifier(callee)) {
|
|
1394
|
+
const sym = scope.get(callee.text);
|
|
1395
|
+
if (sym?.kind === "function") {
|
|
1396
|
+
return { fn: sym.fn, facts: sym.facts, name: sym.name, cls: null, thisProps: /* @__PURE__ */ new Map() };
|
|
1397
|
+
}
|
|
1398
|
+
return null;
|
|
1399
|
+
}
|
|
1400
|
+
if (!ts5.isPropertyAccessExpression(callee)) return null;
|
|
1401
|
+
const obj = callee.expression;
|
|
1402
|
+
const method = callee.name.text;
|
|
1403
|
+
if (ts5.isIdentifier(obj)) {
|
|
1404
|
+
const inst = frame.instances.get(obj.text);
|
|
1405
|
+
if (inst) return methodTarget(inst, method);
|
|
1406
|
+
const sym = scope.get(obj.text);
|
|
1407
|
+
if (sym?.kind === "namespace") {
|
|
1408
|
+
const s = exportedSym(p, sym.facts, method, 0);
|
|
1409
|
+
if (s?.kind === "function") {
|
|
1410
|
+
return { fn: s.fn, facts: s.facts, name: s.name, cls: null, thisProps: /* @__PURE__ */ new Map() };
|
|
1411
|
+
}
|
|
1412
|
+
} else if (sym?.kind === "var") {
|
|
1413
|
+
const vb = varBinding(p, sym);
|
|
1414
|
+
if (vb.instance) return methodTarget(vb.instance, method);
|
|
1415
|
+
}
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
if (obj.kind === ts5.SyntaxKind.ThisKeyword && frame.cls) {
|
|
1419
|
+
const m = frame.cls.methods.get(method);
|
|
1420
|
+
if (!m) return null;
|
|
1421
|
+
return {
|
|
1422
|
+
fn: m,
|
|
1423
|
+
facts: frame.facts,
|
|
1424
|
+
name: `${frame.cls.name}.${method}`,
|
|
1425
|
+
cls: frame.cls,
|
|
1426
|
+
thisProps: frame.thisProps
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
if (ts5.isPropertyAccessExpression(obj) && obj.expression.kind === ts5.SyntaxKind.ThisKeyword) {
|
|
1430
|
+
const tp = frame.thisProps.get(obj.name.text);
|
|
1431
|
+
if (tp?.instance) return methodTarget(tp.instance, method);
|
|
1432
|
+
}
|
|
1433
|
+
return null;
|
|
1434
|
+
}
|
|
1435
|
+
function analyzeFrame(p, frame, acc) {
|
|
1436
|
+
const { rel, sf, fn } = frame;
|
|
1437
|
+
const scope = scopeOf(p, frame.facts);
|
|
1438
|
+
const loc2 = (n) => ({ file: rel, line: lineOf(sf, n) });
|
|
894
1439
|
const body = fn.body ?? fn;
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1440
|
+
for (const [name, sym] of scope) {
|
|
1441
|
+
if (sym.kind !== "var" || frame.clients.has(name) || frame.instances.has(name)) continue;
|
|
1442
|
+
const vb = varBinding(p, sym);
|
|
1443
|
+
if (vb.client) frame.clients.set(name, vb.client);
|
|
1444
|
+
if (vb.instance) frame.instances.set(name, vb.instance);
|
|
1445
|
+
}
|
|
899
1446
|
const addInput = (kind, name, n, bind) => {
|
|
900
|
-
if (!inputs.some((i) => i.kind === kind && i.name === name))
|
|
901
|
-
inputs.push({ kind, name, location: loc2(n) });
|
|
902
|
-
|
|
1447
|
+
if (!acc.inputs.some((i) => i.kind === kind && i.name === name)) {
|
|
1448
|
+
acc.inputs.push({ kind, name, location: loc2(n) });
|
|
1449
|
+
}
|
|
1450
|
+
if (bind) frame.inputNames.add(name);
|
|
1451
|
+
};
|
|
1452
|
+
const isRequestCall = (text) => {
|
|
1453
|
+
if (!/\.(json|formData|text)\(\)$/.test(text)) return false;
|
|
1454
|
+
for (const r of frame.reqNames) if (text.startsWith(`${r}.`)) return true;
|
|
1455
|
+
return frame.reqNames.size === 0 && /^(req|request)\./.test(text);
|
|
903
1456
|
};
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
if (
|
|
910
|
-
const init = unwrap(n.initializer);
|
|
911
|
-
const text = init.getText(sf);
|
|
1457
|
+
for (const decl of collect(body, ts5.isVariableDeclaration)) {
|
|
1458
|
+
if (!decl.initializer) continue;
|
|
1459
|
+
const init = unwrap(decl.initializer);
|
|
1460
|
+
const names = boundNames(decl.name);
|
|
1461
|
+
const text = init.getText(sf);
|
|
1462
|
+
if (frame.depth === 0) {
|
|
912
1463
|
if (/^(params|context\.params|ctx\.params|props\.params)$/.test(text)) {
|
|
913
|
-
for (const nm of
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
for (const nm of
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
for (const nm of
|
|
1464
|
+
for (const nm of names) addInput("route_param", nm, decl, true);
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1467
|
+
if (ts5.isCallExpression(init) && isRequestCall(text)) {
|
|
1468
|
+
for (const nm of names) addInput("body", nm, decl, true);
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
if (/searchParams\.get\(|\.searchParams$|^new URL\(/.test(text)) {
|
|
1472
|
+
for (const nm of names) addInput("query", nm, decl, true);
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
if (/headers\.get\(/.test(text)) {
|
|
1476
|
+
for (const nm of names) addInput("header", nm, decl, true);
|
|
1477
|
+
continue;
|
|
922
1478
|
}
|
|
923
1479
|
}
|
|
924
|
-
if (
|
|
925
|
-
|
|
1480
|
+
if (ts5.isCallExpression(init)) {
|
|
1481
|
+
const client = classifyCall(p, init, sf, scope, frame, 0);
|
|
1482
|
+
if (client && ts5.isIdentifier(decl.name)) {
|
|
1483
|
+
frame.clients.set(decl.name.text, client);
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
const inst = instanceOfCall(p, init, frame, scope);
|
|
1487
|
+
if (inst && ts5.isIdentifier(decl.name)) {
|
|
1488
|
+
frame.instances.set(decl.name.text, inst);
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
if (isQueryChain(init)) continue;
|
|
1492
|
+
if (returnsIdentity(p, init, sf, scope)) continue;
|
|
1493
|
+
const args = init.arguments.map((a) => argBinding(p, a, frame));
|
|
1494
|
+
if (args.some((a) => a.tainted || a.isRequest)) {
|
|
1495
|
+
for (const nm of names) frame.inputNames.add(nm);
|
|
1496
|
+
}
|
|
1497
|
+
} else if (ts5.isNewExpression(init) && ts5.isIdentifier(init.expression)) {
|
|
1498
|
+
const cs = scope.get(init.expression.text);
|
|
1499
|
+
if (cs?.kind === "class" && ts5.isIdentifier(decl.name)) {
|
|
1500
|
+
frame.instances.set(decl.name.text, {
|
|
1501
|
+
cls: cs.cls,
|
|
1502
|
+
facts: cs.facts,
|
|
1503
|
+
ctorArgs: (init.arguments ?? []).map((a) => argBinding(p, a, frame))
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
} else if (ts5.isIdentifier(init)) {
|
|
1507
|
+
const c = frame.clients.get(init.text);
|
|
1508
|
+
if (c && ts5.isIdentifier(decl.name)) frame.clients.set(decl.name.text, c);
|
|
1509
|
+
const i = frame.instances.get(init.text);
|
|
1510
|
+
if (i && ts5.isIdentifier(decl.name)) frame.instances.set(decl.name.text, i);
|
|
1511
|
+
if (frame.inputNames.has(init.text)) {
|
|
1512
|
+
for (const nm of names) {
|
|
1513
|
+
if (frame.depth === 0) addInput("body", nm, decl, true);
|
|
1514
|
+
else frame.inputNames.add(nm);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
} else if (!isChainWithQuery(init) && derivedIn(frame, init)) {
|
|
1518
|
+
for (const nm of names) frame.inputNames.add(nm);
|
|
926
1519
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1520
|
+
}
|
|
1521
|
+
if (frame.depth === 0) {
|
|
1522
|
+
walk(body, (n) => {
|
|
1523
|
+
if (ts5.isPropertyAccessExpression(n) && ts5.isIdentifier(n.expression) && n.expression.text === "params") {
|
|
1524
|
+
addInput("route_param", n.name.text, n, false);
|
|
1525
|
+
}
|
|
1526
|
+
return void 0;
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
933
1529
|
const isWholeInput = (e) => {
|
|
934
1530
|
const u = unwrap(e);
|
|
935
|
-
if (
|
|
936
|
-
if (
|
|
1531
|
+
if (ts5.isIdentifier(u)) return frame.inputNames.has(u.text);
|
|
1532
|
+
if (ts5.isObjectLiteralExpression(u)) {
|
|
937
1533
|
return u.properties.some(
|
|
938
|
-
(
|
|
1534
|
+
(pr) => ts5.isSpreadAssignment(pr) && ts5.isIdentifier(unwrap(pr.expression)) && frame.inputNames.has(unwrap(pr.expression).text)
|
|
939
1535
|
);
|
|
940
1536
|
}
|
|
941
1537
|
return false;
|
|
942
1538
|
};
|
|
943
|
-
const
|
|
944
|
-
if (ts4.isIdentifier(call.expression)) {
|
|
945
|
-
const f = factories.get(call.expression.text);
|
|
946
|
-
if (f) return { kind: f.kind, name: f.name, location: f.location };
|
|
947
|
-
}
|
|
948
|
-
if (isCreateClientCall(call, sf)) {
|
|
949
|
-
const c = classifyCreateClientCall(call, sf);
|
|
950
|
-
return { kind: c.kind, name: call.expression.getText(sf), location: loc2(call) };
|
|
951
|
-
}
|
|
952
|
-
return null;
|
|
953
|
-
};
|
|
954
|
-
const clients = /* @__PURE__ */ new Map();
|
|
955
|
-
const authChecks = [];
|
|
956
|
-
for (const call of collect(body, ts4.isCallExpression)) {
|
|
1539
|
+
for (const call of collect(body, ts5.isCallExpression)) {
|
|
957
1540
|
const calleeText = call.expression.getText(sf);
|
|
958
|
-
if (/\.auth\.(getUser|getSession|getClaims)$/.test(calleeText)) authChecks.push(loc2(call));
|
|
959
|
-
else if (
|
|
960
|
-
authChecks.push(loc2(call));
|
|
961
|
-
}
|
|
962
|
-
for (const decl of collect(body, ts4.isVariableDeclaration)) {
|
|
963
|
-
if (!decl.initializer || !ts4.isIdentifier(decl.name)) continue;
|
|
964
|
-
const init = unwrap(decl.initializer);
|
|
965
|
-
if (!ts4.isCallExpression(init)) continue;
|
|
966
|
-
const binding = classifyClientCall(init);
|
|
967
|
-
if (binding) clients.set(decl.name.text, binding);
|
|
1541
|
+
if (/\.auth\.(getUser|getSession|getClaims)$/.test(calleeText)) acc.authChecks.push(loc2(call));
|
|
1542
|
+
else if (symOfCallee(p, call.expression, scope)?.kind === "auth")
|
|
1543
|
+
acc.authChecks.push(loc2(call));
|
|
968
1544
|
}
|
|
969
|
-
const
|
|
970
|
-
for (const pa of collect(body, ts4.isPropertyAccessExpression)) {
|
|
1545
|
+
for (const pa of collect(body, ts5.isPropertyAccessExpression)) {
|
|
971
1546
|
const inner = pa.expression;
|
|
972
|
-
if (
|
|
973
|
-
metadataAccesses.push({
|
|
1547
|
+
if (ts5.isPropertyAccessExpression(inner) && (inner.name.text === "user_metadata" || inner.name.text === "app_metadata")) {
|
|
1548
|
+
acc.metadataAccesses.push({
|
|
1549
|
+
path: pa.getText(sf),
|
|
1550
|
+
bucket: inner.name.text,
|
|
1551
|
+
location: loc2(pa)
|
|
1552
|
+
});
|
|
974
1553
|
}
|
|
975
1554
|
}
|
|
976
|
-
const queries = [];
|
|
977
1555
|
const seen = /* @__PURE__ */ new Set();
|
|
978
|
-
for (const call of collect(body,
|
|
1556
|
+
for (const call of collect(body, ts5.isCallExpression)) {
|
|
979
1557
|
if (!isChainTail(call)) continue;
|
|
980
1558
|
const chain = flattenChain(call);
|
|
981
1559
|
const fromIdx = chain.segments.findIndex((s) => s.name === "from");
|
|
@@ -985,21 +1563,18 @@ function analyzeHandler(ctx) {
|
|
|
985
1563
|
if (anchorIdx < 0 || !anchor) continue;
|
|
986
1564
|
if (seen.has(anchor.node.pos)) continue;
|
|
987
1565
|
seen.add(anchor.node.pos);
|
|
988
|
-
let
|
|
1566
|
+
let binding = null;
|
|
989
1567
|
let clientName = null;
|
|
990
|
-
let clientLocation = null;
|
|
991
1568
|
const root = unwrap(chain.root);
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
binding = clients.get(root.text) ?? null;
|
|
1569
|
+
if (ts5.isIdentifier(root)) {
|
|
1570
|
+
binding = frame.clients.get(root.text) ?? null;
|
|
995
1571
|
if (!binding) clientName = root.text;
|
|
996
|
-
} else if (
|
|
997
|
-
binding =
|
|
998
|
-
|
|
999
|
-
if (
|
|
1000
|
-
|
|
1001
|
-
clientName =
|
|
1002
|
-
clientLocation = binding.location;
|
|
1572
|
+
} else if (ts5.isCallExpression(root)) {
|
|
1573
|
+
binding = classifyCall(p, root, sf, scope, frame, 0);
|
|
1574
|
+
if (!binding) clientName = root.expression.getText(sf);
|
|
1575
|
+
} else if (ts5.isPropertyAccessExpression(root) && root.expression.kind === ts5.SyntaxKind.ThisKeyword) {
|
|
1576
|
+
binding = frame.thisProps.get(root.name.text)?.client ?? null;
|
|
1577
|
+
if (!binding) clientName = root.getText(sf);
|
|
1003
1578
|
}
|
|
1004
1579
|
const after = chain.segments.slice(anchorIdx + 1);
|
|
1005
1580
|
let operation = fromIdx >= 0 ? "unknown" : "rpc";
|
|
@@ -1011,7 +1586,7 @@ function analyzeHandler(ctx) {
|
|
|
1011
1586
|
if (WRITE_OPERATIONS.has(operation) && arg) {
|
|
1012
1587
|
payload = {
|
|
1013
1588
|
text: arg.getText(sf).replace(/\s+/g, " ").slice(0, 200),
|
|
1014
|
-
inputDerived:
|
|
1589
|
+
inputDerived: derivedIn(frame, arg),
|
|
1015
1590
|
wholeInput: isWholeInput(arg)
|
|
1016
1591
|
};
|
|
1017
1592
|
}
|
|
@@ -1022,14 +1597,14 @@ function analyzeHandler(ctx) {
|
|
|
1022
1597
|
for (const s of after) {
|
|
1023
1598
|
if (!FILTER_METHODS.has(s.name)) continue;
|
|
1024
1599
|
const first = s.args[0];
|
|
1025
|
-
if (s.name === "match" && first &&
|
|
1026
|
-
for (const
|
|
1027
|
-
if (
|
|
1600
|
+
if (s.name === "match" && first && ts5.isObjectLiteralExpression(first)) {
|
|
1601
|
+
for (const pr of first.properties) {
|
|
1602
|
+
if (ts5.isPropertyAssignment(pr)) {
|
|
1028
1603
|
filters.push({
|
|
1029
1604
|
method: "match",
|
|
1030
|
-
column:
|
|
1031
|
-
valueText:
|
|
1032
|
-
inputDerived:
|
|
1605
|
+
column: pr.name.getText(sf).replace(/['"]/g, ""),
|
|
1606
|
+
valueText: pr.initializer.getText(sf),
|
|
1607
|
+
inputDerived: derivedIn(frame, pr.initializer)
|
|
1033
1608
|
});
|
|
1034
1609
|
}
|
|
1035
1610
|
}
|
|
@@ -1040,38 +1615,137 @@ function analyzeHandler(ctx) {
|
|
|
1040
1615
|
method: s.name,
|
|
1041
1616
|
column: stringLiteralValue(first),
|
|
1042
1617
|
valueText: val ? val.getText(sf) : "",
|
|
1043
|
-
inputDerived: val ?
|
|
1618
|
+
inputDerived: val ? derivedIn(frame, val) : false
|
|
1044
1619
|
});
|
|
1045
1620
|
}
|
|
1046
|
-
|
|
1621
|
+
const query = {
|
|
1047
1622
|
table: stringLiteralValue(anchor.args[0]) ?? "(dynamic)",
|
|
1048
1623
|
operation,
|
|
1049
|
-
client,
|
|
1050
|
-
clientName,
|
|
1051
|
-
clientLocation,
|
|
1624
|
+
client: binding?.kind ?? "unknown",
|
|
1625
|
+
clientName: binding?.name ?? clientName,
|
|
1626
|
+
clientLocation: binding?.location ?? null,
|
|
1052
1627
|
filters,
|
|
1053
1628
|
payload,
|
|
1054
1629
|
location: loc2(anchor.node),
|
|
1055
1630
|
text: call.getText(sf).replace(/\s+/g, " ").slice(0, 200)
|
|
1631
|
+
};
|
|
1632
|
+
if (frame.via.length > 0) query.via = frame.via;
|
|
1633
|
+
acc.queries.push(query);
|
|
1634
|
+
}
|
|
1635
|
+
if (frame.depth >= MAX_DEPTH) return;
|
|
1636
|
+
for (const call of collect(body, ts5.isCallExpression)) {
|
|
1637
|
+
const target = callTarget(p, call, frame, scope);
|
|
1638
|
+
if (!target) continue;
|
|
1639
|
+
const tsf = p.sources.get(target.facts.file);
|
|
1640
|
+
if (!tsf) continue;
|
|
1641
|
+
const child = {
|
|
1642
|
+
rel: target.facts.file,
|
|
1643
|
+
sf: tsf,
|
|
1644
|
+
facts: target.facts,
|
|
1645
|
+
fn: target.fn,
|
|
1646
|
+
depth: frame.depth + 1,
|
|
1647
|
+
via: [...frame.via, `${target.name} (${target.facts.file}:${lineOf(tsf, target.fn)})`],
|
|
1648
|
+
inputNames: /* @__PURE__ */ new Set(),
|
|
1649
|
+
reqNames: /* @__PURE__ */ new Set(),
|
|
1650
|
+
clients: /* @__PURE__ */ new Map(),
|
|
1651
|
+
instances: /* @__PURE__ */ new Map(),
|
|
1652
|
+
cls: target.cls,
|
|
1653
|
+
thisProps: target.thisProps
|
|
1654
|
+
};
|
|
1655
|
+
target.fn.parameters.forEach((param, i) => {
|
|
1656
|
+
const ab = argBinding(p, call.arguments[i], frame);
|
|
1657
|
+
const names = boundNames(param.name);
|
|
1658
|
+
const head = names[0];
|
|
1659
|
+
if (ab.client && head !== void 0 && ts5.isIdentifier(param.name)) {
|
|
1660
|
+
child.clients.set(head, ab.client);
|
|
1661
|
+
}
|
|
1662
|
+
if (ab.instance && head !== void 0 && ts5.isIdentifier(param.name)) {
|
|
1663
|
+
child.instances.set(head, ab.instance);
|
|
1664
|
+
}
|
|
1665
|
+
for (const nm of names) {
|
|
1666
|
+
if (ab.isRequest) child.reqNames.add(nm);
|
|
1667
|
+
if (ab.tainted) child.inputNames.add(nm);
|
|
1668
|
+
}
|
|
1056
1669
|
});
|
|
1670
|
+
const signature = [
|
|
1671
|
+
...[...child.clients].map(([n, c]) => `${n}=${c.kind}`),
|
|
1672
|
+
...[...child.inputNames].map((n) => `${n}!`),
|
|
1673
|
+
...[...child.reqNames].map((n) => `${n}?`),
|
|
1674
|
+
...[...child.thisProps].map(([n, b]) => `this.${n}=${b.client?.kind ?? "-"}`)
|
|
1675
|
+
].sort();
|
|
1676
|
+
const key = `${target.facts.file}#${target.name}#${signature.join(",")}`;
|
|
1677
|
+
if (acc.visited.has(key)) continue;
|
|
1678
|
+
acc.visited.add(key);
|
|
1679
|
+
analyzeFrame(p, child, acc);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
function isChainWithQuery(e) {
|
|
1683
|
+
return ts5.isCallExpression(e) && isQueryChain(e);
|
|
1684
|
+
}
|
|
1685
|
+
var IDENTITY_CALLEE = /\.auth\.|user|session|claims|auth|principal|viewer/i;
|
|
1686
|
+
function returnsIdentity(p, call, sf, scope) {
|
|
1687
|
+
if (symOfCallee(p, call.expression, scope)?.kind === "auth") return true;
|
|
1688
|
+
return IDENTITY_CALLEE.test(call.expression.getText(sf));
|
|
1689
|
+
}
|
|
1690
|
+
function analyzeHandler(p, h) {
|
|
1691
|
+
const { rel, sf, fn } = h;
|
|
1692
|
+
const loc2 = (n) => ({ file: rel, line: lineOf(sf, n) });
|
|
1693
|
+
const acc = {
|
|
1694
|
+
inputs: [],
|
|
1695
|
+
authChecks: [],
|
|
1696
|
+
queries: [],
|
|
1697
|
+
metadataAccesses: [],
|
|
1698
|
+
visited: /* @__PURE__ */ new Set()
|
|
1699
|
+
};
|
|
1700
|
+
const frame = {
|
|
1701
|
+
rel,
|
|
1702
|
+
sf,
|
|
1703
|
+
facts: h.facts,
|
|
1704
|
+
fn,
|
|
1705
|
+
depth: 0,
|
|
1706
|
+
via: [],
|
|
1707
|
+
inputNames: /* @__PURE__ */ new Set(["params", "searchParams"]),
|
|
1708
|
+
reqNames: /* @__PURE__ */ new Set(),
|
|
1709
|
+
clients: /* @__PURE__ */ new Map(),
|
|
1710
|
+
instances: /* @__PURE__ */ new Map(),
|
|
1711
|
+
cls: null,
|
|
1712
|
+
thisProps: /* @__PURE__ */ new Map()
|
|
1713
|
+
};
|
|
1714
|
+
const first = fn.parameters[0];
|
|
1715
|
+
if (h.kind === "route" && first) {
|
|
1716
|
+
if (ts5.isIdentifier(first.name)) frame.reqNames.add(first.name.text);
|
|
1717
|
+
else for (const nm of boundNames(first.name)) if (REQUEST_NAME.test(nm)) frame.reqNames.add(nm);
|
|
1718
|
+
}
|
|
1719
|
+
if (h.kind === "server_action") {
|
|
1720
|
+
const params = h.wrapper ? fn.parameters.slice(0, 1) : fn.parameters;
|
|
1721
|
+
for (const prm of params) {
|
|
1722
|
+
for (const nm of boundNames(prm.name)) {
|
|
1723
|
+
if (!acc.inputs.some((i) => i.kind === "action_arg" && i.name === nm)) {
|
|
1724
|
+
acc.inputs.push({ kind: "action_arg", name: nm, location: loc2(prm) });
|
|
1725
|
+
}
|
|
1726
|
+
frame.inputNames.add(nm);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1057
1729
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1730
|
+
if (h.wrapper && WRAPPER_AUTH.test(h.wrapper)) acc.authChecks.push(loc2(h.node));
|
|
1731
|
+
analyzeFrame(p, frame, acc);
|
|
1732
|
+
const entry = h.kind === "route" ? `${h.method} ${h.route}` : h.kind === "page" ? `PAGE ${h.route}` : `server action ${h.route}`;
|
|
1733
|
+
const stmt = enclosingStatement(h.node);
|
|
1060
1734
|
const ignores = parseIgnoreDirectives(sf, stmt.getFullStart()).map((d) => ({
|
|
1061
1735
|
ruleId: d.ruleId,
|
|
1062
1736
|
reason: d.reason,
|
|
1063
1737
|
location: { file: rel, line: d.line }
|
|
1064
1738
|
}));
|
|
1065
1739
|
return {
|
|
1066
|
-
kind:
|
|
1067
|
-
route:
|
|
1068
|
-
method:
|
|
1740
|
+
kind: h.kind,
|
|
1741
|
+
route: h.route,
|
|
1742
|
+
method: h.method,
|
|
1069
1743
|
entry,
|
|
1070
|
-
location: loc2(
|
|
1071
|
-
inputs,
|
|
1072
|
-
authChecks,
|
|
1073
|
-
queries,
|
|
1074
|
-
metadataAccesses,
|
|
1744
|
+
location: loc2(h.node),
|
|
1745
|
+
inputs: acc.inputs,
|
|
1746
|
+
authChecks: acc.authChecks,
|
|
1747
|
+
queries: acc.queries,
|
|
1748
|
+
metadataAccesses: acc.metadataAccesses,
|
|
1075
1749
|
ignores
|
|
1076
1750
|
};
|
|
1077
1751
|
}
|
|
@@ -1087,7 +1761,7 @@ function findExposures(rel, sf) {
|
|
|
1087
1761
|
});
|
|
1088
1762
|
}
|
|
1089
1763
|
if (isClientComponentFile(sf)) {
|
|
1090
|
-
for (const call of collect(sf,
|
|
1764
|
+
for (const call of collect(sf, ts5.isCallExpression)) {
|
|
1091
1765
|
if (!isCreateClientCall(call, sf)) continue;
|
|
1092
1766
|
const c = classifyCreateClientCall(call, sf);
|
|
1093
1767
|
if (c.kind === "service_role") {
|
|
@@ -1106,12 +1780,16 @@ function findExposures(rel, sf) {
|
|
|
1106
1780
|
}
|
|
1107
1781
|
function parseProject(rootInput, opts = {}) {
|
|
1108
1782
|
const root = resolve2(rootInput);
|
|
1109
|
-
const { source, sql } = discoverFiles(
|
|
1783
|
+
const { source, sql, manifests, tsconfigs } = discoverFiles(
|
|
1784
|
+
root,
|
|
1785
|
+
opts.sqlDirs ?? [],
|
|
1786
|
+
opts.ignore ?? []
|
|
1787
|
+
);
|
|
1110
1788
|
const warnings = [];
|
|
1111
1789
|
const sources = /* @__PURE__ */ new Map();
|
|
1112
1790
|
for (const rel of source) {
|
|
1113
1791
|
try {
|
|
1114
|
-
sources.set(rel, parseSource(rel,
|
|
1792
|
+
sources.set(rel, parseSource(rel, readFileSync2(join3(root, rel), "utf8")));
|
|
1115
1793
|
} catch (e) {
|
|
1116
1794
|
warnings.push(`could not read ${rel}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1117
1795
|
}
|
|
@@ -1121,12 +1799,20 @@ function parseProject(rootInput, opts = {}) {
|
|
|
1121
1799
|
const tables = /* @__PURE__ */ new Map();
|
|
1122
1800
|
for (const rel of sql) {
|
|
1123
1801
|
try {
|
|
1124
|
-
parseSqlForRls(rel,
|
|
1802
|
+
parseSqlForRls(rel, readFileSync2(resolve2(root, rel), "utf8"), tables);
|
|
1125
1803
|
} catch (e) {
|
|
1126
1804
|
warnings.push(`could not read ${rel}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1127
1805
|
}
|
|
1128
1806
|
}
|
|
1129
|
-
const
|
|
1807
|
+
const project = {
|
|
1808
|
+
sources,
|
|
1809
|
+
registry,
|
|
1810
|
+
resolver: new Resolver(root, new Set(source), manifests, tsconfigs, warnings),
|
|
1811
|
+
scopes: /* @__PURE__ */ new Map(),
|
|
1812
|
+
factoryOfFn: /* @__PURE__ */ new Map(),
|
|
1813
|
+
varBindings: /* @__PURE__ */ new Map(),
|
|
1814
|
+
warnings
|
|
1815
|
+
};
|
|
1130
1816
|
const routes = [];
|
|
1131
1817
|
const exposures = [];
|
|
1132
1818
|
const fileIgnores = {};
|
|
@@ -1139,34 +1825,56 @@ function parseProject(rootInput, opts = {}) {
|
|
|
1139
1825
|
if (top.length > 0) fileIgnores[rel] = top;
|
|
1140
1826
|
exposures.push(...findExposures(rel, sf));
|
|
1141
1827
|
const facts = registry.get(rel) ?? analyzeModule(rel, sf);
|
|
1142
|
-
const common = { rel, sf, facts, registry, files, warnings };
|
|
1143
1828
|
const route = routeFromFile(rel);
|
|
1144
1829
|
if (route) {
|
|
1145
1830
|
for (const { method, exported } of routeHandlersIn(sf)) {
|
|
1146
1831
|
routes.push(
|
|
1147
|
-
analyzeHandler({
|
|
1148
|
-
|
|
1832
|
+
analyzeHandler(project, {
|
|
1833
|
+
rel,
|
|
1834
|
+
sf,
|
|
1835
|
+
facts,
|
|
1149
1836
|
kind: "route",
|
|
1150
1837
|
route,
|
|
1151
1838
|
method,
|
|
1152
1839
|
fn: exported.fn,
|
|
1153
|
-
node: exported.node
|
|
1840
|
+
node: exported.node,
|
|
1841
|
+
wrapper: exported.wrapper
|
|
1154
1842
|
})
|
|
1155
1843
|
);
|
|
1156
1844
|
}
|
|
1157
1845
|
} else if (isServerActionFile(sf)) {
|
|
1158
1846
|
for (const ex of exportedFunctions(sf)) {
|
|
1159
1847
|
routes.push(
|
|
1160
|
-
analyzeHandler({
|
|
1161
|
-
|
|
1848
|
+
analyzeHandler(project, {
|
|
1849
|
+
rel,
|
|
1850
|
+
sf,
|
|
1851
|
+
facts,
|
|
1162
1852
|
kind: "server_action",
|
|
1163
1853
|
route: ex.name,
|
|
1164
1854
|
method: "ACTION",
|
|
1165
1855
|
fn: ex.fn,
|
|
1166
|
-
node: ex.node
|
|
1856
|
+
node: ex.node,
|
|
1857
|
+
wrapper: ex.wrapper
|
|
1167
1858
|
})
|
|
1168
1859
|
);
|
|
1169
1860
|
}
|
|
1861
|
+
} else {
|
|
1862
|
+
const page = pageFromFile(rel);
|
|
1863
|
+
const handler = page !== null && !isClientComponentFile(sf) ? pageHandlerIn(sf) : null;
|
|
1864
|
+
if (page !== null && handler) {
|
|
1865
|
+
const analysed = analyzeHandler(project, {
|
|
1866
|
+
rel,
|
|
1867
|
+
sf,
|
|
1868
|
+
facts,
|
|
1869
|
+
kind: "page",
|
|
1870
|
+
route: page,
|
|
1871
|
+
method: "PAGE",
|
|
1872
|
+
fn: handler.fn,
|
|
1873
|
+
node: handler.node,
|
|
1874
|
+
wrapper: handler.wrapper
|
|
1875
|
+
});
|
|
1876
|
+
if (analysed.queries.length > 0 || analysed.inputs.length > 0) routes.push(analysed);
|
|
1877
|
+
}
|
|
1170
1878
|
}
|
|
1171
1879
|
}
|
|
1172
1880
|
routes.sort((a, b) => a.entry.localeCompare(b.entry));
|
|
@@ -1241,6 +1949,9 @@ function queryViews(ctx, handler) {
|
|
|
1241
1949
|
function locations(...refs) {
|
|
1242
1950
|
return refs.filter((r) => r !== void 0);
|
|
1243
1951
|
}
|
|
1952
|
+
function viaNote(q) {
|
|
1953
|
+
return q.via && q.via.length > 0 ? ` Reached through ${q.via.join(" -> ")}.` : "";
|
|
1954
|
+
}
|
|
1244
1955
|
function rlsNote(t, tableName) {
|
|
1245
1956
|
if (!t?.known) return `public.${tableName} was not found in migrations; RLS state unknown.`;
|
|
1246
1957
|
if (!t.rlsEnabled) return `RLS is disabled on public.${tableName}.`;
|
|
@@ -1292,7 +2003,7 @@ var serviceRoleObjectAccessWithoutTenantScope = {
|
|
|
1292
2003
|
const evidence = [
|
|
1293
2004
|
{
|
|
1294
2005
|
kind: "rule",
|
|
1295
|
-
summary: `${q.operation} on public.${tableName} filtered by user-controlled "${idFilter.column}" through a service-role client, with no tenant/owner scoping. ${authNote} ${rlsNote(v.tableData, tableName)}`,
|
|
2006
|
+
summary: `${q.operation} on public.${tableName} filtered by user-controlled "${idFilter.column}" through a service-role client, with no tenant/owner scoping. ${authNote} ${rlsNote(v.tableData, tableName)}${viaNote(q)}`,
|
|
1296
2007
|
locations: locations(h.handler.location, v.query.location, v.client?.location),
|
|
1297
2008
|
data: {
|
|
1298
2009
|
deterministic: false,
|
|
@@ -1326,46 +2037,75 @@ var tableWithoutRls = {
|
|
|
1326
2037
|
confidence: 0.9,
|
|
1327
2038
|
cwe: ["CWE-284", "CWE-862"],
|
|
1328
2039
|
evaluate(ctx) {
|
|
1329
|
-
const
|
|
1330
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2040
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1331
2041
|
for (const h of handlerViews(ctx)) {
|
|
1332
2042
|
for (const v of queryViews(ctx, h.handler)) {
|
|
1333
2043
|
const c = v.clientData;
|
|
1334
2044
|
if (c?.kind !== "anon" && c?.kind !== "user_scoped") continue;
|
|
1335
2045
|
const t = v.tableData;
|
|
1336
2046
|
if (!t?.known || t.rlsEnabled) continue;
|
|
1337
|
-
const
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
`public.${t.table}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
sources: h.inputs.map((i) => `${i.kind}:${i.name}`),
|
|
1351
|
-
sinks: [`supabase.${v.data.operation}:public.${t.table}`],
|
|
1352
|
-
path,
|
|
1353
|
-
evidence: [
|
|
1354
|
-
{
|
|
1355
|
-
kind: "rule",
|
|
1356
|
-
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.`,
|
|
1357
|
-
locations: locations(v.query.location, v.table?.location),
|
|
1358
|
-
data: { deterministic: true, ruleId: this.id }
|
|
1359
|
-
},
|
|
1360
|
-
{ kind: "trace", summary: path.join(" -> ") }
|
|
1361
|
-
]
|
|
1362
|
-
})
|
|
1363
|
-
);
|
|
2047
|
+
const g = group(groups, t.table, () => ({
|
|
2048
|
+
path: [
|
|
2049
|
+
"HTTP request",
|
|
2050
|
+
h.data.entry,
|
|
2051
|
+
`${c.name} (${c.kind}, RLS would apply)`,
|
|
2052
|
+
`public.${t.table} (RLS disabled)`
|
|
2053
|
+
],
|
|
2054
|
+
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.`,
|
|
2055
|
+
title: `Table "${t.table}" is exposed without RLS`,
|
|
2056
|
+
data: { deterministic: true, ruleId: this.id },
|
|
2057
|
+
tail: locations(v.table?.location)
|
|
2058
|
+
}));
|
|
2059
|
+
addReach(g, h, v, `supabase.${v.data.operation}:public.${t.table}`);
|
|
1364
2060
|
}
|
|
1365
2061
|
}
|
|
1366
|
-
return
|
|
2062
|
+
return emitGroups(ctx, this, groups);
|
|
1367
2063
|
}
|
|
1368
2064
|
};
|
|
2065
|
+
function group(groups, key, init) {
|
|
2066
|
+
let g = groups.get(key);
|
|
2067
|
+
if (!g) {
|
|
2068
|
+
g = { ...init(), entrypoints: [], sources: /* @__PURE__ */ new Set(), sinks: /* @__PURE__ */ new Set(), queryLocations: [] };
|
|
2069
|
+
groups.set(key, g);
|
|
2070
|
+
}
|
|
2071
|
+
return g;
|
|
2072
|
+
}
|
|
2073
|
+
function addReach(g, h, v, sink) {
|
|
2074
|
+
if (!g.entrypoints.includes(h.data.entry)) g.entrypoints.push(h.data.entry);
|
|
2075
|
+
for (const i of h.inputs) g.sources.add(`${i.kind}:${i.name}`);
|
|
2076
|
+
g.sinks.add(sink);
|
|
2077
|
+
if (v.query.location && !g.queryLocations.some((l) => sameRef(l, v.query.location))) {
|
|
2078
|
+
g.queryLocations.push(v.query.location);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
function sameRef(a, b) {
|
|
2082
|
+
return b !== void 0 && a.file === b.file && a.line === b.line;
|
|
2083
|
+
}
|
|
2084
|
+
function emitGroups(ctx, rule, groups) {
|
|
2085
|
+
const out = [];
|
|
2086
|
+
for (const g of groups.values()) {
|
|
2087
|
+
const reached = g.entrypoints.length > 1 ? ` Reached from ${g.entrypoints.length} entry points.` : "";
|
|
2088
|
+
out.push(
|
|
2089
|
+
finding(ctx, rule, {
|
|
2090
|
+
title: g.title,
|
|
2091
|
+
entrypoints: g.entrypoints,
|
|
2092
|
+
sources: [...g.sources],
|
|
2093
|
+
sinks: [...g.sinks],
|
|
2094
|
+
path: g.path,
|
|
2095
|
+
evidence: [
|
|
2096
|
+
{
|
|
2097
|
+
kind: "rule",
|
|
2098
|
+
summary: g.summary + reached,
|
|
2099
|
+
locations: [...g.queryLocations, ...g.tail],
|
|
2100
|
+
data: g.data
|
|
2101
|
+
},
|
|
2102
|
+
{ kind: "trace", summary: g.path.join(" -> ") }
|
|
2103
|
+
]
|
|
2104
|
+
})
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
return out;
|
|
2108
|
+
}
|
|
1369
2109
|
var rlsPolicyWithoutCallerPredicate = {
|
|
1370
2110
|
id: "supabase.rls-policy-without-caller-predicate",
|
|
1371
2111
|
title: "RLS policy grants rows without a caller predicate",
|
|
@@ -1374,8 +2114,7 @@ var rlsPolicyWithoutCallerPredicate = {
|
|
|
1374
2114
|
confidence: 0.85,
|
|
1375
2115
|
cwe: ["CWE-863", "CWE-284"],
|
|
1376
2116
|
evaluate(ctx) {
|
|
1377
|
-
const
|
|
1378
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2117
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1379
2118
|
for (const h of handlerViews(ctx)) {
|
|
1380
2119
|
for (const v of queryViews(ctx, h.handler)) {
|
|
1381
2120
|
const c = v.clientData;
|
|
@@ -1388,37 +2127,23 @@ var rlsPolicyWithoutCallerPredicate = {
|
|
|
1388
2127
|
if (p.command !== "all" && p.command !== op) continue;
|
|
1389
2128
|
const expr = op === "insert" ? p.check : p.using;
|
|
1390
2129
|
if (expr === null || policyScopesToCaller(expr)) continue;
|
|
1391
|
-
const
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
`
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
sources: h.inputs.map((i) => `${i.kind}:${i.name}`),
|
|
1405
|
-
sinks: [`supabase.${op}:public.${t.table}`],
|
|
1406
|
-
path,
|
|
1407
|
-
evidence: [
|
|
1408
|
-
{
|
|
1409
|
-
kind: "rule",
|
|
1410
|
-
summary: `Policy "${p.name}" for ${p.command} on public.${t.table} uses (${expr}). The table has a scope column (${t.columns.filter(isScopeColumn).join(", ")}) but the policy never compares it to auth.uid() or the caller's tenant, so RLS lets every ${p.roles.join("/") || "authenticated"} user through.`,
|
|
1411
|
-
locations: locations(v.query.location, p.location),
|
|
1412
|
-
data: { deterministic: false, ruleId: this.id, policy: p.name }
|
|
1413
|
-
},
|
|
1414
|
-
{ kind: "trace", summary: path.join(" -> ") }
|
|
1415
|
-
]
|
|
1416
|
-
})
|
|
1417
|
-
);
|
|
2130
|
+
const g = group(groups, `${t.table}:${p.name}:${p.command}`, () => ({
|
|
2131
|
+
path: [
|
|
2132
|
+
"HTTP request",
|
|
2133
|
+
h.data.entry,
|
|
2134
|
+
`${c.name} (${c.kind})`,
|
|
2135
|
+
`public.${t.table} policy "${p.name}" ${op === "insert" ? "with check" : "using"} (${expr})`
|
|
2136
|
+
],
|
|
2137
|
+
summary: `Policy "${p.name}" for ${p.command} on public.${t.table} uses (${expr}). The table has a scope column (${t.columns.filter(isScopeColumn).join(", ")}) but the policy never compares it to auth.uid() or the caller's tenant, so RLS lets every ${p.roles.join("/") || "authenticated"} user through.`,
|
|
2138
|
+
title: `RLS policy "${p.name}" on "${t.table}" does not scope rows to the caller`,
|
|
2139
|
+
data: { deterministic: false, ruleId: this.id, policy: p.name },
|
|
2140
|
+
tail: locations(p.location)
|
|
2141
|
+
}));
|
|
2142
|
+
addReach(g, h, v, `supabase.${op}:public.${t.table}`);
|
|
1418
2143
|
}
|
|
1419
2144
|
}
|
|
1420
2145
|
}
|
|
1421
|
-
return
|
|
2146
|
+
return emitGroups(ctx, this, groups);
|
|
1422
2147
|
}
|
|
1423
2148
|
};
|
|
1424
2149
|
var userControlledTenantScope = {
|
|
@@ -1698,10 +2423,10 @@ var defaultRules = [...supabaseAuthorizationPack];
|
|
|
1698
2423
|
|
|
1699
2424
|
// packages/scanner/src/scan.ts
|
|
1700
2425
|
function readAuditConfig(root) {
|
|
1701
|
-
const p =
|
|
2426
|
+
const p = join4(root, "audit.config.json");
|
|
1702
2427
|
if (!existsSync(p)) return {};
|
|
1703
2428
|
try {
|
|
1704
|
-
const raw = JSON.parse(
|
|
2429
|
+
const raw = JSON.parse(readFileSync3(p, "utf8"));
|
|
1705
2430
|
return {
|
|
1706
2431
|
...Array.isArray(raw.ignore) ? { ignore: raw.ignore.filter((x) => typeof x === "string") } : {},
|
|
1707
2432
|
...Array.isArray(raw.migrations) ? { migrations: raw.migrations.filter((x) => typeof x === "string") } : {}
|