@justmpm/firebase-audit 0.5.1 → 0.5.3
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-GDO2DLNQ.js → chunk-4BBFPTOR.js} +187 -53
- package/dist/{chunk-QACXCMYB.js → chunk-PWJ4DI24.js} +16 -2
- package/dist/cli.js +11 -6
- 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-LJ3TCHMR.js} +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +7 -1
|
@@ -844,9 +844,10 @@ async function scan(rootDir, opts = {}) {
|
|
|
844
844
|
try {
|
|
845
845
|
if (d.firebaseJson && existsSync3(d.firebaseJson)) {
|
|
846
846
|
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]
|
|
847
|
+
for (const [kind, rel, label] of [
|
|
848
|
+
["rules", raw.firestore?.rules, "firestore.rules"],
|
|
849
|
+
["indexes", raw.firestore?.indexes, "firestore.indexes"],
|
|
850
|
+
["storage-rules", raw.storage?.rules, "storage.rules"]
|
|
850
851
|
]) {
|
|
851
852
|
if (typeof rel === "string" && rel.length > 0) {
|
|
852
853
|
if (!existsSync3(join3(rootDir, rel))) {
|
|
@@ -854,7 +855,7 @@ async function scan(rootDir, opts = {}) {
|
|
|
854
855
|
rule: "RULES_CONFIG_MISMATCH",
|
|
855
856
|
severity: "WARNING",
|
|
856
857
|
confidence: "CONFIRMED",
|
|
857
|
-
message: `firebase.json aponta
|
|
858
|
+
message: `firebase.json aponta ${label} para "${rel}" inexistente \u2014 usando default. Corrija o path.`,
|
|
858
859
|
fingerprint: `RULES_CONFIG_MISMATCH:${kind}:${rel}`,
|
|
859
860
|
evidence: [
|
|
860
861
|
{ id: `ev-config-${kind}`, kind: "config-mismatch", summary: rel, confidence: "CONFIRMED" }
|
|
@@ -866,6 +867,16 @@ async function scan(rootDir, opts = {}) {
|
|
|
866
867
|
}
|
|
867
868
|
}
|
|
868
869
|
} catch {
|
|
870
|
+
findings_pre.push({
|
|
871
|
+
rule: "FIREBASE_JSON_INVALID",
|
|
872
|
+
severity: "WARNING",
|
|
873
|
+
confidence: "CONFIRMED",
|
|
874
|
+
message: "firebase.json inv\xE1lido \u2014 seguindo com defaults (firestore.rules, firestore.indexes.json). Corrija o JSON.",
|
|
875
|
+
fingerprint: "FIREBASE_JSON_INVALID",
|
|
876
|
+
evidence: [
|
|
877
|
+
{ id: "ev-firebase-json", kind: "config-parse", summary: "JSON inv\xE1lido", confidence: "CONFIRMED" }
|
|
878
|
+
]
|
|
879
|
+
});
|
|
869
880
|
}
|
|
870
881
|
if (!d.firestoreRulesFile || !existsSync3(d.firestoreRulesFile)) {
|
|
871
882
|
findings_pre.push({
|
|
@@ -1068,56 +1079,111 @@ async function scan(rootDir, opts = {}) {
|
|
|
1068
1079
|
}
|
|
1069
1080
|
const checksFindings = await runChecks(model, rootDir, { adapterFn: opts.adapterFn ?? model.authorization.adapter ?? void 0 });
|
|
1070
1081
|
const findings = [...findings_pre, ...checksFindings];
|
|
1071
|
-
const checked = [
|
|
1082
|
+
const checked = [];
|
|
1083
|
+
if (d.firestoreRulesFile && existsSync3(d.firestoreRulesFile)) checked.push("firestore.rules");
|
|
1084
|
+
if (d.firestoreIndexesFile && existsSync3(d.firestoreIndexesFile)) checked.push("indexes");
|
|
1085
|
+
if (d.auditYamlFile && existsSync3(d.auditYamlFile)) checked.push("contract");
|
|
1086
|
+
checked.push("client-code");
|
|
1072
1087
|
const skipped = [];
|
|
1088
|
+
if (!d.firestoreRulesFile || !existsSync3(d.firestoreRulesFile)) {
|
|
1089
|
+
skipped.push("firestore.rules n\xE3o observado \u2014 FBA001/FBA002 sem cobertura");
|
|
1090
|
+
}
|
|
1091
|
+
if (!d.firestoreIndexesFile || !existsSync3(d.firestoreIndexesFile)) {
|
|
1092
|
+
skipped.push("firestore.indexes.json n\xE3o observado \u2014 drift de \xEDndices sem base local");
|
|
1093
|
+
}
|
|
1094
|
+
if (!d.auditYamlFile || !existsSync3(d.auditYamlFile)) {
|
|
1095
|
+
skipped.push("firebase-audit.yaml n\xE3o observado \u2014 permiss\xF5es seguem UNKNOWN sem contrato");
|
|
1096
|
+
}
|
|
1073
1097
|
if (d.storageRulesFile && existsSync3(d.storageRulesFile)) {
|
|
1074
1098
|
checked.push("storage.rules");
|
|
1075
1099
|
try {
|
|
1076
1100
|
const raw = readFileSync3(d.storageRulesFile, "utf-8");
|
|
1077
1101
|
const relStorage = model.firebase.storage?.rulesFile ?? "storage.rules";
|
|
1078
1102
|
const clean = stripRuleComments(raw);
|
|
1079
|
-
|
|
1080
|
-
const
|
|
1103
|
+
let evCounter = 0;
|
|
1104
|
+
const evId = (prefix) => {
|
|
1105
|
+
evCounter += 1;
|
|
1106
|
+
return `ev-storage-${prefix}-${evCounter}`;
|
|
1107
|
+
};
|
|
1081
1108
|
for (const m of parseStorageAllows(clean)) {
|
|
1082
|
-
const open = m.condition === void 0 || m.condition.trim().toLowerCase().replace(/\s+/g, "") === "true";
|
|
1083
|
-
if (!open) continue;
|
|
1084
1109
|
const ops = expandStorageOps(m.target);
|
|
1085
1110
|
const key = conditionKeyForFingerprint(m.condition);
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
file: relStorage,
|
|
1094
|
-
evidence: [{ id: `ev-storage-write-${findings.length}`, kind: "storage-public", summary: `${m.target} if true`, confidence: "CONFIRMED" }],
|
|
1095
|
-
fix: "Exigir request.auth (e dono/tenant) no write. Deixe get aberto s\xF3 em pasta de vitrine como logos/."
|
|
1096
|
-
});
|
|
1097
|
-
} else if (ops.has("read") || ops.has("get") || ops.has("list")) {
|
|
1098
|
-
if (hasAllPaths) {
|
|
1111
|
+
const normCond = (m.condition ?? "").toLowerCase().replace(/\s+/g, "");
|
|
1112
|
+
const hasAuth = normCond.includes("request.auth");
|
|
1113
|
+
const bareAuth = (/request\.auth(!==|!=)null/.test(normCond) || /request\.auth\.uid(!==|!=)null/.test(normCond)) && !normCond.includes("request.auth.token") && !normCond.includes("sign_in_provider");
|
|
1114
|
+
const wildcard = /\{\s*\w+\s*=\s*\*\*\s*\}/.test(m.matchPath);
|
|
1115
|
+
const where = `${relStorage}:${m.line} (${m.matchPath}, allow ${m.target})`;
|
|
1116
|
+
if (isOpenStorageCondition(m.condition)) {
|
|
1117
|
+
if (ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete")) {
|
|
1099
1118
|
findings.push({
|
|
1100
|
-
rule: "
|
|
1101
|
-
severity: "
|
|
1102
|
-
confidence: "
|
|
1103
|
-
message: `Storage com
|
|
1104
|
-
fingerprint: `
|
|
1105
|
-
file: relStorage,
|
|
1106
|
-
evidence: [{ id: `ev-storage-all-${findings.length}`, kind: "storage-public", summary: `${m.target} if true`, confidence: "PROBABLE" }],
|
|
1107
|
-
fix: "Troque o curinga total por pastas expl\xEDcitas (ex: /logos/{logoId}) com get: if true e write restrito."
|
|
1108
|
-
});
|
|
1109
|
-
} else {
|
|
1110
|
-
findings.push({
|
|
1111
|
-
rule: "STORAGE_PUBLIC_READ",
|
|
1112
|
-
severity: "INFO",
|
|
1113
|
-
confidence: "PROBABLE",
|
|
1114
|
-
message: `Storage com leitura aberta (${relStorage}, allow ${m.target}). Pode ser vitrine proposital (logos) \u2014 confirme que o write segue restrito.`,
|
|
1115
|
-
fingerprint: `STORAGE_PUBLIC_READ:${m.target}:${key}`,
|
|
1119
|
+
rule: "STORAGE_PUBLIC_WRITE",
|
|
1120
|
+
severity: "ERROR",
|
|
1121
|
+
confidence: "CONFIRMED",
|
|
1122
|
+
message: `Storage com escrita aberta (${where}). Qualquer cliente pode escrever \u2014 cofre aberto, n\xE3o vitrine.`,
|
|
1123
|
+
fingerprint: `STORAGE_PUBLIC_WRITE:${m.matchPath}:${m.target}:${key}`,
|
|
1116
1124
|
file: relStorage,
|
|
1117
|
-
|
|
1125
|
+
line: m.line,
|
|
1126
|
+
evidence: [{ id: evId("write"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "CONFIRMED", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
|
|
1127
|
+
fix: "Exigir request.auth (e dono/tenant) no write. Deixe get aberto s\xF3 em pasta de vitrine como logos/."
|
|
1118
1128
|
});
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
if (ops.has("read") || ops.has("get") || ops.has("list")) {
|
|
1132
|
+
if (wildcard) {
|
|
1133
|
+
findings.push({
|
|
1134
|
+
rule: "STORAGE_ALL_PATHS_OPEN",
|
|
1135
|
+
severity: "WARNING",
|
|
1136
|
+
confidence: "PROBABLE",
|
|
1137
|
+
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}.`,
|
|
1138
|
+
fingerprint: `STORAGE_ALL_PATHS_OPEN:${m.matchPath}:${m.target}:${key}`,
|
|
1139
|
+
file: relStorage,
|
|
1140
|
+
line: m.line,
|
|
1141
|
+
evidence: [{ id: evId("all"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
|
|
1142
|
+
fix: "Troque o curinga total por pastas expl\xEDcitas (ex: /logos/{logoId}) com get: if true e write restrito."
|
|
1143
|
+
});
|
|
1144
|
+
} else {
|
|
1145
|
+
const enumerates = ops.has("read") || ops.has("list");
|
|
1146
|
+
findings.push({
|
|
1147
|
+
rule: "STORAGE_PUBLIC_READ",
|
|
1148
|
+
severity: "INFO",
|
|
1149
|
+
confidence: "PROBABLE",
|
|
1150
|
+
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.`,
|
|
1151
|
+
fingerprint: `STORAGE_PUBLIC_READ:${m.matchPath}:${m.target}:${key}`,
|
|
1152
|
+
file: relStorage,
|
|
1153
|
+
line: m.line,
|
|
1154
|
+
evidence: [{ id: evId("read"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
|
|
1155
|
+
fix: enumerates ? "Para vitrine, troque allow read por allow get (sem list) na pasta p\xFAblica." : void 0
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
continue;
|
|
1119
1159
|
}
|
|
1120
1160
|
}
|
|
1161
|
+
if (bareAuth && (ops.has("read") || ops.has("get") || ops.has("list") || ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete"))) {
|
|
1162
|
+
findings.push({
|
|
1163
|
+
rule: "STORAGE_ANON_ALLOWED",
|
|
1164
|
+
severity: "INFO",
|
|
1165
|
+
confidence: "PROBABLE",
|
|
1166
|
+
message: `Storage aceita qualquer autenticado incluindo an\xF4nimo (${where}). Exija claim/provider para dados sens\xEDveis.`,
|
|
1167
|
+
fingerprint: `STORAGE_ANON_ALLOWED:${m.matchPath}:${m.target}:${key}`,
|
|
1168
|
+
file: relStorage,
|
|
1169
|
+
line: m.line,
|
|
1170
|
+
evidence: [{ id: evId("anon"), kind: "anon-allowed", summary: `${m.matchPath} allow ${m.target}`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
|
|
1171
|
+
fix: "Exigir claim (token.admin/role) ou provider diferente de anonymous para dados sens\xEDveis."
|
|
1172
|
+
});
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
if (!hasAuth && m.condition !== void 0 && (ops.has("read") || ops.has("get") || ops.has("list"))) {
|
|
1176
|
+
findings.push({
|
|
1177
|
+
rule: "STORAGE_PUBLIC_READ",
|
|
1178
|
+
severity: "INFO",
|
|
1179
|
+
confidence: "PROBABLE",
|
|
1180
|
+
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.`,
|
|
1181
|
+
fingerprint: `STORAGE_PUBLIC_READ:${m.matchPath}:${m.target}:${key}`,
|
|
1182
|
+
file: relStorage,
|
|
1183
|
+
line: m.line,
|
|
1184
|
+
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 } } }]
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1121
1187
|
}
|
|
1122
1188
|
} catch {
|
|
1123
1189
|
skipped.push("storage.rules ileg\xEDvel \u2014 Storage sem cobertura neste scan");
|
|
@@ -1126,24 +1192,38 @@ async function scan(rootDir, opts = {}) {
|
|
|
1126
1192
|
skipped.push("storage.rules n\xE3o observado \u2014 Storage fora deste scan");
|
|
1127
1193
|
}
|
|
1128
1194
|
if (d.functionsDir) {
|
|
1129
|
-
const
|
|
1130
|
-
model.functions = { dir: model.functions?.dir, appCheckEnforced:
|
|
1195
|
+
const appcheck = await countAppCheckEnforced(rootDir);
|
|
1196
|
+
model.functions = { dir: model.functions?.dir, appCheckEnforced: appcheck.occurrences };
|
|
1131
1197
|
checked.push("functions-appcheck");
|
|
1132
|
-
if (
|
|
1198
|
+
if (appcheck.manual > 0) {
|
|
1199
|
+
findings.push({
|
|
1200
|
+
rule: "APPCHECK_MANUAL",
|
|
1201
|
+
severity: "INFO",
|
|
1202
|
+
confidence: "PROBABLE",
|
|
1203
|
+
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.`,
|
|
1204
|
+
fingerprint: "APPCHECK_MANUAL",
|
|
1205
|
+
evidence: [{ id: "ev-appcheck-manual", kind: "appcheck", summary: `${appcheck.manual} verifyToken`, confidence: "PROBABLE" }]
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
if (appcheck.occurrences > 0) {
|
|
1133
1209
|
findings.push({
|
|
1134
1210
|
rule: "APPCHECK_ENFORCED",
|
|
1135
1211
|
severity: "INFO",
|
|
1136
1212
|
confidence: "CONFIRMED",
|
|
1137
|
-
message: `
|
|
1213
|
+
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
1214
|
fingerprint: "APPCHECK_ENFORCED",
|
|
1139
|
-
evidence: [{ id: "ev-appcheck", kind: "appcheck", summary: `${
|
|
1215
|
+
evidence: [{ id: "ev-appcheck", kind: "appcheck", summary: `${appcheck.occurrences} ocorr\xEAncias`, confidence: "CONFIRMED" }]
|
|
1140
1216
|
});
|
|
1141
|
-
} else {
|
|
1142
|
-
skipped.push("functions sem enforceAppCheck observado \u2014 AppCheck sem cobertura afirmativa");
|
|
1217
|
+
} else if (appcheck.manual === 0) {
|
|
1218
|
+
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)");
|
|
1219
|
+
}
|
|
1220
|
+
if (appcheck.limitHit) {
|
|
1221
|
+
skipped.push("functions: teto de varredura atingido \u2014 AppCheck parcial neste scan");
|
|
1143
1222
|
}
|
|
1144
1223
|
} else {
|
|
1145
1224
|
skipped.push("functions n\xE3o observadas \u2014 AppCheck fora deste scan");
|
|
1146
1225
|
}
|
|
1226
|
+
skipped.push("isolamento por tenant (ownerId/tenantId) n\xE3o verificado \u2014 fora dos checks implementados, nunca verde silencioso");
|
|
1147
1227
|
const modelCheck = ProjectModelSchema.safeParse(model);
|
|
1148
1228
|
if (!modelCheck.success) {
|
|
1149
1229
|
findings.push({
|
|
@@ -1205,7 +1285,10 @@ async function countAppCheckEnforced(rootDir) {
|
|
|
1205
1285
|
const { join: joinP } = await import("path");
|
|
1206
1286
|
const { stripRuleComments: strip } = await import("./rules-GTGZWN5U.js");
|
|
1207
1287
|
const roots = [joinP(rootDir, "functions")];
|
|
1208
|
-
let
|
|
1288
|
+
let occurrences = 0;
|
|
1289
|
+
const files = /* @__PURE__ */ new Set();
|
|
1290
|
+
let manual = 0;
|
|
1291
|
+
let limitHit = false;
|
|
1209
1292
|
const stack = [...roots];
|
|
1210
1293
|
let guard = 0;
|
|
1211
1294
|
const isTestFile = (p) => {
|
|
@@ -1214,6 +1297,7 @@ async function countAppCheckEnforced(rootDir) {
|
|
|
1214
1297
|
};
|
|
1215
1298
|
while (stack.length > 0 && guard < 200) {
|
|
1216
1299
|
guard += 1;
|
|
1300
|
+
if (guard >= 200 && stack.length > 0) limitHit = true;
|
|
1217
1301
|
const cur = stack.pop();
|
|
1218
1302
|
let entries = [];
|
|
1219
1303
|
try {
|
|
@@ -1235,27 +1319,77 @@ async function countAppCheckEnforced(rootDir) {
|
|
|
1235
1319
|
stack.push(abs);
|
|
1236
1320
|
continue;
|
|
1237
1321
|
}
|
|
1238
|
-
if (!/\.(ts|js|mjs|cjs)$/.test(name)) continue;
|
|
1322
|
+
if (!/\.(ts|js|mjs|cjs|py)$/.test(name)) continue;
|
|
1239
1323
|
if (isTestFile(abs)) continue;
|
|
1240
1324
|
try {
|
|
1241
1325
|
const text = strip(readSync(abs, "utf-8"));
|
|
1242
|
-
const hits = text.match(/enforceAppCheck\s*:\s*true/g);
|
|
1243
|
-
if (hits)
|
|
1326
|
+
const hits = text.match(/enforceAppCheck\s*:\s*true|enforce_app_check\s*=\s*True/g);
|
|
1327
|
+
if (hits) {
|
|
1328
|
+
occurrences += hits.length;
|
|
1329
|
+
files.add(abs);
|
|
1330
|
+
}
|
|
1331
|
+
const manualHits = text.match(/verifyToken\s*\(|verify_token\s*\(|consumeAppCheckToken|getLimitedUseToken|getAppCheck\s*\(/g);
|
|
1332
|
+
if (manualHits) manual += manualHits.length;
|
|
1244
1333
|
} catch {
|
|
1245
1334
|
}
|
|
1246
1335
|
}
|
|
1247
1336
|
}
|
|
1248
|
-
return
|
|
1337
|
+
return { occurrences, files: files.size, manual, limitHit };
|
|
1249
1338
|
} catch {
|
|
1250
|
-
return 0;
|
|
1339
|
+
return { occurrences: 0, files: 0, manual: 0, limitHit: false };
|
|
1251
1340
|
}
|
|
1252
1341
|
}
|
|
1342
|
+
function isOpenStorageCondition(condition) {
|
|
1343
|
+
if (condition === void 0) return true;
|
|
1344
|
+
const norm = stripOuterParens(condition).toLowerCase().replace(/\s+/g, "");
|
|
1345
|
+
if (norm === "" || norm === "true") return true;
|
|
1346
|
+
if (/(^|\|\|)true($|\|\|)/.test(norm)) return true;
|
|
1347
|
+
if (norm.includes("request.auth==null") || norm.includes("request.auth===null")) return true;
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1253
1350
|
function parseStorageAllows(clean) {
|
|
1254
1351
|
const out = [];
|
|
1352
|
+
const matches = [];
|
|
1353
|
+
const matchRe = /match\s+/g;
|
|
1354
|
+
let mm;
|
|
1355
|
+
while ((mm = matchRe.exec(clean)) !== null) {
|
|
1356
|
+
const lineStart = clean.lastIndexOf("\n", mm.index) + 1;
|
|
1357
|
+
const lineEnd = clean.indexOf("\n", mm.index);
|
|
1358
|
+
const line = clean.slice(lineStart, lineEnd === -1 ? void 0 : lineEnd);
|
|
1359
|
+
const brace = line.lastIndexOf("{");
|
|
1360
|
+
if (brace === -1) continue;
|
|
1361
|
+
const path = line.slice(mm.index - lineStart + 5, brace).trim() || "/(unknown)";
|
|
1362
|
+
const openBrace = lineStart + brace;
|
|
1363
|
+
let depth = 0;
|
|
1364
|
+
let quote = null;
|
|
1365
|
+
let end = clean.length;
|
|
1366
|
+
for (let i = openBrace; i < clean.length; i++) {
|
|
1367
|
+
const c = clean[i];
|
|
1368
|
+
if (quote) {
|
|
1369
|
+
if (c === quote && clean[i - 1] !== "\\") quote = null;
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
1373
|
+
quote = c;
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
if (c === "{") depth += 1;
|
|
1377
|
+
else if (c === "}") {
|
|
1378
|
+
depth -= 1;
|
|
1379
|
+
if (depth === 0) {
|
|
1380
|
+
end = i + 1;
|
|
1381
|
+
break;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
matches.push({ path, start: mm.index, end });
|
|
1386
|
+
}
|
|
1255
1387
|
const re = /allow\s+([^;:]+?)(?::\s*if\s+([^;]+))?;/gi;
|
|
1256
1388
|
let m;
|
|
1257
1389
|
while ((m = re.exec(clean)) !== null) {
|
|
1258
|
-
|
|
1390
|
+
const containing = matches.filter((b) => b.start <= m.index && m.index < b.end).sort((a, b) => b.start - a.start);
|
|
1391
|
+
const line = clean.slice(0, m.index).split("\n").length;
|
|
1392
|
+
out.push({ target: m[1].trim(), condition: m[2]?.trim(), matchPath: containing[0]?.path ?? "/(unknown)", line });
|
|
1259
1393
|
}
|
|
1260
1394
|
return out;
|
|
1261
1395
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
scan
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-4BBFPTOR.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 });
|
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-4BBFPTOR.js";
|
|
5
5
|
import "./chunk-7EWTBIKJ.js";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
@@ -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");
|
|
37
35
|
const getArg = (name) => {
|
|
38
36
|
const pref = `--${name}=`;
|
|
39
37
|
const hit = args.find((a) => a.startsWith(pref));
|
|
@@ -42,6 +40,13 @@ 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 asJson = args.includes("--json") || args.includes("--json=true");
|
|
45
50
|
const knownFlags = /* @__PURE__ */ new Set(["--json", "--strict", "--help", "-h", "help", "--version", "-v", "--coverage", "--local", "--remote"]);
|
|
46
51
|
for (const a of args.slice(1)) {
|
|
47
52
|
const base = a.includes("=") ? a.slice(0, a.indexOf("=")) : a;
|
|
@@ -65,7 +70,7 @@ Uso: firebase-audit scan|check|verify|drift [--json] ...`);
|
|
|
65
70
|
const { resolve: resolveRoot } = await import("path");
|
|
66
71
|
const rootDir = cwdArg ? resolveRoot(process.cwd(), cwdArg) : process.cwd();
|
|
67
72
|
if (cmd === "verify") {
|
|
68
|
-
const { scan: scanForVerify } = await import("./scan-
|
|
73
|
+
const { scan: scanForVerify } = await import("./scan-LJ3TCHMR.js");
|
|
69
74
|
const { verify } = await import("./verify-WBVSESHD.js");
|
|
70
75
|
const { resolve: resolveVerify } = await import("path");
|
|
71
76
|
const coverageArg = getArg("coverage");
|
|
@@ -149,7 +154,7 @@ FIREBASE AUDIT drift \u2014 MATCHED ${counts("MATCHED")}, LOCAL_ONLY ${counts("L
|
|
|
149
154
|
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
155
|
if (findings.length === 0) {
|
|
151
156
|
console.log(`
|
|
152
|
-
\u2713 Nenhum problema nos checks implementados (${summary.passedChecks}/${summary.totalChecks} passaram).
|
|
157
|
+
\u2713 Nenhum problema nos checks FBA implementados (${summary.passedChecks}/${summary.totalChecks} passaram).
|
|
153
158
|
Olhei: ${summary.checked.join(", ")}.
|
|
154
159
|
Pulei: ${summary.skipped.join(" | ") || "nada"}.
|
|
155
160
|
`);
|
|
@@ -164,7 +169,7 @@ ${icon} ${f.rule} [${f.confidence}]${where}
|
|
|
164
169
|
if (f.fix) console.log(` \u2192 ${f.fix}`);
|
|
165
170
|
}
|
|
166
171
|
console.log(`
|
|
167
|
-
SUMMARY: ${summary.errors} errors, ${summary.warnings} warnings, ${summary.infos} infos (${summary.passedChecks}/${summary.totalChecks} checks passaram)`);
|
|
172
|
+
SUMMARY: ${summary.errors} errors, ${summary.warnings} warnings, ${summary.infos} infos (${summary.passedChecks}/${summary.totalChecks} checks FBA passaram)`);
|
|
168
173
|
console.log(`Olhei: ${summary.checked.join(", ")}`);
|
|
169
174
|
console.log(`Pulei: ${summary.skipped.join(" | ") || "nada"}
|
|
170
175
|
`);
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpServer,
|
|
3
3
|
startMcpServer
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-PWJ4DI24.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-4BBFPTOR.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-PWJ4DI24.js";
|
|
5
|
+
import "./chunk-4BBFPTOR.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-PWJ4DI24.js";
|
|
5
|
+
import "./chunk-4BBFPTOR.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.3",
|
|
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.
|