@justmpm/firebase-audit 0.5.10 → 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-GR2SM3OL.js";
3
+ } from "./chunk-ZNVQGZMJ.js";
4
4
  import {
5
5
  verify
6
6
  } from "./chunk-CSMT4PIP.js";
@@ -1228,15 +1228,17 @@ async function scan(rootDir, opts = {}) {
1228
1228
  return `ev-storage-${prefix}-${evCounter}`;
1229
1229
  };
1230
1230
  for (const m of parseStorageAllows(clean)) {
1231
- if (m.condition !== void 0 && stripOuterParens(m.condition).toLowerCase().replace(/\s+/g, "") === "false") {
1232
- continue;
1233
- }
1234
1231
  const ops = expandStorageOps(m.target);
1235
1232
  const key = conditionKeyForFingerprint(m.condition);
1236
1233
  const singleStorage = m.condition === void 0 ? void 0 : resolveHelperConditionDetailed(m.condition, storageHelpers);
1237
1234
  const inlinedStorage = m.condition === void 0 ? void 0 : inlineHelpersInCondition(m.condition, storageHelpers);
1238
- const effectiveCond = m.condition === void 0 ? void 0 : singleStorage !== void 0 && singleStorage !== m.condition ? helperBodyToCondition(singleStorage) : inlinedStorage !== void 0 && inlinedStorage !== m.condition ? inlinedStorage : m.condition;
1239
- const normCond = (effectiveCond ?? "").toLowerCase().replace(/\s+/g, "");
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, "");
1240
1242
  const hasAuth = normCond.includes("request.auth");
1241
1243
  const bareAuth = (/request\.auth(!==|!=)null/.test(normCond) || /request\.auth\.uid(!==|!=)null/.test(normCond)) && !normCond.includes("request.auth.token") && !normCond.includes("sign_in_provider");
1242
1244
  const wildcard = /\{\s*\w+\s*=\s*\*\*\s*\}/.test(m.matchPath);
@@ -1286,7 +1288,8 @@ async function scan(rootDir, opts = {}) {
1286
1288
  continue;
1287
1289
  }
1288
1290
  }
1289
- 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"))) {
1290
1293
  findings.push({
1291
1294
  rule: "STORAGE_ANON_ALLOWED",
1292
1295
  severity: "INFO",
@@ -1300,8 +1303,10 @@ async function scan(rootDir, opts = {}) {
1300
1303
  });
1301
1304
  continue;
1302
1305
  }
1303
- if (!hasAuth && effectiveCond !== void 0) {
1304
- const unresolvedHelper = effectiveCond === m.condition ? /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*\(/.exec(m.condition ?? "")?.[1] : 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.` : "";
1305
1310
  const noAuthNote = unresolvedHelper ? ` Helper "${unresolvedHelper}" n\xE3o resolvido no storage.rules \u2014 confirme o corpo no Emulator.` : "";
1306
1311
  const canWrite = ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete");
1307
1312
  const canRead = ops.has("read") || ops.has("get") || ops.has("list");
@@ -1310,7 +1315,7 @@ async function scan(rootDir, opts = {}) {
1310
1315
  rule: "STORAGE_PUBLIC_WRITE",
1311
1316
  severity: "ERROR",
1312
1317
  confidence: "PROBABLE",
1313
- 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.${noAuthNote}`,
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}`,
1314
1319
  fingerprint: `STORAGE_PUBLIC_WRITE:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
1315
1320
  file: relStorage,
1316
1321
  line: m.line,
@@ -1322,7 +1327,7 @@ async function scan(rootDir, opts = {}) {
1322
1327
  rule: "STORAGE_PUBLIC_READ",
1323
1328
  severity: "INFO",
1324
1329
  confidence: "PROBABLE",
1325
- 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.${noAuthNote}`,
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}`,
1326
1331
  fingerprint: `STORAGE_PUBLIC_READ:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
1327
1332
  file: relStorage,
1328
1333
  line: m.line,
@@ -1522,6 +1527,16 @@ function isOpenStorageCondition(condition) {
1522
1527
  if (norm.includes("request.auth==null") || norm.includes("request.auth===null")) return true;
1523
1528
  return false;
1524
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
+ }
1525
1540
  function parseStorageAllows(clean) {
1526
1541
  const out = [];
1527
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-GR2SM3OL.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-YY5UY35J.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-JPHERTX5.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-GR2SM3OL.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-JPHERTX5.js";
5
- import "./chunk-GR2SM3OL.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-JPHERTX5.js";
5
- import "./chunk-GR2SM3OL.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-GR2SM3OL.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.10",
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,7 +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"); helper não declarado vira PROBABLE com o nome na mensagem, nunca silêncio
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`
24
25
  - Seeds/fixtures com Admin SDK moram em `scripts/` ou `server/`, nunca em `src/` (senão o scan acusa FBA003 de propósito)
25
26
 
26
27
  ## Emulador