@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.
@@ -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 firestore.${kind} para "${rel}" inexistente \u2014 usando default. Corrija o path.`,
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 = ["firestore.rules", "indexes", "contract", "client-code"];
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
- const low = clean.toLowerCase().replace(/\s+/g, "");
1080
- const hasAllPaths = low.includes("allpaths=**") || /\{[^}]*=\*\*\}/.test(low);
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
- if (ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete")) {
1087
- findings.push({
1088
- rule: "STORAGE_PUBLIC_WRITE",
1089
- severity: "ERROR",
1090
- confidence: "CONFIRMED",
1091
- message: `Storage com escrita aberta (${relStorage}, allow ${m.target}). Qualquer cliente pode escrever \u2014 cofre aberto, n\xE3o vitrine.`,
1092
- fingerprint: `STORAGE_PUBLIC_WRITE:${m.target}:${key}`,
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: "STORAGE_ALL_PATHS_OPEN",
1101
- severity: "WARNING",
1102
- confidence: "PROBABLE",
1103
- message: `Storage com leitura aberta em curinga total (${relStorage}, allow ${m.target}). Vale para tudo \u2014 se a inten\xE7\xE3o era s\xF3 logo, restrinja a /logos/{id}.`,
1104
- fingerprint: `STORAGE_ALL_PATHS_OPEN:${m.target}:${key}`,
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
- evidence: [{ id: `ev-storage-read-${findings.length}`, kind: "storage-public", summary: `${m.target} if true`, confidence: "PROBABLE" }]
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 enforced = await countAppCheckEnforced(rootDir);
1130
- model.functions = { dir: model.functions?.dir, appCheckEnforced: enforced };
1195
+ const appcheck = await countAppCheckEnforced(rootDir);
1196
+ model.functions = { dir: model.functions?.dir, appCheckEnforced: appcheck.occurrences };
1131
1197
  checked.push("functions-appcheck");
1132
- if (enforced > 0) {
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: `AppCheck ativo em ${enforced} function(s). Carimbo do app oficial \u2014 n\xE3o prova quem \xE9 o usu\xE1rio nem o tenant. Autoriza\xE7\xE3o segue nas Rules/guardas.`,
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: `${enforced} enforced`, confidence: "CONFIRMED" }]
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 count = 0;
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) count += hits.length;
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 count;
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
- out.push({ target: m[1].trim(), condition: m[2]?.trim() });
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-GDO2DLNQ.js";
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-GDO2DLNQ.js";
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-4Q4XVLJU.js");
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-QACXCMYB.js";
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-GDO2DLNQ.js";
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-QACXCMYB.js";
5
- import "./chunk-GDO2DLNQ.js";
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-QACXCMYB.js";
5
- import "./chunk-GDO2DLNQ.js";
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";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  scan
3
- } from "./chunk-GDO2DLNQ.js";
3
+ } from "./chunk-4BBFPTOR.js";
4
4
  import "./chunk-7EWTBIKJ.js";
5
5
  export {
6
6
  scan
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justmpm/firebase-audit",
3
- "version": "0.5.1",
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 `anon/user/admin x read/create/update/delete` com `@firebase/rules-unit-testing`.
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.