@justmpm/firebase-audit 0.5.17 → 0.5.18

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 CHANGED
@@ -46,7 +46,7 @@ Princípio gravado no README:
46
46
  // context("src/orders/delete.ts") → assinaturas sem implementação
47
47
  ```
48
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:
49
+ - `@justmpm/supergrep` (^0.7.0, dependência direta) → acha candidatos `where()`, `orderBy()`, `requirePermission()` ignorando formatação/quebra de linha:
50
50
  ```ts
51
51
  import { executeFind } from "@justmpm/supergrep";
52
52
  // executeFind('where($FIELD, $OP, $VAL)') → candidatos
@@ -97,7 +97,7 @@ Exemplo de `Finding` (agente copia este formato):
97
97
  file: "src/orders/delete.ts",
98
98
  line: 42,
99
99
  message: 'Permission "orders.delete" usada mas não declarada no contrato.',
100
- fingerprint: "FBA004:src/orders/delete.ts:orders.delete",
100
+ fingerprint: "FBA004:src/orders/delete.ts:42:orders.delete",
101
101
  evidence: [{ kind: "permission-call", summary: 'rbac.can("orders.delete")', confidence: "CONFIRMED" }],
102
102
  fix: 'Declarar "orders.delete" em firebase-audit.yaml ou corrigir o nome da chamada.',
103
103
  }
