@justmpm/firebase-audit 0.3.1 → 0.4.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-UMQH7UDI.js → chunk-3SEVYYCX.js} +457 -57
- package/dist/chunk-GOLWQBD5.js +165 -0
- package/dist/{chunk-GUSNXUAT.js → chunk-NJBCSW7E.js} +18 -1
- package/dist/{chunk-CPEIPRCF.js → chunk-PSSMYRRU.js} +49 -10
- package/dist/cli.js +16 -6
- package/dist/{drift-BEXSL2IX.js → drift-KU7HWL7M.js} +1 -1
- package/dist/index.d.ts +102 -11
- package/dist/index.js +46 -4
- package/dist/mcp-cli.d.ts +1 -0
- package/dist/mcp-cli.js +13 -0
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +11 -0
- package/dist/{scan-TLHI777J.js → scan-VREY2FGO.js} +1 -1
- package/dist/{verify-Y7BWAFPL.js → verify-O722ZZBV.js} +1 -1
- package/package.json +5 -4
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {
|
|
2
|
+
scan
|
|
3
|
+
} from "./chunk-3SEVYYCX.js";
|
|
4
|
+
import {
|
|
5
|
+
verify
|
|
6
|
+
} from "./chunk-PSSMYRRU.js";
|
|
7
|
+
import {
|
|
8
|
+
drift
|
|
9
|
+
} from "./chunk-NJBCSW7E.js";
|
|
10
|
+
|
|
11
|
+
// src/mcp.ts
|
|
12
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import { createRequire } from "module";
|
|
16
|
+
var require2 = createRequire(import.meta.url);
|
|
17
|
+
var pkg = require2("../package.json");
|
|
18
|
+
function createMcpServer() {
|
|
19
|
+
const server = new McpServer({
|
|
20
|
+
name: "firebase-audit-mcp-server",
|
|
21
|
+
version: pkg.version
|
|
22
|
+
});
|
|
23
|
+
server.registerTool(
|
|
24
|
+
"firebase_audit_scan",
|
|
25
|
+
{
|
|
26
|
+
title: "Firebase Audit Scan",
|
|
27
|
+
description: "Audita projeto Firebase (c\xF3digo + firestore.rules + \xEDndices + contrato). Static-first, nunca inventa certeza (CONFIRMED/PROBABLE/NOT_OBSERVED/UNKNOWN). Use para descobrir regras p\xFAblicas, Admin em cliente, permiss\xF5es sem contrato.",
|
|
28
|
+
inputSchema: {
|
|
29
|
+
cwd: z.string().min(1).describe("Diret\xF3rio raiz do projeto Firebase a auditar"),
|
|
30
|
+
adapter: z.string().optional().describe("Fun\xE7\xE3o de autoriza\xE7\xE3o (ex: rbac.can). Default: do contrato."),
|
|
31
|
+
strict: z.boolean().optional().describe("Exige contrato (firebase-audit.yaml). CI usa strict.")
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
async ({ cwd, adapter, strict }) => {
|
|
35
|
+
try {
|
|
36
|
+
const res = await scan(cwd, { adapterFn: adapter, strict });
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: "text",
|
|
41
|
+
text: JSON.stringify({ version: pkg.version, summary: res.summary, findings: res.findings }, null, 2)
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
};
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text", text: `scan falhou: ${err instanceof Error ? err.message : err}` }],
|
|
48
|
+
isError: true
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
server.registerTool(
|
|
54
|
+
"firebase_audit_check",
|
|
55
|
+
{
|
|
56
|
+
title: "Firebase Audit Check (strict)",
|
|
57
|
+
description: "Alias de scan --strict: exige contrato e falha (exit 2 no CLI) se houver ERROR. Use no CI.",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
cwd: z.string().min(1).describe("Diret\xF3rio raiz do projeto Firebase"),
|
|
60
|
+
adapter: z.string().optional().describe("Fun\xE7\xE3o de autoriza\xE7\xE3o (ex: rbac.can)")
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
async ({ cwd, adapter }) => {
|
|
64
|
+
try {
|
|
65
|
+
const res = await scan(cwd, { adapterFn: adapter, strict: true });
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: "text",
|
|
70
|
+
text: JSON.stringify({ version: pkg.version, summary: res.summary, findings: res.findings }, null, 2)
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
};
|
|
74
|
+
} catch (err) {
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text", text: `check falhou: ${err instanceof Error ? err.message : err}` }],
|
|
77
|
+
isError: true
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
server.registerTool(
|
|
83
|
+
"firebase_audit_verify",
|
|
84
|
+
{
|
|
85
|
+
title: "Firebase Audit Verify (Emulator)",
|
|
86
|
+
description: "Gera matriz role\xD7opera\xE7\xE3o\xD7path e avalia coverage do :ruleCoverage. Requer Emulator (firebase emulators:exec). Nunca afirma cobertura sem teste real.",
|
|
87
|
+
inputSchema: {
|
|
88
|
+
cwd: z.string().min(1).describe("Diret\xF3rio raiz do projeto"),
|
|
89
|
+
coverage: z.string().optional().describe("Path do coverage.json do :ruleCoverage (relativo ao cwd)")
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
async ({ cwd, coverage }) => {
|
|
93
|
+
try {
|
|
94
|
+
const { resolve } = await import("path");
|
|
95
|
+
const { model } = await scan(cwd);
|
|
96
|
+
const res = verify(model, { coverageFile: coverage ? resolve(cwd, coverage) : void 0 });
|
|
97
|
+
return {
|
|
98
|
+
content: [
|
|
99
|
+
{
|
|
100
|
+
type: "text",
|
|
101
|
+
text: JSON.stringify(
|
|
102
|
+
{ version: pkg.version, cases: res.cases.length, uncovered: res.uncovered, coveragePct: res.coveragePct, findings: res.findings },
|
|
103
|
+
null,
|
|
104
|
+
2
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
};
|
|
109
|
+
} catch (err) {
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: "text", text: `verify falhou: ${err instanceof Error ? err.message : err}` }],
|
|
112
|
+
isError: true
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
server.registerTool(
|
|
118
|
+
"firebase_audit_drift",
|
|
119
|
+
{
|
|
120
|
+
title: "Firebase Audit Drift (\xEDndices)",
|
|
121
|
+
description: "Compara \xEDndices locais vs implantados. Retorna MATCHED/LOCAL_ONLY/REMOTE_ONLY. Nunca diz 'unused', apenas NOT_OBSERVED.",
|
|
122
|
+
inputSchema: {
|
|
123
|
+
local: z.string().nullable().optional().describe("Path do firestore.indexes.json local (ou null). Relativo resolve contra cwd."),
|
|
124
|
+
remote: z.string().nullable().optional().describe("Path do JSON remoto (gcloud/REST) (ou null). Relativo resolve contra cwd."),
|
|
125
|
+
cwd: z.string().optional().describe("Diret\xF3rio base para paths relativos (default: cwd do servidor).")
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
async ({ local, remote, cwd }) => {
|
|
129
|
+
try {
|
|
130
|
+
const { resolve: resolveP, isAbsolute } = await import("path");
|
|
131
|
+
const base = cwd && cwd.length > 0 ? cwd : process.cwd();
|
|
132
|
+
const resPath = (p) => {
|
|
133
|
+
if (p === null || p === void 0) return null;
|
|
134
|
+
return isAbsolute(p) ? p : resolveP(base, p);
|
|
135
|
+
};
|
|
136
|
+
const res = drift(resPath(local), resPath(remote));
|
|
137
|
+
return {
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: "text",
|
|
141
|
+
text: JSON.stringify({ version: pkg.version, entries: res.entries, findings: res.findings }, null, 2)
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
};
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return {
|
|
147
|
+
content: [{ type: "text", text: `drift falhou: ${err instanceof Error ? err.message : err}` }],
|
|
148
|
+
isError: true
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
return server;
|
|
154
|
+
}
|
|
155
|
+
async function startMcpServer() {
|
|
156
|
+
const server = createMcpServer();
|
|
157
|
+
const transport = new StdioServerTransport();
|
|
158
|
+
await server.connect(transport);
|
|
159
|
+
console.error(`[firebase-audit] MCP server v${pkg.version} running via stdio`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export {
|
|
163
|
+
createMcpServer,
|
|
164
|
+
startMcpServer
|
|
165
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/drift.ts
|
|
2
2
|
import { readFileSync, existsSync } from "fs";
|
|
3
3
|
function normalizeMode(f) {
|
|
4
|
-
if (typeof f.fieldPath !== "string" || f.fieldPath.length === 0
|
|
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" };
|
|
6
6
|
if (f.arrayConfig === "CONTAINS") return { fieldPath: f.fieldPath, mode: "ARRAY_CONTAINS" };
|
|
7
7
|
if (typeof f.mode === "string" && (f.mode === "ASCENDING" || f.mode === "DESCENDING" || f.mode === "ARRAY_CONTAINS" || f.mode === "VECTOR")) {
|
|
@@ -19,6 +19,9 @@ function normalizeEntry(raw) {
|
|
|
19
19
|
if (m) cg = m[1];
|
|
20
20
|
}
|
|
21
21
|
if (!cg) return null;
|
|
22
|
+
if (raw.queryScope !== void 0 && raw.queryScope !== "COLLECTION" && raw.queryScope !== "COLLECTION_GROUP") {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
22
25
|
const scope = raw.queryScope === "COLLECTION_GROUP" ? "COLLECTION_GROUP" : "COLLECTION";
|
|
23
26
|
const fields = [];
|
|
24
27
|
if (Array.isArray(raw.fields)) {
|
|
@@ -27,6 +30,7 @@ function normalizeEntry(raw) {
|
|
|
27
30
|
if (n) fields.push(n);
|
|
28
31
|
}
|
|
29
32
|
}
|
|
33
|
+
if (fields.length === 0) return null;
|
|
30
34
|
return { collectionGroup: cg, queryScope: scope, fields };
|
|
31
35
|
}
|
|
32
36
|
function keyOf(i) {
|
|
@@ -42,6 +46,19 @@ function normalizeList(raw) {
|
|
|
42
46
|
}
|
|
43
47
|
function drift(localFile, remoteFile) {
|
|
44
48
|
const findings = [];
|
|
49
|
+
const localMissing = !localFile || !existsSync(localFile);
|
|
50
|
+
const remoteMissing = !remoteFile || !existsSync(remoteFile);
|
|
51
|
+
if (localMissing && remoteMissing) {
|
|
52
|
+
findings.push({
|
|
53
|
+
rule: "DRIFT_NO_INPUT",
|
|
54
|
+
severity: "INFO",
|
|
55
|
+
confidence: "NOT_OBSERVED",
|
|
56
|
+
message: "Drift sem base local nem remota \u2014 nada a comparar.",
|
|
57
|
+
fingerprint: "DRIFT_NO_INPUT",
|
|
58
|
+
evidence: [{ id: "ev-drift-noinput", kind: "drift-parse", summary: "sem base", confidence: "NOT_OBSERVED" }]
|
|
59
|
+
});
|
|
60
|
+
return { entries: [], findings };
|
|
61
|
+
}
|
|
45
62
|
let local = [];
|
|
46
63
|
let remote = [];
|
|
47
64
|
if (localFile && existsSync(localFile)) {
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
// src/verify.ts
|
|
2
2
|
import { readFileSync, existsSync } from "fs";
|
|
3
|
+
function collectVisitCounts(node, out) {
|
|
4
|
+
if (typeof node !== "object" || node === null) return;
|
|
5
|
+
if (Array.isArray(node)) {
|
|
6
|
+
for (const v of node) collectVisitCounts(v, out);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
const rec = node;
|
|
10
|
+
for (const k of ["visitCount", "visit_count"]) {
|
|
11
|
+
const v = rec[k];
|
|
12
|
+
if (typeof v === "number" && Number.isFinite(v)) {
|
|
13
|
+
out.push(v);
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
for (const v of Object.values(rec)) collectVisitCounts(v, out);
|
|
18
|
+
}
|
|
3
19
|
function planVerify(model) {
|
|
4
20
|
const roles = model.authorization.roles.length > 0 ? model.authorization.roles.map((r) => r.name) : ["anonymous", "user", "admin"];
|
|
5
21
|
const expand = (op) => {
|
|
@@ -10,6 +26,7 @@ function planVerify(model) {
|
|
|
10
26
|
};
|
|
11
27
|
const cases = [];
|
|
12
28
|
for (const rule of model.rules) {
|
|
29
|
+
if (rule.path === "/(unknown)") continue;
|
|
13
30
|
for (const role of roles) {
|
|
14
31
|
for (const op of rule.operations) {
|
|
15
32
|
for (const mapped of expand(op)) {
|
|
@@ -32,6 +49,7 @@ function verify(model, opts = {}) {
|
|
|
32
49
|
const cases = planVerify(model);
|
|
33
50
|
const findings = [];
|
|
34
51
|
const uncovered = [];
|
|
52
|
+
let coveragePct = null;
|
|
35
53
|
if (opts.coverageFile) {
|
|
36
54
|
if (!existsSync(opts.coverageFile)) {
|
|
37
55
|
findings.push({
|
|
@@ -46,15 +64,32 @@ function verify(model, opts = {}) {
|
|
|
46
64
|
} else {
|
|
47
65
|
try {
|
|
48
66
|
const raw = JSON.parse(readFileSync(opts.coverageFile, "utf-8"));
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
67
|
+
const counts = [];
|
|
68
|
+
collectVisitCounts(raw, counts);
|
|
69
|
+
if (counts.length === 0) {
|
|
70
|
+
findings.push({
|
|
71
|
+
rule: "COVERAGE_NOT_EVALUATED",
|
|
72
|
+
severity: "WARNING",
|
|
73
|
+
confidence: "NOT_OBSERVED",
|
|
74
|
+
message: "Coverage sem visitCount reconhec\xEDvel nesta vers\xE3o \u2014 uncovered lista tudo por honestidade.",
|
|
75
|
+
fingerprint: "COVERAGE_NOT_EVALUATED",
|
|
76
|
+
evidence: [{ id: "ev-coverage-ne", kind: "coverage-parse", summary: "n\xE3o avaliado", confidence: "NOT_OBSERVED" }]
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
const covered = counts.filter((c) => c > 0).length;
|
|
80
|
+
coveragePct = Math.round(covered / counts.length * 100);
|
|
81
|
+
if (coveragePct < 100) {
|
|
82
|
+
findings.push({
|
|
83
|
+
rule: "COVERAGE_LOW",
|
|
84
|
+
severity: "WARNING",
|
|
85
|
+
confidence: "CONFIRMED",
|
|
86
|
+
message: `Cobertura de Rules em ${coveragePct}% (${covered}/${counts.length} express\xF5es visitadas) \u2014 ramos com visitCount 0 sem teste.`,
|
|
87
|
+
fingerprint: "COVERAGE_LOW",
|
|
88
|
+
evidence: [{ id: "ev-coverage-low", kind: "coverage-parse", summary: `${coveragePct}%`, confidence: "CONFIRMED" }],
|
|
89
|
+
fix: "Adicione casos anon/user/admin x get/list/create/update/delete at\xE9 100%."
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
58
93
|
} catch {
|
|
59
94
|
findings.push({
|
|
60
95
|
rule: "COVERAGE_INVALID",
|
|
@@ -67,10 +102,14 @@ function verify(model, opts = {}) {
|
|
|
67
102
|
}
|
|
68
103
|
}
|
|
69
104
|
}
|
|
105
|
+
if (coveragePct === 100) {
|
|
106
|
+
return { cases, uncovered: [], findings, coveragePct };
|
|
107
|
+
}
|
|
70
108
|
for (const rule of model.rules) {
|
|
109
|
+
if (rule.path === "/(unknown)") continue;
|
|
71
110
|
uncovered.push(rule.path);
|
|
72
111
|
}
|
|
73
|
-
return { cases, uncovered, findings };
|
|
112
|
+
return { cases, uncovered, findings, coveragePct };
|
|
74
113
|
}
|
|
75
114
|
|
|
76
115
|
export {
|
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-3SEVYYCX.js";
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
7
|
import { createRequire } from "module";
|
|
@@ -64,19 +64,20 @@ 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-VREY2FGO.js");
|
|
68
|
+
const { verify } = await import("./verify-O722ZZBV.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({ version: pkg.version, cases: res.cases.length, uncovered: res.uncovered, findings: res.findings }, null, 2));
|
|
75
|
+
console.log(JSON.stringify({ version: pkg.version, cases: res.cases.length, uncovered: res.uncovered, coveragePct: res.coveragePct, findings: res.findings }, null, 2));
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
|
+
const covStr = res.coveragePct === null ? "sem cobertura observada" : `${res.coveragePct}% de express\xF5es visitadas`;
|
|
78
79
|
console.log(`
|
|
79
|
-
FIREBASE AUDIT verify \u2014 ${res.cases.length} casos planejados, ${res.uncovered.length} paths sem
|
|
80
|
+
FIREBASE AUDIT verify \u2014 ${res.cases.length} casos planejados, ${res.uncovered.length} paths sem mapeamento n\xF3\u2192path (${covStr}).`);
|
|
80
81
|
for (const f of res.findings) {
|
|
81
82
|
const icon = f.severity === "ERROR" ? "\u274C" : f.severity === "WARNING" ? "\u26A0" : "\u2139";
|
|
82
83
|
console.log(`
|
|
@@ -93,7 +94,7 @@ ${icon} ${f.rule} [${f.confidence}]
|
|
|
93
94
|
return;
|
|
94
95
|
}
|
|
95
96
|
if (cmd === "drift") {
|
|
96
|
-
const { drift } = await import("./drift-
|
|
97
|
+
const { drift } = await import("./drift-KU7HWL7M.js");
|
|
97
98
|
const { resolve: resolveDrift } = await import("path");
|
|
98
99
|
const { existsSync: existsDrift } = await import("fs");
|
|
99
100
|
const localArg = getArg("local");
|
|
@@ -106,6 +107,15 @@ ${icon} ${f.rule} [${f.confidence}]
|
|
|
106
107
|
const remotePath = remoteArg ? resolveDrift(rootDir, remoteArg) : null;
|
|
107
108
|
for (const [label, p] of [["--local", localPath], ["--remote", remotePath]]) {
|
|
108
109
|
if (p && !existsDrift(p)) {
|
|
110
|
+
if (asJson) {
|
|
111
|
+
const { drift: driftMissing } = await import("./drift-KU7HWL7M.js");
|
|
112
|
+
const res2 = driftMissing(
|
|
113
|
+
localPath && existsDrift(localPath) ? localPath : null,
|
|
114
|
+
remotePath && existsDrift(remotePath) ? remotePath : null
|
|
115
|
+
);
|
|
116
|
+
console.log(JSON.stringify({ version: pkg.version, entries: res2.entries, findings: res2.findings }, null, 2));
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
109
119
|
console.error(`Arquivo ${label} n\xE3o encontrado: ${p}`);
|
|
110
120
|
process.exit(1);
|
|
111
121
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as z from 'zod';
|
|
2
2
|
import { z as z$1 } from 'zod';
|
|
3
|
+
export { createMcpServer, startMcpServer } from './mcp.js';
|
|
4
|
+
import '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* firebase-audit — schemas centrais (Zod v4).
|
|
@@ -566,6 +568,7 @@ declare const FindingSchema: z.ZodObject<{
|
|
|
566
568
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
567
569
|
}, z.core.$strict>;
|
|
568
570
|
declare const AuditYamlSchema: z.ZodObject<{
|
|
571
|
+
$schema: z.ZodOptional<z.ZodString>;
|
|
569
572
|
authorization: z.ZodObject<{
|
|
570
573
|
adapter: z.ZodOptional<z.ZodObject<{
|
|
571
574
|
function: z.ZodString;
|
|
@@ -592,6 +595,13 @@ declare const AuditYamlSchema: z.ZodObject<{
|
|
|
592
595
|
}, z.core.$strict>>;
|
|
593
596
|
}, z.core.$strict>;
|
|
594
597
|
}, z.core.$strict>;
|
|
598
|
+
/** Chaves OIDC/Firebase reservadas — nunca em custom claims (1000 bytes, só acesso). */
|
|
599
|
+
declare const RESERVED_CLAIM_KEYS: readonly string[];
|
|
600
|
+
/** Valida samples de claims: tamanho 1000 bytes + chaves reservadas. */
|
|
601
|
+
declare function validateClaimSamples(samples: Record<string, Record<string, unknown>> | undefined): {
|
|
602
|
+
ok: boolean;
|
|
603
|
+
errors: string[];
|
|
604
|
+
};
|
|
595
605
|
type ProjectModel = z.infer<typeof ProjectModelSchema>;
|
|
596
606
|
type Finding = z.infer<typeof FindingSchema>;
|
|
597
607
|
type AuditYaml = z.infer<typeof AuditYamlSchema>;
|
|
@@ -618,15 +628,25 @@ interface DiscoveryResult {
|
|
|
618
628
|
serverFiles: string[];
|
|
619
629
|
}
|
|
620
630
|
declare function discover(rootDir: string): DiscoveryResult;
|
|
621
|
-
/** Segmentos de servidor
|
|
631
|
+
/** Segmentos de servidor (fail-closed: `api` genérico NÃO é server, ver classifyOrigin). */
|
|
622
632
|
declare const SERVER_HINTS_SEGMENTS: string[];
|
|
623
|
-
/** Heurística CLIENT vs SERVER: caminho manda primeiro (grafo
|
|
633
|
+
/** Heurística CLIENT vs SERVER: caminho manda primeiro (grafo ai-tool preenche listas na Fase 3). */
|
|
624
634
|
declare function classifyOrigin(file: string, content: string): "CLIENT" | "SERVER" | "UNKNOWN";
|
|
625
635
|
declare function emptyModel(rootDir: string, d: DiscoveryResult): ProjectModel;
|
|
636
|
+
/**
|
|
637
|
+
* Fase 3 — enriquece Discovery com grafo do ai-tool (como lib, sem spawn/MCP).
|
|
638
|
+
* Preenche clientFiles/serverFiles a partir do mapa do projeto.
|
|
639
|
+
* Nunca joga: falha do ai-tool mantém listas vazias (honesto).
|
|
640
|
+
*/
|
|
641
|
+
declare function enrichWithGraph(rootDir: string, d: DiscoveryResult): Promise<void>;
|
|
626
642
|
|
|
627
643
|
/**
|
|
628
644
|
* Rules extractor — estrutura + referências + locations (sem avaliar).
|
|
629
645
|
* Parser leve e intencionalmente incompleto: avaliação real é do Emulator (V3).
|
|
646
|
+
*
|
|
647
|
+
* Limitação conhecida: `ALLOW_RE` corta a condição no primeiro `;` mesmo dentro
|
|
648
|
+
* de string literal (ex: `x != "a;b"`). Raríssimo em Rules reais; documentado aqui
|
|
649
|
+
* em vez de silêncio — Emulator acusa a regra de verdade.
|
|
630
650
|
*/
|
|
631
651
|
|
|
632
652
|
type Rule = z$1.infer<typeof RuleSchema>;
|
|
@@ -636,23 +656,77 @@ declare function opsFrom(target: string): {
|
|
|
636
656
|
ops: Rule["operations"];
|
|
637
657
|
known: boolean;
|
|
638
658
|
};
|
|
659
|
+
/** Normaliza target de allow para fingerprint estável (case/espaço/ordem). */
|
|
660
|
+
declare function normalizeTarget(target: string): string;
|
|
661
|
+
/** Bloco match com posição para concatenação de pais (subcoleções). */
|
|
662
|
+
interface MatchBlock {
|
|
663
|
+
path: string;
|
|
664
|
+
line: number;
|
|
665
|
+
blockStart: number;
|
|
666
|
+
blockEnd: number;
|
|
667
|
+
}
|
|
668
|
+
/** Núcleo comum: blocos match com fim por profundidade (fora de strings). */
|
|
669
|
+
declare function parseMatchBlocks(content: string): MatchBlock[];
|
|
670
|
+
/** Concatena paths ancestrais: /users/{u} + /orders/{o} = /users/{u}/orders/{o}. */
|
|
671
|
+
declare function buildFullPath(containingAsc: MatchBlock[]): string;
|
|
672
|
+
/** Mapa function NAME -> corpo para helpers (isAdmin, isSignedIn — padrão oficial). */
|
|
673
|
+
declare function parseRuleFunctions(content: string): Map<string, string>;
|
|
674
|
+
/** Versão com assinatura (params) para inline de helpers com argumentos. */
|
|
675
|
+
declare function parseRuleFunctionsDetailed(content: string): Map<string, {
|
|
676
|
+
params: string[];
|
|
677
|
+
body: string;
|
|
678
|
+
}>;
|
|
679
|
+
/** Inline textual de args nos params (1 nível, sem aninhamento complexo). */
|
|
680
|
+
declare function inlineHelperArgs(body: string, params: string[], args: string[]): string;
|
|
681
|
+
/** Inline de helpers dentro de expressões: `isAdmin() && x==1` vira `(corpo) && x==1`. */
|
|
682
|
+
declare function inlineHelpersInCondition(condition: string, detailed: Map<string, {
|
|
683
|
+
params: string[];
|
|
684
|
+
body: string;
|
|
685
|
+
}>): string;
|
|
686
|
+
/** Se condição é chamada única `nome(...)`, resolve corpo do helper (1 nível, com args). */
|
|
687
|
+
declare function resolveHelperCondition(condition: string, functions: Map<string, string>): string;
|
|
688
|
+
/** Resolução com substituição de args (para helpers parametrizados oficiais). */
|
|
689
|
+
declare function resolveHelperConditionDetailed(condition: string, detailed: Map<string, {
|
|
690
|
+
params: string[];
|
|
691
|
+
body: string;
|
|
692
|
+
}>): string;
|
|
639
693
|
declare function extractRules(rulesFile: string, relFile: string): Rule[];
|
|
694
|
+
/** Extrai de conteúdo (evita triple-read + permite teste sem arquivo). */
|
|
695
|
+
declare function extractRulesContent(rawContent: string, relFile: string): Rule[];
|
|
696
|
+
/** Remove apenas parênteses externos balanceados que embrulham tudo. */
|
|
697
|
+
declare function stripOuterParens(s: string): string;
|
|
640
698
|
/** Normaliza condição para detectar público: sem if, `true`, `(true)`, `|| true`, `auth == null`. */
|
|
641
699
|
declare function isPublicCondition(condition: string | undefined): {
|
|
642
700
|
isPublic: boolean;
|
|
643
701
|
confidence: "CONFIRMED" | "PROBABLE";
|
|
644
702
|
};
|
|
703
|
+
/** Hash curto e estável (8 hex) para fingerprint v2: imune a formatação/espaço/case. */
|
|
704
|
+
declare function hashShort(text: string): string;
|
|
705
|
+
/** Chave de condição para fingerprint: "uncond" ou hash da condição normalizada. */
|
|
706
|
+
declare function conditionKeyForFingerprint(condition: string | undefined): string;
|
|
645
707
|
/** Detecta allows públicos preservando linha, path e confiança para FBA001/FBA002. */
|
|
646
708
|
declare function findPublicAllows(rulesFile: string): {
|
|
647
709
|
line: number;
|
|
648
710
|
target: string;
|
|
649
711
|
path: string;
|
|
650
712
|
confidence: "CONFIRMED" | "PROBABLE";
|
|
713
|
+
conditionKey: string;
|
|
714
|
+
}[];
|
|
715
|
+
/** Extrai expressão de `return` do corpo do helper para avaliação pública. */
|
|
716
|
+
declare function helperBodyToCondition(body: string): string;
|
|
717
|
+
/** Núcleo com conteúdo (reuso em checks sem triple-read). */
|
|
718
|
+
declare function findPublicAllowsContent(rawContent: string): {
|
|
719
|
+
line: number;
|
|
720
|
+
target: string;
|
|
721
|
+
path: string;
|
|
722
|
+
confidence: "CONFIRMED" | "PROBABLE";
|
|
723
|
+
conditionKey: string;
|
|
651
724
|
}[];
|
|
652
725
|
|
|
653
726
|
/**
|
|
654
727
|
* Checks V1+V2 (FBA001–FBA004, FBA009–FBA013, subconjunto intencional).
|
|
655
|
-
* Fingerprints: FBA001/FBA002 por regra-lógica
|
|
728
|
+
* Fingerprints v2: FBA001/FBA002 por regra-lógica + hash da condição
|
|
729
|
+
* (estáveis a formatação, únicos por regra duplicada);
|
|
656
730
|
* FBA003/FBA004/FBA010/FBA012 com linha (únicos por ocorrência).
|
|
657
731
|
*/
|
|
658
732
|
|
|
@@ -668,17 +742,32 @@ declare function runChecks(model: ProjectModel, rootDir: string, opts?: {
|
|
|
668
742
|
interface ScanOptions {
|
|
669
743
|
adapterFn?: string;
|
|
670
744
|
strict?: boolean;
|
|
745
|
+
/**
|
|
746
|
+
* Opt-in: enriquece Discovery com grafo do ai-tool (clientFiles/serverFiles).
|
|
747
|
+
* Default false (scan static-first rápido; nenhum check consome o grafo na 0.4.0).
|
|
748
|
+
*/
|
|
749
|
+
graph?: boolean;
|
|
750
|
+
}
|
|
751
|
+
interface ScanSummary {
|
|
752
|
+
errors: number;
|
|
753
|
+
warnings: number;
|
|
754
|
+
infos: number;
|
|
755
|
+
/** Compat: mesmo que passedChecks. */
|
|
756
|
+
passed: number;
|
|
757
|
+
/** Compat: mesmo que totalChecks. */
|
|
758
|
+
total: number;
|
|
759
|
+
passedChecks: number;
|
|
760
|
+
totalChecks: number;
|
|
761
|
+
/** Findings de infra (fora dos checks FBA): rules/contrato/índices/model. */
|
|
762
|
+
infraSkipped: number;
|
|
763
|
+
/** Cobertura de Rules (0-100) quando verify com coverage avalia; null = não observada.
|
|
764
|
+
* Sempre null no scan() por desenho (cobertura só via verify coveragePct). */
|
|
765
|
+
coveragePct: number | null;
|
|
671
766
|
}
|
|
672
767
|
interface ScanResult {
|
|
673
768
|
model: ProjectModel;
|
|
674
769
|
findings: Finding[];
|
|
675
|
-
summary:
|
|
676
|
-
errors: number;
|
|
677
|
-
warnings: number;
|
|
678
|
-
infos: number;
|
|
679
|
-
passed: number;
|
|
680
|
-
total: number;
|
|
681
|
-
};
|
|
770
|
+
summary: ScanSummary;
|
|
682
771
|
}
|
|
683
772
|
declare function scan(rootDir: string, opts?: ScanOptions): Promise<ScanResult>;
|
|
684
773
|
|
|
@@ -704,6 +793,8 @@ interface VerifyResult {
|
|
|
704
793
|
cases: VerifyCase[];
|
|
705
794
|
uncovered: string[];
|
|
706
795
|
findings: Finding[];
|
|
796
|
+
/** 0-100 quando o coverage tem visitCount avaliável; null = não observada. */
|
|
797
|
+
coveragePct: number | null;
|
|
707
798
|
}
|
|
708
799
|
/** Gera matriz role × operação × path a partir do modelo (sem executar nada).
|
|
709
800
|
* Mapeamento: read≡get+list, write≡create+update+delete (mesma semântica do opsFrom).
|
|
@@ -740,4 +831,4 @@ declare function drift(localFile: string | null, remoteFile: string | null): Dri
|
|
|
740
831
|
|
|
741
832
|
declare const VERSION: string;
|
|
742
833
|
|
|
743
|
-
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, RoleSchema, RuleSchema, SERVER_HINTS_SEGMENTS, type ScanOptions, type ScanResult, SeveritySchema, VERSION, type VerifyCase, type VerifyOptions, type VerifyResult, classifyOrigin, discover, drift, emptyModel, extractRules, findPublicAllows, isPublicCondition, opsFrom, planVerify, runChecks, scan, stripRuleComments, verify };
|
|
834
|
+
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 };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createMcpServer,
|
|
3
|
+
startMcpServer
|
|
4
|
+
} from "./chunk-GOLWQBD5.js";
|
|
1
5
|
import {
|
|
2
6
|
AccessOriginSchema,
|
|
3
7
|
AuditYamlSchema,
|
|
@@ -17,28 +21,46 @@ import {
|
|
|
17
21
|
QueryFilterSchema,
|
|
18
22
|
QueryOrderSchema,
|
|
19
23
|
QueryShapeSchema,
|
|
24
|
+
RESERVED_CLAIM_KEYS,
|
|
20
25
|
RoleSchema,
|
|
21
26
|
RuleSchema,
|
|
22
27
|
SERVER_HINTS_SEGMENTS,
|
|
23
28
|
SeveritySchema,
|
|
29
|
+
buildFullPath,
|
|
24
30
|
classifyOrigin,
|
|
31
|
+
conditionKeyForFingerprint,
|
|
25
32
|
discover,
|
|
26
33
|
emptyModel,
|
|
34
|
+
enrichWithGraph,
|
|
27
35
|
extractRules,
|
|
36
|
+
extractRulesContent,
|
|
28
37
|
findPublicAllows,
|
|
38
|
+
findPublicAllowsContent,
|
|
39
|
+
hashShort,
|
|
40
|
+
helperBodyToCondition,
|
|
41
|
+
inlineHelperArgs,
|
|
42
|
+
inlineHelpersInCondition,
|
|
29
43
|
isPublicCondition,
|
|
44
|
+
normalizeTarget,
|
|
30
45
|
opsFrom,
|
|
46
|
+
parseMatchBlocks,
|
|
47
|
+
parseRuleFunctions,
|
|
48
|
+
parseRuleFunctionsDetailed,
|
|
49
|
+
resolveHelperCondition,
|
|
50
|
+
resolveHelperConditionDetailed,
|
|
31
51
|
runChecks,
|
|
32
52
|
scan,
|
|
33
|
-
|
|
34
|
-
|
|
53
|
+
stripOuterParens,
|
|
54
|
+
stripRuleComments,
|
|
55
|
+
validateClaimSamples
|
|
56
|
+
} from "./chunk-3SEVYYCX.js";
|
|
35
57
|
import {
|
|
36
58
|
planVerify,
|
|
37
59
|
verify
|
|
38
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-PSSMYRRU.js";
|
|
39
61
|
import {
|
|
40
62
|
drift
|
|
41
|
-
} from "./chunk-
|
|
63
|
+
} from "./chunk-NJBCSW7E.js";
|
|
42
64
|
|
|
43
65
|
// src/index.ts
|
|
44
66
|
import { createRequire } from "module";
|
|
@@ -64,22 +86,42 @@ export {
|
|
|
64
86
|
QueryFilterSchema,
|
|
65
87
|
QueryOrderSchema,
|
|
66
88
|
QueryShapeSchema,
|
|
89
|
+
RESERVED_CLAIM_KEYS,
|
|
67
90
|
RoleSchema,
|
|
68
91
|
RuleSchema,
|
|
69
92
|
SERVER_HINTS_SEGMENTS,
|
|
70
93
|
SeveritySchema,
|
|
71
94
|
VERSION,
|
|
95
|
+
buildFullPath,
|
|
72
96
|
classifyOrigin,
|
|
97
|
+
conditionKeyForFingerprint,
|
|
98
|
+
createMcpServer,
|
|
73
99
|
discover,
|
|
74
100
|
drift,
|
|
75
101
|
emptyModel,
|
|
102
|
+
enrichWithGraph,
|
|
76
103
|
extractRules,
|
|
104
|
+
extractRulesContent,
|
|
77
105
|
findPublicAllows,
|
|
106
|
+
findPublicAllowsContent,
|
|
107
|
+
hashShort,
|
|
108
|
+
helperBodyToCondition,
|
|
109
|
+
inlineHelperArgs,
|
|
110
|
+
inlineHelpersInCondition,
|
|
78
111
|
isPublicCondition,
|
|
112
|
+
normalizeTarget,
|
|
79
113
|
opsFrom,
|
|
114
|
+
parseMatchBlocks,
|
|
115
|
+
parseRuleFunctions,
|
|
116
|
+
parseRuleFunctionsDetailed,
|
|
80
117
|
planVerify,
|
|
118
|
+
resolveHelperCondition,
|
|
119
|
+
resolveHelperConditionDetailed,
|
|
81
120
|
runChecks,
|
|
82
121
|
scan,
|
|
122
|
+
startMcpServer,
|
|
123
|
+
stripOuterParens,
|
|
83
124
|
stripRuleComments,
|
|
125
|
+
validateClaimSamples,
|
|
84
126
|
verify
|
|
85
127
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|