@justmpm/firebase-audit 0.5.6 → 0.5.7

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.
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  scan
3
- } from "./chunk-BB6XXEIK.js";
3
+ } from "./chunk-LJW7QXIJ.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";
@@ -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,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
  });
@@ -12,7 +12,7 @@ import {
12
12
  } from "./chunk-FECVBQUO.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
 
@@ -123,6 +123,8 @@ function classifyOrigin(file, content) {
123
123
  if (rel.includes("src/app/api/")) return "SERVER";
124
124
  if (rel.includes("server/api/")) return "SERVER";
125
125
  if (hasSeg(...SERVER_HINTS_SEGMENTS)) return "SERVER";
126
+ const base = segs[segs.length - 1] ?? "";
127
+ if (/^(server|middleware)[.\-]/i.test(base)) return "SERVER";
126
128
  if (content.includes("firebase-admin") || content.includes("firebase-functions")) {
127
129
  return "CLIENT";
128
130
  }
@@ -161,14 +163,14 @@ async function enrichWithGraph(rootDir, d) {
161
163
  const res = await ai.map({ cwd: rootDir, format: "json" });
162
164
  const files = Array.isArray(res.files) ? res.files : [];
163
165
  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");
166
+ const { readFileSync: readSync, existsSync: existsSyncFn, statSync: statSync3 } = await import("fs");
165
167
  const { join: joinP } = await import("path");
166
168
  for (const rel of code) {
167
169
  let content = "";
168
170
  try {
169
171
  const abs = joinP(rootDir, rel);
170
172
  if (!existsSyncFn(abs)) continue;
171
- if (statSync2(abs).size > 100 * 1024) continue;
173
+ if (statSync3(abs).size > 100 * 1024) continue;
172
174
  content = readSync(abs, "utf-8");
173
175
  } catch {
174
176
  content = "";
@@ -273,6 +275,26 @@ async function runChecks(model, rootDir, opts = {}) {
273
275
  }
274
276
  }
275
277
  }
278
+ for (const rule of model.rules) {
279
+ if (rule.operations.length === 0) {
280
+ findings.push(
281
+ make(
282
+ "RULES_UNKNOWN_OP",
283
+ "WARNING",
284
+ "CONFIRMED",
285
+ `Opera\xE7\xE3o desconhecida em ${rule.path} (${rule.location.file}:${rule.location.start.line}). Typo em allow? Deploy rejeita.`,
286
+ `RULES_UNKNOWN_OP:${rule.path}:${rule.location.start.line}`,
287
+ {
288
+ file: rule.location.file,
289
+ line: rule.location.start.line,
290
+ resource: rule.path,
291
+ evidence: [ev("unknown-op", rule.condition ?? "allow sem opera\xE7\xE3o v\xE1lida", "CONFIRMED", rule.location.file, rule.location.start.line)],
292
+ fix: "Corrija para read/get/list/write/create/update/delete."
293
+ }
294
+ )
295
+ );
296
+ }
297
+ }
276
298
  const adapterFn = opts.adapterFn ?? "rbac.can";
277
299
  const permissionCalls = await collectPermissionCalls(rootDir, adapterFn);
278
300
  const declared = new Set(model.authorization.permissions.map((p) => p.name));
@@ -976,13 +998,14 @@ async function scan(rootDir, opts = {}) {
976
998
  if (d.firestoreRulesFile && existsSync3(d.firestoreRulesFile)) {
977
999
  const rel = model.firebase.firestore.rulesFile ?? "firestore.rules";
978
1000
  try {
1001
+ if (tooLarge(d.firestoreRulesFile, 1024 * 1024)) throw new Error("rules acima de 1 MB");
979
1002
  model.rules = extractRules(d.firestoreRulesFile, rel);
980
1003
  } catch {
981
1004
  findings_pre.push({
982
1005
  rule: "RULES_UNREADABLE",
983
1006
  severity: "WARNING",
984
1007
  confidence: "CONFIRMED",
985
- message: "firestore.rules existe mas n\xE3o p\xF4de ser lido \u2014 FBA001/FBA002 sem cobertura.",
1008
+ message: "firestore.rules existe mas n\xE3o p\xF4de ser lido (ileg\xEDvel ou acima de 1 MB) \u2014 FBA001/FBA002 sem cobertura.",
986
1009
  fingerprint: "RULES_UNREADABLE",
987
1010
  evidence: [
988
1011
  { id: "ev-rules-unreadable", kind: "rules-read", summary: "leitura falhou", confidence: "CONFIRMED" }
@@ -992,6 +1015,7 @@ async function scan(rootDir, opts = {}) {
992
1015
  }
993
1016
  if (d.firestoreIndexesFile && existsSync3(d.firestoreIndexesFile)) {
994
1017
  try {
1018
+ if (tooLarge(d.firestoreIndexesFile, 1024 * 1024)) throw new Error("indexes acima de 1 MB");
995
1019
  const raw = JSON.parse(readFileSync3(d.firestoreIndexesFile, "utf-8"));
996
1020
  if (Array.isArray(raw.fieldOverrides) && raw.fieldOverrides.length > 0) {
997
1021
  findings_pre.push({
@@ -1062,7 +1086,7 @@ async function scan(rootDir, opts = {}) {
1062
1086
  rule: "INDEXES_INVALID",
1063
1087
  severity: "WARNING",
1064
1088
  confidence: "CONFIRMED",
1065
- message: "firestore.indexes.json inv\xE1lido \u2014 \xEDndices ignorados neste scan.",
1089
+ message: "firestore.indexes.json inv\xE1lido ou acima de 1 MB \u2014 \xEDndices ignorados neste scan.",
1066
1090
  fingerprint: "INDEXES_INVALID",
1067
1091
  evidence: [
1068
1092
  { id: "ev-indexes-invalid", kind: "indexes-parse", summary: "JSON inv\xE1lido", confidence: "CONFIRMED" }
@@ -1072,6 +1096,7 @@ async function scan(rootDir, opts = {}) {
1072
1096
  }
1073
1097
  if (d.auditYamlFile && existsSync3(d.auditYamlFile)) {
1074
1098
  try {
1099
+ if (tooLarge(d.auditYamlFile, 256 * 1024)) throw new Error("yaml acima de 256 KB");
1075
1100
  const text = readFileSync3(d.auditYamlFile, "utf-8");
1076
1101
  const parsed = parseYaml(text);
1077
1102
  const validated = AuditYamlSchema.safeParse(parsed);
@@ -1180,6 +1205,7 @@ async function scan(rootDir, opts = {}) {
1180
1205
  if (d.storageRulesFile && existsSync3(d.storageRulesFile)) {
1181
1206
  checked.push("storage.rules");
1182
1207
  try {
1208
+ if (tooLarge(d.storageRulesFile, 1024 * 1024)) throw new Error("storage acima de 1 MB");
1183
1209
  const raw = readFileSync3(d.storageRulesFile, "utf-8");
1184
1210
  const relStorage = model.firebase.storage?.rulesFile ?? "storage.rules";
1185
1211
  const clean = stripRuleComments(raw);
@@ -1288,13 +1314,13 @@ async function scan(rootDir, opts = {}) {
1288
1314
  }
1289
1315
  }
1290
1316
  } catch {
1291
- skipped.push("storage.rules ileg\xEDvel \u2014 Storage sem cobertura neste scan");
1317
+ skipped.push("storage.rules ileg\xEDvel ou acima de 1 MB \u2014 Storage sem cobertura neste scan");
1292
1318
  }
1293
1319
  } else {
1294
1320
  skipped.push("storage.rules n\xE3o observado \u2014 Storage fora deste scan");
1295
1321
  }
1296
1322
  if (d.functionsDir) {
1297
- const appcheck = await countAppCheckEnforced(rootDir);
1323
+ const appcheck = await countAppCheckEnforced(rootDir, d.functionsDir);
1298
1324
  model.functions = { dir: model.functions?.dir, appCheckEnforced: appcheck.occurrences };
1299
1325
  checked.push("functions-appcheck");
1300
1326
  if (appcheck.manual > 0) {
@@ -1384,12 +1410,19 @@ async function scan(rootDir, opts = {}) {
1384
1410
  }
1385
1411
  };
1386
1412
  }
1387
- async function countAppCheckEnforced(rootDir) {
1413
+ function tooLarge(abs, maxBytes) {
1414
+ try {
1415
+ return statSync2(abs).size > maxBytes;
1416
+ } catch {
1417
+ return false;
1418
+ }
1419
+ }
1420
+ async function countAppCheckEnforced(rootDir, functionsDir) {
1388
1421
  try {
1389
1422
  const { readdirSync, readFileSync: readSync, statSync: stat } = await import("fs");
1390
1423
  const { join: joinP } = await import("path");
1391
1424
  const { stripRuleComments: strip } = await import("./rules-W4VUUQM4.js");
1392
- const roots = [joinP(rootDir, "functions")];
1425
+ const roots = [functionsDir ?? joinP(rootDir, "functions")];
1393
1426
  let occurrences = 0;
1394
1427
  const files = /* @__PURE__ */ new Set();
1395
1428
  let manual = 0;
@@ -1403,7 +1436,6 @@ async function countAppCheckEnforced(rootDir) {
1403
1436
  };
1404
1437
  while (stack.length > 0 && guard < 200) {
1405
1438
  guard += 1;
1406
- if (guard >= 200 && stack.length > 0) limitHit = true;
1407
1439
  const cur = stack.pop();
1408
1440
  let entries = [];
1409
1441
  try {
@@ -1456,6 +1488,7 @@ async function countAppCheckEnforced(rootDir) {
1456
1488
  }
1457
1489
  }
1458
1490
  }
1491
+ if (stack.length > 0) limitHit = true;
1459
1492
  return { occurrences, files: files.size, manual, ignoredOnRequest, limitHit };
1460
1493
  } catch {
1461
1494
  return { occurrences: 0, files: 0, manual: 0, ignoredOnRequest: 0, limitHit: false };
@@ -1522,9 +1555,14 @@ function parseStorageAllows(clean) {
1522
1555
  const re = /allow\s+([^;:]+?)(?::\s*if\s+([^;]+))?;/gi;
1523
1556
  let m;
1524
1557
  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);
1558
+ const containing = matches.filter((b) => b.start <= m.index && m.index < b.end).sort((a, b) => a.start - b.start);
1559
+ const parts = [];
1560
+ for (const b of containing) {
1561
+ const p = b.path.trim().replace(/^\/+|\/+$/g, "");
1562
+ if (p.length > 0) parts.push(p);
1563
+ }
1526
1564
  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 });
1565
+ out.push({ target: m[1].trim(), condition: m[2]?.trim(), matchPath: parts.length > 0 ? `/${parts.join("/")}` : "/(unknown)", line });
1528
1566
  }
1529
1567
  return out;
1530
1568
  }
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  scan
4
- } from "./chunk-BB6XXEIK.js";
4
+ } from "./chunk-LJW7QXIJ.js";
5
5
  import "./chunk-FECVBQUO.js";
6
6
 
7
7
  // src/cli.ts
@@ -81,8 +81,8 @@ Uso: firebase-audit scan|check|verify|drift [--json] ...`);
81
81
  const { resolve: resolveRoot } = await import("path");
82
82
  const rootDir = cwdArg ? resolveRoot(process.cwd(), cwdArg) : process.cwd();
83
83
  if (cmd === "verify") {
84
- const { scan: scanForVerify } = await import("./scan-SDJSDRDQ.js");
85
- const { verify } = await import("./verify-DECP4IUW.js");
84
+ const { scan: scanForVerify } = await import("./scan-A34FDN66.js");
85
+ const { verify } = await import("./verify-GQSR4FQL.js");
86
86
  const { resolve: resolveVerify } = await import("path");
87
87
  const coverageArg = getArg("coverage");
88
88
  const coveragePath = coverageArg ? resolveVerify(rootDir, coverageArg) : void 0;
@@ -116,7 +116,7 @@ ${icon} ${f.rule} [${f.confidence}]
116
116
  return;
117
117
  }
118
118
  if (cmd === "drift") {
119
- const { drift } = await import("./drift-QXALNO2V.js");
119
+ const { drift } = await import("./drift-C3JZI5ZA.js");
120
120
  const { resolve: resolveDrift } = await import("path");
121
121
  const { existsSync: existsDrift } = await import("fs");
122
122
  const localArg = getArg("local");
@@ -130,7 +130,7 @@ ${icon} ${f.rule} [${f.confidence}]
130
130
  for (const [label, p] of [["--local", localPath], ["--remote", remotePath]]) {
131
131
  if (p && !existsDrift(p)) {
132
132
  if (asJson) {
133
- const { drift: driftMissing } = await import("./drift-QXALNO2V.js");
133
+ const { drift: driftMissing } = await import("./drift-C3JZI5ZA.js");
134
134
  const res2 = driftMissing(
135
135
  localPath && existsDrift(localPath) ? localPath : null,
136
136
  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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createMcpServer,
3
3
  startMcpServer
4
- } from "./chunk-SH6HFX5Y.js";
4
+ } from "./chunk-BHRZOZYA.js";
5
5
  import {
6
6
  AccessOriginSchema,
7
7
  AuditYamlSchema,
@@ -33,7 +33,7 @@ import {
33
33
  runChecks,
34
34
  scan,
35
35
  validateClaimSamples
36
- } from "./chunk-BB6XXEIK.js";
36
+ } from "./chunk-LJW7QXIJ.js";
37
37
  import {
38
38
  buildFullPath,
39
39
  conditionKeyForFingerprint,
@@ -59,10 +59,10 @@ import {
59
59
  import {
60
60
  planVerify,
61
61
  verify
62
- } from "./chunk-DIO36A3I.js";
62
+ } from "./chunk-CSMT4PIP.js";
63
63
  import {
64
64
  drift
65
- } from "./chunk-XZ32YQQC.js";
65
+ } from "./chunk-LD3F73XI.js";
66
66
 
67
67
  // src/index.ts
68
68
  import { createRequire } from "module";
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";
4
+ } from "./chunk-BHRZOZYA.js";
5
+ import "./chunk-LJW7QXIJ.js";
6
6
  import "./chunk-FECVBQUO.js";
7
- import "./chunk-DIO36A3I.js";
8
- import "./chunk-XZ32YQQC.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";
4
+ } from "./chunk-BHRZOZYA.js";
5
+ import "./chunk-LJW7QXIJ.js";
6
6
  import "./chunk-FECVBQUO.js";
7
- import "./chunk-DIO36A3I.js";
8
- import "./chunk-XZ32YQQC.js";
7
+ import "./chunk-CSMT4PIP.js";
8
+ import "./chunk-LD3F73XI.js";
9
9
  export {
10
10
  createMcpServer,
11
11
  startMcpServer
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  scan
3
- } from "./chunk-BB6XXEIK.js";
3
+ } from "./chunk-LJW7QXIJ.js";
4
4
  import "./chunk-FECVBQUO.js";
5
5
  export {
6
6
  scan
@@ -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.7",
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",