@justmpm/firebase-audit 0.4.3 → 0.5.0
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/dist/{chunk-PSSMYRRU.js → chunk-2X3BTSGP.js} +1 -1
- package/dist/{chunk-IXMAWDJH.js → chunk-EHGW2EX6.js} +32 -19
- package/dist/{chunk-3SEVYYCX.js → chunk-TQZTE2NR.js} +157 -16
- package/dist/cli.js +20 -16
- package/dist/index.d.ts +30 -20
- package/dist/index.js +3 -3
- package/dist/mcp-cli.js +3 -3
- package/dist/mcp.js +3 -3
- package/dist/{scan-VREY2FGO.js → scan-FYAWNRIE.js} +1 -1
- package/dist/{verify-O722ZZBV.js → verify-GWRLO344.js} +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +9 -7
|
@@ -71,7 +71,7 @@ function verify(model, opts = {}) {
|
|
|
71
71
|
rule: "COVERAGE_NOT_EVALUATED",
|
|
72
72
|
severity: "WARNING",
|
|
73
73
|
confidence: "NOT_OBSERVED",
|
|
74
|
-
message: "Coverage sem visitCount reconhec\xEDvel
|
|
74
|
+
message: "Coverage sem visitCount reconhec\xEDvel \u2014 uncovered lista tudo por honestidade.",
|
|
75
75
|
fingerprint: "COVERAGE_NOT_EVALUATED",
|
|
76
76
|
evidence: [{ id: "ev-coverage-ne", kind: "coverage-parse", summary: "n\xE3o avaliado", confidence: "NOT_OBSERVED" }]
|
|
77
77
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
scan
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-TQZTE2NR.js";
|
|
4
4
|
import {
|
|
5
5
|
verify
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-2X3BTSGP.js";
|
|
7
7
|
import {
|
|
8
8
|
drift
|
|
9
9
|
} from "./chunk-NJBCSW7E.js";
|
|
@@ -31,10 +31,11 @@ function readPkgText(rel) {
|
|
|
31
31
|
var FALLBACK_SKILL = [
|
|
32
32
|
"# firebase-audit-skill (fallback)",
|
|
33
33
|
"",
|
|
34
|
-
"1. Descobrir projeto (firebase.json, firestore.rules, firestore.indexes.json).",
|
|
35
|
-
"2. Executar scan (
|
|
34
|
+
"1. Descobrir projeto (firebase.json, firestore.rules, storage.rules, firestore.indexes.json).",
|
|
35
|
+
"2. Executar scan (check com contrato no CI). Sem fun\xE7\xE3o de permiss\xE3o que receba o nome como texto \u2192 UNKNOWN, nunca adivinhar.",
|
|
36
36
|
"3. CONFIRMED corrige agora; PROBABLE investiga; NOT_OBSERVED/UNKNOWN n\xE3o afirma.",
|
|
37
|
-
"4.
|
|
37
|
+
"4. AppCheck \xE9 carimbo do app, n\xE3o autoriza\xE7\xE3o. Logo p\xFAblica pode ser vitrine; cofre \xE9 write aberto.",
|
|
38
|
+
"5. Corrigir c\xF3digo, nunca o contrato para esconder finding."
|
|
38
39
|
].join("\n");
|
|
39
40
|
function createMcpServer() {
|
|
40
41
|
const server = new McpServer({
|
|
@@ -45,14 +46,14 @@ function createMcpServer() {
|
|
|
45
46
|
"skill",
|
|
46
47
|
{
|
|
47
48
|
title: "Firebase Audit Skill (guia do agente)",
|
|
48
|
-
description: "LEIA ANTES de auditar: sequ\xEAncia obrigat\xF3ria, travas (nunca mude o contrato para esconder finding, nunca afirme sem evid\xEAncia, nunca diga 'unused') e Emulador. Retorna o guia oficial
|
|
49
|
+
description: "LEIA ANTES de auditar: sequ\xEAncia obrigat\xF3ria, travas (nunca mude o contrato para esconder finding, nunca afirme sem evid\xEAncia, nunca diga 'unused') e Emulador. Retorna o guia oficial instalado + modelo de contrato. Sempre sincronizado \u2014 dispensa skill copiada.",
|
|
49
50
|
inputSchema: {}
|
|
50
51
|
},
|
|
51
52
|
async () => {
|
|
52
53
|
const skill = readPkgText(join("skill", "SKILL.md")) ?? FALLBACK_SKILL;
|
|
53
54
|
const template = readPkgText(join("templates", "firebase-audit.yaml")) ?? "";
|
|
54
55
|
const text = [
|
|
55
|
-
|
|
56
|
+
"# firebase-audit \u2014 guia do agente",
|
|
56
57
|
"",
|
|
57
58
|
skill,
|
|
58
59
|
"",
|
|
@@ -69,10 +70,10 @@ function createMcpServer() {
|
|
|
69
70
|
"scan",
|
|
70
71
|
{
|
|
71
72
|
title: "Firebase Audit Scan",
|
|
72
|
-
description: "Audita projeto Firebase (c\xF3digo + firestore.rules + \xEDndices + contrato).
|
|
73
|
+
description: "Audita projeto Firebase (c\xF3digo + firestore.rules + storage.rules + \xEDndices + contrato + AppCheck). Est\xE1tico primeiro, nunca inventa certeza (CONFIRMED/PROBABLE/NOT_OBSERVED/UNKNOWN). Use para descobrir regras p\xFAblicas, Admin em cliente, permiss\xF5es sem contrato.",
|
|
73
74
|
inputSchema: {
|
|
74
75
|
cwd: z.string().min(1).describe("Diret\xF3rio raiz do projeto Firebase a auditar"),
|
|
75
|
-
adapter: z.string().optional().describe("Fun\xE7\xE3o de
|
|
76
|
+
adapter: z.string().optional().describe("Fun\xE7\xE3o de permiss\xE3o no c\xF3digo que recebe o nome como texto (ex: rbac.can). N\xE3o use guarda de rota nem function do .rules aqui. Default: do contrato."),
|
|
76
77
|
strict: z.boolean().optional().describe("Exige contrato (firebase-audit.yaml). CI usa strict.")
|
|
77
78
|
}
|
|
78
79
|
},
|
|
@@ -83,7 +84,7 @@ function createMcpServer() {
|
|
|
83
84
|
content: [
|
|
84
85
|
{
|
|
85
86
|
type: "text",
|
|
86
|
-
text: JSON.stringify({
|
|
87
|
+
text: JSON.stringify({ summary: res.summary, findings: res.findings }, null, 2)
|
|
87
88
|
}
|
|
88
89
|
]
|
|
89
90
|
};
|
|
@@ -99,10 +100,10 @@ function createMcpServer() {
|
|
|
99
100
|
"check",
|
|
100
101
|
{
|
|
101
102
|
title: "Firebase Audit Check (strict)",
|
|
102
|
-
description: "Alias de scan
|
|
103
|
+
description: "Alias de scan strict: exige contrato e reprova CI se houver ERROR. Use no CI.",
|
|
103
104
|
inputSchema: {
|
|
104
105
|
cwd: z.string().min(1).describe("Diret\xF3rio raiz do projeto Firebase"),
|
|
105
|
-
adapter: z.string().optional().describe("Fun\xE7\xE3o de
|
|
106
|
+
adapter: z.string().optional().describe("Fun\xE7\xE3o de permiss\xE3o no c\xF3digo que recebe o nome como texto (ex: rbac.can)")
|
|
106
107
|
}
|
|
107
108
|
},
|
|
108
109
|
async ({ cwd, adapter }) => {
|
|
@@ -112,7 +113,7 @@ function createMcpServer() {
|
|
|
112
113
|
content: [
|
|
113
114
|
{
|
|
114
115
|
type: "text",
|
|
115
|
-
text: JSON.stringify({
|
|
116
|
+
text: JSON.stringify({ summary: res.summary, findings: res.findings }, null, 2)
|
|
116
117
|
}
|
|
117
118
|
]
|
|
118
119
|
};
|
|
@@ -128,10 +129,10 @@ function createMcpServer() {
|
|
|
128
129
|
"verify",
|
|
129
130
|
{
|
|
130
131
|
title: "Firebase Audit Verify (Emulator)",
|
|
131
|
-
description: "Gera matriz
|
|
132
|
+
description: "Gera matriz papel\xD7opera\xE7\xE3o\xD7path e avalia cobertura do ruleCoverage. Requer Emulator. Sem contrato usa pap\xE9is reduzidos (anonymous/user/admin). Nunca afirma cobertura sem teste real.",
|
|
132
133
|
inputSchema: {
|
|
133
134
|
cwd: z.string().min(1).describe("Diret\xF3rio raiz do projeto"),
|
|
134
|
-
coverage: z.string().optional().describe("Path do coverage.json do
|
|
135
|
+
coverage: z.string().optional().describe("Path do coverage.json do ruleCoverage (relativo ao cwd)")
|
|
135
136
|
}
|
|
136
137
|
},
|
|
137
138
|
async ({ cwd, coverage }) => {
|
|
@@ -139,12 +140,24 @@ function createMcpServer() {
|
|
|
139
140
|
const { resolve } = await import("path");
|
|
140
141
|
const { model } = await scan(cwd);
|
|
141
142
|
const res = verify(model, { coverageFile: coverage ? resolve(cwd, coverage) : void 0 });
|
|
143
|
+
const roles = model.authorization.roles.length > 0 ? model.authorization.roles.map((r) => r.name) : ["anonymous", "user", "admin"];
|
|
144
|
+
const truncated = res.cases.length > 50;
|
|
142
145
|
return {
|
|
143
146
|
content: [
|
|
144
147
|
{
|
|
145
148
|
type: "text",
|
|
146
149
|
text: JSON.stringify(
|
|
147
|
-
{
|
|
150
|
+
{
|
|
151
|
+
roles,
|
|
152
|
+
rolesReduced: model.authorization.roles.length === 0,
|
|
153
|
+
cases: res.cases.slice(0, 50),
|
|
154
|
+
casesTotal: res.cases.length,
|
|
155
|
+
truncated,
|
|
156
|
+
uncovered: res.uncovered.slice(0, 20),
|
|
157
|
+
uncoveredTotal: res.uncovered.length,
|
|
158
|
+
coveragePct: res.coveragePct,
|
|
159
|
+
findings: res.findings
|
|
160
|
+
},
|
|
148
161
|
null,
|
|
149
162
|
2
|
|
150
163
|
)
|
|
@@ -166,7 +179,7 @@ function createMcpServer() {
|
|
|
166
179
|
description: "Compara \xEDndices locais vs implantados. Retorna MATCHED/LOCAL_ONLY/REMOTE_ONLY. Nunca diz 'unused', apenas NOT_OBSERVED.",
|
|
167
180
|
inputSchema: {
|
|
168
181
|
local: z.string().nullable().optional().describe("Path do firestore.indexes.json local (ou null). Relativo resolve contra cwd."),
|
|
169
|
-
remote: z.string().nullable().optional().describe("Path do JSON remoto (
|
|
182
|
+
remote: z.string().nullable().optional().describe("Path do JSON remoto (ou null). Relativo resolve contra cwd."),
|
|
170
183
|
cwd: z.string().optional().describe("Diret\xF3rio base para paths relativos (default: cwd do servidor).")
|
|
171
184
|
}
|
|
172
185
|
},
|
|
@@ -183,7 +196,7 @@ function createMcpServer() {
|
|
|
183
196
|
content: [
|
|
184
197
|
{
|
|
185
198
|
type: "text",
|
|
186
|
-
text: JSON.stringify({
|
|
199
|
+
text: JSON.stringify({ entries: res.entries, findings: res.findings }, null, 2)
|
|
187
200
|
}
|
|
188
201
|
]
|
|
189
202
|
};
|
|
@@ -201,7 +214,7 @@ async function startMcpServer() {
|
|
|
201
214
|
const server = createMcpServer();
|
|
202
215
|
const transport = new StdioServerTransport();
|
|
203
216
|
await server.connect(transport);
|
|
204
|
-
console.error(
|
|
217
|
+
console.error("[firebase-audit] MCP server running via stdio");
|
|
205
218
|
}
|
|
206
219
|
|
|
207
220
|
export {
|
|
@@ -4,7 +4,7 @@ import { join as join3 } from "path";
|
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
5
|
|
|
6
6
|
// src/discovery.ts
|
|
7
|
-
import { existsSync, readFileSync } from "fs";
|
|
7
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
8
8
|
import { join, relative } from "path";
|
|
9
9
|
function discover(rootDir) {
|
|
10
10
|
const pick = (name) => {
|
|
@@ -16,6 +16,16 @@ function discover(rootDir) {
|
|
|
16
16
|
let databaseId = null;
|
|
17
17
|
let rulesFile = pick("firestore.rules");
|
|
18
18
|
let indexesFile = pick("firestore.indexes.json");
|
|
19
|
+
let storageFile = pick("storage.rules");
|
|
20
|
+
let functionsDir = null;
|
|
21
|
+
const functionsPick = pick("functions");
|
|
22
|
+
if (functionsPick) {
|
|
23
|
+
try {
|
|
24
|
+
functionsDir = statSync(functionsPick).isDirectory() ? functionsPick : null;
|
|
25
|
+
} catch {
|
|
26
|
+
functionsDir = null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
19
29
|
const firebaserc = pick(".firebaserc");
|
|
20
30
|
if (firebaserc) {
|
|
21
31
|
try {
|
|
@@ -36,6 +46,10 @@ function discover(rootDir) {
|
|
|
36
46
|
const p = join(rootDir, raw.firestore.indexes);
|
|
37
47
|
if (existsSync(p)) indexesFile = p;
|
|
38
48
|
}
|
|
49
|
+
if (typeof raw.storage?.rules === "string") {
|
|
50
|
+
const p = join(rootDir, raw.storage.rules);
|
|
51
|
+
if (existsSync(p)) storageFile = p;
|
|
52
|
+
}
|
|
39
53
|
} catch {
|
|
40
54
|
}
|
|
41
55
|
}
|
|
@@ -44,6 +58,8 @@ function discover(rootDir) {
|
|
|
44
58
|
firebaseJson,
|
|
45
59
|
firestoreRulesFile: rulesFile,
|
|
46
60
|
firestoreIndexesFile: indexesFile,
|
|
61
|
+
storageRulesFile: storageFile,
|
|
62
|
+
functionsDir,
|
|
47
63
|
auditYamlFile: pick("firebase-audit.yaml"),
|
|
48
64
|
projectId,
|
|
49
65
|
databaseId,
|
|
@@ -93,8 +109,10 @@ function emptyModel(rootDir, d) {
|
|
|
93
109
|
firestore: {
|
|
94
110
|
rulesFile: rel(d.firestoreRulesFile),
|
|
95
111
|
indexesFile: rel(d.firestoreIndexesFile)
|
|
96
|
-
}
|
|
112
|
+
},
|
|
113
|
+
storage: d.storageRulesFile ? { rulesFile: rel(d.storageRulesFile) } : void 0
|
|
97
114
|
},
|
|
115
|
+
functions: d.functionsDir ? { dir: rel(d.functionsDir) } : void 0,
|
|
98
116
|
authorization: { adapter: null, roles: [], permissions: [] },
|
|
99
117
|
queries: [],
|
|
100
118
|
rules: [],
|
|
@@ -109,14 +127,14 @@ async function enrichWithGraph(rootDir, d) {
|
|
|
109
127
|
const res = await ai.map({ cwd: rootDir, format: "json" });
|
|
110
128
|
const files = Array.isArray(res.files) ? res.files : [];
|
|
111
129
|
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);
|
|
112
|
-
const { readFileSync: readSync, existsSync: existsSyncFn, statSync } = await import("fs");
|
|
130
|
+
const { readFileSync: readSync, existsSync: existsSyncFn, statSync: statSync2 } = await import("fs");
|
|
113
131
|
const { join: joinP } = await import("path");
|
|
114
132
|
for (const rel of code) {
|
|
115
133
|
let content = "";
|
|
116
134
|
try {
|
|
117
135
|
const abs = joinP(rootDir, rel);
|
|
118
136
|
if (!existsSyncFn(abs)) continue;
|
|
119
|
-
if (
|
|
137
|
+
if (statSync2(abs).size > 100 * 1024) continue;
|
|
120
138
|
content = readSync(abs, "utf-8");
|
|
121
139
|
} catch {
|
|
122
140
|
content = "";
|
|
@@ -555,12 +573,15 @@ async function runChecks(model, rootDir, opts = {}) {
|
|
|
555
573
|
const permissionCalls = await collectPermissionCalls(rootDir, adapterFn);
|
|
556
574
|
const declared = new Set(model.authorization.permissions.map((p) => p.name));
|
|
557
575
|
if (permissionCalls.literals.length === 0 && permissionCalls.dynamic.length === 0) {
|
|
576
|
+
const tenantHint = [...helperDetailed.keys()].find(
|
|
577
|
+
(n) => /tenant|member|signedin/i.test(n)
|
|
578
|
+
);
|
|
558
579
|
findings.push(
|
|
559
580
|
make(
|
|
560
581
|
"FBA009",
|
|
561
582
|
"WARNING",
|
|
562
583
|
"UNKNOWN",
|
|
563
|
-
`Adapter
|
|
584
|
+
tenantHint ? `Adapter "${adapterFn}" n\xE3o observado no c\xF3digo. Encontrei portaria tenant (${tenantHint}) nas Rules, mas sem fun\xE7\xE3o de permiss\xE3o que receba o nome como texto n\xE3o d\xE1 para ligar papel\xD7recurso \u2014 permiss\xF5es seguem UNKNOWN.` : `Adapter "${adapterFn}" n\xE3o observado no c\xF3digo. Sem fun\xE7\xE3o de permiss\xE3o que receba o nome como texto, permiss\xF5es s\xE3o UNKNOWN.`,
|
|
564
585
|
`FBA009:${adapterFn}`,
|
|
565
586
|
{ evidence: [ev("adapter-missing", adapterFn, "UNKNOWN")] }
|
|
566
587
|
)
|
|
@@ -1041,8 +1062,15 @@ var ProjectModelSchema = z.strictObject({
|
|
|
1041
1062
|
firestore: z.strictObject({
|
|
1042
1063
|
rulesFile: z.string().optional(),
|
|
1043
1064
|
indexesFile: z.string().optional()
|
|
1044
|
-
})
|
|
1065
|
+
}),
|
|
1066
|
+
storage: z.strictObject({
|
|
1067
|
+
rulesFile: z.string().optional()
|
|
1068
|
+
}).optional()
|
|
1045
1069
|
}),
|
|
1070
|
+
functions: z.strictObject({
|
|
1071
|
+
dir: z.string().optional(),
|
|
1072
|
+
appCheckEnforced: z.number().int().nonnegative().optional()
|
|
1073
|
+
}).optional(),
|
|
1046
1074
|
authorization: z.strictObject({
|
|
1047
1075
|
adapter: z.string().nullable(),
|
|
1048
1076
|
roles: z.array(RoleSchema),
|
|
@@ -1206,11 +1234,11 @@ async function scan(rootDir, opts = {}) {
|
|
|
1206
1234
|
}
|
|
1207
1235
|
const validModes = ["ASCENDING", "DESCENDING", "ARRAY_CONTAINS", "VECTOR"];
|
|
1208
1236
|
const entries = [];
|
|
1209
|
-
let
|
|
1237
|
+
let skipped2 = 0;
|
|
1210
1238
|
let scopeSkipped = 0;
|
|
1211
1239
|
for (const i of raw.indexes ?? []) {
|
|
1212
1240
|
if (typeof i.collectionGroup !== "string" || i.collectionGroup.length === 0) {
|
|
1213
|
-
|
|
1241
|
+
skipped2 += 1;
|
|
1214
1242
|
continue;
|
|
1215
1243
|
}
|
|
1216
1244
|
if (i.queryScope !== void 0 && i.queryScope !== "COLLECTION" && i.queryScope !== "COLLECTION_GROUP") {
|
|
@@ -1220,13 +1248,13 @@ async function scan(rootDir, opts = {}) {
|
|
|
1220
1248
|
const fields = [];
|
|
1221
1249
|
for (const f of i.fields ?? []) {
|
|
1222
1250
|
if (typeof f.fieldPath !== "string" || f.fieldPath.length === 0) {
|
|
1223
|
-
|
|
1251
|
+
skipped2 += 1;
|
|
1224
1252
|
continue;
|
|
1225
1253
|
}
|
|
1226
1254
|
const rawMode = f.vectorConfig ? "VECTOR" : f.arrayConfig === "CONTAINS" ? "ARRAY_CONTAINS" : typeof f.mode === "string" ? f.mode : f.order;
|
|
1227
1255
|
const mode = rawMode;
|
|
1228
1256
|
if (mode !== "ASCENDING" && mode !== "DESCENDING" && mode !== "ARRAY_CONTAINS" && mode !== "VECTOR") {
|
|
1229
|
-
|
|
1257
|
+
skipped2 += 1;
|
|
1230
1258
|
continue;
|
|
1231
1259
|
}
|
|
1232
1260
|
fields.push({ fieldPath: f.fieldPath, mode });
|
|
@@ -1239,11 +1267,11 @@ async function scan(rootDir, opts = {}) {
|
|
|
1239
1267
|
const last = entries[entries.length - 1];
|
|
1240
1268
|
if (last.fields.length === 0) {
|
|
1241
1269
|
entries.pop();
|
|
1242
|
-
|
|
1270
|
+
skipped2 += 1;
|
|
1243
1271
|
}
|
|
1244
1272
|
}
|
|
1245
1273
|
model.indexes = entries;
|
|
1246
|
-
const totalSkipped =
|
|
1274
|
+
const totalSkipped = skipped2 + scopeSkipped;
|
|
1247
1275
|
if (totalSkipped > 0) {
|
|
1248
1276
|
findings_pre.push({
|
|
1249
1277
|
rule: "INDEXES_INVALID",
|
|
@@ -1361,6 +1389,75 @@ async function scan(rootDir, opts = {}) {
|
|
|
1361
1389
|
}
|
|
1362
1390
|
const checksFindings = await runChecks(model, rootDir, { adapterFn: opts.adapterFn ?? model.authorization.adapter ?? void 0 });
|
|
1363
1391
|
const findings = [...findings_pre, ...checksFindings];
|
|
1392
|
+
const checked = ["firestore.rules", "indexes", "contract", "client-code"];
|
|
1393
|
+
const skipped = [];
|
|
1394
|
+
if (d.storageRulesFile && existsSync3(d.storageRulesFile)) {
|
|
1395
|
+
checked.push("storage.rules");
|
|
1396
|
+
try {
|
|
1397
|
+
const raw = readFileSync4(d.storageRulesFile, "utf-8");
|
|
1398
|
+
const relStorage = model.firebase.storage?.rulesFile ?? "storage.rules";
|
|
1399
|
+
const low = raw.toLowerCase().replace(/\s+/g, "");
|
|
1400
|
+
const hasAllPaths = raw.includes("allPaths=**") || raw.includes("allPaths = **");
|
|
1401
|
+
const writeOpen = /allowwrite:[^;]*iftrue/.test(low);
|
|
1402
|
+
const getOpen = /allowget:[^;]*iftrue/.test(low);
|
|
1403
|
+
if (writeOpen) {
|
|
1404
|
+
findings.push({
|
|
1405
|
+
rule: "STORAGE_PUBLIC_WRITE",
|
|
1406
|
+
severity: "ERROR",
|
|
1407
|
+
confidence: "CONFIRMED",
|
|
1408
|
+
message: `Storage com escrita aberta (${relStorage}). Qualquer cliente pode escrever \u2014 cofre aberto, n\xE3o vitrine.`,
|
|
1409
|
+
fingerprint: "STORAGE_PUBLIC_WRITE",
|
|
1410
|
+
file: relStorage,
|
|
1411
|
+
evidence: [{ id: "ev-storage-write", kind: "storage-public", summary: "write if true", confidence: "CONFIRMED" }],
|
|
1412
|
+
fix: "Exigir request.auth (e dono/tenant) no write. Deixe get aberto s\xF3 em pasta de vitrine como logos/."
|
|
1413
|
+
});
|
|
1414
|
+
} else if (hasAllPaths && getOpen) {
|
|
1415
|
+
findings.push({
|
|
1416
|
+
rule: "STORAGE_ALL_PATHS_OPEN",
|
|
1417
|
+
severity: "WARNING",
|
|
1418
|
+
confidence: "PROBABLE",
|
|
1419
|
+
message: `Storage com get aberto em /{allPaths=**} (${relStorage}). Vale para tudo \u2014 se a inten\xE7\xE3o era s\xF3 logo, restrinja a /logos/{id}.`,
|
|
1420
|
+
fingerprint: "STORAGE_ALL_PATHS_OPEN",
|
|
1421
|
+
file: relStorage,
|
|
1422
|
+
evidence: [{ id: "ev-storage-all", kind: "storage-public", summary: "allPaths get true", confidence: "PROBABLE" }],
|
|
1423
|
+
fix: "Troque /{allPaths=**} por pastas expl\xEDcitas (ex: /logos/{logoId}) com get: if true e write restrito."
|
|
1424
|
+
});
|
|
1425
|
+
} else if (getOpen) {
|
|
1426
|
+
findings.push({
|
|
1427
|
+
rule: "STORAGE_PUBLIC_READ",
|
|
1428
|
+
severity: "INFO",
|
|
1429
|
+
confidence: "PROBABLE",
|
|
1430
|
+
message: `Storage com leitura aberta (${relStorage}). Pode ser vitrine proposital (logos) \u2014 confirme que o write segue restrito.`,
|
|
1431
|
+
fingerprint: "STORAGE_PUBLIC_READ",
|
|
1432
|
+
file: relStorage,
|
|
1433
|
+
evidence: [{ id: "ev-storage-read", kind: "storage-public", summary: "get if true", confidence: "PROBABLE" }]
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
} catch {
|
|
1437
|
+
skipped.push("storage.rules ileg\xEDvel \u2014 Storage sem cobertura neste scan");
|
|
1438
|
+
}
|
|
1439
|
+
} else {
|
|
1440
|
+
skipped.push("storage.rules n\xE3o observado \u2014 Storage fora deste scan");
|
|
1441
|
+
}
|
|
1442
|
+
if (d.functionsDir) {
|
|
1443
|
+
const enforced = await countAppCheckEnforced(rootDir);
|
|
1444
|
+
model.functions = { dir: model.functions?.dir, appCheckEnforced: enforced };
|
|
1445
|
+
checked.push("functions-appcheck");
|
|
1446
|
+
if (enforced > 0) {
|
|
1447
|
+
findings.push({
|
|
1448
|
+
rule: "APPCHECK_ENFORCED",
|
|
1449
|
+
severity: "INFO",
|
|
1450
|
+
confidence: "CONFIRMED",
|
|
1451
|
+
message: `AppCheck ativo em ${enforced} function(s). Carimbo do app oficial \u2014 n\xE3o prova quem \xE9 o usu\xE1rio nem o tenant. Autoriza\xE7\xE3o segue nas Rules/guardas.`,
|
|
1452
|
+
fingerprint: "APPCHECK_ENFORCED",
|
|
1453
|
+
evidence: [{ id: "ev-appcheck", kind: "appcheck", summary: `${enforced} enforced`, confidence: "CONFIRMED" }]
|
|
1454
|
+
});
|
|
1455
|
+
} else {
|
|
1456
|
+
skipped.push("functions sem enforceAppCheck observado \u2014 AppCheck sem cobertura afirmativa");
|
|
1457
|
+
}
|
|
1458
|
+
} else {
|
|
1459
|
+
skipped.push("functions n\xE3o observadas \u2014 AppCheck fora deste scan");
|
|
1460
|
+
}
|
|
1364
1461
|
const modelCheck = ProjectModelSchema.safeParse(model);
|
|
1365
1462
|
if (!modelCheck.success) {
|
|
1366
1463
|
findings.push({
|
|
@@ -1399,7 +1496,7 @@ async function scan(rootDir, opts = {}) {
|
|
|
1399
1496
|
);
|
|
1400
1497
|
const totalChecks = IMPLEMENTED_CHECKS.length;
|
|
1401
1498
|
const passedChecks = IMPLEMENTED_CHECKS.filter((c) => !failedChecks.has(c)).length;
|
|
1402
|
-
const
|
|
1499
|
+
const infra = findings.filter((f) => !implemented.has(f.rule)).map((f) => ({ rule: f.rule, message: f.message }));
|
|
1403
1500
|
return {
|
|
1404
1501
|
model,
|
|
1405
1502
|
findings,
|
|
@@ -1407,15 +1504,59 @@ async function scan(rootDir, opts = {}) {
|
|
|
1407
1504
|
errors,
|
|
1408
1505
|
warnings,
|
|
1409
1506
|
infos,
|
|
1410
|
-
passed: passedChecks,
|
|
1411
|
-
total: totalChecks,
|
|
1412
1507
|
passedChecks,
|
|
1413
1508
|
totalChecks,
|
|
1414
|
-
|
|
1509
|
+
infra,
|
|
1510
|
+
checked,
|
|
1511
|
+
skipped,
|
|
1415
1512
|
coveragePct: null
|
|
1416
1513
|
}
|
|
1417
1514
|
};
|
|
1418
1515
|
}
|
|
1516
|
+
async function countAppCheckEnforced(rootDir) {
|
|
1517
|
+
try {
|
|
1518
|
+
const { readdirSync, readFileSync: readSync, statSync: stat } = await import("fs");
|
|
1519
|
+
const { join: joinP } = await import("path");
|
|
1520
|
+
const roots = [joinP(rootDir, "functions")];
|
|
1521
|
+
let count = 0;
|
|
1522
|
+
const stack = [...roots];
|
|
1523
|
+
let guard = 0;
|
|
1524
|
+
while (stack.length > 0 && guard < 200) {
|
|
1525
|
+
guard += 1;
|
|
1526
|
+
const cur = stack.pop();
|
|
1527
|
+
let entries = [];
|
|
1528
|
+
try {
|
|
1529
|
+
entries = readdirSync(cur, { withFileTypes: true });
|
|
1530
|
+
} catch {
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
for (const e of entries) {
|
|
1534
|
+
const name = e.name;
|
|
1535
|
+
const abs = joinP(cur, name);
|
|
1536
|
+
let isDir = false;
|
|
1537
|
+
try {
|
|
1538
|
+
isDir = stat(abs).isDirectory();
|
|
1539
|
+
} catch {
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
if (isDir) {
|
|
1543
|
+
if (name === "node_modules" || name === "dist" || name === "lib") continue;
|
|
1544
|
+
stack.push(abs);
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
if (!/\.(ts|js|mjs|cjs)$/.test(name)) continue;
|
|
1548
|
+
try {
|
|
1549
|
+
const text = readSync(abs, "utf-8");
|
|
1550
|
+
if (/enforceAppCheck\s*:\s*true/.test(text)) count += 1;
|
|
1551
|
+
} catch {
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
return count;
|
|
1556
|
+
} catch {
|
|
1557
|
+
return 0;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1419
1560
|
|
|
1420
1561
|
export {
|
|
1421
1562
|
discover,
|
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-TQZTE2NR.js";
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
7
|
import { createRequire } from "module";
|
|
@@ -15,13 +15,13 @@ async function main() {
|
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
17
|
if (cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
18
|
-
console.log(`firebase-audit
|
|
18
|
+
console.log(`firebase-audit
|
|
19
19
|
|
|
20
20
|
Uso:
|
|
21
21
|
firebase-audit scan [--json] [--strict] [--adapter=fn] [--cwd=path]
|
|
22
|
-
firebase-audit check [--json] [--adapter=fn] [--cwd=path] (
|
|
23
|
-
firebase-audit verify [--coverage=path] [--cwd=path] (
|
|
24
|
-
firebase-audit drift --local=indexes.json --remote=deployed.json (
|
|
22
|
+
firebase-audit check [--json] [--adapter=fn] [--cwd=path] (scan strict para CI)
|
|
23
|
+
firebase-audit verify [--coverage=path] [--cwd=path] (matriz + cobertura)
|
|
24
|
+
firebase-audit drift --local=indexes.json --remote=deployed.json (LOCAL_ONLY/REMOTE_ONLY/MATCHED)
|
|
25
25
|
|
|
26
26
|
Exit codes: 0 ok informativo; 2 quando --strict/check encontra ERROR. scan sozinho nunca reprova (use check no CI).`);
|
|
27
27
|
return;
|
|
@@ -64,20 +64,21 @@ Uso: firebase-audit scan|check|verify|drift [--json] ...`);
|
|
|
64
64
|
const { resolve: resolveRoot } = await import("path");
|
|
65
65
|
const rootDir = cwdArg ? resolveRoot(process.cwd(), cwdArg) : process.cwd();
|
|
66
66
|
if (cmd === "verify") {
|
|
67
|
-
const { scan: scanForVerify } = await import("./scan-
|
|
68
|
-
const { verify } = await import("./verify-
|
|
67
|
+
const { scan: scanForVerify } = await import("./scan-FYAWNRIE.js");
|
|
68
|
+
const { verify } = await import("./verify-GWRLO344.js");
|
|
69
69
|
const { resolve: resolveVerify } = await import("path");
|
|
70
70
|
const coverageArg = getArg("coverage");
|
|
71
71
|
const coveragePath = coverageArg ? resolveVerify(rootDir, coverageArg) : void 0;
|
|
72
72
|
const { model } = await scanForVerify(rootDir, { adapterFn: adapterArg });
|
|
73
73
|
const res = verify(model, { coverageFile: coveragePath });
|
|
74
74
|
if (asJson) {
|
|
75
|
-
console.log(JSON.stringify({
|
|
75
|
+
console.log(JSON.stringify({ 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));
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
78
|
const covStr = res.coveragePct === null ? "sem cobertura observada" : `${res.coveragePct}% de express\xF5es visitadas`;
|
|
79
|
+
const rolesNote = "pap\xE9is reduzidos (sem contrato: anonymous/user/admin)";
|
|
79
80
|
console.log(`
|
|
80
|
-
FIREBASE AUDIT verify \u2014 ${res.cases.length} casos planejados, ${res.uncovered.length} paths sem
|
|
81
|
+
FIREBASE AUDIT verify \u2014 ${res.cases.length} casos planejados (${rolesNote}), ${res.uncovered.length} paths sem cobertura (${covStr}).`);
|
|
81
82
|
for (const f of res.findings) {
|
|
82
83
|
const icon = f.severity === "ERROR" ? "\u274C" : f.severity === "WARNING" ? "\u26A0" : "\u2139";
|
|
83
84
|
console.log(`
|
|
@@ -113,7 +114,7 @@ ${icon} ${f.rule} [${f.confidence}]
|
|
|
113
114
|
localPath && existsDrift(localPath) ? localPath : null,
|
|
114
115
|
remotePath && existsDrift(remotePath) ? remotePath : null
|
|
115
116
|
);
|
|
116
|
-
console.log(JSON.stringify({
|
|
117
|
+
console.log(JSON.stringify({ entries: res2.entries, findings: res2.findings }, null, 2));
|
|
117
118
|
process.exit(1);
|
|
118
119
|
}
|
|
119
120
|
console.error(`Arquivo ${label} n\xE3o encontrado: ${p}`);
|
|
@@ -122,7 +123,7 @@ ${icon} ${f.rule} [${f.confidence}]
|
|
|
122
123
|
}
|
|
123
124
|
const res = drift(localPath, remotePath);
|
|
124
125
|
if (asJson) {
|
|
125
|
-
console.log(JSON.stringify({
|
|
126
|
+
console.log(JSON.stringify({ entries: res.entries, findings: res.findings }, null, 2));
|
|
126
127
|
return;
|
|
127
128
|
}
|
|
128
129
|
const counts = (s) => res.entries.filter((e) => e.status === s).length;
|
|
@@ -137,16 +138,17 @@ FIREBASE AUDIT drift \u2014 MATCHED ${counts("MATCHED")}, LOCAL_ONLY ${counts("L
|
|
|
137
138
|
}
|
|
138
139
|
const { findings, summary } = await scan(rootDir, { strict, adapterFn: adapterArg });
|
|
139
140
|
if (asJson) {
|
|
140
|
-
console.log(JSON.stringify({
|
|
141
|
+
console.log(JSON.stringify({ summary, findings }, null, 2));
|
|
141
142
|
if (strict && summary.errors > 0) process.exit(2);
|
|
142
143
|
return;
|
|
143
144
|
}
|
|
144
|
-
console.log(
|
|
145
|
-
FIREBASE AUDIT v${pkg.version}`);
|
|
145
|
+
console.log("\nFIREBASE AUDIT");
|
|
146
146
|
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
147
147
|
if (findings.length === 0) {
|
|
148
148
|
console.log(`
|
|
149
|
-
\u2713 Nenhum problema nos checks
|
|
149
|
+
\u2713 Nenhum problema nos checks implementados (${summary.passedChecks}/${summary.totalChecks} passaram).
|
|
150
|
+
Olhei: ${summary.checked.join(", ")}.
|
|
151
|
+
Pulei: ${summary.skipped.join(" | ") || "nada"}.
|
|
150
152
|
`);
|
|
151
153
|
return;
|
|
152
154
|
}
|
|
@@ -159,7 +161,9 @@ ${icon} ${f.rule} [${f.confidence}]${where}
|
|
|
159
161
|
if (f.fix) console.log(` \u2192 ${f.fix}`);
|
|
160
162
|
}
|
|
161
163
|
console.log(`
|
|
162
|
-
SUMMARY: ${summary.errors} errors, ${summary.warnings} warnings, ${summary.infos} infos
|
|
164
|
+
SUMMARY: ${summary.errors} errors, ${summary.warnings} warnings, ${summary.infos} infos (${summary.passedChecks}/${summary.totalChecks} checks passaram)`);
|
|
165
|
+
console.log(`Olhei: ${summary.checked.join(", ")}`);
|
|
166
|
+
console.log(`Pulei: ${summary.skipped.join(" | ") || "nada"}
|
|
163
167
|
`);
|
|
164
168
|
if (strict && summary.errors > 0) process.exit(2);
|
|
165
169
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -310,7 +310,14 @@ declare const ProjectModelSchema: z.ZodObject<{
|
|
|
310
310
|
rulesFile: z.ZodOptional<z.ZodString>;
|
|
311
311
|
indexesFile: z.ZodOptional<z.ZodString>;
|
|
312
312
|
}, z.core.$strict>;
|
|
313
|
+
storage: z.ZodOptional<z.ZodObject<{
|
|
314
|
+
rulesFile: z.ZodOptional<z.ZodString>;
|
|
315
|
+
}, z.core.$strict>>;
|
|
313
316
|
}, z.core.$strict>;
|
|
317
|
+
functions: z.ZodOptional<z.ZodObject<{
|
|
318
|
+
dir: z.ZodOptional<z.ZodString>;
|
|
319
|
+
appCheckEnforced: z.ZodOptional<z.ZodNumber>;
|
|
320
|
+
}, z.core.$strict>>;
|
|
314
321
|
authorization: z.ZodObject<{
|
|
315
322
|
adapter: z.ZodNullable<z.ZodString>;
|
|
316
323
|
roles: z.ZodArray<z.ZodObject<{
|
|
@@ -618,13 +625,15 @@ interface DiscoveryResult {
|
|
|
618
625
|
firebaseJson: string | null;
|
|
619
626
|
firestoreRulesFile: string | null;
|
|
620
627
|
firestoreIndexesFile: string | null;
|
|
628
|
+
storageRulesFile: string | null;
|
|
629
|
+
functionsDir: string | null;
|
|
621
630
|
auditYamlFile: string | null;
|
|
622
631
|
projectId: string | null;
|
|
623
|
-
/**
|
|
632
|
+
/** databaseId nomeado (sempre null sem config explícita). */
|
|
624
633
|
databaseId: string | null;
|
|
625
|
-
/**
|
|
634
|
+
/** preenchido via varredura + ai-tool (vazio sem grafo). */
|
|
626
635
|
clientFiles: string[];
|
|
627
|
-
/**
|
|
636
|
+
/** preenchido via varredura + ai-tool (vazio sem grafo). */
|
|
628
637
|
serverFiles: string[];
|
|
629
638
|
}
|
|
630
639
|
declare function discover(rootDir: string): DiscoveryResult;
|
|
@@ -634,7 +643,7 @@ declare const SERVER_HINTS_SEGMENTS: string[];
|
|
|
634
643
|
declare function classifyOrigin(file: string, content: string): "CLIENT" | "SERVER" | "UNKNOWN";
|
|
635
644
|
declare function emptyModel(rootDir: string, d: DiscoveryResult): ProjectModel;
|
|
636
645
|
/**
|
|
637
|
-
* Fase
|
|
646
|
+
* Fase opcional — enriquece Discovery com grafo do ai-tool (como lib, sem spawn/MCP).
|
|
638
647
|
* Preenche clientFiles/serverFiles a partir do mapa do projeto.
|
|
639
648
|
* Nunca joga: falha do ai-tool mantém listas vazias (honesto).
|
|
640
649
|
*/
|
|
@@ -724,8 +733,8 @@ declare function findPublicAllowsContent(rawContent: string): {
|
|
|
724
733
|
}[];
|
|
725
734
|
|
|
726
735
|
/**
|
|
727
|
-
* Checks
|
|
728
|
-
* Fingerprints
|
|
736
|
+
* Checks (FBA001–FBA004, FBA009–FBA013, subconjunto intencional).
|
|
737
|
+
* Fingerprints: FBA001/FBA002 por regra-lógica + hash da condição
|
|
729
738
|
* (estáveis a formatação, únicos por regra duplicada);
|
|
730
739
|
* FBA003/FBA004/FBA010/FBA012 com linha (únicos por ocorrência).
|
|
731
740
|
*/
|
|
@@ -736,7 +745,7 @@ declare function runChecks(model: ProjectModel, rootDir: string, opts?: {
|
|
|
736
745
|
}): Promise<Finding[]>;
|
|
737
746
|
|
|
738
747
|
/**
|
|
739
|
-
* scan — orquestra Discovery → Rules → Checks (
|
|
748
|
+
* scan — orquestra Discovery → Rules → Checks (estático primeiro).
|
|
740
749
|
*/
|
|
741
750
|
|
|
742
751
|
interface ScanOptions {
|
|
@@ -744,7 +753,7 @@ interface ScanOptions {
|
|
|
744
753
|
strict?: boolean;
|
|
745
754
|
/**
|
|
746
755
|
* Opt-in: enriquece Discovery com grafo do ai-tool (clientFiles/serverFiles).
|
|
747
|
-
* Default false (scan
|
|
756
|
+
* Default false (scan estático rápido; nenhum check consome o grafo por padrão).
|
|
748
757
|
*/
|
|
749
758
|
graph?: boolean;
|
|
750
759
|
}
|
|
@@ -752,14 +761,16 @@ interface ScanSummary {
|
|
|
752
761
|
errors: number;
|
|
753
762
|
warnings: number;
|
|
754
763
|
infos: number;
|
|
755
|
-
/** Compat: mesmo que passedChecks. */
|
|
756
|
-
passed: number;
|
|
757
|
-
/** Compat: mesmo que totalChecks. */
|
|
758
|
-
total: number;
|
|
759
764
|
passedChecks: number;
|
|
760
765
|
totalChecks: number;
|
|
761
|
-
/**
|
|
762
|
-
|
|
766
|
+
/** Infra nomeada (fora dos checks FBA): cada item diz qual e por quê. */
|
|
767
|
+
infra: {
|
|
768
|
+
rule: string;
|
|
769
|
+
message: string;
|
|
770
|
+
}[];
|
|
771
|
+
/** O que foi olhado e o que foi pulado — nunca dizer "tudo certo" sem lista. */
|
|
772
|
+
checked: string[];
|
|
773
|
+
skipped: string[];
|
|
763
774
|
/** Cobertura de Rules (0-100) quando verify com coverage avalia; null = não observada.
|
|
764
775
|
* Sempre null no scan() por desenho (cobertura só via verify coveragePct). */
|
|
765
776
|
coveragePct: number | null;
|
|
@@ -772,11 +783,11 @@ interface ScanResult {
|
|
|
772
783
|
declare function scan(rootDir: string, opts?: ScanOptions): Promise<ScanResult>;
|
|
773
784
|
|
|
774
785
|
/**
|
|
775
|
-
*
|
|
786
|
+
* Verifier (Emulator) como interface honesta.
|
|
776
787
|
*
|
|
777
788
|
* Não executa o Emulator sozinho (precisa de firebase-tools + Java no host).
|
|
778
|
-
* Gera a matriz de casos
|
|
779
|
-
* o JSON do endpoint
|
|
789
|
+
* Gera a matriz de casos e avalia cobertura quando
|
|
790
|
+
* o JSON do endpoint `ruleCoverage` é fornecido (--coverage).
|
|
780
791
|
*/
|
|
781
792
|
|
|
782
793
|
interface VerifyCase {
|
|
@@ -804,10 +815,9 @@ declare function planVerify(model: ProjectModel): VerifyCase[];
|
|
|
804
815
|
declare function verify(model: ProjectModel, opts?: VerifyOptions): VerifyResult;
|
|
805
816
|
|
|
806
817
|
/**
|
|
807
|
-
*
|
|
818
|
+
* Drift local vs implantado (comparação honesta de índices).
|
|
808
819
|
*
|
|
809
|
-
* Compara `firestore.indexes.json` local contra um JSON remoto
|
|
810
|
-
* (`gcloud firestore indexes composite list --format=json` ou REST).
|
|
820
|
+
* Compara `firestore.indexes.json` local contra um JSON remoto.
|
|
811
821
|
* Nunca afirma "unused" — apenas LOCAL_ONLY / REMOTE_ONLY / MATCHED.
|
|
812
822
|
*/
|
|
813
823
|
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpServer,
|
|
3
3
|
startMcpServer
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-EHGW2EX6.js";
|
|
5
5
|
import {
|
|
6
6
|
AccessOriginSchema,
|
|
7
7
|
AuditYamlSchema,
|
|
@@ -53,11 +53,11 @@ import {
|
|
|
53
53
|
stripOuterParens,
|
|
54
54
|
stripRuleComments,
|
|
55
55
|
validateClaimSamples
|
|
56
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-TQZTE2NR.js";
|
|
57
57
|
import {
|
|
58
58
|
planVerify,
|
|
59
59
|
verify
|
|
60
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-2X3BTSGP.js";
|
|
61
61
|
import {
|
|
62
62
|
drift
|
|
63
63
|
} from "./chunk-NJBCSW7E.js";
|
package/dist/mcp-cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
startMcpServer
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-EHGW2EX6.js";
|
|
5
|
+
import "./chunk-TQZTE2NR.js";
|
|
6
|
+
import "./chunk-2X3BTSGP.js";
|
|
7
7
|
import "./chunk-NJBCSW7E.js";
|
|
8
8
|
|
|
9
9
|
// src/mcp-cli.ts
|
package/dist/mcp.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpServer,
|
|
3
3
|
startMcpServer
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-EHGW2EX6.js";
|
|
5
|
+
import "./chunk-TQZTE2NR.js";
|
|
6
|
+
import "./chunk-2X3BTSGP.js";
|
|
7
7
|
import "./chunk-NJBCSW7E.js";
|
|
8
8
|
export {
|
|
9
9
|
createMcpServer,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justmpm/firebase-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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
|
@@ -4,11 +4,11 @@ Skill para agentes usarem o `@justmpm/firebase-audit` do jeito certo.
|
|
|
4
4
|
|
|
5
5
|
## Sequência obrigatória
|
|
6
6
|
|
|
7
|
-
1. Descobrir projeto (`firebase.json`, `firestore.rules`, `firestore.indexes.json`)
|
|
8
|
-
2. Verificar `firebase-audit.yaml` (se não existe,
|
|
9
|
-
3. Inspecionar
|
|
10
|
-
4. Executar `firebase-audit scan` (ou
|
|
11
|
-
5. Ler findings (CONFIRMED corrige agora; PROBABLE investiga; NOT_OBSERVED/UNKNOWN não afirma)
|
|
7
|
+
1. Descobrir projeto (`firebase.json`, `firestore.rules`, `storage.rules`, `firestore.indexes.json`)
|
|
8
|
+
2. Verificar `firebase-audit.yaml` (se não existe, scan roda zero-config)
|
|
9
|
+
3. Inspecionar função de permissão no código que recebe o nome como texto (ex: `rbac.can`). Guarda de rota ou function do `.rules` (ex: `isTenantMember`) não vale como adapter — sem adapter, permissões seguem UNKNOWN, nunca adivinhar
|
|
10
|
+
4. Executar `firebase-audit scan` (ou `check` com contrato no CI)
|
|
11
|
+
5. Ler findings (CONFIRMED corrige agora; PROBABLE investiga; NOT_OBSERVED/UNKNOWN não afirma) + `summary.skipped` (o que foi pulado)
|
|
12
12
|
6. Corrigir código, nunca o contrato para esconder finding
|
|
13
13
|
7. Reexecutar scan antes de declarar pronto
|
|
14
14
|
|
|
@@ -18,12 +18,14 @@ Skill para agentes usarem o `@justmpm/firebase-audit` do jeito certo.
|
|
|
18
18
|
- Nunca transformar UNKNOWN em CONFIRMED sem teste no Emulator
|
|
19
19
|
- Nunca dizer "unused" — apenas NOT_OBSERVED
|
|
20
20
|
- Claims: 1000 bytes max, sem chaves OIDC reservadas, só controle de acesso
|
|
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 `/{allPaths=**}` aberto
|
|
21
23
|
|
|
22
|
-
## Emulador
|
|
24
|
+
## Emulador
|
|
23
25
|
|
|
24
26
|
```bash
|
|
25
27
|
firebase emulators:exec --only firestore "npm test"
|
|
26
28
|
```
|
|
27
29
|
|
|
28
30
|
Matriz `anon/user/admin x read/create/update/delete` com `@firebase/rules-unit-testing`.
|
|
29
|
-
Cobertura em
|
|
31
|
+
Cobertura em `ruleCoverage` (JSON). Emulator não prova índice. Sem contrato, papéis reduzidos (anonymous/user/admin).
|