@justmpm/firebase-audit 0.1.0 → 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.
@@ -1,679 +0,0 @@
1
- // src/discovery.ts
2
- import { existsSync, readFileSync } from "fs";
3
- import { join, relative } from "path";
4
- var SERVER_HINTS = ["functions/", "src/app/api/", "server/", "admin-sdk", "firebase-admin"];
5
- function discover(rootDir) {
6
- const pick = (name) => {
7
- const p = join(rootDir, name);
8
- return existsSync(p) ? p : null;
9
- };
10
- const firebaseJson = pick("firebase.json");
11
- let projectId = null;
12
- let databaseId = null;
13
- let rulesFile = pick("firestore.rules");
14
- let indexesFile = pick("firestore.indexes.json");
15
- if (firebaseJson) {
16
- try {
17
- const raw = JSON.parse(readFileSync(firebaseJson, "utf-8"));
18
- if (typeof raw.firestore?.rules === "string") {
19
- const p = join(rootDir, raw.firestore.rules);
20
- if (existsSync(p)) rulesFile = p;
21
- }
22
- if (typeof raw.firestore?.indexes === "string") {
23
- const p = join(rootDir, raw.firestore.indexes);
24
- if (existsSync(p)) indexesFile = p;
25
- }
26
- } catch {
27
- }
28
- }
29
- return {
30
- rootDir,
31
- firebaseJson,
32
- firestoreRulesFile: rulesFile,
33
- firestoreIndexesFile: indexesFile,
34
- auditYamlFile: pick("firebase-audit.yaml"),
35
- projectId,
36
- databaseId,
37
- clientFiles: [],
38
- serverFiles: []
39
- };
40
- }
41
- function classifyOrigin(file, content) {
42
- const rel = file.replace(/\\/g, "/");
43
- if (content.includes("firebase-admin") || content.includes("firebase-functions")) {
44
- return "SERVER";
45
- }
46
- if (SERVER_HINTS.some((h) => rel.includes(h))) {
47
- return "SERVER";
48
- }
49
- if (rel.includes("src/") || rel.includes("app/") || rel.includes("components/")) {
50
- return "CLIENT";
51
- }
52
- return "UNKNOWN";
53
- }
54
- function emptyModel(rootDir, d) {
55
- const rel = (abs) => abs ? relative(rootDir, abs).replace(/\\/g, "/") : void 0;
56
- return {
57
- schemaVersion: 1,
58
- rootDir,
59
- firebase: {
60
- configFile: rel(d.firebaseJson),
61
- projectId: d.projectId,
62
- databaseId: d.databaseId,
63
- firestore: {
64
- rulesFile: rel(d.firestoreRulesFile),
65
- indexesFile: rel(d.firestoreIndexesFile)
66
- }
67
- },
68
- authorization: { adapter: null, roles: [], permissions: [] },
69
- queries: [],
70
- rules: [],
71
- indexes: [],
72
- evidence: [],
73
- graph: []
74
- };
75
- }
76
-
77
- // src/rules.ts
78
- import { readFileSync as readFileSync2 } from "fs";
79
- var MATCH_RE = /match\s+(\/[^\s{]+)\s*\{/g;
80
- var ALLOW_RE = /allow\s+([^:]+):\s*if\s+([^;]+);/g;
81
- function opsFrom(target) {
82
- const t = target.trim().toLowerCase();
83
- if (t === "read") return ["read"];
84
- if (t === "write") return ["write", "create", "update", "delete"];
85
- const parts = t.split(",").map((s) => s.trim()).filter(Boolean);
86
- const valid = ["read", "get", "list", "create", "update", "delete", "write"];
87
- const out = [];
88
- for (const p of parts) {
89
- if (valid.includes(p)) {
90
- out.push(p);
91
- if (p === "write") {
92
- for (const extra of ["create", "update", "delete"]) {
93
- if (!out.includes(extra)) out.push(extra);
94
- }
95
- }
96
- }
97
- }
98
- return out.length > 0 ? out : ["read"];
99
- }
100
- function extractRules(rulesFile, relFile) {
101
- const content = readFileSync2(rulesFile, "utf-8");
102
- const lines = content.split("\n");
103
- const rules = [];
104
- const matchBlocks = [];
105
- let m;
106
- MATCH_RE.lastIndex = 0;
107
- while ((m = MATCH_RE.exec(content)) !== null) {
108
- const before = content.slice(0, m.index);
109
- const line = before.split("\n").length;
110
- matchBlocks.push({ path: m[1], line, blockStart: m.index });
111
- }
112
- ALLOW_RE.lastIndex = 0;
113
- let a;
114
- let idx = 0;
115
- while ((a = ALLOW_RE.exec(content)) !== null) {
116
- const before = content.slice(0, a.index);
117
- const line = before.split("\n").length;
118
- const target = a[1];
119
- const condition = a[2].trim();
120
- const currentMatch = [...matchBlocks].reverse().find((b) => b.blockStart <= a.index);
121
- const authReferences = [];
122
- if (condition.includes("request.auth")) authReferences.push("request.auth");
123
- if (condition.includes("request.auth.uid")) authReferences.push("request.auth.uid");
124
- const claimReferences = [...condition.matchAll(/request\.auth\.token\.([A-Za-z0-9_]+)/g)].map(
125
- (x) => x[1]
126
- );
127
- const resourceReferences = [...condition.matchAll(/resource\.data\.([A-Za-z0-9_]+)/g)].map(
128
- (x) => x[1]
129
- );
130
- const requestResourceReferences = [
131
- ...condition.matchAll(/request\.resource\.data\.([A-Za-z0-9_]+)/g)
132
- ].map((x) => x[1]);
133
- rules.push({
134
- id: `rule-${idx++}`,
135
- path: currentMatch?.path ?? "/(unknown)",
136
- operations: opsFrom(target),
137
- conditionPresent: condition.length > 0 && condition !== "true" && condition !== "false",
138
- authReferences,
139
- claimReferences,
140
- resourceReferences,
141
- requestResourceReferences,
142
- confidence: "CONFIRMED",
143
- location: { file: relFile, start: { line, column: 0 } }
144
- });
145
- void lines;
146
- }
147
- return rules;
148
- }
149
- function findPublicAllows(rulesFile) {
150
- const content = readFileSync2(rulesFile, "utf-8");
151
- const out = [];
152
- const re = /allow\s+([^:]+):\s*if\s+true\s*;/g;
153
- let mm;
154
- while ((mm = re.exec(content)) !== null) {
155
- const line = content.slice(0, mm.index).split("\n").length;
156
- out.push({ line, target: mm[1].trim() });
157
- }
158
- return out;
159
- }
160
-
161
- // src/checks.ts
162
- import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
163
- import { join as join2 } from "path";
164
- import { executeFind } from "@justmpm/supergrep";
165
- var evidenceCounter = 0;
166
- function ev(kind, summary, confidence, file, line) {
167
- evidenceCounter += 1;
168
- return {
169
- id: `ev-${evidenceCounter}`,
170
- kind,
171
- summary,
172
- confidence,
173
- location: file && line ? { file, start: { line, column: 0 } } : void 0
174
- };
175
- }
176
- function make(rule, severity, confidence, message, fingerprint, opts = {}) {
177
- return {
178
- rule,
179
- severity,
180
- confidence,
181
- message,
182
- fingerprint,
183
- evidence: [],
184
- ...opts
185
- };
186
- }
187
- async function runChecks(model, rootDir, opts = {}) {
188
- const findings = [];
189
- const relRules = model.firebase.firestore.rulesFile;
190
- const absRules = relRules ? join2(rootDir, relRules) : null;
191
- if (absRules && existsSync2(absRules)) {
192
- for (const pub of findPublicAllows(absRules)) {
193
- const t = pub.target.toLowerCase();
194
- const isWrite = t.includes("write") || t.includes("create") || t.includes("update") || t.includes("delete");
195
- const isRead = t.includes("read") || t.includes("get") || t.includes("list");
196
- if (isWrite) {
197
- findings.push(
198
- make(
199
- "FBA001",
200
- "ERROR",
201
- "CONFIRMED",
202
- `Public write aberto (${pub.target}: if true). Qualquer cliente pode escrever.`,
203
- `FBA001:${relRules}:${pub.target}`,
204
- {
205
- file: relRules,
206
- line: pub.line,
207
- evidence: [ev("public-allow", `${pub.target}: if true`, "CONFIRMED", relRules, pub.line)],
208
- fix: "Troque `if true` por checagem de auth/claim. Se for proposital, documente no contrato."
209
- }
210
- )
211
- );
212
- }
213
- if (isRead) {
214
- findings.push(
215
- make(
216
- "FBA002",
217
- "WARNING",
218
- "CONFIRMED",
219
- `Leitura p\xFAblica confirmada (${pub.target}: if true). Pode ser proposital.`,
220
- `FBA002:${relRules}:${pub.target}`,
221
- {
222
- file: relRules,
223
- line: pub.line,
224
- evidence: [ev("public-allow", `${pub.target}: if true`, "CONFIRMED", relRules, pub.line)]
225
- }
226
- )
227
- );
228
- }
229
- }
230
- }
231
- const adapterFn = opts.adapterFn ?? "rbac.can";
232
- const permissionCalls = await collectPermissionCalls(rootDir, adapterFn);
233
- const declared = new Set(model.authorization.permissions.map((p) => p.name));
234
- if (permissionCalls.length === 0) {
235
- findings.push(
236
- make(
237
- "FBA009",
238
- "WARNING",
239
- "UNKNOWN",
240
- `Adapter de autoriza\xE7\xE3o "${adapterFn}" n\xE3o observado no c\xF3digo. Sem adapter, permiss\xF5es s\xE3o UNKNOWN.`,
241
- `FBA009:${adapterFn}`,
242
- { evidence: [ev("adapter-missing", adapterFn, "UNKNOWN")] }
243
- )
244
- );
245
- }
246
- for (const call of permissionCalls) {
247
- if (!declared.has(call.permission) && declared.size > 0) {
248
- findings.push(
249
- make(
250
- "FBA004",
251
- "ERROR",
252
- "CONFIRMED",
253
- `Permission "${call.permission}" usada mas n\xE3o declarada no contrato.`,
254
- `FBA004:${call.file}:${call.permission}`,
255
- {
256
- file: call.file,
257
- line: call.line,
258
- resource: call.permission.split(".")[0],
259
- evidence: [ev("permission-call", `${adapterFn}("${call.permission}")`, "CONFIRMED", call.file, call.line)],
260
- fix: `Declarar "${call.permission}" em firebase-audit.yaml ou corrigir o nome da chamada.`
261
- }
262
- )
263
- );
264
- }
265
- }
266
- const adminHits = await collectAdminImports(rootDir);
267
- for (const hit of adminHits) {
268
- let origin = "UNKNOWN";
269
- try {
270
- const content = readFileSync3(join2(rootDir, hit.file), "utf-8");
271
- origin = classifyOrigin(hit.file, content);
272
- } catch {
273
- origin = "UNKNOWN";
274
- }
275
- if (origin === "CLIENT") {
276
- findings.push(
277
- make(
278
- "FBA003",
279
- "ERROR",
280
- "PROBABLE",
281
- `Admin SDK em arquivo de cliente (${hit.file}). Rules n\xE3o valem aqui \u2014 precisa auth da app.`,
282
- `FBA003:${hit.file}`,
283
- {
284
- file: hit.file,
285
- line: hit.line,
286
- evidence: [ev("admin-import", hit.text, "PROBABLE", hit.file, hit.line)],
287
- fix: "Mover para Functions/API server ou trocar por Client SDK com Rules."
288
- }
289
- )
290
- );
291
- }
292
- }
293
- if (model.indexes.length > 0 && model.queries.length === 0) {
294
- findings.push(
295
- make(
296
- "FBA007",
297
- "INFO",
298
- "NOT_OBSERVED",
299
- "\xCDndices locais sem query observada (query-shape V1 ainda parcial). N\xE3o significa unused.",
300
- "FBA007:indexes",
301
- { evidence: [ev("index-not-observed", `${model.indexes.length} \xEDndices`, "NOT_OBSERVED")] }
302
- )
303
- );
304
- }
305
- return findings;
306
- }
307
- async function collectPermissionCalls(rootDir, adapterFn) {
308
- const out = [];
309
- const short = adapterFn.includes(".") ? adapterFn.split(".").pop() : adapterFn;
310
- const patterns = [`${short}($PERM)`, `${adapterFn}($PERM)`];
311
- for (const pattern of patterns) {
312
- try {
313
- const { result } = await executeFind({ pattern, path: ".", cwd: rootDir });
314
- for (const m of result.matches) {
315
- const perm = (m.metaVariables["PERM"] ?? "").replace(/['"]/g, "").trim();
316
- if (perm.length >= 3) {
317
- out.push({ file: m.file, line: m.line + 1, permission: perm });
318
- }
319
- }
320
- } catch {
321
- }
322
- }
323
- const seen = /* @__PURE__ */ new Set();
324
- return out.filter((o) => {
325
- const k = `${o.file}:${o.line}:${o.permission}`;
326
- if (seen.has(k)) return false;
327
- seen.add(k);
328
- return true;
329
- });
330
- }
331
- async function collectAdminImports(rootDir) {
332
- const out = [];
333
- try {
334
- const { result } = await executeFind({
335
- pattern: "import { $$$ITEMS } from '$MODULE'",
336
- path: ".",
337
- cwd: rootDir
338
- });
339
- for (const m of result.matches) {
340
- const mod = m.metaVariables["MODULE"] ?? "";
341
- if (mod.includes("firebase-admin")) {
342
- out.push({ file: m.file, line: m.line + 1, text: m.text });
343
- }
344
- }
345
- } catch {
346
- }
347
- return out;
348
- }
349
-
350
- // src/schemas.ts
351
- import * as z from "zod";
352
- var SeveritySchema = z.enum(["ERROR", "WARNING", "INFO"]);
353
- var ConfidenceSchema = z.enum([
354
- "CONFIRMED",
355
- "PROBABLE",
356
- "NOT_OBSERVED",
357
- "UNKNOWN"
358
- ]);
359
- var OperationSchema = z.enum([
360
- "read",
361
- "get",
362
- "list",
363
- "create",
364
- "update",
365
- "delete",
366
- "write"
367
- ]);
368
- var AccessOriginSchema = z.enum(["CLIENT", "SERVER", "UNKNOWN"]);
369
- var PositionSchema = z.strictObject({
370
- line: z.number().int().positive(),
371
- column: z.number().int().nonnegative()
372
- });
373
- var LocationSchema = z.strictObject({
374
- file: z.string().min(1),
375
- start: PositionSchema,
376
- end: PositionSchema.optional()
377
- });
378
- var EvidenceSchema = z.strictObject({
379
- id: z.string().min(1),
380
- kind: z.string().min(1),
381
- location: LocationSchema.optional(),
382
- summary: z.string().min(1),
383
- confidence: ConfidenceSchema,
384
- value: z.unknown().optional(),
385
- fingerprint: z.string().min(1).optional()
386
- });
387
- var DynamicValueSchema = z.discriminatedUnion("kind", [
388
- z.strictObject({ kind: z.literal("literal"), value: z.unknown() }),
389
- z.strictObject({ kind: z.literal("symbolic"), expression: z.string() }),
390
- z.strictObject({ kind: z.literal("unknown"), reason: z.string() })
391
- ]);
392
- var QueryFilterSchema = z.strictObject({
393
- fieldPath: z.string().min(1),
394
- operator: z.enum([
395
- "==",
396
- "!=",
397
- "<",
398
- "<=",
399
- ">",
400
- ">=",
401
- "in",
402
- "not-in",
403
- "array-contains",
404
- "array-contains-any"
405
- ]),
406
- value: DynamicValueSchema
407
- });
408
- var QueryOrderSchema = z.strictObject({
409
- fieldPath: z.string().min(1),
410
- direction: z.enum(["ASCENDING", "DESCENDING"])
411
- });
412
- var QueryShapeSchema = z.strictObject({
413
- scope: z.enum(["COLLECTION", "COLLECTION_GROUP"]),
414
- collectionId: z.string().min(1),
415
- pathTemplate: z.string().min(1),
416
- filters: z.array(QueryFilterSchema),
417
- orderBy: z.array(QueryOrderSchema),
418
- limit: DynamicValueSchema.optional(),
419
- dynamic: z.boolean(),
420
- confidence: ConfidenceSchema,
421
- location: LocationSchema
422
- });
423
- var PermissionSchema = z.strictObject({
424
- name: z.string().min(3),
425
- resource: z.string().min(1),
426
- operation: OperationSchema
427
- });
428
- var RoleSchema = z.strictObject({
429
- name: z.string().min(1),
430
- permissions: z.array(z.string().min(1))
431
- });
432
- var RuleSchema = z.strictObject({
433
- id: z.string().min(1),
434
- path: z.string().min(1),
435
- operations: z.array(OperationSchema),
436
- conditionPresent: z.boolean(),
437
- authReferences: z.array(z.string()),
438
- claimReferences: z.array(z.string()),
439
- resourceReferences: z.array(z.string()),
440
- requestResourceReferences: z.array(z.string()),
441
- confidence: ConfidenceSchema,
442
- location: LocationSchema
443
- });
444
- var IndexFieldSchema = z.strictObject({
445
- fieldPath: z.string().min(1),
446
- mode: z.enum(["ASCENDING", "DESCENDING", "ARRAY_CONTAINS", "VECTOR"])
447
- });
448
- var FirestoreIndexSchema = z.strictObject({
449
- collectionGroup: z.string().min(1),
450
- queryScope: z.enum(["COLLECTION", "COLLECTION_GROUP"]),
451
- fields: z.array(IndexFieldSchema),
452
- location: LocationSchema.optional()
453
- });
454
- var GraphEdgeSchema = z.strictObject({
455
- from: z.string().min(1),
456
- to: z.string().min(1),
457
- kind: z.enum([
458
- "CALLS",
459
- "USES_PERMISSION",
460
- "TARGETS_RESOURCE",
461
- "GUARDED_BY_RULE",
462
- "REQUIRES_INDEX",
463
- "REFERENCES_ROLE",
464
- "REFERENCES_CLAIM"
465
- ]),
466
- confidence: ConfidenceSchema,
467
- evidenceIds: z.array(z.string())
468
- });
469
- var ProjectModelSchema = z.strictObject({
470
- schemaVersion: z.literal(1),
471
- rootDir: z.string().min(1),
472
- firebase: z.strictObject({
473
- configFile: z.string().optional(),
474
- projectId: z.string().nullable(),
475
- databaseId: z.string().nullable(),
476
- firestore: z.strictObject({
477
- rulesFile: z.string().optional(),
478
- indexesFile: z.string().optional()
479
- })
480
- }),
481
- authorization: z.strictObject({
482
- adapter: z.string().nullable(),
483
- roles: z.array(RoleSchema),
484
- permissions: z.array(PermissionSchema)
485
- }),
486
- queries: z.array(QueryShapeSchema),
487
- rules: z.array(RuleSchema),
488
- indexes: z.array(FirestoreIndexSchema),
489
- evidence: z.array(EvidenceSchema),
490
- graph: z.array(GraphEdgeSchema)
491
- });
492
- var FindingSchema = z.strictObject({
493
- rule: z.string().min(1),
494
- severity: SeveritySchema,
495
- confidence: ConfidenceSchema,
496
- file: z.string().optional(),
497
- line: z.number().int().positive().optional(),
498
- message: z.string().min(1),
499
- evidence: z.array(EvidenceSchema),
500
- fix: z.string().optional(),
501
- resource: z.string().optional(),
502
- operation: OperationSchema.optional(),
503
- fingerprint: z.string().min(1),
504
- metadata: z.record(z.string(), z.unknown()).optional()
505
- });
506
- var PermissionNameSchema = z.string().regex(/^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*)?$/);
507
- var RoleNameSchema = z.string().regex(/^[a-z][a-z0-9_-]*$/);
508
- var AuditYamlSchema = z.strictObject({
509
- authorization: z.strictObject({
510
- adapter: z.strictObject({ function: z.string().min(1) }).optional(),
511
- roles: z.record(RoleNameSchema, z.strictObject({ permissions: z.array(PermissionNameSchema) })),
512
- permissions: z.record(
513
- PermissionNameSchema,
514
- z.strictObject({ resource: z.string().min(1), operation: OperationSchema })
515
- ),
516
- claims: z.strictObject({
517
- enabled: z.boolean(),
518
- roleKey: z.string().min(1),
519
- samples: z.record(RoleNameSchema, z.record(z.string().min(1), z.json())).optional()
520
- }).optional()
521
- })
522
- });
523
-
524
- // src/scan.ts
525
- import { readFileSync as readFileSync4, existsSync as existsSync3 } from "fs";
526
- import { join as join3 } from "path";
527
- async function scan(rootDir, opts = {}) {
528
- const d = discover(rootDir);
529
- const model = emptyModel(rootDir, d);
530
- if (d.firestoreRulesFile && existsSync3(d.firestoreRulesFile)) {
531
- const rel = d.firestoreRulesFile.replace(/\\/g, "/").startsWith(rootDir.replace(/\\/g, "/")) ? d.firestoreRulesFile.slice(rootDir.length + 1).replace(/\\/g, "/") : model.firebase.firestore.rulesFile ?? "firestore.rules";
532
- model.rules = extractRules(d.firestoreRulesFile, rel);
533
- }
534
- if (d.firestoreIndexesFile && existsSync3(d.firestoreIndexesFile)) {
535
- try {
536
- const raw = JSON.parse(readFileSync4(d.firestoreIndexesFile, "utf-8"));
537
- model.indexes = raw.indexes?.map((i) => ({
538
- collectionGroup: i.collectionGroup ?? "(unknown)",
539
- queryScope: i.queryScope === "COLLECTION_GROUP" ? "COLLECTION_GROUP" : "COLLECTION",
540
- fields: (i.fields ?? []).map((f) => ({
541
- fieldPath: f.fieldPath ?? "(unknown)",
542
- mode: f.arrayConfig ? "ARRAY_CONTAINS" : f.order ?? "ASCENDING"
543
- }))
544
- })) ?? [];
545
- } catch {
546
- }
547
- }
548
- if (d.auditYamlFile && existsSync3(d.auditYamlFile)) {
549
- try {
550
- const text = readFileSync4(d.auditYamlFile, "utf-8");
551
- const parsed = parseSimpleYaml(text);
552
- const validated = AuditYamlSchema.safeParse(parsed);
553
- if (validated.success) {
554
- const auth = validated.data.authorization;
555
- model.authorization.adapter = auth.adapter?.function ?? null;
556
- model.authorization.permissions = Object.entries(auth.permissions).map(([name, p]) => ({
557
- name,
558
- resource: p.resource,
559
- operation: p.operation
560
- }));
561
- model.authorization.roles = Object.entries(auth.roles).map(([name, r]) => ({
562
- name,
563
- permissions: r.permissions
564
- }));
565
- }
566
- } catch {
567
- }
568
- }
569
- const findings = await runChecks(model, rootDir, { adapterFn: opts.adapterFn ?? model.authorization.adapter ?? void 0 });
570
- if (opts.strict) {
571
- const hasContract = model.authorization.permissions.length > 0;
572
- if (!hasContract) {
573
- findings.push({
574
- rule: "CONTRACT_MISSING",
575
- severity: "ERROR",
576
- confidence: "CONFIRMED",
577
- message: "Modo --strict exige firebase-audit.yaml com permissions declaradas.",
578
- fingerprint: "CONTRACT_MISSING",
579
- evidence: [
580
- { id: "ev-strict", kind: "strict-mode", summary: "sem contrato", confidence: "CONFIRMED" }
581
- ],
582
- fix: "Crie firebase-audit.yaml (agente sugere, humano aprova)."
583
- });
584
- }
585
- }
586
- const errors = findings.filter((f) => f.severity === "ERROR").length;
587
- const warnings = findings.filter((f) => f.severity === "WARNING").length;
588
- const infos = findings.filter((f) => f.severity === "INFO").length;
589
- return { model, findings, summary: { errors, warnings, infos, passed: findings.length === 0 ? 10 : 0 } };
590
- }
591
- function parseSimpleYaml(text) {
592
- const lines = text.split("\n");
593
- const root = {};
594
- const stack = [
595
- { indent: -1, obj: root }
596
- ];
597
- let pendingListKey = null;
598
- const cur = () => stack[stack.length - 1].obj;
599
- for (const rawLine of lines) {
600
- const line = rawLine.replace(/\t/g, " ");
601
- if (line.trim() === "" || line.trim().startsWith("#")) continue;
602
- const indent = line.length - line.trimStart().length;
603
- const trimmed = line.trim();
604
- if (trimmed.startsWith("- ")) {
605
- const val = trimmed.slice(2).trim();
606
- if (pendingListKey && indent >= pendingListKey.indent) {
607
- pendingListKey.arr.push(val);
608
- continue;
609
- }
610
- continue;
611
- }
612
- while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
613
- stack.pop();
614
- }
615
- pendingListKey = null;
616
- const colon = trimmed.indexOf(":");
617
- if (colon === -1) continue;
618
- const key = trimmed.slice(0, colon).trim();
619
- const rest = trimmed.slice(colon + 1).trim();
620
- if (rest === "") {
621
- const child = {};
622
- cur()[key] = child;
623
- stack.push({ indent, obj: child, key });
624
- const arr = [];
625
- child.__maybeList = arr;
626
- pendingListKey = { indent: indent + 1, arr };
627
- stack[stack.length - 1].listProbe = arr;
628
- void join3;
629
- } else {
630
- const parent = cur();
631
- delete parent.__maybeList;
632
- parent[key] = rest === "true" ? true : rest === "false" ? false : rest;
633
- }
634
- }
635
- const fix = (o) => {
636
- if (Array.isArray(o)) return o.map(fix);
637
- if (o && typeof o === "object") {
638
- const rec = o;
639
- if (Array.isArray(rec.__maybeList) && Object.keys(rec).length === 1) {
640
- return rec.__maybeList;
641
- }
642
- delete rec.__maybeList;
643
- for (const k of Object.keys(rec)) rec[k] = fix(rec[k]);
644
- return rec;
645
- }
646
- return o;
647
- };
648
- return fix(root);
649
- }
650
-
651
- export {
652
- discover,
653
- classifyOrigin,
654
- emptyModel,
655
- extractRules,
656
- findPublicAllows,
657
- runChecks,
658
- SeveritySchema,
659
- ConfidenceSchema,
660
- OperationSchema,
661
- AccessOriginSchema,
662
- PositionSchema,
663
- LocationSchema,
664
- EvidenceSchema,
665
- DynamicValueSchema,
666
- QueryFilterSchema,
667
- QueryOrderSchema,
668
- QueryShapeSchema,
669
- PermissionSchema,
670
- RoleSchema,
671
- RuleSchema,
672
- IndexFieldSchema,
673
- FirestoreIndexSchema,
674
- GraphEdgeSchema,
675
- ProjectModelSchema,
676
- FindingSchema,
677
- AuditYamlSchema,
678
- scan
679
- };