@justmpm/firebase-audit 0.4.3 → 0.5.1
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/chunk-7EWTBIKJ.js +356 -0
- package/dist/{chunk-3SEVYYCX.js → chunk-GDO2DLNQ.js} +218 -377
- package/dist/{chunk-IXMAWDJH.js → chunk-QACXCMYB.js} +33 -20
- package/dist/{chunk-NJBCSW7E.js → chunk-XZ32YQQC.js} +10 -0
- package/dist/{chunk-PSSMYRRU.js → chunk-Y4VXWSKX.js} +5 -2
- package/dist/cli.js +25 -18
- package/dist/{drift-KU7HWL7M.js → drift-QXALNO2V.js} +1 -1
- package/dist/index.d.ts +30 -20
- package/dist/index.js +12 -10
- package/dist/mcp-cli.js +5 -4
- package/dist/mcp.js +5 -4
- package/dist/rules-GTGZWN5U.js +44 -0
- package/dist/scan-4Q4XVLJU.js +7 -0
- package/dist/{verify-O722ZZBV.js → verify-WBVSESHD.js} +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +10 -7
- package/dist/scan-VREY2FGO.js +0 -6
|
@@ -1,10 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
conditionKeyForFingerprint,
|
|
3
|
+
extractRules,
|
|
4
|
+
findPublicAllowsContent,
|
|
5
|
+
inlineHelpersInCondition,
|
|
6
|
+
normalizeTarget,
|
|
7
|
+
opsFrom,
|
|
8
|
+
parseRuleFunctionsDetailed,
|
|
9
|
+
resolveHelperConditionDetailed,
|
|
10
|
+
stripOuterParens,
|
|
11
|
+
stripRuleComments
|
|
12
|
+
} from "./chunk-7EWTBIKJ.js";
|
|
13
|
+
|
|
1
14
|
// src/scan.ts
|
|
2
|
-
import { readFileSync as
|
|
15
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
3
16
|
import { join as join3 } from "path";
|
|
4
17
|
import { parse as parseYaml } from "yaml";
|
|
5
18
|
|
|
6
19
|
// src/discovery.ts
|
|
7
|
-
import { existsSync, readFileSync } from "fs";
|
|
20
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
8
21
|
import { join, relative } from "path";
|
|
9
22
|
function discover(rootDir) {
|
|
10
23
|
const pick = (name) => {
|
|
@@ -16,6 +29,16 @@ function discover(rootDir) {
|
|
|
16
29
|
let databaseId = null;
|
|
17
30
|
let rulesFile = pick("firestore.rules");
|
|
18
31
|
let indexesFile = pick("firestore.indexes.json");
|
|
32
|
+
let storageFile = pick("storage.rules");
|
|
33
|
+
let functionsDir = null;
|
|
34
|
+
const functionsPick = pick("functions");
|
|
35
|
+
if (functionsPick) {
|
|
36
|
+
try {
|
|
37
|
+
functionsDir = statSync(functionsPick).isDirectory() ? functionsPick : null;
|
|
38
|
+
} catch {
|
|
39
|
+
functionsDir = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
19
42
|
const firebaserc = pick(".firebaserc");
|
|
20
43
|
if (firebaserc) {
|
|
21
44
|
try {
|
|
@@ -36,6 +59,10 @@ function discover(rootDir) {
|
|
|
36
59
|
const p = join(rootDir, raw.firestore.indexes);
|
|
37
60
|
if (existsSync(p)) indexesFile = p;
|
|
38
61
|
}
|
|
62
|
+
if (typeof raw.storage?.rules === "string") {
|
|
63
|
+
const p = join(rootDir, raw.storage.rules);
|
|
64
|
+
if (existsSync(p)) storageFile = p;
|
|
65
|
+
}
|
|
39
66
|
} catch {
|
|
40
67
|
}
|
|
41
68
|
}
|
|
@@ -44,6 +71,8 @@ function discover(rootDir) {
|
|
|
44
71
|
firebaseJson,
|
|
45
72
|
firestoreRulesFile: rulesFile,
|
|
46
73
|
firestoreIndexesFile: indexesFile,
|
|
74
|
+
storageRulesFile: storageFile,
|
|
75
|
+
functionsDir,
|
|
47
76
|
auditYamlFile: pick("firebase-audit.yaml"),
|
|
48
77
|
projectId,
|
|
49
78
|
databaseId,
|
|
@@ -93,8 +122,10 @@ function emptyModel(rootDir, d) {
|
|
|
93
122
|
firestore: {
|
|
94
123
|
rulesFile: rel(d.firestoreRulesFile),
|
|
95
124
|
indexesFile: rel(d.firestoreIndexesFile)
|
|
96
|
-
}
|
|
125
|
+
},
|
|
126
|
+
storage: d.storageRulesFile ? { rulesFile: rel(d.storageRulesFile) } : void 0
|
|
97
127
|
},
|
|
128
|
+
functions: d.functionsDir ? { dir: rel(d.functionsDir) } : void 0,
|
|
98
129
|
authorization: { adapter: null, roles: [], permissions: [] },
|
|
99
130
|
queries: [],
|
|
100
131
|
rules: [],
|
|
@@ -109,14 +140,14 @@ async function enrichWithGraph(rootDir, d) {
|
|
|
109
140
|
const res = await ai.map({ cwd: rootDir, format: "json" });
|
|
110
141
|
const files = Array.isArray(res.files) ? res.files : [];
|
|
111
142
|
const code = files.map((f) => String(f.path).replace(/\\/g, "/")).filter((p) => /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(p)).filter((p) => !p.includes("node_modules/") && !/(^|\/)dist\//.test(p)).slice(0, 500);
|
|
112
|
-
const { readFileSync: readSync, existsSync: existsSyncFn, statSync } = await import("fs");
|
|
143
|
+
const { readFileSync: readSync, existsSync: existsSyncFn, statSync: statSync2 } = await import("fs");
|
|
113
144
|
const { join: joinP } = await import("path");
|
|
114
145
|
for (const rel of code) {
|
|
115
146
|
let content = "";
|
|
116
147
|
try {
|
|
117
148
|
const abs = joinP(rootDir, rel);
|
|
118
149
|
if (!existsSyncFn(abs)) continue;
|
|
119
|
-
if (
|
|
150
|
+
if (statSync2(abs).size > 100 * 1024) continue;
|
|
120
151
|
content = readSync(abs, "utf-8");
|
|
121
152
|
} catch {
|
|
122
153
|
content = "";
|
|
@@ -129,342 +160,8 @@ async function enrichWithGraph(rootDir, d) {
|
|
|
129
160
|
}
|
|
130
161
|
}
|
|
131
162
|
|
|
132
|
-
// src/rules.ts
|
|
133
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
134
|
-
import { createHash } from "crypto";
|
|
135
|
-
var MATCH_RE = /match\s+(\/[^{\s]*(?:\{[^}]*\}[^{\s]*)*)\s*\{/g;
|
|
136
|
-
var ALLOW_RE = /allow\s+([^;:]+)(?::\s*if\s+([^;]+))?;/g;
|
|
137
|
-
function stripRuleComments(content) {
|
|
138
|
-
let out = "";
|
|
139
|
-
let i = 0;
|
|
140
|
-
let quote = null;
|
|
141
|
-
while (i < content.length) {
|
|
142
|
-
const c = content[i];
|
|
143
|
-
const next = content[i + 1] ?? "";
|
|
144
|
-
if (quote) {
|
|
145
|
-
out += c;
|
|
146
|
-
if (c === quote && content[i - 1] !== "\\") quote = null;
|
|
147
|
-
i += 1;
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
if (c === '"' || c === "'" || c === "`") {
|
|
151
|
-
quote = c;
|
|
152
|
-
out += c;
|
|
153
|
-
i += 1;
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
if (c === "/" && next === "/") {
|
|
157
|
-
while (i < content.length && content[i] !== "\n") i += 1;
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
if (c === "/" && next === "*") {
|
|
161
|
-
i += 2;
|
|
162
|
-
while (i < content.length && !(content[i] === "*" && content[i + 1] === "/")) {
|
|
163
|
-
if (content[i] === "\n") out += "\n";
|
|
164
|
-
i += 1;
|
|
165
|
-
}
|
|
166
|
-
i += 2;
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
out += c;
|
|
170
|
-
i += 1;
|
|
171
|
-
}
|
|
172
|
-
return out;
|
|
173
|
-
}
|
|
174
|
-
function opsFrom(target) {
|
|
175
|
-
const t = target.trim().toLowerCase();
|
|
176
|
-
if (t === "read") return { ops: ["read", "get", "list"], known: true };
|
|
177
|
-
if (t === "write") return { ops: ["write", "create", "update", "delete"], known: true };
|
|
178
|
-
const parts = t.split(",").map((s) => s.trim()).filter(Boolean);
|
|
179
|
-
const valid = ["read", "get", "list", "create", "update", "delete", "write"];
|
|
180
|
-
const out = [];
|
|
181
|
-
let known = false;
|
|
182
|
-
for (const p of parts) {
|
|
183
|
-
if (valid.includes(p)) {
|
|
184
|
-
known = true;
|
|
185
|
-
out.push(p);
|
|
186
|
-
if (p === "write") {
|
|
187
|
-
for (const extra of ["create", "update", "delete"]) {
|
|
188
|
-
if (!out.includes(extra)) out.push(extra);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
if (p === "read") {
|
|
192
|
-
for (const extra of ["get", "list"]) {
|
|
193
|
-
if (!out.includes(extra)) out.push(extra);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
return { ops: out, known: out.length > 0 && known };
|
|
199
|
-
}
|
|
200
|
-
function findBlockEnd(content, openBraceIndex) {
|
|
201
|
-
let depth = 0;
|
|
202
|
-
let quote = null;
|
|
203
|
-
for (let i = openBraceIndex; i < content.length; i++) {
|
|
204
|
-
const c = content[i];
|
|
205
|
-
if (quote) {
|
|
206
|
-
if (c === quote && content[i - 1] !== "\\") quote = null;
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
if (c === '"' || c === "'" || c === "`") {
|
|
210
|
-
quote = c;
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
if (c === "{") depth += 1;
|
|
214
|
-
else if (c === "}") {
|
|
215
|
-
depth -= 1;
|
|
216
|
-
if (depth === 0) return i + 1;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return content.length;
|
|
220
|
-
}
|
|
221
|
-
function normalizeTarget(target) {
|
|
222
|
-
return target.toLowerCase().split(",").map((s) => s.trim()).filter(Boolean).sort().join(",");
|
|
223
|
-
}
|
|
224
|
-
function parseMatchBlocks(content) {
|
|
225
|
-
const blocks = [];
|
|
226
|
-
const matchRe = new RegExp(MATCH_RE.source, "g");
|
|
227
|
-
let m;
|
|
228
|
-
while ((m = matchRe.exec(content)) !== null) {
|
|
229
|
-
const before = content.slice(0, m.index);
|
|
230
|
-
const line = before.split("\n").length;
|
|
231
|
-
const openBrace = m.index + m[0].length - 1;
|
|
232
|
-
blocks.push({ path: m[1], line, blockStart: m.index, blockEnd: findBlockEnd(content, openBrace) });
|
|
233
|
-
}
|
|
234
|
-
return blocks;
|
|
235
|
-
}
|
|
236
|
-
function buildFullPath(containingAsc) {
|
|
237
|
-
const parts = [];
|
|
238
|
-
for (const b of containingAsc) {
|
|
239
|
-
const p = b.path.trim();
|
|
240
|
-
if (/^\/databases\/\{[^}]+\}\/documents\/?$/.test(p)) continue;
|
|
241
|
-
const stripped = p.replace(/^\/+|\/+$/g, "");
|
|
242
|
-
if (stripped.length === 0) continue;
|
|
243
|
-
const docIdx = stripped.indexOf("/documents/");
|
|
244
|
-
if (stripped.startsWith("databases/") && docIdx !== -1) {
|
|
245
|
-
const rest = stripped.slice(docIdx + "/documents/".length);
|
|
246
|
-
if (rest.length > 0) parts.push(rest);
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
parts.push(stripped);
|
|
250
|
-
}
|
|
251
|
-
if (parts.length === 0) return "/(unknown)";
|
|
252
|
-
return "/" + parts.join("/");
|
|
253
|
-
}
|
|
254
|
-
function parseRuleFunctions(content) {
|
|
255
|
-
const out = /* @__PURE__ */ new Map();
|
|
256
|
-
for (const [name, fn] of parseRuleFunctionsDetailed(content)) {
|
|
257
|
-
out.set(name, fn.body);
|
|
258
|
-
}
|
|
259
|
-
return out;
|
|
260
|
-
}
|
|
261
|
-
function parseRuleFunctionsDetailed(content) {
|
|
262
|
-
const out = /* @__PURE__ */ new Map();
|
|
263
|
-
const re = /function\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)\s*\{/g;
|
|
264
|
-
let m;
|
|
265
|
-
while ((m = re.exec(content)) !== null) {
|
|
266
|
-
const openBrace = m.index + m[0].length - 1;
|
|
267
|
-
const end = findBlockEnd(content, openBrace);
|
|
268
|
-
const body = content.slice(openBrace + 1, end - 1);
|
|
269
|
-
const params = m[2].trim() === "" ? [] : m[2].split(",").map((s) => s.trim()).filter((s) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(s));
|
|
270
|
-
out.set(m[1], { params, body });
|
|
271
|
-
}
|
|
272
|
-
return out;
|
|
273
|
-
}
|
|
274
|
-
function inlineHelperArgs(body, params, args) {
|
|
275
|
-
let out = body;
|
|
276
|
-
for (let i = 0; i < params.length; i++) {
|
|
277
|
-
const p = params[i];
|
|
278
|
-
const a = (args[i] ?? "").trim();
|
|
279
|
-
if (!p || !a) continue;
|
|
280
|
-
out = out.replace(new RegExp(`\\b${p}\\b`, "g"), () => a);
|
|
281
|
-
}
|
|
282
|
-
return out;
|
|
283
|
-
}
|
|
284
|
-
function inlineHelpersInCondition(condition, detailed) {
|
|
285
|
-
let out = condition;
|
|
286
|
-
for (const [name, fn] of detailed) {
|
|
287
|
-
const re = new RegExp(`\\b${name}\\s*\\(([^()]*)\\)`, "g");
|
|
288
|
-
out = out.replace(re, (_m, argsStr) => {
|
|
289
|
-
const args = argsStr.length === 0 ? [] : String(argsStr).split(",").map((s) => s.trim());
|
|
290
|
-
const inlined = inlineHelperArgs(fn.body, fn.params, args);
|
|
291
|
-
return `(${inlined})`;
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
return out;
|
|
295
|
-
}
|
|
296
|
-
function resolveHelperCondition(condition, functions) {
|
|
297
|
-
const t = condition.trim().replace(/;$/, "").trim();
|
|
298
|
-
const call = /^([A-Za-z_][A-Za-z0-9_]*)\s*\((.*)\)$/.exec(t);
|
|
299
|
-
if (!call) return condition;
|
|
300
|
-
const body = functions.get(call[1]);
|
|
301
|
-
if (body === void 0) return condition;
|
|
302
|
-
return body;
|
|
303
|
-
}
|
|
304
|
-
function resolveHelperConditionDetailed(condition, detailed) {
|
|
305
|
-
const t = condition.trim().replace(/;$/, "").trim();
|
|
306
|
-
const call = /^([A-Za-z_][A-Za-z0-9_]*)\s*\((.*)\)$/.exec(t);
|
|
307
|
-
if (!call) return condition;
|
|
308
|
-
const fn = detailed.get(call[1]);
|
|
309
|
-
if (!fn) return condition;
|
|
310
|
-
const args = call[2].length === 0 ? [] : call[2].split(",").map((s) => s.trim());
|
|
311
|
-
return inlineHelperArgs(fn.body, fn.params, args);
|
|
312
|
-
}
|
|
313
|
-
function extractRules(rulesFile, relFile) {
|
|
314
|
-
const rawContent = readFileSync2(rulesFile, "utf-8");
|
|
315
|
-
return extractRulesContent(rawContent, relFile);
|
|
316
|
-
}
|
|
317
|
-
function extractRulesContent(rawContent, relFile) {
|
|
318
|
-
const content = stripRuleComments(rawContent);
|
|
319
|
-
const rules = [];
|
|
320
|
-
const functions = parseRuleFunctions(content);
|
|
321
|
-
const detailed = parseRuleFunctionsDetailed(content);
|
|
322
|
-
const matchBlocks = parseMatchBlocks(content);
|
|
323
|
-
const allowRe = new RegExp(ALLOW_RE.source, "g");
|
|
324
|
-
let a;
|
|
325
|
-
let idx = 0;
|
|
326
|
-
while ((a = allowRe.exec(content)) !== null) {
|
|
327
|
-
const before = content.slice(0, a.index);
|
|
328
|
-
const line = before.split("\n").length;
|
|
329
|
-
const target = a[1];
|
|
330
|
-
const rawCondition = (a[2] ?? "").trim();
|
|
331
|
-
const unconditional = a[2] === void 0;
|
|
332
|
-
const containing = matchBlocks.filter((b) => b.blockStart <= a.index && a.index < b.blockEnd).sort((x, y) => x.blockStart - y.blockStart);
|
|
333
|
-
const fullPath = containing.length > 0 ? buildFullPath(containing) : "/(unknown)";
|
|
334
|
-
const resolved = unconditional ? "" : resolveHelperCondition(rawCondition, functions);
|
|
335
|
-
const resolvedDetailed = unconditional ? "" : resolveHelperConditionDetailed(rawCondition, detailed);
|
|
336
|
-
const inlinedBody = unconditional ? "" : inlineHelpersInCondition(rawCondition, detailed);
|
|
337
|
-
const effectiveBody = resolvedDetailed !== rawCondition ? resolvedDetailed : resolved !== rawCondition ? resolved : inlinedBody;
|
|
338
|
-
const condition = rawCondition;
|
|
339
|
-
const authReferences = [];
|
|
340
|
-
const authSource = `${rawCondition} ${resolved} ${effectiveBody} ${inlinedBody}`;
|
|
341
|
-
if (authSource.includes("request.auth")) authReferences.push("request.auth");
|
|
342
|
-
if (authSource.includes("request.auth.uid")) authReferences.push("request.auth.uid");
|
|
343
|
-
const big = `${rawCondition} ${resolved} ${effectiveBody} ${inlinedBody}`;
|
|
344
|
-
const claimReferences = [
|
|
345
|
-
...big.matchAll(/request\.auth\.token\.([A-Za-z0-9_]+)/g),
|
|
346
|
-
...big.matchAll(/request\.auth\.token\.get\(\s*['"]([A-Za-z0-9_]+)['"]/g),
|
|
347
|
-
...big.matchAll(/request\.auth\.token\[\s*['"]([A-Za-z0-9_]+)['"]\s*\]/g)
|
|
348
|
-
].map((x) => x[1]);
|
|
349
|
-
const resourceReferences = [...big.matchAll(/resource\.data\.([A-Za-z0-9_]+)/g)].map(
|
|
350
|
-
(x) => x[1]
|
|
351
|
-
);
|
|
352
|
-
const requestResourceReferences = [
|
|
353
|
-
...big.matchAll(/request\.resource\.data\.([A-Za-z0-9_]+)/g)
|
|
354
|
-
].map((x) => x[1]);
|
|
355
|
-
const { ops, known } = opsFrom(target);
|
|
356
|
-
const isHelperCall = !unconditional && rawCondition !== resolved;
|
|
357
|
-
const hasAuth = authSource.includes("request.auth");
|
|
358
|
-
rules.push({
|
|
359
|
-
id: `rule-${idx++}`,
|
|
360
|
-
path: fullPath,
|
|
361
|
-
operations: ops,
|
|
362
|
-
conditionPresent: !unconditional && condition.length > 0,
|
|
363
|
-
condition: unconditional ? void 0 : condition,
|
|
364
|
-
authReferences,
|
|
365
|
-
claimReferences,
|
|
366
|
-
resourceReferences,
|
|
367
|
-
requestResourceReferences,
|
|
368
|
-
confidence: !known || isHelperCall && !hasAuth ? "UNKNOWN" : "CONFIRMED",
|
|
369
|
-
location: { file: relFile, start: { line, column: 0 } }
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
return rules;
|
|
373
|
-
}
|
|
374
|
-
function stripOuterParens(s) {
|
|
375
|
-
let norm = s.trim();
|
|
376
|
-
for (; ; ) {
|
|
377
|
-
if (!(norm.startsWith("(") && norm.endsWith(")"))) return norm;
|
|
378
|
-
let depth = 0;
|
|
379
|
-
let wrapsAll = true;
|
|
380
|
-
for (let i = 0; i < norm.length; i++) {
|
|
381
|
-
if (norm[i] === "(") depth += 1;
|
|
382
|
-
else if (norm[i] === ")") {
|
|
383
|
-
depth -= 1;
|
|
384
|
-
if (depth === 0 && i !== norm.length - 1) {
|
|
385
|
-
wrapsAll = false;
|
|
386
|
-
break;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
if (!wrapsAll || depth !== 0) return norm;
|
|
391
|
-
norm = norm.slice(1, -1).trim();
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
function isPublicCondition(condition) {
|
|
395
|
-
if (condition === void 0) return { isPublic: true, confidence: "CONFIRMED" };
|
|
396
|
-
const norm = stripOuterParens(condition);
|
|
397
|
-
const low = norm.toLowerCase().replace(/\s+/g, "");
|
|
398
|
-
if (low === "false") return { isPublic: false, confidence: "CONFIRMED" };
|
|
399
|
-
if (/^!\(.*request\.auth(===|==)null/.test(low)) {
|
|
400
|
-
return { isPublic: false, confidence: "CONFIRMED" };
|
|
401
|
-
}
|
|
402
|
-
if (/^!\(.*request\.auth(!==|!=)null/.test(low)) {
|
|
403
|
-
return { isPublic: true, confidence: "CONFIRMED" };
|
|
404
|
-
}
|
|
405
|
-
if (low.includes("!(request.auth==null)") || low.includes("!(request.auth===null)") || low === "!request.auth==null" || low === "!request.auth===null") {
|
|
406
|
-
return { isPublic: false, confidence: "CONFIRMED" };
|
|
407
|
-
}
|
|
408
|
-
if (low === "true") return { isPublic: true, confidence: "CONFIRMED" };
|
|
409
|
-
if (/(^|\|\|)true($|\|\|)/.test(low)) return { isPublic: true, confidence: "PROBABLE" };
|
|
410
|
-
if (low.includes("request.auth==null") || low.includes("request.auth===null")) {
|
|
411
|
-
return { isPublic: true, confidence: "CONFIRMED" };
|
|
412
|
-
}
|
|
413
|
-
if (!low.includes("request.auth")) {
|
|
414
|
-
return { isPublic: true, confidence: "PROBABLE" };
|
|
415
|
-
}
|
|
416
|
-
return { isPublic: false, confidence: "CONFIRMED" };
|
|
417
|
-
}
|
|
418
|
-
function hashShort(text) {
|
|
419
|
-
return createHash("sha256").update(text, "utf-8").digest("hex").slice(0, 8);
|
|
420
|
-
}
|
|
421
|
-
function conditionKeyForFingerprint(condition) {
|
|
422
|
-
if (condition === void 0) return "uncond";
|
|
423
|
-
const norm = stripOuterParens(condition).toLowerCase().replace(/\s+/g, "");
|
|
424
|
-
if (norm === "") return "uncond";
|
|
425
|
-
return hashShort(norm);
|
|
426
|
-
}
|
|
427
|
-
function findPublicAllows(rulesFile) {
|
|
428
|
-
const rawContent = readFileSync2(rulesFile, "utf-8");
|
|
429
|
-
return findPublicAllowsContent(rawContent);
|
|
430
|
-
}
|
|
431
|
-
function helperBodyToCondition(body) {
|
|
432
|
-
const m = /\breturn\b\s*([^;]+);?/.exec(body);
|
|
433
|
-
if (m) return m[1].trim();
|
|
434
|
-
return body;
|
|
435
|
-
}
|
|
436
|
-
function findPublicAllowsContent(rawContent) {
|
|
437
|
-
const content = stripRuleComments(rawContent);
|
|
438
|
-
const functions = parseRuleFunctions(content);
|
|
439
|
-
const detailed = parseRuleFunctionsDetailed(content);
|
|
440
|
-
const out = [];
|
|
441
|
-
const parsed = parseMatchBlocks(content);
|
|
442
|
-
const re = new RegExp(ALLOW_RE.source, "g");
|
|
443
|
-
let mm;
|
|
444
|
-
while ((mm = re.exec(content)) !== null) {
|
|
445
|
-
const line = content.slice(0, mm.index).split("\n").length;
|
|
446
|
-
const target = mm[1].trim();
|
|
447
|
-
const single = mm[2] === void 0 ? void 0 : resolveHelperCondition(mm[2], functions);
|
|
448
|
-
const singleD = mm[2] === void 0 ? void 0 : resolveHelperConditionDetailed(mm[2], detailed);
|
|
449
|
-
const inlined = mm[2] === void 0 ? void 0 : inlineHelpersInCondition(mm[2], detailed);
|
|
450
|
-
const effective = singleD !== void 0 && singleD !== mm[2] ? singleD : single !== void 0 && single !== mm[2] ? single : inlined !== void 0 && inlined !== mm[2] ? inlined : mm[2];
|
|
451
|
-
if (mm[2] !== void 0 && effective !== mm[2] && !String(effective).includes("request.auth")) {
|
|
452
|
-
const bodyCheck = isPublicCondition(helperBodyToCondition(String(effective)));
|
|
453
|
-
if (!bodyCheck.isPublic) {
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
const check = isPublicCondition(effective);
|
|
458
|
-
if (!check.isPublic) continue;
|
|
459
|
-
const ascBlocks = parsed.filter((b) => b.blockStart <= mm.index && mm.index < b.blockEnd).sort((x, y) => x.blockStart - y.blockStart);
|
|
460
|
-
const path = ascBlocks.length > 0 ? buildFullPath(ascBlocks) : "/(unknown)";
|
|
461
|
-
out.push({ line, target, path, confidence: check.confidence, conditionKey: conditionKeyForFingerprint(mm[2]) });
|
|
462
|
-
}
|
|
463
|
-
return out;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
163
|
// src/checks.ts
|
|
467
|
-
import { readFileSync as
|
|
164
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
468
165
|
import { join as join2 } from "path";
|
|
469
166
|
import { executeFind } from "@justmpm/supergrep";
|
|
470
167
|
var IMPLEMENTED_CHECKS = ["FBA001", "FBA002", "FBA003", "FBA004", "FBA009", "FBA010", "FBA011", "FBA012", "FBA013"];
|
|
@@ -503,7 +200,7 @@ async function runChecks(model, rootDir, opts = {}) {
|
|
|
503
200
|
let helperDetailed = /* @__PURE__ */ new Map();
|
|
504
201
|
if (absRules && existsSync2(absRules)) {
|
|
505
202
|
try {
|
|
506
|
-
const rawAll =
|
|
203
|
+
const rawAll = readFileSync2(absRules, "utf-8");
|
|
507
204
|
pubs = findPublicAllowsContent(rawAll);
|
|
508
205
|
helperDetailed = parseRuleFunctionsDetailed(rawAll);
|
|
509
206
|
} catch {
|
|
@@ -555,12 +252,15 @@ async function runChecks(model, rootDir, opts = {}) {
|
|
|
555
252
|
const permissionCalls = await collectPermissionCalls(rootDir, adapterFn);
|
|
556
253
|
const declared = new Set(model.authorization.permissions.map((p) => p.name));
|
|
557
254
|
if (permissionCalls.literals.length === 0 && permissionCalls.dynamic.length === 0) {
|
|
255
|
+
const tenantHint = [...helperDetailed.keys()].find(
|
|
256
|
+
(n) => /tenant|isMember|isSignedIn|_member/i.test(n)
|
|
257
|
+
);
|
|
558
258
|
findings.push(
|
|
559
259
|
make(
|
|
560
260
|
"FBA009",
|
|
561
261
|
"WARNING",
|
|
562
262
|
"UNKNOWN",
|
|
563
|
-
`Adapter
|
|
263
|
+
tenantHint ? `Adapter "${adapterFn}" n\xE3o observado no c\xF3digo. Encontrei portaria tenant (${tenantHint}) nas Rules, mas sem fun\xE7\xE3o de permiss\xE3o que receba o nome como texto n\xE3o d\xE1 para ligar papel\xD7recurso \u2014 permiss\xF5es seguem UNKNOWN.` : `Adapter "${adapterFn}" n\xE3o observado no c\xF3digo. Sem fun\xE7\xE3o de permiss\xE3o que receba o nome como texto, permiss\xF5es s\xE3o UNKNOWN.`,
|
|
564
264
|
`FBA009:${adapterFn}`,
|
|
565
265
|
{ evidence: [ev("adapter-missing", adapterFn, "UNKNOWN")] }
|
|
566
266
|
)
|
|
@@ -790,7 +490,7 @@ async function runChecks(model, rootDir, opts = {}) {
|
|
|
790
490
|
for (const hit of adminHits) {
|
|
791
491
|
let origin = "UNKNOWN";
|
|
792
492
|
try {
|
|
793
|
-
const content =
|
|
493
|
+
const content = readFileSync2(join2(rootDir, hit.file), "utf-8");
|
|
794
494
|
origin = classifyOrigin(hit.file, content);
|
|
795
495
|
} catch {
|
|
796
496
|
origin = "UNKNOWN";
|
|
@@ -1041,8 +741,15 @@ var ProjectModelSchema = z.strictObject({
|
|
|
1041
741
|
firestore: z.strictObject({
|
|
1042
742
|
rulesFile: z.string().optional(),
|
|
1043
743
|
indexesFile: z.string().optional()
|
|
1044
|
-
})
|
|
744
|
+
}),
|
|
745
|
+
storage: z.strictObject({
|
|
746
|
+
rulesFile: z.string().optional()
|
|
747
|
+
}).optional()
|
|
1045
748
|
}),
|
|
749
|
+
functions: z.strictObject({
|
|
750
|
+
dir: z.string().optional(),
|
|
751
|
+
appCheckEnforced: z.number().int().nonnegative().optional()
|
|
752
|
+
}).optional(),
|
|
1046
753
|
authorization: z.strictObject({
|
|
1047
754
|
adapter: z.string().nullable(),
|
|
1048
755
|
roles: z.array(RoleSchema),
|
|
@@ -1136,7 +843,7 @@ async function scan(rootDir, opts = {}) {
|
|
|
1136
843
|
const findings_pre = [];
|
|
1137
844
|
try {
|
|
1138
845
|
if (d.firebaseJson && existsSync3(d.firebaseJson)) {
|
|
1139
|
-
const raw = JSON.parse(
|
|
846
|
+
const raw = JSON.parse(readFileSync3(d.firebaseJson, "utf-8"));
|
|
1140
847
|
for (const [kind, rel] of [
|
|
1141
848
|
["rules", raw.firestore?.rules],
|
|
1142
849
|
["indexes", raw.firestore?.indexes]
|
|
@@ -1191,7 +898,7 @@ async function scan(rootDir, opts = {}) {
|
|
|
1191
898
|
}
|
|
1192
899
|
if (d.firestoreIndexesFile && existsSync3(d.firestoreIndexesFile)) {
|
|
1193
900
|
try {
|
|
1194
|
-
const raw = JSON.parse(
|
|
901
|
+
const raw = JSON.parse(readFileSync3(d.firestoreIndexesFile, "utf-8"));
|
|
1195
902
|
if (Array.isArray(raw.fieldOverrides) && raw.fieldOverrides.length > 0) {
|
|
1196
903
|
findings_pre.push({
|
|
1197
904
|
rule: "FIELD_OVERRIDES_PRESENT",
|
|
@@ -1206,11 +913,11 @@ async function scan(rootDir, opts = {}) {
|
|
|
1206
913
|
}
|
|
1207
914
|
const validModes = ["ASCENDING", "DESCENDING", "ARRAY_CONTAINS", "VECTOR"];
|
|
1208
915
|
const entries = [];
|
|
1209
|
-
let
|
|
916
|
+
let skipped2 = 0;
|
|
1210
917
|
let scopeSkipped = 0;
|
|
1211
918
|
for (const i of raw.indexes ?? []) {
|
|
1212
919
|
if (typeof i.collectionGroup !== "string" || i.collectionGroup.length === 0) {
|
|
1213
|
-
|
|
920
|
+
skipped2 += 1;
|
|
1214
921
|
continue;
|
|
1215
922
|
}
|
|
1216
923
|
if (i.queryScope !== void 0 && i.queryScope !== "COLLECTION" && i.queryScope !== "COLLECTION_GROUP") {
|
|
@@ -1220,13 +927,13 @@ async function scan(rootDir, opts = {}) {
|
|
|
1220
927
|
const fields = [];
|
|
1221
928
|
for (const f of i.fields ?? []) {
|
|
1222
929
|
if (typeof f.fieldPath !== "string" || f.fieldPath.length === 0) {
|
|
1223
|
-
|
|
930
|
+
skipped2 += 1;
|
|
1224
931
|
continue;
|
|
1225
932
|
}
|
|
1226
933
|
const rawMode = f.vectorConfig ? "VECTOR" : f.arrayConfig === "CONTAINS" ? "ARRAY_CONTAINS" : typeof f.mode === "string" ? f.mode : f.order;
|
|
1227
934
|
const mode = rawMode;
|
|
1228
935
|
if (mode !== "ASCENDING" && mode !== "DESCENDING" && mode !== "ARRAY_CONTAINS" && mode !== "VECTOR") {
|
|
1229
|
-
|
|
936
|
+
skipped2 += 1;
|
|
1230
937
|
continue;
|
|
1231
938
|
}
|
|
1232
939
|
fields.push({ fieldPath: f.fieldPath, mode });
|
|
@@ -1239,11 +946,11 @@ async function scan(rootDir, opts = {}) {
|
|
|
1239
946
|
const last = entries[entries.length - 1];
|
|
1240
947
|
if (last.fields.length === 0) {
|
|
1241
948
|
entries.pop();
|
|
1242
|
-
|
|
949
|
+
skipped2 += 1;
|
|
1243
950
|
}
|
|
1244
951
|
}
|
|
1245
952
|
model.indexes = entries;
|
|
1246
|
-
const totalSkipped =
|
|
953
|
+
const totalSkipped = skipped2 + scopeSkipped;
|
|
1247
954
|
if (totalSkipped > 0) {
|
|
1248
955
|
findings_pre.push({
|
|
1249
956
|
rule: "INDEXES_INVALID",
|
|
@@ -1271,7 +978,7 @@ async function scan(rootDir, opts = {}) {
|
|
|
1271
978
|
}
|
|
1272
979
|
if (d.auditYamlFile && existsSync3(d.auditYamlFile)) {
|
|
1273
980
|
try {
|
|
1274
|
-
const text =
|
|
981
|
+
const text = readFileSync3(d.auditYamlFile, "utf-8");
|
|
1275
982
|
const parsed = parseYaml(text);
|
|
1276
983
|
const validated = AuditYamlSchema.safeParse(parsed);
|
|
1277
984
|
if (validated.success) {
|
|
@@ -1361,6 +1068,82 @@ async function scan(rootDir, opts = {}) {
|
|
|
1361
1068
|
}
|
|
1362
1069
|
const checksFindings = await runChecks(model, rootDir, { adapterFn: opts.adapterFn ?? model.authorization.adapter ?? void 0 });
|
|
1363
1070
|
const findings = [...findings_pre, ...checksFindings];
|
|
1071
|
+
const checked = ["firestore.rules", "indexes", "contract", "client-code"];
|
|
1072
|
+
const skipped = [];
|
|
1073
|
+
if (d.storageRulesFile && existsSync3(d.storageRulesFile)) {
|
|
1074
|
+
checked.push("storage.rules");
|
|
1075
|
+
try {
|
|
1076
|
+
const raw = readFileSync3(d.storageRulesFile, "utf-8");
|
|
1077
|
+
const relStorage = model.firebase.storage?.rulesFile ?? "storage.rules";
|
|
1078
|
+
const clean = stripRuleComments(raw);
|
|
1079
|
+
const low = clean.toLowerCase().replace(/\s+/g, "");
|
|
1080
|
+
const hasAllPaths = low.includes("allpaths=**") || /\{[^}]*=\*\*\}/.test(low);
|
|
1081
|
+
for (const m of parseStorageAllows(clean)) {
|
|
1082
|
+
const open = m.condition === void 0 || m.condition.trim().toLowerCase().replace(/\s+/g, "") === "true";
|
|
1083
|
+
if (!open) continue;
|
|
1084
|
+
const ops = expandStorageOps(m.target);
|
|
1085
|
+
const key = conditionKeyForFingerprint(m.condition);
|
|
1086
|
+
if (ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete")) {
|
|
1087
|
+
findings.push({
|
|
1088
|
+
rule: "STORAGE_PUBLIC_WRITE",
|
|
1089
|
+
severity: "ERROR",
|
|
1090
|
+
confidence: "CONFIRMED",
|
|
1091
|
+
message: `Storage com escrita aberta (${relStorage}, allow ${m.target}). Qualquer cliente pode escrever \u2014 cofre aberto, n\xE3o vitrine.`,
|
|
1092
|
+
fingerprint: `STORAGE_PUBLIC_WRITE:${m.target}:${key}`,
|
|
1093
|
+
file: relStorage,
|
|
1094
|
+
evidence: [{ id: `ev-storage-write-${findings.length}`, kind: "storage-public", summary: `${m.target} if true`, confidence: "CONFIRMED" }],
|
|
1095
|
+
fix: "Exigir request.auth (e dono/tenant) no write. Deixe get aberto s\xF3 em pasta de vitrine como logos/."
|
|
1096
|
+
});
|
|
1097
|
+
} else if (ops.has("read") || ops.has("get") || ops.has("list")) {
|
|
1098
|
+
if (hasAllPaths) {
|
|
1099
|
+
findings.push({
|
|
1100
|
+
rule: "STORAGE_ALL_PATHS_OPEN",
|
|
1101
|
+
severity: "WARNING",
|
|
1102
|
+
confidence: "PROBABLE",
|
|
1103
|
+
message: `Storage com leitura aberta em curinga total (${relStorage}, allow ${m.target}). Vale para tudo \u2014 se a inten\xE7\xE3o era s\xF3 logo, restrinja a /logos/{id}.`,
|
|
1104
|
+
fingerprint: `STORAGE_ALL_PATHS_OPEN:${m.target}:${key}`,
|
|
1105
|
+
file: relStorage,
|
|
1106
|
+
evidence: [{ id: `ev-storage-all-${findings.length}`, kind: "storage-public", summary: `${m.target} if true`, confidence: "PROBABLE" }],
|
|
1107
|
+
fix: "Troque o curinga total por pastas expl\xEDcitas (ex: /logos/{logoId}) com get: if true e write restrito."
|
|
1108
|
+
});
|
|
1109
|
+
} else {
|
|
1110
|
+
findings.push({
|
|
1111
|
+
rule: "STORAGE_PUBLIC_READ",
|
|
1112
|
+
severity: "INFO",
|
|
1113
|
+
confidence: "PROBABLE",
|
|
1114
|
+
message: `Storage com leitura aberta (${relStorage}, allow ${m.target}). Pode ser vitrine proposital (logos) \u2014 confirme que o write segue restrito.`,
|
|
1115
|
+
fingerprint: `STORAGE_PUBLIC_READ:${m.target}:${key}`,
|
|
1116
|
+
file: relStorage,
|
|
1117
|
+
evidence: [{ id: `ev-storage-read-${findings.length}`, kind: "storage-public", summary: `${m.target} if true`, confidence: "PROBABLE" }]
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
} catch {
|
|
1123
|
+
skipped.push("storage.rules ileg\xEDvel \u2014 Storage sem cobertura neste scan");
|
|
1124
|
+
}
|
|
1125
|
+
} else {
|
|
1126
|
+
skipped.push("storage.rules n\xE3o observado \u2014 Storage fora deste scan");
|
|
1127
|
+
}
|
|
1128
|
+
if (d.functionsDir) {
|
|
1129
|
+
const enforced = await countAppCheckEnforced(rootDir);
|
|
1130
|
+
model.functions = { dir: model.functions?.dir, appCheckEnforced: enforced };
|
|
1131
|
+
checked.push("functions-appcheck");
|
|
1132
|
+
if (enforced > 0) {
|
|
1133
|
+
findings.push({
|
|
1134
|
+
rule: "APPCHECK_ENFORCED",
|
|
1135
|
+
severity: "INFO",
|
|
1136
|
+
confidence: "CONFIRMED",
|
|
1137
|
+
message: `AppCheck ativo em ${enforced} function(s). Carimbo do app oficial \u2014 n\xE3o prova quem \xE9 o usu\xE1rio nem o tenant. Autoriza\xE7\xE3o segue nas Rules/guardas.`,
|
|
1138
|
+
fingerprint: "APPCHECK_ENFORCED",
|
|
1139
|
+
evidence: [{ id: "ev-appcheck", kind: "appcheck", summary: `${enforced} enforced`, confidence: "CONFIRMED" }]
|
|
1140
|
+
});
|
|
1141
|
+
} else {
|
|
1142
|
+
skipped.push("functions sem enforceAppCheck observado \u2014 AppCheck sem cobertura afirmativa");
|
|
1143
|
+
}
|
|
1144
|
+
} else {
|
|
1145
|
+
skipped.push("functions n\xE3o observadas \u2014 AppCheck fora deste scan");
|
|
1146
|
+
}
|
|
1364
1147
|
const modelCheck = ProjectModelSchema.safeParse(model);
|
|
1365
1148
|
if (!modelCheck.success) {
|
|
1366
1149
|
findings.push({
|
|
@@ -1399,7 +1182,7 @@ async function scan(rootDir, opts = {}) {
|
|
|
1399
1182
|
);
|
|
1400
1183
|
const totalChecks = IMPLEMENTED_CHECKS.length;
|
|
1401
1184
|
const passedChecks = IMPLEMENTED_CHECKS.filter((c) => !failedChecks.has(c)).length;
|
|
1402
|
-
const
|
|
1185
|
+
const infra = findings.filter((f) => !implemented.has(f.rule)).map((f) => ({ rule: f.rule, message: f.message }));
|
|
1403
1186
|
return {
|
|
1404
1187
|
model,
|
|
1405
1188
|
findings,
|
|
@@ -1407,15 +1190,93 @@ async function scan(rootDir, opts = {}) {
|
|
|
1407
1190
|
errors,
|
|
1408
1191
|
warnings,
|
|
1409
1192
|
infos,
|
|
1410
|
-
passed: passedChecks,
|
|
1411
|
-
total: totalChecks,
|
|
1412
1193
|
passedChecks,
|
|
1413
1194
|
totalChecks,
|
|
1414
|
-
|
|
1195
|
+
infra,
|
|
1196
|
+
checked,
|
|
1197
|
+
skipped,
|
|
1415
1198
|
coveragePct: null
|
|
1416
1199
|
}
|
|
1417
1200
|
};
|
|
1418
1201
|
}
|
|
1202
|
+
async function countAppCheckEnforced(rootDir) {
|
|
1203
|
+
try {
|
|
1204
|
+
const { readdirSync, readFileSync: readSync, statSync: stat } = await import("fs");
|
|
1205
|
+
const { join: joinP } = await import("path");
|
|
1206
|
+
const { stripRuleComments: strip } = await import("./rules-GTGZWN5U.js");
|
|
1207
|
+
const roots = [joinP(rootDir, "functions")];
|
|
1208
|
+
let count = 0;
|
|
1209
|
+
const stack = [...roots];
|
|
1210
|
+
let guard = 0;
|
|
1211
|
+
const isTestFile = (p) => {
|
|
1212
|
+
const n = p.replace(/\\/g, "/");
|
|
1213
|
+
return /\.test\.[tj]sx?$/.test(n) || /\.spec\.[tj]sx?$/.test(n) || /(^|\/)__tests__\//.test(n);
|
|
1214
|
+
};
|
|
1215
|
+
while (stack.length > 0 && guard < 200) {
|
|
1216
|
+
guard += 1;
|
|
1217
|
+
const cur = stack.pop();
|
|
1218
|
+
let entries = [];
|
|
1219
|
+
try {
|
|
1220
|
+
entries = readdirSync(cur, { withFileTypes: true });
|
|
1221
|
+
} catch {
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
for (const e of entries) {
|
|
1225
|
+
const name = e.name;
|
|
1226
|
+
const abs = joinP(cur, name);
|
|
1227
|
+
let isDir = false;
|
|
1228
|
+
try {
|
|
1229
|
+
isDir = stat(abs).isDirectory();
|
|
1230
|
+
} catch {
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
if (isDir) {
|
|
1234
|
+
if (name === "node_modules" || name === "dist" || name === "lib") continue;
|
|
1235
|
+
stack.push(abs);
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
if (!/\.(ts|js|mjs|cjs)$/.test(name)) continue;
|
|
1239
|
+
if (isTestFile(abs)) continue;
|
|
1240
|
+
try {
|
|
1241
|
+
const text = strip(readSync(abs, "utf-8"));
|
|
1242
|
+
const hits = text.match(/enforceAppCheck\s*:\s*true/g);
|
|
1243
|
+
if (hits) count += hits.length;
|
|
1244
|
+
} catch {
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
return count;
|
|
1249
|
+
} catch {
|
|
1250
|
+
return 0;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
function parseStorageAllows(clean) {
|
|
1254
|
+
const out = [];
|
|
1255
|
+
const re = /allow\s+([^;:]+?)(?::\s*if\s+([^;]+))?;/gi;
|
|
1256
|
+
let m;
|
|
1257
|
+
while ((m = re.exec(clean)) !== null) {
|
|
1258
|
+
out.push({ target: m[1].trim(), condition: m[2]?.trim() });
|
|
1259
|
+
}
|
|
1260
|
+
return out;
|
|
1261
|
+
}
|
|
1262
|
+
function expandStorageOps(target) {
|
|
1263
|
+
const out = /* @__PURE__ */ new Set();
|
|
1264
|
+
for (const part of target.toLowerCase().split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
1265
|
+
if (part === "read") {
|
|
1266
|
+
out.add("read");
|
|
1267
|
+
out.add("get");
|
|
1268
|
+
out.add("list");
|
|
1269
|
+
} else if (part === "write") {
|
|
1270
|
+
out.add("write");
|
|
1271
|
+
out.add("create");
|
|
1272
|
+
out.add("update");
|
|
1273
|
+
out.add("delete");
|
|
1274
|
+
} else {
|
|
1275
|
+
out.add(part);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return out;
|
|
1279
|
+
}
|
|
1419
1280
|
|
|
1420
1281
|
export {
|
|
1421
1282
|
discover,
|
|
@@ -1423,26 +1284,6 @@ export {
|
|
|
1423
1284
|
classifyOrigin,
|
|
1424
1285
|
emptyModel,
|
|
1425
1286
|
enrichWithGraph,
|
|
1426
|
-
stripRuleComments,
|
|
1427
|
-
opsFrom,
|
|
1428
|
-
normalizeTarget,
|
|
1429
|
-
parseMatchBlocks,
|
|
1430
|
-
buildFullPath,
|
|
1431
|
-
parseRuleFunctions,
|
|
1432
|
-
parseRuleFunctionsDetailed,
|
|
1433
|
-
inlineHelperArgs,
|
|
1434
|
-
inlineHelpersInCondition,
|
|
1435
|
-
resolveHelperCondition,
|
|
1436
|
-
resolveHelperConditionDetailed,
|
|
1437
|
-
extractRules,
|
|
1438
|
-
extractRulesContent,
|
|
1439
|
-
stripOuterParens,
|
|
1440
|
-
isPublicCondition,
|
|
1441
|
-
hashShort,
|
|
1442
|
-
conditionKeyForFingerprint,
|
|
1443
|
-
findPublicAllows,
|
|
1444
|
-
helperBodyToCondition,
|
|
1445
|
-
findPublicAllowsContent,
|
|
1446
1287
|
IMPLEMENTED_CHECKS,
|
|
1447
1288
|
runChecks,
|
|
1448
1289
|
SeveritySchema,
|