@justmpm/firebase-audit 0.3.1 → 0.4.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/{chunk-UMQH7UDI.js → chunk-5PVQB3ZI.js} +448 -53
- package/dist/{chunk-GUSNXUAT.js → chunk-NJBCSW7E.js} +18 -1
- package/dist/{chunk-CPEIPRCF.js → chunk-TVEZQU7C.js} +47 -10
- package/dist/chunk-YVV7YRF3.js +165 -0
- package/dist/cli.js +7 -6
- package/dist/{drift-BEXSL2IX.js → drift-KU7HWL7M.js} +1 -1
- package/dist/index.d.ts +98 -11
- package/dist/index.js +46 -4
- package/dist/mcp-cli.d.ts +1 -0
- package/dist/mcp-cli.js +13 -0
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +11 -0
- package/dist/{scan-TLHI777J.js → scan-6FRUE5BE.js} +1 -1
- package/dist/{verify-Y7BWAFPL.js → verify-R6LI2KNZ.js} +1 -1
- package/package.json +5 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/scan.ts
|
|
2
2
|
import { readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
|
|
3
|
+
import { join as join3 } from "path";
|
|
3
4
|
import { parse as parseYaml } from "yaml";
|
|
4
5
|
|
|
5
6
|
// src/discovery.ts
|
|
@@ -54,7 +55,6 @@ var SERVER_HINTS_SEGMENTS = [
|
|
|
54
55
|
"functions",
|
|
55
56
|
"server",
|
|
56
57
|
"backend",
|
|
57
|
-
"api",
|
|
58
58
|
"scripts"
|
|
59
59
|
];
|
|
60
60
|
function classifyOrigin(file, content) {
|
|
@@ -71,7 +71,8 @@ function classifyOrigin(file, content) {
|
|
|
71
71
|
return "SERVER";
|
|
72
72
|
}
|
|
73
73
|
if (rel.includes("src/app/api/")) return "SERVER";
|
|
74
|
-
if (
|
|
74
|
+
if (rel.includes("server/api/")) return "SERVER";
|
|
75
|
+
if (hasSeg(...SERVER_HINTS_SEGMENTS)) return "SERVER";
|
|
75
76
|
if (content.includes("firebase-admin") || content.includes("firebase-functions")) {
|
|
76
77
|
return "CLIENT";
|
|
77
78
|
}
|
|
@@ -102,9 +103,35 @@ function emptyModel(rootDir, d) {
|
|
|
102
103
|
graph: []
|
|
103
104
|
};
|
|
104
105
|
}
|
|
106
|
+
async function enrichWithGraph(rootDir, d) {
|
|
107
|
+
try {
|
|
108
|
+
const ai = await import("@justmpm/ai-tool");
|
|
109
|
+
const res = await ai.map({ cwd: rootDir, format: "json" });
|
|
110
|
+
const files = Array.isArray(res.files) ? res.files : [];
|
|
111
|
+
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");
|
|
113
|
+
const { join: joinP } = await import("path");
|
|
114
|
+
for (const rel of code) {
|
|
115
|
+
let content = "";
|
|
116
|
+
try {
|
|
117
|
+
const abs = joinP(rootDir, rel);
|
|
118
|
+
if (!existsSyncFn(abs)) continue;
|
|
119
|
+
if (statSync(abs).size > 100 * 1024) continue;
|
|
120
|
+
content = readSync(abs, "utf-8");
|
|
121
|
+
} catch {
|
|
122
|
+
content = "";
|
|
123
|
+
}
|
|
124
|
+
const origin = classifyOrigin(rel, content);
|
|
125
|
+
if (origin === "CLIENT") d.clientFiles.push(rel);
|
|
126
|
+
else if (origin === "SERVER") d.serverFiles.push(rel);
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
}
|
|
130
|
+
}
|
|
105
131
|
|
|
106
132
|
// src/rules.ts
|
|
107
133
|
import { readFileSync as readFileSync2 } from "fs";
|
|
134
|
+
import { createHash } from "crypto";
|
|
108
135
|
var MATCH_RE = /match\s+(\/[^{\s]*(?:\{[^}]*\}[^{\s]*)*)\s*\{/g;
|
|
109
136
|
var ALLOW_RE = /allow\s+([^;:]+)(?::\s*if\s+([^;]+))?;/g;
|
|
110
137
|
function stripRuleComments(content) {
|
|
@@ -194,19 +221,105 @@ function findBlockEnd(content, openBraceIndex) {
|
|
|
194
221
|
function normalizeTarget(target) {
|
|
195
222
|
return target.toLowerCase().split(",").map((s) => s.trim()).filter(Boolean).sort().join(",");
|
|
196
223
|
}
|
|
197
|
-
function
|
|
198
|
-
const
|
|
199
|
-
const content = stripRuleComments(rawContent);
|
|
200
|
-
const rules = [];
|
|
201
|
-
const matchBlocks = [];
|
|
224
|
+
function parseMatchBlocks(content) {
|
|
225
|
+
const blocks = [];
|
|
202
226
|
const matchRe = new RegExp(MATCH_RE.source, "g");
|
|
203
227
|
let m;
|
|
204
228
|
while ((m = matchRe.exec(content)) !== null) {
|
|
205
229
|
const before = content.slice(0, m.index);
|
|
206
230
|
const line = before.split("\n").length;
|
|
207
231
|
const openBrace = m.index + m[0].length - 1;
|
|
208
|
-
|
|
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
|
+
});
|
|
209
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);
|
|
210
323
|
const allowRe = new RegExp(ALLOW_RE.source, "g");
|
|
211
324
|
let a;
|
|
212
325
|
let idx = 0;
|
|
@@ -214,26 +327,37 @@ function extractRules(rulesFile, relFile) {
|
|
|
214
327
|
const before = content.slice(0, a.index);
|
|
215
328
|
const line = before.split("\n").length;
|
|
216
329
|
const target = a[1];
|
|
217
|
-
const
|
|
330
|
+
const rawCondition = (a[2] ?? "").trim();
|
|
218
331
|
const unconditional = a[2] === void 0;
|
|
219
|
-
const containing = matchBlocks.filter((b) => b.blockStart <= a.index && a.index < b.blockEnd);
|
|
220
|
-
const
|
|
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;
|
|
221
339
|
const authReferences = [];
|
|
222
|
-
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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(
|
|
228
350
|
(x) => x[1]
|
|
229
351
|
);
|
|
230
352
|
const requestResourceReferences = [
|
|
231
|
-
...
|
|
353
|
+
...big.matchAll(/request\.resource\.data\.([A-Za-z0-9_]+)/g)
|
|
232
354
|
].map((x) => x[1]);
|
|
233
355
|
const { ops, known } = opsFrom(target);
|
|
356
|
+
const isHelperCall = !unconditional && rawCondition !== resolved;
|
|
357
|
+
const hasAuth = authSource.includes("request.auth");
|
|
234
358
|
rules.push({
|
|
235
359
|
id: `rule-${idx++}`,
|
|
236
|
-
path:
|
|
360
|
+
path: fullPath,
|
|
237
361
|
operations: ops,
|
|
238
362
|
conditionPresent: !unconditional && condition.length > 0,
|
|
239
363
|
condition: unconditional ? void 0 : condition,
|
|
@@ -241,7 +365,7 @@ function extractRules(rulesFile, relFile) {
|
|
|
241
365
|
claimReferences,
|
|
242
366
|
resourceReferences,
|
|
243
367
|
requestResourceReferences,
|
|
244
|
-
confidence: known ? "
|
|
368
|
+
confidence: !known || isHelperCall && !hasAuth ? "UNKNOWN" : "CONFIRMED",
|
|
245
369
|
location: { file: relFile, start: { line, column: 0 } }
|
|
246
370
|
});
|
|
247
371
|
}
|
|
@@ -271,6 +395,13 @@ function isPublicCondition(condition) {
|
|
|
271
395
|
if (condition === void 0) return { isPublic: true, confidence: "CONFIRMED" };
|
|
272
396
|
const norm = stripOuterParens(condition);
|
|
273
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
|
+
}
|
|
274
405
|
if (low.includes("!(request.auth==null)") || low.includes("!(request.auth===null)") || low === "!request.auth==null" || low === "!request.auth===null") {
|
|
275
406
|
return { isPublic: false, confidence: "CONFIRMED" };
|
|
276
407
|
}
|
|
@@ -279,29 +410,55 @@ function isPublicCondition(condition) {
|
|
|
279
410
|
if (low.includes("request.auth==null") || low.includes("request.auth===null")) {
|
|
280
411
|
return { isPublic: true, confidence: "CONFIRMED" };
|
|
281
412
|
}
|
|
413
|
+
if (!low.includes("request.auth")) {
|
|
414
|
+
return { isPublic: true, confidence: "PROBABLE" };
|
|
415
|
+
}
|
|
282
416
|
return { isPublic: false, confidence: "CONFIRMED" };
|
|
283
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
|
+
}
|
|
284
427
|
function findPublicAllows(rulesFile) {
|
|
285
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) {
|
|
286
437
|
const content = stripRuleComments(rawContent);
|
|
438
|
+
const functions = parseRuleFunctions(content);
|
|
439
|
+
const detailed = parseRuleFunctionsDetailed(content);
|
|
287
440
|
const out = [];
|
|
288
|
-
const
|
|
289
|
-
const blocks = [];
|
|
290
|
-
let mm0;
|
|
291
|
-
while ((mm0 = matchRe.exec(content)) !== null) {
|
|
292
|
-
const openBrace = mm0.index + mm0[0].length - 1;
|
|
293
|
-
blocks.push({ path: mm0[1], index: mm0.index, end: findBlockEnd(content, openBrace) });
|
|
294
|
-
}
|
|
441
|
+
const parsed = parseMatchBlocks(content);
|
|
295
442
|
const re = new RegExp(ALLOW_RE.source, "g");
|
|
296
443
|
let mm;
|
|
297
444
|
while ((mm = re.exec(content)) !== null) {
|
|
298
445
|
const line = content.slice(0, mm.index).split("\n").length;
|
|
299
446
|
const target = mm[1].trim();
|
|
300
|
-
const
|
|
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);
|
|
301
458
|
if (!check.isPublic) continue;
|
|
302
|
-
const
|
|
303
|
-
const path =
|
|
304
|
-
out.push({ line, target, path, confidence: check.confidence });
|
|
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]) });
|
|
305
462
|
}
|
|
306
463
|
return out;
|
|
307
464
|
}
|
|
@@ -342,8 +499,17 @@ async function runChecks(model, rootDir, opts = {}) {
|
|
|
342
499
|
const findings = [];
|
|
343
500
|
const relRules = model.firebase.firestore.rulesFile;
|
|
344
501
|
const absRules = relRules ? join2(rootDir, relRules) : null;
|
|
502
|
+
let pubs = [];
|
|
503
|
+
let helperDetailed = /* @__PURE__ */ new Map();
|
|
345
504
|
if (absRules && existsSync2(absRules)) {
|
|
346
|
-
|
|
505
|
+
try {
|
|
506
|
+
const rawAll = readFileSync3(absRules, "utf-8");
|
|
507
|
+
pubs = findPublicAllowsContent(rawAll);
|
|
508
|
+
helperDetailed = parseRuleFunctionsDetailed(rawAll);
|
|
509
|
+
} catch {
|
|
510
|
+
pubs = [];
|
|
511
|
+
}
|
|
512
|
+
for (const pub of pubs) {
|
|
347
513
|
const t = pub.target.toLowerCase();
|
|
348
514
|
const tokens = t.split(",").map((s) => s.trim());
|
|
349
515
|
const isWrite = tokens.some((x) => ["write", "create", "update", "delete"].includes(x));
|
|
@@ -355,7 +521,7 @@ async function runChecks(model, rootDir, opts = {}) {
|
|
|
355
521
|
"ERROR",
|
|
356
522
|
pub.confidence,
|
|
357
523
|
`Escrita p\xFAblica em ${pub.path} (${pub.target}). Qualquer cliente pode escrever.`,
|
|
358
|
-
`FBA001:${relRules}:${pub.path}:${normalizeTarget(pub.target)}`,
|
|
524
|
+
`FBA001:${relRules}:${pub.path}:${normalizeTarget(pub.target)}:${pub.conditionKey}`,
|
|
359
525
|
{
|
|
360
526
|
file: relRules,
|
|
361
527
|
line: pub.line,
|
|
@@ -373,7 +539,7 @@ async function runChecks(model, rootDir, opts = {}) {
|
|
|
373
539
|
"WARNING",
|
|
374
540
|
pub.confidence,
|
|
375
541
|
`Leitura p\xFAblica em ${pub.path} (${pub.target}). Pode ser proposital.`,
|
|
376
|
-
`FBA002:${relRules}:${pub.path}:${normalizeTarget(pub.target)}`,
|
|
542
|
+
`FBA002:${relRules}:${pub.path}:${normalizeTarget(pub.target)}:${pub.conditionKey}`,
|
|
377
543
|
{
|
|
378
544
|
file: relRules,
|
|
379
545
|
line: pub.line,
|
|
@@ -473,12 +639,10 @@ async function runChecks(model, rootDir, opts = {}) {
|
|
|
473
639
|
return [op];
|
|
474
640
|
};
|
|
475
641
|
const publicPaths = /* @__PURE__ */ new Set();
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
publicPaths.add(`${p.path}::${op.toLowerCase()}`);
|
|
481
|
-
}
|
|
642
|
+
for (const p of pubs) {
|
|
643
|
+
const { ops } = opsFrom(p.target);
|
|
644
|
+
for (const op of /* @__PURE__ */ new Set([...ops, ...ops.flatMap(expandOp)])) {
|
|
645
|
+
publicPaths.add(`${p.path}::${op.toLowerCase()}`);
|
|
482
646
|
}
|
|
483
647
|
}
|
|
484
648
|
const permsByResource = /* @__PURE__ */ new Map();
|
|
@@ -499,9 +663,41 @@ async function runChecks(model, rootDir, opts = {}) {
|
|
|
499
663
|
(op) => publicPaths.has(`${rule.path}::${op.toLowerCase()}`)
|
|
500
664
|
);
|
|
501
665
|
if (isPublicRule) return;
|
|
502
|
-
const
|
|
503
|
-
const
|
|
504
|
-
|
|
666
|
+
const condRaw = rule.condition ?? "";
|
|
667
|
+
const condResolved = resolveHelperConditionDetailed(condRaw, helperDetailed);
|
|
668
|
+
const condInlined = inlineHelpersInCondition(condRaw, helperDetailed);
|
|
669
|
+
const cond = condResolved !== condRaw ? condResolved : condInlined !== condRaw ? condInlined : condRaw;
|
|
670
|
+
const isInvertedAdmin = /admin\s*==\s*false/i.test(cond) || /admin\s*!=\s*true/i.test(cond) || /admin\s*!==\s*true/i.test(cond) || /!\s*request\.auth\.token\.admin\b/i.test(cond) || /!\s*\(\s*request\.auth\.token\.admin\b/i.test(cond) || /!\s*\(\s*.*admin\s*==\s*true/i.test(cond);
|
|
671
|
+
const normClaim = (c) => c.toLowerCase().replace(/[_-]/g, "");
|
|
672
|
+
const isAdminCall = /\bisadmin\s*\(([^)]*)\)/i.exec(condRaw) ?? /\bisadmin\s*\(([^)]*)\)/i.exec(cond);
|
|
673
|
+
let isAdminBodyHasAdmin = false;
|
|
674
|
+
if (isAdminCall) {
|
|
675
|
+
const fnName = (() => {
|
|
676
|
+
const m = /\b([A-Za-z_][A-Za-z0-9_]*)\s*\(/i.exec(isAdminCall[0]);
|
|
677
|
+
return m ? m[1] : "isAdmin";
|
|
678
|
+
})();
|
|
679
|
+
const fn = helperDetailed.get(fnName) ?? helperDetailed.get("isAdmin") ?? helperDetailed.get("isadmin");
|
|
680
|
+
if (fn) {
|
|
681
|
+
const b = `${fn.body}`;
|
|
682
|
+
isAdminBodyHasAdmin = /token\.admin\b/i.test(b) || /token\.get\(\s*['"]admin['"]/i.test(b) || /token\s*\[\s*['"]admin['"]/i.test(b) || /==\s*['"]admin['"]/i.test(b);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const checksAdminClaim = !isInvertedAdmin && rule.claimReferences.some((c) => {
|
|
686
|
+
const n = normClaim(c);
|
|
687
|
+
return n === "admin" || n === "isadmin";
|
|
688
|
+
});
|
|
689
|
+
const hasTopOr = /\|\|/.test(cond);
|
|
690
|
+
const adminPerDisjunct = hasTopOr ? cond.split("||").every((part) => {
|
|
691
|
+
const p = part;
|
|
692
|
+
return /token\.admin\b/i.test(p) || /token\.get\(\s*['"]admin['"]/i.test(p) || /token\s*\[\s*['"]admin['"]/i.test(p) || /token\.\w+\s*==\s*['"]admin['"]/i.test(p) || /token\.\w+\s+in\s+[^\n;]*['"]admin['"]/i.test(p);
|
|
693
|
+
}) : true;
|
|
694
|
+
const checksAdminRole = !isInvertedAdmin && hasTopOr ? adminPerDisjunct : /token\.\w+\s*==\s*['"]admin['"]/i.test(cond) || /token\s*\[\s*['"]\w+['"]\s*\]\s*==\s*['"]admin['"]/i.test(cond) || /token\.\w+\s+in\s+[^\n;]*['"]admin['"]/i.test(cond) || /token\.get\(\s*['"]admin['"]/i.test(cond) || isAdminBodyHasAdmin;
|
|
695
|
+
if (checksAdminClaim || checksAdminRole) {
|
|
696
|
+
if (hasTopOr && !adminPerDisjunct) {
|
|
697
|
+
} else {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
505
701
|
const checksAuth = rule.authReferences.length > 0;
|
|
506
702
|
if (!checksAuth) return;
|
|
507
703
|
findings.push(
|
|
@@ -510,7 +706,7 @@ async function runChecks(model, rootDir, opts = {}) {
|
|
|
510
706
|
"WARNING",
|
|
511
707
|
"PROBABLE",
|
|
512
708
|
`Rule ${rule.path} [${rule.operations.join(", ")}] aceita autenticado sem checar claim de admin, mas contrato restringe ${restricted.map((r) => r.perm).join(", ")} a admin.`,
|
|
513
|
-
`FBA012:${rule.path}:${[...rule.operations].sort().join(",")}:${rule.
|
|
709
|
+
`FBA012:${rule.path}:${[...rule.operations].sort().join(",")}:${conditionKeyForFingerprint(rule.condition)}:${resourceKey}`,
|
|
514
710
|
{
|
|
515
711
|
file: rule.location.file,
|
|
516
712
|
line: rule.location.start.line,
|
|
@@ -558,6 +754,34 @@ async function runChecks(model, rootDir, opts = {}) {
|
|
|
558
754
|
)
|
|
559
755
|
);
|
|
560
756
|
}
|
|
757
|
+
for (const rule of model.rules) {
|
|
758
|
+
const raw = (rule.condition ?? "").trim();
|
|
759
|
+
const rawResolved = resolveHelperConditionDetailed(raw, helperDetailed);
|
|
760
|
+
const rawInlined = inlineHelpersInCondition(raw, helperDetailed);
|
|
761
|
+
const effectiveRaw = rawResolved !== raw ? rawResolved : rawInlined !== raw ? rawInlined : raw;
|
|
762
|
+
const norm = stripOuterParens(effectiveRaw).toLowerCase().replace(/\s+/g, "");
|
|
763
|
+
const hasBareAuth = norm === "request.auth!=null" || norm === "request.auth!==null" || norm === "request.auth.uid!=null" || norm === "request.auth.uid!==null";
|
|
764
|
+
const hasWeakConjunction = (norm.includes("request.auth!=null") || norm.includes("request.auth!==null") || norm.includes("request.auth.uid!=null") || norm.includes("request.auth.uid!==null")) && !norm.includes("sign_in_provider") && !norm.includes("email_verified") && !norm.includes("request.auth.token");
|
|
765
|
+
const isBareAuth = hasBareAuth || hasWeakConjunction;
|
|
766
|
+
if (isBareAuth && rule.claimReferences.length === 0) {
|
|
767
|
+
findings.push(
|
|
768
|
+
make(
|
|
769
|
+
"AUTH_ANON_ALLOWED",
|
|
770
|
+
"INFO",
|
|
771
|
+
"PROBABLE",
|
|
772
|
+
`Rule ${rule.path} aceita qualquer autenticado incluindo an\xF4nimo (request.auth != null sem claim/provider).`,
|
|
773
|
+
`AUTH_ANON_ALLOWED:${rule.path}:${conditionKeyForFingerprint(rule.condition)}`,
|
|
774
|
+
{
|
|
775
|
+
file: rule.location.file,
|
|
776
|
+
line: rule.location.start.line,
|
|
777
|
+
resource: rule.path,
|
|
778
|
+
evidence: [ev("anon-allowed", rule.condition ?? "auth!=null", "PROBABLE", rule.location.file, rule.location.start.line)],
|
|
779
|
+
fix: "Exigir claim (token.admin/role) ou checar provider diferente de anonymous para dados sens\xEDveis."
|
|
780
|
+
}
|
|
781
|
+
)
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
561
785
|
const adminHits = await collectAdminImports(rootDir);
|
|
562
786
|
for (const hit of adminHits) {
|
|
563
787
|
let origin = "UNKNOWN";
|
|
@@ -590,7 +814,12 @@ async function runChecks(model, rootDir, opts = {}) {
|
|
|
590
814
|
async function collectPermissionCalls(rootDir, adapterFn) {
|
|
591
815
|
const literals = [];
|
|
592
816
|
const dynamic = [];
|
|
593
|
-
const
|
|
817
|
+
const ADAPTER_RE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/;
|
|
818
|
+
const safeAdapter = ADAPTER_RE.test(adapterFn) ? adapterFn : null;
|
|
819
|
+
if (!safeAdapter) {
|
|
820
|
+
return { literals, dynamic };
|
|
821
|
+
}
|
|
822
|
+
const bases = [safeAdapter];
|
|
594
823
|
const patterns = [];
|
|
595
824
|
for (const b of bases) {
|
|
596
825
|
patterns.push(`${b}($PERM)`, `${b}($PERM, $$$ARGS)`);
|
|
@@ -619,9 +848,9 @@ async function collectPermissionCalls(rootDir, adapterFn) {
|
|
|
619
848
|
const isQuoted = /^['"`].*['"`]$/.test(raw);
|
|
620
849
|
if (isQuoted) {
|
|
621
850
|
const perm = raw.replace(/^['"`]|['"`]$/g, "").trim();
|
|
622
|
-
if (perm
|
|
623
|
-
else
|
|
624
|
-
const k = `${m.file}:${line}:${raw}:
|
|
851
|
+
if (/^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*)*$/.test(perm)) pushLiteral(m.file, line, perm);
|
|
852
|
+
else {
|
|
853
|
+
const k = `${m.file}:${line}:${raw}:invalid`;
|
|
625
854
|
if (!seen.has(k)) {
|
|
626
855
|
seen.add(k);
|
|
627
856
|
dynamic.push({ file: m.file, line, text: m.text });
|
|
@@ -838,6 +1067,7 @@ var FindingSchema = z.strictObject({
|
|
|
838
1067
|
var PermissionNameSchema = z.string().regex(/^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*)*$/);
|
|
839
1068
|
var RoleNameSchema = z.string().regex(/^[a-z][a-z0-9_-]*$/);
|
|
840
1069
|
var AuditYamlSchema = z.strictObject({
|
|
1070
|
+
$schema: z.string().optional(),
|
|
841
1071
|
authorization: z.strictObject({
|
|
842
1072
|
adapter: z.strictObject({ function: z.string().min(1) }).optional(),
|
|
843
1073
|
roles: z.record(RoleNameSchema, z.strictObject({ permissions: z.array(PermissionNameSchema) })),
|
|
@@ -852,12 +1082,80 @@ var AuditYamlSchema = z.strictObject({
|
|
|
852
1082
|
}).optional()
|
|
853
1083
|
})
|
|
854
1084
|
});
|
|
1085
|
+
var RESERVED_CLAIM_KEYS = [
|
|
1086
|
+
"sub",
|
|
1087
|
+
"iat",
|
|
1088
|
+
"iss",
|
|
1089
|
+
"exp",
|
|
1090
|
+
"aud",
|
|
1091
|
+
"auth_time",
|
|
1092
|
+
"acr",
|
|
1093
|
+
"amr",
|
|
1094
|
+
"azp",
|
|
1095
|
+
"cnf",
|
|
1096
|
+
"c_hash",
|
|
1097
|
+
"at_hash",
|
|
1098
|
+
"jti",
|
|
1099
|
+
"nbf",
|
|
1100
|
+
"nonce",
|
|
1101
|
+
"firebase",
|
|
1102
|
+
"user_id"
|
|
1103
|
+
];
|
|
1104
|
+
function validateClaimSamples(samples) {
|
|
1105
|
+
const errors = [];
|
|
1106
|
+
if (!samples) return { ok: true, errors };
|
|
1107
|
+
const enc = new TextEncoder();
|
|
1108
|
+
for (const [role, claims] of Object.entries(samples)) {
|
|
1109
|
+
const bytes = enc.encode(JSON.stringify(claims)).length;
|
|
1110
|
+
if (bytes > 1e3) {
|
|
1111
|
+
errors.push(`samples.${role} com ${bytes} bytes (limite 1000 do crach\xE1)`);
|
|
1112
|
+
}
|
|
1113
|
+
for (const k of Object.keys(claims)) {
|
|
1114
|
+
if (RESERVED_CLAIM_KEYS.includes(k)) {
|
|
1115
|
+
errors.push(`samples.${role}.${k} \xE9 chave reservada OIDC/Firebase`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return { ok: errors.length === 0, errors };
|
|
1120
|
+
}
|
|
855
1121
|
|
|
856
1122
|
// src/scan.ts
|
|
857
1123
|
async function scan(rootDir, opts = {}) {
|
|
858
1124
|
const d = discover(rootDir);
|
|
1125
|
+
if (opts.graph === true) {
|
|
1126
|
+
try {
|
|
1127
|
+
await enrichWithGraph(rootDir, d);
|
|
1128
|
+
} catch {
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
859
1131
|
const model = emptyModel(rootDir, d);
|
|
860
1132
|
const findings_pre = [];
|
|
1133
|
+
try {
|
|
1134
|
+
if (d.firebaseJson && existsSync3(d.firebaseJson)) {
|
|
1135
|
+
const raw = JSON.parse(readFileSync4(d.firebaseJson, "utf-8"));
|
|
1136
|
+
for (const [kind, rel] of [
|
|
1137
|
+
["rules", raw.firestore?.rules],
|
|
1138
|
+
["indexes", raw.firestore?.indexes]
|
|
1139
|
+
]) {
|
|
1140
|
+
if (typeof rel === "string" && rel.length > 0) {
|
|
1141
|
+
if (!existsSync3(join3(rootDir, rel))) {
|
|
1142
|
+
findings_pre.push({
|
|
1143
|
+
rule: "RULES_CONFIG_MISMATCH",
|
|
1144
|
+
severity: "WARNING",
|
|
1145
|
+
confidence: "CONFIRMED",
|
|
1146
|
+
message: `firebase.json aponta firestore.${kind} para "${rel}" inexistente \u2014 usando default. Corrija o path.`,
|
|
1147
|
+
fingerprint: `RULES_CONFIG_MISMATCH:${kind}:${rel}`,
|
|
1148
|
+
evidence: [
|
|
1149
|
+
{ id: `ev-config-${kind}`, kind: "config-mismatch", summary: rel, confidence: "CONFIRMED" }
|
|
1150
|
+
],
|
|
1151
|
+
fix: "Ajuste o path em firebase.json ou crie o arquivo."
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
} catch {
|
|
1158
|
+
}
|
|
861
1159
|
if (!d.firestoreRulesFile || !existsSync3(d.firestoreRulesFile)) {
|
|
862
1160
|
findings_pre.push({
|
|
863
1161
|
rule: "RULES_NOT_OBSERVED",
|
|
@@ -890,6 +1188,18 @@ async function scan(rootDir, opts = {}) {
|
|
|
890
1188
|
if (d.firestoreIndexesFile && existsSync3(d.firestoreIndexesFile)) {
|
|
891
1189
|
try {
|
|
892
1190
|
const raw = JSON.parse(readFileSync4(d.firestoreIndexesFile, "utf-8"));
|
|
1191
|
+
if (Array.isArray(raw.fieldOverrides) && raw.fieldOverrides.length > 0) {
|
|
1192
|
+
findings_pre.push({
|
|
1193
|
+
rule: "FIELD_OVERRIDES_PRESENT",
|
|
1194
|
+
severity: "INFO",
|
|
1195
|
+
confidence: "CONFIRMED",
|
|
1196
|
+
message: `firestore.indexes.json com ${raw.fieldOverrides.length} fieldOverrides (isen\xE7\xE3o campo \xFAnico, fora do drift composto).`,
|
|
1197
|
+
fingerprint: "FIELD_OVERRIDES_PRESENT",
|
|
1198
|
+
evidence: [
|
|
1199
|
+
{ id: "ev-overrides", kind: "indexes-overrides", summary: `${raw.fieldOverrides.length} overrides`, confidence: "CONFIRMED" }
|
|
1200
|
+
]
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
893
1203
|
const validModes = ["ASCENDING", "DESCENDING", "ARRAY_CONTAINS", "VECTOR"];
|
|
894
1204
|
const entries = [];
|
|
895
1205
|
let skipped = 0;
|
|
@@ -905,7 +1215,7 @@ async function scan(rootDir, opts = {}) {
|
|
|
905
1215
|
}
|
|
906
1216
|
const fields = [];
|
|
907
1217
|
for (const f of i.fields ?? []) {
|
|
908
|
-
if (typeof f.fieldPath !== "string" || f.fieldPath.length === 0
|
|
1218
|
+
if (typeof f.fieldPath !== "string" || f.fieldPath.length === 0) {
|
|
909
1219
|
skipped += 1;
|
|
910
1220
|
continue;
|
|
911
1221
|
}
|
|
@@ -922,6 +1232,11 @@ async function scan(rootDir, opts = {}) {
|
|
|
922
1232
|
queryScope: i.queryScope === "COLLECTION_GROUP" ? "COLLECTION_GROUP" : "COLLECTION",
|
|
923
1233
|
fields
|
|
924
1234
|
});
|
|
1235
|
+
const last = entries[entries.length - 1];
|
|
1236
|
+
if (last.fields.length === 0) {
|
|
1237
|
+
entries.pop();
|
|
1238
|
+
skipped += 1;
|
|
1239
|
+
}
|
|
925
1240
|
}
|
|
926
1241
|
model.indexes = entries;
|
|
927
1242
|
const totalSkipped = skipped + scopeSkipped;
|
|
@@ -957,7 +1272,24 @@ async function scan(rootDir, opts = {}) {
|
|
|
957
1272
|
const validated = AuditYamlSchema.safeParse(parsed);
|
|
958
1273
|
if (validated.success) {
|
|
959
1274
|
const auth = validated.data.authorization;
|
|
960
|
-
|
|
1275
|
+
const rawAdapter = auth.adapter?.function ?? null;
|
|
1276
|
+
const ADAPTER_RE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/;
|
|
1277
|
+
if (rawAdapter !== null && !ADAPTER_RE.test(rawAdapter)) {
|
|
1278
|
+
findings_pre.push({
|
|
1279
|
+
rule: "CONTRACT_INVALID",
|
|
1280
|
+
severity: "WARNING",
|
|
1281
|
+
confidence: "CONFIRMED",
|
|
1282
|
+
message: `adapter.function "${rawAdapter}" inv\xE1lido \u2014 use identificador como "rbac.can". Coleta de permiss\xF5es ignorada.`,
|
|
1283
|
+
fingerprint: "CONTRACT_INVALID:adapter",
|
|
1284
|
+
evidence: [
|
|
1285
|
+
{ id: "ev-adapter-invalid", kind: "contract-parse", summary: rawAdapter, confidence: "CONFIRMED" }
|
|
1286
|
+
],
|
|
1287
|
+
fix: "Corrija authorization.adapter.function para identificador v\xE1lido."
|
|
1288
|
+
});
|
|
1289
|
+
model.authorization.adapter = null;
|
|
1290
|
+
} else {
|
|
1291
|
+
model.authorization.adapter = rawAdapter;
|
|
1292
|
+
}
|
|
961
1293
|
model.authorization.permissions = Object.entries(auth.permissions).map(([name, p]) => ({
|
|
962
1294
|
name,
|
|
963
1295
|
resource: p.resource,
|
|
@@ -967,6 +1299,35 @@ async function scan(rootDir, opts = {}) {
|
|
|
967
1299
|
name,
|
|
968
1300
|
permissions: r.permissions
|
|
969
1301
|
}));
|
|
1302
|
+
const samples = auth.claims?.samples;
|
|
1303
|
+
if (samples) {
|
|
1304
|
+
const res = validateClaimSamples(samples);
|
|
1305
|
+
if (!res.ok) {
|
|
1306
|
+
findings_pre.push({
|
|
1307
|
+
rule: "CONTRACT_INVALID",
|
|
1308
|
+
severity: "WARNING",
|
|
1309
|
+
confidence: "CONFIRMED",
|
|
1310
|
+
message: `claims.samples inv\xE1lido: ${res.errors.join("; ")} \u2014 samples ignorados.`,
|
|
1311
|
+
fingerprint: "CONTRACT_INVALID:claims",
|
|
1312
|
+
evidence: [
|
|
1313
|
+
{ id: "ev-claims-invalid", kind: "contract-parse", summary: res.errors.join("; "), confidence: "CONFIRMED" }
|
|
1314
|
+
],
|
|
1315
|
+
fix: "Claims s\xF3 para acesso, 1000 bytes, sem chaves OIDC/Firebase reservadas."
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
if (auth.claims && !auth.claims.enabled) {
|
|
1320
|
+
findings_pre.push({
|
|
1321
|
+
rule: "CLAIMS_DISABLED",
|
|
1322
|
+
severity: "INFO",
|
|
1323
|
+
confidence: "CONFIRMED",
|
|
1324
|
+
message: "Contrato com claims desabilitadas \u2014 FBA012 usa heur\xEDstica de papel sem crach\xE1.",
|
|
1325
|
+
fingerprint: "CLAIMS_DISABLED",
|
|
1326
|
+
evidence: [
|
|
1327
|
+
{ id: "ev-claims-off", kind: "contract-parse", summary: "claims off", confidence: "CONFIRMED" }
|
|
1328
|
+
]
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
970
1331
|
} else {
|
|
971
1332
|
findings_pre.push({
|
|
972
1333
|
rule: "CONTRACT_INVALID",
|
|
@@ -1027,12 +1388,28 @@ async function scan(rootDir, opts = {}) {
|
|
|
1027
1388
|
const errors = findings.filter((f) => f.severity === "ERROR").length;
|
|
1028
1389
|
const warnings = findings.filter((f) => f.severity === "WARNING").length;
|
|
1029
1390
|
const infos = findings.filter((f) => f.severity === "INFO").length;
|
|
1391
|
+
const implemented = new Set(IMPLEMENTED_CHECKS);
|
|
1030
1392
|
const failedChecks = new Set(
|
|
1031
1393
|
findings.filter((f) => f.severity === "ERROR" || f.severity === "WARNING").map((f) => f.rule)
|
|
1032
1394
|
);
|
|
1033
|
-
const
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1395
|
+
const totalChecks = IMPLEMENTED_CHECKS.length;
|
|
1396
|
+
const passedChecks = IMPLEMENTED_CHECKS.filter((c) => !failedChecks.has(c)).length;
|
|
1397
|
+
const infraSkipped = findings.filter((f) => !implemented.has(f.rule)).length;
|
|
1398
|
+
return {
|
|
1399
|
+
model,
|
|
1400
|
+
findings,
|
|
1401
|
+
summary: {
|
|
1402
|
+
errors,
|
|
1403
|
+
warnings,
|
|
1404
|
+
infos,
|
|
1405
|
+
passed: passedChecks,
|
|
1406
|
+
total: totalChecks,
|
|
1407
|
+
passedChecks,
|
|
1408
|
+
totalChecks,
|
|
1409
|
+
infraSkipped,
|
|
1410
|
+
coveragePct: null
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1036
1413
|
}
|
|
1037
1414
|
|
|
1038
1415
|
export {
|
|
@@ -1040,11 +1417,27 @@ export {
|
|
|
1040
1417
|
SERVER_HINTS_SEGMENTS,
|
|
1041
1418
|
classifyOrigin,
|
|
1042
1419
|
emptyModel,
|
|
1420
|
+
enrichWithGraph,
|
|
1043
1421
|
stripRuleComments,
|
|
1044
1422
|
opsFrom,
|
|
1423
|
+
normalizeTarget,
|
|
1424
|
+
parseMatchBlocks,
|
|
1425
|
+
buildFullPath,
|
|
1426
|
+
parseRuleFunctions,
|
|
1427
|
+
parseRuleFunctionsDetailed,
|
|
1428
|
+
inlineHelperArgs,
|
|
1429
|
+
inlineHelpersInCondition,
|
|
1430
|
+
resolveHelperCondition,
|
|
1431
|
+
resolveHelperConditionDetailed,
|
|
1045
1432
|
extractRules,
|
|
1433
|
+
extractRulesContent,
|
|
1434
|
+
stripOuterParens,
|
|
1046
1435
|
isPublicCondition,
|
|
1436
|
+
hashShort,
|
|
1437
|
+
conditionKeyForFingerprint,
|
|
1047
1438
|
findPublicAllows,
|
|
1439
|
+
helperBodyToCondition,
|
|
1440
|
+
findPublicAllowsContent,
|
|
1048
1441
|
IMPLEMENTED_CHECKS,
|
|
1049
1442
|
runChecks,
|
|
1050
1443
|
SeveritySchema,
|
|
@@ -1067,5 +1460,7 @@ export {
|
|
|
1067
1460
|
ProjectModelSchema,
|
|
1068
1461
|
FindingSchema,
|
|
1069
1462
|
AuditYamlSchema,
|
|
1463
|
+
RESERVED_CLAIM_KEYS,
|
|
1464
|
+
validateClaimSamples,
|
|
1070
1465
|
scan
|
|
1071
1466
|
};
|