@justmpm/firebase-audit 0.5.1 → 0.5.2
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-QACXCMYB.js → chunk-AXTYJMA7.js} +1 -1
- package/dist/{chunk-GDO2DLNQ.js → chunk-UKEKA2NQ.js} +111 -26
- package/dist/cli.js +5 -5
- package/dist/index.js +2 -2
- package/dist/mcp-cli.js +2 -2
- package/dist/mcp.js +2 -2
- package/dist/{scan-4Q4XVLJU.js → scan-OEN65GHM.js} +1 -1
- package/package.json +1 -1
|
@@ -866,6 +866,16 @@ async function scan(rootDir, opts = {}) {
|
|
|
866
866
|
}
|
|
867
867
|
}
|
|
868
868
|
} catch {
|
|
869
|
+
findings_pre.push({
|
|
870
|
+
rule: "FIREBASE_JSON_INVALID",
|
|
871
|
+
severity: "WARNING",
|
|
872
|
+
confidence: "CONFIRMED",
|
|
873
|
+
message: "firebase.json inv\xE1lido \u2014 seguindo com defaults (firestore.rules, firestore.indexes.json). Corrija o JSON.",
|
|
874
|
+
fingerprint: "FIREBASE_JSON_INVALID",
|
|
875
|
+
evidence: [
|
|
876
|
+
{ id: "ev-firebase-json", kind: "config-parse", summary: "JSON inv\xE1lido", confidence: "CONFIRMED" }
|
|
877
|
+
]
|
|
878
|
+
});
|
|
869
879
|
}
|
|
870
880
|
if (!d.firestoreRulesFile || !existsSync3(d.firestoreRulesFile)) {
|
|
871
881
|
findings_pre.push({
|
|
@@ -1076,34 +1086,40 @@ async function scan(rootDir, opts = {}) {
|
|
|
1076
1086
|
const raw = readFileSync3(d.storageRulesFile, "utf-8");
|
|
1077
1087
|
const relStorage = model.firebase.storage?.rulesFile ?? "storage.rules";
|
|
1078
1088
|
const clean = stripRuleComments(raw);
|
|
1079
|
-
|
|
1080
|
-
const
|
|
1089
|
+
let evCounter = 0;
|
|
1090
|
+
const evId = (prefix) => {
|
|
1091
|
+
evCounter += 1;
|
|
1092
|
+
return `ev-storage-${prefix}-${evCounter}`;
|
|
1093
|
+
};
|
|
1081
1094
|
for (const m of parseStorageAllows(clean)) {
|
|
1082
|
-
|
|
1083
|
-
if (!open) continue;
|
|
1095
|
+
if (!isOpenStorageCondition(m.condition)) continue;
|
|
1084
1096
|
const ops = expandStorageOps(m.target);
|
|
1085
1097
|
const key = conditionKeyForFingerprint(m.condition);
|
|
1098
|
+
const wildcard = /\{[^}]*=\*\*\}/.test(m.matchPath) || m.matchPath.toLowerCase().includes("allpaths");
|
|
1099
|
+
const where = `${relStorage}:${m.line} (${m.matchPath}, allow ${m.target})`;
|
|
1086
1100
|
if (ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete")) {
|
|
1087
1101
|
findings.push({
|
|
1088
1102
|
rule: "STORAGE_PUBLIC_WRITE",
|
|
1089
1103
|
severity: "ERROR",
|
|
1090
1104
|
confidence: "CONFIRMED",
|
|
1091
|
-
message: `Storage com escrita aberta (${
|
|
1092
|
-
fingerprint: `STORAGE_PUBLIC_WRITE:${m.target}:${key}`,
|
|
1105
|
+
message: `Storage com escrita aberta (${where}). Qualquer cliente pode escrever \u2014 cofre aberto, n\xE3o vitrine.`,
|
|
1106
|
+
fingerprint: `STORAGE_PUBLIC_WRITE:${m.matchPath}:${m.target}:${key}`,
|
|
1093
1107
|
file: relStorage,
|
|
1094
|
-
|
|
1108
|
+
line: m.line,
|
|
1109
|
+
evidence: [{ id: evId("write"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "CONFIRMED", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
|
|
1095
1110
|
fix: "Exigir request.auth (e dono/tenant) no write. Deixe get aberto s\xF3 em pasta de vitrine como logos/."
|
|
1096
1111
|
});
|
|
1097
1112
|
} else if (ops.has("read") || ops.has("get") || ops.has("list")) {
|
|
1098
|
-
if (
|
|
1113
|
+
if (wildcard) {
|
|
1099
1114
|
findings.push({
|
|
1100
1115
|
rule: "STORAGE_ALL_PATHS_OPEN",
|
|
1101
1116
|
severity: "WARNING",
|
|
1102
1117
|
confidence: "PROBABLE",
|
|
1103
|
-
message: `Storage com leitura aberta em curinga total (${
|
|
1104
|
-
fingerprint: `STORAGE_ALL_PATHS_OPEN:${m.target}:${key}`,
|
|
1118
|
+
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}.`,
|
|
1119
|
+
fingerprint: `STORAGE_ALL_PATHS_OPEN:${m.matchPath}:${m.target}:${key}`,
|
|
1105
1120
|
file: relStorage,
|
|
1106
|
-
|
|
1121
|
+
line: m.line,
|
|
1122
|
+
evidence: [{ id: evId("all"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
|
|
1107
1123
|
fix: "Troque o curinga total por pastas expl\xEDcitas (ex: /logos/{logoId}) com get: if true e write restrito."
|
|
1108
1124
|
});
|
|
1109
1125
|
} else {
|
|
@@ -1111,10 +1127,11 @@ async function scan(rootDir, opts = {}) {
|
|
|
1111
1127
|
rule: "STORAGE_PUBLIC_READ",
|
|
1112
1128
|
severity: "INFO",
|
|
1113
1129
|
confidence: "PROBABLE",
|
|
1114
|
-
message: `Storage com leitura aberta (${
|
|
1115
|
-
fingerprint: `STORAGE_PUBLIC_READ:${m.target}:${key}`,
|
|
1130
|
+
message: `Storage com leitura aberta (${where}). Pode ser vitrine proposital (logos) \u2014 confirme que o write segue restrito.`,
|
|
1131
|
+
fingerprint: `STORAGE_PUBLIC_READ:${m.matchPath}:${m.target}:${key}`,
|
|
1116
1132
|
file: relStorage,
|
|
1117
|
-
|
|
1133
|
+
line: m.line,
|
|
1134
|
+
evidence: [{ id: evId("read"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }]
|
|
1118
1135
|
});
|
|
1119
1136
|
}
|
|
1120
1137
|
}
|
|
@@ -1126,24 +1143,38 @@ async function scan(rootDir, opts = {}) {
|
|
|
1126
1143
|
skipped.push("storage.rules n\xE3o observado \u2014 Storage fora deste scan");
|
|
1127
1144
|
}
|
|
1128
1145
|
if (d.functionsDir) {
|
|
1129
|
-
const
|
|
1130
|
-
model.functions = { dir: model.functions?.dir, appCheckEnforced:
|
|
1146
|
+
const appcheck = await countAppCheckEnforced(rootDir);
|
|
1147
|
+
model.functions = { dir: model.functions?.dir, appCheckEnforced: appcheck.occurrences };
|
|
1131
1148
|
checked.push("functions-appcheck");
|
|
1132
|
-
if (
|
|
1149
|
+
if (appcheck.manual > 0) {
|
|
1150
|
+
findings.push({
|
|
1151
|
+
rule: "APPCHECK_MANUAL",
|
|
1152
|
+
severity: "INFO",
|
|
1153
|
+
confidence: "PROBABLE",
|
|
1154
|
+
message: `Verifica\xE7\xE3o manual de AppCheck observada (${appcheck.manual} ocorr\xEAncia(s) de verifyToken). Cubra rota a rota no onRequest \u2014 o scan n\xE3o audita cada rota.`,
|
|
1155
|
+
fingerprint: "APPCHECK_MANUAL",
|
|
1156
|
+
evidence: [{ id: "ev-appcheck-manual", kind: "appcheck", summary: `${appcheck.manual} verifyToken`, confidence: "PROBABLE" }]
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
if (appcheck.occurrences > 0) {
|
|
1133
1160
|
findings.push({
|
|
1134
1161
|
rule: "APPCHECK_ENFORCED",
|
|
1135
1162
|
severity: "INFO",
|
|
1136
1163
|
confidence: "CONFIRMED",
|
|
1137
|
-
message: `
|
|
1164
|
+
message: `enforceAppCheck observado (${appcheck.occurrences} ocorr\xEAncia(s) em ${appcheck.files} arquivo(s)). Carimbo do app oficial \u2014 n\xE3o prova quem \xE9 o usu\xE1rio nem o tenant. Autoriza\xE7\xE3o segue nas Rules/guardas.`,
|
|
1138
1165
|
fingerprint: "APPCHECK_ENFORCED",
|
|
1139
|
-
evidence: [{ id: "ev-appcheck", kind: "appcheck", summary: `${
|
|
1166
|
+
evidence: [{ id: "ev-appcheck", kind: "appcheck", summary: `${appcheck.occurrences} ocorr\xEAncias`, confidence: "CONFIRMED" }]
|
|
1140
1167
|
});
|
|
1141
|
-
} else {
|
|
1142
|
-
skipped.push("functions sem enforceAppCheck observado \u2014 AppCheck sem cobertura afirmativa");
|
|
1168
|
+
} else if (appcheck.manual === 0) {
|
|
1169
|
+
skipped.push("functions sem enforceAppCheck nem verifyToken observado \u2014 AppCheck sem cobertura afirmativa (onRequest exige verifica\xE7\xE3o manual por rota)");
|
|
1170
|
+
}
|
|
1171
|
+
if (appcheck.limitHit) {
|
|
1172
|
+
skipped.push("functions: teto de varredura atingido \u2014 AppCheck parcial neste scan");
|
|
1143
1173
|
}
|
|
1144
1174
|
} else {
|
|
1145
1175
|
skipped.push("functions n\xE3o observadas \u2014 AppCheck fora deste scan");
|
|
1146
1176
|
}
|
|
1177
|
+
skipped.push("isolamento por tenant (ownerId/tenantId) n\xE3o verificado \u2014 fora dos checks implementados, nunca verde silencioso");
|
|
1147
1178
|
const modelCheck = ProjectModelSchema.safeParse(model);
|
|
1148
1179
|
if (!modelCheck.success) {
|
|
1149
1180
|
findings.push({
|
|
@@ -1205,7 +1236,10 @@ async function countAppCheckEnforced(rootDir) {
|
|
|
1205
1236
|
const { join: joinP } = await import("path");
|
|
1206
1237
|
const { stripRuleComments: strip } = await import("./rules-GTGZWN5U.js");
|
|
1207
1238
|
const roots = [joinP(rootDir, "functions")];
|
|
1208
|
-
let
|
|
1239
|
+
let occurrences = 0;
|
|
1240
|
+
const files = /* @__PURE__ */ new Set();
|
|
1241
|
+
let manual = 0;
|
|
1242
|
+
let limitHit = false;
|
|
1209
1243
|
const stack = [...roots];
|
|
1210
1244
|
let guard = 0;
|
|
1211
1245
|
const isTestFile = (p) => {
|
|
@@ -1214,6 +1248,7 @@ async function countAppCheckEnforced(rootDir) {
|
|
|
1214
1248
|
};
|
|
1215
1249
|
while (stack.length > 0 && guard < 200) {
|
|
1216
1250
|
guard += 1;
|
|
1251
|
+
if (guard >= 200 && stack.length > 0) limitHit = true;
|
|
1217
1252
|
const cur = stack.pop();
|
|
1218
1253
|
let entries = [];
|
|
1219
1254
|
try {
|
|
@@ -1240,22 +1275,72 @@ async function countAppCheckEnforced(rootDir) {
|
|
|
1240
1275
|
try {
|
|
1241
1276
|
const text = strip(readSync(abs, "utf-8"));
|
|
1242
1277
|
const hits = text.match(/enforceAppCheck\s*:\s*true/g);
|
|
1243
|
-
if (hits)
|
|
1278
|
+
if (hits) {
|
|
1279
|
+
occurrences += hits.length;
|
|
1280
|
+
files.add(abs);
|
|
1281
|
+
}
|
|
1282
|
+
const manualHits = text.match(/verifyToken\s*\(/g);
|
|
1283
|
+
if (manualHits) manual += manualHits.length;
|
|
1244
1284
|
} catch {
|
|
1245
1285
|
}
|
|
1246
1286
|
}
|
|
1247
1287
|
}
|
|
1248
|
-
return
|
|
1288
|
+
return { occurrences, files: files.size, manual, limitHit };
|
|
1249
1289
|
} catch {
|
|
1250
|
-
return 0;
|
|
1290
|
+
return { occurrences: 0, files: 0, manual: 0, limitHit: false };
|
|
1251
1291
|
}
|
|
1252
1292
|
}
|
|
1293
|
+
function isOpenStorageCondition(condition) {
|
|
1294
|
+
if (condition === void 0) return true;
|
|
1295
|
+
const norm = stripOuterParens(condition).toLowerCase().replace(/\s+/g, "");
|
|
1296
|
+
if (norm === "" || norm === "true") return true;
|
|
1297
|
+
if (/(^|\|\|)true($|\|\|)/.test(norm)) return true;
|
|
1298
|
+
if (norm.includes("request.auth==null") || norm.includes("request.auth===null")) return true;
|
|
1299
|
+
return false;
|
|
1300
|
+
}
|
|
1253
1301
|
function parseStorageAllows(clean) {
|
|
1254
1302
|
const out = [];
|
|
1303
|
+
const matches = [];
|
|
1304
|
+
const matchRe = /match\s+/g;
|
|
1305
|
+
let mm;
|
|
1306
|
+
while ((mm = matchRe.exec(clean)) !== null) {
|
|
1307
|
+
const lineStart = clean.lastIndexOf("\n", mm.index) + 1;
|
|
1308
|
+
const lineEnd = clean.indexOf("\n", mm.index);
|
|
1309
|
+
const line = clean.slice(lineStart, lineEnd === -1 ? void 0 : lineEnd);
|
|
1310
|
+
const brace = line.lastIndexOf("{");
|
|
1311
|
+
if (brace === -1) continue;
|
|
1312
|
+
const path = line.slice(mm.index - lineStart + 5, brace).trim() || "/(unknown)";
|
|
1313
|
+
const openBrace = lineStart + brace;
|
|
1314
|
+
let depth = 0;
|
|
1315
|
+
let quote = null;
|
|
1316
|
+
let end = clean.length;
|
|
1317
|
+
for (let i = openBrace; i < clean.length; i++) {
|
|
1318
|
+
const c = clean[i];
|
|
1319
|
+
if (quote) {
|
|
1320
|
+
if (c === quote && clean[i - 1] !== "\\") quote = null;
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
1324
|
+
quote = c;
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
if (c === "{") depth += 1;
|
|
1328
|
+
else if (c === "}") {
|
|
1329
|
+
depth -= 1;
|
|
1330
|
+
if (depth === 0) {
|
|
1331
|
+
end = i + 1;
|
|
1332
|
+
break;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
matches.push({ path, start: mm.index, end });
|
|
1337
|
+
}
|
|
1255
1338
|
const re = /allow\s+([^;:]+?)(?::\s*if\s+([^;]+))?;/gi;
|
|
1256
1339
|
let m;
|
|
1257
1340
|
while ((m = re.exec(clean)) !== null) {
|
|
1258
|
-
|
|
1341
|
+
const containing = matches.filter((b) => b.start <= m.index && m.index < b.end).sort((a, b) => b.start - a.start);
|
|
1342
|
+
const line = clean.slice(0, m.index).split("\n").length;
|
|
1343
|
+
out.push({ target: m[1].trim(), condition: m[2]?.trim(), matchPath: containing[0]?.path ?? "/(unknown)", line });
|
|
1259
1344
|
}
|
|
1260
1345
|
return out;
|
|
1261
1346
|
}
|
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-UKEKA2NQ.js";
|
|
5
5
|
import "./chunk-7EWTBIKJ.js";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
@@ -33,7 +33,7 @@ Uso: firebase-audit scan [--json] [--strict] [--adapter=fn] [--cwd=path]`);
|
|
|
33
33
|
process.exit(1);
|
|
34
34
|
}
|
|
35
35
|
const strict = args.includes("--strict") || cmd === "check";
|
|
36
|
-
const asJson = args.includes("--json");
|
|
36
|
+
const asJson = args.includes("--json") || args.includes("--json=true");
|
|
37
37
|
const getArg = (name) => {
|
|
38
38
|
const pref = `--${name}=`;
|
|
39
39
|
const hit = args.find((a) => a.startsWith(pref));
|
|
@@ -65,7 +65,7 @@ Uso: firebase-audit scan|check|verify|drift [--json] ...`);
|
|
|
65
65
|
const { resolve: resolveRoot } = await import("path");
|
|
66
66
|
const rootDir = cwdArg ? resolveRoot(process.cwd(), cwdArg) : process.cwd();
|
|
67
67
|
if (cmd === "verify") {
|
|
68
|
-
const { scan: scanForVerify } = await import("./scan-
|
|
68
|
+
const { scan: scanForVerify } = await import("./scan-OEN65GHM.js");
|
|
69
69
|
const { verify } = await import("./verify-WBVSESHD.js");
|
|
70
70
|
const { resolve: resolveVerify } = await import("path");
|
|
71
71
|
const coverageArg = getArg("coverage");
|
|
@@ -149,7 +149,7 @@ FIREBASE AUDIT drift \u2014 MATCHED ${counts("MATCHED")}, LOCAL_ONLY ${counts("L
|
|
|
149
149
|
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
150
150
|
if (findings.length === 0) {
|
|
151
151
|
console.log(`
|
|
152
|
-
\u2713 Nenhum problema nos checks implementados (${summary.passedChecks}/${summary.totalChecks} passaram).
|
|
152
|
+
\u2713 Nenhum problema nos checks FBA implementados (${summary.passedChecks}/${summary.totalChecks} passaram).
|
|
153
153
|
Olhei: ${summary.checked.join(", ")}.
|
|
154
154
|
Pulei: ${summary.skipped.join(" | ") || "nada"}.
|
|
155
155
|
`);
|
|
@@ -164,7 +164,7 @@ ${icon} ${f.rule} [${f.confidence}]${where}
|
|
|
164
164
|
if (f.fix) console.log(` \u2192 ${f.fix}`);
|
|
165
165
|
}
|
|
166
166
|
console.log(`
|
|
167
|
-
SUMMARY: ${summary.errors} errors, ${summary.warnings} warnings, ${summary.infos} infos (${summary.passedChecks}/${summary.totalChecks} checks passaram)`);
|
|
167
|
+
SUMMARY: ${summary.errors} errors, ${summary.warnings} warnings, ${summary.infos} infos (${summary.passedChecks}/${summary.totalChecks} checks FBA passaram)`);
|
|
168
168
|
console.log(`Olhei: ${summary.checked.join(", ")}`);
|
|
169
169
|
console.log(`Pulei: ${summary.skipped.join(" | ") || "nada"}
|
|
170
170
|
`);
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpServer,
|
|
3
3
|
startMcpServer
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-AXTYJMA7.js";
|
|
5
5
|
import {
|
|
6
6
|
AccessOriginSchema,
|
|
7
7
|
AuditYamlSchema,
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
runChecks,
|
|
34
34
|
scan,
|
|
35
35
|
validateClaimSamples
|
|
36
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-UKEKA2NQ.js";
|
|
37
37
|
import {
|
|
38
38
|
buildFullPath,
|
|
39
39
|
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-AXTYJMA7.js";
|
|
5
|
+
import "./chunk-UKEKA2NQ.js";
|
|
6
6
|
import "./chunk-7EWTBIKJ.js";
|
|
7
7
|
import "./chunk-Y4VXWSKX.js";
|
|
8
8
|
import "./chunk-XZ32YQQC.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-AXTYJMA7.js";
|
|
5
|
+
import "./chunk-UKEKA2NQ.js";
|
|
6
6
|
import "./chunk-7EWTBIKJ.js";
|
|
7
7
|
import "./chunk-Y4VXWSKX.js";
|
|
8
8
|
import "./chunk-XZ32YQQC.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.2",
|
|
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",
|