@justmpm/firebase-audit 0.5.8 → 0.5.10

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.
@@ -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,
@@ -912,10 +913,9 @@ function validateClaimSamples(samples) {
912
913
 
913
914
  // src/scan.ts
914
915
  async function scan(rootDir, opts = {}) {
915
- const { statSync: statRoot } = await import("fs");
916
916
  let rootOk = false;
917
917
  try {
918
- rootOk = statRoot(rootDir).isDirectory();
918
+ rootOk = statSync2(rootDir).isDirectory();
919
919
  } catch {
920
920
  rootOk = false;
921
921
  }
@@ -1221,6 +1221,7 @@ async function scan(rootDir, opts = {}) {
1221
1221
  const raw = readFileSync3(d.storageRulesFile, "utf-8");
1222
1222
  const relStorage = model.firebase.storage?.rulesFile ?? "storage.rules";
1223
1223
  const clean = stripRuleComments(raw);
1224
+ const storageHelpers = parseRuleFunctionsDetailed(clean);
1224
1225
  let evCounter = 0;
1225
1226
  const evId = (prefix) => {
1226
1227
  evCounter += 1;
@@ -1232,12 +1233,15 @@ async function scan(rootDir, opts = {}) {
1232
1233
  }
1233
1234
  const ops = expandStorageOps(m.target);
1234
1235
  const key = conditionKeyForFingerprint(m.condition);
1235
- const normCond = (m.condition ?? "").toLowerCase().replace(/\s+/g, "");
1236
+ const singleStorage = m.condition === void 0 ? void 0 : resolveHelperConditionDetailed(m.condition, storageHelpers);
1237
+ 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, "");
1236
1240
  const hasAuth = normCond.includes("request.auth");
1237
1241
  const bareAuth = (/request\.auth(!==|!=)null/.test(normCond) || /request\.auth\.uid(!==|!=)null/.test(normCond)) && !normCond.includes("request.auth.token") && !normCond.includes("sign_in_provider");
1238
1242
  const wildcard = /\{\s*\w+\s*=\s*\*\*\s*\}/.test(m.matchPath);
1239
1243
  const where = `${relStorage}:${m.line} (${m.matchPath}, allow ${m.target})`;
1240
- if (isOpenStorageCondition(m.condition)) {
1244
+ if (isOpenStorageCondition(effectiveCond)) {
1241
1245
  if (ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete")) {
1242
1246
  findings.push({
1243
1247
  rule: "STORAGE_PUBLIC_WRITE",
@@ -1296,7 +1300,9 @@ async function scan(rootDir, opts = {}) {
1296
1300
  });
1297
1301
  continue;
1298
1302
  }
1299
- if (!hasAuth && m.condition !== void 0) {
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;
1305
+ const noAuthNote = unresolvedHelper ? ` Helper "${unresolvedHelper}" n\xE3o resolvido no storage.rules \u2014 confirme o corpo no Emulator.` : "";
1300
1306
  const canWrite = ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete");
1301
1307
  const canRead = ops.has("read") || ops.has("get") || ops.has("list");
1302
1308
  if (canWrite) {
@@ -1304,7 +1310,7 @@ async function scan(rootDir, opts = {}) {
1304
1310
  rule: "STORAGE_PUBLIC_WRITE",
1305
1311
  severity: "ERROR",
1306
1312
  confidence: "PROBABLE",
1307
- 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.`,
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}`,
1308
1314
  fingerprint: `STORAGE_PUBLIC_WRITE:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
1309
1315
  file: relStorage,
1310
1316
  line: m.line,
@@ -1316,7 +1322,7 @@ async function scan(rootDir, opts = {}) {
1316
1322
  rule: "STORAGE_PUBLIC_READ",
1317
1323
  severity: "INFO",
1318
1324
  confidence: "PROBABLE",
1319
- 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.`,
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}`,
1320
1326
  fingerprint: `STORAGE_PUBLIC_READ:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
1321
1327
  file: relStorage,
1322
1328
  line: m.line,
@@ -1507,9 +1513,12 @@ function isOpenStorageCondition(condition) {
1507
1513
  if (condition === void 0) return true;
1508
1514
  const norm = stripOuterParens(condition).toLowerCase().replace(/\s+/g, "");
1509
1515
  if (norm === "" || norm === "true") return true;
1516
+ for (const part of norm.split("||")) {
1517
+ const branch = stripOuterParens(part.replace(/return/g, "").replace(/;/g, "").trim());
1518
+ if (branch === "true") return true;
1519
+ }
1510
1520
  if (/^!\(.*request\.auth(===|==)null/.test(norm)) return false;
1511
1521
  if (/^!\(.*request\.auth(!==|!=)null/.test(norm)) return true;
1512
- if (/(^|\|\|)true($|\|\|)/.test(norm)) return true;
1513
1522
  if (norm.includes("request.auth==null") || norm.includes("request.auth===null")) return true;
1514
1523
  return false;
1515
1524
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  scan
3
- } from "./chunk-NZT4NIVA.js";
3
+ } from "./chunk-GR2SM3OL.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-NZT4NIVA.js";
4
+ } from "./chunk-GR2SM3OL.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-RTIMO3I2.js");
87
+ const { scan: scanForVerify } = await import("./scan-YY5UY35J.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-D75SRJ7P.js";
4
+ } from "./chunk-JPHERTX5.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-NZT4NIVA.js";
37
+ } from "./chunk-GR2SM3OL.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-D75SRJ7P.js";
5
- import "./chunk-NZT4NIVA.js";
4
+ } from "./chunk-JPHERTX5.js";
5
+ import "./chunk-GR2SM3OL.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-D75SRJ7P.js";
5
- import "./chunk-NZT4NIVA.js";
4
+ } from "./chunk-JPHERTX5.js";
5
+ import "./chunk-GR2SM3OL.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-NZT4NIVA.js";
3
+ } from "./chunk-GR2SM3OL.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.8",
3
+ "version": "0.5.10",
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,7 @@ 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
24
  - Seeds/fixtures com Admin SDK moram em `scripts/` ou `server/`, nunca em `src/` (senão o scan acusa FBA003 de propósito)
24
25
 
25
26
  ## Emulador