@justmpm/firebase-audit 0.4.3 → 0.5.1
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-7EWTBIKJ.js +356 -0
- package/dist/{chunk-3SEVYYCX.js → chunk-GDO2DLNQ.js} +218 -377
- package/dist/{chunk-IXMAWDJH.js → chunk-QACXCMYB.js} +33 -20
- package/dist/{chunk-NJBCSW7E.js → chunk-XZ32YQQC.js} +10 -0
- package/dist/{chunk-PSSMYRRU.js → chunk-Y4VXWSKX.js} +5 -2
- package/dist/cli.js +25 -18
- package/dist/{drift-KU7HWL7M.js → drift-QXALNO2V.js} +1 -1
- package/dist/index.d.ts +30 -20
- package/dist/index.js +12 -10
- package/dist/mcp-cli.js +5 -4
- package/dist/mcp.js +5 -4
- package/dist/rules-GTGZWN5U.js +44 -0
- package/dist/scan-4Q4XVLJU.js +7 -0
- package/dist/{verify-O722ZZBV.js → verify-WBVSESHD.js} +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +10 -7
- package/dist/scan-VREY2FGO.js +0 -6
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
scan
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-GDO2DLNQ.js";
|
|
4
4
|
import {
|
|
5
5
|
verify
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-Y4VXWSKX.js";
|
|
7
7
|
import {
|
|
8
8
|
drift
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-XZ32YQQC.js";
|
|
10
10
|
|
|
11
11
|
// src/mcp.ts
|
|
12
12
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.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 {
|
|
@@ -128,6 +128,16 @@ function drift(localFile, remoteFile) {
|
|
|
128
128
|
}
|
|
129
129
|
const remoteByKey = new Map(remote.map((r) => [keyOf(r), r]));
|
|
130
130
|
const localByKey = new Map(local.map((l) => [keyOf(l), l]));
|
|
131
|
+
if (local.length !== localByKey.size || remote.length !== remoteByKey.size) {
|
|
132
|
+
findings.push({
|
|
133
|
+
rule: "DRIFT_DUPLICATE_KEY",
|
|
134
|
+
severity: "INFO",
|
|
135
|
+
confidence: "CONFIRMED",
|
|
136
|
+
message: "\xCDndices duplicados colapsados por chave \u2014 revise o arquivo de origem.",
|
|
137
|
+
fingerprint: "DRIFT_DUPLICATE_KEY",
|
|
138
|
+
evidence: [{ id: "ev-drift-dup", kind: "drift-parse", summary: "chave duplicada", confidence: "CONFIRMED" }]
|
|
139
|
+
});
|
|
140
|
+
}
|
|
131
141
|
const entries = [];
|
|
132
142
|
for (const [key, l] of localByKey) {
|
|
133
143
|
if (remoteByKey.has(key)) entries.push({ key, status: "MATCHED", local: l, remote: remoteByKey.get(key) });
|
|
@@ -39,7 +39,7 @@ function planVerify(model) {
|
|
|
39
39
|
}
|
|
40
40
|
const seen = /* @__PURE__ */ new Set();
|
|
41
41
|
return cases.filter((c) => {
|
|
42
|
-
const k = `${c.role}:${c.operation}:${c.path}`;
|
|
42
|
+
const k = `${c.role}:${c.operation}:${c.pathTemplate}:${c.path}`;
|
|
43
43
|
if (seen.has(k)) return false;
|
|
44
44
|
seen.add(k);
|
|
45
45
|
return true;
|
|
@@ -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
|
});
|
|
@@ -105,8 +105,11 @@ function verify(model, opts = {}) {
|
|
|
105
105
|
if (coveragePct === 100) {
|
|
106
106
|
return { cases, uncovered: [], findings, coveragePct };
|
|
107
107
|
}
|
|
108
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
108
109
|
for (const rule of model.rules) {
|
|
109
110
|
if (rule.path === "/(unknown)") continue;
|
|
111
|
+
if (seenPaths.has(rule.path)) continue;
|
|
112
|
+
seenPaths.add(rule.path);
|
|
110
113
|
uncovered.push(rule.path);
|
|
111
114
|
}
|
|
112
115
|
return { cases, uncovered, findings, coveragePct };
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
scan
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-GDO2DLNQ.js";
|
|
5
|
+
import "./chunk-7EWTBIKJ.js";
|
|
5
6
|
|
|
6
7
|
// src/cli.ts
|
|
7
8
|
import { createRequire } from "module";
|
|
@@ -15,13 +16,13 @@ async function main() {
|
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
17
18
|
if (cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
18
|
-
console.log(`firebase-audit
|
|
19
|
+
console.log(`firebase-audit
|
|
19
20
|
|
|
20
21
|
Uso:
|
|
21
22
|
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 (
|
|
23
|
+
firebase-audit check [--json] [--adapter=fn] [--cwd=path] (scan strict para CI)
|
|
24
|
+
firebase-audit verify [--coverage=path] [--cwd=path] (matriz + cobertura)
|
|
25
|
+
firebase-audit drift --local=indexes.json --remote=deployed.json (LOCAL_ONLY/REMOTE_ONLY/MATCHED)
|
|
25
26
|
|
|
26
27
|
Exit codes: 0 ok informativo; 2 quando --strict/check encontra ERROR. scan sozinho nunca reprova (use check no CI).`);
|
|
27
28
|
return;
|
|
@@ -64,20 +65,23 @@ Uso: firebase-audit scan|check|verify|drift [--json] ...`);
|
|
|
64
65
|
const { resolve: resolveRoot } = await import("path");
|
|
65
66
|
const rootDir = cwdArg ? resolveRoot(process.cwd(), cwdArg) : process.cwd();
|
|
66
67
|
if (cmd === "verify") {
|
|
67
|
-
const { scan: scanForVerify } = await import("./scan-
|
|
68
|
-
const { verify } = await import("./verify-
|
|
68
|
+
const { scan: scanForVerify } = await import("./scan-4Q4XVLJU.js");
|
|
69
|
+
const { verify } = await import("./verify-WBVSESHD.js");
|
|
69
70
|
const { resolve: resolveVerify } = await import("path");
|
|
70
71
|
const coverageArg = getArg("coverage");
|
|
71
72
|
const coveragePath = coverageArg ? resolveVerify(rootDir, coverageArg) : void 0;
|
|
72
73
|
const { model } = await scanForVerify(rootDir, { adapterFn: adapterArg });
|
|
73
74
|
const res = verify(model, { coverageFile: coveragePath });
|
|
75
|
+
const roles = model.authorization.roles.length > 0 ? model.authorization.roles.map((r) => r.name) : ["anonymous", "user", "admin"];
|
|
76
|
+
const rolesReduced = model.authorization.roles.length === 0;
|
|
74
77
|
if (asJson) {
|
|
75
|
-
console.log(JSON.stringify({
|
|
78
|
+
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));
|
|
76
79
|
return;
|
|
77
80
|
}
|
|
78
81
|
const covStr = res.coveragePct === null ? "sem cobertura observada" : `${res.coveragePct}% de express\xF5es visitadas`;
|
|
82
|
+
const rolesNote = rolesReduced ? "pap\xE9is reduzidos (sem contrato: anonymous/user/admin)" : `${roles.length} pap\xE9is do contrato (${roles.join(", ")})`;
|
|
79
83
|
console.log(`
|
|
80
|
-
FIREBASE AUDIT verify \u2014 ${res.cases.length} casos planejados, ${res.uncovered.length} paths sem
|
|
84
|
+
FIREBASE AUDIT verify \u2014 ${res.cases.length} casos planejados (${rolesNote}), ${res.uncovered.length} paths sem cobertura (${covStr}).`);
|
|
81
85
|
for (const f of res.findings) {
|
|
82
86
|
const icon = f.severity === "ERROR" ? "\u274C" : f.severity === "WARNING" ? "\u26A0" : "\u2139";
|
|
83
87
|
console.log(`
|
|
@@ -94,7 +98,7 @@ ${icon} ${f.rule} [${f.confidence}]
|
|
|
94
98
|
return;
|
|
95
99
|
}
|
|
96
100
|
if (cmd === "drift") {
|
|
97
|
-
const { drift } = await import("./drift-
|
|
101
|
+
const { drift } = await import("./drift-QXALNO2V.js");
|
|
98
102
|
const { resolve: resolveDrift } = await import("path");
|
|
99
103
|
const { existsSync: existsDrift } = await import("fs");
|
|
100
104
|
const localArg = getArg("local");
|
|
@@ -108,12 +112,12 @@ ${icon} ${f.rule} [${f.confidence}]
|
|
|
108
112
|
for (const [label, p] of [["--local", localPath], ["--remote", remotePath]]) {
|
|
109
113
|
if (p && !existsDrift(p)) {
|
|
110
114
|
if (asJson) {
|
|
111
|
-
const { drift: driftMissing } = await import("./drift-
|
|
115
|
+
const { drift: driftMissing } = await import("./drift-QXALNO2V.js");
|
|
112
116
|
const res2 = driftMissing(
|
|
113
117
|
localPath && existsDrift(localPath) ? localPath : null,
|
|
114
118
|
remotePath && existsDrift(remotePath) ? remotePath : null
|
|
115
119
|
);
|
|
116
|
-
console.log(JSON.stringify({
|
|
120
|
+
console.log(JSON.stringify({ entries: res2.entries, findings: res2.findings }, null, 2));
|
|
117
121
|
process.exit(1);
|
|
118
122
|
}
|
|
119
123
|
console.error(`Arquivo ${label} n\xE3o encontrado: ${p}`);
|
|
@@ -122,7 +126,7 @@ ${icon} ${f.rule} [${f.confidence}]
|
|
|
122
126
|
}
|
|
123
127
|
const res = drift(localPath, remotePath);
|
|
124
128
|
if (asJson) {
|
|
125
|
-
console.log(JSON.stringify({
|
|
129
|
+
console.log(JSON.stringify({ entries: res.entries, findings: res.findings }, null, 2));
|
|
126
130
|
return;
|
|
127
131
|
}
|
|
128
132
|
const counts = (s) => res.entries.filter((e) => e.status === s).length;
|
|
@@ -137,16 +141,17 @@ FIREBASE AUDIT drift \u2014 MATCHED ${counts("MATCHED")}, LOCAL_ONLY ${counts("L
|
|
|
137
141
|
}
|
|
138
142
|
const { findings, summary } = await scan(rootDir, { strict, adapterFn: adapterArg });
|
|
139
143
|
if (asJson) {
|
|
140
|
-
console.log(JSON.stringify({
|
|
144
|
+
console.log(JSON.stringify({ summary, findings }, null, 2));
|
|
141
145
|
if (strict && summary.errors > 0) process.exit(2);
|
|
142
146
|
return;
|
|
143
147
|
}
|
|
144
|
-
console.log(
|
|
145
|
-
FIREBASE AUDIT v${pkg.version}`);
|
|
148
|
+
console.log("\nFIREBASE AUDIT");
|
|
146
149
|
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
150
|
if (findings.length === 0) {
|
|
148
151
|
console.log(`
|
|
149
|
-
\u2713 Nenhum problema nos checks
|
|
152
|
+
\u2713 Nenhum problema nos checks implementados (${summary.passedChecks}/${summary.totalChecks} passaram).
|
|
153
|
+
Olhei: ${summary.checked.join(", ")}.
|
|
154
|
+
Pulei: ${summary.skipped.join(" | ") || "nada"}.
|
|
150
155
|
`);
|
|
151
156
|
return;
|
|
152
157
|
}
|
|
@@ -159,7 +164,9 @@ ${icon} ${f.rule} [${f.confidence}]${where}
|
|
|
159
164
|
if (f.fix) console.log(` \u2192 ${f.fix}`);
|
|
160
165
|
}
|
|
161
166
|
console.log(`
|
|
162
|
-
SUMMARY: ${summary.errors} errors, ${summary.warnings} warnings, ${summary.infos} infos
|
|
167
|
+
SUMMARY: ${summary.errors} errors, ${summary.warnings} warnings, ${summary.infos} infos (${summary.passedChecks}/${summary.totalChecks} checks passaram)`);
|
|
168
|
+
console.log(`Olhei: ${summary.checked.join(", ")}`);
|
|
169
|
+
console.log(`Pulei: ${summary.skipped.join(" | ") || "nada"}
|
|
163
170
|
`);
|
|
164
171
|
if (strict && summary.errors > 0) process.exit(2);
|
|
165
172
|
}
|
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-QACXCMYB.js";
|
|
5
5
|
import {
|
|
6
6
|
AccessOriginSchema,
|
|
7
7
|
AuditYamlSchema,
|
|
@@ -26,12 +26,17 @@ import {
|
|
|
26
26
|
RuleSchema,
|
|
27
27
|
SERVER_HINTS_SEGMENTS,
|
|
28
28
|
SeveritySchema,
|
|
29
|
-
buildFullPath,
|
|
30
29
|
classifyOrigin,
|
|
31
|
-
conditionKeyForFingerprint,
|
|
32
30
|
discover,
|
|
33
31
|
emptyModel,
|
|
34
32
|
enrichWithGraph,
|
|
33
|
+
runChecks,
|
|
34
|
+
scan,
|
|
35
|
+
validateClaimSamples
|
|
36
|
+
} from "./chunk-GDO2DLNQ.js";
|
|
37
|
+
import {
|
|
38
|
+
buildFullPath,
|
|
39
|
+
conditionKeyForFingerprint,
|
|
35
40
|
extractRules,
|
|
36
41
|
extractRulesContent,
|
|
37
42
|
findPublicAllows,
|
|
@@ -48,19 +53,16 @@ import {
|
|
|
48
53
|
parseRuleFunctionsDetailed,
|
|
49
54
|
resolveHelperCondition,
|
|
50
55
|
resolveHelperConditionDetailed,
|
|
51
|
-
runChecks,
|
|
52
|
-
scan,
|
|
53
56
|
stripOuterParens,
|
|
54
|
-
stripRuleComments
|
|
55
|
-
|
|
56
|
-
} from "./chunk-3SEVYYCX.js";
|
|
57
|
+
stripRuleComments
|
|
58
|
+
} from "./chunk-7EWTBIKJ.js";
|
|
57
59
|
import {
|
|
58
60
|
planVerify,
|
|
59
61
|
verify
|
|
60
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-Y4VXWSKX.js";
|
|
61
63
|
import {
|
|
62
64
|
drift
|
|
63
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-XZ32YQQC.js";
|
|
64
66
|
|
|
65
67
|
// src/index.ts
|
|
66
68
|
import { createRequire } from "module";
|
package/dist/mcp-cli.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
startMcpServer
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-QACXCMYB.js";
|
|
5
|
+
import "./chunk-GDO2DLNQ.js";
|
|
6
|
+
import "./chunk-7EWTBIKJ.js";
|
|
7
|
+
import "./chunk-Y4VXWSKX.js";
|
|
8
|
+
import "./chunk-XZ32YQQC.js";
|
|
8
9
|
|
|
9
10
|
// src/mcp-cli.ts
|
|
10
11
|
startMcpServer().catch((err) => {
|
package/dist/mcp.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpServer,
|
|
3
3
|
startMcpServer
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-QACXCMYB.js";
|
|
5
|
+
import "./chunk-GDO2DLNQ.js";
|
|
6
|
+
import "./chunk-7EWTBIKJ.js";
|
|
7
|
+
import "./chunk-Y4VXWSKX.js";
|
|
8
|
+
import "./chunk-XZ32YQQC.js";
|
|
8
9
|
export {
|
|
9
10
|
createMcpServer,
|
|
10
11
|
startMcpServer
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildFullPath,
|
|
3
|
+
conditionKeyForFingerprint,
|
|
4
|
+
extractRules,
|
|
5
|
+
extractRulesContent,
|
|
6
|
+
findPublicAllows,
|
|
7
|
+
findPublicAllowsContent,
|
|
8
|
+
hashShort,
|
|
9
|
+
helperBodyToCondition,
|
|
10
|
+
inlineHelperArgs,
|
|
11
|
+
inlineHelpersInCondition,
|
|
12
|
+
isPublicCondition,
|
|
13
|
+
normalizeTarget,
|
|
14
|
+
opsFrom,
|
|
15
|
+
parseMatchBlocks,
|
|
16
|
+
parseRuleFunctions,
|
|
17
|
+
parseRuleFunctionsDetailed,
|
|
18
|
+
resolveHelperCondition,
|
|
19
|
+
resolveHelperConditionDetailed,
|
|
20
|
+
stripOuterParens,
|
|
21
|
+
stripRuleComments
|
|
22
|
+
} from "./chunk-7EWTBIKJ.js";
|
|
23
|
+
export {
|
|
24
|
+
buildFullPath,
|
|
25
|
+
conditionKeyForFingerprint,
|
|
26
|
+
extractRules,
|
|
27
|
+
extractRulesContent,
|
|
28
|
+
findPublicAllows,
|
|
29
|
+
findPublicAllowsContent,
|
|
30
|
+
hashShort,
|
|
31
|
+
helperBodyToCondition,
|
|
32
|
+
inlineHelperArgs,
|
|
33
|
+
inlineHelpersInCondition,
|
|
34
|
+
isPublicCondition,
|
|
35
|
+
normalizeTarget,
|
|
36
|
+
opsFrom,
|
|
37
|
+
parseMatchBlocks,
|
|
38
|
+
parseRuleFunctions,
|
|
39
|
+
parseRuleFunctionsDetailed,
|
|
40
|
+
resolveHelperCondition,
|
|
41
|
+
resolveHelperConditionDetailed,
|
|
42
|
+
stripOuterParens,
|
|
43
|
+
stripRuleComments
|
|
44
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justmpm/firebase-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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,15 @@ 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 curinga total aberto
|
|
23
|
+
- Seeds/fixtures com Admin SDK moram em `scripts/` ou `server/`, nunca em `src/` (senão o scan acusa FBA003 de propósito)
|
|
21
24
|
|
|
22
|
-
## Emulador
|
|
25
|
+
## Emulador
|
|
23
26
|
|
|
24
27
|
```bash
|
|
25
28
|
firebase emulators:exec --only firestore "npm test"
|
|
26
29
|
```
|
|
27
30
|
|
|
28
31
|
Matriz `anon/user/admin x read/create/update/delete` com `@firebase/rules-unit-testing`.
|
|
29
|
-
Cobertura em
|
|
32
|
+
Cobertura em `ruleCoverage` (JSON). Emulator não prova índice. Sem contrato, papéis reduzidos (anonymous/user/admin).
|