@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.
- package/dist/{chunk-ZE5K5HRR.js → chunk-LK5MKSCZ.js} +1 -1
- package/dist/{chunk-ZNVQGZMJ.js → chunk-NOIROFKB.js} +41 -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-M6OYRIKL.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,
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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 <
|
|
1550
|
-
const b =
|
|
1566
|
+
while (scan2 < text.length) {
|
|
1567
|
+
const b = text.indexOf("{", scan2);
|
|
1551
1568
|
if (b === -1) break;
|
|
1552
|
-
const between =
|
|
1569
|
+
const between = text.slice(from, b);
|
|
1553
1570
|
if (/match\s|allow\s|;/.test(between)) break;
|
|
1554
|
-
const after =
|
|
1555
|
-
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;
|
|
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 =
|
|
1580
|
+
const path = text.slice(from, openBrace).trim() || "/(unknown)";
|
|
1564
1581
|
let depth = 0;
|
|
1565
1582
|
let quote = null;
|
|
1566
|
-
let end =
|
|
1567
|
-
for (let i = openBrace; i <
|
|
1568
|
-
const c =
|
|
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 &&
|
|
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(
|
|
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 =
|
|
1598
|
-
|
|
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-
|
|
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
|
|