@justmpm/firebase-audit 0.5.1 → 0.5.2

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,6 +1,6 @@
1
1
  import {
2
2
  scan
3
- } from "./chunk-GDO2DLNQ.js";
3
+ } from "./chunk-UKEKA2NQ.js";
4
4
  import {
5
5
  verify
6
6
  } from "./chunk-Y4VXWSKX.js";
@@ -866,6 +866,16 @@ async function scan(rootDir, opts = {}) {
866
866
  }
867
867
  }
868
868
  } catch {
869
+ findings_pre.push({
870
+ rule: "FIREBASE_JSON_INVALID",
871
+ severity: "WARNING",
872
+ confidence: "CONFIRMED",
873
+ message: "firebase.json inv\xE1lido \u2014 seguindo com defaults (firestore.rules, firestore.indexes.json). Corrija o JSON.",
874
+ fingerprint: "FIREBASE_JSON_INVALID",
875
+ evidence: [
876
+ { id: "ev-firebase-json", kind: "config-parse", summary: "JSON inv\xE1lido", confidence: "CONFIRMED" }
877
+ ]
878
+ });
869
879
  }
870
880
  if (!d.firestoreRulesFile || !existsSync3(d.firestoreRulesFile)) {
871
881
  findings_pre.push({
@@ -1076,34 +1086,40 @@ async function scan(rootDir, opts = {}) {
1076
1086
  const raw = readFileSync3(d.storageRulesFile, "utf-8");
1077
1087
  const relStorage = model.firebase.storage?.rulesFile ?? "storage.rules";
1078
1088
  const clean = stripRuleComments(raw);
1079
- const low = clean.toLowerCase().replace(/\s+/g, "");
1080
- const hasAllPaths = low.includes("allpaths=**") || /\{[^}]*=\*\*\}/.test(low);
1089
+ let evCounter = 0;
1090
+ const evId = (prefix) => {
1091
+ evCounter += 1;
1092
+ return `ev-storage-${prefix}-${evCounter}`;
1093
+ };
1081
1094
  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;
1095
+ if (!isOpenStorageCondition(m.condition)) continue;
1084
1096
  const ops = expandStorageOps(m.target);
1085
1097
  const key = conditionKeyForFingerprint(m.condition);
1098
+ const wildcard = /\{[^}]*=\*\*\}/.test(m.matchPath) || m.matchPath.toLowerCase().includes("allpaths");
1099
+ const where = `${relStorage}:${m.line} (${m.matchPath}, allow ${m.target})`;
1086
1100
  if (ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete")) {
1087
1101
  findings.push({
1088
1102
  rule: "STORAGE_PUBLIC_WRITE",
1089
1103
  severity: "ERROR",
1090
1104
  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}`,
1105
+ message: `Storage com escrita aberta (${where}). Qualquer cliente pode escrever \u2014 cofre aberto, n\xE3o vitrine.`,
1106
+ fingerprint: `STORAGE_PUBLIC_WRITE:${m.matchPath}:${m.target}:${key}`,
1093
1107
  file: relStorage,
1094
- evidence: [{ id: `ev-storage-write-${findings.length}`, kind: "storage-public", summary: `${m.target} if true`, confidence: "CONFIRMED" }],
1108
+ line: m.line,
1109
+ evidence: [{ id: evId("write"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "CONFIRMED", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
1095
1110
  fix: "Exigir request.auth (e dono/tenant) no write. Deixe get aberto s\xF3 em pasta de vitrine como logos/."
1096
1111
  });
1097
1112
  } else if (ops.has("read") || ops.has("get") || ops.has("list")) {
1098
- if (hasAllPaths) {
1113
+ if (wildcard) {
1099
1114
  findings.push({
1100
1115
  rule: "STORAGE_ALL_PATHS_OPEN",
1101
1116
  severity: "WARNING",
1102
1117
  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}`,
1118
+ message: `Storage com leitura aberta em curinga total (${where}). Vale para tudo \u2014 se a inten\xE7\xE3o era s\xF3 logo, restrinja a /logos/{id}.`,
1119
+ fingerprint: `STORAGE_ALL_PATHS_OPEN:${m.matchPath}:${m.target}:${key}`,
1105
1120
  file: relStorage,
1106
- evidence: [{ id: `ev-storage-all-${findings.length}`, kind: "storage-public", summary: `${m.target} if true`, confidence: "PROBABLE" }],
1121
+ line: m.line,
1122
+ evidence: [{ id: evId("all"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
1107
1123
  fix: "Troque o curinga total por pastas expl\xEDcitas (ex: /logos/{logoId}) com get: if true e write restrito."
1108
1124
  });
1109
1125
  } else {
@@ -1111,10 +1127,11 @@ async function scan(rootDir, opts = {}) {
1111
1127
  rule: "STORAGE_PUBLIC_READ",
1112
1128
  severity: "INFO",
1113
1129
  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}`,
1130
+ message: `Storage com leitura aberta (${where}). Pode ser vitrine proposital (logos) \u2014 confirme que o write segue restrito.`,
1131
+ fingerprint: `STORAGE_PUBLIC_READ:${m.matchPath}:${m.target}:${key}`,
1116
1132
  file: relStorage,
1117
- evidence: [{ id: `ev-storage-read-${findings.length}`, kind: "storage-public", summary: `${m.target} if true`, confidence: "PROBABLE" }]
1133
+ line: m.line,
1134
+ evidence: [{ id: evId("read"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }]
1118
1135
  });
1119
1136
  }
1120
1137
  }
@@ -1126,24 +1143,38 @@ async function scan(rootDir, opts = {}) {
1126
1143
  skipped.push("storage.rules n\xE3o observado \u2014 Storage fora deste scan");
1127
1144
  }
1128
1145
  if (d.functionsDir) {
1129
- const enforced = await countAppCheckEnforced(rootDir);
1130
- model.functions = { dir: model.functions?.dir, appCheckEnforced: enforced };
1146
+ const appcheck = await countAppCheckEnforced(rootDir);
1147
+ model.functions = { dir: model.functions?.dir, appCheckEnforced: appcheck.occurrences };
1131
1148
  checked.push("functions-appcheck");
1132
- if (enforced > 0) {
1149
+ if (appcheck.manual > 0) {
1150
+ findings.push({
1151
+ rule: "APPCHECK_MANUAL",
1152
+ severity: "INFO",
1153
+ confidence: "PROBABLE",
1154
+ message: `Verifica\xE7\xE3o manual de AppCheck observada (${appcheck.manual} ocorr\xEAncia(s) de verifyToken). Cubra rota a rota no onRequest \u2014 o scan n\xE3o audita cada rota.`,
1155
+ fingerprint: "APPCHECK_MANUAL",
1156
+ evidence: [{ id: "ev-appcheck-manual", kind: "appcheck", summary: `${appcheck.manual} verifyToken`, confidence: "PROBABLE" }]
1157
+ });
1158
+ }
1159
+ if (appcheck.occurrences > 0) {
1133
1160
  findings.push({
1134
1161
  rule: "APPCHECK_ENFORCED",
1135
1162
  severity: "INFO",
1136
1163
  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.`,
1164
+ message: `enforceAppCheck observado (${appcheck.occurrences} ocorr\xEAncia(s) em ${appcheck.files} arquivo(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
1165
  fingerprint: "APPCHECK_ENFORCED",
1139
- evidence: [{ id: "ev-appcheck", kind: "appcheck", summary: `${enforced} enforced`, confidence: "CONFIRMED" }]
1166
+ evidence: [{ id: "ev-appcheck", kind: "appcheck", summary: `${appcheck.occurrences} ocorr\xEAncias`, confidence: "CONFIRMED" }]
1140
1167
  });
1141
- } else {
1142
- skipped.push("functions sem enforceAppCheck observado \u2014 AppCheck sem cobertura afirmativa");
1168
+ } else if (appcheck.manual === 0) {
1169
+ skipped.push("functions sem enforceAppCheck nem verifyToken observado \u2014 AppCheck sem cobertura afirmativa (onRequest exige verifica\xE7\xE3o manual por rota)");
1170
+ }
1171
+ if (appcheck.limitHit) {
1172
+ skipped.push("functions: teto de varredura atingido \u2014 AppCheck parcial neste scan");
1143
1173
  }
1144
1174
  } else {
1145
1175
  skipped.push("functions n\xE3o observadas \u2014 AppCheck fora deste scan");
1146
1176
  }
1177
+ skipped.push("isolamento por tenant (ownerId/tenantId) n\xE3o verificado \u2014 fora dos checks implementados, nunca verde silencioso");
1147
1178
  const modelCheck = ProjectModelSchema.safeParse(model);
1148
1179
  if (!modelCheck.success) {
1149
1180
  findings.push({
@@ -1205,7 +1236,10 @@ async function countAppCheckEnforced(rootDir) {
1205
1236
  const { join: joinP } = await import("path");
1206
1237
  const { stripRuleComments: strip } = await import("./rules-GTGZWN5U.js");
1207
1238
  const roots = [joinP(rootDir, "functions")];
1208
- let count = 0;
1239
+ let occurrences = 0;
1240
+ const files = /* @__PURE__ */ new Set();
1241
+ let manual = 0;
1242
+ let limitHit = false;
1209
1243
  const stack = [...roots];
1210
1244
  let guard = 0;
1211
1245
  const isTestFile = (p) => {
@@ -1214,6 +1248,7 @@ async function countAppCheckEnforced(rootDir) {
1214
1248
  };
1215
1249
  while (stack.length > 0 && guard < 200) {
1216
1250
  guard += 1;
1251
+ if (guard >= 200 && stack.length > 0) limitHit = true;
1217
1252
  const cur = stack.pop();
1218
1253
  let entries = [];
1219
1254
  try {
@@ -1240,22 +1275,72 @@ async function countAppCheckEnforced(rootDir) {
1240
1275
  try {
1241
1276
  const text = strip(readSync(abs, "utf-8"));
1242
1277
  const hits = text.match(/enforceAppCheck\s*:\s*true/g);
1243
- if (hits) count += hits.length;
1278
+ if (hits) {
1279
+ occurrences += hits.length;
1280
+ files.add(abs);
1281
+ }
1282
+ const manualHits = text.match(/verifyToken\s*\(/g);
1283
+ if (manualHits) manual += manualHits.length;
1244
1284
  } catch {
1245
1285
  }
1246
1286
  }
1247
1287
  }
1248
- return count;
1288
+ return { occurrences, files: files.size, manual, limitHit };
1249
1289
  } catch {
1250
- return 0;
1290
+ return { occurrences: 0, files: 0, manual: 0, limitHit: false };
1251
1291
  }
1252
1292
  }
1293
+ function isOpenStorageCondition(condition) {
1294
+ if (condition === void 0) return true;
1295
+ const norm = stripOuterParens(condition).toLowerCase().replace(/\s+/g, "");
1296
+ if (norm === "" || norm === "true") return true;
1297
+ if (/(^|\|\|)true($|\|\|)/.test(norm)) return true;
1298
+ if (norm.includes("request.auth==null") || norm.includes("request.auth===null")) return true;
1299
+ return false;
1300
+ }
1253
1301
  function parseStorageAllows(clean) {
1254
1302
  const out = [];
1303
+ const matches = [];
1304
+ const matchRe = /match\s+/g;
1305
+ let mm;
1306
+ while ((mm = matchRe.exec(clean)) !== null) {
1307
+ const lineStart = clean.lastIndexOf("\n", mm.index) + 1;
1308
+ const lineEnd = clean.indexOf("\n", mm.index);
1309
+ const line = clean.slice(lineStart, lineEnd === -1 ? void 0 : lineEnd);
1310
+ const brace = line.lastIndexOf("{");
1311
+ if (brace === -1) continue;
1312
+ const path = line.slice(mm.index - lineStart + 5, brace).trim() || "/(unknown)";
1313
+ const openBrace = lineStart + brace;
1314
+ let depth = 0;
1315
+ let quote = null;
1316
+ let end = clean.length;
1317
+ for (let i = openBrace; i < clean.length; i++) {
1318
+ const c = clean[i];
1319
+ if (quote) {
1320
+ if (c === quote && clean[i - 1] !== "\\") quote = null;
1321
+ continue;
1322
+ }
1323
+ if (c === '"' || c === "'" || c === "`") {
1324
+ quote = c;
1325
+ continue;
1326
+ }
1327
+ if (c === "{") depth += 1;
1328
+ else if (c === "}") {
1329
+ depth -= 1;
1330
+ if (depth === 0) {
1331
+ end = i + 1;
1332
+ break;
1333
+ }
1334
+ }
1335
+ }
1336
+ matches.push({ path, start: mm.index, end });
1337
+ }
1255
1338
  const re = /allow\s+([^;:]+?)(?::\s*if\s+([^;]+))?;/gi;
1256
1339
  let m;
1257
1340
  while ((m = re.exec(clean)) !== null) {
1258
- out.push({ target: m[1].trim(), condition: m[2]?.trim() });
1341
+ const containing = matches.filter((b) => b.start <= m.index && m.index < b.end).sort((a, b) => b.start - a.start);
1342
+ const line = clean.slice(0, m.index).split("\n").length;
1343
+ out.push({ target: m[1].trim(), condition: m[2]?.trim(), matchPath: containing[0]?.path ?? "/(unknown)", line });
1259
1344
  }
1260
1345
  return out;
1261
1346
  }
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  scan
4
- } from "./chunk-GDO2DLNQ.js";
4
+ } from "./chunk-UKEKA2NQ.js";
5
5
  import "./chunk-7EWTBIKJ.js";
