@justmpm/firebase-audit 0.5.6 → 0.5.8

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.
@@ -42,11 +42,12 @@ function stripRuleComments(content) {
42
42
  }
43
43
  function opsFrom(target) {
44
44
  const t = target.trim().toLowerCase();
45
- if (t === "read") return { ops: ["read", "get", "list"], known: true };
46
- if (t === "write") return { ops: ["write", "create", "update", "delete"], known: true };
45
+ if (t === "read") return { ops: ["read", "get", "list"], known: true, unknown: [] };
46
+ if (t === "write") return { ops: ["write", "create", "update", "delete"], known: true, unknown: [] };
47
47
  const parts = t.split(",").map((s) => s.trim()).filter(Boolean);
48
48
  const valid = ["read", "get", "list", "create", "update", "delete", "write"];
49
49
  const out = [];
50
+ const unknown = [];
50
51
  let known = false;
51
52
  for (const p of parts) {
52
53
  if (valid.includes(p)) {
@@ -62,9 +63,11 @@ function opsFrom(target) {
62
63
  if (!out.includes(extra)) out.push(extra);
63
64
  }
64
65
  }
66
+ } else {
67
+ unknown.push(p);
65
68
  }
66
69
  }
67
- return { ops: out, known: out.length > 0 && known };
70
+ return { ops: out, known: out.length > 0 && known, unknown };
68
71
  }
69
72
  function findBlockEnd(content, openBraceIndex) {
70
73
  let depth = 0;
@@ -247,13 +250,14 @@ function extractRulesContent(rawContent, relFile) {
247
250
  const requestResourceReferences = [
248
251
  ...big.matchAll(/request\.resource\.data\.([A-Za-z0-9_]+)/g)
249
252
  ].map((x) => x[1]);
250
- const { ops, known } = opsFrom(target);
253
+ const { ops, known, unknown } = opsFrom(target);
251
254
  const isHelperCall = !unconditional && rawCondition !== resolved;
252
255
  const hasAuth = authSource.includes("request.auth");
253
256
  rules.push({
254
257
  id: `rule-${idx++}`,
255
258
  path: fullPath,
256
259
  operations: ops,
260
+ unknownOperations: unknown.length > 0 ? unknown : void 0,
257
261
  conditionPresent: !unconditional && condition.length > 0,
258
262
  condition: unconditional ? void 0 : condition,
259
263
  authReferences,
@@ -1,9 +1,9 @@
1
1
  // src/verify.ts
2
- import { readFileSync, existsSync } from "fs";
3
- function collectVisitCounts(node, out) {
4
- if (typeof node !== "object" || node === null) return;
2
+ import { readFileSync, existsSync, statSync } from "fs";
3
+ function collectVisitCounts(node, out, depth = 0) {
4
+ if (typeof node !== "object" || node === null || depth > 100) return;
5
5
  if (Array.isArray(node)) {
6
- for (const v of node) collectVisitCounts(v, out);
6
+ for (const v of node) collectVisitCounts(v, out, depth + 1);
7
7
  return;
8
8
  }
9
9
  const rec = node;
@@ -14,7 +14,7 @@ function collectVisitCounts(node, out) {
14
14
  break;
15
15
  }
16
16
  }
17
- for (const v of Object.values(rec)) collectVisitCounts(v, out);
17
+ for (const v of Object.values(rec)) collectVisitCounts(v, out, depth + 1);
18
18
  }
19
19
  function planVerify(model) {
20
20
  const roles = model.authorization.roles.length > 0 ? model.authorization.roles.map((r) => r.name) : ["anonymous", "user", "admin"];
@@ -63,6 +63,13 @@ function verify(model, opts = {}) {
63
63
  });
64
64
  } else {
65
65
  try {
66
+ let tooBig = false;
67
+ try {
68
+ tooBig = statSync(opts.coverageFile).size > 5 * 1024 * 1024;
69
+ } catch {
70
+ tooBig = false;
71
+ }
72
+ if (tooBig) throw new Error("coverage acima de 5 MB");
66
73
  const raw = JSON.parse(readFileSync(opts.coverageFile, "utf-8"));
67
74
  const counts = [];
68
75
  collectVisitCounts(raw, counts);
@@ -95,7 +102,7 @@ function verify(model, opts = {}) {
95
102
  rule: "COVERAGE_INVALID",
96
103
  severity: "WARNING",
97
104
  confidence: "CONFIRMED",
98
- message: "Arquivo de cobertura inv\xE1lido \u2014 Verifier sem cobertura neste run.",
105
+ message: "Arquivo de cobertura inv\xE1lido ou acima de 5 MB \u2014 Verifier sem cobertura neste run.",
99
106
  fingerprint: "COVERAGE_INVALID",
100
107
  evidence: [{ id: "ev-coverage", kind: "coverage-parse", summary: "JSON inv\xE1lido", confidence: "CONFIRMED" }]
101
108
  });
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  scan
3
- } from "./chunk-BB6XXEIK.js";
3
+ } from "./chunk-NZT4NIVA.js";
4
4
  import {
5
5
  verify
6
- } from "./chunk-DIO36A3I.js";
6
+ } from "./chunk-CSMT4PIP.js";
7
7
  import {
8
8
  drift
9
- } from "./chunk-XZ32YQQC.js";
9
+ } from "./chunk-LD3F73XI.js";
10
10
 
11
11
  // src/mcp.ts
