@justmpm/firebase-audit 0.5.11 → 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-ZNVQGZMJ.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,
@@ -1230,6 +1231,20 @@ async function scan(rootDir, opts = {}) {
1230
1231
  for (const m of parseStorageAllows(clean)) {
1231
1232
  const ops = expandStorageOps(m.target);
1232
1233
  const key = conditionKeyForFingerprint(m.condition);
1234
+ const { unknown: unknownStorageOps } = opsFrom(m.target);
1235
+ if (unknownStorageOps.length > 0) {
1236
+ findings.push({
1237
+ rule: "RULES_UNKNOWN_OP",
1238
+ severity: "WARNING",
1239
+ confidence: "CONFIRMED",
1240
+ message: `Opera\xE7\xE3o desconhecida no Storage em ${m.matchPath} (trecho desconhecido: ${unknownStorageOps.join(", ")}) (${relStorage}:${m.line}). Typo em allow? Deploy rejeita.`,
1241
+ fingerprint: `RULES_UNKNOWN_OP:${m.matchPath}:${m.line}:${normalizeTarget(m.target)}`,
1242
+ file: relStorage,
1243
+ line: m.line,
1244
+ 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 } } }],
1245
+ fix: "Corrija para read/get/list/write/create/update/delete."
1246
+ });
1247
+ }
1233
1248
  const singleStorage = m.condition === void 0 ? void 0 : resolveHelperConditionDetailed(m.condition, storageHelpers);
1234
1249
  const inlinedStorage = m.condition === void 0 ? void 0 : inlineHelpersInCondition(m.condition, storageHelpers);
1235
1250
  const singleResolved = singleStorage !== void 0 && singleStorage !== m.condition ? singleStorage : void 0;
@@ -1250,7 +1265,7 @@ async function scan(rootDir, opts = {}) {
1250
1265
  severity: "ERROR",
1251
1266
  confidence: "CONFIRMED",
1252
1267
  message: `Storage com escrita aberta (${where}). Qualquer cliente pode escrever \u2014 cofre aberto, n\xE3o vitrine.`,
1253
- fingerprint: `STORAGE_PUBLIC_WRITE:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
1268
+ fingerprint: `STORAGE_PUBLIC_WRITE:${m.fpPath}:${normalizeTarget(m.target)}:${key}`,
1254
1269
  file: relStorage,
1255
1270
  line: m.line,
1256
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 } } }],
@@ -1265,7 +1280,7 @@ async function scan(rootDir, opts = {}) {
1265
1280
  severity: "WARNING",
1266
1281
  confidence: "PROBABLE",
1267
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}.`,
1268
- fingerprint: `STORAGE_ALL_PATHS_OPEN:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
1283
+ fingerprint: `STORAGE_ALL_PATHS_OPEN:${m.fpPath}:${normalizeTarget(m.target)}:${key}`,
1269
1284
  file: relStorage,
1270
1285
  line: m.line,
1271
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 } } }],
@@ -1278,7 +1293,7 @@ async function scan(rootDir, opts = {}) {
1278
1293
  severity: "INFO",
1279
1294
  confidence: "PROBABLE",
1280
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.`,
1281
- fingerprint: `STORAGE_PUBLIC_READ:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
1296
+ fingerprint: `STORAGE_PUBLIC_READ:${m.fpPath}:${normalizeTarget(m.target)}:${key}`,
1282
1297
  file: relStorage,
1283
1298
  line: m.line,
1284
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 } } }],
@@ -1290,12 +1305,13 @@ async function scan(rootDir, opts = {}) {
1290
1305
  }
1291
1306
  const noAuthBranch = findNoAuthBranch(effectiveCond);
1292
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);
1293
1309
  findings.push({
1294
1310
  rule: "STORAGE_ANON_ALLOWED",
1295
1311
  severity: "INFO",
1296
1312
  confidence: "PROBABLE",
1297
- message: `Storage aceita qualquer autenticado incluindo an\xF4nimo (${where}). Exija claim/provider para dados sens\xEDveis.`,
1298
- 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}`,
1299
1315
  file: relStorage,
1300
1316
  line: m.line,
1301
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 } } }],
@@ -1316,7 +1332,7 @@ async function scan(rootDir, opts = {}) {
1316
1332
  severity: "ERROR",
1317
1333
  confidence: "PROBABLE",
1318
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}`,
1319
- fingerprint: `STORAGE_PUBLIC_WRITE:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
1335
+ fingerprint: `STORAGE_PUBLIC_WRITE:${m.fpPath}:${normalizeTarget(m.target)}:${key}`,
1320
1336
  file: relStorage,
