@justmpm/firebase-audit 0.5.2 → 0.5.4
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-UKEKA2NQ.js → chunk-AKPQ4OAJ.js} +144 -44
- package/dist/{chunk-7EWTBIKJ.js → chunk-FECVBQUO.js} +39 -10
- package/dist/{chunk-AXTYJMA7.js → chunk-RKQ3ZWWI.js} +17 -2
- package/dist/cli.js +25 -9
- package/dist/index.js +3 -3
- package/dist/mcp-cli.js +3 -3
- package/dist/mcp.js +3 -3
- package/dist/{rules-GTGZWN5U.js → rules-W4VUUQM4.js} +1 -1
- package/dist/scan-YUXXYBRU.js +7 -0
- package/package.json +1 -1
- package/skill/SKILL.md +7 -1
- package/dist/scan-OEN65GHM.js +0 -7
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
resolveHelperConditionDetailed,
|
|
10
10
|
stripOuterParens,
|
|
11
11
|
stripRuleComments
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-FECVBQUO.js";
|
|
13
13
|
|
|
14
14
|
// src/scan.ts
|
|
15
15
|
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
@@ -253,7 +253,7 @@ async function runChecks(model, rootDir, opts = {}) {
|
|
|
253
253
|
const declared = new Set(model.authorization.permissions.map((p) => p.name));
|
|
254
254
|
if (permissionCalls.literals.length === 0 && permissionCalls.dynamic.length === 0) {
|
|
255
255
|
const tenantHint = [...helperDetailed.keys()].find(
|
|
256
|
-
(n) => /tenant|isMember|
|
|
256
|
+
(n) => /tenant|isMember|_member/i.test(n)
|
|
257
257
|
);
|
|
258
258
|
findings.push(
|
|
259
259
|
make(
|
|
@@ -367,7 +367,7 @@ async function runChecks(model, rootDir, opts = {}) {
|
|
|
367
367
|
const condResolved = resolveHelperConditionDetailed(condRaw, helperDetailed);
|
|
368
368
|
const condInlined = inlineHelpersInCondition(condRaw, helperDetailed);
|
|
369
369
|
const cond = condResolved !== condRaw ? condResolved : condInlined !== condRaw ? condInlined : condRaw;
|
|
370
|
-
const isInvertedAdmin = /admin\s*==\s*false/i.test(cond) || /admin\s*!=\s*true/i.test(cond) || /admin\s*!==\s*true/i.test(cond) || /!\s*request\.auth\.token\.admin\b/i.test(cond) || /!\s*\(\s*request\.auth\.token\.admin\b/i.test(cond) || /!\s*\(
|
|
370
|
+
const isInvertedAdmin = /admin\s*==\s*false/i.test(cond) || /admin\s*!=\s*true/i.test(cond) || /admin\s*!==\s*true/i.test(cond) || /!\s*request\.auth\.token\.admin\b/i.test(cond) || /!\s*\(\s*request\.auth\.token\.admin\b/i.test(cond) || /!\s*\([^()]*admin\s*==\s*true/i.test(cond);
|
|
371
371
|
const normClaim = (c) => c.toLowerCase().replace(/[_-]/g, "");
|
|
372
372
|
const isAdminCall = /\bisadmin\s*\(([^)]*)\)/i.exec(condRaw) ?? /\bisadmin\s*\(([^)]*)\)/i.exec(cond);
|
|
373
373
|
let isAdminBodyHasAdmin = false;
|
|
@@ -511,6 +511,22 @@ async function runChecks(model, rootDir, opts = {}) {
|
|
|
511
511
|
}
|
|
512
512
|
)
|
|
513
513
|
);
|
|
514
|
+
} else if (origin === "UNKNOWN" && !isTestPath(hit.file)) {
|
|
515
|
+
findings.push(
|
|
516
|
+
make(
|
|
517
|
+
"ADMIN_ORIGIN_UNKNOWN",
|
|
518
|
+
"INFO",
|
|
519
|
+
"UNKNOWN",
|
|
520
|
+
`Admin SDK em arquivo de origem indefinida (${hit.file}). Se for cliente, Rules n\xE3o valem \u2014 revise manualmente.`,
|
|
521
|
+
`ADMIN_ORIGIN_UNKNOWN:${hit.file}:${hit.line}`,
|
|
522
|
+
{
|
|
523
|
+
file: hit.file,
|
|
524
|
+
line: hit.line,
|
|
525
|
+
evidence: [ev("admin-import-unknown", hit.text, "UNKNOWN", hit.file, hit.line)],
|
|
526
|
+
fix: "Mova para functions/, server/ ou scripts/ se for server; se for cliente, troque por Client SDK."
|
|
527
|
+
}
|
|
528
|
+
)
|
|
529
|
+
);
|
|
514
530
|
}
|
|
515
531
|
}
|
|
516
532
|
return findings;
|
|
@@ -844,9 +860,10 @@ async function scan(rootDir, opts = {}) {
|
|
|
844
860
|
try {
|
|
845
861
|
if (d.firebaseJson && existsSync3(d.firebaseJson)) {
|
|
846
862
|
const raw = JSON.parse(readFileSync3(d.firebaseJson, "utf-8"));
|
|
847
|
-
for (const [kind, rel] of [
|
|
848
|
-
["rules", raw.firestore?.rules],
|
|
849
|
-
["indexes", raw.firestore?.indexes]
|
|
863
|
+
for (const [kind, rel, label] of [
|
|
864
|
+
["rules", raw.firestore?.rules, "firestore.rules"],
|
|
865
|
+
["indexes", raw.firestore?.indexes, "firestore.indexes"],
|
|
866
|
+
["storage-rules", raw.storage?.rules, "storage.rules"]
|
|
850
867
|
]) {
|
|
851
868
|
if (typeof rel === "string" && rel.length > 0) {
|
|
852
869
|
if (!existsSync3(join3(rootDir, rel))) {
|
|
@@ -854,7 +871,7 @@ async function scan(rootDir, opts = {}) {
|
|
|
854
871
|
rule: "RULES_CONFIG_MISMATCH",
|
|
855
872
|
severity: "WARNING",
|
|
856
873
|
confidence: "CONFIRMED",
|
|
857
|
-
message: `firebase.json aponta
|
|
874
|
+
message: `firebase.json aponta ${label} para "${rel}" inexistente \u2014 usando default. Corrija o path.`,
|
|
858
875
|
fingerprint: `RULES_CONFIG_MISMATCH:${kind}:${rel}`,
|
|
859
876
|
evidence: [
|
|
860
877
|
{ id: `ev-config-${kind}`, kind: "config-mismatch", summary: rel, confidence: "CONFIRMED" }
|
|
@@ -1078,8 +1095,21 @@ async function scan(rootDir, opts = {}) {
|
|
|
1078
1095
|
}
|
|
1079
1096
|
const checksFindings = await runChecks(model, rootDir, { adapterFn: opts.adapterFn ?? model.authorization.adapter ?? void 0 });
|
|
1080
1097
|
const findings = [...findings_pre, ...checksFindings];
|
|
1081
|
-
const checked = [
|
|
1098
|
+
const checked = [];
|
|
1099
|
+
if (d.firestoreRulesFile && existsSync3(d.firestoreRulesFile)) checked.push("firestore.rules");
|
|
1100
|
+
if (d.firestoreIndexesFile && existsSync3(d.firestoreIndexesFile)) checked.push("indexes");
|
|
1101
|
+
if (d.auditYamlFile && existsSync3(d.auditYamlFile)) checked.push("contract");
|
|
1102
|
+
checked.push("client-code");
|
|
1082
1103
|
const skipped = [];
|
|
1104
|
+
if (!d.firestoreRulesFile || !existsSync3(d.firestoreRulesFile)) {
|
|
1105
|
+
skipped.push("firestore.rules n\xE3o observado \u2014 FBA001/FBA002 sem cobertura");
|
|
1106
|
+
}
|
|
1107
|
+
if (!d.firestoreIndexesFile || !existsSync3(d.firestoreIndexesFile)) {
|
|
1108
|
+
skipped.push("firestore.indexes.json n\xE3o observado \u2014 drift de \xEDndices sem base local");
|
|
1109
|
+
}
|
|
1110
|
+
if (!d.auditYamlFile || !existsSync3(d.auditYamlFile)) {
|
|
1111
|
+
skipped.push("firebase-audit.yaml n\xE3o observado \u2014 permiss\xF5es seguem UNKNOWN sem contrato");
|
|
1112
|
+
}
|
|
1083
1113
|
if (d.storageRulesFile && existsSync3(d.storageRulesFile)) {
|
|
1084
1114
|
checked.push("storage.rules");
|
|
1085
1115
|
try {
|
|
@@ -1092,46 +1122,97 @@ async function scan(rootDir, opts = {}) {
|
|
|
1092
1122
|
return `ev-storage-${prefix}-${evCounter}`;
|
|
1093
1123
|
};
|
|
1094
1124
|
for (const m of parseStorageAllows(clean)) {
|
|
1095
|
-
if (!isOpenStorageCondition(m.condition)) continue;
|
|
1096
1125
|
const ops = expandStorageOps(m.target);
|
|
1097
1126
|
const key = conditionKeyForFingerprint(m.condition);
|
|
1098
|
-
const
|
|
1127
|
+
const normCond = (m.condition ?? "").toLowerCase().replace(/\s+/g, "");
|
|
1128
|
+
const hasAuth = normCond.includes("request.auth");
|
|
1129
|
+
const bareAuth = (/request\.auth(!==|!=)null/.test(normCond) || /request\.auth\.uid(!==|!=)null/.test(normCond)) && !normCond.includes("request.auth.token") && !normCond.includes("sign_in_provider");
|
|
1130
|
+
const wildcard = /\{\s*\w+\s*=\s*\*\*\s*\}/.test(m.matchPath);
|
|
1099
1131
|
const where = `${relStorage}:${m.line} (${m.matchPath}, allow ${m.target})`;
|
|
1100
|
-
if (
|
|
1132
|
+
if (isOpenStorageCondition(m.condition)) {
|
|
1133
|
+
if (ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete")) {
|
|
1134
|
+
findings.push({
|
|
1135
|
+
rule: "STORAGE_PUBLIC_WRITE",
|
|
1136
|
+
severity: "ERROR",
|
|
1137
|
+
confidence: "CONFIRMED",
|
|
1138
|
+
message: `Storage com escrita aberta (${where}). Qualquer cliente pode escrever \u2014 cofre aberto, n\xE3o vitrine.`,
|
|
1139
|
+
fingerprint: `STORAGE_PUBLIC_WRITE:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
|
|
1140
|
+
file: relStorage,
|
|
1141
|
+
line: m.line,
|
|
1142
|
+
evidence: [{ id: evId("write"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "CONFIRMED", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
|
|
1143
|
+
fix: "Exigir request.auth (e dono/tenant) no write. Deixe get aberto s\xF3 em pasta de vitrine como logos/."
|
|
1144
|
+
});
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
if (ops.has("read") || ops.has("get") || ops.has("list")) {
|
|
1148
|
+
if (wildcard) {
|
|
1149
|
+
findings.push({
|
|
1150
|
+
rule: "STORAGE_ALL_PATHS_OPEN",
|
|
1151
|
+
severity: "WARNING",
|
|
1152
|
+
confidence: "PROBABLE",
|
|
1153
|
+
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}.`,
|
|
1154
|
+
fingerprint: `STORAGE_ALL_PATHS_OPEN:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
|
|
1155
|
+
file: relStorage,
|
|
1156
|
+
line: m.line,
|
|
1157
|
+
evidence: [{ id: evId("all"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
|
|
1158
|
+
fix: "Troque o curinga total por pastas expl\xEDcitas (ex: /logos/{logoId}) com get: if true e write restrito."
|
|
1159
|
+
});
|
|
1160
|
+
} else {
|
|
1161
|
+
const enumerates = ops.has("read") || ops.has("list");
|
|
1162
|
+
findings.push({
|
|
1163
|
+
rule: "STORAGE_PUBLIC_READ",
|
|
1164
|
+
severity: "INFO",
|
|
1165
|
+
confidence: "PROBABLE",
|
|
1166
|
+
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.`,
|
|
1167
|
+
fingerprint: `STORAGE_PUBLIC_READ:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
|
|
1168
|
+
file: relStorage,
|
|
1169
|
+
line: m.line,
|
|
1170
|
+
evidence: [{ id: evId("read"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
|
|
1171
|
+
fix: enumerates ? "Para vitrine, troque allow read por allow get (sem list) na pasta p\xFAblica." : void 0
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
if (bareAuth && (ops.has("read") || ops.has("get") || ops.has("list") || ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete"))) {
|
|
1101
1178
|
findings.push({
|
|
1102
|
-
rule: "
|
|
1103
|
-
severity: "
|
|
1104
|
-
confidence: "
|
|
1105
|
-
message: `Storage
|
|
1106
|
-
fingerprint: `
|
|
1179
|
+
rule: "STORAGE_ANON_ALLOWED",
|
|
1180
|
+
severity: "INFO",
|
|
1181
|
+
confidence: "PROBABLE",
|
|
1182
|
+
message: `Storage aceita qualquer autenticado incluindo an\xF4nimo (${where}). Exija claim/provider para dados sens\xEDveis.`,
|
|
1183
|
+
fingerprint: `STORAGE_ANON_ALLOWED:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
|
|
1107
1184
|
file: relStorage,
|
|
1108
1185
|
line: m.line,
|
|
1109
|
-
evidence: [{ id: evId("
|
|
1110
|
-
fix: "Exigir
|
|
1186
|
+
evidence: [{ id: evId("anon"), kind: "anon-allowed", summary: `${m.matchPath} allow ${m.target}`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
|
|
1187
|
+
fix: "Exigir claim (token.admin/role) ou provider diferente de anonymous para dados sens\xEDveis."
|
|
1111
1188
|
});
|
|
1112
|
-
|
|
1113
|
-
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
if (!hasAuth && m.condition !== void 0) {
|
|
1192
|
+
const canWrite = ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete");
|
|
1193
|
+
const canRead = ops.has("read") || ops.has("get") || ops.has("list");
|
|
1194
|
+
if (canWrite) {
|
|
1114
1195
|
findings.push({
|
|
1115
|
-
rule: "
|
|
1116
|
-
severity: "
|
|
1196
|
+
rule: "STORAGE_PUBLIC_WRITE",
|
|
1197
|
+
severity: "ERROR",
|
|
1117
1198
|
confidence: "PROBABLE",
|
|
1118
|
-
message: `Storage com
|
|
1119
|
-
fingerprint: `
|
|
1199
|
+
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.`,
|
|
1200
|
+
fingerprint: `STORAGE_PUBLIC_WRITE:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
|
|
1120
1201
|
file: relStorage,
|
|
1121
1202
|
line: m.line,
|
|
1122
|
-
evidence: [{ id: evId("
|
|
1123
|
-
fix: "
|
|
1203
|
+
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 } } }],
|
|
1204
|
+
fix: "Exigir request.auth (e dono/tenant) em qualquer condi\xE7\xE3o de write."
|
|
1124
1205
|
});
|
|
1125
|
-
} else {
|
|
1206
|
+
} else if (canRead) {
|
|
1126
1207
|
findings.push({
|
|
1127
1208
|
rule: "STORAGE_PUBLIC_READ",
|
|
1128
1209
|
severity: "INFO",
|
|
1129
1210
|
confidence: "PROBABLE",
|
|
1130
|
-
message: `Storage com leitura
|
|
1131
|
-
fingerprint: `STORAGE_PUBLIC_READ:${m.matchPath}:${m.target}:${key}`,
|
|
1211
|
+
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.`,
|
|
1212
|
+
fingerprint: `STORAGE_PUBLIC_READ:${m.matchPath}:${normalizeTarget(m.target)}:${key}`,
|
|
1132
1213
|
file: relStorage,
|
|
1133
1214
|
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 } } }]
|
|
1215
|
+
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 } } }]
|
|
1135
1216
|
});
|
|
1136
1217
|
}
|
|
1137
1218
|
}
|
|
@@ -1166,7 +1247,7 @@ async function scan(rootDir, opts = {}) {
|
|
|
1166
1247
|
evidence: [{ id: "ev-appcheck", kind: "appcheck", summary: `${appcheck.occurrences} ocorr\xEAncias`, confidence: "CONFIRMED" }]
|
|
1167
1248
|
});
|
|
1168
1249
|
} 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)");
|
|
1250
|
+
skipped.push("functions sem enforceAppCheck literal nem verifyToken observado \u2014 AppCheck sem cobertura afirmativa (config via vari\xE1vel conta como n\xE3o observada; onRequest exige verifica\xE7\xE3o manual por rota)");
|
|
1170
1251
|
}
|
|
1171
1252
|
if (appcheck.limitHit) {
|
|
1172
1253
|
skipped.push("functions: teto de varredura atingido \u2014 AppCheck parcial neste scan");
|
|
@@ -1234,7 +1315,7 @@ async function countAppCheckEnforced(rootDir) {
|
|
|
1234
1315
|
try {
|
|
1235
1316
|
const { readdirSync, readFileSync: readSync, statSync: stat } = await import("fs");
|
|
1236
1317
|
const { join: joinP } = await import("path");
|
|
1237
|
-
const { stripRuleComments: strip } = await import("./rules-
|
|
1318
|
+
const { stripRuleComments: strip } = await import("./rules-W4VUUQM4.js");
|
|
1238
1319
|
const roots = [joinP(rootDir, "functions")];
|
|
1239
1320
|
let occurrences = 0;
|
|
1240
1321
|
const files = /* @__PURE__ */ new Set();
|
|
@@ -1244,7 +1325,7 @@ async function countAppCheckEnforced(rootDir) {
|
|
|
1244
1325
|
let guard = 0;
|
|
1245
1326
|
const isTestFile = (p) => {
|
|
1246
1327
|
const n = p.replace(/\\/g, "/");
|
|
1247
|
-
return /\.test\.[tj]sx?$/.test(n) || /\.spec\.[tj]sx?$/.test(n) || /(^|\/)__tests__\//.test(n);
|
|
1328
|
+
return /\.test\.[tj]sx?$/.test(n) || /\.spec\.[tj]sx?$/.test(n) || /(^|\/)__tests__\//.test(n) || /(^|\/)(test|tests|testing)\//.test(n);
|
|
1248
1329
|
};
|
|
1249
1330
|
while (stack.length > 0 && guard < 200) {
|
|
1250
1331
|
guard += 1;
|
|
@@ -1270,16 +1351,22 @@ async function countAppCheckEnforced(rootDir) {
|
|
|
1270
1351
|
stack.push(abs);
|
|
1271
1352
|
continue;
|
|
1272
1353
|
}
|
|
1273
|
-
if (!/\.(ts|js|mjs|cjs)$/.test(name)) continue;
|
|
1354
|
+
if (!/\.(ts|js|mjs|cjs|py)$/.test(name)) continue;
|
|
1274
1355
|
if (isTestFile(abs)) continue;
|
|
1275
1356
|
try {
|
|
1276
|
-
|
|
1277
|
-
|
|
1357
|
+
let text = strip(readSync(abs, "utf-8"));
|
|
1358
|
+
if (name.endsWith(".py")) {
|
|
1359
|
+
text = text.split("\n").map((l) => {
|
|
1360
|
+
const idx = l.indexOf("#");
|
|
1361
|
+
return idx === -1 ? l : l.slice(0, idx);
|
|
1362
|
+
}).join("\n");
|
|
1363
|
+
}
|
|
1364
|
+
const hits = text.match(/enforceAppCheck\s*:\s*true|enforce_app_check\s*=\s*True/g);
|
|
1278
1365
|
if (hits) {
|
|
1279
1366
|
occurrences += hits.length;
|
|
1280
1367
|
files.add(abs);
|
|
1281
1368
|
}
|
|
1282
|
-
const manualHits = text.match(/verifyToken\s*\(/g);
|
|
1369
|
+
const manualHits = text.match(/verifyToken\s*\(|verify_token\s*\(|consumeAppCheckToken|getLimitedUseToken|getAppCheck\s*\(/g);
|
|
1283
1370
|
if (manualHits) manual += manualHits.length;
|
|
1284
1371
|
} catch {
|
|
1285
1372
|
}
|
|
@@ -1294,6 +1381,8 @@ function isOpenStorageCondition(condition) {
|
|
|
1294
1381
|
if (condition === void 0) return true;
|
|
1295
1382
|
const norm = stripOuterParens(condition).toLowerCase().replace(/\s+/g, "");
|
|
1296
1383
|
if (norm === "" || norm === "true") return true;
|
|
1384
|
+
if (/^!\(.*request\.auth(===|==)null/.test(norm)) return false;
|
|
1385
|
+
if (/^!\(.*request\.auth(!==|!=)null/.test(norm)) return true;
|
|
1297
1386
|
if (/(^|\|\|)true($|\|\|)/.test(norm)) return true;
|
|
1298
1387
|
if (norm.includes("request.auth==null") || norm.includes("request.auth===null")) return true;
|
|
1299
1388
|
return false;
|
|
@@ -1304,13 +1393,24 @@ function parseStorageAllows(clean) {
|
|
|
1304
1393
|
const matchRe = /match\s+/g;
|
|
1305
1394
|
let mm;
|
|
1306
1395
|
while ((mm = matchRe.exec(clean)) !== null) {
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1396
|
+
const from = mm.index + mm[0].length;
|
|
1397
|
+
let openBrace = -1;
|
|
1398
|
+
let scan2 = from;
|
|
1399
|
+
while (scan2 < clean.length) {
|
|
1400
|
+
const b = clean.indexOf("{", scan2);
|
|
1401
|
+
if (b === -1) break;
|
|
1402
|
+
const between = clean.slice(from, b);
|
|
1403
|
+
if (/match\s|allow\s|;/.test(between)) break;
|
|
1404
|
+
const after = clean.slice(b + 1, b + 12).trimStart().replace(/^\n+/, "");
|
|
1405
|
+
const eol = clean.slice(b + 1).match(/^[ \t]*(\n|$)/) !== null;
|
|
1406
|
+
if (/^(allow|match|function|\})/.test(after) || eol) {
|
|
1407
|
+
openBrace = b;
|
|
1408
|
+
break;
|
|
1409
|
+
}
|
|
1410
|
+
scan2 = b + 1;
|
|
1411
|
+
}
|
|
1412
|
+
if (openBrace === -1) continue;
|
|
1413
|
+
const path = clean.slice(from, openBrace).trim() || "/(unknown)";
|
|
1314
1414
|
let depth = 0;
|
|
1315
1415
|
let quote = null;
|
|
1316
1416
|
let end = clean.length;
|
|
@@ -162,21 +162,47 @@ function inlineHelpersInCondition(condition, detailed) {
|
|
|
162
162
|
}
|
|
163
163
|
return out;
|
|
164
164
|
}
|
|
165
|
+
function isSingleCall(condition) {
|
|
166
|
+
const m = /^([A-Za-z_][A-Za-z0-9_]*)\s*\(/.exec(condition.trim());
|
|
167
|
+
if (!m) return null;
|
|
168
|
+
const open = condition.indexOf("(", m[0].length - 1);
|
|
169
|
+
let depth = 0;
|
|
170
|
+
let quote = null;
|
|
171
|
+
for (let i = open; i < condition.length; i++) {
|
|
172
|
+
const c = condition[i];
|
|
173
|
+
if (quote) {
|
|
174
|
+
if (c === quote && condition[i - 1] !== "\\") quote = null;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
178
|
+
quote = c;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (c === "(") depth += 1;
|
|
182
|
+
else if (c === ")") {
|
|
183
|
+
depth -= 1;
|
|
184
|
+
if (depth === 0) {
|
|
185
|
+
const rest = condition.slice(i + 1).replace(/;$/, "").trim();
|
|
186
|
+
if (rest !== "") return null;
|
|
187
|
+
return { name: m[1], args: condition.slice(open + 1, i) };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
165
193
|
function resolveHelperCondition(condition, functions) {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const body = functions.get(call[1]);
|
|
194
|
+
const single = isSingleCall(condition);
|
|
195
|
+
if (!single) return condition;
|
|
196
|
+
const body = functions.get(single.name);
|
|
170
197
|
if (body === void 0) return condition;
|
|
171
198
|
return body;
|
|
172
199
|
}
|
|
173
200
|
function resolveHelperConditionDetailed(condition, detailed) {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const fn = detailed.get(call[1]);
|
|
201
|
+
const single = isSingleCall(condition);
|
|
202
|
+
if (!single) return condition;
|
|
203
|
+
const fn = detailed.get(single.name);
|
|
178
204
|
if (!fn) return condition;
|
|
179
|
-
const args =
|
|
205
|
+
const args = single.args.length === 0 ? [] : single.args.split(",").map((s) => s.trim());
|
|
180
206
|
return inlineHelperArgs(fn.body, fn.params, args);
|
|
181
207
|
}
|
|
182
208
|
function extractRules(rulesFile, relFile) {
|
|
@@ -275,7 +301,10 @@ function isPublicCondition(condition) {
|
|
|
275
301
|
return { isPublic: false, confidence: "CONFIRMED" };
|
|
276
302
|
}
|
|
277
303
|
if (low === "true") return { isPublic: true, confidence: "CONFIRMED" };
|
|
278
|
-
|
|
304
|
+
for (const part of low.split("||")) {
|
|
305
|
+
const branch = stripOuterParens(part.replace(/return/g, "").replace(/;/g, "").trim());
|
|
306
|
+
if (branch === "true") return { isPublic: true, confidence: "PROBABLE" };
|
|
307
|
+
}
|
|
279
308
|
if (low.includes("request.auth==null") || low.includes("request.auth===null")) {
|
|
280
309
|
return { isPublic: true, confidence: "CONFIRMED" };
|
|
281
310
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
scan
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-AKPQ4OAJ.js";
|
|
4
4
|
import {
|
|
5
5
|
verify
|
|
6
6
|
} from "./chunk-Y4VXWSKX.js";
|
|
@@ -28,6 +28,16 @@ function readPkgText(rel) {
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
function assertCwd(cwd) {
|
|
32
|
+
const { existsSync: existsSync2, statSync } = require2("node:fs");
|
|
33
|
+
let ok = false;
|
|
34
|
+
try {
|
|
35
|
+
ok = existsSync2(cwd) && statSync(cwd).isDirectory();
|
|
36
|
+
} catch {
|
|
37
|
+
ok = false;
|
|
38
|
+
}
|
|
39
|
+
if (!ok) throw new Error(`Diret\xF3rio n\xE3o existe: ${cwd}`);
|
|
40
|
+
}
|
|
31
41
|
var FALLBACK_SKILL = [
|
|
32
42
|
"# firebase-audit-skill (fallback)",
|
|
33
43
|
"",
|
|
@@ -79,6 +89,7 @@ function createMcpServer() {
|
|
|
79
89
|
},
|
|
80
90
|
async ({ cwd, adapter, strict }) => {
|
|
81
91
|
try {
|
|
92
|
+
assertCwd(cwd);
|
|
82
93
|
const res = await scan(cwd, { adapterFn: adapter, strict });
|
|
83
94
|
return {
|
|
84
95
|
content: [
|
|
@@ -108,6 +119,7 @@ function createMcpServer() {
|
|
|
108
119
|
},
|
|
109
120
|
async ({ cwd, adapter }) => {
|
|
110
121
|
try {
|
|
122
|
+
assertCwd(cwd);
|
|
111
123
|
const res = await scan(cwd, { adapterFn: adapter, strict: true });
|
|
112
124
|
return {
|
|
113
125
|
content: [
|
|
@@ -115,7 +127,8 @@ function createMcpServer() {
|
|
|
115
127
|
type: "text",
|
|
116
128
|
text: JSON.stringify({ summary: res.summary, findings: res.findings }, null, 2)
|
|
117
129
|
}
|
|
118
|
-
]
|
|
130
|
+
],
|
|
131
|
+
isError: res.summary.errors > 0
|
|
119
132
|
};
|
|
120
133
|
} catch (err) {
|
|
121
134
|
return {
|
|
@@ -137,6 +150,7 @@ function createMcpServer() {
|
|
|
137
150
|
},
|
|
138
151
|
async ({ cwd, coverage }) => {
|
|
139
152
|
try {
|
|
153
|
+
assertCwd(cwd);
|
|
140
154
|
const { resolve } = await import("path");
|
|
141
155
|
const { model } = await scan(cwd);
|
|
142
156
|
const res = verify(model, { coverageFile: coverage ? resolve(cwd, coverage) : void 0 });
|
|
@@ -187,6 +201,7 @@ function createMcpServer() {
|
|
|
187
201
|
try {
|
|
188
202
|
const { resolve: resolveP, isAbsolute } = await import("path");
|
|
189
203
|
const base = cwd && cwd.length > 0 ? cwd : process.cwd();
|
|
204
|
+
assertCwd(base);
|
|
190
205
|
const resPath = (p) => {
|
|
191
206
|
if (p === null || p === void 0) return null;
|
|
192
207
|
return isAbsolute(p) ? p : resolveP(base, p);
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
scan
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-AKPQ4OAJ.js";
|
|
5
|
+
import "./chunk-FECVBQUO.js";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
8
8
|
import { createRequire } from "module";
|
|
@@ -32,8 +32,6 @@ Exit codes: 0 ok informativo; 2 quando --strict/check encontra ERROR. scan sozin
|
|
|
32
32
|
Uso: firebase-audit scan [--json] [--strict] [--adapter=fn] [--cwd=path]`);
|
|
33
33
|
process.exit(1);
|
|
34
34
|
}
|
|
35
|
-
const strict = args.includes("--strict") || cmd === "check";
|
|
36
|
-
const asJson = args.includes("--json") || args.includes("--json=true");
|
|
37
35
|
const getArg = (name) => {
|
|
38
36
|
const pref = `--${name}=`;
|
|
39
37
|
const hit = args.find((a) => a.startsWith(pref));
|
|
@@ -42,6 +40,18 @@ Uso: firebase-audit scan [--json] [--strict] [--adapter=fn] [--cwd=path]`);
|
|
|
42
40
|
if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith("--")) return args[idx + 1];
|
|
43
41
|
return void 0;
|
|
44
42
|
};
|
|
43
|
+
const strictArg = getArg("strict");
|
|
44
|
+
const strict = args.includes("--strict") || strictArg === "true" || cmd === "check";
|
|
45
|
+
if (strictArg !== void 0 && strictArg !== "true" && strictArg !== "false") {
|
|
46
|
+
console.error(`Valor inv\xE1lido para --strict: ${strictArg} (use --strict, --strict=true ou --strict=false)`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const asJsonArg = getArg("json");
|
|
50
|
+
const asJson = args.includes("--json") || asJsonArg === "true";
|
|
51
|
+
if (asJsonArg !== void 0 && asJsonArg !== "true" && asJsonArg !== "false") {
|
|
52
|
+
console.error(`Valor inv\xE1lido para --json: ${asJsonArg} (use --json, --json=true ou --json=false)`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
45
55
|
const knownFlags = /* @__PURE__ */ new Set(["--json", "--strict", "--help", "-h", "help", "--version", "-v", "--coverage", "--local", "--remote"]);
|
|
46
56
|
for (const a of args.slice(1)) {
|
|
47
57
|
const base = a.includes("=") ? a.slice(0, a.indexOf("=")) : a;
|
|
@@ -54,18 +64,24 @@ Uso: firebase-audit scan|check|verify|drift [--json] ...`);
|
|
|
54
64
|
const adapterArg = getArg("adapter");
|
|
55
65
|
const cwdArg = getArg("cwd");
|
|
56
66
|
if (cwdArg) {
|
|
57
|
-
const { existsSync: existsCwd } = await import("fs");
|
|
67
|
+
const { existsSync: existsCwd, statSync: statCwd } = await import("fs");
|
|
58
68
|
const { resolve: resolveCwd } = await import("path");
|
|
59
69
|
const abs = resolveCwd(process.cwd(), cwdArg);
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
let isDir = false;
|
|
71
|
+
try {
|
|
72
|
+
isDir = existsCwd(abs) && statCwd(abs).isDirectory();
|
|
73
|
+
} catch {
|
|
74
|
+
isDir = false;
|
|
75
|
+
}
|
|
76
|
+
if (!isDir) {
|
|
77
|
+
console.error(`Diret\xF3rio --cwd n\xE3o existe ou n\xE3o \xE9 diret\xF3rio: ${cwdArg}`);
|
|
62
78
|
process.exit(1);
|
|
63
79
|
}
|
|
64
80
|
}
|
|
65
81
|
const { resolve: resolveRoot } = await import("path");
|
|
66
82
|
const rootDir = cwdArg ? resolveRoot(process.cwd(), cwdArg) : process.cwd();
|
|
67
83
|
if (cmd === "verify") {
|
|
68
|
-
const { scan: scanForVerify } = await import("./scan-
|
|
84
|
+
const { scan: scanForVerify } = await import("./scan-YUXXYBRU.js");
|
|
69
85
|
const { verify } = await import("./verify-WBVSESHD.js");
|
|
70
86
|
const { resolve: resolveVerify } = await import("path");
|
|
71
87
|
const coverageArg = getArg("coverage");
|
|
@@ -164,7 +180,7 @@ ${icon} ${f.rule} [${f.confidence}]${where}
|
|
|
164
180
|
if (f.fix) console.log(` \u2192 ${f.fix}`);
|
|
165
181
|
}
|
|
166
182
|
console.log(`
|
|
167
|
-
SUMMARY: ${summary.errors} errors, ${summary.warnings} warnings, ${summary.infos} infos (${summary.passedChecks}/${summary.totalChecks} checks FBA passaram)`);
|
|
183
|
+
SUMMARY: ${summary.errors} errors, ${summary.warnings} warnings, ${summary.infos} infos (${summary.passedChecks}/${summary.totalChecks} checks FBA passaram${summary.infra.length > 0 ? `, ${summary.infra.length} achado(s) fora dos FBA` : ""})`);
|
|
168
184
|
console.log(`Olhei: ${summary.checked.join(", ")}`);
|
|
169
185
|
console.log(`Pulei: ${summary.skipped.join(" | ") || "nada"}
|
|
170
186
|
`);
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpServer,
|
|
3
3
|
startMcpServer
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-RKQ3ZWWI.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-AKPQ4OAJ.js";
|
|
37
37
|
import {
|
|
38
38
|
buildFullPath,
|
|
39
39
|
conditionKeyForFingerprint,
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
resolveHelperConditionDetailed,
|
|
56
56
|
stripOuterParens,
|
|
57
57
|
stripRuleComments
|
|
58
|
-
} from "./chunk-
|
|
58
|
+
} from "./chunk-FECVBQUO.js";
|
|
59
59
|
import {
|
|
60
60
|
planVerify,
|
|
61
61
|
verify
|
package/dist/mcp-cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
startMcpServer
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-RKQ3ZWWI.js";
|
|
5
|
+
import "./chunk-AKPQ4OAJ.js";
|
|
6
|
+
import "./chunk-FECVBQUO.js";
|
|
7
7
|
import "./chunk-Y4VXWSKX.js";
|
|
8
8
|
import "./chunk-XZ32YQQC.js";
|
|
9
9
|
|
package/dist/mcp.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpServer,
|
|
3
3
|
startMcpServer
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-RKQ3ZWWI.js";
|
|
5
|
+
import "./chunk-AKPQ4OAJ.js";
|
|
6
|
+
import "./chunk-FECVBQUO.js";
|
|
7
7
|
import "./chunk-Y4VXWSKX.js";
|
|
8
8
|
import "./chunk-XZ32YQQC.js";
|
|
9
9
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justmpm/firebase-audit",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
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
|
@@ -28,5 +28,11 @@ Skill para agentes usarem o `@justmpm/firebase-audit` do jeito certo.
|
|
|
28
28
|
firebase emulators:exec --only firestore "npm test"
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Matriz `
|
|
31
|
+
Matriz papel×operação×path (`read` = `get` + `list`) com `@firebase/rules-unit-testing`.
|
|
32
32
|
Cobertura em `ruleCoverage` (JSON). Emulator não prova índice. Sem contrato, papéis reduzidos (anonymous/user/admin).
|
|
33
|
+
Use `drift` para comparar índices locais vs implantados (MATCHED/LOCAL_ONLY/REMOTE_ONLY).
|
|
34
|
+
|
|
35
|
+
## Limites honestos
|
|
36
|
+
|
|
37
|
+
- AppCheck: varredura TS/Python por texto (`enforceAppCheck`, `verifyToken` e afins). Config via variável conta como não observada; `onRequest` exige revisão rota a rota.
|
|
38
|
+
- Isolamento por tenant (ownerId/tenantId): fora dos checks — revise manualmente ou no Emulator.
|