12
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -142,17 +142,18 @@ function createMcpServer() {
142
142
  "verify",
143
143
  {
144
144
  title: "Firebase Audit Verify (Emulator)",
145
- description: "Gera matriz papel\xD7opera\xE7\xE3o\xD7path e avalia cobertura do ruleCoverage. Requer Emulator. Sem contrato usa pap\xE9is reduzidos (anonymous/user/admin). Nunca afirma cobertura sem teste real.",
145
+ description: "Gera matriz papel\xD7opera\xE7\xE3o\xD7path e avalia cobertura do ruleCoverage. Requer Emulator. Sem contrato usa pap\xE9is reduzidos (anonymous/user/admin). Nunca afirma cobertura sem teste real. N\xE3o substitui scan/check.",
146
146
  inputSchema: {
147
147
  cwd: z.string().min(1).describe("Diret\xF3rio raiz do projeto"),
148
- coverage: z.string().optional().describe("Path do coverage.json do ruleCoverage (relativo ao cwd)")
148
+ coverage: z.string().optional().describe("Path do coverage.json do ruleCoverage (relativo ao cwd)"),
149
+ adapter: z.string().optional().describe("Fun\xE7\xE3o de permiss\xE3o no c\xF3digo que recebe o nome como texto (ex: rbac.can). Default: do contrato.")
149
150
  }
150
151
  },
151
- async ({ cwd, coverage }) => {
152
+ async ({ cwd, coverage, adapter }) => {
152
153
  try {
153
154
  assertCwd(cwd);
154
155
  const { resolve } = await import("path");
155
- const { summary: scanSummary, model } = await scan(cwd);
156
+ const { summary: scanSummary, model } = await scan(cwd, { adapterFn: adapter });
156
157
  const res = verify(model, { coverageFile: coverage ? resolve(cwd, coverage) : void 0 });
157
158
  const roles = model.authorization.roles.length > 0 ? model.authorization.roles.map((r) => r.name) : ["anonymous", "user", "admin"];
158
159
  const truncated = res.cases.length > 50;
@@ -1,5 +1,5 @@
1
1
  // src/drift.ts
2
- import { readFileSync, existsSync } from "fs";
2
+ import { readFileSync, existsSync, statSync } from "fs";
3
3
  function normalizeMode(f) {
4
4
  if (typeof f.fieldPath !== "string" || f.fieldPath.length === 0) return null;
5
5
  if (f.vectorConfig !== void 0 && f.vectorConfig !== null) return { fieldPath: f.fieldPath, mode: "VECTOR" };
@@ -59,10 +59,19 @@ function drift(localFile, remoteFile) {
59
59
  });
60
60
  return { entries: [], findings };
61
61
  }
62
+ const tooLarge = (f) => {
63
+ if (!f || !existsSync(f)) return false;
64
+ try {
65
+ return statSync(f).size > 5 * 1024 * 1024;
66
+ } catch {
67
+ return false;
68
+ }
69
+ };
62
70
  let local = [];
63
71
  let remote = [];
64
72
  if (localFile && existsSync(localFile)) {
65
73
  try {
74
+ if (tooLarge(localFile)) throw new Error("local acima de 5 MB");
66
75
  const raw = JSON.parse(readFileSync(localFile, "utf-8"));
67
76
  const rawIndexes = normalizeList(raw);
68
77
  const parsed = [];
@@ -88,7 +97,7 @@ function drift(localFile, remoteFile) {
88
97
  rule: "DRIFT_LOCAL_INVALID",
89
98
  severity: "WARNING",
90
99
  confidence: "CONFIRMED",
91
- message: "\xCDndices locais inv\xE1lidos \u2014 drift sem base local.",
100
+ message: "\xCDndices locais inv\xE1lidos ou acima de 5 MB \u2014 drift sem base local.",
92
101
  fingerprint: "DRIFT_LOCAL_INVALID",
93
102
  evidence: [{ id: "ev-drift-local", kind: "drift-parse", summary: "local inv\xE1lido", confidence: "CONFIRMED" }]
94
103
  });
@@ -96,6 +105,7 @@ function drift(localFile, remoteFile) {
96
105
  }
97
106
  if (remoteFile && existsSync(remoteFile)) {
98
107
  try {
108
+ if (tooLarge(remoteFile)) throw new Error("remoto acima de 5 MB");
99
109
  const list = normalizeList(JSON.parse(readFileSync(remoteFile, "utf-8")));
100
110
  const parsed = [];
101
111
  let skipped = 0;
@@ -120,7 +130,7 @@ function drift(localFile, remoteFile) {
120
130
  rule: "DRIFT_REMOTE_INVALID",
121
131
  severity: "WARNING",
122
132
  confidence: "CONFIRMED",
123
- message: "\xCDndices remotos inv\xE1lidos \u2014 drift sem base remota.",
133
+ message: "\xCDndices remotos inv\xE1lidos ou acima de 5 MB \u2014 drift sem base remota.",
124
134
  fingerprint: "DRIFT_REMOTE_INVALID",
125
135
  evidence: [{ id: "ev-drift-remote", kind: "drift-parse", summary: "remoto inv\xE1lido", confidence: "CONFIRMED" }]
126
136
  });
@@ -9,10 +9,10 @@ import {
9
9
  resolveHelperConditionDetailed,
10
10
  stripOuterParens,
11
11
  stripRuleComments
12
- } from "./chunk-FECVBQUO.js";
12
+ } from "./chunk-7J3ADEBW.js";
13
13
 
14
14
  // src/scan.ts
15
- import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
15
+ import { readFileSync as readFileSync3, existsSync as existsSync3, statSync as statSync2 } from "fs";
16
16
  import { join as join3 } from "path";
17
17
  import { parse as parseYaml } from "yaml";
18
18
 
@@ -107,6 +107,10 @@ var SERVER_HINTS_SEGMENTS = [
107
107
  "backend",
108
108
  "scripts"
109
109
  ];
110
+ function isTestPath(file) {
111
+ const n = file.replace(/\\/g, "/");
112
+ return /\.test\.[tj]sx?$/.test(n) || /\.spec\.[tj]sx?$/.test(n) || /(^|\/)__tests__\//.test(n) || /(^|\/)(test|tests|testing)\//.test(n);
113
+ }
110
114
  function classifyOrigin(file, content) {
111
115
  const rel = file.replace(/\\/g, "/");
112
116
  const segs = rel.split("/");
@@ -123,6 +127,8 @@ function classifyOrigin(file, content) {
123
127
  if (rel.includes("src/app/api/")) return "SERVER";
124
128
  if (rel.includes("server/api/")) return "SERVER";
125
129
  if (hasSeg(...SERVER_HINTS_SEGMENTS)) return "SERVER";
130
+ const base = segs[segs.length - 1] ?? "";
131
+ if (/^(server|middleware)[.\-]/i.test(base)) return "SERVER";
126
132
  if (content.includes("firebase-admin") || content.includes("firebase-functions")) {
127
133
  return "CLIENT";
128
134
  }
@@ -161,14 +167,14 @@ async function enrichWithGraph(rootDir, d) {
161
167
  const res = await ai.map({ cwd: rootDir, format: "json" });
162
168
  const files = Array.isArray(res.files) ? res.files : [];
163
169
  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|build|\.next|coverage|lib)\//.test(p)).slice(0, 500);
164
- const { readFileSync: readSync, existsSync: existsSyncFn, statSync: statSync2 } = await import("fs");
170
+ const { readFileSync: readSync, existsSync: existsSyncFn, statSync: statSync3 } = await import("fs");
165
171
  const { join: joinP } = await import("path");
166
172
  for (const rel of code) {
167
173
  let content = "";
168
174
  try {
169
175
  const abs = joinP(rootDir, rel);
170
176
  if (!existsSyncFn(abs)) continue;
171
- if (statSync2(abs).size > 100 * 1024) continue;
177
+ if (statSync3(abs).size > 100 * 1024) continue;
172
178
  content = readSync(abs, "utf-8");
173
179
  } catch {
174
180
  content = "";
@@ -187,7 +193,7 @@ import { join as join2 } from "path";
187
193
  import { executeFind } from "@justmpm/supergrep";
188
194
  function isBuildOutput(file) {
189
195
  const n = file.replace(/\\/g, "/");
190
- return /(^|\/)(node_modules|dist|build|\.next|coverage|out)\//.test(n);
196
+ return /(^|\/)(node_modules|dist|build|\.next|coverage|out)\//.test(n) || /\/functions\/lib\//.test(n);
191
197
  }
192
198
  var IMPLEMENTED_CHECKS = ["FBA001", "FBA002", "FBA003", "FBA004", "FBA009", "FBA010", "FBA011", "FBA012", "FBA013"];
193
199
  function createEvidence() {
@@ -273,6 +279,28 @@ async function runChecks(model, rootDir, opts = {}) {
273
279
  }
274
280
  }
275
281
  }
282
+ for (const rule of model.rules) {
283
+ const unknown2 = rule.unknownOperations ?? [];
284
+ if (rule.operations.length === 0 || unknown2.length > 0) {
285
+ const detail = unknown2.length > 0 ? ` (trecho desconhecido: ${unknown2.join(", ")})` : "";
286
+ findings.push(
287
+ make(
288
+ "RULES_UNKNOWN_OP",
289
+ "WARNING",
290
+ "CONFIRMED",
291
+ `Opera\xE7\xE3o desconhecida em ${rule.path}${detail} (${rule.location.file}:${rule.location.start.line}). Typo em allow? Deploy rejeita.`,
292
+ `RULES_UNKNOWN_OP:${rule.path}:${rule.location.start.line}:${[...rule.operations].sort().join(",")}`,
293
+ {
294
+ file: rule.location.file,
295
+ line: rule.location.start.line,
296
+ resource: rule.path,
297
+ evidence: [ev("unknown-op", rule.condition ?? "allow sem opera\xE7\xE3o v\xE1lida", "CONFIRMED", rule.location.file, rule.location.start.line)],
298
+ fix: "Corrija para read/get/list/write/create/update/delete."
299
+ }
300
+ )
301
+ );
302
+ }
303
+ }
276
304
  const adapterFn = opts.adapterFn ?? "rbac.can";
277
305
  const permissionCalls = await collectPermissionCalls(rootDir, adapterFn);
278
306
  const declared = new Set(model.authorization.permissions.map((p) => p.name));
@@ -508,10 +536,6 @@ async function runChecks(model, rootDir, opts = {}) {
508
536
  }
509
537
  }
510
538
  const adminHits = await collectAdminImports(rootDir);
511
- const isTestPath = (f) => {
512
- const n = f.replace(/\\/g, "/");
513
- return /\.test\.[tj]sx?$/.test(n) || /\.spec\.[tj]sx?$/.test(n) || /(^|\/)__tests__\//.test(n) || /(^|\/)(test|tests|testing)\//.test(n);
514
- };
515
539
  for (const hit of adminHits) {
516
540
  let origin = "UNKNOWN";
517
541
  try {
@@ -571,7 +595,7 @@ async function collectPermissionCalls(rootDir, adapterFn) {
571
595
  }
572
596
  const seen = /* @__PURE__ */ new Set();
573
597
  const pushLiteral = (file, line, permission) => {
574
- if (isBuildOutput(file)) return;
598
+ if (isBuildOutput(file) || isTestPath(file)) return;
575
599
  const k = `${file}:${line}:${permission}`;
576
600
  if (seen.has(k)) return;
577
601
  seen.add(k);
@@ -584,7 +608,7 @@ async function collectPermissionCalls(rootDir, adapterFn) {
584
608
  const raw = (m.metaVariables["PERM"] ?? "").trim();
585
609
  const line = m.line + 1;
586
610
  if (raw.includes("${")) {
587
- if (!isBuildOutput(m.file)) {
611
+ if (!isBuildOutput(m.file) && !isTestPath(m.file)) {
588
612
  const k = `${m.file}:${line}:${raw}`;
589
613
  if (!seen.has(k)) {
590
614
  seen.add(k);
@@ -598,7 +622,7 @@ async function collectPermissionCalls(rootDir, adapterFn) {
598
622
  const perm = raw.replace(/^['"`]|['"`]$/g, "").trim();
599
623
  if (/^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*)*$/.test(perm)) pushLiteral(m.file, line, perm);
600
624
  else {
601
- if (!isBuildOutput(m.file)) {
625
+ if (!isBuildOutput(m.file) && !isTestPath(m.file)) {
602
626
  const k = `${m.file}:${line}:${raw}:invalid`;
603
627
  if (!seen.has(k)) {
604
628
  seen.add(k);
@@ -607,7 +631,7 @@ async function collectPermissionCalls(rootDir, adapterFn) {
607
631
  }
608
632
  }
609
633
  } else if (raw.length > 0) {
610
- if (!isBuildOutput(m.file)) {
634
+ if (!isBuildOutput(m.file) && !isTestPath(m.file)) {
611
635
  const k = `${m.file}:${line}:${raw}`;
612
636
  if (!seen.has(k)) {
613
637
  seen.add(k);
@@ -750,6 +774,8 @@ var RuleSchema = z.strictObject({
750
774
  id: z.string().min(1),
751
775
  path: z.string().min(1),
752
776
  operations: z.array(OperationSchema),
777
+ /** Partes de `allow` não reconhecidas (typo parcial: `get, gett`). */
778
+ unknownOperations: z.array(z.string()).optional(),
753
779
  conditionPresent: z.boolean(),
754
780
  condition: z.string().optional(),
755
781
  authReferences: z.array(z.string()),
@@ -886,6 +912,14 @@ function validateClaimSamples(samples) {
886
912
 
887
913
  // src/scan.ts
888
914
  async function scan(rootDir, opts = {}) {
915
+ const { statSync: statRoot } = await import("fs");
916
+ let rootOk = false;
917
+ try {
918
+ rootOk = statRoot(rootDir).isDirectory();
919
+ } catch {
920
+ rootOk = false;
921
+ }
922
+ if (!rootOk) throw new Error(`Diret\xF3rio n\xE3o existe: ${rootDir}`);
889
923
  const d = discover(rootDir);
890
924
  if (opts.graph === true) {
891
925
  try {
@@ -976,13 +1010,14 @@ async function scan(rootDir, opts = {}) {
976
1010
  if (d.firestoreRulesFile && existsSync3(d.firestoreRulesFile)) {
977
1011
  const rel = model.firebase.firestore.rulesFile ?? "firestore.rules";
978
1012
  try {
1013
+ if (tooLarge(d.firestoreRulesFile, 1024 * 1024)) throw new Error("rules acima de 1 MB");
979
1014
  model.rules = extractRules(d.firestoreRulesFile, rel);
980
1015
  } catch {
981
1016
  findings_pre.push({
982
1017
  rule: "RULES_UNREADABLE",
983
1018
  severity: "WARNING",
984
1019
  confidence: "CONFIRMED",
985
- message: "firestore.rules existe mas n\xE3o p\xF4de ser lido \u2014 FBA001/FBA002 sem cobertura.",
1020
+ message: "firestore.rules existe mas n\xE3o p\xF4de ser lido (ileg\xEDvel ou acima de 1 MB) \u2014 FBA001/FBA002 sem cobertura.",
986
1021
  fingerprint: "RULES_UNREADABLE",
987
1022
  evidence: [
988
1023
  { id: "ev-rules-unreadable", kind: "rules-read", summary: "leitura falhou", confidence: "CONFIRMED" }
@@ -992,6 +1027,7 @@ async function scan(rootDir, opts = {}) {
992
1027
  }
993
1028
  if (d.firestoreIndexesFile && existsSync3(d.firestoreIndexesFile)) {
994
1029
  try {
1030
+ if (tooLarge(d.firestoreIndexesFile, 1024 * 1024)) throw new Error("indexes acima de 1 MB");
995
1031
  const raw = JSON.parse(readFileSync3(d.firestoreIndexesFile, "utf-8"));
996
1032
  if (Array.isArray(raw.fieldOverrides) && raw.fieldOverrides.length > 0) {
997
1033
  findings_pre.push({
@@ -1062,7 +1098,7 @@ async function scan(rootDir, opts = {}) {
1062
1098
  rule: "INDEXES_INVALID",
1063
1099
  severity: "WARNING",
1064
1100
  confidence: "CONFIRMED",
1065
- message: "firestore.indexes.json inv\xE1lido \u2014 \xEDndices ignorados neste scan.",
1101
+ message: "firestore.indexes.json inv\xE1lido ou acima de 1 MB \u2014 \xEDndices ignorados neste scan.",
1066
1102
  fingerprint: "INDEXES_INVALID",
1067
1103
  evidence: [
1068
1104
  { id: "ev-indexes-invalid", kind: "indexes-parse", summary: "JSON inv\xE1lido", confidence: "CONFIRMED" }
@@ -1072,6 +1108,7 @@ async function scan(rootDir, opts = {}) {
1072
1108
  }
1073
1109
  if (d.auditYamlFile && existsSync3(d.auditYamlFile)) {
1074
1110
  try {
1111
+ if (tooLarge(d.auditYamlFile, 256 * 1024)) throw new Error("yaml acima de 256 KB");
1075
1112
  const text = readFileSync3(d.auditYamlFile, "utf-8");
1076
1113
  const parsed = parseYaml(text);
1077
1114
  const validated = AuditYamlSchema.safeParse(parsed);
@@ -1180,6 +1217,7 @@ async function scan(rootDir, opts = {}) {
1180
1217
  if (d.storageRulesFile && existsSync3(d.storageRulesFile)) {
1181
1218
  checked.push("storage.rules");
1182
1219
  try {
1220
+ if (tooLarge(d.storageRulesFile, 1024 * 1024)) throw new Error("storage acima de 1 MB");
1183
1221
  const raw = readFileSync3(d.storageRulesFile, "utf-8");
1184
1222
  const relStorage = model.firebase.storage?.rulesFile ?? "storage.rules";
1185
1223
  const clean = stripRuleComments(raw);
@@ -1288,13 +1326,13 @@ async function scan(rootDir, opts = {}) {
1288
1326
  }
1289
1327
  }
1290
1328
  } catch {
1291
- skipped.push("storage.rules ileg\xEDvel \u2014 Storage sem cobertura neste scan");
1329
+ skipped.push("storage.rules ileg\xEDvel ou acima de 1 MB \u2014 Storage sem cobertura neste scan");
1292
1330
  }
1293
1331
  } else {
1294
1332
  skipped.push("storage.rules n\xE3o observado \u2014 Storage fora deste scan");
1295
1333
  }
1296
1334
  if (d.functionsDir) {
1297
- const appcheck = await countAppCheckEnforced(rootDir);
1335
+ const appcheck = await countAppCheckEnforced(rootDir, d.functionsDir);
1298
1336
  model.functions = { dir: model.functions?.dir, appCheckEnforced: appcheck.occurrences };
1299
1337
  checked.push("functions-appcheck");
1300
1338
  if (appcheck.manual > 0) {
@@ -1384,12 +1422,19 @@ async function scan(rootDir, opts = {}) {
1384
1422
  }
1385
1423
  };
1386
1424
  }
1387
- async function countAppCheckEnforced(rootDir) {
1425
+ function tooLarge(abs, maxBytes) {
1426
+ try {
1427
+ return statSync2(abs).size > maxBytes;
1428
+ } catch {
1429
+ return false;
1430
+ }
1431
+ }
1432
+ async function countAppCheckEnforced(rootDir, functionsDir) {
1388
1433
  try {
1389
1434
  const { readdirSync, readFileSync: readSync, statSync: stat } = await import("fs");
1390
1435
  const { join: joinP } = await import("path");
1391
- const { stripRuleComments: strip } = await import("./rules-W4VUUQM4.js");
1392
- const roots = [joinP(rootDir, "functions")];
1436
+ const { stripRuleComments: strip } = await import("./rules-HCXATUST.js");
1437
+ const roots = [functionsDir ?? joinP(rootDir, "functions")];
1393
1438
  let occurrences = 0;
1394
1439
  const files = /* @__PURE__ */ new Set();
1395
1440
  let manual = 0;
@@ -1397,13 +1442,9 @@ async function countAppCheckEnforced(rootDir) {
1397
1442
  let limitHit = false;
1398
1443
  const stack = [...roots];
1399
1444
  let guard = 0;
1400
- const isTestFile = (p) => {
1401
- const n = p.replace(/\\/g, "/");
1402
- return /\.test\.[tj]sx?$/.test(n) || /\.spec\.[tj]sx?$/.test(n) || /(^|\/)__tests__\//.test(n) || /(^|\/)(test|tests|testing)\//.test(n);
1403
- };
1445
+ const isTestFile = isTestPath;
1404
1446
  while (stack.length > 0 && guard < 200) {
1405
1447
  guard += 1;
1406
- if (guard >= 200 && stack.length > 0) limitHit = true;
1407
1448
  const cur = stack.pop();
1408
1449
  let entries = [];
1409
1450
  try {
@@ -1450,12 +1491,13 @@ async function countAppCheckEnforced(rootDir) {
1450
1491
  files.add(abs);
1451
1492
  }
1452
1493
  }
1453
- const manualHits = text.match(/verifyToken\s*\(|verify_token\s*\(|consumeAppCheckToken|getLimitedUseToken|getAppCheck\s*\(/g);
1494
+ const manualHits = text.match(/verifyToken\s*\(|verify_token\s*\(|consumeAppCheckToken|getLimitedUseToken/g);
1454
1495
  if (manualHits) manual += manualHits.length;
1455
1496
  } catch {
1456
1497
  }
1457
1498
  }
1458
1499
  }
1500
+ if (stack.length > 0) limitHit = true;
1459
1501
  return { occurrences, files: files.size, manual, ignoredOnRequest, limitHit };
1460
1502
  } catch {
1461
1503
  return { occurrences: 0, files: 0, manual: 0, ignoredOnRequest: 0, limitHit: false };
@@ -1522,9 +1564,14 @@ function parseStorageAllows(clean) {
1522
1564
  const re = /allow\s+([^;:]+?)(?::\s*if\s+([^;]+))?;/gi;
1523
1565
  let m;
1524
1566
  while ((m = re.exec(clean)) !== null) {
1525
- const containing = matches.filter((b) => b.start <= m.index && m.index < b.end).sort((a, b) => b.start - a.start);
1567
+ const containing = matches.filter((b) => b.start <= m.index && m.index < b.end).sort((a, b) => a.start - b.start);
1568
+ const parts = [];
1569
+ for (const b of containing) {
1570
+ const p = b.path.trim().replace(/^\/+|\/+$/g, "");
1571
+ if (p.length > 0) parts.push(p);
1572
+ }
1526
1573
  const line = clean.slice(0, m.index).split("\n").length;
1527
- out.push({ target: m[1].trim(), condition: m[2]?.trim(), matchPath: containing[0]?.path ?? "/(unknown)", line });
1574
+ out.push({ target: m[1].trim(), condition: m[2]?.trim(), matchPath: parts.length > 0 ? `/${parts.join("/")}` : "/(unknown)", line });
1528
1575
  }
1529
1576
  return out;
1530
1577
  }
@@ -1550,6 +1597,7 @@ function expandStorageOps(target) {
1550
1597
  export {
1551
1598
  discover,
1552
1599
  SERVER_HINTS_SEGMENTS,
1600
+ isTestPath,
1553
1601
  classifyOrigin,
1554
1602
  emptyModel,
1555
1603
  enrichWithGraph,
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  scan
4
- } from "./chunk-BB6XXEIK.js";
5
- import "./chunk-FECVBQUO.js";
4
+ } from "./chunk-NZT4NIVA.js";
5
+ import "./chunk-7J3ADEBW.js";
6
6
 
7
7
  // src/cli.ts
8
8
  import { createRequire } from "module";
@@ -46,6 +46,9 @@ Uso: firebase-audit scan [--json] [--strict] [--adapter=fn] [--cwd=path]`);
46
46
  console.error(`Valor inv\xE1lido para --strict: ${strictArg} (use --strict, --strict=true ou --strict=false)`);
47
47
  process.exit(1);
48
48
  }
49
+ if (cmd === "check" && strictArg === "false") {
50
+ console.error("Aviso: 'check' \xE9 sempre strict por defini\xE7\xE3o \u2014 --strict=false ignorado.");
51
+ }
49
52
  const asJsonArg = getArg("json");
50
53
  const asJson = (args.includes("--json") || asJsonArg === "true") && asJsonArg !== "false";
51
54
  if (asJsonArg !== void 0 && asJsonArg !== "true" && asJsonArg !== "false") {
@@ -81,8 +84,8 @@ Uso: firebase-audit scan|check|verify|drift [--json] ...`);
81
84
  const { resolve: resolveRoot } = await import("path");
82
85
  const rootDir = cwdArg ? resolveRoot(process.cwd(), cwdArg) : process.cwd();
83
86
  if (cmd === "verify") {
84
- const { scan: scanForVerify } = await import("./scan-SDJSDRDQ.js");
85
- const { verify } = await import("./verify-DECP4IUW.js");
87
+ const { scan: scanForVerify } = await import("./scan-RTIMO3I2.js");
88
+ const { verify } = await import("./verify-GQSR4FQL.js");
86
89
  const { resolve: resolveVerify } = await import("path");
87
90
  const coverageArg = getArg("coverage");
88
91
  const coveragePath = coverageArg ? resolveVerify(rootDir, coverageArg) : void 0;
@@ -116,7 +119,7 @@ ${icon} ${f.rule} [${f.confidence}]
116
119
  return;
117
120
  }
118
121
  if (cmd === "drift") {
119
- const { drift } = await import("./drift-QXALNO2V.js");
122
+ const { drift } = await import("./drift-C3JZI5ZA.js");
120
123
  const { resolve: resolveDrift } = await import("path");
121
124
  const { existsSync: existsDrift } = await import("fs");
122
125
  const localArg = getArg("local");
@@ -130,7 +133,7 @@ ${icon} ${f.rule} [${f.confidence}]
130
133
  for (const [label, p] of [["--local", localPath], ["--remote", remotePath]]) {
131
134
  if (p && !existsDrift(p)) {
132
135
  if (asJson) {
133
- const { drift: driftMissing } = await import("./drift-QXALNO2V.js");
136
+ const { drift: driftMissing } = await import("./drift-C3JZI5ZA.js");
134
137
  const res2 = driftMissing(
135
138
  localPath && existsDrift(localPath) ? localPath : null,
136
139
  remotePath && existsDrift(remotePath) ? remotePath : null
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  drift
3
- } from "./chunk-XZ32YQQC.js";
3
+ } from "./chunk-LD3F73XI.js";
4
4
  export {
5
5
  drift
6
6
  };
package/dist/index.d.ts CHANGED
@@ -217,6 +217,7 @@ declare const RuleSchema: z.ZodObject<{
217
217
  delete: "delete";
218
218
  write: "write";
219
219
  }>>;
220
+ unknownOperations: z.ZodOptional<z.ZodArray<z.ZodString>>;
220
221
  conditionPresent: z.ZodBoolean;
221
222
  condition: z.ZodOptional<z.ZodString>;
222
223
  authReferences: z.ZodArray<z.ZodString>;
@@ -419,6 +420,7 @@ declare const ProjectModelSchema: z.ZodObject<{
419
420
  delete: "delete";
420
421
  write: "write";
421
422
  }>>;
423
+ unknownOperations: z.ZodOptional<z.ZodArray<z.ZodString>>;
422
424
  conditionPresent: z.ZodBoolean;
423
425
  condition: z.ZodOptional<z.ZodString>;
424
426
  authReferences: z.ZodArray<z.ZodString>;
@@ -639,6 +641,8 @@ interface DiscoveryResult {
639
641
  declare function discover(rootDir: string): DiscoveryResult;
640
642
  /** Segmentos de servidor (fail-closed: `api` genérico NÃO é server, ver classifyOrigin). */
641
643
  declare const SERVER_HINTS_SEGMENTS: string[];
644
+ /** Arquivo de teste/setup de Emulator (com Admin legitimamente): nunca reprova CI. */
645
+ declare function isTestPath(file: string): boolean;
642
646
  /** Heurística CLIENT vs SERVER: caminho manda primeiro (grafo ai-tool preenche listas na Fase 3).
643
647
  * Segmentos ancorados — nunca substring solta (`myapp/` não é `app/`). */
644
648
  declare function classifyOrigin(file: string, content: string): "CLIENT" | "SERVER" | "UNKNOWN";
@@ -665,6 +669,7 @@ declare function stripRuleComments(content: string): string;
665
669
  declare function opsFrom(target: string): {
666
670
  ops: Rule["operations"];
667
671
  known: boolean;
672
+ unknown: string[];
668
673
  };
669
674
  /** Normaliza target de allow para fingerprint estável (case/espaço/ordem). */
670
675
  declare function normalizeTarget(target: string): string;
@@ -763,7 +768,9 @@ interface ScanSummary {
763
768
  errors: number;
764
769
  warnings: number;
765
770
  infos: number;
771
+ /** Checks FBA que passaram (só FBA; infra nomeada vai em `infra`). */
766
772
  passedChecks: number;
773
+ /** Total de checks FBA implementados. */
767
774
  totalChecks: number;
768
775
  /** Infra nomeada (fora dos checks FBA): cada item diz qual e por quê. */
769
776
  infra: {
@@ -845,4 +852,4 @@ declare function drift(localFile: string | null, remoteFile: string | null): Dri
845
852
 
846
853
  declare const VERSION: string;
847
854
 
848
- export { AccessOriginSchema, type AuditYaml, AuditYamlSchema, ConfidenceSchema, type DiscoveryResult, type DriftEntry, type DriftResult, type DriftStatus, DynamicValueSchema, type Evidence, EvidenceSchema, type Finding, FindingSchema, FirestoreIndexSchema, GraphEdgeSchema, IMPLEMENTED_CHECKS, IndexFieldSchema, LocationSchema, OperationSchema, PermissionSchema, PositionSchema, type ProjectModel, ProjectModelSchema, QueryFilterSchema, QueryOrderSchema, type QueryShape, QueryShapeSchema, RESERVED_CLAIM_KEYS, RoleSchema, RuleSchema, SERVER_HINTS_SEGMENTS, type ScanOptions, type ScanResult, type ScanSummary, SeveritySchema, VERSION, type VerifyCase, type VerifyOptions, type VerifyResult, buildFullPath, classifyOrigin, conditionKeyForFingerprint, discover, drift, emptyModel, enrichWithGraph, extractRules, extractRulesContent, findPublicAllows, findPublicAllowsContent, hashShort, helperBodyToCondition, inlineHelperArgs, inlineHelpersInCondition, isPublicCondition, normalizeTarget, opsFrom, parseMatchBlocks, parseRuleFunctions, parseRuleFunctionsDetailed, planVerify, resolveHelperCondition, resolveHelperConditionDetailed, runChecks, scan, stripOuterParens, stripRuleComments, validateClaimSamples, verify };
855
+ export { AccessOriginSchema, type AuditYaml, AuditYamlSchema, ConfidenceSchema, type DiscoveryResult, type DriftEntry, type DriftResult, type DriftStatus, DynamicValueSchema, type Evidence, EvidenceSchema, type Finding, FindingSchema, FirestoreIndexSchema, GraphEdgeSchema, IMPLEMENTED_CHECKS, IndexFieldSchema, LocationSchema, OperationSchema, PermissionSchema, PositionSchema, type ProjectModel, ProjectModelSchema, QueryFilterSchema, QueryOrderSchema, type QueryShape, QueryShapeSchema, RESERVED_CLAIM_KEYS, RoleSchema, RuleSchema, SERVER_HINTS_SEGMENTS, type ScanOptions, type ScanResult, type ScanSummary, SeveritySchema, VERSION, type VerifyCase, type VerifyOptions, type VerifyResult, buildFullPath, classifyOrigin, conditionKeyForFingerprint, discover, drift, emptyModel, enrichWithGraph, extractRules, extractRulesContent, findPublicAllows, findPublicAllowsContent, hashShort, helperBodyToCondition, inlineHelperArgs, inlineHelpersInCondition, isPublicCondition, isTestPath, normalizeTarget, opsFrom, parseMatchBlocks, parseRuleFunctions, parseRuleFunctionsDetailed, planVerify, resolveHelperCondition, resolveHelperConditionDetailed, runChecks, scan, stripOuterParens, stripRuleComments, validateClaimSamples, verify };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createMcpServer,
3
3
  startMcpServer
4
- } from "./chunk-SH6HFX5Y.js";
4
+ } from "./chunk-D75SRJ7P.js";
5
5
  import {
6
6
  AccessOriginSchema,
7
7
  AuditYamlSchema,
@@ -30,10 +30,11 @@ import {
30
30
  discover,
31
31
  emptyModel,
32
32
  enrichWithGraph,
33
+ isTestPath,
33
34
  runChecks,
34
35
  scan,
35
36
  validateClaimSamples
36
- } from "./chunk-BB6XXEIK.js";
37
+ } from "./chunk-NZT4NIVA.js";
37
38
  import {
38
39
  buildFullPath,
39
40
  conditionKeyForFingerprint,
@@ -55,14 +56,14 @@ import {
55
56
  resolveHelperConditionDetailed,
56
57
  stripOuterParens,
57
58
  stripRuleComments
58
- } from "./chunk-FECVBQUO.js";
59
+ } from "./chunk-7J3ADEBW.js";
59
60
  import {
60
61
  planVerify,
61
62
  verify
62
- } from "./chunk-DIO36A3I.js";
63
+ } from "./chunk-CSMT4PIP.js";
63
64
  import {
64
65
  drift
65
- } from "./chunk-XZ32YQQC.js";
66
+ } from "./chunk-LD3F73XI.js";
66
67
 
67
68
  // src/index.ts
68
69
  import { createRequire } from "module";
@@ -111,6 +112,7 @@ export {
111
112
  inlineHelperArgs,
112
113
  inlineHelpersInCondition,
113
114
  isPublicCondition,
115
+ isTestPath,
114
116
  normalizeTarget,
115
117
  opsFrom,
116
118
  parseMatchBlocks,
package/dist/mcp-cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startMcpServer
4
- } from "./chunk-SH6HFX5Y.js";
5
- import "./chunk-BB6XXEIK.js";
6
- import "./chunk-FECVBQUO.js";
7
- import "./chunk-DIO36A3I.js";
8
- import "./chunk-XZ32YQQC.js";
4
+ } from "./chunk-D75SRJ7P.js";
5
+ import "./chunk-NZT4NIVA.js";
6
+ import "./chunk-7J3ADEBW.js";
7
+ import "./chunk-CSMT4PIP.js";
8
+ import "./chunk-LD3F73XI.js";
9
9
 
10
10
  // src/mcp-cli.ts
11
11
  startMcpServer().catch((err) => {
package/dist/mcp.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  createMcpServer,
3
3
  startMcpServer
4
- } from "./chunk-SH6HFX5Y.js";
5
- import "./chunk-BB6XXEIK.js";
6
- import "./chunk-FECVBQUO.js";
7
- import "./chunk-DIO36A3I.js";
8
- import "./chunk-XZ32YQQC.js";
4
+ } from "./chunk-D75SRJ7P.js";
5
+ import "./chunk-NZT4NIVA.js";
6
+ import "./chunk-7J3ADEBW.js";
7
+ import "./chunk-CSMT4PIP.js";
8
+ import "./chunk-LD3F73XI.js";
9
9
  export {
10
10
  createMcpServer,
11
11
  startMcpServer
@@ -19,7 +19,7 @@ import {
19
19
  resolveHelperConditionDetailed,
20
20
  stripOuterParens,
21
21
  stripRuleComments
22
- } from "./chunk-FECVBQUO.js";
22
+ } from "./chunk-7J3ADEBW.js";
23
23
  export {
24
24
  buildFullPath,
25
25
  conditionKeyForFingerprint,
@@ -0,0 +1,7 @@
1
+ import {
2
+ scan
3
+ } from "./chunk-NZT4NIVA.js";
4
+ import "./chunk-7J3ADEBW.js";
5
+ export {
6
+ scan
7
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  planVerify,
3
3
  verify
4
- } from "./chunk-DIO36A3I.js";
4
+ } from "./chunk-CSMT4PIP.js";
5
5
  export {
6
6
  planVerify,
7
7
  verify
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justmpm/firebase-audit",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "description": "Auditor de consistência e segurança para projetos Firebase: código + Rules + índices + contrato vs comportamento. Static-first, nunca inventa certeza.",
5
5
  "keywords": [
6
6
  "firebase",
package/skill/SKILL.md CHANGED
@@ -34,6 +34,6 @@ Use `drift` para comparar índices locais vs implantados (MATCHED/LOCAL_ONLY/REM
34
34
 
35
35
  ## Limites honestos
36
36
 
37
- - AppCheck: varredura TS/Python por texto (`enforceAppCheck`, `verifyToken` e afins). Config via variável conta como não observada; `onRequest` exige revisão rota a rota.
37
+ - AppCheck: varredura TS/Python por texto (`enforceAppCheck`, `verifyToken` e afins). Config via variável ou objeto compartilhado conta como não observada; janela de trigger pode errar em arquivo multi-função — `onRequest` exige revisão rota a rota.
38
38
  - Isolamento por tenant (ownerId/tenantId) e mismatch de papel não-admin: fora dos checks — revise manualmente ou no Emulator.
39
39
  - `graph: true` é prévia opt-in da biblioteca (nenhum check consome ainda).
@@ -1,7 +0,0 @@
1
- import {
2
- scan
3
- } from "./chunk-BB6XXEIK.js";
4
- import "./chunk-FECVBQUO.js";
5
- export {
6
- scan
7
- };