@justmpm/firebase-audit
v0.6.2
Published
Auditor de consistência e segurança para projetos Firebase: código + Rules + índices + contrato vs comportamento. Static-first, nunca inventa certeza.
Maintainers
Readme
@justmpm/firebase-audit
Auditor de consistência e segurança para projetos Firebase. Responde: "O sistema que você escreveu está coerente com o sistema que você pretendia construir?"
- Pacote:
@justmpm/firebase-audit - Comando:
firebase-audit scan | check | verify | drift | init | validate - Runtime: Node
>=20(ambiente de desenvolvimento usa a faixa compatível com Vitest 4/Vite 8). - Skill:
firebase-audit-skill(junto do pacote) - Modos: biblioteca + CLI + MCP (mesmo padrão do
@justmpm/ai-toole@justmpm/supergrep)
1. Ideia final
Cruzamos 4 fontes:
CÓDIGO (queries, writes, permission calls)
+ FIREBASE (firebase.json, firestore.rules, firestore.indexes.json)
+ CONTRATO (firebase-audit.yaml — intenção)
+ REALIDADE (Emulator + índices implantados)
↓
Estas coisas estão coerentes?4 motores:
1. DISCOVERY — acha tudo (usa ai-tool)
2. ANALYZER — monta ProjectModel de evidências + relações
3. VERIFIER — prova no Emulator + compara drift produção
4. FINDINGS — decide o que reportar (CONFIRMED / PROBABLE / NOT_OBSERVED / UNKNOWN)
QueryShapee o grafo são contratos de modelo/roadmap: a v0.5.25 ainda não extrai queries nem cruza query→índice.driftcompara apenas arquivos de índices fornecidos.
Princípio gravado no README:
Static analysis discovers evidence. The contract defines intent. The emulator verifies behavior. The auditor never upgrades uncertainty into certainty.
O que reaproveitamos (não reinventar)
@justmpm/ai-tool(v6.1.1, já é biblioteca) → Discovery + grafo + símbolos. Usar como dependência:
Proibido chamar via terminal (import { find, impact, context, map } from "@justmpm/ai-tool"; // find("rbac.can") → onde a função de permissão é definida + usada (pega alias) // impact("src/orders/delete.ts") → quem quebra se esse arquivo mudar (CLIENT vs SERVER) // context("src/orders/delete.ts") → assinaturas sem implementaçãospawn npx ai-tool) ou via MCP dentro do código. Chamada direta = tipada, rápida, testável.@justmpm/supergrep(^0.7.0, dependência direta) → acha candidatoswhere(),orderBy(),requirePermission()ignorando formatação/quebra de linha:
Regra: supergrep acha; uma chamada só vira FBA004import { executeFind } from "@justmpm/supergrep"; // executeFind('where($FIELD, $OP, $VAL)') → candidatos // FBA004 só confirma adapter qualificado; adapter simples sem identidade // semântica comprovada permanece PROBABLE/UNKNOWN.CONFIRMEDquando o adapter qualificado está explicitamente no contrato. Adapter simples sem identidade semântica comprovada permanecePROBABLE/UNKNOWN; nunca adivinha. Consequência no--strict: um adapter de um único segmento (requirePermission,can) não tem módulo para comparar, então a identidade nunca é confirmada — o resultado éADAPTER_IDENTITY_UNKNOWN, que bloqueia ocheck. Para passar no strict, configure o adapter comomodulo.funcao(ex.:rbac.can).
ProjectModel (núcleo)
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.
Unidade fundamental: OperationObservation (não separar query de write):
CLIENT → orders.delete → orders/{id} → delete → Rule X
SERVER → orders.delete → Admin SDK → Rules bypassed (vale IAM + auth da app)Schemas em Zod v4 (z.strictObject, z.record(chave, valor), z.json(), z.unknown().optional(), sem any).
Exemplo de QueryShape canônica (schema/roadmap; a v0.5.25 ainda não extrai queries):
// Código:
// query(collection(db, "orders"), where("userId", "==", uid), where("status", "==", s), orderBy("createdAt", "desc"))
{
scope: "COLLECTION",
collectionId: "orders",
pathTemplate: "orders",
filters: [
{ fieldPath: "userId", operator: "==", value: { kind: "symbolic", expression: "uid" } },
{ fieldPath: "status", operator: "==", value: { kind: "symbolic", expression: "s" } },
],
orderBy: [{ fieldPath: "createdAt", direction: "DESCENDING" }],
dynamic: false,
confidence: "CONFIRMED",
}
// Regras: filtros ordenados no fingerprint (where a + where b == where b + where a).
// orderBy preserva ordem. collectionGroup tem scope próprio (índice diferente).
// Valor dinâmico (user.status) não quebra shape; campo dinâmico (where(fieldVar)) → UNKNOWN.Exemplo de Finding (agente copia este formato):
{
rule: "FBA004",
severity: "ERROR",
confidence: "CONFIRMED",
file: "src/orders/delete.ts",
line: 42,
message: 'Permission "orders.delete" usada mas não declarada no contrato.',
fingerprint: "FBA004:src/orders/delete.ts:42:orders.delete",
evidence: [{ kind: "permission-call", summary: 'rbac.can("orders.delete")', confidence: "CONFIRMED" }],
fix: 'Declarar "orders.delete" em firebase-audit.yaml ou corrigir o nome da chamada.',
}Contrato YAML (intenção)
Agente sugere, humano aprova, auditor cobra. Agente nunca muda contrato para esconder finding.
authorization:
adapter:
function: "rbac.can"
roles:
admin:
permissions: [users.read, users.write, orders.read, orders.delete]
user:
permissions: [users.read, orders.read, orders.create]
permissions:
orders.delete:
resource: orders
operation: delete
claims:
enabled: true
roleKey: role
samples:
admin: { role: admin }
user: { role: user }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.
O resource representa um ID de coleção do Firestore: pode preservar camelCase, mas não pode ser ., .., conter / ou seguir o padrão reservado __.*__, além do limite de 1.500 bytes. Regras sem service cloud.firestore não são tratadas como cobertura observada: o parser gera RULES_PARSE_UNVERIFIED e o modo estrito não aprova esse arquivo.
Adapters configuráveis desde o dia 1 (requirePermission, rbac.can, authorize, etc.). Sem adapter resolvido → UNKNOWN, nunca adivinhar.
Rascunho seguro e aprovação humana
O firebase-audit init nunca cria autorização pronta para uso. Quando encontra firestore.rules, ele gera:
authorization.roleseauthorization.permissionsvazios;draft.adaptercomo sugestão (ounullquando nenhum adapter foi informado);draft.roles,draft.permissionserequiresHumanApproval: true.
Isso evita que um agente invente rbac.can, distribua roles ou autorize usuários sem evidência. O humano deve confirmar a função que realmente recebe o nome da permissão no código (adapter), revisar cada role/permission, mover o que foi aprovado para authorization e só então rodar validate e check. O arquivo de exemplo em templates/firebase-audit.yaml representa um contrato já revisado; ele não é o rascunho emitido por padrão.
Terminologia: client-code é a evidência localizada em código que pode entrar no navegador/app; não significa autorização. adapter é a função de autorização do backend que recebe o nome da permissão, como rbac.can; uma guarda de rota ou uma função dentro do .rules não cumpre esse papel.
Skill para agentes
O pacote publica skill/SKILL.md e templates/firebase-audit.yaml; não há pastas references/ ou examples/ nesta versão.
Trava principal: The contract is an assertion of intent. Do not modify the assertion to make implementation violations disappear.
Emulador e coverage (planejado/fornecido)
O verify atual é um planejador de matriz + agregador de coverage: ele não inicia o Emulator, não executa casos e não reprova o build. Para coverage real, rode o Emulator em um processo persistente (firebase emulators:start) ou capture ruleCoverage dentro do script executado por emulators:exec; depois passe o arquivo ao verify. O arquivo é marcado FILE_UNVERIFIED até haver essa prova. O verificador exige o envelope rules do endpoint e aceita hitCount, visitCount e visit_count; coverageExact preserva visitados/total, evitando que 99,9% seja exibido como 100%; wildcard recursivo gera aviso porque subcoleções não podem ser enumeradas automaticamente.
Limites assumidos: Emulator não prova índice (produção exige, ele deixa passar). Admin SDK ignora Rules. Rules não são filtros.
Níveis de certeza (obrigatório)
CONFIRMED— evidência forte ou teste reprovou no EmulatorPROBABLE— estático sugere, mas há dinâmico no caminho (ex: índice)NOT_OBSERVED— não achou uso (nunca dizer "inútil" / "sem uso")UNKNOWN— impossível resolver estaticamente (ex:where(fieldVar, ...))CONFLICTING— interno, duas evidências se contradizem (ex: typoorder.deletevsorders.delete)
10 checks implementados (+ avisos de infra e Storage/AppCheck)
| # | Check | Sev | Conf |
|---|-------|-----|------|
| FBA001 | allow write/create/update/delete público (sem login ou auth == null alcançável: ERROR; dúvida COM login: WARNING com motivo) | ERROR/WARNING | CONFIRMED/PROBABLE |
| FBA002 | allow read/get/list público (pode ser proposital; com login + dúvida: motivo citado) | WARNING | CONFIRMED/PROBABLE |
| FBA003 | Admin SDK em arquivo de cliente (runtime import/export; tipo puro não conta; origem indefinida como WARNING/NOT_OBSERVED) | ERROR/WARNING | PROBABLE/UNKNOWN |
| FBA004 | Permission usada e não declarada | ERROR/WARNING | CONFIRMED/PROBABLE |
| FBA009 | Adapter não observado (com hint de portaria tenant) | WARNING/INFO | UNKNOWN/NOT_OBSERVED |
| FBA010 | Permissão dinâmica (não resolvida estaticamente) | INFO (WARNING no --strict, onde bloqueia) | UNKNOWN |
| FBA011 | Role referencia permission não declarada | ERROR | CONFIRMED |
| FBA012 | Rule ampla vs contrato admin-only | WARNING | PROBABLE |
| FBA013 | Role sem permissions | WARNING | CONFIRMED |
| FBA014 | Create/update/write próprio em admins/ sem gate de admin (auto-promoção) | WARNING | PROBABLE |
| AUTH_ANON_ALLOWED | auth != null sozinho (anônimo passa) | INFO | PROBABLE |
| STORAGE_* | Escrita aberta (ERROR) / curinga total na raiz (WARNING) / curinga em pasta dedicada ou leitura aberta = vitrine a confirmar (INFO, prefira get sem list) / anônimo (INFO, por bloco do match) | ERROR/WARNING/INFO | CONFIRMED/PROBABLE |
| APPCHECK_* | Carimbo observado / verificação manual (onRequest exige verifyToken por rota) | INFO | CONFIRMED/PROBABLE |
IDs FBA005–FBA008 são legados aposentados (sem check fantasma): o conjunto atual é FBA001–FBA004 + FBA009–FBA014.
No modo estrito, enforceAppCheck só conta quando ligado no primeiro argumento de um onCall cujo binding foi observado em import oficial (firebase-functions/v2) ou na forma v1 runWith({ enforceAppCheck: true }).https.onCall. Um callable v1 sem runWith protegido, flags dentro do handler, homônimos locais, aliases sem binding e onRequest sem verifyToken não fecham cobertura. Triggers de background não têm superfície AppCheck aplicável e não reprovam o strict sozinhos. Imports dentro de strings nunca contam como binding.
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; no --strict, qualquer database fora do default precisa de cobertura explícita.
summary.implementedChecks lista os checks FBA cobrados (10); summary.checked lista áreas olhadas (arquivos); summary.failedChecks contém FBA com ERROR (o gate do CI), summary.infraFailed contém infra com ERROR (ex STRICT_*, também reprova — nunca errors > 0 com as duas listas vazias), warningChecks contém WARNING, infoOnlyChecks contém INFO e notObservedChecks separa check sem cobertura (sem cobertura não é regressão, ver scoreNote). summary.infra resume por regra+pasta. Contrato cobre nomes e admin-only vs Rules; alcance papel×código e function sem guarda só se prova no Emulator.
Condições avaliadas por lógica no Storage e no Firestore FBA001/FBA002 (precedência ! > && > ||, strings ignoradas, erro em null nega até sob !, erro na condição de ternário também nega): auth && helper com login dominante não vira falso público; comparações encadeadas como request.auth != null == false são tratadas pelo valor ausente real; auth == null/auth || neutro continuam PROBABLE/ERROR; chamadas de lista/CEL com auth aninhado ficam UNKNOWN; dúvida COM login é WARNING com motivo; mesma fingerprint agrega multi-linha; drift sem lado visto emite nota única (DRIFT_REMOTE/LOCAL_NOT_OBSERVED) com entries mantidas. Claims e papéis são case-sensitive (admin ≠ Admin).
Contrato: firebase-audit init gera rascunho seguro a partir das Rules (preserva get/list/create/update/delete, subcoleções e resource camelCase com permission name válido), exige o envelope service cloud.firestore { match /databases/{database}/documents { ... } }, falha quando firestore.rules não foi observado, contém operação desconhecida ou contém allow não compreendido, respeita o limite de 256 KB e nunca fabrica placeholder; wildcard recursivo recebe aviso de subcoleções não enumeradas. Em Storage, o serviço e o envelope /b/{bucket}/o precisam estar aninhados. Configuração Firestore explícita sem rules/indexes válidos nunca usa o arquivo default. A escrita sem --force é exclusiva e --force rejeita symlink; validate usa o mesmo validador semântico do scan (adapter sem $, claims e referências de permissions existentes), só lê arquivos regulares dentro da raiz e não devolve a linha problemática do YAML ao MCP. MCP tem 7 tools (skill/scan/check/verify/drift/init/validate) com Roots explícitas, rate limit por instância, concorrência máxima, timeout/cancelamento, output de findings sem texto cru e responseFormat: summary|detailed com cursor offset:NN em scan/check/verify/drift; summary omite detalhes e não expõe cursor útil, e summaryOnly continua valendo no verify/drift. verify marca coverage como FILE_UNVERIFIED até haver prova do Emulator e usa o path da coleção em operações list; a matriz tem teto de 10.000 casos e falha explicitamente ao excedê-lo; drift assume queryScope: COLLECTION quando ausente, aceita COLLECTION_RECURSIVE, inclui apiScope, density, multikey, unique, searchConfig e searchIndexOptions na identidade, rejeita modos concorrentes, distingue REMOTE_PENDING de REMOTE_FAILED (NEEDS_REPAIR/NON_FUNCTIONAL), preserva dimensão vetorial e normaliza __name__ implícito somente em STANDARD (Enterprise mantém a distinção); páginas ou entradas incompletas ficam pendentes, nunca definitivas. Em --strict, databases Firestore adicionais, buckets Storage não observados, configuração/sintaxe inválida, remoteSource não observável e cobertura de código incompleta reprovam; AST usa somente fontes elegíveis, incluindo .mts/.cts staged para o parser, tem teto agregado de bytes e incompletude nunca vira verde. functions.source: "." não prova isolamento: código compartilhado fica UNKNOWN, e paths server explícitos continuam server.
Hardening de cobertura e limites atuais
- Rules customizadas precisam terminar em
return;matchdeve apontar para documento, ficar dentro do serviço e não pode haverallowfora dessa hierarquia. - Storage Rules v1 trata
readcomoget;listexigerules_version = '2'. A opção AppCheck só fecha quando o valor efetivo é determinístico: spread, chave duplicada, binding local ou decorator Python sem import oficial permanecem não observados. - Só o codebase
functions/declarado e os Route Handlerspages/apieapp/api/route.*provam servidor por convenção.app/apisemroute.*e pastas genéricas comoserver//backend/scriptsficamUNKNOWN: oscanemiteADMIN_ORIGIN_UNKNOWNe o--strictreprova, porque o caminho sozinho não prova que o arquivo entrou no bundle. A opção de grafo (graph: true) existe na API mas nenhum check a consome ainda — para passar no strict, mova o arquivo para o source de Functions declarado. - Staging AST falha fechado em colisões de nomes; imports Admin dinâmicos com template literal e fontes Python também entram no FBA003, enquanto strings/comentários Python não contam como AppCheck.
initdescarta condições sempre falsas; Rules v2 aceita wildcard recursivo no meio do path; envelope ausente, gateget()sob negação aninhada elistcom wildcard não global não viram cobertura/veredito inventado.- Python de teste não entra como Admin de produção; ambientes virtuais e
site-packagesficam fora da coleta de código. No drift,DENSITY_UNSPECIFIEDusa o default da edição, efirebase.jsonaceitaeditionem qualquer caixa, mas rejeitadataAccessModefora de Enterprise. - No modo exploratório, Rules/Storage sem serviço ou envelope podem aparecer como candidatos; no
strict, a coleta incompleta bloqueia a cobertura e não transforma esses candidatos em vereditos dependentes observados. - Import dinâmico com constante simples é resolvido no FBA003; import com módulo desconhecido deixa a coleta explicitamente não observada, e
get()amarrado ao uid vale como doc-gate de FBA014. - Coverage usa
visitCountquando disponível;hitCounté fallback. Contadores negativos, fracionários ou profundidade acima do limite são rejeitados. Lado de drift observado como lista vazia continua sendo observado; ausência do outro lado ainda gera nota. - Fingerprints usam 128 bits (32 hexadecimal).
initevalidatetambém respeitam timeout/cancelamento do MCP; YAML não aceita aliases. - A leitura de Rules e arquivos de projeto exige UTF-8 válido; bytes inválidos viram arquivo ilegível em vez de texto substituído silenciosamente.
roleKeyrepresenta um custom claim emitido pelo backend; claims de perfil/provedor (name,email,picture,sign_in_provider, etc.) são rejeitados como fonte de papel, e FBA014 exige doc-gate amarrado arequest.auth.uid.- O parser aplica os limites estruturais do Firestore Rules: profundidade de
match10, 100 segmentos de path, 20 captures, profundidade de chamada 20, 7 argumentos, 10 bindingslete 1000 expressões; source acima de 256 KB fica não observada. Wildcards recursivos múltiplos, recursão indireta e código depois dereturnnão fecham parsing. require('./rbac').cané reconhecido como identidade do adapter sem confundir a própria declaração com um homônimo local; import dinâmico com parâmetro sombreado eTYPE_CHECKINGPython não produzem FBA003 inventado.- FBA014 avalia os caminhos de
||,&&e ternários aninhados; um gate em apenas um ramo não silencia o risco de auto-promoção, enquanto admin nos dois caminhos é aceito. Erros de shape em uma seção dofirebase.jsonnão descartam sources válidos de outras seções.
Hardening da auditoria final
FBA014 respeita a semântica do curinga recursivo: Rules v1 exigem pelo menos um segmento, enquanto Rules v2 permitem zero ou mais; coleções
adminsaninhadas continuam no escopo.O índice vetorial aceita
dimensionsomente dentro devectorConfig; um campo top-level é inválido tanto noscanquanto nodrift.import(\${...}`)com interpolação fica como módulo não resolvido e marca a cobertura AST comoNOT_OBSERVED`; constantes simples continuam resolvidas.O alias
helpsó é global no primeiro argumento do CLI, para que valores como--cwd helpsejam auditados normalmente.O
actionMCP usa apenas ações estruturais e não transportaroleKeynem texto controlado pelo projeto.initreconhece a coleção estática depois de wildcard recursivo v2 e mantémauthorization.adaptervazio no rascunho; o adapter informado fica somente emdraft.Imports AppChecklocated dentro de strings não confirmam binding;
require/import()com specifier não resolvido e imports Pythonimportlib/__import__mantêm a cobertura fail-closed.Wildcards recursivos malformados e condições Rules com operador/operando pendente viram
RULES_PARSE_UNVERIFIED.O
actionMCP é allowlisted por regra;finding.fixcom permission, role ou roleKey do projeto não atravessa a fronteira.
Decisões (porquês — não reverter sem discutir)
- Static-first, Verifier como interface. V1 entrega valor sem Emulator. Verifier entra depois sem reescrever o núcleo.
- Índice faltando é sempre PROBABLE no V1. Emulator deixa passar query sem índice, produção barra. Afirmar CONFIRMED seria mentir.
- Sem adapter → UNKNOWN, nunca adivinhar. Cada projeto autoriza de um jeito (
requirePermission,rbac.can,if role===). Chutar gera falso positivo. - 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. - Evidência antes de veredito. Analyzer registra pista com local + certeza; check decide depois. Por isso
NOT_OBSERVEDnunca vira "inútil" eUNKNOWNnunca viraCONFIRMEDsem teste real. - "Leitura pública" é WARNING, não ERROR. Pode ser blog/página pública proposital. Confirmamos o acesso aberto, não a vulnerabilidade.
2. Escopo
Scan: Discovery + checks + firebase-audit scan [--json|--strict]. Zero-config, valor imediato.
Check com contrato: YAML + Code ↔ Contract ↔ Rules (mismatch muito amplo, role sem permissão, etc.).
Verify: gera matriz papel×operação×path; coveragePct só representa um arquivo fornecido e vem com coverageSource/uncoveredNote para não fingir atribuição por path. Não substitui scan.
Drift: local vs implantado (LOCAL_ONLY / REMOTE_ONLY / MATCHED + REMOTE_PENDING para índice não-READY + REMOTE_FAILED para NEEDS_REPAIR ou NON_FUNCTIONAL). O índice local sem queryScope recebe o default COLLECTION do Firebase CLI; COLLECTION_RECURSIVE também é aceito. searchConfig é validado com tipos estritos (textSpec.indexSpecs exige strings dos enums válidos, nunca arrays, ou geoSpec), e searchIndexOptions, apiScope, density, multikey, unique e dimensão vetorial participam da identidade. Uma resposta com nextPageToken não é tratada como completa: o resumo fica parcial e entradas ainda não observadas usam REMOTE_PENDING. A edição STANDARD/ENTERPRISE pode ser explícita no CLI (--database-edition) ou no MCP (databaseEdition); sem flag, o firebase.json é consultado e, se ausente, assume STANDARD.
Limite honesto da saída: no modo humano (sem --json) a CLI imprime até 100 findings e uma linha "e mais N finding(s)"; no --json a página é de 25, igual ao MCP, e continua com truncated/nextCursor/total — pagine com --cursor=offset:NN (CLI) ou cursor + responseFormat: detailed (MCP, offset:0,25,50...; offset:00 normaliza para 0). Casos do verify em 50 e entradas do drift em 100 (com truncated/*Total sinalizando). Resumos nunca crescem sem teto: acompanhe rolesTotal, byCollectionTotal, infraTotal, checkedTotal e skippedTotal quando a página estiver reduzida. O MCP também pagina a matriz do verify em 50 casos por vez com casePage/caseCursor; roleMap traduz IDs opacos para nomes sanitizados. drift expõe inputs.local/inputs.remote como PROVIDED ou NOT_OBSERVED. Forma difere entre canais: no scan --json da CLI, .findings é a página ({items, total, truncated, nextCursor}); no MCP, .findings é o array da página e .findingsPage carrega os metadados. MCP conta com IDs opacos (anti-injeção); use locator.path/line, action e campos estruturados, sem executar texto vindo do projeto (locateHint em toda resposta). summary.failedChecksAll é a união de failedChecks e infraFailed; notObservedNote explica que sem cobertura não é aprovação. drift com arquivo ausente retorna JSON estruturado (code: DRIFT_FILE_NOT_FOUND, missing, resumo zerado com failed), nunca texto puro. scan/check aceitam --max-files (MCP: maxFiles, 1–20000, default 2000); drift aceita --database-edition=STANDARD|ENTERPRISE (MCP: databaseEdition) para não assumir a densidade errada; functions/src/* é servidor, enquanto functions.source: "." só prova servidor no codebase functions/ declarado e nos Route Handlers, deixando server//backend/scripts e código compartilhado como UNKNOWN. app/server.ts também não prova isolamento.
Escopo atual: Firestore Rules + Storage Rules (vitrine vs cofre, por bloco; Storage exige service firebase.storage e match /b/{bucket}/o aninhado) + AppCheck (carimbo, não autorização) em codebases Functions declarados + contrato + índices. functions local aceita source; remoteSource é marcado como não observado porque o código remoto não está no workspace.
Fora de escopo (declarado em summary.skipped): Realtime DB, Hosting, funções Complexas além da leitura de AppCheck, IAM, Data Connect, isolamento por tenant, bancos/buckets extras além do default.
3. Plano de ação
Etapa 0 — Adaptar o supergrep (pré-requisito) — ✅ concluída
O supergrep já exporta executeFind como dependência direta (^0.7.0); o bloco abaixo é histórico, mantido como contexto:
~~Hoje o supergrep só exporta startMcpServer. O find core está preso em src/tools/find.ts.~~
- Exportar
executeFind(pattern, options)egetTree()emsrc/index.tsdo supergrep. - Publicar
@justmpm/supergrepminor nova. - No firebase-audit, declarar
"@justmpm/supergrep": "^x.y.z"e importar direto (sem spawn, sem MCP). - Fallback: se a etapa travar, usar
@ast-grep/napidireto no firebase-audit temporariamente.
Etapa 1 — Esqueleto
mcps-ai/firebase-audit/compackage.json(@justmpm/firebase-audit, binfirebase-audit, deps:@justmpm/ai-tool,@justmpm/supergrep,zod,firebase-adminsó dev).- Schemas Zod v4:
ProjectModel,Finding(comfingerprint+metadata),AuditYaml. - CLI
scan --json --strict+ saída terminal/JSON. - Fixtures de projeto pequeno real.
Etapa 2 — Discovery (via ai-tool)
- Achar
firebase.json,firestore.rules,firestore.indexes.json,src/**,functions/**. - Classificar
CLIENT / SERVER / UNKNOWNusando grafo do ai-tool (cuidado comsrc/app/api/*que é servidor).
Etapa 3 — Extractors
- Rules: só estrutura + referências + locations (sem avaliar).
- Auth:
requirePermission("x")/ adapter configurado, seguindo referências de símbolo (pega alias). - Query:
where/orderBy/collection/collectionGroup→QueryShapecanônica (igualdades ordenadas por fingerprint,orderBypreserva ordem) + estadosLITERAL / RESOLVED / SYMBOLIC / UNKNOWN.
Etapa 4 — 10 checks + CLI
- Implementar FBA001–FBA004, FBA009–FBA014 com
fingerprintestável pra CI (v2: path + target normalizado + hash da condição, ex:FBA001:firestore.rules:orders:write:<hash>). - Regressão com fixtures.
Etapa 5 — V2 contrato + V3 emulator + V4 drift
- Na ordem, sem pular. Emulator dividido em
TestCase → Scenario → Runner(cenário completo comresource.data.ownerIdsó na V3).
4. Fontes validadas
- Firebase Firestore Docs: Emulator não rastreia compostos, coverage em
:ruleCoverage, Admin bypassa Rules, indices viafirestore.indexes.json/ gcloud / REST. - Firebase Auth Docs:
setCustomUserClaims+request.auth.token.*, 1000 bytes, chaves reservadas, claims só pra acesso. - Zod V4 Docs:
z.strictObject,z.record(key, value)com 2 args,z.json(),z.unknown().optional().