@@ -176,13 +176,15 @@ Limites assumidos: Emulator não prova índice (produção exige, ele deixa pass
176
176
  | FBA011 | Role referencia permission não declarada | ERROR | CONFIRMED |
177
177
  | FBA012 | Rule ampla vs contrato admin-only | WARNING | PROBABLE |
178
178
  | FBA013 | Role sem permissions | WARNING | CONFIRMED |
179
- | FBA014 | Create/update próprio em `admins/` sem gate de admin (auto-promoção) | WARNING | PROBABLE |
179
+ | FBA014 | Create/update/write próprio em `admins/` sem gate de admin (auto-promoção) | WARNING | PROBABLE |
180
180
  | AUTH_ANON_ALLOWED | `auth != null` sozinho (anônimo passa) | INFO | PROBABLE |
181
- | STORAGE_* | Escrita aberta / curinga total / leitura aberta / anônimo (por bloco do match) | ERROR/WARNING/INFO | CONFIRMED/PROBABLE |
181
+ | 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 |
182
182
  | APPCHECK_* | Carimbo observado / verificação manual (onRequest por rota) | INFO | CONFIRMED/PROBABLE |
183
183
 
184
184
  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.
185
185
 
186
+ `summary.implementedChecks` lista os checks FBA cobrados (10); `summary.checked` lista áreas olhadas (arquivos) — não confundir um com o outro.
187
+
186
188
  ### Decisões (porquês — não reverter sem discutir)
187
189
 
188
190
  1. **Static-first, Verifier como interface.** V1 entrega valor sem Emulator. Verifier entra depois sem reescrever o núcleo.
@@ -208,9 +210,11 @@ Detalhes que quebram se deixar pra depois: `write` = create+update+delete (guard
208
210
 
209
211
  ## 3. Plano de ação
210
212
 
211
- ### Etapa 0 — Adaptar o supergrep (pré-requisito)
213
+ ### Etapa 0 — Adaptar o supergrep (pré-requisito) — ✅ concluída
214
+
215
+ O supergrep já exporta `executeFind` como dependência direta (`^0.7.0`); o bloco abaixo é histórico, mantido como contexto:
212
216
 
213
- Hoje o supergrep só exporta `startMcpServer`. O `find` core está preso em `src/tools/find.ts`.
217
+ ~~Hoje o supergrep só exporta `startMcpServer`. O `find` core está preso em `src/tools/find.ts`.~~
214
218
 
215
219
  1. Exportar `executeFind(pattern, options)` e `getTree()` em `src/index.ts` do supergrep.
216
220
  2. Publicar `@justmpm/supergrep` minor nova.
@@ -237,7 +241,7 @@ Hoje o supergrep só exporta `startMcpServer`. O `find` core está preso em `src
237
241
 
238
242
  ### Etapa 4 — 10 checks + CLI
239
243
 
240
- - Implementar FBA001–FBA010 com `fingerprint` estável pra CI (`FBA001:firestore.rules:orders:write`).
244
+ - 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>`).
241
245
  - Regressão com fixtures.
242
246
 
243
247
  ### Etapa 5 — V2 contrato + V3 emulator + V4 drift
@@ -1348,16 +1348,18 @@ async function scan(rootDir, opts = {}) {
1348
1348
  }
1349
1349
  if (ops.has("read") || ops.has("get") || ops.has("list")) {
1350
1350
  if (wildcard) {
1351
+ const scope = storageScopeOf(m.matchPath);
1352
+ const enumerates = ops.has("read") || ops.has("list");
1351
1353
  findings.push({
1352
1354
  rule: "STORAGE_ALL_PATHS_OPEN",
1353
- severity: "WARNING",
1355
+ severity: scope.isRoot ? "WARNING" : "INFO",
1354
1356
  confidence: "PROBABLE",
1355
- message: `Storage com leitura aberta em curinga total (${where}). Vale para tudo \u2014 se a inten\xE7\xE3o era s\xF3 logo, restrinja a /logos/{id}.`,
1357
+ message: scope.isRoot ? `Storage com leitura aberta em curinga total na raiz (${where}). Vale para o bucket inteiro \u2014 pelo OR, anula as pastas privadas dos outros blocos. Restrinja a pastas expl\xEDcitas (ex: /logos/{logoId}) com get: if true e write restrito.` : `Storage com leitura aberta em curinga da pasta ${scope.folder} (${where}). Pode ser vitrine proposital \u2014 mas o =** libera tudo abaixo da pasta${enumerates ? " e allow read/list permite enumerar" : ""}; para vitrine segura prefira allow get (sem list). Confirme que o write segue restrito.`,
1356
1358
  fingerprint: `STORAGE_ALL_PATHS_OPEN:${m.fpPath}:${normalizeTarget(m.target)}:${key}`,
1357
1359
  file: relStorage,
1358
1360
  line: m.line,
1359
1361
  evidence: [{ id: evId("all"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
1360
- fix: "Troque o curinga total por pastas expl\xEDcitas (ex: /logos/{logoId}) com get: if true e write restrito."
1362
+ fix: scope.isRoot ? "Troque o curinga total por pastas expl\xEDcitas (ex: /logos/{logoId}) com get: if true e write restrito." : "Para vitrine, troque allow read por allow get (sem list) na pasta p\xFAblica; valide no Emulator."
1361
1363
  });
1362
1364
  } else {
1363
1365
  const enumerates = ops.has("read") || ops.has("list");
@@ -1365,7 +1367,7 @@ async function scan(rootDir, opts = {}) {
1365
1367
  rule: "STORAGE_PUBLIC_READ",
1366
1368
  severity: "INFO",
1367
1369
  confidence: "PROBABLE",
1368
- message: enumerates ? `Storage com leitura aberta (${where}). Pode ser vitrine proposital \u2014 mas allow read/list permite enumerar a pasta; para vitrine segura prefira allow get. Confirme que o write segue restrito.` : `Storage com leitura aberta (${where}). Pode ser vitrine proposital (logos) \u2014 confirme que o write segue restrito.`,
1370
+ message: enumerates ? `Storage com leitura aberta (${where}). Pode ser vitrine proposital \u2014 mas allow read/list permite enumerar a pasta; para vitrine segura prefira allow get. Confirme que o write segue restrito.` : `Storage com leitura aberta (${where}). Pode ser vitrine proposital \u2014 confirme que o write segue restrito.`,
1369
1371
  fingerprint: `STORAGE_PUBLIC_READ:${m.fpPath}:${normalizeTarget(m.target)}:${key}`,
1370
1372
  file: relStorage,
1371
1373
  line: m.line,
@@ -1515,6 +1517,7 @@ async function scan(rootDir, opts = {}) {
1515
1517
  infos,
1516
1518
  passedChecks,
1517
1519
  totalChecks,
1520
+ implementedChecks: [...IMPLEMENTED_CHECKS],
1518
1521
  infra,
1519
1522
  checked,
1520
1523
  skipped,
@@ -1626,6 +1629,16 @@ function findNoAuthBranch(effective) {
1626
1629
  }
1627
1630
  return void 0;
1628
1631
  }
1632
+ function storageScopeOf(matchPath) {
1633
+ const segs = matchPath.split("/").filter((s) => s.length > 0);
1634
+ let rest = segs;
1635
+ if (rest.length >= 3 && rest[0] === "b" && rest[2] === "o") rest = rest.slice(3);
1636
+ const wildIdx = rest.findIndex((s) => /\{\s*\w+\s*=\s*\*\*\s*\}/.test(s));
1637
+ const prefix = wildIdx === -1 ? rest : rest.slice(0, wildIdx);
1638
+ const meaningful = prefix.filter((s) => !/^\{.*\}$/.test(s) && !/^\(.*\)$/.test(s));
1639
+ if (meaningful.length === 0) return { isRoot: true, folder: "/" };
1640
+ return { isRoot: false, folder: `/${meaningful.join("/")}/` };
1641
+ }
1629
1642
  function parseStorageAllows(clean) {
1630
1643
  const text = clean.replace(/\r\n?/g, "\n");
1631
1644
  const out = [];
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  scan
3
- } from "./chunk-NZPB7V2V.js";
3
+ } from "./chunk-4EL5FUQG.js";
4
4
  import {
5
5
  verify
6
6
  } from "./chunk-CSMT4PIP.js";
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  scan
4
- } from "./chunk-NZPB7V2V.js";
4
+ } from "./chunk-4EL5FUQG.js";
5
5
  import "./chunk-7J3ADEBW.js";
6
6
 
7
7
  // src/cli.ts
@@ -84,7 +84,7 @@ Uso: firebase-audit scan|check|verify|drift [--json] ...`);
84
84
  const { resolve: resolveRoot } = await import("path");
85
85
  const rootDir = cwdArg ? resolveRoot(process.cwd(), cwdArg) : process.cwd();
86
86
  if (cmd === "verify") {
87
- const { scan: scanForVerify } = await import("./scan-MG76BJPM.js");
87
+ const { scan: scanForVerify } = await import("./scan-FMH76BG3.js");
88
88
  const { verify } = await import("./verify-GQSR4FQL.js");
89
89
  const { resolve: resolveVerify } = await import("path");
90
90
  const coverageArg = getArg("coverage");
package/dist/index.d.ts CHANGED
@@ -773,6 +773,9 @@ interface ScanSummary {
773
773
  passedChecks: number;
774
774
  /** Total de checks FBA implementados. */
775
775
  totalChecks: number;
776
+ /** Quais são os checks FBA (ordem de IMPLEMENTED_CHECKS): `checked[]`
777
+ * lista ÁREAS olhadas (arquivos), este lista os CHECKS cobrados. */
778
+ implementedChecks: string[];
776
779
  /** Infra nomeada (fora dos checks FBA): cada item diz qual e por quê. */
777
780
  infra: {
778
781
  rule: string;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createMcpServer,
3
3
  startMcpServer
4
- } from "./chunk-5KZCV2CB.js";
4
+ } from "./chunk-JOM45NO4.js";
5
5
  import {
6
6
  AccessOriginSchema,
7
7
  AuditYamlSchema,
@@ -34,7 +34,7 @@ import {
34
34
  runChecks,
35
35
  scan,
36
36
  validateClaimSamples
37
- } from "./chunk-NZPB7V2V.js";
37
+ } from "./chunk-4EL5FUQG.js";
38
38
  import {
39
39
  buildFullPath,
40
40
  conditionKeyForFingerprint,
package/dist/mcp-cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startMcpServer
4
- } from "./chunk-5KZCV2CB.js";
5
- import "./chunk-NZPB7V2V.js";
4
+ } from "./chunk-JOM45NO4.js";
5
+ import "./chunk-4EL5FUQG.js";
6
6
  import "./chunk-7J3ADEBW.js";
7
7
  import "./chunk-CSMT4PIP.js";
8
8
  import "./chunk-LD3F73XI.js";
package/dist/mcp.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createMcpServer,
3
3
  startMcpServer
4
- } from "./chunk-5KZCV2CB.js";
5
- import "./chunk-NZPB7V2V.js";
4
+ } from "./chunk-JOM45NO4.js";
5
+ import "./chunk-4EL5FUQG.js";
6
6
  import "./chunk-7J3ADEBW.js";
7
7
  import "./chunk-CSMT4PIP.js";
8
8
  import "./chunk-LD3F73XI.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  scan
3
- } from "./chunk-NZPB7V2V.js";
3
+ } from "./chunk-4EL5FUQG.js";
4
4
  import "./chunk-7J3ADEBW.js";
5
5
  export {
6
6
  scan
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justmpm/firebase-audit",
3
- "version": "0.5.17",
3
+ "version": "0.5.18",
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
@@ -19,11 +19,12 @@ Skill para agentes usarem o `@justmpm/firebase-audit` do jeito certo.
19
19
  - Nunca dizer "unused" — apenas NOT_OBSERVED
20
20
  - Claims: 1000 bytes max, sem chaves OIDC reservadas, só controle de acesso
21
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
22
+ - Logo pública (`get: if true` em `/logos/`) pode ser vitrine proposital — cofre é `write: if true` ou curinga total na raiz; curinga em pasta dedicada com leitura aberta é vitrine INFO (prefira `get` sem `list`), não cofre
23
23
  - Storage resolve funções auxiliares do `.rules` antes de acusar (portaria com auth dentro não é "sem auth", inclui `let` do rules v2); helper não declarado vira PROBABLE com o nome na mensagem, nunca silêncio
24
24
  - Storage tolera CRLF (arquivo Windows) e path `/(unknown)` nunca repete fingerprint (sufixo de linha só no degenerado)
25
25
  - Regra com `auth != null && exists(admins/uid)` diz "login + documento via exists()", não "qualquer autenticado" — sem claim/provider, anônimo com login passa no conjunto se obtiver o doc
26
- - FBA014: `allow create/update` próprio em `admins/` sem claim/papel de admin nem doc-gate é auto-promoção provável (caso cadastro que escreve `role: 'admin'`) — doc-gate só vale amarrado ao uid em todo ramo de `||`; confirme o signup e o Emulator; `users/{uid}` + campo role e `admin` singular estão fora do escopo estático
26
+ - FBA014: `allow create/update/write` próprio em `admins/` sem claim/papel de admin nem doc-gate é auto-promoção provável (caso cadastro que escreve `role: 'admin'`) — doc-gate só vale amarrado ao uid em todo ramo de `||`; confirme o signup e o Emulator; `users/{uid}` + campo role, `admin` singular e `if false` (deny) estão fora do escopo estático
27
+ - `summary.implementedChecks` lista os checks FBA cobrados; `summary.checked` lista áreas olhadas (arquivos) — 10 checks, não 10 arquivos
27
28
  - Storage avalia disjunção por ramo (`auth || tamanho` acusa o ramo sem auth) e `return false` via helper é deny silencioso, igual ao `if false`
28
29
  - Seeds/fixtures com Admin SDK moram em `scripts/` ou `server/`, nunca em `src/` (senão o scan acusa FBA003 de propósito)
29
30