@justmpm/firebase-audit 0.5.4 → 0.5.5
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 +18 -15
- package/dist/{chunk-RKQ3ZWWI.js → chunk-CHC7ZHUK.js} +6 -4
- package/dist/{chunk-Y4VXWSKX.js → chunk-DIO36A3I.js} +3 -2
- package/dist/{chunk-AKPQ4OAJ.js → chunk-ME6WO45S.js} +91 -26
- package/dist/cli.js +10 -5
- package/dist/index.d.ts +9 -6
- package/dist/index.js +3 -3
- package/dist/mcp-cli.js +3 -3
- package/dist/mcp.js +3 -3
- package/dist/{scan-YUXXYBRU.js → scan-HE5WAMWJ.js} +1 -1
- package/dist/{verify-WBVSESHD.js → verify-DECP4IUW.js} +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +2 -1
package/README.md
CHANGED
|
@@ -163,20 +163,22 @@ Limites assumidos: Emulator não prova índice (produção exige, ele deixa pass
|
|
|
163
163
|
- `UNKNOWN` — impossível resolver estaticamente (ex: `where(fieldVar, ...)`)
|
|
164
164
|
- `CONFLICTING` — interno, duas evidências se contradizem (ex: typo `order.delete` vs `orders.delete`)
|
|
165
165
|
|
|
166
|
-
###
|
|
166
|
+
### 9 checks implementados (+ avisos de infra e Storage/AppCheck)
|
|
167
167
|
|
|
168
168
|
| # | Check | Sev | Conf |
|
|
169
169
|
|---|-------|-----|------|
|
|
170
|
-
| FBA001 | `allow write/create/update/delete
|
|
171
|
-
| FBA002 | `allow read
|
|
172
|
-
| FBA003 | Admin SDK
|
|
170
|
+
| FBA001 | `allow write/create/update/delete` público | ERROR | CONFIRMED/PROBABLE |
|
|
171
|
+
| FBA002 | `allow read/get/list` público (pode ser proposital) | WARNING | CONFIRMED/PROBABLE |
|
|
172
|
+
| FBA003 | Admin SDK em arquivo de cliente (+ origem indefinida como INFO) | ERROR/INFO | PROBABLE/UNKNOWN |
|
|
173
173
|
| FBA004 | Permission usada e não declarada | ERROR | CONFIRMED |
|
|
174
|
-
|
|
|
175
|
-
|
|
|
176
|
-
|
|
|
177
|
-
|
|
|
178
|
-
|
|
|
179
|
-
|
|
|
174
|
+
| FBA009 | Adapter não observado (com hint de portaria tenant) | WARNING/INFO | UNKNOWN/NOT_OBSERVED |
|
|
175
|
+
| FBA010 | Permissão dinâmica (não resolvida estaticamente) | INFO | UNKNOWN |
|
|
176
|
+
| FBA011 | Role referencia permission não declarada | ERROR | CONFIRMED |
|
|
177
|
+
| FBA012 | Rule ampla vs contrato admin-only | WARNING | PROBABLE |
|
|
178
|
+
| FBA013 | Role sem permissions | WARNING | CONFIRMED |
|
|
179
|
+
| AUTH_ANON_ALLOWED | `auth != null` sozinho (anônimo passa) | INFO | PROBABLE |
|
|
180
|
+
| STORAGE_* | Escrita aberta / curinga total / leitura aberta / anônimo (por bloco do match) | ERROR/WARNING/INFO | CONFIRMED/PROBABLE |
|
|
181
|
+
| APPCHECK_* | Carimbo observado / verificação manual (onRequest por rota) | INFO | CONFIRMED/PROBABLE |
|
|
180
182
|
|
|
181
183
|
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
184
|
|
|
@@ -193,12 +195,13 @@ Detalhes que quebram se deixar pra depois: `write` = create+update+delete (guard
|
|
|
193
195
|
|
|
194
196
|
## 2. Escopo
|
|
195
197
|
|
|
196
|
-
**
|
|
197
|
-
**
|
|
198
|
-
**
|
|
199
|
-
**
|
|
198
|
+
**Scan:** Discovery + checks + `firebase-audit scan [--json|--strict]`. Zero-config, valor imediato.
|
|
199
|
+
**Check com contrato:** YAML + `Code ↔ Contract ↔ Rules` (mismatch muito amplo, role sem permissão, etc.).
|
|
200
|
+
**Verify:** gera matriz papel×operação×path → roda no Emulator → cobertura `ruleCoverage` (não substitui scan).
|
|
201
|
+
**Drift:** local vs implantado (`LOCAL_ONLY / REMOTE_ONLY / MATCHED`).
|
|
200
202
|
|
|
201
|
-
|
|
203
|
+
**Escopo atual:** Firestore Rules + Storage Rules (vitrine vs cofre, por bloco) + AppCheck (carimbo, não autorização) + contrato + índices.
|
|
204
|
+
**Fora de escopo (declarado em `summary.skipped`):** Realtime DB, Hosting, Functions complexas, IAM, Data Connect, isolamento por tenant, bancos/buckets extras além do default.
|
|
202
205
|
|
|
203
206
|
---
|
|
204
207
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
scan
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ME6WO45S.js";
|
|
4
4
|
import {
|
|
5
5
|
verify
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-DIO36A3I.js";
|
|
7
7
|
import {
|
|
8
8
|
drift
|
|
9
9
|
} from "./chunk-XZ32YQQC.js";
|
|
@@ -152,7 +152,7 @@ function createMcpServer() {
|
|
|
152
152
|
try {
|
|
153
153
|
assertCwd(cwd);
|
|
154
154
|
const { resolve } = await import("path");
|
|
155
|
-
const { model } = await scan(cwd);
|
|
155
|
+
const { summary: scanSummary, model } = await scan(cwd);
|
|
156
156
|
const res = verify(model, { coverageFile: coverage ? resolve(cwd, coverage) : void 0 });
|
|
157
157
|
const roles = model.authorization.roles.length > 0 ? model.authorization.roles.map((r) => r.name) : ["anonymous", "user", "admin"];
|
|
158
158
|
const truncated = res.cases.length > 50;
|
|
@@ -169,8 +169,10 @@ function createMcpServer() {
|
|
|
169
169
|
truncated,
|
|
170
170
|
uncovered: res.uncovered.slice(0, 20),
|
|
171
171
|
uncoveredTotal: res.uncovered.length,
|
|
172
|
+
uncoveredNote: res.uncoveredNote,
|
|
172
173
|
coveragePct: res.coveragePct,
|
|
173
|
-
findings: res.findings
|
|
174
|
+
findings: res.findings,
|
|
175
|
+
scan: { errors: scanSummary.errors, warnings: scanSummary.warnings, note: "verify n\xE3o substitui scan/check" }
|
|
174
176
|
},
|
|
175
177
|
null,
|
|
176
178
|
2
|
|
@@ -102,8 +102,9 @@ function verify(model, opts = {}) {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
+
const uncoveredNote = "uncovered lista os paths do modelo \u2014 atribui\xE7\xE3o contador\u2192path indispon\xEDvel (s\xF3 coverage 100% esvazia)";
|
|
105
106
|
if (coveragePct === 100) {
|
|
106
|
-
return { cases, uncovered: [], findings, coveragePct };
|
|
107
|
+
return { cases, uncovered: [], findings, coveragePct, uncoveredNote };
|
|
107
108
|
}
|
|
108
109
|
const seenPaths = /* @__PURE__ */ new Set();
|
|
109
110
|
for (const rule of model.rules) {
|
|
@@ -112,7 +113,7 @@ function verify(model, opts = {}) {
|
|
|
112
113
|
seenPaths.add(rule.path);
|
|
113
114
|
uncovered.push(rule.path);
|
|
114
115
|
}
|
|
115
|
-
return { cases, uncovered, findings, coveragePct };
|
|
116
|
+
return { cases, uncovered, findings, coveragePct, uncoveredNote };
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
export {
|
|
@@ -51,18 +51,35 @@ function discover(rootDir) {
|
|
|
51
51
|
if (firebaseJson) {
|
|
52
52
|
try {
|
|
53
53
|
const raw = JSON.parse(readFileSync(firebaseJson, "utf-8"));
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
const first = (v) => Array.isArray(v) ? v[0] : v;
|
|
55
|
+
const fs = first(raw.firestore);
|
|
56
|
+
const st = first(raw.storage);
|
|
57
|
+
if (typeof fs?.rules === "string") {
|
|
58
|
+
const p = join(rootDir, fs.rules);
|
|
56
59
|
if (existsSync(p)) rulesFile = p;
|
|
57
60
|
}
|
|
58
|
-
if (typeof
|
|
59
|
-
const p = join(rootDir,
|
|
61
|
+
if (typeof fs?.indexes === "string") {
|
|
62
|
+
const p = join(rootDir, fs.indexes);
|
|
60
63
|
if (existsSync(p)) indexesFile = p;
|
|
61
64
|
}
|
|
62
|
-
if (typeof
|
|
63
|
-
const p = join(rootDir,
|
|
65
|
+
if (typeof st?.rules === "string") {
|
|
66
|
+
const p = join(rootDir, st.rules);
|
|
64
67
|
if (existsSync(p)) storageFile = p;
|
|
65
68
|
}
|
|
69
|
+
const fns = raw.functions;
|
|
70
|
+
if (typeof fns === "string" && fns.length > 0) {
|
|
71
|
+
const p = join(rootDir, fns);
|
|
72
|
+
try {
|
|
73
|
+
if (statSync(p).isDirectory()) functionsDir = p;
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
} else if (fns && typeof fns === "object" && typeof fns.source === "string" && fns.source.length > 0) {
|
|
77
|
+
const p = join(rootDir, fns.source);
|
|
78
|
+
try {
|
|
79
|
+
if (statSync(p).isDirectory()) functionsDir = p;
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
}
|
|
66
83
|
} catch {
|
|
67
84
|
}
|
|
68
85
|
}
|
|
@@ -105,7 +122,7 @@ function classifyOrigin(file, content) {
|
|
|
105
122
|
if (content.includes("firebase-admin") || content.includes("firebase-functions")) {
|
|
106
123
|
return "CLIENT";
|
|
107
124
|
}
|
|
108
|
-
if (
|
|
125
|
+
if (hasSeg("src", "app", "components")) {
|
|
109
126
|
return "CLIENT";
|
|
110
127
|
}
|
|
111
128
|
return "UNKNOWN";
|
|
@@ -139,7 +156,7 @@ async function enrichWithGraph(rootDir, d) {
|
|
|
139
156
|
const ai = await import("@justmpm/ai-tool");
|
|
140
157
|
const res = await ai.map({ cwd: rootDir, format: "json" });
|
|
141
158
|
const files = Array.isArray(res.files) ? res.files : [];
|
|
142
|
-
const code = files.map((f) => String(f.path).replace(/\\/g, "/")).filter((p) => /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(p)).filter((p) => !p.includes("node_modules/") && !/(^|\/)dist\//.test(p)).slice(0, 500);
|
|
159
|
+
const code = files.map((f) => String(f.path).replace(/\\/g, "/")).filter((p) => /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(p)).filter((p) => !p.includes("node_modules/") && !/(^|\/)(dist|build|\.next|coverage|lib)\//.test(p)).slice(0, 500);
|
|
143
160
|
const { readFileSync: readSync, existsSync: existsSyncFn, statSync: statSync2 } = await import("fs");
|
|
144
161
|
const { join: joinP } = await import("path");
|
|
145
162
|
for (const rel of code) {
|
|
@@ -598,6 +615,10 @@ async function collectAdminImports(rootDir) {
|
|
|
598
615
|
"import $DEFAULT from '$MODULE'",
|
|
599
616
|
'import * as $NS from "$MODULE"',
|
|
600
617
|
"import * as $NS from '$MODULE'",
|
|
618
|
+
'export { $$$ITEMS } from "$MODULE"',
|
|
619
|
+
"export { $$$ITEMS } from '$MODULE'",
|
|
620
|
+
'export * from "$MODULE"',
|
|
621
|
+
"export * from '$MODULE'",
|
|
601
622
|
'import "$MODULE"',
|
|
602
623
|
"import '$MODULE'",
|
|
603
624
|
'const $X = require("$MODULE")',
|
|
@@ -796,7 +817,7 @@ var RoleNameSchema = z.string().regex(/^[a-z][a-z0-9_-]*$/);
|
|
|
796
817
|
var AuditYamlSchema = z.strictObject({
|
|
797
818
|
$schema: z.string().optional(),
|
|
798
819
|
authorization: z.strictObject({
|
|
799
|
-
adapter: z.strictObject({ function: z.string().min(1) }).
|
|
820
|
+
adapter: z.strictObject({ function: z.string().min(1) }).nullish(),
|
|
800
821
|
roles: z.record(RoleNameSchema, z.strictObject({ permissions: z.array(PermissionNameSchema) })),
|
|
801
822
|
permissions: z.record(
|
|
802
823
|
PermissionNameSchema,
|
|
@@ -804,9 +825,10 @@ var AuditYamlSchema = z.strictObject({
|
|
|
804
825
|
),
|
|
805
826
|
claims: z.strictObject({
|
|
806
827
|
enabled: z.boolean(),
|
|
807
|
-
|
|
828
|
+
// Reservado para o futuro (nenhum check consome ainda): opcional.
|
|
829
|
+
roleKey: z.string().min(1).optional(),
|
|
808
830
|
samples: z.record(RoleNameSchema, z.record(z.string().min(1), z.json())).optional()
|
|
809
|
-
}).
|
|
831
|
+
}).nullish()
|
|
810
832
|
})
|
|
811
833
|
});
|
|
812
834
|
var RESERVED_CLAIM_KEYS = [
|
|
@@ -860,11 +882,40 @@ async function scan(rootDir, opts = {}) {
|
|
|
860
882
|
try {
|
|
861
883
|
if (d.firebaseJson && existsSync3(d.firebaseJson)) {
|
|
862
884
|
const raw = JSON.parse(readFileSync3(d.firebaseJson, "utf-8"));
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
885
|
+
const asList = (v) => v === void 0 ? [] : Array.isArray(v) ? v : [v];
|
|
886
|
+
const entries = [];
|
|
887
|
+
for (const f of asList(raw.firestore)) {
|
|
888
|
+
entries.push({ kind: "rules", rel: f.rules, label: "firestore.rules" });
|
|
889
|
+
entries.push({ kind: "indexes", rel: f.indexes, label: "firestore.indexes" });
|
|
890
|
+
}
|
|
891
|
+
for (const s of asList(raw.storage)) {
|
|
892
|
+
entries.push({ kind: "storage-rules", rel: s.rules, label: "storage.rules" });
|
|
893
|
+
}
|
|
894
|
+
if (Array.isArray(raw.firestore) && raw.firestore.length > 1) {
|
|
895
|
+
findings_pre.push({
|
|
896
|
+
rule: "FIREBASE_JSON_ARRAY",
|
|
897
|
+
severity: "INFO",
|
|
898
|
+
confidence: "CONFIRMED",
|
|
899
|
+
message: `firebase.json com ${raw.firestore.length} databases \u2014 este scan audita s\xF3 o default. Bancos nomeados (databaseId) fora do escopo.`,
|
|
900
|
+
fingerprint: "FIREBASE_JSON_ARRAY:firestore",
|
|
901
|
+
evidence: [
|
|
902
|
+
{ id: "ev-config-array-fs", kind: "config-parse", summary: `${raw.firestore.length} databases`, confidence: "CONFIRMED" }
|
|
903
|
+
]
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
if (Array.isArray(raw.storage) && raw.storage.length > 1) {
|
|
907
|
+
findings_pre.push({
|
|
908
|
+
rule: "FIREBASE_JSON_ARRAY",
|
|
909
|
+
severity: "INFO",
|
|
910
|
+
confidence: "CONFIRMED",
|
|
911
|
+
message: `firebase.json com ${raw.storage.length} buckets \u2014 este scan audita s\xF3 o default. Buckets extras fora do escopo.`,
|
|
912
|
+
fingerprint: "FIREBASE_JSON_ARRAY:storage",
|
|
913
|
+
evidence: [
|
|
914
|
+
{ id: "ev-config-array-st", kind: "config-parse", summary: `${raw.storage.length} buckets`, confidence: "CONFIRMED" }
|
|
915
|
+
]
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
for (const [kind, rel, label] of entries.map((e) => [e.kind, e.rel, e.label])) {
|
|
868
919
|
if (typeof rel === "string" && rel.length > 0) {
|
|
869
920
|
if (!existsSync3(join3(rootDir, rel))) {
|
|
870
921
|
findings_pre.push({
|
|
@@ -1242,20 +1293,23 @@ async function scan(rootDir, opts = {}) {
|
|
|
1242
1293
|
rule: "APPCHECK_ENFORCED",
|
|
1243
1294
|
severity: "INFO",
|
|
1244
1295
|
confidence: "CONFIRMED",
|
|
1245
|
-
message: `enforceAppCheck observado (${appcheck.occurrences} ocorr\xEAncia(s) em ${appcheck.files} arquivo(s)). Carimbo do app oficial \u2014 n\xE3o prova quem \xE9 o usu\xE1rio nem o tenant. Autoriza\xE7\xE3o segue nas Rules/guardas.`,
|
|
1296
|
+
message: `enforceAppCheck observado (${appcheck.occurrences} ocorr\xEAncia(s) em ${appcheck.files} arquivo(s), fora de onRequest). Carimbo do app oficial \u2014 n\xE3o prova quem \xE9 o usu\xE1rio nem o tenant. Autoriza\xE7\xE3o segue nas Rules/guardas.`,
|
|
1246
1297
|
fingerprint: "APPCHECK_ENFORCED",
|
|
1247
1298
|
evidence: [{ id: "ev-appcheck", kind: "appcheck", summary: `${appcheck.occurrences} ocorr\xEAncias`, confidence: "CONFIRMED" }]
|
|
1248
1299
|
});
|
|
1249
|
-
} else if (appcheck.manual === 0) {
|
|
1300
|
+
} else if (appcheck.manual === 0 && appcheck.ignoredOnRequest === 0) {
|
|
1250
1301
|
skipped.push("functions sem enforceAppCheck literal nem verifyToken observado \u2014 AppCheck sem cobertura afirmativa (config via vari\xE1vel conta como n\xE3o observada; onRequest exige verifica\xE7\xE3o manual por rota)");
|
|
1251
1302
|
}
|
|
1303
|
+
if (appcheck.ignoredOnRequest > 0) {
|
|
1304
|
+
skipped.push(`enforceAppCheck em onRequest ignorado pelo runtime (${appcheck.ignoredOnRequest} ocorr\xEAncia(s)) \u2014 onRequest exige verifyToken por rota`);
|
|
1305
|
+
}
|
|
1252
1306
|
if (appcheck.limitHit) {
|
|
1253
1307
|
skipped.push("functions: teto de varredura atingido \u2014 AppCheck parcial neste scan");
|
|
1254
1308
|
}
|
|
1255
1309
|
} else {
|
|
1256
1310
|
skipped.push("functions n\xE3o observadas \u2014 AppCheck fora deste scan");
|
|
1257
1311
|
}
|
|
1258
|
-
skipped.push("isolamento por tenant (ownerId/tenantId) n\xE3o
|
|
1312
|
+
skipped.push("isolamento por tenant (ownerId/tenantId) e mismatch de papel n\xE3o-admin fora dos checks \u2014 coberto s\xF3 via verify/Emulator, nunca verde silencioso");
|
|
1259
1313
|
const modelCheck = ProjectModelSchema.safeParse(model);
|
|
1260
1314
|
if (!modelCheck.success) {
|
|
1261
1315
|
findings.push({
|
|
@@ -1320,6 +1374,7 @@ async function countAppCheckEnforced(rootDir) {
|
|
|
1320
1374
|
let occurrences = 0;
|
|
1321
1375
|
const files = /* @__PURE__ */ new Set();
|
|
1322
1376
|
let manual = 0;
|
|
1377
|
+
let ignoredOnRequest = 0;
|
|
1323
1378
|
let limitHit = false;
|
|
1324
1379
|
const stack = [...roots];
|
|
1325
1380
|
let guard = 0;
|
|
@@ -1341,8 +1396,11 @@ async function countAppCheckEnforced(rootDir) {
|
|
|
1341
1396
|
const name = e.name;
|
|
1342
1397
|
const abs = joinP(cur, name);
|
|
1343
1398
|
let isDir = false;
|
|
1399
|
+
let size = 0;
|
|
1344
1400
|
try {
|
|
1345
|
-
|
|
1401
|
+
const st = stat(abs);
|
|
1402
|
+
isDir = st.isDirectory();
|
|
1403
|
+
size = st.size;
|
|
1346
1404
|
} catch {
|
|
1347
1405
|
continue;
|
|
1348
1406
|
}
|
|
@@ -1353,6 +1411,7 @@ async function countAppCheckEnforced(rootDir) {
|
|
|
1353
1411
|
}
|
|
1354
1412
|
if (!/\.(ts|js|mjs|cjs|py)$/.test(name)) continue;
|
|
1355
1413
|
if (isTestFile(abs)) continue;
|
|
1414
|
+
if (size > 100 * 1024) continue;
|
|
1356
1415
|
try {
|
|
1357
1416
|
let text = strip(readSync(abs, "utf-8"));
|
|
1358
1417
|
if (name.endsWith(".py")) {
|
|
@@ -1361,10 +1420,16 @@ async function countAppCheckEnforced(rootDir) {
|
|
|
1361
1420
|
return idx === -1 ? l : l.slice(0, idx);
|
|
1362
1421
|
}).join("\n");
|
|
1363
1422
|
}
|
|
1364
|
-
const
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1423
|
+
for (const em of text.matchAll(/enforceAppCheck\s*:\s*true|enforce_app_check\s*=\s*True/g)) {
|
|
1424
|
+
const before = text.slice(Math.max(0, (em.index ?? 0) - 600), em.index ?? 0);
|
|
1425
|
+
const triggers = [...before.matchAll(/on(Request|Call|request|call)\s*[\(.]/g)].map((t) => t[1].toLowerCase());
|
|
1426
|
+
const last = triggers[triggers.length - 1];
|
|
1427
|
+
if (last === "request") {
|
|
1428
|
+
ignoredOnRequest += 1;
|
|
1429
|
+
} else {
|
|
1430
|
+
occurrences += 1;
|
|
1431
|
+
files.add(abs);
|
|
1432
|
+
}
|
|
1368
1433
|
}
|
|
1369
1434
|
const manualHits = text.match(/verifyToken\s*\(|verify_token\s*\(|consumeAppCheckToken|getLimitedUseToken|getAppCheck\s*\(/g);
|
|
1370
1435
|
if (manualHits) manual += manualHits.length;
|
|
@@ -1372,9 +1437,9 @@ async function countAppCheckEnforced(rootDir) {
|
|
|
1372
1437
|
}
|
|
1373
1438
|
}
|
|
1374
1439
|
}
|
|
1375
|
-
return { occurrences, files: files.size, manual, limitHit };
|
|
1440
|
+
return { occurrences, files: files.size, manual, ignoredOnRequest, limitHit };
|
|
1376
1441
|
} catch {
|
|
1377
|
-
return { occurrences: 0, files: 0, manual: 0, limitHit: false };
|
|
1442
|
+
return { occurrences: 0, files: 0, manual: 0, ignoredOnRequest: 0, limitHit: false };
|
|
1378
1443
|
}
|
|
1379
1444
|
}
|
|
1380
1445
|
function isOpenStorageCondition(condition) {
|
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-ME6WO45S.js";
|
|
5
5
|
import "./chunk-FECVBQUO.js";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
@@ -81,23 +81,25 @@ Uso: firebase-audit scan|check|verify|drift [--json] ...`);
|
|
|
81
81
|
const { resolve: resolveRoot } = await import("path");
|
|
82
82
|
const rootDir = cwdArg ? resolveRoot(process.cwd(), cwdArg) : process.cwd();
|
|
83
83
|
if (cmd === "verify") {
|
|
84
|
-
const { scan: scanForVerify } = await import("./scan-
|
|
85
|
-
const { verify } = await import("./verify-
|
|
84
|
+
const { scan: scanForVerify } = await import("./scan-HE5WAMWJ.js");
|
|
85
|
+
const { verify } = await import("./verify-DECP4IUW.js");
|
|
86
86
|
const { resolve: resolveVerify } = await import("path");
|
|
87
87
|
const coverageArg = getArg("coverage");
|
|
88
88
|
const coveragePath = coverageArg ? resolveVerify(rootDir, coverageArg) : void 0;
|
|
89
|
-
const { model } = await scanForVerify(rootDir, { adapterFn: adapterArg });
|
|
89
|
+
const { model, summary: scanSummary } = await scanForVerify(rootDir, { adapterFn: adapterArg });
|
|
90
90
|
const res = verify(model, { coverageFile: coveragePath });
|
|
91
91
|
const roles = model.authorization.roles.length > 0 ? model.authorization.roles.map((r) => r.name) : ["anonymous", "user", "admin"];
|
|
92
92
|
const rolesReduced = model.authorization.roles.length === 0;
|
|
93
|
+
const scanRef = { errors: scanSummary.errors, warnings: scanSummary.warnings };
|
|
93
94
|
if (asJson) {
|
|
94
|
-
console.log(JSON.stringify({ roles, rolesReduced, cases: res.cases.slice(0, 50), casesTotal: res.cases.length, truncated: res.cases.length > 50, uncovered: res.uncovered.slice(0, 20), uncoveredTotal: res.uncovered.length, coveragePct: res.coveragePct, findings: res.findings }, null, 2));
|
|
95
|
+
console.log(JSON.stringify({ roles, rolesReduced, cases: res.cases.slice(0, 50), casesTotal: res.cases.length, truncated: res.cases.length > 50, uncovered: res.uncovered.slice(0, 20), uncoveredTotal: res.uncovered.length, uncoveredNote: res.uncoveredNote, coveragePct: res.coveragePct, findings: res.findings, scan: scanRef }, null, 2));
|
|
95
96
|
return;
|
|
96
97
|
}
|
|
97
98
|
const covStr = res.coveragePct === null ? "sem cobertura observada" : `${res.coveragePct}% de express\xF5es visitadas`;
|
|
98
99
|
const rolesNote = rolesReduced ? "pap\xE9is reduzidos (sem contrato: anonymous/user/admin)" : `${roles.length} pap\xE9is do contrato (${roles.join(", ")})`;
|
|
99
100
|
console.log(`
|
|
100
101
|
FIREBASE AUDIT verify \u2014 ${res.cases.length} casos planejados (${rolesNote}), ${res.uncovered.length} paths sem cobertura (${covStr}).`);
|
|
102
|
+
console.log(`Scan est\xE1tico junto: ${scanRef.errors} errors, ${scanRef.warnings} warnings \u2014 verify n\xE3o substitui scan/check.`);
|
|
101
103
|
for (const f of res.findings) {
|
|
102
104
|
const icon = f.severity === "ERROR" ? "\u274C" : f.severity === "WARNING" ? "\u26A0" : "\u2139";
|
|
103
105
|
console.log(`
|
|
@@ -156,6 +158,9 @@ FIREBASE AUDIT drift \u2014 MATCHED ${counts("MATCHED")}, LOCAL_ONLY ${counts("L
|
|
|
156
158
|
return;
|
|
157
159
|
}
|
|
158
160
|
const { findings, summary } = await scan(rootDir, { strict, adapterFn: adapterArg });
|
|
161
|
+
if (!strict && summary.errors > 0) {
|
|
162
|
+
console.error(`\u26A0 scan encontrou ${summary.errors} ERROR(s) mas sai 0 \u2014 use 'check' no CI para reprovar.`);
|
|
163
|
+
}
|
|
159
164
|
if (asJson) {
|
|
160
165
|
console.log(JSON.stringify({ summary, findings }, null, 2));
|
|
161
166
|
if (strict && summary.errors > 0) process.exit(2);
|
package/dist/index.d.ts
CHANGED
|
@@ -577,9 +577,9 @@ declare const FindingSchema: z.ZodObject<{
|
|
|
577
577
|
declare const AuditYamlSchema: z.ZodObject<{
|
|
578
578
|
$schema: z.ZodOptional<z.ZodString>;
|
|
579
579
|
authorization: z.ZodObject<{
|
|
580
|
-
adapter: z.ZodOptional<z.ZodObject<{
|
|
580
|
+
adapter: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
581
581
|
function: z.ZodString;
|
|
582
|
-
}, z.core.$strict
|
|
582
|
+
}, z.core.$strict>>>;
|
|
583
583
|
roles: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
584
584
|
permissions: z.ZodArray<z.ZodString>;
|
|
585
585
|
}, z.core.$strict>>;
|
|
@@ -595,11 +595,11 @@ declare const AuditYamlSchema: z.ZodObject<{
|
|
|
595
595
|
write: "write";
|
|
596
596
|
}>;
|
|
597
597
|
}, z.core.$strict>>;
|
|
598
|
-
claims: z.ZodOptional<z.ZodObject<{
|
|
598
|
+
claims: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
599
599
|
enabled: z.ZodBoolean;
|
|
600
|
-
roleKey: z.ZodString
|
|
600
|
+
roleKey: z.ZodOptional<z.ZodString>;
|
|
601
601
|
samples: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodJSONSchema>>>;
|
|
602
|
-
}, z.core.$strict
|
|
602
|
+
}, z.core.$strict>>>;
|
|
603
603
|
}, z.core.$strict>;
|
|
604
604
|
}, z.core.$strict>;
|
|
605
605
|
/** Chaves OIDC/Firebase reservadas — nunca em custom claims (1000 bytes, só acesso). */
|
|
@@ -639,7 +639,8 @@ interface DiscoveryResult {
|
|
|
639
639
|
declare function discover(rootDir: string): DiscoveryResult;
|
|
640
640
|
/** Segmentos de servidor (fail-closed: `api` genérico NÃO é server, ver classifyOrigin). */
|
|
641
641
|
declare const SERVER_HINTS_SEGMENTS: string[];
|
|
642
|
-
/** Heurística CLIENT vs SERVER: caminho manda primeiro (grafo ai-tool preenche listas na Fase 3).
|
|
642
|
+
/** Heurística CLIENT vs SERVER: caminho manda primeiro (grafo ai-tool preenche listas na Fase 3).
|
|
643
|
+
* Segmentos ancorados — nunca substring solta (`myapp/` não é `app/`). */
|
|
643
644
|
declare function classifyOrigin(file: string, content: string): "CLIENT" | "SERVER" | "UNKNOWN";
|
|
644
645
|
declare function emptyModel(rootDir: string, d: DiscoveryResult): ProjectModel;
|
|
645
646
|
/**
|
|
@@ -806,6 +807,8 @@ interface VerifyResult {
|
|
|
806
807
|
findings: Finding[];
|
|
807
808
|
/** 0-100 quando o coverage tem visitCount avaliável; null = não observada. */
|
|
808
809
|
coveragePct: number | null;
|
|
810
|
+
/** Como ler `uncovered`: atribuição por expressão indisponível (só 100% esvazia). */
|
|
811
|
+
uncoveredNote: string;
|
|
809
812
|
}
|
|
810
813
|
/** Gera matriz role × operação × path a partir do modelo (sem executar nada).
|
|
811
814
|
* Mapeamento: read≡get+list, write≡create+update+delete (mesma semântica do opsFrom).
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpServer,
|
|
3
3
|
startMcpServer
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-CHC7ZHUK.js";
|
|
5
5
|
import {
|
|
6
6
|
AccessOriginSchema,
|
|
7
7
|
AuditYamlSchema,
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
runChecks,
|
|
34
34
|
scan,
|
|
35
35
|
validateClaimSamples
|
|
36
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-ME6WO45S.js";
|
|
37
37
|
import {
|
|
38
38
|
buildFullPath,
|
|
39
39
|
conditionKeyForFingerprint,
|
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
import {
|
|
60
60
|
planVerify,
|
|
61
61
|
verify
|
|
62
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-DIO36A3I.js";
|
|
63
63
|
import {
|
|
64
64
|
drift
|
|
65
65
|
} from "./chunk-XZ32YQQC.js";
|
package/dist/mcp-cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
startMcpServer
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-CHC7ZHUK.js";
|
|
5
|
+
import "./chunk-ME6WO45S.js";
|
|
6
6
|
import "./chunk-FECVBQUO.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-DIO36A3I.js";
|
|
8
8
|
import "./chunk-XZ32YQQC.js";
|
|
9
9
|
|
|
10
10
|
// src/mcp-cli.ts
|
package/dist/mcp.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpServer,
|
|
3
3
|
startMcpServer
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-CHC7ZHUK.js";
|
|
5
|
+
import "./chunk-ME6WO45S.js";
|
|
6
6
|
import "./chunk-FECVBQUO.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-DIO36A3I.js";
|
|
8
8
|
import "./chunk-XZ32YQQC.js";
|
|
9
9
|
export {
|
|
10
10
|
createMcpServer,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justmpm/firebase-audit",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
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
|
@@ -35,4 +35,5 @@ Use `drift` para comparar índices locais vs implantados (MATCHED/LOCAL_ONLY/REM
|
|
|
35
35
|
## Limites honestos
|
|
36
36
|
|
|
37
37
|
- AppCheck: varredura TS/Python por texto (`enforceAppCheck`, `verifyToken` e afins). Config via variável conta como não observada; `onRequest` exige revisão rota a rota.
|
|
38
|
-
- Isolamento por tenant (ownerId/tenantId): fora dos checks — revise manualmente ou no Emulator.
|
|
38
|
+
- Isolamento por tenant (ownerId/tenantId) e mismatch de papel não-admin: fora dos checks — revise manualmente ou no Emulator.
|
|
39
|
+
- `graph: true` é prévia opt-in da biblioteca (nenhum check consome ainda).
|