@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 +11 -7
- package/dist/{chunk-NZPB7V2V.js → chunk-4EL5FUQG.js} +17 -4
- package/dist/{chunk-5KZCV2CB.js → chunk-JOM45NO4.js} +1 -1
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -2
- package/dist/mcp-cli.js +2 -2
- package/dist/mcp.js +2 -2
- package/dist/{scan-MG76BJPM.js → scan-FMH76BG3.js} +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +3 -2
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` (
|
|
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–
|
|
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
|
|
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
|
|
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 = [];
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
scan
|
|
4
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
5
|
-
import "./chunk-
|
|
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-
|
|
5
|
-
import "./chunk-
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justmpm/firebase-audit",
|
|
3
|
-
"version": "0.5.
|
|
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
|
|
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
|
|
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
|
|