npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

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-tool e @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)

QueryShape e o grafo são contratos de modelo/roadmap: a v0.5.25 ainda não extrai queries nem cruza query→índice. drift compara 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:
    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ção
    Proibido chamar via terminal (spawn 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 candidatos where(), orderBy(), requirePermission() ignorando formatação/quebra de linha:
    import { 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.
    Regra: supergrep acha; uma chamada só vira FBA004 CONFIRMED quando o adapter qualificado está explicitamente no contrato. Adapter simples sem identidade semântica comprovada permanece PROBABLE/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 o check. Para passar no strict, configure o adapter como modulo.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.roles e authorization.permissions vazios;
  • draft.adapter como sugestão (ou null quando nenhum adapter foi informado);
  • draft.roles, draft.permissions e requiresHumanApproval: 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 Emulator
  • PROBABLE — 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: typo order.delete vs orders.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; match deve apontar para documento, ficar dentro do serviço e não pode haver allow fora dessa hierarquia.
  • Storage Rules v1 trata read como get; list exige rules_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 Handlers pages/api e app/api/route.* provam servidor por convenção. app/api sem route.* e pastas genéricas como server//backend/scripts ficam UNKNOWN: o scan emite ADMIN_ORIGIN_UNKNOWN e o --strict reprova, 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.
  • init descarta condições sempre falsas; Rules v2 aceita wildcard recursivo no meio do path; envelope ausente, gate get() sob negação aninhada e list com 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-packages ficam fora da coleta de código. No drift, DENSITY_UNSPECIFIED usa o default da edição, e firebase.json aceita edition em qualquer caixa, mas rejeita dataAccessMode fora 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 visitCount quando 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). init e validate també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.
  • roleKey representa 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 a request.auth.uid.
  • O parser aplica os limites estruturais do Firestore Rules: profundidade de match 10, 100 segmentos de path, 20 captures, profundidade de chamada 20, 7 argumentos, 10 bindings let e 1000 expressões; source acima de 256 KB fica não observada. Wildcards recursivos múltiplos, recursão indireta e código depois de return nã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 e TYPE_CHECKING Python 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 do firebase.json nã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 admins aninhadas continuam no escopo.

  • O índice vetorial aceita dimension somente dentro de vectorConfig; um campo top-level é inválido tanto no scan quanto no drift.

  • import(\${...}`)com interpolação fica como módulo não resolvido e marca a cobertura AST comoNOT_OBSERVED`; constantes simples continuam resolvidas.

  • O alias help só é global no primeiro argumento do CLI, para que valores como --cwd help sejam auditados normalmente.

  • O action MCP usa apenas ações estruturais e não transporta roleKey nem texto controlado pelo projeto.

  • init reconhece a coleção estática depois de wildcard recursivo v2 e mantém authorization.adapter vazio no rascunho; o adapter informado fica somente em draft.

  • Imports AppChecklocated dentro de strings não confirmam binding; require/import() com specifier não resolvido e imports Python importlib/__import__ mantêm a cobertura fail-closed.

  • Wildcards recursivos malformados e condições Rules com operador/operando pendente viram RULES_PARSE_UNVERIFIED.

  • O action MCP é allowlisted por regra; finding.fix com permission, role ou roleKey do projeto não atravessa a fronteira.

Decisões (porquês — não reverter sem discutir)

  1. Static-first, Verifier como interface. V1 entrega valor sem Emulator. Verifier entra depois sem reescrever o núcleo.
  2. Índice faltando é sempre PROBABLE no V1. Emulator deixa passar query sem índice, produção barra. Afirmar CONFIRMED seria mentir.
  3. Sem adapter → UNKNOWN, nunca adivinhar. Cada projeto autoriza de um jeito (requirePermission, rbac.can, if role===). Chutar gera falso positivo.
  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.
  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.
  6. "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.~~

  1. Exportar executeFind(pattern, options) e getTree() em src/index.ts do supergrep.
  2. Publicar @justmpm/supergrep minor nova.
  3. No firebase-audit, declarar "@justmpm/supergrep": "^x.y.z" e importar direto (sem spawn, sem MCP).
  4. Fallback: se a etapa travar, usar @ast-grep/napi direto no firebase-audit temporariamente.

Etapa 1 — Esqueleto

  • mcps-ai/firebase-audit/ com package.json (@justmpm/firebase-audit, bin firebase-audit, deps: @justmpm/ai-tool, @justmpm/supergrep, zod, firebase-admin só dev).
  • Schemas Zod v4: ProjectModel, Finding (com fingerprint + 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 / UNKNOWN usando grafo do ai-tool (cuidado com src/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 → QueryShape canônica (igualdades ordenadas por fingerprint, orderBy preserva ordem) + estados LITERAL / RESOLVED / SYMBOLIC / UNKNOWN.

Etapa 4 — 10 checks + CLI

  • Implementar FBA001–FBA004, FBA009–FBA014 com fingerprint está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 com resource.data.ownerId só na V3).

4. Fontes validadas

  • Firebase Firestore Docs: Emulator não rastreia compostos, coverage em :ruleCoverage, Admin bypassa Rules, indices via firestore.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().