@justmpm/firebase-audit 0.4.3 → 0.5.1

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.
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  scan
3
- } from "./chunk-3SEVYYCX.js";
3
+ } from "./chunk-GDO2DLNQ.js";
4
4
  import {
5
5
  verify
6
- } from "./chunk-PSSMYRRU.js";
6
+ } from "./chunk-Y4VXWSKX.js";
7
7
  import {
8
8
  drift
9
- } from "./chunk-NJBCSW7E.js";
9
+ } from "./chunk-XZ32YQQC.js";
10
10
 
11
11
  // src/mcp.ts
12
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -31,10 +31,11 @@ function readPkgText(rel) {
31
31
  var FALLBACK_SKILL = [
32
32
  "# firebase-audit-skill (fallback)",
33
33
  "",
34
- "1. Descobrir projeto (firebase.json, firestore.rules, firestore.indexes.json).",
35
- "2. Executar scan (strict no CI). Sem adapter resolvido \u2192 UNKNOWN, nunca adivinhar.",
34
+ "1. Descobrir projeto (firebase.json, firestore.rules, storage.rules, firestore.indexes.json).",
35
+ "2. Executar scan (check com contrato no CI). Sem fun\xE7\xE3o de permiss\xE3o que receba o nome como texto \u2192 UNKNOWN, nunca adivinhar.",
36
36
  "3. CONFIRMED corrige agora; PROBABLE investiga; NOT_OBSERVED/UNKNOWN n\xE3o afirma.",
37
- "4. Corrigir c\xF3digo, nunca o contrato para esconder finding."
37
+ "4. AppCheck \xE9 carimbo do app, n\xE3o autoriza\xE7\xE3o. Logo p\xFAblica pode ser vitrine; cofre \xE9 write aberto.",
38
+ "5. Corrigir c\xF3digo, nunca o contrato para esconder finding."
38
39
  ].join("\n");
39
40
  function createMcpServer() {
40
41
  const server = new McpServer({
@@ -45,14 +46,14 @@ function createMcpServer() {
45
46
  "skill",
46
47
  {
47
48
  title: "Firebase Audit Skill (guia do agente)",
48
- description: "LEIA ANTES de auditar: sequ\xEAncia obrigat\xF3ria, travas (nunca mude o contrato para esconder finding, nunca afirme sem evid\xEAncia, nunca diga 'unused') e Emulador. Retorna o guia oficial da vers\xE3o instalada + modelo de contrato. Sempre sincronizado \u2014 dispensa skill copiada.",
49
+ description: "LEIA ANTES de auditar: sequ\xEAncia obrigat\xF3ria, travas (nunca mude o contrato para esconder finding, nunca afirme sem evid\xEAncia, nunca diga 'unused') e Emulador. Retorna o guia oficial instalado + modelo de contrato. Sempre sincronizado \u2014 dispensa skill copiada.",
49
50
  inputSchema: {}
50
51
  },
51
52
  async () => {
52
53
  const skill = readPkgText(join("skill", "SKILL.md")) ?? FALLBACK_SKILL;
53
54
  const template = readPkgText(join("templates", "firebase-audit.yaml")) ?? "";
54
55
  const text = [
55
- `# firebase-audit v${pkg.version} \u2014 guia do agente`,
56
+ "# firebase-audit \u2014 guia do agente",
56
57
  "",
57
58
  skill,
58
59
  "",
@@ -69,10 +70,10 @@ function createMcpServer() {
69
70
  "scan",
70
71
  {
71
72
  title: "Firebase Audit Scan",
72
- description: "Audita projeto Firebase (c\xF3digo + firestore.rules + \xEDndices + contrato). Static-first, nunca inventa certeza (CONFIRMED/PROBABLE/NOT_OBSERVED/UNKNOWN). Use para descobrir regras p\xFAblicas, Admin em cliente, permiss\xF5es sem contrato.",
73
+ description: "Audita projeto Firebase (c\xF3digo + firestore.rules + storage.rules + \xEDndices + contrato + AppCheck). Est\xE1tico primeiro, nunca inventa certeza (CONFIRMED/PROBABLE/NOT_OBSERVED/UNKNOWN). Use para descobrir regras p\xFAblicas, Admin em cliente, permiss\xF5es sem contrato.",
73
74
  inputSchema: {
74
75
  cwd: z.string().min(1).describe("Diret\xF3rio raiz do projeto Firebase a auditar"),
75
- adapter: z.string().optional().describe("Fun\xE7\xE3o de autoriza\xE7\xE3o (ex: rbac.can). Default: do contrato."),
76
+ adapter: z.string().optional().describe("Fun\xE7\xE3o de permiss\xE3o no c\xF3digo que recebe o nome como texto (ex: rbac.can). N\xE3o use guarda de rota nem function do .rules aqui. Default: do contrato."),
76
77
  strict: z.boolean().optional().describe("Exige contrato (firebase-audit.yaml). CI usa strict.")
77
78
  }
78
79
  },
@@ -83,7 +84,7 @@ function createMcpServer() {
83
84
  content: [
84
85
  {
85
86
  type: "text",
86
- text: JSON.stringify({ version: pkg.version, summary: res.summary, findings: res.findings }, null, 2)
87
+ text: JSON.stringify({ summary: res.summary, findings: res.findings }, null, 2)
87
88
  }
88
89
  ]
89
90
  };
@@ -99,10 +100,10 @@ function createMcpServer() {
99
100
  "check",
100
101
  {
101
102
  title: "Firebase Audit Check (strict)",
102
- description: "Alias de scan --strict: exige contrato e falha (exit 2 no CLI) se houver ERROR. Use no CI.",
103
+ description: "Alias de scan strict: exige contrato e reprova CI se houver ERROR. Use no CI.",
103
104
  inputSchema: {
104
105
  cwd: z.string().min(1).describe("Diret\xF3rio raiz do projeto Firebase"),
105
- adapter: z.string().optional().describe("Fun\xE7\xE3o de autoriza\xE7\xE3o (ex: rbac.can)")
106
+ adapter: z.string().optional().describe("Fun\xE7\xE3o de permiss\xE3o no c\xF3digo que recebe o nome como texto (ex: rbac.can)")
106
107
  }
107
108
  },
108
109
  async ({ cwd, adapter }) => {
@@ -112,7 +113,7 @@ function createMcpServer() {
112
113
  content: [
113
114
  {
114
115
  type: "text",
115
- text: JSON.stringify({ version: pkg.version, summary: res.summary, findings: res.findings }, null, 2)
116
+ text: JSON.stringify({ summary: res.summary, findings: res.findings }, null, 2)
116
117
  }
117
118
  ]
118
119
  };
@@ -128,10 +129,10 @@ function createMcpServer() {
128
129
  "verify",
129
130
  {
130
131
  title: "Firebase Audit Verify (Emulator)",
131
- description: "Gera matriz role\xD7opera\xE7\xE3o\xD7path e avalia coverage do :ruleCoverage. Requer Emulator (firebase emulators:exec). Nunca afirma cobertura sem teste real.",
132
+ description: "Gera matriz papel\xD7opera\xE7\xE3o\xD7path e avalia cobertura do ruleCoverage. Requer Emulator. Sem contrato usa pap\xE9is reduzidos (anonymous/user/admin). Nunca afirma cobertura sem teste real.",
132
133
  inputSchema: {
133
134
  cwd: z.string().min(1).describe("Diret\xF3rio raiz do projeto"),
134
- coverage: z.string().optional().describe("Path do coverage.json do :ruleCoverage (relativo ao cwd)")
135
+ coverage: z.string().optional().describe("Path do coverage.json do ruleCoverage (relativo ao cwd)")
135
136
  }
136
137
  },
137
138
  async ({ cwd, coverage }) => {
@@ -139,12 +140,24 @@ function createMcpServer() {
139
140
  const { resolve } = await import("path");
140
141
  const { model } = await scan(cwd);
141
142
  const res = verify(model, { coverageFile: coverage ? resolve(cwd, coverage) : void 0 });
143
+ const roles = model.authorization.roles.length > 0 ? model.authorization.roles.map((r) => r.name) : ["anonymous", "user", "admin"];
144
+ const truncated = res.cases.length > 50;
142
145
  return {
143
146
  content: [
144
147
  {
145
148
  type: "text",
146
149
  text: JSON.stringify(
147
- { version: pkg.version, cases: res.cases.length, uncovered: res.uncovered, coveragePct: res.coveragePct, findings: res.findings },
150
+ {
151
+ roles,
152
+ rolesReduced: model.authorization.roles.length === 0,
153
+ cases: res.cases.slice(0, 50),
154
+ casesTotal: res.cases.length,
155
+ truncated,
156
+ uncovered: res.uncovered.slice(0, 20),
157
+ uncoveredTotal: res.uncovered.length,
158
+ coveragePct: res.coveragePct,
159
+ findings: res.findings
160
+ },
148
161
  null,
149
162
  2
150
163
  )
@@ -166,7 +179,7 @@ function createMcpServer() {
166
179
  description: "Compara \xEDndices locais vs implantados. Retorna MATCHED/LOCAL_ONLY/REMOTE_ONLY. Nunca diz 'unused', apenas NOT_OBSERVED.",
167
180
  inputSchema: {
168
181
  local: z.string().nullable().optional().describe("Path do firestore.indexes.json local (ou null). Relativo resolve contra cwd."),
169
- remote: z.string().nullable().optional().describe("Path do JSON remoto (gcloud/REST) (ou null). Relativo resolve contra cwd."),
182
+ remote: z.string().nullable().optional().describe("Path do JSON remoto (ou null). Relativo resolve contra cwd."),
170
183
  cwd: z.string().optional().describe("Diret\xF3rio base para paths relativos (default: cwd do servidor).")
171
184
  }
172
185
  },
@@ -183,7 +196,7 @@ function createMcpServer() {
183
196
  content: [
184
197
  {
185
198
  type: "text",
186
- text: JSON.stringify({ version: pkg.version, entries: res.entries, findings: res.findings }, null, 2)
199
+ text: JSON.stringify({ entries: res.entries, findings: res.findings }, null, 2)
187
200
  }
188
201
  ]
189
202
  };
@@ -201,7 +214,7 @@ async function startMcpServer() {
201
214
  const server = createMcpServer();
202
215
  const transport = new StdioServerTransport();
203
216
  await server.connect(transport);
204
- console.error(`[firebase-audit] MCP server v${pkg.version} running via stdio`);
217
+ console.error("[firebase-audit] MCP server running via stdio");
205
218
  }
206
219
 
207
220
  export {
@@ -128,6 +128,16 @@ function drift(localFile, remoteFile) {
128
128
  }
129
129
  const remoteByKey = new Map(remote.map((r) => [keyOf(r), r]));
130
130
  const localByKey = new Map(local.map((l) => [keyOf(l), l]));
131
+ if (local.length !== localByKey.size || remote.length !== remoteByKey.size) {
132
+ findings.push({
133
+ rule: "DRIFT_DUPLICATE_KEY",
134
+ severity: "INFO",
135
+ confidence: "CONFIRMED",
136
+ message: "\xCDndices duplicados colapsados por chave \u2014 revise o arquivo de origem.",
137
+ fingerprint: "DRIFT_DUPLICATE_KEY",
138
+ evidence: [{ id: "ev-drift-dup", kind: "drift-parse", summary: "chave duplicada", confidence: "CONFIRMED" }]
139
+ });
140
+ }
131
141
  const entries = [];
132
142
  for (const [key, l] of localByKey) {
133
143
  if (remoteByKey.has(key)) entries.push({ key, status: "MATCHED", local: l, remote: remoteByKey.get(key) });
@@ -39,7 +39,7 @@ function planVerify(model) {
39
39
  }
40
40
  const seen = /* @__PURE__ */ new Set();
41
41
  return cases.filter((c) => {
42
- const k = `${c.role}:${c.operation}:${c.path}`;
42
+ const k = `${c.role}:${c.operation}:${c.pathTemplate}:${c.path}`;
43
43
  if (seen.has(k)) return false;
44
44
  seen.add(k);
45
45
  return true;
@@ -71,7 +71,7 @@ function verify(model, opts = {}) {
71
71
  rule: "COVERAGE_NOT_EVALUATED",
72
72
  severity: "WARNING",
73
73
  confidence: "NOT_OBSERVED",
74
- message: "Coverage sem visitCount reconhec\xEDvel nesta vers\xE3o \u2014 uncovered lista tudo por honestidade.",
74
+ message: "Coverage sem visitCount reconhec\xEDvel \u2014 uncovered lista tudo por honestidade.",
75
75
  fingerprint: "COVERAGE_NOT_EVALUATED",
76
76
  evidence: [{ id: "ev-coverage-ne", kind: "coverage-parse", summary: "n\xE3o avaliado", confidence: "NOT_OBSERVED" }]
77
77
  });
@@ -105,8 +105,11 @@ function verify(model, opts = {}) {
105
105
  if (coveragePct === 100) {
106
106
  return { cases, uncovered: [], findings, coveragePct };
107
107
  }
108
+ const seenPaths = /* @__PURE__ */ new Set();
108
109
  for (const rule of model.rules) {
109
110
  if (rule.path === "/(unknown)") continue;
111
+ if (seenPaths.has(rule.path)) continue;
112
+ seenPaths.add(rule.path);
110
113
  uncovered.push(rule.path);
111
114
  }
112
115
  return { cases, uncovered, findings, coveragePct };
package/dist/cli.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  scan
4
- } from "./chunk-3SEVYYCX.js";
4
+ } from "./chunk-GDO2DLNQ.js";
5
+ import "./chunk-7EWTBIKJ.js";
5
6
 
6
7
  // src/cli.ts
7
8
  import { createRequire } from "module";
@@ -15,13 +16,13 @@ async function main() {
15
16
  return;
16
17
  }
17
18
  if (cmd === "--help" || cmd === "-h" || cmd === "help") {
18
- console.log(`firebase-audit v${pkg.version}
19
+ console.log(`firebase-audit
19
20
 
20
21
  Uso:
21
22
  firebase-audit scan [--json] [--strict] [--adapter=fn] [--cwd=path]
22
- firebase-audit check [--json] [--adapter=fn] [--cwd=path] (alias de scan --strict)
23
- firebase-audit verify [--coverage=path] [--cwd=path] (V3: matriz + cobertura)
24
- firebase-audit drift --local=indexes.json --remote=deployed.json (V4: LOCAL_ONLY/REMOTE_ONLY/MATCHED)
23
+ firebase-audit check [--json] [--adapter=fn] [--cwd=path] (scan strict para CI)
24
+ firebase-audit verify [--coverage=path] [--cwd=path] (matriz + cobertura)
25
+ firebase-audit drift --local=indexes.json --remote=deployed.json (LOCAL_ONLY/REMOTE_ONLY/MATCHED)
25
26
 
26
27
  Exit codes: 0 ok informativo; 2 quando --strict/check encontra ERROR. scan sozinho nunca reprova (use check no CI).`);
27
28
  return;
@@ -64,20 +65,23 @@ Uso: firebase-audit scan|check|verify|drift [--json] ...`);
64
65
  const { resolve: resolveRoot } = await import("path");
65
66
  const rootDir = cwdArg ? resolveRoot(process.cwd(), cwdArg) : process.cwd();
66
67
  if (cmd === "verify") {
67
- const { scan: scanForVerify } = await import("./scan-VREY2FGO.js");
68
- const { verify } = await import("./verify-O722ZZBV.js");
68
+ const { scan: scanForVerify } = await import("./scan-4Q4XVLJU.js");
69
+ const { verify } = await import("./verify-WBVSESHD.js");
69
70
  const { resolve: resolveVerify } = await import("path");
70
71
  const coverageArg = getArg("coverage");
71
72
  const coveragePath = coverageArg ? resolveVerify(rootDir, coverageArg) : void 0;
72
73
  const { model } = await scanForVerify(rootDir, { adapterFn: adapterArg });
73
74
  const res = verify(model, { coverageFile: coveragePath });
75
+ const roles = model.authorization.roles.length > 0 ? model.authorization.roles.map((r) => r.name) : ["anonymous", "user", "admin"];
76
+ const rolesReduced = model.authorization.roles.length === 0;
74
77
  if (asJson) {
75
- console.log(JSON.stringify({ version: pkg.version, cases: res.cases.length, uncovered: res.uncovered, coveragePct: res.coveragePct, findings: res.findings }, null, 2));
78
+ console.log(JSON.stringify({ roles, rolesReduced, cases: res.cases.slice(0, 50), casesTotal: res.cases.length, truncated: res.cases.length > 50, uncovered: res.uncovered.slice(0, 20), uncoveredTotal: res.uncovered.length, coveragePct: res.coveragePct, findings: res.findings }, null, 2));
76
79
  return;
77
80
  }
78
81
  const covStr = res.coveragePct === null ? "sem cobertura observada" : `${res.coveragePct}% de express\xF5es visitadas`;
82
+ const rolesNote = rolesReduced ? "pap\xE9is reduzidos (sem contrato: anonymous/user/admin)" : `${roles.length} pap\xE9is do contrato (${roles.join(", ")})`;
79
83
  console.log(`
80
- FIREBASE AUDIT verify \u2014 ${res.cases.length} casos planejados, ${res.uncovered.length} paths sem mapeamento n\xF3\u2192path (${covStr}).`);
84
+ FIREBASE AUDIT verify \u2014 ${res.cases.length} casos planejados (${rolesNote}), ${res.uncovered.length} paths sem cobertura (${covStr}).`);
81
85
  for (const f of res.findings) {
82
86
  const icon = f.severity === "ERROR" ? "\u274C" : f.severity === "WARNING" ? "\u26A0" : "\u2139";
83
87
  console.log(`
@@ -94,7 +98,7 @@ ${icon} ${f.rule} [${f.confidence}]
94
98
  return;
95
99
  }
96
100
  if (cmd === "drift") {
97
- const { drift } = await import("./drift-KU7HWL7M.js");
101
+ const { drift } = await import("./drift-QXALNO2V.js");
98
102
  const { resolve: resolveDrift } = await import("path");
99
103
  const { existsSync: existsDrift } = await import("fs");
100
104
  const localArg = getArg("local");
@@ -108,12 +112,12 @@ ${icon} ${f.rule} [${f.confidence}]
108
112
  for (const [label, p] of [["--local", localPath], ["--remote", remotePath]]) {
109
113
  if (p && !existsDrift(p)) {
110
114
  if (asJson) {
111
- const { drift: driftMissing } = await import("./drift-KU7HWL7M.js");
115
+ const { drift: driftMissing } = await import("./drift-QXALNO2V.js");
112
116
  const res2 = driftMissing(
113
117
  localPath && existsDrift(localPath) ? localPath : null,
114
118
  remotePath && existsDrift(remotePath) ? remotePath : null
115
119
  );
116
- console.log(JSON.stringify({ version: pkg.version, entries: res2.entries, findings: res2.findings }, null, 2));
120
+ console.log(JSON.stringify({ entries: res2.entries, findings: res2.findings }, null, 2));
117
121
  process.exit(1);
118
122
  }
119
123
  console.error(`Arquivo ${label} n\xE3o encontrado: ${p}`);
@@ -122,7 +126,7 @@ ${icon} ${f.rule} [${f.confidence}]
122
126
  }
123
127
  const res = drift(localPath, remotePath);
124
128
  if (asJson) {
125
- console.log(JSON.stringify({ version: pkg.version, entries: res.entries, findings: res.findings }, null, 2));
129
+ console.log(JSON.stringify({ entries: res.entries, findings: res.findings }, null, 2));
126
130
  return;
127
131
  }
128
132
  const counts = (s) => res.entries.filter((e) => e.status === s).length;
@@ -137,16 +141,17 @@ FIREBASE AUDIT drift \u2014 MATCHED ${counts("MATCHED")}, LOCAL_ONLY ${counts("L
137
141
  }
138
142
  const { findings, summary } = await scan(rootDir, { strict, adapterFn: adapterArg });
139
143
  if (asJson) {
140
- console.log(JSON.stringify({ version: pkg.version, summary, findings }, null, 2));
144
+ console.log(JSON.stringify({ summary, findings }, null, 2));
141
145
  if (strict && summary.errors > 0) process.exit(2);
142
146
  return;
143
147
  }
144
- console.log(`
145
- FIREBASE AUDIT v${pkg.version}`);
148
+ console.log("\nFIREBASE AUDIT");
146
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");
147
150
  if (findings.length === 0) {
148
151
  console.log(`
149
- \u2713 Nenhum problema nos checks V1+V2 implementados (parcial: FBA001-FBA004, FBA009-FBA013).
152
+ \u2713 Nenhum problema nos checks implementados (${summary.passedChecks}/${summary.totalChecks} passaram).
153
+ Olhei: ${summary.checked.join(", ")}.
154
+ Pulei: ${summary.skipped.join(" | ") || "nada"}.
150
155
  `);
151
156
  return;
152
157
  }
@@ -159,7 +164,9 @@ ${icon} ${f.rule} [${f.confidence}]${where}
159
164
  if (f.fix) console.log(` \u2192 ${f.fix}`);
160
165
  }
161
166
  console.log(`
162
- SUMMARY: ${summary.errors} errors, ${summary.warnings} warnings, ${summary.infos} infos
167
+ SUMMARY: ${summary.errors} errors, ${summary.warnings} warnings, ${summary.infos} infos (${summary.passedChecks}/${summary.totalChecks} checks passaram)`);
168
+ console.log(`Olhei: ${summary.checked.join(", ")}`);
169
+ console.log(`Pulei: ${summary.skipped.join(" | ") || "nada"}
163
170
  `);
164
171
  if (strict && summary.errors > 0) process.exit(2);
165
172
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  drift
3
- } from "./chunk-NJBCSW7E.js";
3
+ } from "./chunk-XZ32YQQC.js";
4
4
  export {
5
5
  drift
6
6
  };
package/dist/index.d.ts CHANGED
@@ -310,7 +310,14 @@ declare const ProjectModelSchema: z.ZodObject<{
310
310
  rulesFile: z.ZodOptional<z.ZodString>;
311
311
  indexesFile: z.ZodOptional<z.ZodString>;
312
312
  }, z.core.$strict>;
313
+ storage: z.ZodOptional<z.ZodObject<{
314
+ rulesFile: z.ZodOptional<z.ZodString>;
315
+ }, z.core.$strict>>;
313
316
  }, z.core.$strict>;
317
+ functions: z.ZodOptional<z.ZodObject<{
318
+ dir: z.ZodOptional<z.ZodString>;
319
+ appCheckEnforced: z.ZodOptional<z.ZodNumber>;
320
+ }, z.core.$strict>>;
314
321
  authorization: z.ZodObject<{
315
322
  adapter: z.ZodNullable<z.ZodString>;
316
323
  roles: z.ZodArray<z.ZodObject<{
@@ -618,13 +625,15 @@ interface DiscoveryResult {
618
625
  firebaseJson: string | null;
619
626
  firestoreRulesFile: string | null;
620
627
  firestoreIndexesFile: string | null;
628
+ storageRulesFile: string | null;
629
+ functionsDir: string | null;
621
630
  auditYamlFile: string | null;
622
631
  projectId: string | null;
623
- /** V2: databaseId nomeado (sempre null na V1). */
632
+ /** databaseId nomeado (sempre null sem config explícita). */
624
633
  databaseId: string | null;
625
- /** V2: preenchido via varredura + ai-tool (sempre [] na V1). */
634
+ /** preenchido via varredura + ai-tool (vazio sem grafo). */
626
635
  clientFiles: string[];
627
- /** V2: preenchido via varredura + ai-tool (sempre [] na V1). */
636
+ /** preenchido via varredura + ai-tool (vazio sem grafo). */
628
637
  serverFiles: string[];
629
638
  }
630
639
  declare function discover(rootDir: string): DiscoveryResult;
@@ -634,7 +643,7 @@ declare const SERVER_HINTS_SEGMENTS: string[];
634
643
  declare function classifyOrigin(file: string, content: string): "CLIENT" | "SERVER" | "UNKNOWN";
635
644
  declare function emptyModel(rootDir: string, d: DiscoveryResult): ProjectModel;
636
645
  /**
637
- * Fase 3 — enriquece Discovery com grafo do ai-tool (como lib, sem spawn/MCP).
646
+ * Fase opcional — enriquece Discovery com grafo do ai-tool (como lib, sem spawn/MCP).
638
647
  * Preenche clientFiles/serverFiles a partir do mapa do projeto.
639
648
  * Nunca joga: falha do ai-tool mantém listas vazias (honesto).
640
649
  */
@@ -724,8 +733,8 @@ declare function findPublicAllowsContent(rawContent: string): {
724
733
  }[];
725
734
 
726
735
  /**
727
- * Checks V1+V2 (FBA001–FBA004, FBA009–FBA013, subconjunto intencional).
728
- * Fingerprints v2: FBA001/FBA002 por regra-lógica + hash da condição
736
+ * Checks (FBA001–FBA004, FBA009–FBA013, subconjunto intencional).
737
+ * Fingerprints: FBA001/FBA002 por regra-lógica + hash da condição
729
738
  * (estáveis a formatação, únicos por regra duplicada);
730
739
  * FBA003/FBA004/FBA010/FBA012 com linha (únicos por ocorrência).
731
740
  */
@@ -736,7 +745,7 @@ declare function runChecks(model: ProjectModel, rootDir: string, opts?: {
736
745
  }): Promise<Finding[]>;
737
746
 
738
747
  /**
739
- * scan — orquestra Discovery → Rules → Checks (V1 static-first).
748
+ * scan — orquestra Discovery → Rules → Checks (estático primeiro).
740
749
  */
741
750
 
742
751
  interface ScanOptions {
@@ -744,7 +753,7 @@ interface ScanOptions {
744
753
  strict?: boolean;
745
754
  /**
746
755
  * Opt-in: enriquece Discovery com grafo do ai-tool (clientFiles/serverFiles).
747
- * Default false (scan static-first rápido; nenhum check consome o grafo na 0.4.0).
756
+ * Default false (scan estático rápido; nenhum check consome o grafo por padrão).
748
757
  */
749
758
  graph?: boolean;
750
759
  }
@@ -752,14 +761,16 @@ interface ScanSummary {
752
761
  errors: number;
753
762
  warnings: number;
754
763
  infos: number;
755
- /** Compat: mesmo que passedChecks. */
756
- passed: number;
757
- /** Compat: mesmo que totalChecks. */
758
- total: number;
759
764
  passedChecks: number;
760
765
  totalChecks: number;
761
- /** Findings de infra (fora dos checks FBA): rules/contrato/índices/model. */
762
- infraSkipped: number;
766
+ /** Infra nomeada (fora dos checks FBA): cada item diz qual e por quê. */
767
+ infra: {
768
+ rule: string;
769
+ message: string;
770
+ }[];
771
+ /** O que foi olhado e o que foi pulado — nunca dizer "tudo certo" sem lista. */
772
+ checked: string[];
773
+ skipped: string[];
763
774
  /** Cobertura de Rules (0-100) quando verify com coverage avalia; null = não observada.
764
775
  * Sempre null no scan() por desenho (cobertura só via verify coveragePct). */
765
776
  coveragePct: number | null;
@@ -772,11 +783,11 @@ interface ScanResult {
772
783
  declare function scan(rootDir: string, opts?: ScanOptions): Promise<ScanResult>;
773
784
 
774
785
  /**
775
- * V3 — Verifier (Emulator) como interface honesta.
786
+ * Verifier (Emulator) como interface honesta.
776
787
  *
777
788
  * Não executa o Emulator sozinho (precisa de firebase-tools + Java no host).
778
- * Gera a matriz de casos (TestCase → Scenario) e avalia cobertura quando
779
- * o JSON do endpoint `:ruleCoverage` é fornecido (--coverage).
789
+ * Gera a matriz de casos e avalia cobertura quando
790
+ * o JSON do endpoint `ruleCoverage` é fornecido (--coverage).
780
791
  */
781
792
 
782
793
  interface VerifyCase {
@@ -804,10 +815,9 @@ declare function planVerify(model: ProjectModel): VerifyCase[];
804
815
  declare function verify(model: ProjectModel, opts?: VerifyOptions): VerifyResult;
805
816
 
806
817
  /**
807
- * V4 — Drift local vs implantado (comparação honesta de índices).
818
+ * Drift local vs implantado (comparação honesta de índices).
808
819
  *
809
- * Compara `firestore.indexes.json` local contra um JSON remoto
810
- * (`gcloud firestore indexes composite list --format=json` ou REST).
820
+ * Compara `firestore.indexes.json` local contra um JSON remoto.
811
821
  * Nunca afirma "unused" — apenas LOCAL_ONLY / REMOTE_ONLY / MATCHED.
812
822
  */
813
823
 
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createMcpServer,
3
3
  startMcpServer
4
- } from "./chunk-IXMAWDJH.js";
4
+ } from "./chunk-QACXCMYB.js";
5
5
  import {
6
6
  AccessOriginSchema,
7
7
  AuditYamlSchema,
@@ -26,12 +26,17 @@ import {
26
26
  RuleSchema,
27
27
  SERVER_HINTS_SEGMENTS,
28
28
  SeveritySchema,
29
- buildFullPath,
30
29
  classifyOrigin,
31
- conditionKeyForFingerprint,
32
30
  discover,
33
31
  emptyModel,
34
32
  enrichWithGraph,
33
+ runChecks,
34
+ scan,
35
+ validateClaimSamples
36
+ } from "./chunk-GDO2DLNQ.js";
37
+ import {
38
+ buildFullPath,
39
+ conditionKeyForFingerprint,
35
40
  extractRules,
36
41
  extractRulesContent,
37
42
  findPublicAllows,
@@ -48,19 +53,16 @@ import {
48
53
  parseRuleFunctionsDetailed,
49
54
  resolveHelperCondition,
50
55
  resolveHelperConditionDetailed,
51
- runChecks,
52
- scan,
53
56
  stripOuterParens,
54
- stripRuleComments,
55
- validateClaimSamples
56
- } from "./chunk-3SEVYYCX.js";
57
+ stripRuleComments
58
+ } from "./chunk-7EWTBIKJ.js";
57
59
  import {
58
60
  planVerify,
59
61
  verify
60
- } from "./chunk-PSSMYRRU.js";
62
+ } from "./chunk-Y4VXWSKX.js";
61
63
  import {
62
64
  drift
63
- } from "./chunk-NJBCSW7E.js";
65
+ } from "./chunk-XZ32YQQC.js";
64
66
 
65
67
  // src/index.ts
66
68
  import { createRequire } from "module";
package/dist/mcp-cli.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startMcpServer
4
- } from "./chunk-IXMAWDJH.js";
5
- import "./chunk-3SEVYYCX.js";
6
- import "./chunk-PSSMYRRU.js";
7
- import "./chunk-NJBCSW7E.js";
4
+ } from "./chunk-QACXCMYB.js";
5
+ import "./chunk-GDO2DLNQ.js";
6
+ import "./chunk-7EWTBIKJ.js";
7
+ import "./chunk-Y4VXWSKX.js";
8
+ import "./chunk-XZ32YQQC.js";
8
9
 
9
10
  // src/mcp-cli.ts
10
11
  startMcpServer().catch((err) => {
package/dist/mcp.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  createMcpServer,
3
3
  startMcpServer
4
- } from "./chunk-IXMAWDJH.js";
5
- import "./chunk-3SEVYYCX.js";
6
- import "./chunk-PSSMYRRU.js";
7
- import "./chunk-NJBCSW7E.js";
4
+ } from "./chunk-QACXCMYB.js";
5
+ import "./chunk-GDO2DLNQ.js";
6
+ import "./chunk-7EWTBIKJ.js";
7
+ import "./chunk-Y4VXWSKX.js";
8
+ import "./chunk-XZ32YQQC.js";
8
9
  export {
9
10
  createMcpServer,
10
11
  startMcpServer
@@ -0,0 +1,44 @@
1
+ import {
2
+ buildFullPath,
3
+ conditionKeyForFingerprint,
4
+ extractRules,
5
+ extractRulesContent,
6
+ findPublicAllows,
7
+ findPublicAllowsContent,
8
+ hashShort,
9
+ helperBodyToCondition,
10
+ inlineHelperArgs,
11
+ inlineHelpersInCondition,
12
+ isPublicCondition,
13
+ normalizeTarget,
14
+ opsFrom,
15
+ parseMatchBlocks,
16
+ parseRuleFunctions,
17
+ parseRuleFunctionsDetailed,
18
+ resolveHelperCondition,
19
+ resolveHelperConditionDetailed,
20
+ stripOuterParens,
21
+ stripRuleComments
22
+ } from "./chunk-7EWTBIKJ.js";
23
+ export {
24
+ buildFullPath,
25
+ conditionKeyForFingerprint,
26
+ extractRules,
27
+ extractRulesContent,
28
+ findPublicAllows,
29
+ findPublicAllowsContent,
30
+ hashShort,
31
+ helperBodyToCondition,
32
+ inlineHelperArgs,
33
+ inlineHelpersInCondition,
34
+ isPublicCondition,
35
+ normalizeTarget,
36
+ opsFrom,
37
+ parseMatchBlocks,
38
+ parseRuleFunctions,
39
+ parseRuleFunctionsDetailed,
40
+ resolveHelperCondition,
41
+ resolveHelperConditionDetailed,
42
+ stripOuterParens,
43
+ stripRuleComments
44
+ };
@@ -0,0 +1,7 @@
1
+ import {
2
+ scan
3
+ } from "./chunk-GDO2DLNQ.js";
4
+ import "./chunk-7EWTBIKJ.js";
5
+ export {
6
+ scan
7
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  planVerify,
3
3
  verify
4
- } from "./chunk-PSSMYRRU.js";
4
+ } from "./chunk-Y4VXWSKX.js";
5
5
  export {
6
6
  planVerify,
7
7
  verify
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justmpm/firebase-audit",
3
- "version": "0.4.3",
3
+ "version": "0.5.1",
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
@@ -4,11 +4,11 @@ Skill para agentes usarem o `@justmpm/firebase-audit` do jeito certo.
4
4
 
5
5
  ## Sequência obrigatória
6
6
 
7
- 1. Descobrir projeto (`firebase.json`, `firestore.rules`, `firestore.indexes.json`)
8
- 2. Verificar `firebase-audit.yaml` (se não existe, V1 roda zero-config)
9
- 3. Inspecionar adapter de autorização (ex: `rbac.can`). Sem adapter resolvido UNKNOWN, nunca adivinhar
10
- 4. Executar `firebase-audit scan` (ou `--strict` com contrato)
11
- 5. Ler findings (CONFIRMED corrige agora; PROBABLE investiga; NOT_OBSERVED/UNKNOWN não afirma)
7
+ 1. Descobrir projeto (`firebase.json`, `firestore.rules`, `storage.rules`, `firestore.indexes.json`)
8
+ 2. Verificar `firebase-audit.yaml` (se não existe, scan roda zero-config)
9
+ 3. Inspecionar função de permissão no código que recebe o nome como texto (ex: `rbac.can`). Guarda de rota ou function do `.rules` (ex: `isTenantMember`) não vale como adapter sem adapter, permissões seguem UNKNOWN, nunca adivinhar
10
+ 4. Executar `firebase-audit scan` (ou `check` com contrato no CI)
11
+ 5. Ler findings (CONFIRMED corrige agora; PROBABLE investiga; NOT_OBSERVED/UNKNOWN não afirma) + `summary.skipped` (o que foi pulado)
12
12
  6. Corrigir código, nunca o contrato para esconder finding
13
13
  7. Reexecutar scan antes de declarar pronto
14
14
 
@@ -18,12 +18,15 @@ Skill para agentes usarem o `@justmpm/firebase-audit` do jeito certo.
18
18
  - Nunca transformar UNKNOWN em CONFIRMED sem teste no Emulator
19
19
  - Nunca dizer "unused" — apenas NOT_OBSERVED
20
20
  - Claims: 1000 bytes max, sem chaves OIDC reservadas, só controle de acesso
21
+ - AppCheck é carimbo do app oficial — não prova usuário nem tenant
22
+ - Logo pública (`get: if true` em `/logos/`) pode ser vitrine proposital — cofre é `write: if true` ou curinga total aberto
23
+ - Seeds/fixtures com Admin SDK moram em `scripts/` ou `server/`, nunca em `src/` (senão o scan acusa FBA003 de propósito)
21
24
 
22
- ## Emulador (V3)
25
+ ## Emulador
23
26
 
24
27
  ```bash
25
28
  firebase emulators:exec --only firestore "npm test"
26
29
  ```
27
30
 
28
31
  Matriz `anon/user/admin x read/create/update/delete` com `@firebase/rules-unit-testing`.
29
- Cobertura em `:ruleCoverage` (JSON). Emulator não prova índice.
32
+ Cobertura em `ruleCoverage` (JSON). Emulator não prova índice. Sem contrato, papéis reduzidos (anonymous/user/admin).
@@ -1,6 +0,0 @@
1
- import {
2
- scan
3
- } from "./chunk-3SEVYYCX.js";
4
- export {
5
- scan
6
- };