@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.
@@ -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(3),
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
- model.indexes = raw.indexes?.map((i) => ({
730
- collectionGroup: i.collectionGroup ?? "(unknown)",
731
- queryScope: i.queryScope === "COLLECTION_GROUP" ? "COLLECTION_GROUP" : "COLLECTION",
732
- fields: (i.fields ?? []).map((f) => ({
733
- fieldPath: f.fieldPath ?? "(unknown)",
734
- mode: f.vectorConfig ? "VECTOR" : f.arrayConfig === "CONTAINS" ? "ARRAY_CONTAINS" : f.order ?? "ASCENDING"
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-N72YT24M.js";
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 !== "scan") {
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 strict = args.includes("--strict");
24
- const adapterArg = args.find((a) => a.startsWith("--adapter="))?.split("=")[1];
25
- const cwdArg = args.find((a) => a.startsWith("--cwd="))?.split("=")[1];
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-FBA011).
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
@@ -29,7 +29,7 @@ import {
29
29
  runChecks,
30
30
  scan,
31
31
  stripRuleComments
32
- } from "./chunk-N72YT24M.js";
32
+ } from "./chunk-PIRAC3KT.js";
33
33
 
34
34
  // src/index.ts
35
35
  import { createRequire } from "module";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justmpm/firebase-audit",
3
- "version": "0.1.1",
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",