1321
1337
  line: m.line,
1322
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 } } }],
@@ -1328,7 +1344,7 @@ async function scan(rootDir, opts = {}) {
1328
1344
  severity: "INFO",
1329
1345
  confidence: "PROBABLE",
1330
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}`,
1331
- fingerprint: `STORAGE_PUBLIC_READ:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
1347
+ fingerprint: `STORAGE_PUBLIC_READ:${m.fpPath}:${normalizeTarget(m.target)}:${key}`,
1332
1348
  file: relStorage,
1333
1349
  line: m.line,
1334
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 } } }]
@@ -1538,21 +1554,22 @@ function findNoAuthBranch(effective) {
1538
1554
  return void 0;
1539
1555
  }
1540
1556
  function parseStorageAllows(clean) {
1557
+ const text = clean.replace(/\r\n?/g, "\n");
1541
1558
  const out = [];
1542
1559
  const matches = [];
1543
1560
  const matchRe = /match\s+/g;
1544
1561
  let mm;
1545
- while ((mm = matchRe.exec(clean)) !== null) {
1562
+ while ((mm = matchRe.exec(text)) !== null) {
1546
1563
  const from = mm.index + mm[0].length;
1547
1564
  let openBrace = -1;
1548
1565
  let scan2 = from;
1549
- while (scan2 < clean.length) {
1550
- const b = clean.indexOf("{", scan2);
1566
+ while (scan2 < text.length) {
1567
+ const b = text.indexOf("{", scan2);
1551
1568
  if (b === -1) break;
1552
- const between = clean.slice(from, b);
1569
+ const between = text.slice(from, b);
1553
1570
  if (/match\s|allow\s|;/.test(between)) break;
1554
- const after = clean.slice(b + 1, b + 12).trimStart().replace(/^\n+/, "");
1555
- 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;
1556
1573
  if (/^(allow|match|function|\})/.test(after) || eol) {
1557
1574
  openBrace = b;
1558
1575
  break;
@@ -1560,14 +1577,14 @@ function parseStorageAllows(clean) {
1560
1577
  scan2 = b + 1;
1561
1578
  }
1562
1579
  if (openBrace === -1) continue;
1563
- const path = clean.slice(from, openBrace).trim() || "/(unknown)";
1580
+ const path = text.slice(from, openBrace).trim() || "/(unknown)";
1564
1581
  let depth = 0;
1565
1582
  let quote = null;
1566
- let end = clean.length;
1567
- for (let i = openBrace; i < clean.length; i++) {
1568
- const c = clean[i];
1583
+ let end = text.length;
1584
+ for (let i = openBrace; i < text.length; i++) {
1585
+ const c = text[i];
1569
1586
  if (quote) {
1570
- if (c === quote && clean[i - 1] !== "\\") quote = null;
1587
+ if (c === quote && text[i - 1] !== "\\") quote = null;
1571
1588
  continue;
1572
1589
  }
1573
1590
  if (c === '"' || c === "'" || c === "`") {
@@ -1587,15 +1604,17 @@ function parseStorageAllows(clean) {
1587
1604
  }
1588
1605
  const re = /allow\s+([^;:]+?)(?::\s*if\s+([^;]+))?;/gi;
1589
1606
  let m;
1590
- while ((m = re.exec(clean)) !== null) {
1607
+ while ((m = re.exec(text)) !== null) {
1591
1608
  const containing = matches.filter((b) => b.start <= m.index && m.index < b.end).sort((a, b) => a.start - b.start);
1592
1609
  const parts = [];
1593
1610
  for (const b of containing) {
1594
1611
  const p = b.path.trim().replace(/^\/+|\/+$/g, "");
1595
1612
  if (p.length > 0) parts.push(p);
1596
1613
  }
1597
- const line = clean.slice(0, m.index).split("\n").length;
1598
- 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 });
1599
1618
  }
1600
1619
  return out;
1601
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-ZNVQGZMJ.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-M6OYRIKL.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-ZE5K5HRR.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-ZNVQGZMJ.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-ZE5K5HRR.js";
5
- import "./chunk-ZNVQGZMJ.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-ZE5K5HRR.js";
5
- import "./chunk-ZNVQGZMJ.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-ZNVQGZMJ.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.11",
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