@justmpm/firebase-audit 0.5.10 → 0.5.12

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.
@@ -1228,15 +1228,31 @@ 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);
1233
+ const { unknown: unknownStorageOps } = opsFrom(m.target);
1234
+ if (unknownStorageOps.length > 0) {
1235
+ findings.push({
1236
+ rule: "RULES_UNKNOWN_OP",
1237
+ severity: "WARNING",
1238
+ confidence: "CONFIRMED",
1239
+ message: `Opera\xE7\xE3o desconhecida no Storage em ${m.matchPath} (trecho desconhecido: ${unknownStorageOps.join(", ")}) (${relStorage}:${m.line}). Typo em allow? Deploy rejeita.`,
1240
+ fingerprint: `RULES_UNKNOWN_OP:${m.matchPath}:${m.line}:${normalizeTarget(m.target)}`,
1241
+ file: relStorage,
1242
+ line: m.line,
1243
+ evidence: [{ id: evId("unknown-op"), kind: "unknown-op", summary: `${m.matchPath} allow ${m.target}`, confidence: "CONFIRMED", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
1244
+ fix: "Corrija para read/get/list/write/create/update/delete."
1245
+ });
1246
+ }
1236
1247
  const singleStorage = m.condition === void 0 ? void 0 : resolveHelperConditionDetailed(m.condition, storageHelpers);
1237
1248
  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, "");
1249
+ const singleResolved = singleStorage !== void 0 && singleStorage !== m.condition ? singleStorage : void 0;
1250
+ const effectiveCond = m.condition === void 0 ? void 0 : singleResolved !== void 0 ? helperBodyToCondition(singleResolved) : inlinedStorage !== void 0 && inlinedStorage !== m.condition ? inlinedStorage : m.condition;
1251
+ const normEffFalse = effectiveCond === void 0 ? "" : stripOuterParens(effectiveCond).toLowerCase().replace(/\s+/g, "");
1252
+ if (normEffFalse === "false") {
1253
+ continue;
1254
+ }
1255
+ const normCond = `${singleResolved ?? ""} ${effectiveCond ?? ""}`.toLowerCase().replace(/\s+/g, "");
1240
1256
  const hasAuth = normCond.includes("request.auth");
1241
1257
  const bareAuth = (/request\.auth(!==|!=)null/.test(normCond) || /request\.auth\.uid(!==|!=)null/.test(normCond)) && !normCond.includes("request.auth.token") && !normCond.includes("sign_in_provider");
1242
1258
  const wildcard = /\{\s*\w+\s*=\s*\*\*\s*\}/.test(m.matchPath);
@@ -1286,7 +1302,8 @@ async function scan(rootDir, opts = {}) {
1286
1302
  continue;
1287
1303
  }
1288
1304
  }
1289
- if (bareAuth && (ops.has("read") || ops.has("get") || ops.has("list") || ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete"))) {
1305
+ const noAuthBranch = findNoAuthBranch(effectiveCond);
1306
+ 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
1307
  findings.push({
1291
1308
  rule: "STORAGE_ANON_ALLOWED",
1292
1309
  severity: "INFO",
@@ -1300,8 +1317,10 @@ async function scan(rootDir, opts = {}) {
1300
1317
  });
1301
1318
  continue;
1302
1319
  }
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;
1320
+ if ((!hasAuth || noAuthBranch !== void 0) && effectiveCond !== void 0) {
1321
+ const branchScope = noAuthBranch ?? (effectiveCond === m.condition ? m.condition ?? "" : "");
1322
+ const unresolvedHelper = /^[\s(]*([A-Za-z_][A-Za-z0-9_]*)\s*\(/.exec(branchScope)?.[1];
1323
+ const branchNote = noAuthBranch !== void 0 ? ` Ramo sem request.auth ("${noAuthBranch}"): an\xF4nimo passa por ele.` : "";
1305
1324
  const noAuthNote = unresolvedHelper ? ` Helper "${unresolvedHelper}" n\xE3o resolvido no storage.rules \u2014 confirme o corpo no Emulator.` : "";
1306
1325
  const canWrite = ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete");
1307
1326
  const canRead = ops.has("read") || ops.has("get") || ops.has("list");
@@ -1310,7 +1329,7 @@ async function scan(rootDir, opts = {}) {
1310
1329
  rule: "STORAGE_PUBLIC_WRITE",
1311
1330
  severity: "ERROR",
1312
1331
  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}`,
1332
+ 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
1333
  fingerprint: `STORAGE_PUBLIC_WRITE:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
1315
1334
  file: relStorage,
1316
1335
  line: m.line,
@@ -1322,7 +1341,7 @@ async function scan(rootDir, opts = {}) {
1322
1341
  rule: "STORAGE_PUBLIC_READ",
1323
1342
  severity: "INFO",
1324
1343
  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}`,
1344
+ 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
1345
  fingerprint: `STORAGE_PUBLIC_READ:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
1327
1346
  file: relStorage,
1328
1347
  line: m.line,
@@ -1522,6 +1541,16 @@ function isOpenStorageCondition(condition) {
1522
1541
  if (norm.includes("request.auth==null") || norm.includes("request.auth===null")) return true;
1523
1542
  return false;
1524
1543
  }
1544
+ function findNoAuthBranch(effective) {
1545
+ if (effective === void 0 || !effective.includes("||")) return void 0;
1546
+ for (const part of effective.split("||")) {
1547
+ const branch = stripOuterParens(part.replace(/return/g, "").replace(/;/g, "").trim());
1548
+ const norm = branch.toLowerCase().replace(/\s+/g, "");
1549
+ if (norm === "" || norm === "false") continue;
1550
+ if (!norm.includes("request.auth")) return part.trim().slice(0, 80);
1551
+ }
1552
+ return void 0;
1553
+ }
1525
1554
  function parseStorageAllows(clean) {
1526
1555
  const out = [];
1527
1556
  const matches = [];
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  scan
3
- } from "./chunk-GR2SM3OL.js";
3
+ } from "./chunk-CTQPAMOE.js";
4
4
  import {
5
5
  verify
6
6
  } from "./chunk-CSMT4PIP.js";
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-CTQPAMOE.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-CU54MUN7.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-OYWTDV45.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-CTQPAMOE.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-OYWTDV45.js";
5
+ import "./chunk-CTQPAMOE.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-OYWTDV45.js";
5
+ import "./chunk-CTQPAMOE.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-CTQPAMOE.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.12",
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