6
6
 
7
7
  // src/cli.ts
@@ -33,7 +33,7 @@ Uso: firebase-audit scan [--json] [--strict] [--adapter=fn] [--cwd=path]`);
33
33
  process.exit(1);
34
34
  }
35
35
  const strict = args.includes("--strict") || cmd === "check";
36
- const asJson = args.includes("--json");
36
+ const asJson = args.includes("--json") || args.includes("--json=true");
37
37
  const getArg = (name) => {
38
38
  const pref = `--${name}=`;
39
39
  const hit = args.find((a) => a.startsWith(pref));
@@ -65,7 +65,7 @@ Uso: firebase-audit scan|check|verify|drift [--json] ...`);
65
65
  const { resolve: resolveRoot } = await import("path");
66
66
  const rootDir = cwdArg ? resolveRoot(process.cwd(), cwdArg) : process.cwd();
67
67
  if (cmd === "verify") {
68
- const { scan: scanForVerify } = await import("./scan-4Q4XVLJU.js");
68
+ const { scan: scanForVerify } = await import("./scan-OEN65GHM.js");
69
69
  const { verify } = await import("./verify-WBVSESHD.js");
70
70
  const { resolve: resolveVerify } = await import("path");
71
71
  const coverageArg = getArg("coverage");
@@ -149,7 +149,7 @@ FIREBASE AUDIT drift \u2014 MATCHED ${counts("MATCHED")}, LOCAL_ONLY ${counts("L
149
149
  console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
150
150
  if (findings.length === 0) {
151
151
  console.log(`
152
- \u2713 Nenhum problema nos checks implementados (${summary.passedChecks}/${summary.totalChecks} passaram).
152
+ \u2713 Nenhum problema nos checks FBA implementados (${summary.passedChecks}/${summary.totalChecks} passaram).
153
153
  Olhei: ${summary.checked.join(", ")}.
154
154
  Pulei: ${summary.skipped.join(" | ") || "nada"}.
155
155
  `);
@@ -164,7 +164,7 @@ ${icon} ${f.rule} [${f.confidence}]${where}
164
164
  if (f.fix) console.log(` \u2192 ${f.fix}`);
165
165
  }
166
166
  console.log(`
167
- SUMMARY: ${summary.errors} errors, ${summary.warnings} warnings, ${summary.infos} infos (${summary.passedChecks}/${summary.totalChecks} checks passaram)`);
167
+ SUMMARY: ${summary.errors} errors, ${summary.warnings} warnings, ${summary.infos} infos (${summary.passedChecks}/${summary.totalChecks} checks FBA passaram)`);
168
168
  console.log(`Olhei: ${summary.checked.join(", ")}`);
169
169
  console.log(`Pulei: ${summary.skipped.join(" | ") || "nada"}
170
170
  `);
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createMcpServer,
3
3
  startMcpServer
4
- } from "./chunk-QACXCMYB.js";
4
+ } from "./chunk-AXTYJMA7.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-GDO2DLNQ.js";
36
+ } from "./chunk-UKEKA2NQ.js";
37
37
  import {
38
38
  buildFullPath,
39
39
  conditionKeyForFingerprint,
package/dist/mcp-cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startMcpServer
4
- } from "./chunk-QACXCMYB.js";
5
- import "./chunk-GDO2DLNQ.js";
4
+ } from "./chunk-AXTYJMA7.js";
5
+ import "./chunk-UKEKA2NQ.js";
6
6
  import "./chunk-7EWTBIKJ.js";
7
7
  import "./chunk-Y4VXWSKX.js";
8
8
  import "./chunk-XZ32YQQC.js";
package/dist/mcp.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createMcpServer,
3
3
  startMcpServer
4
- } from "./chunk-QACXCMYB.js";
5
- import "./chunk-GDO2DLNQ.js";
4
+ } from "./chunk-AXTYJMA7.js";
5
+ import "./chunk-UKEKA2NQ.js";
6
6
  import "./chunk-7EWTBIKJ.js";
7
7
  import "./chunk-Y4VXWSKX.js";
8
8
  import "./chunk-XZ32YQQC.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  scan
3
- } from "./chunk-GDO2DLNQ.js";
3
+ } from "./chunk-UKEKA2NQ.js";
4
4
  import "./chunk-7EWTBIKJ.js";
5
5
  export {
6
6
  scan
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justmpm/firebase-audit",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
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",