@justmpm/firebase-audit 0.5.12 → 0.5.13

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-CTQPAMOE.js";
3
+ } from "./chunk-NOIROFKB.js";
4
4
  import {
5
5
  verify
6
6
  } from "./chunk-CSMT4PIP.js";
@@ -518,12 +518,13 @@ async function runChecks(model, rootDir, opts = {}) {
518
518
  const hasWeakConjunction = (norm.includes("request.auth!=null") || norm.includes("request.auth!==null") || norm.includes("request.auth.uid!=null") || norm.includes("request.auth.uid!==null")) && !norm.includes("sign_in_provider") && !norm.includes("email_verified") && !norm.includes("request.auth.token");
519
519
  const isBareAuth = hasBareAuth || hasWeakConjunction;
520
520
  if (isBareAuth && rule.claimReferences.length === 0) {
521
+ const gatedByDoc = /exists\s*\(/i.test(effectiveRaw);
521
522
  findings.push(
522
523
  make(
523
524
  "AUTH_ANON_ALLOWED",
524
525
  "INFO",
525
526
  "PROBABLE",
526
- `Rule ${rule.path} aceita qualquer autenticado incluindo an\xF4nimo (request.auth != null sem claim/provider).`,
527
+ gatedByDoc ? `Rule ${rule.path} exige login + documento via exists() (sem claim/provider) \u2014 an\xF4nimo com login passa no conjunto se obtiver o doc.` : `Rule ${rule.path} aceita qualquer autenticado incluindo an\xF4nimo (request.auth != null sem claim/provider).`,
527
528
  `AUTH_ANON_ALLOWED:${rule.path}:${conditionKeyForFingerprint(rule.condition)}`,
528
529
  {
529
530
  file: rule.location.file,
@@ -1264,7 +1265,7 @@ async function scan(rootDir, opts = {}) {
1264
1265
  severity: "ERROR",
1265
1266
  confidence: "CONFIRMED",
1266
1267
  message: `Storage com escrita aberta (${where}). Qualquer cliente pode escrever \u2014 cofre aberto, n\xE3o vitrine.`,
1267
- fingerprint: `STORAGE_PUBLIC_WRITE:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
1268
+ fingerprint: `STORAGE_PUBLIC_WRITE:${m.fpPath}:${normalizeTarget(m.target)}:${key}`,
1268
1269
  file: relStorage,
1269
1270
  line: m.line,
1270
1271
  evidence: [{ id: evId("write"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "CONFIRMED", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
@@ -1279,7 +1280,7 @@ async function scan(rootDir, opts = {}) {
1279
1280
  severity: "WARNING",
1280
1281
  confidence: "PROBABLE",
1281
1282
  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}.`,
1282
- fingerprint: `STORAGE_ALL_PATHS_OPEN:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
1283
+ fingerprint: `STORAGE_ALL_PATHS_OPEN:${m.fpPath}:${normalizeTarget(m.target)}:${key}`,
1283
1284
  file: relStorage,
1284
1285
  line: m.line,
1285
1286
  evidence: [{ id: evId("all"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
@@ -1292,7 +1293,7 @@ async function scan(rootDir, opts = {}) {
1292
1293
  severity: "INFO",
1293
1294
  confidence: "PROBABLE",
1294
1295
  message: enumerates ? `Storage com leitura aberta (${where}). Pode ser vitrine proposital \u2014 mas allow read/list permite enumerar a pasta; para vitrine segura prefira allow get. Confirme que o write segue restrito.` : `Storage com leitura aberta (${where}). Pode ser vitrine proposital (logos) \u2014 confirme que o write segue restrito.`,
1295
- fingerprint: `STORAGE_PUBLIC_READ:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
1296
+ fingerprint: `STORAGE_PUBLIC_READ:${m.fpPath}:${normalizeTarget(m.target)}:${key}`,
1296
1297
  file: relStorage,
1297
1298
  line: m.line,
1298
1299
  evidence: [{ id: evId("read"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
@@ -1304,12 +1305,13 @@ async function scan(rootDir, opts = {}) {
1304
1305
  }
1305
1306
  const noAuthBranch = findNoAuthBranch(effectiveCond);
1306
1307
  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"))) {
1308
+ const gatedByDoc = /exists\(/.test(normCond);
1307
1309
  findings.push({
1308
1310
  rule: "STORAGE_ANON_ALLOWED",
1309
1311
  severity: "INFO",
1310
1312
  confidence: "PROBABLE",
1311
- message: `Storage aceita qualquer autenticado incluindo an\xF4nimo (${where}). Exija claim/provider para dados sens\xEDveis.`,
1312
- fingerprint: `STORAGE_ANON_ALLOWED:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
1313
+ message: gatedByDoc ? `Storage exige login + documento via exists() (${where}) \u2014 mais forte que login puro, mas sem claim/provider: an\xF4nimo com login passa no conjunto se obtiver o doc. Exija claim/provider para dados sens\xEDveis.` : `Storage aceita qualquer autenticado incluindo an\xF4nimo (${where}). Exija claim/provider para dados sens\xEDveis.`,
1314
+ fingerprint: `STORAGE_ANON_ALLOWED:${m.fpPath}:${normalizeTarget(m.target)}:${key}`,
1313
1315
  file: relStorage,
1314
1316
  line: m.line,
1315
1317
  evidence: [{ id: evId("anon"), kind: "anon-allowed", summary: `${m.matchPath} allow ${m.target}`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
@@ -1330,7 +1332,7 @@ async function scan(rootDir, opts = {}) {
1330
1332
  severity: "ERROR",
1331
1333
  confidence: "PROBABLE",
1332
1334
  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}`,
1333
- fingerprint: `STORAGE_PUBLIC_WRITE:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
1335
+ fingerprint: `STORAGE_PUBLIC_WRITE:${m.fpPath}:${normalizeTarget(m.target)}:${key}`,
1334
1336
  file: relStorage,
1335
1337
  line: m.line,
1336
1338
  evidence: [{ id: evId("write"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target} sem auth`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
@@ -1342,7 +1344,7 @@ async function scan(rootDir, opts = {}) {
1342
1344
  severity: "INFO",
1343
1345
  confidence: "PROBABLE",
1344
1346
  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}`,
1345
- fingerprint: `STORAGE_PUBLIC_READ:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
1347
+ fingerprint: `STORAGE_PUBLIC_READ:${m.fpPath}:${normalizeTarget(m.target)}:${key}`,
1346
1348
  file: relStorage,
1347
1349
  line: m.line,
1348
1350
  evidence: [{ id: evId("read"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target} sem auth`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }]
@@ -1552,21 +1554,22 @@ function findNoAuthBranch(effective) {
1552
1554
  return void 0;
1553
1555
  }
1554
1556
  function parseStorageAllows(clean) {
1557
+ const text = clean.replace(/\r\n?/g, "\n");
1555
1558
  const out = [];
1556
1559
  const matches = [];
1557
1560
  const matchRe = /match\s+/g;
1558
1561
  let mm;
1559
- while ((mm = matchRe.exec(clean)) !== null) {
1562
+ while ((mm = matchRe.exec(text)) !== null) {
1560
1563
  const from = mm.index + mm[0].length;
1561
1564
  let openBrace = -1;
1562
1565
  let scan2 = from;
1563
- while (scan2 < clean.length) {
1564
- const b = clean.indexOf("{", scan2);
1566
+ while (scan2 < text.length) {
1567
+ const b = text.indexOf("{", scan2);
1565
1568
  if (b === -1) break;
1566
- const between = clean.slice(from, b);
1569
+ const between = text.slice(from, b);
1567
1570
  if (/match\s|allow\s|;/.test(between)) break;
1568
- const after = clean.slice(b + 1, b + 12).trimStart().replace(/^\n+/, "");
1569
- const eol = clean.slice(b + 1).match(/^[ \t]*(\n|$)/) !== null;
1571
+ const after = text.slice(b + 1, b + 12).trimStart().replace(/^\n+/, "");
1572
+ const eol = text.slice(b + 1).match(/^[ \t]*(\n|$)/) !== null;
1570
1573
  if (/^(allow|match|function|\})/.test(after) || eol) {
1571
1574
  openBrace = b;
1572
1575
  break;
@@ -1574,14 +1577,14 @@ function parseStorageAllows(clean) {
1574
1577
  scan2 = b + 1;
1575
1578
  }
1576
1579
  if (openBrace === -1) continue;
1577
- const path = clean.slice(from, openBrace).trim() || "/(unknown)";
1580
+ const path = text.slice(from, openBrace).trim() || "/(unknown)";
1578
1581
  let depth = 0;
1579
1582
  let quote = null;
1580
- let end = clean.length;
1581
- for (let i = openBrace; i < clean.length; i++) {
1582
- const c = clean[i];
1583
+ let end = text.length;
1584
+ for (let i = openBrace; i < text.length; i++) {
1585
+ const c = text[i];
1583
1586
  if (quote) {
1584
- if (c === quote && clean[i - 1] !== "\\") quote = null;
1587
+ if (c === quote && text[i - 1] !== "\\") quote = null;
1585
1588
  continue;
1586
1589
  }
1587
1590
  if (c === '"' || c === "'" || c === "`") {
@@ -1601,15 +1604,17 @@ function parseStorageAllows(clean) {
1601
1604
  }
1602
1605
  const re = /allow\s+([^;:]+?)(?::\s*if\s+([^;]+))?;/gi;
1603
1606
  let m;
1604
- while ((m = re.exec(clean)) !== null) {
1607
+ while ((m = re.exec(text)) !== null) {
1605
1608
  const containing = matches.filter((b) => b.start <= m.index && m.index < b.end).sort((a, b) => a.start - b.start);
1606
1609
  const parts = [];
1607
1610
  for (const b of containing) {
1608
1611
  const p = b.path.trim().replace(/^\/+|\/+$/g, "");
1609
1612
  if (p.length > 0) parts.push(p);
1610
1613
  }
1611
- const line = clean.slice(0, m.index).split("\n").length;
1612
- out.push({ target: m[1].trim(), condition: m[2]?.trim(), matchPath: parts.length > 0 ? `/${parts.join("/")}` : "/(unknown)", line });
1614
+ const line = text.slice(0, m.index).split("\n").length;
1615
+ const matchPath = parts.length > 0 ? `/${parts.join("/")}` : "/(unknown)";
1616
+ const fpPath = matchPath === "/(unknown)" ? `/(unknown):L${line}` : matchPath;
1617
+ out.push({ target: m[1].trim(), condition: m[2]?.trim(), matchPath, fpPath, line });
1613
1618
  }
1614
1619
  return out;
1615
1620
  }
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  scan
4
- } from "./chunk-CTQPAMOE.js";
4
+ } from "./chunk-NOIROFKB.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-CU54MUN7.js");
87
+ const { scan: scanForVerify } = await import("./scan-2TMTMC7C.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-OYWTDV45.js";
4
+ } from "./chunk-LK5MKSCZ.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-CTQPAMOE.js";
37
+ } from "./chunk-NOIROFKB.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-OYWTDV45.js";
5
- import "./chunk-CTQPAMOE.js";
4
+ } from "./chunk-LK5MKSCZ.js";
5
+ import "./chunk-NOIROFKB.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-OYWTDV45.js";
5
- import "./chunk-CTQPAMOE.js";
4
+ } from "./chunk-LK5MKSCZ.js";
5
+ import "./chunk-NOIROFKB.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-CTQPAMOE.js";
3
+ } from "./chunk-NOIROFKB.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.12",
3
+ "version": "0.5.13",
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
@@ -21,6 +21,8 @@ Skill para agentes usarem o `@justmpm/firebase-audit` do jeito certo.
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
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 tolera CRLF (arquivo Windows) e path `/(unknown)` nunca repete fingerprint (sufixo de linha só no degenerado)
25
+ - Regra com `auth != null && exists(admins/uid)` diz "login + documento via exists()", não "qualquer autenticado" — sem claim/provider, anônimo com login passa no conjunto se obtiver o doc
24
26
  - Storage avalia disjunção por ramo (`auth || tamanho` acusa o ramo sem auth) e `return false` via helper é deny silencioso, igual ao `if false`
25
27
  - Seeds/fixtures com Admin SDK moram em `scripts/` ou `server/`, nunca em `src/` (senão o scan acusa FBA003 de propósito)
26
28