@justmpm/firebase-audit 0.5.9 → 0.5.11

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-B4ATI4OJ.js";
3
+ } from "./chunk-ZNVQGZMJ.js";
4
4
  import {
5
5
  verify
6
6
  } from "./chunk-CSMT4PIP.js";
@@ -2,6 +2,7 @@ import {
2
2
  conditionKeyForFingerprint,
3
3
  extractRules,
4
4
  findPublicAllowsContent,
5
+ helperBodyToCondition,
5
6
  inlineHelpersInCondition,
6
7
  normalizeTarget,
7
8
  opsFrom,
@@ -1220,23 +1221,29 @@ async function scan(rootDir, opts = {}) {
1220
1221
  const raw = readFileSync3(d.storageRulesFile, "utf-8");
1221
1222
  const relStorage = model.firebase.storage?.rulesFile ?? "storage.rules";
1222
1223
  const clean = stripRuleComments(raw);
1224
+ const storageHelpers = parseRuleFunctionsDetailed(clean);
1223
1225
  let evCounter = 0;
1224
1226
  const evId = (prefix) => {
1225
1227
  evCounter += 1;
1226
1228
  return `ev-storage-${prefix}-${evCounter}`;
1227
1229
  };
1228
1230
  for (const m of parseStorageAllows(clean)) {
1229
- if (m.condition !== void 0 && stripOuterParens(m.condition).toLowerCase().replace(/\s+/g, "") === "false") {
1230
- continue;
1231
- }
1232
1231
  const ops = expandStorageOps(m.target);
1233
1232
  const key = conditionKeyForFingerprint(m.condition);
1234
- const normCond = (m.condition ?? "").toLowerCase().replace(/\s+/g, "");
1233
+ const singleStorage = m.condition === void 0 ? void 0 : resolveHelperConditionDetailed(m.condition, storageHelpers);
1234
+ const inlinedStorage = m.condition === void 0 ? void 0 : inlineHelpersInCondition(m.condition, storageHelpers);
1235
+ const singleResolved = singleStorage !== void 0 && singleStorage !== m.condition ? singleStorage : void 0;
1236
+ const effectiveCond = m.condition === void 0 ? void 0 : singleResolved !== void 0 ? helperBodyToCondition(singleResolved) : inlinedStorage !== void 0 && inlinedStorage !== m.condition ? inlinedStorage : m.condition;
1237
+ const normEffFalse = effectiveCond === void 0 ? "" : stripOuterParens(effectiveCond).toLowerCase().replace(/\s+/g, "");
1238
+ if (normEffFalse === "false") {
1239
+ continue;
1240
+ }
1241
+ const normCond = `${singleResolved ?? ""} ${effectiveCond ?? ""}`.toLowerCase().replace(/\s+/g, "");
1235
1242
  const hasAuth = normCond.includes("request.auth");
1236
1243
  const bareAuth = (/request\.auth(!==|!=)null/.test(normCond) || /request\.auth\.uid(!==|!=)null/.test(normCond)) && !normCond.includes("request.auth.token") && !normCond.includes("sign_in_provider");
1237
1244
  const wildcard = /\{\s*\w+\s*=\s*\*\*\s*\}/.test(m.matchPath);
1238
1245
  const where = `${relStorage}:${m.line} (${m.matchPath}, allow ${m.target})`;
1239
- if (isOpenStorageCondition(m.condition)) {
1246
+ if (isOpenStorageCondition(effectiveCond)) {
1240
1247
  if (ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete")) {
1241
1248
  findings.push({
1242
1249
  rule: "STORAGE_PUBLIC_WRITE",
@@ -1281,7 +1288,8 @@ async function scan(rootDir, opts = {}) {
1281
1288
  continue;
1282
1289
  }
1283
1290
  }
1284
- if (bareAuth && (ops.has("read") || ops.has("get") || ops.has("list") || ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete"))) {
1291
+ const noAuthBranch = findNoAuthBranch(effectiveCond);
1292
+ if (noAuthBranch === void 0 && bareAuth && (ops.has("read") || ops.has("get") || ops.has("list") || ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete"))) {
1285
1293
  findings.push({
1286
1294
  rule: "STORAGE_ANON_ALLOWED",
1287
1295
  severity: "INFO",
@@ -1295,7 +1303,11 @@ async function scan(rootDir, opts = {}) {
1295
1303
  });
1296
1304
  continue;
1297
1305
  }
1298
- if (!hasAuth && m.condition !== void 0) {
1306
+ if ((!hasAuth || noAuthBranch !== void 0) && effectiveCond !== void 0) {
1307
+ const branchScope = noAuthBranch ?? (effectiveCond === m.condition ? m.condition ?? "" : "");
1308
+ const unresolvedHelper = /^[\s(]*([A-Za-z_][A-Za-z0-9_]*)\s*\(/.exec(branchScope)?.[1];
1309
+ const branchNote = noAuthBranch !== void 0 ? ` Ramo sem request.auth ("${noAuthBranch}"): an\xF4nimo passa por ele.` : "";
1310
+ const noAuthNote = unresolvedHelper ? ` Helper "${unresolvedHelper}" n\xE3o resolvido no storage.rules \u2014 confirme o corpo no Emulator.` : "";
1299
1311
  const canWrite = ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete");
1300
1312
  const canRead = ops.has("read") || ops.has("get") || ops.has("list");
1301
1313
  if (canWrite) {
@@ -1303,7 +1315,7 @@ async function scan(rootDir, opts = {}) {
1303
1315
  rule: "STORAGE_PUBLIC_WRITE",
1304
1316
  severity: "ERROR",
1305
1317
  confidence: "PROBABLE",
1306
- message: `Storage com escrita condicionada sem request.auth (${where}). Se a condi\xE7\xE3o for verdadeira para an\xF4nimo, ele escreve \u2014 cofre prov\xE1vel.`,
1318
+ message: `Storage com escrita condicionada sem request.auth (${where}). Se a condi\xE7\xE3o for verdadeira para an\xF4nimo, ele escreve \u2014 cofre prov\xE1vel.${branchNote}${noAuthNote}`,
1307
1319
  fingerprint: `STORAGE_PUBLIC_WRITE:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
1308
1320
  file: relStorage,
1309
1321
  line: m.line,
@@ -1315,7 +1327,7 @@ async function scan(rootDir, opts = {}) {
1315
1327
  rule: "STORAGE_PUBLIC_READ",
1316
1328
  severity: "INFO",
1317
1329
  confidence: "PROBABLE",
1318
- message: `Storage com leitura condicionada sem request.auth (${where}). Se a condi\xE7\xE3o for verdadeira para an\xF4nimo, ele l\xEA \u2014 confirme que \xE9 vitrine.`,
1330
+ message: `Storage com leitura condicionada sem request.auth (${where}). Se a condi\xE7\xE3o for verdadeira para an\xF4nimo, ele l\xEA \u2014 confirme que \xE9 vitrine.${branchNote}${noAuthNote}`,
1319
1331
  fingerprint: `STORAGE_PUBLIC_READ:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
1320
1332
  file: relStorage,
1321
1333
  line: m.line,
@@ -1506,12 +1518,25 @@ function isOpenStorageCondition(condition) {
1506
1518
  if (condition === void 0) return true;
1507
1519
  const norm = stripOuterParens(condition).toLowerCase().replace(/\s+/g, "");
1508
1520
  if (norm === "" || norm === "true") return true;
1521
+ for (const part of norm.split("||")) {
1522
+ const branch = stripOuterParens(part.replace(/return/g, "").replace(/;/g, "").trim());
1523
+ if (branch === "true") return true;
1524
+ }
1509
1525
  if (/^!\(.*request\.auth(===|==)null/.test(norm)) return false;
1510
1526
  if (/^!\(.*request\.auth(!==|!=)null/.test(norm)) return true;
1511
- if (/(^|\|\|)true($|\|\|)/.test(norm)) return true;
1512
1527
  if (norm.includes("request.auth==null") || norm.includes("request.auth===null")) return true;
1513
1528
  return false;
1514
1529
  }
1530
+ function findNoAuthBranch(effective) {
1531
+ if (effective === void 0 || !effective.includes("||")) return void 0;
1532
+ for (const part of effective.split("||")) {
1533
+ const branch = stripOuterParens(part.replace(/return/g, "").replace(/;/g, "").trim());
1534
+ const norm = branch.toLowerCase().replace(/\s+/g, "");
1535
+ if (norm === "" || norm === "false") continue;
1536
+ if (!norm.includes("request.auth")) return part.trim().slice(0, 80);
1537
+ }
1538
+ return void 0;
1539
+ }
1515
1540
  function parseStorageAllows(clean) {
1516
1541
  const out = [];
1517
1542
  const matches = [];
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  scan
4
- } from "./chunk-B4ATI4OJ.js";
4
+ } from "./chunk-ZNVQGZMJ.js";
5
5
  import "./chunk-7J3ADEBW.js";
6
6
 
7
7
  // src/cli.ts
@@ -84,7 +84,7 @@ Uso: firebase-audit scan|check|verify|drift [--json] ...`);
84
84
  const { resolve: resolveRoot } = await import("path");
85
85
  const rootDir = cwdArg ? resolveRoot(process.cwd(), cwdArg) : process.cwd();
86
86
  if (cmd === "verify") {
87
- const { scan: scanForVerify } = await import("./scan-CG54EVVP.js");
87
+ const { scan: scanForVerify } = await import("./scan-M6OYRIKL.js");
88
88
  const { verify } = await import("./verify-GQSR4FQL.js");
89
89
  const { resolve: resolveVerify } = await import("path");
90
90
  const coverageArg = getArg("coverage");
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createMcpServer,
3
3
  startMcpServer
4
- } from "./chunk-4ST23J6W.js";
4
+ } from "./chunk-ZE5K5HRR.js";
5
5
  import {
6
6
  AccessOriginSchema,
7
7
  AuditYamlSchema,
@@ -34,7 +34,7 @@ import {
34
34
  runChecks,
35
35
  scan,
36
36
  validateClaimSamples
37
- } from "./chunk-B4ATI4OJ.js";
37
+ } from "./chunk-ZNVQGZMJ.js";
38
38
  import {
39
39
  buildFullPath,
40
40
  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-4ST23J6W.js";
5
- import "./chunk-B4ATI4OJ.js";
4
+ } from "./chunk-ZE5K5HRR.js";
5
+ import "./chunk-ZNVQGZMJ.js";
6
6
  import "./chunk-7J3ADEBW.js";
7
7
  import "./chunk-CSMT4PIP.js";
8
8
  import "./chunk-LD3F73XI.js";
package/dist/mcp.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createMcpServer,
3
3
  startMcpServer
4
- } from "./chunk-4ST23J6W.js";
5
- import "./chunk-B4ATI4OJ.js";
4
+ } from "./chunk-ZE5K5HRR.js";
5
+ import "./chunk-ZNVQGZMJ.js";
6
6
  import "./chunk-7J3ADEBW.js";
7
7
  import "./chunk-CSMT4PIP.js";
8
8
  import "./chunk-LD3F73XI.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  scan
3
- } from "./chunk-B4ATI4OJ.js";
3
+ } from "./chunk-ZNVQGZMJ.js";
4
4
  import "./chunk-7J3ADEBW.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.9",
3
+ "version": "0.5.11",
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
@@ -20,6 +20,8 @@ Skill para agentes usarem o `@justmpm/firebase-audit` do jeito certo.
20
20
  - Claims: 1000 bytes max, sem chaves OIDC reservadas, só controle de acesso
21
21
  - AppCheck é carimbo do app oficial — não prova usuário nem tenant
22
22
  - Logo pública (`get: if true` em `/logos/`) pode ser vitrine proposital — cofre é `write: if true` ou curinga total aberto
23
+ - Storage resolve funções auxiliares do `.rules` antes de acusar (portaria com auth dentro não é "sem auth", inclui `let` do rules v2); helper não declarado vira PROBABLE com o nome na mensagem, nunca silêncio
24
+ - Storage avalia disjunção por ramo (`auth || tamanho` acusa o ramo sem auth) e `return false` via helper é deny silencioso, igual ao `if false`
23
25
  - Seeds/fixtures com Admin SDK moram em `scripts/` ou `server/`, nunca em `src/` (senão o scan acusa FBA003 de propósito)
24
26
 
25
27
  ## Emulador