@justmpm/firebase-audit 0.1.0

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/README.md ADDED
@@ -0,0 +1,249 @@
1
+ # @justmpm/firebase-audit
2
+
3
+ > Auditor de consistência e segurança para projetos Firebase.
4
+ > Responde: **"O sistema que você escreveu está coerente com o sistema que você pretendia construir?"**
5
+
6
+ - **Pacote:** `@justmpm/firebase-audit`
7
+ - **Comando:** `firebase-audit scan | check | verify`
8
+ - **Skill:** `firebase-audit-skill` (junto do pacote)
9
+ - **Modos:** biblioteca + CLI + MCP (mesmo padrão do `@justmpm/ai-tool` e `@justmpm/supergrep`)
10
+
11
+ ---
12
+
13
+ ## 1. Ideia final
14
+
15
+ Cruzamos 4 fontes:
16
+
17
+ ```text
18
+ CÓDIGO (queries, writes, permission calls)
19
+ + FIREBASE (firebase.json, firestore.rules, firestore.indexes.json)
20
+ + CONTRATO (firebase-audit.yaml — intenção)
21
+ + REALIDADE (Emulator + índices implantados)
22
+ ↓
23
+ Estas coisas estão coerentes?
24
+ ```
25
+
26
+ 4 motores:
27
+
28
+ ```text
29
+ 1. DISCOVERY — acha tudo (usa ai-tool)
30
+ 2. ANALYZER — monta ProjectModel de evidências + relações
31
+ 3. VERIFIER — prova no Emulator + compara drift produção
32
+ 4. FINDINGS — decide o que reportar (CONFIRMED / PROBABLE / NOT_OBSERVED / UNKNOWN)
33
+ ```
34
+
35
+ Princípio gravado no README:
36
+
37
+ > **Static analysis discovers evidence. The contract defines intent. The emulator verifies behavior. The auditor never upgrades uncertainty into certainty.**
38
+
39
+ ### O que reaproveitamos (não reinventar)
40
+
41
+ - `@justmpm/ai-tool` (v6.1.1, já é biblioteca) → Discovery + grafo + símbolos. Usar como **dependência**:
42
+ ```ts
43
+ import { find, impact, context, map } from "@justmpm/ai-tool";
44
+ // find("rbac.can") → onde a função de permissão é definida + usada (pega alias)
45
+ // impact("src/orders/delete.ts") → quem quebra se esse arquivo mudar (CLIENT vs SERVER)
46
+ // context("src/orders/delete.ts") → assinaturas sem implementação
47
+ ```
48
+ Proibido chamar via terminal (`spawn npx ai-tool`) ou via MCP dentro do código. Chamada direta = tipada, rápida, testável.
49
+ - `@justmpm/supergrep` (v0.6.0, hoje só MCP) → acha candidatos `where()`, `orderBy()`, `requirePermission()` ignorando formatação/quebra de linha. Usar como **dependência** após a Etapa 0:
50
+ ```ts
51
+ import { executeFind } from "@justmpm/supergrep";
52
+ // executeFind('where($FIELD, $OP, $VAL)') → candidatos
53
+ // depois validar cada candidato com ts-morph: importa de "firebase/firestore"?
54
+ ```
55
+ Regra: supergrep acha, ts-morph confirma. Nunca decidir só pelo texto do supergrep (ele é sintático, não resolve import nem tipo).
56
+ - Validação final sempre no AST com ts-morph (que já vem dentro do ai-tool).
57
+
58
+ ### ProjectModel (núcleo)
59
+
60
+ Modelo de **evidências + relações**, não só lista de entidades. Cada achado registra onde, por quê e com qual certeza. Depois os checks inferem em cima.
61
+
62
+ Unidade fundamental: `OperationObservation` (não separar query de write):
63
+
64
+ ```text
65
+ CLIENT → orders.delete → orders/{id} → delete → Rule X
66
+ SERVER → orders.delete → Admin SDK → Rules bypassed (vale IAM + auth da app)
67
+ ```
68
+
69
+ Schemas em Zod v4 (`z.strictObject`, `z.record(chave, valor)`, `z.json()`, `z.unknown().optional()`, sem `any`).
70
+
71
+ Exemplo de `QueryShape` canônica (agente copia este formato):
72
+
73
+ ```ts
74
+ // Código:
75
+ // query(collection(db, "orders"), where("userId", "==", uid), where("status", "==", s), orderBy("createdAt", "desc"))
76
+ {
77
+ scope: "COLLECTION",
78
+ collectionId: "orders",
79
+ pathTemplate: "orders",
80
+ filters: { equalities: ["status", "userId"], inequalities: [], arrays: [] },
81
+ orderBy: [{ fieldPath: "createdAt", direction: "DESCENDING" }],
82
+ dynamic: false,
83
+ confidence: "CONFIRMED",
84
+ }
85
+ // Regras: igualdades ordenadas no fingerprint (where a + where b == where b + where a).
86
+ // orderBy preserva ordem. collectionGroup tem scope próprio (índice diferente).
87
+ // Valor dinâmico (user.status) não quebra shape; campo dinâmico (where(fieldVar)) → UNKNOWN.
88
+ ```
89
+
90
+ Exemplo de `Finding` (agente copia este formato):
91
+
92
+ ```ts
93
+ {
94
+ rule: "FBA004",
95
+ severity: "ERROR",
96
+ confidence: "CONFIRMED",
97
+ file: "src/orders/delete.ts",
98
+ line: 42,
99
+ message: 'Permission "orders.delete" usada mas não declarada no contrato.',
100
+ fingerprint: "FBA004:src/orders/delete.ts:orders.delete",
101
+ evidence: [{ kind: "permission-call", summary: 'rbac.can("orders.delete")', confidence: "CONFIRMED" }],
102
+ fix: 'Declarar "orders.delete" em firebase-audit.yaml ou corrigir o nome da chamada.',
103
+ }
104
+ ```
105
+
106
+ ### Contrato YAML (intenção)
107
+
108
+ Agente sugere, humano aprova, auditor cobra. Agente nunca muda contrato para esconder finding.
109
+
110
+ ```yaml
111
+ authorization:
112
+ adapter:
113
+ function: "rbac.can"
114
+ roles:
115
+ admin:
116
+ permissions: [users.read, users.write, orders.read, orders.delete]
117
+ user:
118
+ permissions: [users.read, orders.read, orders.create]
119
+ permissions:
120
+ orders.delete:
121
+ resource: orders
122
+ operation: delete
123
+ claims:
124
+ enabled: true
125
+ roleKey: role
126
+ samples:
127
+ admin: { role: admin }
128
+ user: { role: user }
129
+ ```
130
+
131
+ Detalhe: limite de 1000 bytes é do **payload do crachá** (custom claim), não do mapa role→permissions. Validar tamanho com `TextEncoder` + lista de chaves reservadas OIDC/Firebase em arquivo versionado separado.
132
+
133
+ Adapters configuráveis desde o dia 1 (`requirePermission`, `rbac.can`, `authorize`, etc.). Sem adapter resolvido → `UNKNOWN`, nunca adivinhar.
134
+
135
+ ### Skill para agentes
136
+
137
+ ```text
138
+ firebase-audit-skill/
139
+ ├── SKILL.md (sequência: descobrir → ver YAML → ver adapter → scan → ler findings → corrigir → re-scan)
140
+ ├── references/ (authorization, adapters, emulator, claims, indexes)
141
+ ├── examples/ (basic.yaml, rbac.yaml)
142
+ └── templates/ (firebase-audit.yaml)
143
+ ```
144
+
145
+ Trava principal: `The contract is an assertion of intent. Do not modify the assertion to make implementation violations disappear.`
146
+
147
+ ### Emulador 100% headless (sem navegador)
148
+
149
+ ```bash
150
+ firebase emulators:exec --only firestore "npm test"
151
+ curl http://localhost:8080/emulator/v1/projects/demo:ruleCoverage -o coverage.json
152
+ ```
153
+
154
+ Matriz `anon / user / admin x read / create / update / delete` com `@firebase/rules-unit-testing` (`unauthenticatedContext`, `authenticatedContext`). Reaproveita `firebase.json` e porta do projeto. Requer Java. Falha build se regra ficar sem cobertura.
155
+
156
+ Limites assumidos: Emulator não prova índice (produção exige, ele deixa passar). Admin SDK ignora Rules. Rules não são filtros.
157
+
158
+ ### Níveis de certeza (obrigatório)
159
+
160
+ - `CONFIRMED` — evidência forte ou teste reprovou no Emulator
161
+ - `PROBABLE` — estático sugere, mas há dinâmico no caminho (ex: índice)
162
+ - `NOT_OBSERVED` — não achou uso (nunca dizer "inútil" / "sem uso")
163
+ - `UNKNOWN` — impossível resolver estaticamente (ex: `where(fieldVar, ...)`)
164
+ - `CONFLICTING` — interno, duas evidências se contradizem (ex: typo `order.delete` vs `orders.delete`)
165
+
166
+ ### 10 checks do V1
167
+
168
+ | # | Check | Sev | Conf |
169
+ |---|-------|-----|------|
170
+ | FBA001 | `allow write/create/update/delete: if true` | ERROR | CONFIRMED |
171
+ | FBA002 | `allow read: if true` (pode ser público proposital) | WARNING | CONFIRMED |
172
+ | FBA003 | Admin SDK dentro de boundary Client | ERROR | PROBABLE |
173
+ | FBA004 | Permission usada e não declarada | ERROR | CONFIRMED |
174
+ | FBA005 | Claim nas Rules não declarada no contrato (pega typo `admn`) | WARNING | PROBABLE |
175
+ | FBA006 | Query provavelmente exige composto não encontrado | WARNING | PROBABLE |
176
+ | FBA007 | Índice local sem query observada | INFO | NOT_OBSERVED |
177
+ | FBA008 | Rule sem operação observada | INFO | NOT_OBSERVED |
178
+ | FBA009 | Adapter não resolvido | WARNING | UNKNOWN |
179
+ | FBA010 | Query shape dinâmica | INFO | UNKNOWN |
180
+
181
+ Detalhes que quebram se deixar pra depois: `write` = create+update+delete (guardar origem), `OR` nas Rules (`isAdmin() || public==true`), `resource` vs `request.resource`, `collectionGroup` com escopo próprio, `databaseId` nomeado desde o dia 1.
182
+
183
+ ### Decisões (porquês — não reverter sem discutir)
184
+
185
+ 1. **Static-first, Verifier como interface.** V1 entrega valor sem Emulator. Verifier entra depois sem reescrever o núcleo.
186
+ 2. **Índice faltando é sempre PROBABLE no V1.** Emulator deixa passar query sem índice, produção barra. Afirmar CONFIRMED seria mentir.
187
+ 3. **Sem adapter → UNKNOWN, nunca adivinhar.** Cada projeto autoriza de um jeito (`requirePermission`, `rbac.can`, `if role===`). Chutar gera falso positivo.
188
+ 4. **Contrato YAML depois do scan, não antes.** Exigir ficha antes de mostrar valor mata adoção e cria drift. V1 infere, V2 cobra contrato no `--strict`.
189
+ 5. **Evidência antes de veredito.** Analyzer registra pista com local + certeza; check decide depois. Por isso `NOT_OBSERVED` nunca vira "inútil" e `UNKNOWN` nunca vira `CONFIRMED` sem teste real.
190
+ 6. **"Leitura pública" é WARNING, não ERROR.** Pode ser blog/página pública proposital. Confirmamos o acesso aberto, não a vulnerabilidade.
191
+
192
+ ---
193
+
194
+ ## 2. Escopo
195
+
196
+ **V1 — scan estático:** Discovery + 10 checks + `firebase-audit scan [--json|--strict]`. Zero-config, valor imediato.
197
+ **V2 — check com contrato:** YAML + `Code ↔ Contract ↔ Rules` (mismatch muito amplo, role sem permissão, etc.).
198
+ **V3 — verify:** gera cenários → roda no Emulator → tabela ALLOW/DENY + coverage.
199
+ **V4 — drift:** local vs implantado (`LOCAL_ONLY / REMOTE_ONLY / MATCHED`).
200
+
201
+ Fora do V1: Storage, Realtime DB, Hosting, App Check, Functions complexas, IAM, Data Connect.
202
+
203
+ ---
204
+
205
+ ## 3. Plano de ação
206
+
207
+ ### Etapa 0 — Adaptar o supergrep (pré-requisito)
208
+
209
+ Hoje o supergrep só exporta `startMcpServer`. O `find` core está preso em `src/tools/find.ts`.
210
+
211
+ 1. Exportar `executeFind(pattern, options)` e `getTree()` em `src/index.ts` do supergrep.
212
+ 2. Publicar `@justmpm/supergrep` minor nova.
213
+ 3. No firebase-audit, declarar `"@justmpm/supergrep": "^x.y.z"` e importar direto (sem spawn, sem MCP).
214
+ 4. Fallback: se a etapa travar, usar `@ast-grep/napi` direto no firebase-audit temporariamente.
215
+
216
+ ### Etapa 1 — Esqueleto
217
+
218
+ - `mcps-ai/firebase-audit/` com `package.json` (`@justmpm/firebase-audit`, bin `firebase-audit`, deps: `@justmpm/ai-tool`, `@justmpm/supergrep`, `zod`, `firebase-admin` só dev).
219
+ - Schemas Zod v4: `ProjectModel`, `Finding` (com `fingerprint` + `metadata`), `AuditYaml`.
220
+ - CLI `scan --json --strict` + saída terminal/JSON.
221
+ - Fixtures de projeto pequeno real.
222
+
223
+ ### Etapa 2 — Discovery (via ai-tool)
224
+
225
+ - Achar `firebase.json`, `firestore.rules`, `firestore.indexes.json`, `src/**`, `functions/**`.
226
+ - Classificar `CLIENT / SERVER / UNKNOWN` usando grafo do ai-tool (cuidado com `src/app/api/*` que é servidor).
227
+
228
+ ### Etapa 3 — Extractors
229
+
230
+ - Rules: só estrutura + referências + locations (sem avaliar).
231
+ - Auth: `requirePermission("x")` / adapter configurado, seguindo referências de símbolo (pega alias).
232
+ - Query: `where/orderBy/collection/collectionGroup` → `QueryShape` canônica (igualdades ordenadas por fingerprint, `orderBy` preserva ordem) + estados `LITERAL / RESOLVED / SYMBOLIC / UNKNOWN`.
233
+
234
+ ### Etapa 4 — 10 checks + CLI
235
+
236
+ - Implementar FBA001–FBA010 com `fingerprint` estável pra CI (`FBA001:firestore.rules:orders:write`).
237
+ - Regressão com fixtures.
238
+
239
+ ### Etapa 5 — V2 contrato + V3 emulator + V4 drift
240
+
241
+ - Na ordem, sem pular. Emulator dividido em `TestCase → Scenario → Runner` (cenário completo com `resource.data.ownerId` só na V3).
242
+
243
+ ---
244
+
245
+ ## 4. Fontes validadas
246
+
247
+ - Firebase Firestore Docs: Emulator não rastreia compostos, coverage em `:ruleCoverage`, Admin bypassa Rules, indices via `firestore.indexes.json` / gcloud / REST.
248
+ - Firebase Auth Docs: `setCustomUserClaims` + `request.auth.token.*`, 1000 bytes, chaves reservadas, claims só pra acesso.
249
+ - Zod V4 Docs: `z.strictObject`, `z.record(key, value)` com 2 args, `z.json()`, `z.unknown().optional()`.