@justmpm/firebase-audit 0.5.6 → 0.5.8
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-FECVBQUO.js → chunk-7J3ADEBW.js} +8 -4
- package/dist/{chunk-DIO36A3I.js → chunk-CSMT4PIP.js} +13 -6
- package/dist/{chunk-SH6HFX5Y.js → chunk-D75SRJ7P.js} +8 -7
- package/dist/{chunk-XZ32YQQC.js → chunk-LD3F73XI.js} +13 -3
- package/dist/{chunk-BB6XXEIK.js → chunk-NZT4NIVA.js} +76 -28
- package/dist/cli.js +9 -6
- package/dist/{drift-QXALNO2V.js → drift-C3JZI5ZA.js} +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +7 -5
- package/dist/mcp-cli.js +5 -5
- package/dist/mcp.js +5 -5
- package/dist/{rules-W4VUUQM4.js → rules-HCXATUST.js} +1 -1
- package/dist/scan-RTIMO3I2.js +7 -0
- package/dist/{verify-DECP4IUW.js → verify-GQSR4FQL.js} +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
- package/dist/scan-SDJSDRDQ.js +0 -7
|
@@ -42,11 +42,12 @@ function stripRuleComments(content) {
|
|
|
42
42
|
}
|
|
43
43
|
function opsFrom(target) {
|
|
44
44
|
const t = target.trim().toLowerCase();
|
|
45
|
-
if (t === "read") return { ops: ["read", "get", "list"], known: true };
|
|
46
|
-
if (t === "write") return { ops: ["write", "create", "update", "delete"], known: true };
|
|
45
|
+
if (t === "read") return { ops: ["read", "get", "list"], known: true, unknown: [] };
|
|
46
|
+
if (t === "write") return { ops: ["write", "create", "update", "delete"], known: true, unknown: [] };
|
|
47
47
|
const parts = t.split(",").map((s) => s.trim()).filter(Boolean);
|
|
48
48
|
const valid = ["read", "get", "list", "create", "update", "delete", "write"];
|
|
49
49
|
const out = [];
|
|
50
|
+
const unknown = [];
|
|
50
51
|
let known = false;
|
|
51
52
|
for (const p of parts) {
|
|
52
53
|
if (valid.includes(p)) {
|
|
@@ -62,9 +63,11 @@ function opsFrom(target) {
|
|
|
62
63
|
if (!out.includes(extra)) out.push(extra);
|
|
63
64
|
}
|
|
64
65
|
}
|
|
66
|
+
} else {
|
|
67
|
+
unknown.push(p);
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
|
-
return { ops: out, known: out.length > 0 && known };
|
|
70
|
+
return { ops: out, known: out.length > 0 && known, unknown };
|
|
68
71
|
}
|
|
69
72
|
function findBlockEnd(content, openBraceIndex) {
|
|
70
73
|
let depth = 0;
|
|
@@ -247,13 +250,14 @@ function extractRulesContent(rawContent, relFile) {
|
|
|
247
250
|
const requestResourceReferences = [
|
|
248
251
|
...big.matchAll(/request\.resource\.data\.([A-Za-z0-9_]+)/g)
|
|
249
252
|
].map((x) => x[1]);
|
|
250
|
-
const { ops, known } = opsFrom(target);
|
|
253
|
+
const { ops, known, unknown } = opsFrom(target);
|
|
251
254
|
const isHelperCall = !unconditional && rawCondition !== resolved;
|
|
252
255
|
const hasAuth = authSource.includes("request.auth");
|
|
253
256
|
rules.push({
|
|
254
257
|
id: `rule-${idx++}`,
|
|
255
258
|
path: fullPath,
|
|
256
259
|
operations: ops,
|
|
260
|
+
unknownOperations: unknown.length > 0 ? unknown : void 0,
|
|
257
261
|
conditionPresent: !unconditional && condition.length > 0,
|
|
258
262
|
condition: unconditional ? void 0 : condition,
|
|
259
263
|
authReferences,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// src/verify.ts
|
|
2
|
-
import { readFileSync, existsSync } from "fs";
|
|
3
|
-
function collectVisitCounts(node, out) {
|
|
4
|
-
if (typeof node !== "object" || node === null) return;
|
|
2
|
+
import { readFileSync, existsSync, statSync } from "fs";
|
|
3
|
+
function collectVisitCounts(node, out, depth = 0) {
|
|
4
|
+
if (typeof node !== "object" || node === null || depth > 100) return;
|
|
5
5
|
if (Array.isArray(node)) {
|
|
6
|
-
for (const v of node) collectVisitCounts(v, out);
|
|
6
|
+
for (const v of node) collectVisitCounts(v, out, depth + 1);
|
|
7
7
|
return;
|
|
8
8
|
}
|
|
9
9
|
const rec = node;
|
|
@@ -14,7 +14,7 @@ function collectVisitCounts(node, out) {
|
|
|
14
14
|
break;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
-
for (const v of Object.values(rec)) collectVisitCounts(v, out);
|
|
17
|
+
for (const v of Object.values(rec)) collectVisitCounts(v, out, depth + 1);
|
|
18
18
|
}
|
|
19
19
|
function planVerify(model) {
|
|
20
20
|
const roles = model.authorization.roles.length > 0 ? model.authorization.roles.map((r) => r.name) : ["anonymous", "user", "admin"];
|
|
@@ -63,6 +63,13 @@ function verify(model, opts = {}) {
|
|
|
63
63
|
});
|
|
64
64
|
} else {
|
|
65
65
|
try {
|
|
66
|
+
let tooBig = false;
|
|
67
|
+
try {
|
|
68
|
+
tooBig = statSync(opts.coverageFile).size > 5 * 1024 * 1024;
|
|
69
|
+
} catch {
|
|
70
|
+
tooBig = false;
|
|
71
|
+
}
|
|
72
|
+
if (tooBig) throw new Error("coverage acima de 5 MB");
|
|
66
73
|
const raw = JSON.parse(readFileSync(opts.coverageFile, "utf-8"));
|
|
67
74
|
const counts = [];
|
|
68
75
|
collectVisitCounts(raw, counts);
|
|
@@ -95,7 +102,7 @@ function verify(model, opts = {}) {
|
|
|
95
102
|
rule: "COVERAGE_INVALID",
|
|
96
103
|
severity: "WARNING",
|
|
97
104
|
confidence: "CONFIRMED",
|
|
98
|
-
message: "Arquivo de cobertura inv\xE1lido \u2014 Verifier sem cobertura neste run.",
|
|
105
|
+
message: "Arquivo de cobertura inv\xE1lido ou acima de 5 MB \u2014 Verifier sem cobertura neste run.",
|
|
99
106
|
fingerprint: "COVERAGE_INVALID",
|
|
100
107
|
evidence: [{ id: "ev-coverage", kind: "coverage-parse", summary: "JSON inv\xE1lido", confidence: "CONFIRMED" }]
|
|
101
108
|
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
scan
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-NZT4NIVA.js";
|
|
4
4
|
import {
|
|
5
5
|
verify
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-CSMT4PIP.js";
|
|
7
7
|
import {
|
|
8
8
|
drift
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-LD3F73XI.js";
|
|
10
10
|
|
|
11
11
|
// src/mcp.ts
|
|
12
12
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -142,17 +142,18 @@ function createMcpServer() {
|
|
|
142
142
|
"verify",
|
|
143
143
|
{
|
|
144
144
|
title: "Firebase Audit Verify (Emulator)",
|
|
145
|
-
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.",
|
|
145
|
+
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. N\xE3o substitui scan/check.",
|
|
146
146
|
inputSchema: {
|
|
147
147
|
cwd: z.string().min(1).describe("Diret\xF3rio raiz do projeto"),
|
|
148
|
-
coverage: z.string().optional().describe("Path do coverage.json do ruleCoverage (relativo ao cwd)")
|
|
148
|
+
coverage: z.string().optional().describe("Path do coverage.json do ruleCoverage (relativo ao cwd)"),
|
|
149
|
+
adapter: z.string().optional().describe("Fun\xE7\xE3o de permiss\xE3o no c\xF3digo que recebe o nome como texto (ex: rbac.can). Default: do contrato.")
|
|
149
150
|
}
|
|
150
151
|
},
|
|
151
|
-
async ({ cwd, coverage }) => {
|
|
152
|
+
async ({ cwd, coverage, adapter }) => {
|
|
152
153
|
try {
|
|
153
154
|
assertCwd(cwd);
|
|
154
155
|
const { resolve } = await import("path");
|
|
155
|
-
const { summary: scanSummary, model } = await scan(cwd);
|
|
156
|
+
const { summary: scanSummary, model } = await scan(cwd, { adapterFn: adapter });
|
|
156
157
|
const res = verify(model, { coverageFile: coverage ? resolve(cwd, coverage) : void 0 });
|
|
157
158
|
const roles = model.authorization.roles.length > 0 ? model.authorization.roles.map((r) => r.name) : ["anonymous", "user", "admin"];
|
|
158
159
|
const truncated = res.cases.length > 50;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/drift.ts
|
|
2
|
-
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { readFileSync, existsSync, statSync } from "fs";
|
|
3
3
|
function normalizeMode(f) {
|
|
4
4
|
if (typeof f.fieldPath !== "string" || f.fieldPath.length === 0) return null;
|
|
5
5
|
if (f.vectorConfig !== void 0 && f.vectorConfig !== null) return { fieldPath: f.fieldPath, mode: "VECTOR" };
|
|
@@ -59,10 +59,19 @@ function drift(localFile, remoteFile) {
|
|
|
59
59
|
});
|
|
60
60
|
return { entries: [], findings };
|
|
61
61
|
}
|
|
62
|
+
const tooLarge = (f) => {
|
|
63
|
+
if (!f || !existsSync(f)) return false;
|
|
64
|
+
try {
|
|
65
|
+
return statSync(f).size > 5 * 1024 * 1024;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
62
70
|
let local = [];
|
|
63
71
|
let remote = [];
|
|
64
72
|
if (localFile && existsSync(localFile)) {
|
|
65
73
|
try {
|
|
74
|
+
if (tooLarge(localFile)) throw new Error("local acima de 5 MB");
|
|
66
75
|
const raw = JSON.parse(readFileSync(localFile, "utf-8"));
|
|
67
76
|
const rawIndexes = normalizeList(raw);
|
|
68
77
|
const parsed = [];
|
|
@@ -88,7 +97,7 @@ function drift(localFile, remoteFile) {
|
|
|
88
97
|
rule: "DRIFT_LOCAL_INVALID",
|
|
89
98
|
severity: "WARNING",
|
|
90
99
|
confidence: "CONFIRMED",
|
|
91
|
-
message: "\xCDndices locais inv\xE1lidos \u2014 drift sem base local.",
|
|
100
|
+
message: "\xCDndices locais inv\xE1lidos ou acima de 5 MB \u2014 drift sem base local.",
|
|
92
101
|
fingerprint: "DRIFT_LOCAL_INVALID",
|
|
93
102
|
evidence: [{ id: "ev-drift-local", kind: "drift-parse", summary: "local inv\xE1lido", confidence: "CONFIRMED" }]
|
|
94
103
|
});
|
|
@@ -96,6 +105,7 @@ function drift(localFile, remoteFile) {
|
|
|
96
105
|
}
|
|
97
106
|
if (remoteFile && existsSync(remoteFile)) {
|
|
98
107
|
try {
|
|
108
|
+
if (tooLarge(remoteFile)) throw new Error("remoto acima de 5 MB");
|
|
99
109
|
const list = normalizeList(JSON.parse(readFileSync(remoteFile, "utf-8")));
|
|
100
110
|
const parsed = [];
|
|
101
111
|
let skipped = 0;
|
|
@@ -120,7 +130,7 @@ function drift(localFile, remoteFile) {
|
|
|
120
130
|
rule: "DRIFT_REMOTE_INVALID",
|
|
121
131
|
severity: "WARNING",
|
|
122
132
|
confidence: "CONFIRMED",
|
|
123
|
-
message: "\xCDndices remotos inv\xE1lidos \u2014 drift sem base remota.",
|
|
133
|
+
message: "\xCDndices remotos inv\xE1lidos ou acima de 5 MB \u2014 drift sem base remota.",
|
|
124
134
|
fingerprint: "DRIFT_REMOTE_INVALID",
|
|
125
135
|
evidence: [{ id: "ev-drift-remote", kind: "drift-parse", summary: "remoto inv\xE1lido", confidence: "CONFIRMED" }]
|
|
126
136
|
});
|
|
@@ -9,10 +9,10 @@ import {
|
|
|
9
9
|
resolveHelperConditionDetailed,
|
|
10
10
|
stripOuterParens,
|
|
11
11
|
stripRuleComments
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-7J3ADEBW.js";
|
|
13
13
|
|
|
14
14
|
// src/scan.ts
|
|
15
|
-
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
15
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
16
16
|
import { join as join3 } from "path";
|
|
17
17
|
import { parse as parseYaml } from "yaml";
|
|
18
18
|
|
|
@@ -107,6 +107,10 @@ var SERVER_HINTS_SEGMENTS = [
|
|
|
107
107
|
"backend",
|
|
108
108
|
"scripts"
|
|
109
109
|
];
|
|
110
|
+
function isTestPath(file) {
|
|
111
|
+
const n = file.replace(/\\/g, "/");
|
|
112
|
+
return /\.test\.[tj]sx?$/.test(n) || /\.spec\.[tj]sx?$/.test(n) || /(^|\/)__tests__\//.test(n) || /(^|\/)(test|tests|testing)\//.test(n);
|
|
113
|
+
}
|
|
110
114
|
function classifyOrigin(file, content) {
|
|
111
115
|
const rel = file.replace(/\\/g, "/");
|
|
112
116
|
const segs = rel.split("/");
|
|
@@ -123,6 +127,8 @@ function classifyOrigin(file, content) {
|
|
|
123
127
|
if (rel.includes("src/app/api/")) return "SERVER";
|
|
124
128
|
if (rel.includes("server/api/")) return "SERVER";
|
|
125
129
|
if (hasSeg(...SERVER_HINTS_SEGMENTS)) return "SERVER";
|
|
130
|
+
const base = segs[segs.length - 1] ?? "";
|
|
131
|
+
if (/^(server|middleware)[.\-]/i.test(base)) return "SERVER";
|
|
126
132
|
if (content.includes("firebase-admin") || content.includes("firebase-functions")) {
|
|
127
133
|
return "CLIENT";
|
|
128
134
|
}
|
|
@@ -161,14 +167,14 @@ async function enrichWithGraph(rootDir, d) {
|
|
|
161
167
|
const res = await ai.map({ cwd: rootDir, format: "json" });
|
|
162
168
|
const files = Array.isArray(res.files) ? res.files : [];
|
|
163
169
|
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);
|
|
164
|
-
const { readFileSync: readSync, existsSync: existsSyncFn, statSync:
|
|
170
|
+
const { readFileSync: readSync, existsSync: existsSyncFn, statSync: statSync3 } = await import("fs");
|
|
165
171
|
const { join: joinP } = await import("path");
|
|
166
172
|
for (const rel of code) {
|
|
167
173
|
let content = "";
|
|
168
174
|
try {
|
|
169
175
|
const abs = joinP(rootDir, rel);
|
|
170
176
|
if (!existsSyncFn(abs)) continue;
|
|
171
|
-
if (
|
|
177
|
+
if (statSync3(abs).size > 100 * 1024) continue;
|
|
172
178
|
content = readSync(abs, "utf-8");
|
|
173
179
|
} catch {
|
|
174
180
|
content = "";
|
|
@@ -187,7 +193,7 @@ import { join as join2 } from "path";
|
|
|
187
193
|
import { executeFind } from "@justmpm/supergrep";
|
|
188
194
|
function isBuildOutput(file) {
|
|
189
195
|
const n = file.replace(/\\/g, "/");
|
|
190
|
-
return /(^|\/)(node_modules|dist|build|\.next|coverage|out)\//.test(n);
|
|
196
|
+
return /(^|\/)(node_modules|dist|build|\.next|coverage|out)\//.test(n) || /\/functions\/lib\//.test(n);
|
|
191
197
|
}
|
|
192
198
|
var IMPLEMENTED_CHECKS = ["FBA001", "FBA002", "FBA003", "FBA004", "FBA009", "FBA010", "FBA011", "FBA012", "FBA013"];
|
|
193
199
|
function createEvidence() {
|
|
@@ -273,6 +279,28 @@ async function runChecks(model, rootDir, opts = {}) {
|
|
|
273
279
|
}
|
|
274
280
|
}
|
|
275
281
|
}
|
|
282
|
+
for (const rule of model.rules) {
|
|
283
|
+
const unknown2 = rule.unknownOperations ?? [];
|
|
284
|
+
if (rule.operations.length === 0 || unknown2.length > 0) {
|
|
285
|
+
const detail = unknown2.length > 0 ? ` (trecho desconhecido: ${unknown2.join(", ")})` : "";
|
|
286
|
+
findings.push(
|
|
287
|
+
make(
|
|
288
|
+
"RULES_UNKNOWN_OP",
|
|
289
|
+
"WARNING",
|
|
290
|
+
"CONFIRMED",
|
|
291
|
+
`Opera\xE7\xE3o desconhecida em ${rule.path}${detail} (${rule.location.file}:${rule.location.start.line}). Typo em allow? Deploy rejeita.`,
|
|
292
|
+
`RULES_UNKNOWN_OP:${rule.path}:${rule.location.start.line}:${[...rule.operations].sort().join(",")}`,
|
|
293
|
+
{
|
|
294
|
+
file: rule.location.file,
|
|
295
|
+
line: rule.location.start.line,
|
|
296
|
+
resource: rule.path,
|
|
297
|
+
evidence: [ev("unknown-op", rule.condition ?? "allow sem opera\xE7\xE3o v\xE1lida", "CONFIRMED", rule.location.file, rule.location.start.line)],
|
|
298
|
+
fix: "Corrija para read/get/list/write/create/update/delete."
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
276
304
|
const adapterFn = opts.adapterFn ?? "rbac.can";
|
|
277
305
|
const permissionCalls = await collectPermissionCalls(rootDir, adapterFn);
|
|
278
306
|
const declared = new Set(model.authorization.permissions.map((p) => p.name));
|
|
@@ -508,10 +536,6 @@ async function runChecks(model, rootDir, opts = {}) {
|
|
|
508
536
|
}
|
|
509
537
|
}
|
|
510
538
|
const adminHits = await collectAdminImports(rootDir);
|
|
511
|
-
const isTestPath = (f) => {
|
|
512
|
-
const n = f.replace(/\\/g, "/");
|
|
513
|
-
return /\.test\.[tj]sx?$/.test(n) || /\.spec\.[tj]sx?$/.test(n) || /(^|\/)__tests__\//.test(n) || /(^|\/)(test|tests|testing)\//.test(n);
|
|
514
|
-
};
|
|
515
539
|
for (const hit of adminHits) {
|
|
516
540
|
let origin = "UNKNOWN";
|
|
517
541
|
try {
|
|
@@ -571,7 +595,7 @@ async function collectPermissionCalls(rootDir, adapterFn) {
|
|
|
571
595
|
}
|
|
572
596
|
const seen = /* @__PURE__ */ new Set();
|
|
573
597
|
const pushLiteral = (file, line, permission) => {
|
|
574
|
-
if (isBuildOutput(file)) return;
|
|
598
|
+
if (isBuildOutput(file) || isTestPath(file)) return;
|
|
575
599
|
const k = `${file}:${line}:${permission}`;
|
|
576
600
|
if (seen.has(k)) return;
|
|
577
601
|
seen.add(k);
|
|
@@ -584,7 +608,7 @@ async function collectPermissionCalls(rootDir, adapterFn) {
|
|
|
584
608
|
const raw = (m.metaVariables["PERM"] ?? "").trim();
|
|
585
609
|
const line = m.line + 1;
|
|
586
610
|
if (raw.includes("${")) {
|
|
587
|
-
if (!isBuildOutput(m.file)) {
|
|
611
|
+
if (!isBuildOutput(m.file) && !isTestPath(m.file)) {
|
|
588
612
|
const k = `${m.file}:${line}:${raw}`;
|
|
589
613
|
if (!seen.has(k)) {
|
|
590
614
|
seen.add(k);
|
|
@@ -598,7 +622,7 @@ async function collectPermissionCalls(rootDir, adapterFn) {
|
|
|
598
622
|
const perm = raw.replace(/^['"`]|['"`]$/g, "").trim();
|
|
599
623
|
if (/^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*)*$/.test(perm)) pushLiteral(m.file, line, perm);
|
|
600
624
|
else {
|
|
601
|
-
if (!isBuildOutput(m.file)) {
|
|
625
|
+
if (!isBuildOutput(m.file) && !isTestPath(m.file)) {
|
|
602
626
|
const k = `${m.file}:${line}:${raw}:invalid`;
|
|
603
627
|
if (!seen.has(k)) {
|
|
604
628
|
seen.add(k);
|
|
@@ -607,7 +631,7 @@ async function collectPermissionCalls(rootDir, adapterFn) {
|
|
|
607
631
|
}
|
|
608
632
|
}
|
|
609
633
|
} else if (raw.length > 0) {
|
|
610
|
-
if (!isBuildOutput(m.file)) {
|
|
634
|
+
if (!isBuildOutput(m.file) && !isTestPath(m.file)) {
|
|
611
635
|
const k = `${m.file}:${line}:${raw}`;
|
|
612
636
|
if (!seen.has(k)) {
|
|
613
637
|
seen.add(k);
|
|
@@ -750,6 +774,8 @@ var RuleSchema = z.strictObject({
|
|
|
750
774
|
id: z.string().min(1),
|
|
751
775
|
path: z.string().min(1),
|
|
752
776
|
operations: z.array(OperationSchema),
|
|
777
|
+
/** Partes de `allow` não reconhecidas (typo parcial: `get, gett`). */
|
|
778
|
+
unknownOperations: z.array(z.string()).optional(),
|
|
753
779
|
conditionPresent: z.boolean(),
|
|
754
780
|
condition: z.string().optional(),
|
|
755
781
|
authReferences: z.array(z.string()),
|
|
@@ -886,6 +912,14 @@ function validateClaimSamples(samples) {
|
|
|
886
912
|
|
|
887
913
|
// src/scan.ts
|
|
888
914
|
async function scan(rootDir, opts = {}) {
|
|
915
|
+
const { statSync: statRoot } = await import("fs");
|
|
916
|
+
let rootOk = false;
|
|
917
|
+
try {
|
|
918
|
+
rootOk = statRoot(rootDir).isDirectory();
|
|
919
|
+
} catch {
|
|
920
|
+
rootOk = false;
|
|
921
|
+
}
|
|
922
|
+
if (!rootOk) throw new Error(`Diret\xF3rio n\xE3o existe: ${rootDir}`);
|
|
889
923
|
const d = discover(rootDir);
|
|
890
924
|
if (opts.graph === true) {
|
|
891
925
|
try {
|
|
@@ -976,13 +1010,14 @@ async function scan(rootDir, opts = {}) {
|
|
|
976
1010
|
if (d.firestoreRulesFile && existsSync3(d.firestoreRulesFile)) {
|
|
977
1011
|
const rel = model.firebase.firestore.rulesFile ?? "firestore.rules";
|
|
978
1012
|
try {
|
|
1013
|
+
if (tooLarge(d.firestoreRulesFile, 1024 * 1024)) throw new Error("rules acima de 1 MB");
|
|
979
1014
|
model.rules = extractRules(d.firestoreRulesFile, rel);
|
|
980
1015
|
} catch {
|
|
981
1016
|
findings_pre.push({
|
|
982
1017
|
rule: "RULES_UNREADABLE",
|
|
983
1018
|
severity: "WARNING",
|
|
984
1019
|
confidence: "CONFIRMED",
|
|
985
|
-
message: "firestore.rules existe mas n\xE3o p\xF4de ser lido \u2014 FBA001/FBA002 sem cobertura.",
|
|
1020
|
+
message: "firestore.rules existe mas n\xE3o p\xF4de ser lido (ileg\xEDvel ou acima de 1 MB) \u2014 FBA001/FBA002 sem cobertura.",
|
|
986
1021
|
fingerprint: "RULES_UNREADABLE",
|
|
987
1022
|
evidence: [
|
|
988
1023
|
{ id: "ev-rules-unreadable", kind: "rules-read", summary: "leitura falhou", confidence: "CONFIRMED" }
|
|
@@ -992,6 +1027,7 @@ async function scan(rootDir, opts = {}) {
|
|
|
992
1027
|
}
|
|
993
1028
|
if (d.firestoreIndexesFile && existsSync3(d.firestoreIndexesFile)) {
|
|
994
1029
|
try {
|
|
1030
|
+
if (tooLarge(d.firestoreIndexesFile, 1024 * 1024)) throw new Error("indexes acima de 1 MB");
|
|
995
1031
|
const raw = JSON.parse(readFileSync3(d.firestoreIndexesFile, "utf-8"));
|
|
996
1032
|
if (Array.isArray(raw.fieldOverrides) && raw.fieldOverrides.length > 0) {
|
|
997
1033
|
findings_pre.push({
|
|
@@ -1062,7 +1098,7 @@ async function scan(rootDir, opts = {}) {
|
|
|
1062
1098
|
rule: "INDEXES_INVALID",
|
|
1063
1099
|
severity: "WARNING",
|
|
1064
1100
|
confidence: "CONFIRMED",
|
|
1065
|
-
message: "firestore.indexes.json inv\xE1lido \u2014 \xEDndices ignorados neste scan.",
|
|
1101
|
+
message: "firestore.indexes.json inv\xE1lido ou acima de 1 MB \u2014 \xEDndices ignorados neste scan.",
|
|
1066
1102
|
fingerprint: "INDEXES_INVALID",
|
|
1067
1103
|
evidence: [
|
|
1068
1104
|
{ id: "ev-indexes-invalid", kind: "indexes-parse", summary: "JSON inv\xE1lido", confidence: "CONFIRMED" }
|
|
@@ -1072,6 +1108,7 @@ async function scan(rootDir, opts = {}) {
|
|
|
1072
1108
|
}
|
|
1073
1109
|
if (d.auditYamlFile && existsSync3(d.auditYamlFile)) {
|
|
1074
1110
|
try {
|
|
1111
|
+
if (tooLarge(d.auditYamlFile, 256 * 1024)) throw new Error("yaml acima de 256 KB");
|
|
1075
1112
|
const text = readFileSync3(d.auditYamlFile, "utf-8");
|
|
1076
1113
|
const parsed = parseYaml(text);
|
|
1077
1114
|
const validated = AuditYamlSchema.safeParse(parsed);
|
|
@@ -1180,6 +1217,7 @@ async function scan(rootDir, opts = {}) {
|
|
|
1180
1217
|
if (d.storageRulesFile && existsSync3(d.storageRulesFile)) {
|
|
1181
1218
|
checked.push("storage.rules");
|
|
1182
1219
|
try {
|
|
1220
|
+
if (tooLarge(d.storageRulesFile, 1024 * 1024)) throw new Error("storage acima de 1 MB");
|
|
1183
1221
|
const raw = readFileSync3(d.storageRulesFile, "utf-8");
|
|
1184
1222
|
const relStorage = model.firebase.storage?.rulesFile ?? "storage.rules";
|
|
1185
1223
|
const clean = stripRuleComments(raw);
|
|
@@ -1288,13 +1326,13 @@ async function scan(rootDir, opts = {}) {
|
|
|
1288
1326
|
}
|
|
1289
1327
|
}
|
|
1290
1328
|
} catch {
|
|
1291
|
-
skipped.push("storage.rules ileg\xEDvel \u2014 Storage sem cobertura neste scan");
|
|
1329
|
+
skipped.push("storage.rules ileg\xEDvel ou acima de 1 MB \u2014 Storage sem cobertura neste scan");
|
|
1292
1330
|
}
|
|
1293
1331
|
} else {
|
|
1294
1332
|
skipped.push("storage.rules n\xE3o observado \u2014 Storage fora deste scan");
|
|
1295
1333
|
}
|
|
1296
1334
|
if (d.functionsDir) {
|
|
1297
|
-
const appcheck = await countAppCheckEnforced(rootDir);
|
|
1335
|
+
const appcheck = await countAppCheckEnforced(rootDir, d.functionsDir);
|
|
1298
1336
|
model.functions = { dir: model.functions?.dir, appCheckEnforced: appcheck.occurrences };
|
|
1299
1337
|
checked.push("functions-appcheck");
|
|
1300
1338
|
if (appcheck.manual > 0) {
|
|
@@ -1384,12 +1422,19 @@ async function scan(rootDir, opts = {}) {
|
|
|
1384
1422
|
}
|
|
1385
1423
|
};
|
|
1386
1424
|
}
|
|
1387
|
-
|
|
1425
|
+
function tooLarge(abs, maxBytes) {
|
|
1426
|
+
try {
|
|
1427
|
+
return statSync2(abs).size > maxBytes;
|
|
1428
|
+
} catch {
|
|
1429
|
+
return false;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
async function countAppCheckEnforced(rootDir, functionsDir) {
|
|
1388
1433
|
try {
|
|
1389
1434
|
const { readdirSync, readFileSync: readSync, statSync: stat } = await import("fs");
|
|
1390
1435
|
const { join: joinP } = await import("path");
|
|
1391
|
-
const { stripRuleComments: strip } = await import("./rules-
|
|
1392
|
-
const roots = [joinP(rootDir, "functions")];
|
|
1436
|
+
const { stripRuleComments: strip } = await import("./rules-HCXATUST.js");
|
|
1437
|
+
const roots = [functionsDir ?? joinP(rootDir, "functions")];
|
|
1393
1438
|
let occurrences = 0;
|
|
1394
1439
|
const files = /* @__PURE__ */ new Set();
|
|
1395
1440
|
let manual = 0;
|
|
@@ -1397,13 +1442,9 @@ async function countAppCheckEnforced(rootDir) {
|
|
|
1397
1442
|
let limitHit = false;
|
|
1398
1443
|
const stack = [...roots];
|
|
1399
1444
|
let guard = 0;
|
|
1400
|
-
const isTestFile =
|
|
1401
|
-
const n = p.replace(/\\/g, "/");
|
|
1402
|
-
return /\.test\.[tj]sx?$/.test(n) || /\.spec\.[tj]sx?$/.test(n) || /(^|\/)__tests__\//.test(n) || /(^|\/)(test|tests|testing)\//.test(n);
|
|
1403
|
-
};
|
|
1445
|
+
const isTestFile = isTestPath;
|
|
1404
1446
|
while (stack.length > 0 && guard < 200) {
|
|
1405
1447
|
guard += 1;
|
|
1406
|
-
if (guard >= 200 && stack.length > 0) limitHit = true;
|
|
1407
1448
|
const cur = stack.pop();
|
|
1408
1449
|
let entries = [];
|
|
1409
1450
|
try {
|
|
@@ -1450,12 +1491,13 @@ async function countAppCheckEnforced(rootDir) {
|
|
|
1450
1491
|
files.add(abs);
|
|
1451
1492
|
}
|
|
1452
1493
|
}
|
|
1453
|
-
const manualHits = text.match(/verifyToken\s*\(|verify_token\s*\(|consumeAppCheckToken|getLimitedUseToken
|
|
1494
|
+
const manualHits = text.match(/verifyToken\s*\(|verify_token\s*\(|consumeAppCheckToken|getLimitedUseToken/g);
|
|
1454
1495
|
if (manualHits) manual += manualHits.length;
|
|
1455
1496
|
} catch {
|
|
1456
1497
|
}
|
|
1457
1498
|
}
|
|
1458
1499
|
}
|
|
1500
|
+
if (stack.length > 0) limitHit = true;
|
|
1459
1501
|
return { occurrences, files: files.size, manual, ignoredOnRequest, limitHit };
|
|
1460
1502
|
} catch {
|
|
1461
1503
|
return { occurrences: 0, files: 0, manual: 0, ignoredOnRequest: 0, limitHit: false };
|
|
@@ -1522,9 +1564,14 @@ function parseStorageAllows(clean) {
|
|
|
1522
1564
|
const re = /allow\s+([^;:]+?)(?::\s*if\s+([^;]+))?;/gi;
|
|
1523
1565
|
let m;
|
|
1524
1566
|
while ((m = re.exec(clean)) !== null) {
|
|
1525
|
-
const containing = matches.filter((b) => b.start <= m.index && m.index < b.end).sort((a, b) =>
|
|
1567
|
+
const containing = matches.filter((b) => b.start <= m.index && m.index < b.end).sort((a, b) => a.start - b.start);
|
|
1568
|
+
const parts = [];
|
|
1569
|
+
for (const b of containing) {
|
|
1570
|
+
const p = b.path.trim().replace(/^\/+|\/+$/g, "");
|
|
1571
|
+
if (p.length > 0) parts.push(p);
|
|
1572
|
+
}
|
|
1526
1573
|
const line = clean.slice(0, m.index).split("\n").length;
|
|
1527
|
-
out.push({ target: m[1].trim(), condition: m[2]?.trim(), matchPath:
|
|
1574
|
+
out.push({ target: m[1].trim(), condition: m[2]?.trim(), matchPath: parts.length > 0 ? `/${parts.join("/")}` : "/(unknown)", line });
|
|
1528
1575
|
}
|
|
1529
1576
|
return out;
|
|
1530
1577
|
}
|
|
@@ -1550,6 +1597,7 @@ function expandStorageOps(target) {
|
|
|
1550
1597
|
export {
|
|
1551
1598
|
discover,
|
|
1552
1599
|
SERVER_HINTS_SEGMENTS,
|
|
1600
|
+
isTestPath,
|
|
1553
1601
|
classifyOrigin,
|
|
1554
1602
|
emptyModel,
|
|
1555
1603
|
enrichWithGraph,
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
scan
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-NZT4NIVA.js";
|
|
5
|
+
import "./chunk-7J3ADEBW.js";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
8
8
|
import { createRequire } from "module";
|
|
@@ -46,6 +46,9 @@ Uso: firebase-audit scan [--json] [--strict] [--adapter=fn] [--cwd=path]`);
|
|
|
46
46
|
console.error(`Valor inv\xE1lido para --strict: ${strictArg} (use --strict, --strict=true ou --strict=false)`);
|
|
47
47
|
process.exit(1);
|
|
48
48
|
}
|
|
49
|
+
if (cmd === "check" && strictArg === "false") {
|
|
50
|
+
console.error("Aviso: 'check' \xE9 sempre strict por defini\xE7\xE3o \u2014 --strict=false ignorado.");
|
|
51
|
+
}
|
|
49
52
|
const asJsonArg = getArg("json");
|
|
50
53
|
const asJson = (args.includes("--json") || asJsonArg === "true") && asJsonArg !== "false";
|
|
51
54
|
if (asJsonArg !== void 0 && asJsonArg !== "true" && asJsonArg !== "false") {
|
|
@@ -81,8 +84,8 @@ Uso: firebase-audit scan|check|verify|drift [--json] ...`);
|
|
|
81
84
|
const { resolve: resolveRoot } = await import("path");
|
|
82
85
|
const rootDir = cwdArg ? resolveRoot(process.cwd(), cwdArg) : process.cwd();
|
|
83
86
|
if (cmd === "verify") {
|
|
84
|
-
const { scan: scanForVerify } = await import("./scan-
|
|
85
|
-
const { verify } = await import("./verify-
|
|
87
|
+
const { scan: scanForVerify } = await import("./scan-RTIMO3I2.js");
|
|
88
|
+
const { verify } = await import("./verify-GQSR4FQL.js");
|
|
86
89
|
const { resolve: resolveVerify } = await import("path");
|
|
87
90
|
const coverageArg = getArg("coverage");
|
|
88
91
|
const coveragePath = coverageArg ? resolveVerify(rootDir, coverageArg) : void 0;
|
|
@@ -116,7 +119,7 @@ ${icon} ${f.rule} [${f.confidence}]
|
|
|
116
119
|
return;
|
|
117
120
|
}
|
|
118
121
|
if (cmd === "drift") {
|
|
119
|
-
const { drift } = await import("./drift-
|
|
122
|
+
const { drift } = await import("./drift-C3JZI5ZA.js");
|
|
120
123
|
const { resolve: resolveDrift } = await import("path");
|
|
121
124
|
const { existsSync: existsDrift } = await import("fs");
|
|
122
125
|
const localArg = getArg("local");
|
|
@@ -130,7 +133,7 @@ ${icon} ${f.rule} [${f.confidence}]
|
|
|
130
133
|
for (const [label, p] of [["--local", localPath], ["--remote", remotePath]]) {
|
|
131
134
|
if (p && !existsDrift(p)) {
|
|
132
135
|
if (asJson) {
|
|
133
|
-
const { drift: driftMissing } = await import("./drift-
|
|
136
|
+
const { drift: driftMissing } = await import("./drift-C3JZI5ZA.js");
|
|
134
137
|
const res2 = driftMissing(
|
|
135
138
|
localPath && existsDrift(localPath) ? localPath : null,
|
|
136
139
|
remotePath && existsDrift(remotePath) ? remotePath : null
|
package/dist/index.d.ts
CHANGED
|
@@ -217,6 +217,7 @@ declare const RuleSchema: z.ZodObject<{
|
|
|
217
217
|
delete: "delete";
|
|
218
218
|
write: "write";
|
|
219
219
|
}>>;
|
|
220
|
+
unknownOperations: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
220
221
|
conditionPresent: z.ZodBoolean;
|
|
221
222
|
condition: z.ZodOptional<z.ZodString>;
|
|
222
223
|
authReferences: z.ZodArray<z.ZodString>;
|
|
@@ -419,6 +420,7 @@ declare const ProjectModelSchema: z.ZodObject<{
|
|
|
419
420
|
delete: "delete";
|
|
420
421
|
write: "write";
|
|
421
422
|
}>>;
|
|
423
|
+
unknownOperations: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
422
424
|
conditionPresent: z.ZodBoolean;
|
|
423
425
|
condition: z.ZodOptional<z.ZodString>;
|
|
424
426
|
authReferences: z.ZodArray<z.ZodString>;
|
|
@@ -639,6 +641,8 @@ interface DiscoveryResult {
|
|
|
639
641
|
declare function discover(rootDir: string): DiscoveryResult;
|
|
640
642
|
/** Segmentos de servidor (fail-closed: `api` genérico NÃO é server, ver classifyOrigin). */
|
|
641
643
|
declare const SERVER_HINTS_SEGMENTS: string[];
|
|
644
|
+
/** Arquivo de teste/setup de Emulator (com Admin legitimamente): nunca reprova CI. */
|
|
645
|
+
declare function isTestPath(file: string): boolean;
|
|
642
646
|
/** Heurística CLIENT vs SERVER: caminho manda primeiro (grafo ai-tool preenche listas na Fase 3).
|
|
643
647
|
* Segmentos ancorados — nunca substring solta (`myapp/` não é `app/`). */
|
|
644
648
|
declare function classifyOrigin(file: string, content: string): "CLIENT" | "SERVER" | "UNKNOWN";
|
|
@@ -665,6 +669,7 @@ declare function stripRuleComments(content: string): string;
|
|
|
665
669
|
declare function opsFrom(target: string): {
|
|
666
670
|
ops: Rule["operations"];
|
|
667
671
|
known: boolean;
|
|
672
|
+
unknown: string[];
|
|
668
673
|
};
|
|
669
674
|
/** Normaliza target de allow para fingerprint estável (case/espaço/ordem). */
|
|
670
675
|
declare function normalizeTarget(target: string): string;
|
|
@@ -763,7 +768,9 @@ interface ScanSummary {
|
|
|
763
768
|
errors: number;
|
|
764
769
|
warnings: number;
|
|
765
770
|
infos: number;
|
|
771
|
+
/** Checks FBA que passaram (só FBA; infra nomeada vai em `infra`). */
|
|
766
772
|
passedChecks: number;
|
|
773
|
+
/** Total de checks FBA implementados. */
|
|
767
774
|
totalChecks: number;
|
|
768
775
|
/** Infra nomeada (fora dos checks FBA): cada item diz qual e por quê. */
|
|
769
776
|
infra: {
|
|
@@ -845,4 +852,4 @@ declare function drift(localFile: string | null, remoteFile: string | null): Dri
|
|
|
845
852
|
|
|
846
853
|
declare const VERSION: string;
|
|
847
854
|
|
|
848
|
-
export { AccessOriginSchema, type AuditYaml, AuditYamlSchema, ConfidenceSchema, type DiscoveryResult, type DriftEntry, type DriftResult, type DriftStatus, DynamicValueSchema, type Evidence, EvidenceSchema, type Finding, FindingSchema, FirestoreIndexSchema, GraphEdgeSchema, IMPLEMENTED_CHECKS, IndexFieldSchema, LocationSchema, OperationSchema, PermissionSchema, PositionSchema, type ProjectModel, ProjectModelSchema, QueryFilterSchema, QueryOrderSchema, type QueryShape, QueryShapeSchema, RESERVED_CLAIM_KEYS, RoleSchema, RuleSchema, SERVER_HINTS_SEGMENTS, type ScanOptions, type ScanResult, type ScanSummary, SeveritySchema, VERSION, type VerifyCase, type VerifyOptions, type VerifyResult, buildFullPath, classifyOrigin, conditionKeyForFingerprint, discover, drift, emptyModel, enrichWithGraph, extractRules, extractRulesContent, findPublicAllows, findPublicAllowsContent, hashShort, helperBodyToCondition, inlineHelperArgs, inlineHelpersInCondition, isPublicCondition, normalizeTarget, opsFrom, parseMatchBlocks, parseRuleFunctions, parseRuleFunctionsDetailed, planVerify, resolveHelperCondition, resolveHelperConditionDetailed, runChecks, scan, stripOuterParens, stripRuleComments, validateClaimSamples, verify };
|
|
855
|
+
export { AccessOriginSchema, type AuditYaml, AuditYamlSchema, ConfidenceSchema, type DiscoveryResult, type DriftEntry, type DriftResult, type DriftStatus, DynamicValueSchema, type Evidence, EvidenceSchema, type Finding, FindingSchema, FirestoreIndexSchema, GraphEdgeSchema, IMPLEMENTED_CHECKS, IndexFieldSchema, LocationSchema, OperationSchema, PermissionSchema, PositionSchema, type ProjectModel, ProjectModelSchema, QueryFilterSchema, QueryOrderSchema, type QueryShape, QueryShapeSchema, RESERVED_CLAIM_KEYS, RoleSchema, RuleSchema, SERVER_HINTS_SEGMENTS, type ScanOptions, type ScanResult, type ScanSummary, SeveritySchema, VERSION, type VerifyCase, type VerifyOptions, type VerifyResult, buildFullPath, classifyOrigin, conditionKeyForFingerprint, discover, drift, emptyModel, enrichWithGraph, extractRules, extractRulesContent, findPublicAllows, findPublicAllowsContent, hashShort, helperBodyToCondition, inlineHelperArgs, inlineHelpersInCondition, isPublicCondition, isTestPath, normalizeTarget, opsFrom, parseMatchBlocks, parseRuleFunctions, parseRuleFunctionsDetailed, planVerify, resolveHelperCondition, resolveHelperConditionDetailed, runChecks, scan, stripOuterParens, stripRuleComments, validateClaimSamples, verify };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpServer,
|
|
3
3
|
startMcpServer
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-D75SRJ7P.js";
|
|
5
5
|
import {
|
|
6
6
|
AccessOriginSchema,
|
|
7
7
|
AuditYamlSchema,
|
|
@@ -30,10 +30,11 @@ import {
|
|
|
30
30
|
discover,
|
|
31
31
|
emptyModel,
|
|
32
32
|
enrichWithGraph,
|
|
33
|
+
isTestPath,
|
|
33
34
|
runChecks,
|
|
34
35
|
scan,
|
|
35
36
|
validateClaimSamples
|
|
36
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-NZT4NIVA.js";
|
|
37
38
|
import {
|
|
38
39
|
buildFullPath,
|
|
39
40
|
conditionKeyForFingerprint,
|
|
@@ -55,14 +56,14 @@ import {
|
|
|
55
56
|
resolveHelperConditionDetailed,
|
|
56
57
|
stripOuterParens,
|
|
57
58
|
stripRuleComments
|
|
58
|
-
} from "./chunk-
|
|
59
|
+
} from "./chunk-7J3ADEBW.js";
|
|
59
60
|
import {
|
|
60
61
|
planVerify,
|
|
61
62
|
verify
|
|
62
|
-
} from "./chunk-
|
|
63
|
+
} from "./chunk-CSMT4PIP.js";
|
|
63
64
|
import {
|
|
64
65
|
drift
|
|
65
|
-
} from "./chunk-
|
|
66
|
+
} from "./chunk-LD3F73XI.js";
|
|
66
67
|
|
|
67
68
|
// src/index.ts
|
|
68
69
|
import { createRequire } from "module";
|
|
@@ -111,6 +112,7 @@ export {
|
|
|
111
112
|
inlineHelperArgs,
|
|
112
113
|
inlineHelpersInCondition,
|
|
113
114
|
isPublicCondition,
|
|
115
|
+
isTestPath,
|
|
114
116
|
normalizeTarget,
|
|
115
117
|
opsFrom,
|
|
116
118
|
parseMatchBlocks,
|
package/dist/mcp-cli.js
CHANGED
|
@@ -1,11 +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-
|
|
8
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-D75SRJ7P.js";
|
|
5
|
+
import "./chunk-NZT4NIVA.js";
|
|
6
|
+
import "./chunk-7J3ADEBW.js";
|
|
7
|
+
import "./chunk-CSMT4PIP.js";
|
|
8
|
+
import "./chunk-LD3F73XI.js";
|
|
9
9
|
|
|
10
10
|
// src/mcp-cli.ts
|
|
11
11
|
startMcpServer().catch((err) => {
|
package/dist/mcp.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpServer,
|
|
3
3
|
startMcpServer
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-D75SRJ7P.js";
|
|
5
|
+
import "./chunk-NZT4NIVA.js";
|
|
6
|
+
import "./chunk-7J3ADEBW.js";
|
|
7
|
+
import "./chunk-CSMT4PIP.js";
|
|
8
|
+
import "./chunk-LD3F73XI.js";
|
|
9
9
|
export {
|
|
10
10
|
createMcpServer,
|
|
11
11
|
startMcpServer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justmpm/firebase-audit",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
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
|
@@ -34,6 +34,6 @@ Use `drift` para comparar índices locais vs implantados (MATCHED/LOCAL_ONLY/REM
|
|
|
34
34
|
|
|
35
35
|
## Limites honestos
|
|
36
36
|
|
|
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.
|
|
37
|
+
- AppCheck: varredura TS/Python por texto (`enforceAppCheck`, `verifyToken` e afins). Config via variável ou objeto compartilhado conta como não observada; janela de trigger pode errar em arquivo multi-função — `onRequest` exige revisão rota a rota.
|
|
38
38
|
- Isolamento por tenant (ownerId/tenantId) e mismatch de papel não-admin: fora dos checks — revise manualmente ou no Emulator.
|
|
39
39
|
- `graph: true` é prévia opt-in da biblioteca (nenhum check consome ainda).
|