@justmpm/firebase-audit 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-N72YT24M.js → chunk-PIRAC3KT.js} +111 -10
- package/dist/cli.js +32 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -239,6 +239,9 @@ function isPublicCondition(condition) {
|
|
|
239
239
|
if (condition === void 0) return { isPublic: true, confidence: "CONFIRMED" };
|
|
240
240
|
const norm = stripOuterParens(condition);
|
|
241
241
|
const low = norm.toLowerCase().replace(/\s+/g, "");
|
|
242
|
+
if (low.includes("!(request.auth==null)") || low.includes("!(request.auth===null)") || low === "!request.auth==null" || low === "!request.auth===null") {
|
|
243
|
+
return { isPublic: false, confidence: "CONFIRMED" };
|
|
244
|
+
}
|
|
242
245
|
if (low === "true") return { isPublic: true, confidence: "CONFIRMED" };
|
|
243
246
|
if (/(^|\|\|)true($|\|\||&&)/.test(low)) return { isPublic: true, confidence: "PROBABLE" };
|
|
244
247
|
if (low.includes("request.auth==null") || low.includes("request.auth===null")) {
|
|
@@ -271,7 +274,7 @@ function findPublicAllows(rulesFile) {
|
|
|
271
274
|
import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
|
|
272
275
|
import { join as join2 } from "path";
|
|
273
276
|
import { executeFind } from "@justmpm/supergrep";
|
|
274
|
-
var IMPLEMENTED_CHECKS = ["FBA001", "FBA002", "FBA003", "FBA004", "FBA009", "FBA010", "FBA011"];
|
|
277
|
+
var IMPLEMENTED_CHECKS = ["FBA001", "FBA002", "FBA003", "FBA004", "FBA009", "FBA010", "FBA011", "FBA012", "FBA013"];
|
|
275
278
|
function createEvidence() {
|
|
276
279
|
let counter = 0;
|
|
277
280
|
return {
|
|
@@ -411,6 +414,72 @@ async function runChecks(model, rootDir, opts = {}) {
|
|
|
411
414
|
}
|
|
412
415
|
}
|
|
413
416
|
}
|
|
417
|
+
for (const role of model.authorization.roles) {
|
|
418
|
+
if (role.permissions.length === 0) {
|
|
419
|
+
findings.push(
|
|
420
|
+
make(
|
|
421
|
+
"FBA013",
|
|
422
|
+
"WARNING",
|
|
423
|
+
"CONFIRMED",
|
|
424
|
+
`Role "${role.name}" sem permissions \u2014 nunca autoriza nada.`,
|
|
425
|
+
`FBA013:${role.name}`,
|
|
426
|
+
{
|
|
427
|
+
evidence: [ev("role-empty", role.name, "CONFIRMED")],
|
|
428
|
+
fix: `Adicionar permissions \xE0 role "${role.name}" ou remov\xEA-la.`
|
|
429
|
+
}
|
|
430
|
+
)
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const publicPaths = new Set(
|
|
435
|
+
absRules && existsSync2(absRules) ? findPublicAllows(absRules).map((p) => `${p.path}::${p.target}`) : []
|
|
436
|
+
);
|
|
437
|
+
const expandOp = (op) => {
|
|
438
|
+
if (op === "read") return ["read", "get", "list"];
|
|
439
|
+
if (op === "write") return ["write", "create", "update", "delete"];
|
|
440
|
+
return [op];
|
|
441
|
+
};
|
|
442
|
+
const permsByResource = /* @__PURE__ */ new Map();
|
|
443
|
+
for (const p of model.authorization.permissions) {
|
|
444
|
+
const holders = model.authorization.roles.filter((r) => r.permissions.includes(p.name)).map((r) => r.name);
|
|
445
|
+
const adminOnly = holders.length > 0 && holders.every((h) => h === "admin");
|
|
446
|
+
const list = permsByResource.get(p.resource) ?? [];
|
|
447
|
+
list.push({ perm: p.name, operation: p.operation, adminOnly });
|
|
448
|
+
permsByResource.set(p.resource, list);
|
|
449
|
+
}
|
|
450
|
+
for (const rule of model.rules) {
|
|
451
|
+
const resourceKey = rule.path.replace(/^\/+/, "").split("/")[0] ?? "";
|
|
452
|
+
if (resourceKey === "databases" || resourceKey === "(unknown)" || resourceKey === "") continue;
|
|
453
|
+
const entries = permsByResource.get(resourceKey);
|
|
454
|
+
if (!entries || entries.length === 0) continue;
|
|
455
|
+
const ruleOps = new Set(rule.operations.flatMap(expandOp));
|
|
456
|
+
const restricted = entries.filter((e) => e.adminOnly && ruleOps.has(e.operation));
|
|
457
|
+
if (restricted.length === 0) continue;
|
|
458
|
+
const isPublicRule = rule.operations.some(
|
|
459
|
+
(op) => publicPaths.has(`${rule.path}::${op}`)
|
|
460
|
+
) || rule.operations.some((op) => publicPaths.has(`${rule.path}::read, ${op}`));
|
|
461
|
+
if (isPublicRule) continue;
|
|
462
|
+
const checksAdminClaim = rule.claimReferences.some((c) => c.toLowerCase() === "admin");
|
|
463
|
+
if (checksAdminClaim) continue;
|
|
464
|
+
const checksAuth = rule.authReferences.length > 0;
|
|
465
|
+
if (!checksAuth) continue;
|
|
466
|
+
findings.push(
|
|
467
|
+
make(
|
|
468
|
+
"FBA012",
|
|
469
|
+
"WARNING",
|
|
470
|
+
"PROBABLE",
|
|
471
|
+
`Rule ${rule.path} [${rule.operations.join(", ")}] aceita autenticado sem checar claim de admin, mas contrato restringe ${restricted.map((r) => r.perm).join(", ")} a admin.`,
|
|
472
|
+
`FBA012:${rule.path}:${[...rule.operations].sort().join(",")}`,
|
|
473
|
+
{
|
|
474
|
+
file: rule.location.file,
|
|
475
|
+
line: rule.location.start.line,
|
|
476
|
+
resource: rule.path,
|
|
477
|
+
evidence: [ev("rule-contract-mismatch", `${rule.path} vs ${restricted.map((r) => r.perm).join(", ")}`, "PROBABLE", rule.location.file, rule.location.start.line)],
|
|
478
|
+
fix: "Adicionar checagem de claim de admin na Rule (ex: request.auth.token.admin == true) ou relaxar o contrato."
|
|
479
|
+
}
|
|
480
|
+
)
|
|
481
|
+
);
|
|
482
|
+
}
|
|
414
483
|
for (const dyn of permissionCalls.dynamic) {
|
|
415
484
|
findings.push(
|
|
416
485
|
make(
|
|
@@ -600,7 +669,7 @@ var QueryShapeSchema = z.strictObject({
|
|
|
600
669
|
location: LocationSchema
|
|
601
670
|
});
|
|
602
671
|
var PermissionSchema = z.strictObject({
|
|
603
|
-
name: z.string().min(
|
|
672
|
+
name: z.string().min(1),
|
|
604
673
|
resource: z.string().min(1),
|
|
605
674
|
operation: OperationSchema
|
|
606
675
|
});
|
|
@@ -726,14 +795,46 @@ async function scan(rootDir, opts = {}) {
|
|
|
726
795
|
if (d.firestoreIndexesFile && existsSync3(d.firestoreIndexesFile)) {
|
|
727
796
|
try {
|
|
728
797
|
const raw = JSON.parse(readFileSync4(d.firestoreIndexesFile, "utf-8"));
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
798
|
+
const validModes = ["ASCENDING", "DESCENDING", "ARRAY_CONTAINS", "VECTOR"];
|
|
799
|
+
const entries = [];
|
|
800
|
+
let skipped = 0;
|
|
801
|
+
for (const i of raw.indexes ?? []) {
|
|
802
|
+
if (typeof i.collectionGroup !== "string" || i.collectionGroup.length === 0) {
|
|
803
|
+
skipped += 1;
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
const fields = [];
|
|
807
|
+
for (const f of i.fields ?? []) {
|
|
808
|
+
if (typeof f.fieldPath !== "string" || f.fieldPath.length === 0) {
|
|
809
|
+
skipped += 1;
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
const mode = f.vectorConfig ? "VECTOR" : f.arrayConfig === "CONTAINS" ? "ARRAY_CONTAINS" : f.order;
|
|
813
|
+
if (mode !== "ASCENDING" && mode !== "DESCENDING" && mode !== "ARRAY_CONTAINS" && mode !== "VECTOR") {
|
|
814
|
+
skipped += 1;
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
fields.push({ fieldPath: f.fieldPath, mode });
|
|
818
|
+
}
|
|
819
|
+
entries.push({
|
|
820
|
+
collectionGroup: i.collectionGroup,
|
|
821
|
+
queryScope: i.queryScope === "COLLECTION_GROUP" ? "COLLECTION_GROUP" : "COLLECTION",
|
|
822
|
+
fields
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
model.indexes = entries;
|
|
826
|
+
if (skipped > 0) {
|
|
827
|
+
findings_pre.push({
|
|
828
|
+
rule: "INDEXES_INVALID",
|
|
829
|
+
severity: "WARNING",
|
|
830
|
+
confidence: "CONFIRMED",
|
|
831
|
+
message: `firestore.indexes.json com ${skipped} entrada(s) inv\xE1lidas ignoradas \u2014 verifique campos.`,
|
|
832
|
+
fingerprint: "INDEXES_INVALID",
|
|
833
|
+
evidence: [
|
|
834
|
+
{ id: "ev-indexes-skipped", kind: "indexes-parse", summary: `${skipped} inv\xE1lidas`, confidence: "CONFIRMED" }
|
|
835
|
+
]
|
|
836
|
+
});
|
|
837
|
+
}
|
|
737
838
|
} catch {
|
|
738
839
|
findings_pre.push({
|
|
739
840
|
rule: "INDEXES_INVALID",
|
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-PIRAC3KT.js";
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
7
|
import { createRequire } from "module";
|
|
@@ -14,15 +14,41 @@ async function main() {
|
|
|
14
14
|
console.log(pkg.version);
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
|
-
if (cmd
|
|
17
|
+
if (cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
18
|
+
console.log(`firebase-audit v${pkg.version}
|
|
19
|
+
|
|
20
|
+
Uso:
|
|
21
|
+
firebase-audit scan [--json] [--strict] [--adapter=fn] [--cwd=path]
|
|
22
|
+
firebase-audit check [--json] [--adapter=fn] [--cwd=path] (alias de scan --strict)
|
|
23
|
+
|
|
24
|
+
Exit codes: 0 ok (ou WARNING/INFO fora do strict); 2 quando --strict encontra ERROR.`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (cmd !== "scan" && cmd !== "check") {
|
|
18
28
|
console.error(`Comando desconhecido: ${cmd}
|
|
19
29
|
Uso: firebase-audit scan [--json] [--strict] [--adapter=fn] [--cwd=path]`);
|
|
20
30
|
process.exit(1);
|
|
21
31
|
}
|
|
32
|
+
const strict = args.includes("--strict") || cmd === "check";
|
|
22
33
|
const asJson = args.includes("--json");
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
const getArg = (name) => {
|
|
35
|
+
const pref = `--${name}=`;
|
|
36
|
+
const hit = args.find((a) => a.startsWith(pref));
|
|
37
|
+
if (hit) return hit.slice(pref.length);
|
|
38
|
+
const idx = args.findIndex((a) => a === `--${name}`);
|
|
39
|
+
if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith("--")) return args[idx + 1];
|
|
40
|
+
return void 0;
|
|
41
|
+
};
|
|
42
|
+
const knownFlags = /* @__PURE__ */ new Set(["--json", "--strict", "--help", "-h", "help", "--version", "-v"]);
|
|
43
|
+
for (const a of args.slice(1)) {
|
|
44
|
+
if (a.startsWith("--") && !a.startsWith("--adapter=") && !a.startsWith("--cwd=") && a !== "--adapter" && a !== "--cwd" && !knownFlags.has(a)) {
|
|
45
|
+
console.error(`Flag desconhecida: ${a}
|
|
46
|
+
Uso: firebase-audit scan [--json] [--strict] [--adapter=fn] [--cwd=path]`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const adapterArg = getArg("adapter");
|
|
51
|
+
const cwdArg = getArg("cwd");
|
|
26
52
|
const rootDir = cwdArg ?? process.cwd();
|
|
27
53
|
const { findings, summary } = await scan(rootDir, { strict, adapterFn: adapterArg });
|
|
28
54
|
if (asJson) {
|
|
@@ -35,7 +61,7 @@ FIREBASE AUDIT v${pkg.version}`);
|
|
|
35
61
|
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");
|
|
36
62
|
if (findings.length === 0) {
|
|
37
63
|
console.log(`
|
|
38
|
-
\u2713 Nenhum problema nos checks V1 implementados (parcial: FBA001-FBA004, FBA009-
|
|
64
|
+
\u2713 Nenhum problema nos checks V1+V2 implementados (parcial: FBA001-FBA004, FBA009-FBA013).
|
|
39
65
|
`);
|
|
40
66
|
return;
|
|
41
67
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -650,7 +650,7 @@ declare function findPublicAllows(rulesFile: string): {
|
|
|
650
650
|
* arquivo geram findings distintos).
|
|
651
651
|
*/
|
|
652
652
|
|
|
653
|
-
declare const IMPLEMENTED_CHECKS: readonly ["FBA001", "FBA002", "FBA003", "FBA004", "FBA009", "FBA010", "FBA011"];
|
|
653
|
+
declare const IMPLEMENTED_CHECKS: readonly ["FBA001", "FBA002", "FBA003", "FBA004", "FBA009", "FBA010", "FBA011", "FBA012", "FBA013"];
|
|
654
654
|
declare function runChecks(model: ProjectModel, rootDir: string, opts?: {
|
|
655
655
|
adapterFn?: string;
|
|
656
656
|
}): Promise<Finding[]>;
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justmpm/firebase-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Auditor de consistência e segurança para projetos Firebase: código + Rules + índices + contrato vs comportamento. Static-first, nunca inventa certeza.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"firebase",
|