@justmpm/firebase-audit 0.5.9 → 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.
- package/dist/{chunk-4ST23J6W.js → chunk-ZE5K5HRR.js} +1 -1
- package/dist/{chunk-B4ATI4OJ.js → chunk-ZNVQGZMJ.js} +35 -10
- package/dist/cli.js +2 -2
- package/dist/index.js +2 -2
- package/dist/mcp-cli.js +2 -2
- package/dist/mcp.js +2 -2
- package/dist/{scan-CG54EVVP.js → scan-M6OYRIKL.js} +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +2 -0
|
@@ -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,
|
|
@@ -1220,23 +1221,29 @@ async function scan(rootDir, opts = {}) {
|
|
|
1220
1221
|
const raw = readFileSync3(d.storageRulesFile, "utf-8");
|
|
1221
1222
|
const relStorage = model.firebase.storage?.rulesFile ?? "storage.rules";
|
|
1222
1223
|
const clean = stripRuleComments(raw);
|
|
1224
|
+
const storageHelpers = parseRuleFunctionsDetailed(clean);
|
|
1223
1225
|
let evCounter = 0;
|
|
1224
1226
|
const evId = (prefix) => {
|
|
1225
1227
|
evCounter += 1;
|
|
1226
1228
|
return `ev-storage-${prefix}-${evCounter}`;
|
|
1227
1229
|
};
|
|
1228
1230
|
for (const m of parseStorageAllows(clean)) {
|
|
1229
|
-
if (m.condition !== void 0 && stripOuterParens(m.condition).toLowerCase().replace(/\s+/g, "") === "false") {
|
|
1230
|
-
continue;
|
|
1231
|
-
}
|
|
1232
1231
|
const ops = expandStorageOps(m.target);
|
|
1233
1232
|
const key = conditionKeyForFingerprint(m.condition);
|
|
1234
|
-
const
|
|
1233
|
+
const singleStorage = m.condition === void 0 ? void 0 : resolveHelperConditionDetailed(m.condition, storageHelpers);
|
|
1234
|
+
const inlinedStorage = m.condition === void 0 ? void 0 : inlineHelpersInCondition(m.condition, storageHelpers);
|
|
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, "");
|
|
1235
1242
|
const hasAuth = normCond.includes("request.auth");
|
|
1236
1243
|
const bareAuth = (/request\.auth(!==|!=)null/.test(normCond) || /request\.auth\.uid(!==|!=)null/.test(normCond)) && !normCond.includes("request.auth.token") && !normCond.includes("sign_in_provider");
|
|
1237
1244
|
const wildcard = /\{\s*\w+\s*=\s*\*\*\s*\}/.test(m.matchPath);
|
|
1238
1245
|
const where = `${relStorage}:${m.line} (${m.matchPath}, allow ${m.target})`;
|
|
1239
|
-
if (isOpenStorageCondition(
|
|
1246
|
+
if (isOpenStorageCondition(effectiveCond)) {
|
|
1240
1247
|
if (ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete")) {
|
|
1241
1248
|
findings.push({
|
|
1242
1249
|
rule: "STORAGE_PUBLIC_WRITE",
|
|
@@ -1281,7 +1288,8 @@ async function scan(rootDir, opts = {}) {
|
|
|
1281
1288
|
continue;
|
|
1282
1289
|
}
|
|
1283
1290
|
}
|
|
1284
|
-
|
|
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"))) {
|
|
1285
1293
|
findings.push({
|
|
1286
1294
|
rule: "STORAGE_ANON_ALLOWED",
|
|
1287
1295
|
severity: "INFO",
|
|
@@ -1295,7 +1303,11 @@ async function scan(rootDir, opts = {}) {
|
|
|
1295
1303
|
});
|
|
1296
1304
|
continue;
|
|
1297
1305
|
}
|
|
1298
|
-
if (!hasAuth &&
|
|
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.` : "";
|
|
1310
|
+
const noAuthNote = unresolvedHelper ? ` Helper "${unresolvedHelper}" n\xE3o resolvido no storage.rules \u2014 confirme o corpo no Emulator.` : "";
|
|
1299
1311
|
const canWrite = ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete");
|
|
1300
1312
|
const canRead = ops.has("read") || ops.has("get") || ops.has("list");
|
|
1301
1313
|
if (canWrite) {
|
|
@@ -1303,7 +1315,7 @@ async function scan(rootDir, opts = {}) {
|
|
|
1303
1315
|
rule: "STORAGE_PUBLIC_WRITE",
|
|
1304
1316
|
severity: "ERROR",
|
|
1305
1317
|
confidence: "PROBABLE",
|
|
1306
|
-
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
|
|
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}`,
|
|
1307
1319
|
fingerprint: `STORAGE_PUBLIC_WRITE:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
|
|
1308
1320
|
file: relStorage,
|
|
1309
1321
|
line: m.line,
|
|
@@ -1315,7 +1327,7 @@ async function scan(rootDir, opts = {}) {
|
|
|
1315
1327
|
rule: "STORAGE_PUBLIC_READ",
|
|
1316
1328
|
severity: "INFO",
|
|
1317
1329
|
confidence: "PROBABLE",
|
|
1318
|
-
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
|
|
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}`,
|
|
1319
1331
|
fingerprint: `STORAGE_PUBLIC_READ:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
|
|
1320
1332
|
file: relStorage,
|
|
1321
1333
|
line: m.line,
|
|
@@ -1506,12 +1518,25 @@ function isOpenStorageCondition(condition) {
|
|
|
1506
1518
|
if (condition === void 0) return true;
|
|
1507
1519
|
const norm = stripOuterParens(condition).toLowerCase().replace(/\s+/g, "");
|
|
1508
1520
|
if (norm === "" || norm === "true") return true;
|
|
1521
|
+
for (const part of norm.split("||")) {
|
|
1522
|
+
const branch = stripOuterParens(part.replace(/return/g, "").replace(/;/g, "").trim());
|
|
1523
|
+
if (branch === "true") return true;
|
|
1524
|
+
}
|
|
1509
1525
|
if (/^!\(.*request\.auth(===|==)null/.test(norm)) return false;
|
|
1510
1526
|
if (/^!\(.*request\.auth(!==|!=)null/.test(norm)) return true;
|
|
1511
|
-
if (/(^|\|\|)true($|\|\|)/.test(norm)) return true;
|
|
1512
1527
|
if (norm.includes("request.auth==null") || norm.includes("request.auth===null")) return true;
|
|
1513
1528
|
return false;
|
|
1514
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
|
+
}
|
|
1515
1540
|
function parseStorageAllows(clean) {
|
|
1516
1541
|
const out = [];
|
|
1517
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
5
|
-
import "./chunk-
|
|
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-
|
|
5
|
-
import "./chunk-
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justmpm/firebase-audit",
|
|
3
|
-
"version": "0.5.
|
|
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,6 +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", 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`
|
|
23
25
|
- Seeds/fixtures com Admin SDK moram em `scripts/` ou `server/`, nunca em `src/` (senão o scan acusa FBA003 de propósito)
|
|
24
26
|
|
|
25
27
|
## Emulador
|