@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.
- package/dist/{chunk-OYWTDV45.js → chunk-LK5MKSCZ.js} +1 -1
- package/dist/{chunk-CTQPAMOE.js → chunk-NOIROFKB.js} +27 -22
- 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-CU54MUN7.js → scan-2TMTMC7C.js} +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +2 -0
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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 <
|
|
1564
|
-
const b =
|
|
1566
|
+
while (scan2 < text.length) {
|
|
1567
|
+
const b = text.indexOf("{", scan2);
|
|
1565
1568
|
if (b === -1) break;
|
|
1566
|
-
const between =
|
|
1569
|
+
const between = text.slice(from, b);
|
|
1567
1570
|
if (/match\s|allow\s|;/.test(between)) break;
|
|
1568
|
-
const after =
|
|
1569
|
-
const eol =
|
|
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 =
|
|
1580
|
+
const path = text.slice(from, openBrace).trim() || "/(unknown)";
|
|
1578
1581
|
let depth = 0;
|
|
1579
1582
|
let quote = null;
|
|
1580
|
-
let end =
|
|
1581
|
-
for (let i = openBrace; i <
|
|
1582
|
-
const c =
|
|
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 &&
|
|
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(
|
|
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 =
|
|
1612
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
5
|
-
import "./chunk-
|
|
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-
|
|
5
|
-
import "./chunk-
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justmpm/firebase-audit",
|
|
3
|
-
"version": "0.5.
|
|
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
|
|