@justmpm/firebase-audit 0.5.2 → 0.5.3
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-UKEKA2NQ.js → chunk-4BBFPTOR.js} +92 -43
- package/dist/{chunk-AXTYJMA7.js → chunk-PWJ4DI24.js} +16 -2
- package/dist/cli.js +9 -4
- package/dist/index.js +2 -2
- package/dist/mcp-cli.js +2 -2
- package/dist/mcp.js +2 -2
- package/dist/{scan-OEN65GHM.js → scan-LJ3TCHMR.js} +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +7 -1
|
@@ -844,9 +844,10 @@ async function scan(rootDir, opts = {}) {
|
|
|
844
844
|
try {
|
|
845
845
|
if (d.firebaseJson && existsSync3(d.firebaseJson)) {
|
|
846
846
|
const raw = JSON.parse(readFileSync3(d.firebaseJson, "utf-8"));
|
|
847
|
-
for (const [kind, rel] of [
|
|
848
|
-
["rules", raw.firestore?.rules],
|
|
849
|
-
["indexes", raw.firestore?.indexes]
|
|
847
|
+
for (const [kind, rel, label] of [
|
|
848
|
+
["rules", raw.firestore?.rules, "firestore.rules"],
|
|
849
|
+
["indexes", raw.firestore?.indexes, "firestore.indexes"],
|
|
850
|
+
["storage-rules", raw.storage?.rules, "storage.rules"]
|
|
850
851
|
]) {
|
|
851
852
|
if (typeof rel === "string" && rel.length > 0) {
|
|
852
853
|
if (!existsSync3(join3(rootDir, rel))) {
|
|
@@ -854,7 +855,7 @@ async function scan(rootDir, opts = {}) {
|
|
|
854
855
|
rule: "RULES_CONFIG_MISMATCH",
|
|
855
856
|
severity: "WARNING",
|
|
856
857
|
confidence: "CONFIRMED",
|
|
857
|
-
message: `firebase.json aponta
|
|
858
|
+
message: `firebase.json aponta ${label} para "${rel}" inexistente \u2014 usando default. Corrija o path.`,
|
|
858
859
|
fingerprint: `RULES_CONFIG_MISMATCH:${kind}:${rel}`,
|
|
859
860
|
evidence: [
|
|
860
861
|
{ id: `ev-config-${kind}`, kind: "config-mismatch", summary: rel, confidence: "CONFIRMED" }
|
|
@@ -1078,8 +1079,21 @@ async function scan(rootDir, opts = {}) {
|
|
|
1078
1079
|
}
|
|
1079
1080
|
const checksFindings = await runChecks(model, rootDir, { adapterFn: opts.adapterFn ?? model.authorization.adapter ?? void 0 });
|
|
1080
1081
|
const findings = [...findings_pre, ...checksFindings];
|
|
1081
|
-
const checked = [
|
|
1082
|
+
const checked = [];
|
|
1083
|
+
if (d.firestoreRulesFile && existsSync3(d.firestoreRulesFile)) checked.push("firestore.rules");
|
|
1084
|
+
if (d.firestoreIndexesFile && existsSync3(d.firestoreIndexesFile)) checked.push("indexes");
|
|
1085
|
+
if (d.auditYamlFile && existsSync3(d.auditYamlFile)) checked.push("contract");
|
|
1086
|
+
checked.push("client-code");
|
|
1082
1087
|
const skipped = [];
|
|
1088
|
+
if (!d.firestoreRulesFile || !existsSync3(d.firestoreRulesFile)) {
|
|
1089
|
+
skipped.push("firestore.rules n\xE3o observado \u2014 FBA001/FBA002 sem cobertura");
|
|
1090
|
+
}
|
|
1091
|
+
if (!d.firestoreIndexesFile || !existsSync3(d.firestoreIndexesFile)) {
|
|
1092
|
+
skipped.push("firestore.indexes.json n\xE3o observado \u2014 drift de \xEDndices sem base local");
|
|
1093
|
+
}
|
|
1094
|
+
if (!d.auditYamlFile || !existsSync3(d.auditYamlFile)) {
|
|
1095
|
+
skipped.push("firebase-audit.yaml n\xE3o observado \u2014 permiss\xF5es seguem UNKNOWN sem contrato");
|
|
1096
|
+
}
|
|
1083
1097
|
if (d.storageRulesFile && existsSync3(d.storageRulesFile)) {
|
|
1084
1098
|
checked.push("storage.rules");
|
|
1085
1099
|
try {
|
|
@@ -1092,48 +1106,83 @@ async function scan(rootDir, opts = {}) {
|
|
|
1092
1106
|
return `ev-storage-${prefix}-${evCounter}`;
|
|
1093
1107
|
};
|
|
1094
1108
|
for (const m of parseStorageAllows(clean)) {
|
|
1095
|
-
if (!isOpenStorageCondition(m.condition)) continue;
|
|
1096
1109
|
const ops = expandStorageOps(m.target);
|
|
1097
1110
|
const key = conditionKeyForFingerprint(m.condition);
|
|
1098
|
-
const
|
|
1111
|
+
const normCond = (m.condition ?? "").toLowerCase().replace(/\s+/g, "");
|
|
1112
|
+
const hasAuth = normCond.includes("request.auth");
|
|
1113
|
+
const bareAuth = (/request\.auth(!==|!=)null/.test(normCond) || /request\.auth\.uid(!==|!=)null/.test(normCond)) && !normCond.includes("request.auth.token") && !normCond.includes("sign_in_provider");
|
|
1114
|
+
const wildcard = /\{\s*\w+\s*=\s*\*\*\s*\}/.test(m.matchPath);
|
|
1099
1115
|
const where = `${relStorage}:${m.line} (${m.matchPath}, allow ${m.target})`;
|
|
1100
|
-
if (
|
|
1101
|
-
|
|
1102
|
-
rule: "STORAGE_PUBLIC_WRITE",
|
|
1103
|
-
severity: "ERROR",
|
|
1104
|
-
confidence: "CONFIRMED",
|
|
1105
|
-
message: `Storage com escrita aberta (${where}). Qualquer cliente pode escrever \u2014 cofre aberto, n\xE3o vitrine.`,
|
|
1106
|
-
fingerprint: `STORAGE_PUBLIC_WRITE:${m.matchPath}:${m.target}:${key}`,
|
|
1107
|
-
file: relStorage,
|
|
1108
|
-
line: m.line,
|
|
1109
|
-
evidence: [{ id: evId("write"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "CONFIRMED", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
|
|
1110
|
-
fix: "Exigir request.auth (e dono/tenant) no write. Deixe get aberto s\xF3 em pasta de vitrine como logos/."
|
|
1111
|
-
});
|
|
1112
|
-
} else if (ops.has("read") || ops.has("get") || ops.has("list")) {
|
|
1113
|
-
if (wildcard) {
|
|
1114
|
-
findings.push({
|
|
1115
|
-
rule: "STORAGE_ALL_PATHS_OPEN",
|
|
1116
|
-
severity: "WARNING",
|
|
1117
|
-
confidence: "PROBABLE",
|
|
1118
|
-
message: `Storage com leitura aberta em curinga total (${where}). Vale para tudo \u2014 se a inten\xE7\xE3o era s\xF3 logo, restrinja a /logos/{id}.`,
|
|
1119
|
-
fingerprint: `STORAGE_ALL_PATHS_OPEN:${m.matchPath}:${m.target}:${key}`,
|
|
1120
|
-
file: relStorage,
|
|
1121
|
-
line: m.line,
|
|
1122
|
-
evidence: [{ id: evId("all"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
|
|
1123
|
-
fix: "Troque o curinga total por pastas expl\xEDcitas (ex: /logos/{logoId}) com get: if true e write restrito."
|
|
1124
|
-
});
|
|
1125
|
-
} else {
|
|
1116
|
+
if (isOpenStorageCondition(m.condition)) {
|
|
1117
|
+
if (ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete")) {
|
|
1126
1118
|
findings.push({
|
|
1127
|
-
rule: "
|
|
1128
|
-
severity: "
|
|
1129
|
-
confidence: "
|
|
1130
|
-
message: `Storage com
|
|
1131
|
-
fingerprint: `
|
|
1119
|
+
rule: "STORAGE_PUBLIC_WRITE",
|
|
1120
|
+
severity: "ERROR",
|
|
1121
|
+
confidence: "CONFIRMED",
|
|
1122
|
+
message: `Storage com escrita aberta (${where}). Qualquer cliente pode escrever \u2014 cofre aberto, n\xE3o vitrine.`,
|
|
1123
|
+
fingerprint: `STORAGE_PUBLIC_WRITE:${m.matchPath}:${m.target}:${key}`,
|
|
1132
1124
|
file: relStorage,
|
|
1133
1125
|
line: m.line,
|
|
1134
|
-
evidence: [{ id: evId("
|
|
1126
|
+
evidence: [{ id: evId("write"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "CONFIRMED", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
|
|
1127
|
+
fix: "Exigir request.auth (e dono/tenant) no write. Deixe get aberto s\xF3 em pasta de vitrine como logos/."
|
|
1135
1128
|
});
|
|
1129
|
+
continue;
|
|
1136
1130
|
}
|
|
1131
|
+
if (ops.has("read") || ops.has("get") || ops.has("list")) {
|
|
1132
|
+
if (wildcard) {
|
|
1133
|
+
findings.push({
|
|
1134
|
+
rule: "STORAGE_ALL_PATHS_OPEN",
|
|
1135
|
+
severity: "WARNING",
|
|
1136
|
+
confidence: "PROBABLE",
|
|
1137
|
+
message: `Storage com leitura aberta em curinga total (${where}). Vale para tudo \u2014 se a inten\xE7\xE3o era s\xF3 logo, restrinja a /logos/{id}.`,
|
|
1138
|
+
fingerprint: `STORAGE_ALL_PATHS_OPEN:${m.matchPath}:${m.target}:${key}`,
|
|
1139
|
+
file: relStorage,
|
|
1140
|
+
line: m.line,
|
|
1141
|
+
evidence: [{ id: evId("all"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
|
|
1142
|
+
fix: "Troque o curinga total por pastas expl\xEDcitas (ex: /logos/{logoId}) com get: if true e write restrito."
|
|
1143
|
+
});
|
|
1144
|
+
} else {
|
|
1145
|
+
const enumerates = ops.has("read") || ops.has("list");
|
|
1146
|
+
findings.push({
|
|
1147
|
+
rule: "STORAGE_PUBLIC_READ",
|
|
1148
|
+
severity: "INFO",
|
|
1149
|
+
confidence: "PROBABLE",
|
|
1150
|
+
message: enumerates ? `Storage com leitura aberta (${where}). Pode ser vitrine proposital \u2014 mas allow read/list permite enumerar a pasta; para vitrine segura prefira allow get. Confirme que o write segue restrito.` : `Storage com leitura aberta (${where}). Pode ser vitrine proposital (logos) \u2014 confirme que o write segue restrito.`,
|
|
1151
|
+
fingerprint: `STORAGE_PUBLIC_READ:${m.matchPath}:${m.target}:${key}`,
|
|
1152
|
+
file: relStorage,
|
|
1153
|
+
line: m.line,
|
|
1154
|
+
evidence: [{ id: evId("read"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target}`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
|
|
1155
|
+
fix: enumerates ? "Para vitrine, troque allow read por allow get (sem list) na pasta p\xFAblica." : void 0
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (bareAuth && (ops.has("read") || ops.has("get") || ops.has("list") || ops.has("write") || ops.has("create") || ops.has("update") || ops.has("delete"))) {
|
|
1162
|
+
findings.push({
|
|
1163
|
+
rule: "STORAGE_ANON_ALLOWED",
|
|
1164
|
+
severity: "INFO",
|
|
1165
|
+
confidence: "PROBABLE",
|
|
1166
|
+
message: `Storage aceita qualquer autenticado incluindo an\xF4nimo (${where}). Exija claim/provider para dados sens\xEDveis.`,
|
|
1167
|
+
fingerprint: `STORAGE_ANON_ALLOWED:${m.matchPath}:${m.target}:${key}`,
|
|
1168
|
+
file: relStorage,
|
|
1169
|
+
line: m.line,
|
|
1170
|
+
evidence: [{ id: evId("anon"), kind: "anon-allowed", summary: `${m.matchPath} allow ${m.target}`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }],
|
|
1171
|
+
fix: "Exigir claim (token.admin/role) ou provider diferente de anonymous para dados sens\xEDveis."
|
|
1172
|
+
});
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
if (!hasAuth && m.condition !== void 0 && (ops.has("read") || ops.has("get") || ops.has("list"))) {
|
|
1176
|
+
findings.push({
|
|
1177
|
+
rule: "STORAGE_PUBLIC_READ",
|
|
1178
|
+
severity: "INFO",
|
|
1179
|
+
confidence: "PROBABLE",
|
|
1180
|
+
message: `Storage com leitura condicionada sem request.auth (${where}). Se a condi\xE7\xE3o for verdadeira para an\xF4nimo, ele l\xEA \u2014 confirme que \xE9 vitrine.`,
|
|
1181
|
+
fingerprint: `STORAGE_PUBLIC_READ:${m.matchPath}:${m.target}:${key}`,
|
|
1182
|
+
file: relStorage,
|
|
1183
|
+
line: m.line,
|
|
1184
|
+
evidence: [{ id: evId("read"), kind: "storage-public", summary: `${m.matchPath} allow ${m.target} sem auth`, confidence: "PROBABLE", location: { file: relStorage, start: { line: m.line, column: 0 } } }]
|
|
1185
|
+
});
|
|
1137
1186
|
}
|
|
1138
1187
|
}
|
|
1139
1188
|
} catch {
|
|
@@ -1166,7 +1215,7 @@ async function scan(rootDir, opts = {}) {
|
|
|
1166
1215
|
evidence: [{ id: "ev-appcheck", kind: "appcheck", summary: `${appcheck.occurrences} ocorr\xEAncias`, confidence: "CONFIRMED" }]
|
|
1167
1216
|
});
|
|
1168
1217
|
} else if (appcheck.manual === 0) {
|
|
1169
|
-
skipped.push("functions sem enforceAppCheck nem verifyToken observado \u2014 AppCheck sem cobertura afirmativa (onRequest exige verifica\xE7\xE3o manual por rota)");
|
|
1218
|
+
skipped.push("functions sem enforceAppCheck literal nem verifyToken observado \u2014 AppCheck sem cobertura afirmativa (config via vari\xE1vel conta como n\xE3o observada; onRequest exige verifica\xE7\xE3o manual por rota)");
|
|
1170
1219
|
}
|
|
1171
1220
|
if (appcheck.limitHit) {
|
|
1172
1221
|
skipped.push("functions: teto de varredura atingido \u2014 AppCheck parcial neste scan");
|
|
@@ -1270,16 +1319,16 @@ async function countAppCheckEnforced(rootDir) {
|
|
|
1270
1319
|
stack.push(abs);
|
|
1271
1320
|
continue;
|
|
1272
1321
|
}
|
|
1273
|
-
if (!/\.(ts|js|mjs|cjs)$/.test(name)) continue;
|
|
1322
|
+
if (!/\.(ts|js|mjs|cjs|py)$/.test(name)) continue;
|
|
1274
1323
|
if (isTestFile(abs)) continue;
|
|
1275
1324
|
try {
|
|
1276
1325
|
const text = strip(readSync(abs, "utf-8"));
|
|
1277
|
-
const hits = text.match(/enforceAppCheck\s*:\s*true/g);
|
|
1326
|
+
const hits = text.match(/enforceAppCheck\s*:\s*true|enforce_app_check\s*=\s*True/g);
|
|
1278
1327
|
if (hits) {
|
|
1279
1328
|
occurrences += hits.length;
|
|
1280
1329
|
files.add(abs);
|
|
1281
1330
|
}
|
|
1282
|
-
const manualHits = text.match(/verifyToken\s*\(/g);
|
|
1331
|
+
const manualHits = text.match(/verifyToken\s*\(|verify_token\s*\(|consumeAppCheckToken|getLimitedUseToken|getAppCheck\s*\(/g);
|
|
1283
1332
|
if (manualHits) manual += manualHits.length;
|
|
1284
1333
|
} catch {
|
|
1285
1334
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
scan
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-4BBFPTOR.js";
|
|
4
4
|
import {
|
|
5
5
|
verify
|
|
6
6
|
} from "./chunk-Y4VXWSKX.js";
|
|
@@ -28,6 +28,16 @@ function readPkgText(rel) {
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
function assertCwd(cwd) {
|
|
32
|
+
const { existsSync: existsSync2, statSync } = require2("node:fs");
|
|
33
|
+
let ok = false;
|
|
34
|
+
try {
|
|
35
|
+
ok = existsSync2(cwd) && statSync(cwd).isDirectory();
|
|
36
|
+
} catch {
|
|
37
|
+
ok = false;
|
|
38
|
+
}
|
|
39
|
+
if (!ok) throw new Error(`Diret\xF3rio n\xE3o existe: ${cwd}`);
|
|
40
|
+
}
|
|
31
41
|
var FALLBACK_SKILL = [
|
|
32
42
|
"# firebase-audit-skill (fallback)",
|
|
33
43
|
"",
|
|
@@ -79,6 +89,7 @@ function createMcpServer() {
|
|
|
79
89
|
},
|
|
80
90
|
async ({ cwd, adapter, strict }) => {
|
|
81
91
|
try {
|
|
92
|
+
assertCwd(cwd);
|
|
82
93
|
const res = await scan(cwd, { adapterFn: adapter, strict });
|
|
83
94
|
return {
|
|
84
95
|
content: [
|
|
@@ -108,6 +119,7 @@ function createMcpServer() {
|
|
|
108
119
|
},
|
|
109
120
|
async ({ cwd, adapter }) => {
|
|
110
121
|
try {
|
|
122
|
+
assertCwd(cwd);
|
|
111
123
|
const res = await scan(cwd, { adapterFn: adapter, strict: true });
|
|
112
124
|
return {
|
|
113
125
|
content: [
|
|
@@ -115,7 +127,8 @@ function createMcpServer() {
|
|
|
115
127
|
type: "text",
|
|
116
128
|
text: JSON.stringify({ summary: res.summary, findings: res.findings }, null, 2)
|
|
117
129
|
}
|
|
118
|
-
]
|
|
130
|
+
],
|
|
131
|
+
isError: res.summary.errors > 0
|
|
119
132
|
};
|
|
120
133
|
} catch (err) {
|
|
121
134
|
return {
|
|
@@ -137,6 +150,7 @@ function createMcpServer() {
|
|
|
137
150
|
},
|
|
138
151
|
async ({ cwd, coverage }) => {
|
|
139
152
|
try {
|
|
153
|
+
assertCwd(cwd);
|
|
140
154
|
const { resolve } = await import("path");
|
|
141
155
|
const { model } = await scan(cwd);
|
|
142
156
|
const res = verify(model, { coverageFile: coverage ? resolve(cwd, coverage) : void 0 });
|
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-4BBFPTOR.js";
|
|
5
5
|
import "./chunk-7EWTBIKJ.js";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
@@ -32,8 +32,6 @@ Exit codes: 0 ok informativo; 2 quando --strict/check encontra ERROR. scan sozin
|
|
|
32
32
|
Uso: firebase-audit scan [--json] [--strict] [--adapter=fn] [--cwd=path]`);
|
|
33
33
|
process.exit(1);
|
|
34
34
|
}
|
|
35
|
-
const strict = args.includes("--strict") || cmd === "check";
|
|
36
|
-
const asJson = args.includes("--json") || args.includes("--json=true");
|
|
37
35
|
const getArg = (name) => {
|
|
38
36
|
const pref = `--${name}=`;
|
|
39
37
|
const hit = args.find((a) => a.startsWith(pref));
|
|
@@ -42,6 +40,13 @@ Uso: firebase-audit scan [--json] [--strict] [--adapter=fn] [--cwd=path]`);
|
|
|
42
40
|
if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith("--")) return args[idx + 1];
|
|
43
41
|
return void 0;
|
|
44
42
|
};
|
|
43
|
+
const strictArg = getArg("strict");
|
|
44
|
+
const strict = args.includes("--strict") || strictArg === "true" || cmd === "check";
|
|
45
|
+
if (strictArg !== void 0 && strictArg !== "true" && strictArg !== "false") {
|
|
46
|
+
console.error(`Valor inv\xE1lido para --strict: ${strictArg} (use --strict, --strict=true ou --strict=false)`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const asJson = args.includes("--json") || args.includes("--json=true");
|
|
45
50
|
const knownFlags = /* @__PURE__ */ new Set(["--json", "--strict", "--help", "-h", "help", "--version", "-v", "--coverage", "--local", "--remote"]);
|
|
46
51
|
for (const a of args.slice(1)) {
|
|
47
52
|
const base = a.includes("=") ? a.slice(0, a.indexOf("=")) : a;
|
|
@@ -65,7 +70,7 @@ Uso: firebase-audit scan|check|verify|drift [--json] ...`);
|
|
|
65
70
|
const { resolve: resolveRoot } = await import("path");
|
|
66
71
|
const rootDir = cwdArg ? resolveRoot(process.cwd(), cwdArg) : process.cwd();
|
|
67
72
|
if (cmd === "verify") {
|
|
68
|
-
const { scan: scanForVerify } = await import("./scan-
|
|
73
|
+
const { scan: scanForVerify } = await import("./scan-LJ3TCHMR.js");
|
|
69
74
|
const { verify } = await import("./verify-WBVSESHD.js");
|
|
70
75
|
const { resolve: resolveVerify } = await import("path");
|
|
71
76
|
const coverageArg = getArg("coverage");
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpServer,
|
|
3
3
|
startMcpServer
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-PWJ4DI24.js";
|
|
5
5
|
import {
|
|
6
6
|
AccessOriginSchema,
|
|
7
7
|
AuditYamlSchema,
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
runChecks,
|
|
34
34
|
scan,
|
|
35
35
|
validateClaimSamples
|
|
36
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-4BBFPTOR.js";
|
|
37
37
|
import {
|
|
38
38
|
buildFullPath,
|
|
39
39
|
conditionKeyForFingerprint,
|
package/dist/mcp-cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
startMcpServer
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-PWJ4DI24.js";
|
|
5
|
+
import "./chunk-4BBFPTOR.js";
|
|
6
6
|
import "./chunk-7EWTBIKJ.js";
|
|
7
7
|
import "./chunk-Y4VXWSKX.js";
|
|
8
8
|
import "./chunk-XZ32YQQC.js";
|
package/dist/mcp.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMcpServer,
|
|
3
3
|
startMcpServer
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-PWJ4DI24.js";
|
|
5
|
+
import "./chunk-4BBFPTOR.js";
|
|
6
6
|
import "./chunk-7EWTBIKJ.js";
|
|
7
7
|
import "./chunk-Y4VXWSKX.js";
|
|
8
8
|
import "./chunk-XZ32YQQC.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justmpm/firebase-audit",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
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
|
@@ -28,5 +28,11 @@ Skill para agentes usarem o `@justmpm/firebase-audit` do jeito certo.
|
|
|
28
28
|
firebase emulators:exec --only firestore "npm test"
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Matriz `
|
|
31
|
+
Matriz papel×operação×path (`read` = `get` + `list`) com `@firebase/rules-unit-testing`.
|
|
32
32
|
Cobertura em `ruleCoverage` (JSON). Emulator não prova índice. Sem contrato, papéis reduzidos (anonymous/user/admin).
|
|
33
|
+
Use `drift` para comparar índices locais vs implantados (MATCHED/LOCAL_ONLY/REMOTE_ONLY).
|
|
34
|
+
|
|
35
|
+
## Limites honestos
|
|
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.
|
|
38
|
+
- Isolamento por tenant (ownerId/tenantId): fora dos checks — revise manualmente ou no Emulator.
|