@dailephd/my-dev-kit-lab 0.4.0 → 0.4.2

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.
Files changed (158) hide show
  1. package/README.md +269 -47
  2. package/dist/scripts/audits/runAudit.js +28 -1
  3. package/dist/scripts/security/validate.js +38 -5
  4. package/dist/src/audits/core/auditConfig.js +16 -1
  5. package/dist/src/audits/core/auditRunner.js +6 -2
  6. package/dist/src/audits/report/auditReportModel.js +7 -0
  7. package/dist/src/audits/report/renderAuditJsonReport.js +1 -0
  8. package/dist/src/audits/report/renderAuditTextReport.js +37 -0
  9. package/dist/src/audits/security/androidAuditIntegration.js +145 -0
  10. package/dist/src/audits/security/mapAndroidSecurityFindingToAuditIssue.js +116 -0
  11. package/dist/src/audits/security/mapSecurityFindingToAuditIssue.js +15 -5
  12. package/dist/src/audits/security/securityAuditAdapter.js +20 -2
  13. package/dist/src/audits/security/securityAuditTypes.js +37 -0
  14. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/analyzeBackupConfiguration.js +300 -0
  15. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/checkResult.js +41 -0
  16. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/index.js +8 -0
  17. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/manifestEvidence.js +102 -0
  18. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/parseBackupRules.js +110 -0
  19. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/types.js +27 -0
  20. package/dist/src/mobile/android/advancedSecurity/candidateEvidence.js +50 -0
  21. package/dist/src/mobile/android/advancedSecurity/clipboard/analyzeClipboardSecurity.js +166 -0
  22. package/dist/src/mobile/android/advancedSecurity/clipboard/checkResult.js +74 -0
  23. package/dist/src/mobile/android/advancedSecurity/clipboard/collectClipboardEvidence.js +82 -0
  24. package/dist/src/mobile/android/advancedSecurity/clipboard/index.js +1 -0
  25. package/dist/src/mobile/android/advancedSecurity/clipboard/types.js +1 -0
  26. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/checkResult.js +255 -0
  27. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/index.js +1 -0
  28. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintSarif.js +37 -0
  29. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintXml.js +36 -0
  30. package/dist/src/mobile/android/advancedSecurity/externalTools/artifacts.js +95 -0
  31. package/dist/src/mobile/android/advancedSecurity/externalTools/boundedOutput.js +33 -0
  32. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/checkResult.js +173 -0
  33. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/command.js +33 -0
  34. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/index.js +1 -0
  35. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/parseDependencyCheckJson.js +133 -0
  36. package/dist/src/mobile/android/advancedSecurity/externalTools/discoverExecutable.js +13 -0
  37. package/dist/src/mobile/android/advancedSecurity/externalTools/index.js +8 -0
  38. package/dist/src/mobile/android/advancedSecurity/externalTools/minimalEnvironment.js +26 -0
  39. package/dist/src/mobile/android/advancedSecurity/externalTools/mutation.js +77 -0
  40. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/checkResult.js +161 -0
  41. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/command.js +25 -0
  42. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/index.js +1 -0
  43. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/parseOsvJson.js +136 -0
  44. package/dist/src/mobile/android/advancedSecurity/externalTools/runBoundedExternalTool.js +51 -0
  45. package/dist/src/mobile/android/advancedSecurity/externalTools/runRequestedAndroidExternalTools.js +54 -0
  46. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/checkResult.js +170 -0
  47. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/command.js +28 -0
  48. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/index.js +2 -0
  49. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/parseSemgrepJson.js +141 -0
  50. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/rules.js +105 -0
  51. package/dist/src/mobile/android/advancedSecurity/externalTools/types.js +107 -0
  52. package/dist/src/mobile/android/advancedSecurity/fileProvider/checkResult.js +188 -0
  53. package/dist/src/mobile/android/advancedSecurity/fileProvider/index.js +1 -0
  54. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/analyzeFirebaseGoogleServices.js +197 -0
  55. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/checkResult.js +77 -0
  56. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/collectFirebaseServiceEvidence.js +193 -0
  57. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/discoverFirebaseArtifacts.js +128 -0
  58. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/index.js +1 -0
  59. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseDatabaseRules.js +55 -0
  60. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirebaseConfig.js +75 -0
  61. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirestoreStorageRules.js +88 -0
  62. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseGoogleServicesConfig.js +55 -0
  63. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/types.js +6 -0
  64. package/dist/src/mobile/android/advancedSecurity/networkSecurity/analyzeNetworkSecurity.js +394 -0
  65. package/dist/src/mobile/android/advancedSecurity/networkSecurity/checkResult.js +47 -0
  66. package/dist/src/mobile/android/advancedSecurity/networkSecurity/deriveEffectiveNetworkPolicy.js +52 -0
  67. package/dist/src/mobile/android/advancedSecurity/networkSecurity/index.js +14 -0
  68. package/dist/src/mobile/android/advancedSecurity/networkSecurity/manifestEvidence.js +109 -0
  69. package/dist/src/mobile/android/advancedSecurity/networkSecurity/parseNetworkSecurityConfig.js +186 -0
  70. package/dist/src/mobile/android/advancedSecurity/networkSecurity/types.js +1 -0
  71. package/dist/src/mobile/android/advancedSecurity/ordering.js +44 -0
  72. package/dist/src/mobile/android/advancedSecurity/redaction.js +54 -0
  73. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/analyzeReleaseConfiguration.js +221 -0
  74. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/checkResult.js +39 -0
  75. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/index.js +7 -0
  76. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/manifestEvidence.js +19 -0
  77. package/dist/src/mobile/android/advancedSecurity/resourceReference.js +69 -0
  78. package/dist/src/mobile/android/advancedSecurity/resourceResolution.js +118 -0
  79. package/dist/src/mobile/android/advancedSecurity/ruleIds.js +141 -0
  80. package/dist/src/mobile/android/advancedSecurity/secretCandidates/analyzeSecretCandidates.js +116 -0
  81. package/dist/src/mobile/android/advancedSecurity/secretCandidates/checkResult.js +80 -0
  82. package/dist/src/mobile/android/advancedSecurity/secretCandidates/discoverSecretSourceFiles.js +152 -0
  83. package/dist/src/mobile/android/advancedSecurity/secretCandidates/index.js +9 -0
  84. package/dist/src/mobile/android/advancedSecurity/secretCandidates/matchSecretCandidates.js +136 -0
  85. package/dist/src/mobile/android/advancedSecurity/secretCandidates/suppressValue.js +61 -0
  86. package/dist/src/mobile/android/advancedSecurity/secretCandidates/types.js +11 -0
  87. package/dist/src/mobile/android/advancedSecurity/sensitiveCategories.js +34 -0
  88. package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifyDirectExpression.js +99 -0
  89. package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifySensitiveIdentifier.js +111 -0
  90. package/dist/src/mobile/android/advancedSecurity/sensitiveData/index.js +3 -0
  91. package/dist/src/mobile/android/advancedSecurity/sensitiveData/localSourceContext.js +90 -0
  92. package/dist/src/mobile/android/advancedSecurity/sensitiveData/splitArguments.js +59 -0
  93. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/analyzeSensitiveLogging.js +117 -0
  94. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/checkResult.js +74 -0
  95. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/collectLoggingEvidence.js +87 -0
  96. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/index.js +1 -0
  97. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/types.js +1 -0
  98. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/analyzeSensitiveStorage.js +182 -0
  99. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/checkResult.js +74 -0
  100. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/collectStorageEvidence.js +109 -0
  101. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/index.js +1 -0
  102. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/types.js +4 -0
  103. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/analyzeSigningConfiguration.js +270 -0
  104. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/checkResult.js +119 -0
  105. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/discoverKeystoreCandidates.js +55 -0
  106. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/extractSigningConfigurations.js +81 -0
  107. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/index.js +8 -0
  108. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/types.js +20 -0
  109. package/dist/src/mobile/android/advancedSecurity/sourceLocation.js +15 -0
  110. package/dist/src/mobile/android/advancedSecurity/webview/checkResult.js +210 -0
  111. package/dist/src/mobile/android/advancedSecurity/webview/index.js +1 -0
  112. package/dist/src/mobile/android/gradle/moduleMetadataExtractor.js +123 -17
  113. package/dist/src/mobile/android/manifest/parseAndroidManifest.js +27 -4
  114. package/dist/src/mobile/android/manifest/xml/parseXml.js +25 -9
  115. package/dist/src/mobile/android/report/model.js +32 -1
  116. package/dist/src/mobile/android/report/renderAndroidReport.js +63 -0
  117. package/dist/src/mobile/android/validate/validateAndroidTarget.js +146 -3
  118. package/dist/src/mobile/android/validation/checkResult.js +7 -0
  119. package/dist/src/securityValidation/commandRunner.js +10 -1
  120. package/dist/src/securityValidation/validate/cliOptions.js +46 -0
  121. package/package.json +1 -1
  122. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/debug/res/xml/ambiguous_backup_rules.xml +4 -0
  123. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/ambiguous_backup_rules.xml +4 -0
  124. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/backup_rules.xml +5 -0
  125. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/broad_backup_rules.xml +4 -0
  126. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/data_extraction_rules.xml +10 -0
  127. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/unsupported_domain_rules.xml +4 -0
  128. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/malformed_backup_rules.xml +4 -0
  129. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/unsupported_root.xml +4 -0
  130. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/debug/res/xml/ambiguous_config.xml +4 -0
  131. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/ambiguous_config.xml +4 -0
  132. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/base_cleartext_false.xml +8 -0
  133. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/broad_and_duplicate_domains.xml +8 -0
  134. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/empty_domain.xml +6 -0
  135. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/malformed_pin.xml +11 -0
  136. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/network_security_config.xml +26 -0
  137. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/malformed_network_security_config.xml +6 -0
  138. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/unsupported_root.xml +4 -0
  139. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-dynamic.gradle +7 -0
  140. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-false.gradle +9 -0
  141. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-true.gradle +13 -0
  142. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-false.gradle.kts +7 -0
  143. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-true.gradle.kts +10 -0
  144. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/missing-release-build-type.gradle.kts +7 -0
  145. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/debug/res/xml/network_security_config.xml +8 -0
  146. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/backup_rules.xml +4 -0
  147. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/network_security_config.xml +8 -0
  148. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-b/src/main/res/xml/malformed_network_security_config.xml +7 -0
  149. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.java +8 -0
  150. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.kt +9 -0
  151. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle +11 -0
  152. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle.kts +11 -0
  153. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/google-services.json +16 -0
  154. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/local.properties +3 -0
  155. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/binary.properties +0 -0
  156. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/oversized.json +1 -0
  157. package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/app/fake-release-keystore.jks +1 -0
  158. package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/build/generated-fake.keystore +1 -0
@@ -0,0 +1,197 @@
1
+ import { makeAndroidFinding } from "../../audit/androidFinding.js";
2
+ import { makeCandidateEvidence } from "../candidateEvidence.js";
3
+ import { buildAndroidSourceLocation } from "../sourceLocation.js";
4
+ import { discoverFirebaseArtifacts } from "./discoverFirebaseArtifacts.js";
5
+ import { parseGoogleServicesConfig } from "./parseGoogleServicesConfig.js";
6
+ import { parseFirebaseJson, parseFirebaseRc } from "./parseFirebaseConfig.js";
7
+ import { parseDatabaseRules } from "./parseDatabaseRules.js";
8
+ import { parseFirestoreOrStorageRules } from "./parseFirestoreStorageRules.js";
9
+ import { collectAnalyticsEventEvidence, collectCollectionSetterEvidence, collectGradleServiceEvidence, collectManifestCollectionMetadata, collectSourceServiceEvidence, } from "./collectFirebaseServiceEvidence.js";
10
+ function fileLocationLine(targetRoot, file, line) {
11
+ return buildAndroidSourceLocation(targetRoot, file.absolutePath, { line });
12
+ }
13
+ export function analyzeFirebaseGoogleServices(targetRoot, modulePaths) {
14
+ const candidates = [];
15
+ const findings = [];
16
+ const discovery = discoverFirebaseArtifacts(targetRoot, modulePaths);
17
+ const limitations = [
18
+ "Bounded static analysis of target-contained local files only; does not contact Firebase or Google services, does not verify deployed rules, and does not evaluate Gradle or resolve dependency selection.",
19
+ ];
20
+ const candidate = (ruleId, file, summary, rawValue, confidence = "medium", resolutionState = "resolved", line) => candidates.push(makeCandidateEvidence({
21
+ ruleId,
22
+ category: "android-firebase-google-services",
23
+ confidence,
24
+ modulePath: file.modulePath,
25
+ location: fileLocationLine(targetRoot, file, line),
26
+ summary,
27
+ rawValue,
28
+ resolutionState,
29
+ staticAnalysisLimitations: limitations,
30
+ }));
31
+ const permissiveFinding = (ruleId, file, title, line, evidenceDetails) => findings.push(makeAndroidFinding({
32
+ ruleId,
33
+ title,
34
+ severity: "major",
35
+ confidence: "high",
36
+ description: `${title}. This reflects a local rules file only; it is not proof of the currently deployed configuration.`,
37
+ manifestPath: file.relativePath,
38
+ identity: `${ruleId}:${line ?? 0}`,
39
+ location: { line },
40
+ evidenceDetails,
41
+ recommendation: "Restrict the rule to authenticated, authorized access before deploying, and verify the deployed rules match the local file.",
42
+ }));
43
+ // --- google-services.json -------------------------------------------------
44
+ const googleServicesConfigs = discovery.googleServicesFiles.map(parseGoogleServicesConfig);
45
+ for (const config of googleServicesConfigs) {
46
+ if (config.malformed) {
47
+ candidate("android-firebase-google-configuration-evidence", config.file, "google-services.json could not be parsed as JSON", undefined, "low", "malformed");
48
+ continue;
49
+ }
50
+ candidate("android-firebase-google-configuration-evidence", config.file, `google-services.json parsed: project=${config.projectId ?? "(none)"}, clients=${config.clients.length}, apiKeys=${config.clients.reduce((n, c) => n + c.apiKeyCount, 0)}`, config.projectId, "low", "not-applicable");
51
+ const manifestPackages = discovery.manifestFiles
52
+ .map((m) => /\bpackage\s*=\s*"([^"]+)"/.exec(m.content)?.[1])
53
+ .filter((v) => Boolean(v));
54
+ if (manifestPackages.length > 0) {
55
+ const matchingClients = config.clients.filter((c) => c.packageName !== undefined && manifestPackages.includes(c.packageName));
56
+ if (matchingClients.length === 0) {
57
+ candidate("android-firebase-google-configuration-evidence", config.file, "No google-services.json client matches the detected application package", undefined, "medium", "unresolved");
58
+ }
59
+ else if (matchingClients.length > 1) {
60
+ candidate("android-firebase-google-configuration-evidence", config.file, "Multiple google-services.json clients match the detected application package", undefined, "medium", "unresolved");
61
+ }
62
+ }
63
+ }
64
+ // --- firebase.json / .firebaserc ------------------------------------------
65
+ const firebaseJsonConfigs = discovery.firebaseJsonFiles.map(parseFirebaseJson);
66
+ for (const config of firebaseJsonConfigs) {
67
+ if (config.malformed) {
68
+ candidate("android-firebase-google-configuration-evidence", config.file, "firebase.json could not be parsed as JSON", undefined, "low", "malformed");
69
+ }
70
+ }
71
+ for (const rc of discovery.firebaseRcFiles.map(parseFirebaseRc)) {
72
+ if (rc.malformed) {
73
+ candidate("android-firebase-google-configuration-evidence", rc.file, ".firebaserc could not be parsed as JSON", undefined, "low", "malformed");
74
+ }
75
+ else if (Object.keys(rc.aliases).length > 0) {
76
+ candidate("android-firebase-google-configuration-evidence", rc.file, `.firebaserc project alias metadata: ${Object.keys(rc.aliases).join(", ")}`, rc.defaultProject, "low", "not-applicable");
77
+ }
78
+ }
79
+ // --- rules resolution (referenced by firebase.json, or conventional names) --
80
+ const referencedPaths = new Set(firebaseJsonConfigs.flatMap((c) => [...c.databaseRulesPaths, ...c.firestoreRulesPaths, ...c.storageRulesPaths]));
81
+ for (const firebaseJson of discovery.firebaseJsonFiles) {
82
+ const config = firebaseJsonConfigs.find((c) => c.file === firebaseJson);
83
+ if (!config)
84
+ continue;
85
+ for (const rulesPath of [...config.databaseRulesPaths, ...config.firestoreRulesPaths, ...config.storageRulesPaths]) {
86
+ const normalized = rulesPath.replace(/^\.?\//, "");
87
+ const resolved = discovery.databaseRulesJsonFiles.find((f) => f.relativePath.endsWith(normalized)) ??
88
+ discovery.rulesFiles.find((f) => f.relativePath.endsWith(normalized));
89
+ if (!resolved) {
90
+ candidate("android-firebase-missing-local-rules", firebaseJson, `firebase.json references "${rulesPath}", but that rules file was not found in the target`, rulesPath, "medium", "missing");
91
+ }
92
+ }
93
+ }
94
+ void referencedPaths;
95
+ // --- Realtime Database rules -----------------------------------------------
96
+ for (const dbRulesFile of discovery.databaseRulesJsonFiles) {
97
+ const result = parseDatabaseRules(dbRulesFile);
98
+ if (result.malformed) {
99
+ candidate("android-firebase-google-configuration-evidence", dbRulesFile, "database.rules.json could not be parsed as JSON", undefined, "low", "malformed");
100
+ continue;
101
+ }
102
+ for (const entry of result.entries) {
103
+ if ((entry.operation === ".read" || entry.operation === ".write") && entry.literalBoolean === true) {
104
+ permissiveFinding("android-firebase-realtime-database-permissive-rules", dbRulesFile, `Realtime Database ${entry.operation} rule at ${entry.jsonPath} is unconditionally true`, undefined, [`path=${entry.jsonPath}`, `operation=${entry.operation}`]);
105
+ }
106
+ else if (entry.operation === ".read" || entry.operation === ".write") {
107
+ candidate("android-firebase-google-configuration-evidence", dbRulesFile, `Realtime Database ${entry.operation} rule at ${entry.jsonPath}: ${entry.conditionSummary}`, entry.conditionSummary, "low", "not-applicable");
108
+ }
109
+ }
110
+ }
111
+ // --- Firestore / Storage rules ----------------------------------------------
112
+ for (const rulesFile of discovery.rulesFiles) {
113
+ const isStorage = /storage\.rules$/i.test(rulesFile.relativePath) || /service\s+firebase\.storage/.test(rulesFile.content);
114
+ const isFirestore = !isStorage && (/firestore\.rules$/i.test(rulesFile.relativePath) || /service\s+cloud\.firestore/.test(rulesFile.content));
115
+ if (!isStorage && !isFirestore)
116
+ continue;
117
+ const service = isStorage ? "storage" : "firestore";
118
+ const result = parseFirestoreOrStorageRules(rulesFile, service);
119
+ const ruleId = isStorage ? "android-firebase-storage-permissive-rules" : "android-firebase-firestore-permissive-rules";
120
+ for (const match of result.matches) {
121
+ if (match.isLiteralTrue && !match.hasAuthCondition) {
122
+ permissiveFinding(ruleId, rulesFile, `${service === "storage" ? "Storage" : "Firestore"} rule at ${match.matchPath} allows ${match.operations.join(", ")} unconditionally`, match.line, [
123
+ `path=${match.matchPath}`,
124
+ `operations=${match.operations.join(",")}`,
125
+ ]);
126
+ }
127
+ else if (match.isTestModeTimeCondition) {
128
+ candidate("android-firebase-google-configuration-evidence", rulesFile, `Time-limited test-mode rule at ${match.matchPath}: ${match.conditionSummary}`, match.conditionSummary, "medium", "not-applicable", match.line);
129
+ }
130
+ else if (!match.hasAuthCondition && !match.isLiteralFalse) {
131
+ candidate("android-firebase-google-configuration-evidence", rulesFile, `Unresolved/complex rule condition at ${match.matchPath}: ${match.conditionSummary}`, match.conditionSummary, "low", "unresolved", match.line);
132
+ }
133
+ }
134
+ }
135
+ // --- Gradle + source service-use evidence -----------------------------------
136
+ const allServiceEvidence = [
137
+ ...discovery.gradleFiles.flatMap(collectGradleServiceEvidence),
138
+ ...discovery.sourceFiles.flatMap(collectSourceServiceEvidence),
139
+ ];
140
+ const servicesUsed = new Set(allServiceEvidence.map((e) => e.service));
141
+ if (allServiceEvidence.length > 0) {
142
+ const gradleFile = discovery.gradleFiles[0] ?? discovery.sourceFiles[0];
143
+ if (gradleFile) {
144
+ candidate("android-firebase-google-configuration-evidence", gradleFile, `Firebase service-use evidence found for: ${[...servicesUsed].sort().join(", ")}`, undefined, "low", "not-applicable");
145
+ }
146
+ }
147
+ const anyRulesResolved = discovery.databaseRulesJsonFiles.length > 0 || discovery.rulesFiles.length > 0;
148
+ const anchorFile = discovery.gradleFiles[0] ?? discovery.sourceFiles[0];
149
+ if (anchorFile) {
150
+ if (servicesUsed.has("database") && discovery.databaseRulesJsonFiles.length === 0) {
151
+ candidate("android-firebase-missing-local-rules", anchorFile, "Realtime Database service use detected but no local database.rules.json was found", undefined, "medium", "missing");
152
+ }
153
+ if (servicesUsed.has("firestore") && !discovery.rulesFiles.some((f) => /firestore\.rules$/i.test(f.relativePath) || /service\s+cloud\.firestore/.test(f.content))) {
154
+ candidate("android-firebase-missing-local-rules", anchorFile, "Firestore service use detected but no local firestore.rules was found", undefined, "medium", "missing");
155
+ }
156
+ if (servicesUsed.has("storage") && !discovery.rulesFiles.some((f) => /storage\.rules$/i.test(f.relativePath) || /service\s+firebase\.storage/.test(f.content))) {
157
+ candidate("android-firebase-missing-local-rules", anchorFile, "Storage service use detected but no local storage.rules was found", undefined, "medium", "missing");
158
+ }
159
+ }
160
+ void anyRulesResolved;
161
+ // --- collection / auto-init metadata -----------------------------------------
162
+ for (const manifest of discovery.manifestFiles) {
163
+ for (const meta of collectManifestCollectionMetadata(manifest)) {
164
+ candidate("android-firebase-google-configuration-evidence", manifest, `${meta.name} manifest metadata is ${meta.state} (${meta.rawValue})`, meta.rawValue, "low", meta.state === "dynamic" ? "unresolved" : "not-applicable", meta.line);
165
+ }
166
+ }
167
+ for (const source of discovery.sourceFiles) {
168
+ for (const setter of collectCollectionSetterEvidence(source)) {
169
+ candidate("android-firebase-google-configuration-evidence", source, `${setter.service} collection setter called with state=${setter.state}`, undefined, "low", setter.state === "dynamic" ? "unresolved" : "not-applicable", setter.line);
170
+ }
171
+ }
172
+ // --- analytics event parameter review ------------------------------------------
173
+ for (const source of discovery.sourceFiles) {
174
+ for (const event of collectAnalyticsEventEvidence(source)) {
175
+ if (event.sensitiveParameterKey === undefined)
176
+ continue;
177
+ if (event.isDirectLiteralValue) {
178
+ findings.push(makeAndroidFinding({
179
+ ruleId: "android-firebase-google-configuration-evidence",
180
+ title: "Direct sensitive value sent as a Firebase Analytics event parameter",
181
+ severity: "major",
182
+ confidence: "high",
183
+ description: "A directly visible sensitive parameter key/value was passed to a Firebase Analytics logEvent() Bundle in the same method. Static source evidence only; does not prove the event was uploaded.",
184
+ manifestPath: source.relativePath,
185
+ identity: `analytics-event:${event.line}`,
186
+ location: { line: event.line },
187
+ evidenceDetails: [`key=${event.sensitiveParameterKey}`],
188
+ recommendation: "Do not send credentials or highly sensitive personal data as analytics event parameters.",
189
+ }));
190
+ }
191
+ else {
192
+ candidate("android-firebase-google-configuration-evidence", source, `Sensitive-looking analytics event parameter key "${event.sensitiveParameterKey}" with a dynamic value`, event.sensitiveParameterValueExpression, "medium", "unresolved", event.line);
193
+ }
194
+ }
195
+ }
196
+ return { candidates, findings };
197
+ }
@@ -0,0 +1,77 @@
1
+ import { sortCandidateEvidence } from "../ordering.js";
2
+ import { analyzeFirebaseGoogleServices } from "./analyzeFirebaseGoogleServices.js";
3
+ import { discoverFirebaseArtifacts } from "./discoverFirebaseArtifacts.js";
4
+ // ---------------------------------------------------------------------------
5
+ // v0.4.1 Batch 6 — standalone Android Firebase/Google services check result.
6
+ // Follows the exact Batch 4/5 standalone pattern: not called from
7
+ // validateAndroidTarget / any active orchestration, not rendered in reports,
8
+ // no CLI effect. Batch 8 integrates.
9
+ // ---------------------------------------------------------------------------
10
+ export const ANDROID_FIREBASE_GOOGLE_SERVICES_AUDIT_CHECK_ID = "android-firebase-google-services-audit";
11
+ export function auditAndroidFirebaseGoogleServices(targetRoot, detection) {
12
+ if (detection.projectKind === "non-android") {
13
+ return {
14
+ id: ANDROID_FIREBASE_GOOGLE_SERVICES_AUDIT_CHECK_ID,
15
+ category: "android-firebase-google-services",
16
+ title: "Android Firebase/Google services audit",
17
+ status: "unsupported",
18
+ requirementLevel: "optional",
19
+ ran: false,
20
+ skipped: true,
21
+ skipInfo: {
22
+ checkId: ANDROID_FIREBASE_GOOGLE_SERVICES_AUDIT_CHECK_ID,
23
+ reason: "Target was not detected as an Android project.",
24
+ requirementLevel: "optional",
25
+ missingCapability: "android-detection",
26
+ verdictImpact: "does not apply to a non-Android target",
27
+ recommendedNextAction: "Re-run against an Android Gradle project, or verify project detection.",
28
+ },
29
+ evidence: [],
30
+ findings: [],
31
+ warnings: [],
32
+ errors: [],
33
+ sourcePaths: [],
34
+ confidence: "unknown",
35
+ environmentRequirements: [],
36
+ candidateEvidence: [],
37
+ };
38
+ }
39
+ const modulePaths = detection.modules.map((m) => m.path);
40
+ const discovery = discoverFirebaseArtifacts(targetRoot, modulePaths);
41
+ const { candidates, findings } = analyzeFirebaseGoogleServices(targetRoot, modulePaths);
42
+ const sortedFindings = [...new Map(findings.map((f) => [f.id, f])).values()].sort((a, b) => a.id.localeCompare(b.id));
43
+ const candidateEvidence = sortCandidateEvidence([...new Map(candidates.map((c) => [c.id, c])).values()]);
44
+ const hasReportableFinding = sortedFindings.some((f) => f.severity !== "informational");
45
+ const sourcePaths = [
46
+ ...discovery.googleServicesFiles,
47
+ ...discovery.firebaseJsonFiles,
48
+ ...discovery.firebaseRcFiles,
49
+ ...discovery.databaseRulesJsonFiles,
50
+ ...discovery.rulesFiles,
51
+ ...discovery.gradleFiles,
52
+ ...discovery.sourceFiles,
53
+ ...discovery.manifestFiles,
54
+ ].map((f) => f.relativePath);
55
+ return {
56
+ id: ANDROID_FIREBASE_GOOGLE_SERVICES_AUDIT_CHECK_ID,
57
+ category: "android-firebase-google-services",
58
+ title: "Android Firebase/Google services audit",
59
+ status: hasReportableFinding ? "failed" : "passed",
60
+ requirementLevel: "required",
61
+ ran: true,
62
+ skipped: false,
63
+ evidence: [
64
+ `${discovery.googleServicesFiles.length} google-services.json, ${discovery.firebaseJsonFiles.length} firebase.json, ${discovery.databaseRulesJsonFiles.length + discovery.rulesFiles.length} rules file(s) found`,
65
+ ],
66
+ findings: sortedFindings,
67
+ warnings: [
68
+ ...discovery.skipped.map((s) => `${s.relativePath}: skipped (${s.reason}${s.detail ? `: ${s.detail}` : ""})`),
69
+ "Static analysis only: local configuration/rules files are not proof of the deployed Firebase project configuration; no remote Firebase or Google service was contacted.",
70
+ ],
71
+ errors: [],
72
+ sourcePaths,
73
+ confidence: discovery.skipped.length > 0 ? "medium" : "high",
74
+ environmentRequirements: [],
75
+ candidateEvidence,
76
+ };
77
+ }
@@ -0,0 +1,193 @@
1
+ import { executableMask, lineForOffset, methodScopes, scopeAt } from "../sensitiveData/localSourceContext.js";
2
+ import { splitTopLevelArguments } from "../sensitiveData/splitArguments.js";
3
+ import { classifyDirectExpression } from "../sensitiveData/classifyDirectExpression.js";
4
+ import { classifySensitiveIdentifier } from "../sensitiveData/classifySensitiveIdentifier.js";
5
+ // ---------------------------------------------------------------------------
6
+ // v0.4.1 Batch 6 — bounded Firebase/Google service-use, collection-metadata,
7
+ // and analytics-event evidence collection. Purely lexical: Gradle files are
8
+ // never evaluated, no dependency graph is resolved, and no Bundle/object
9
+ // contents are inferred beyond direct same-method arguments.
10
+ // ---------------------------------------------------------------------------
11
+ const GRADLE_PLUGIN = /\bid\s*\(?["']([\w.-]*google-services|com\.google\.firebase\.crashlytics)["']\)?/g;
12
+ const GRADLE_DEPENDENCY = /["'](?:com\.google\.firebase:)?(firebase-(?:database|firestore|storage|auth|analytics|crashlytics|messaging|perf))[:"']/g;
13
+ // Gradle dependency/plugin coordinates are always string literals, unlike
14
+ // Java/Kotlin executable calls — masking string *contents* (executableMask)
15
+ // would blank out exactly the text this scanner needs to match. Only
16
+ // comments are masked here; string literals are left intact.
17
+ const COMMENT_ONLY = /\/\/[^\n]*|\/\*[\s\S]*?\*\//g;
18
+ function maskCommentsOnly(source) {
19
+ return source.replace(COMMENT_ONLY, (match) => match.replace(/[^\n]/g, " "));
20
+ }
21
+ const SERVICE_BY_ARTIFACT = {
22
+ "firebase-database": "database",
23
+ "firebase-firestore": "firestore",
24
+ "firebase-storage": "storage",
25
+ "firebase-auth": "auth",
26
+ "firebase-analytics": "analytics",
27
+ "firebase-crashlytics": "crashlytics",
28
+ "firebase-messaging": "messaging",
29
+ "firebase-perf": "performance",
30
+ };
31
+ const SOURCE_USAGE = [
32
+ [/\bFirebaseDatabase\b/, "database"],
33
+ [/\bFirebaseFirestore\b/, "firestore"],
34
+ [/\bFirebaseStorage\b/, "storage"],
35
+ [/\bFirebaseAuth\b/, "auth"],
36
+ [/\bFirebaseAnalytics\b/, "analytics"],
37
+ [/\bFirebaseCrashlytics\b/, "crashlytics"],
38
+ [/\bFirebaseMessaging(?:Service)?\b/, "messaging"],
39
+ [/\bFirebasePerformance\b/, "performance"],
40
+ ];
41
+ export function collectGradleServiceEvidence(file) {
42
+ const evidence = [];
43
+ const mask = maskCommentsOnly(file.content);
44
+ for (const match of mask.matchAll(GRADLE_PLUGIN)) {
45
+ const offset = match.index ?? 0;
46
+ evidence.push({
47
+ service: match[1].includes("crashlytics") ? "crashlytics" : "database",
48
+ source: "gradle-plugin",
49
+ detail: match[1],
50
+ relativePath: file.relativePath,
51
+ modulePath: file.modulePath,
52
+ line: lineForOffset(file.content, offset),
53
+ });
54
+ }
55
+ for (const match of mask.matchAll(GRADLE_DEPENDENCY)) {
56
+ const offset = match.index ?? 0;
57
+ const service = SERVICE_BY_ARTIFACT[match[1]];
58
+ if (!service)
59
+ continue;
60
+ evidence.push({
61
+ service,
62
+ source: "gradle-dependency",
63
+ detail: match[1],
64
+ relativePath: file.relativePath,
65
+ modulePath: file.modulePath,
66
+ line: lineForOffset(file.content, offset),
67
+ });
68
+ }
69
+ return evidence;
70
+ }
71
+ export function collectSourceServiceEvidence(file) {
72
+ const evidence = [];
73
+ const mask = executableMask(file.content);
74
+ for (const [pattern, service] of SOURCE_USAGE) {
75
+ const re = new RegExp(pattern.source, "g");
76
+ for (const match of mask.matchAll(re)) {
77
+ const offset = match.index ?? 0;
78
+ evidence.push({
79
+ service,
80
+ source: /^import\b/.test(mask.slice(Math.max(0, offset - 8), offset)) ? "source-import" : "source-usage",
81
+ detail: match[0],
82
+ relativePath: file.relativePath,
83
+ modulePath: file.modulePath,
84
+ line: lineForOffset(file.content, offset),
85
+ });
86
+ }
87
+ }
88
+ return evidence;
89
+ }
90
+ const COLLECTION_SETTER = /\b(setAnalyticsCollectionEnabled|setCrashlyticsCollectionEnabled|setAutoInitEnabled)\s*\(([^;\n]*?)\)/gd;
91
+ function realGroup(content, match, group) {
92
+ const span = match.indices?.[group];
93
+ return span ? content.slice(span[0], span[1]) : match[group];
94
+ }
95
+ const SETTER_SERVICE = {
96
+ setAnalyticsCollectionEnabled: "analytics",
97
+ setCrashlyticsCollectionEnabled: "crashlytics",
98
+ setAutoInitEnabled: "messaging",
99
+ };
100
+ function stateOf(argument) {
101
+ if (argument === undefined)
102
+ return "dynamic";
103
+ const classification = classifyDirectExpression(argument);
104
+ if (classification.kind === "boolean-literal")
105
+ return argument.trim() === "true" ? "true" : "false";
106
+ return "dynamic";
107
+ }
108
+ export function collectCollectionSetterEvidence(file) {
109
+ const mask = executableMask(file.content);
110
+ const evidence = [];
111
+ for (const match of mask.matchAll(COLLECTION_SETTER)) {
112
+ const offset = match.index ?? 0;
113
+ const args = splitTopLevelArguments(realGroup(file.content, match, 2) ?? "");
114
+ evidence.push({
115
+ service: SETTER_SERVICE[match[1]],
116
+ state: stateOf(args[0]),
117
+ relativePath: file.relativePath,
118
+ modulePath: file.modulePath,
119
+ line: lineForOffset(file.content, offset),
120
+ });
121
+ }
122
+ return evidence;
123
+ }
124
+ const META_DATA_TAG = /<meta-data\b[^>]*\/?>(?:\s*<\/meta-data>)?/g;
125
+ const NAME_ATTR = /android:name\s*=\s*"([^"]*)"/;
126
+ const VALUE_ATTR = /android:value\s*=\s*"([^"]*)"/;
127
+ const COLLECTION_META_NAMES = new Set([
128
+ "firebase_analytics_collection_enabled",
129
+ "firebase_crashlytics_collection_enabled",
130
+ "firebase_messaging_auto_init_enabled",
131
+ "firebase_performance_collection_enabled",
132
+ ]);
133
+ export function collectManifestCollectionMetadata(file) {
134
+ const evidence = [];
135
+ for (const match of file.content.matchAll(META_DATA_TAG)) {
136
+ const tag = match[0];
137
+ const name = NAME_ATTR.exec(tag)?.[1];
138
+ if (!name || !COLLECTION_META_NAMES.has(name))
139
+ continue;
140
+ const rawValue = VALUE_ATTR.exec(tag)?.[1];
141
+ const state = rawValue === "true" ? "true" : rawValue === "false" ? "false" : "dynamic";
142
+ evidence.push({
143
+ name,
144
+ rawValue: rawValue ?? "(missing)",
145
+ state,
146
+ relativePath: file.relativePath,
147
+ line: lineForOffset(file.content, match.index ?? 0),
148
+ });
149
+ }
150
+ return evidence;
151
+ }
152
+ const LOG_EVENT_CALL = /\b(?:FirebaseAnalytics|analytics)\.getInstance\([^)]*\)\.logEvent\s*\(([^;\n]*?)\)|\blogEvent\s*\(([^;\n]*?)\)/gd;
153
+ const BUNDLE_PUT = /([A-Za-z_][\w.]*)\.(putString|putLong|putDouble|putBoolean)\s*\(([^;\n]*?)\)/gd;
154
+ export function collectAnalyticsEventEvidence(file) {
155
+ const mask = executableMask(file.content);
156
+ const scopes = methodScopes(mask);
157
+ const evidence = [];
158
+ const bundlePuts = [];
159
+ for (const match of mask.matchAll(BUNDLE_PUT)) {
160
+ const offset = match.index ?? 0;
161
+ const args = splitTopLevelArguments(realGroup(file.content, match, 3) ?? "");
162
+ bundlePuts.push({ key: args[0], value: args[1], offset, scopeId: scopeAt(scopes, offset)?.id });
163
+ }
164
+ for (const match of mask.matchAll(LOG_EVENT_CALL)) {
165
+ const offset = match.index ?? 0;
166
+ const rawArgs = realGroup(file.content, match, 1) ?? realGroup(file.content, match, 2) ?? "";
167
+ const args = splitTopLevelArguments(rawArgs);
168
+ const scopeId = scopeAt(scopes, offset)?.id;
169
+ let sensitiveKey;
170
+ let sensitiveValue;
171
+ let isDirectLiteral = false;
172
+ for (const put of bundlePuts.filter((p) => p.scopeId === scopeId)) {
173
+ const literal = put.key !== undefined && classifyDirectExpression(put.key).kind === "string-literal" ? classifyDirectExpression(put.key).literalValue : undefined;
174
+ const classification = literal !== undefined ? classifySensitiveIdentifier(literal) : undefined;
175
+ if (classification) {
176
+ sensitiveKey = put.key;
177
+ sensitiveValue = put.value;
178
+ isDirectLiteral = put.value !== undefined && classifyDirectExpression(put.value).kind === "string-literal";
179
+ break;
180
+ }
181
+ }
182
+ evidence.push({
183
+ eventNameExpression: args[0],
184
+ sensitiveParameterKey: sensitiveKey,
185
+ sensitiveParameterValueExpression: sensitiveValue,
186
+ isDirectLiteralValue: isDirectLiteral,
187
+ relativePath: file.relativePath,
188
+ modulePath: file.modulePath,
189
+ line: lineForOffset(file.content, offset),
190
+ });
191
+ }
192
+ return evidence;
193
+ }
@@ -0,0 +1,128 @@
1
+ import fs from "node:fs";
2
+ import { collectFilesForGlobs } from "../../../../core/fileGlobs.js";
3
+ import { discoverSecretSourceFiles, MAX_SECRET_SCAN_FILE_BYTES } from "../secretCandidates/discoverSecretSourceFiles.js";
4
+ // ---------------------------------------------------------------------------
5
+ // v0.4.1 Batch 6 — bounded, target-contained discovery of Firebase/Google
6
+ // configuration and rules artifacts.
7
+ //
8
+ // google-services.json and firebase.json are already covered by Batch 4's
9
+ // discoverSecretSourceFiles (its glob list includes **/*.json), so those are
10
+ // reused directly rather than re-discovered. .firebaserc and *.rules files
11
+ // are not covered by that glob list (dotfiles and a non-listed extension),
12
+ // so a small supplemental scan reuses collectFilesForGlobs (the same
13
+ // target-containment primitive discoverSecretSourceFiles itself wraps)
14
+ // rather than introducing a second traversal engine.
15
+ // ---------------------------------------------------------------------------
16
+ const NUL_CHAR = String.fromCharCode(0);
17
+ function readSupplemental(targetRoot, globs, knownModulePaths) {
18
+ const files = [];
19
+ const skipped = [];
20
+ const matches = [];
21
+ const seen = new Set();
22
+ for (const glob of globs) {
23
+ let globMatches;
24
+ try {
25
+ globMatches = collectFilesForGlobs(targetRoot, [glob]);
26
+ }
27
+ catch {
28
+ continue;
29
+ }
30
+ for (const match of globMatches) {
31
+ if (seen.has(match.relativePath))
32
+ continue;
33
+ seen.add(match.relativePath);
34
+ matches.push(match);
35
+ }
36
+ }
37
+ for (const match of matches) {
38
+ const relativePath = match.relativePath.replace(/\\/g, "/");
39
+ if (relativePath.includes("node_modules/") || relativePath.includes(".gradle/") || relativePath.includes("build/"))
40
+ continue;
41
+ let stat;
42
+ try {
43
+ stat = fs.statSync(match.absolutePath);
44
+ }
45
+ catch (error) {
46
+ skipped.push({ relativePath, reason: "unreadable", detail: error instanceof Error ? error.message : undefined });
47
+ continue;
48
+ }
49
+ if (!stat.isFile())
50
+ continue;
51
+ if (stat.size > MAX_SECRET_SCAN_FILE_BYTES) {
52
+ skipped.push({ relativePath, reason: "oversized", detail: `${stat.size} bytes exceeds ${MAX_SECRET_SCAN_FILE_BYTES}` });
53
+ continue;
54
+ }
55
+ let content;
56
+ try {
57
+ content = fs.readFileSync(match.absolutePath, "utf8");
58
+ }
59
+ catch (error) {
60
+ skipped.push({ relativePath, reason: "unreadable", detail: error instanceof Error ? error.message : undefined });
61
+ continue;
62
+ }
63
+ if (content.indexOf(NUL_CHAR) !== -1) {
64
+ skipped.push({ relativePath, reason: "binary-like" });
65
+ continue;
66
+ }
67
+ const modulePath = knownModulePaths.find((m) => relativePath.startsWith(`${m}/`));
68
+ files.push({ relativePath, absolutePath: match.absolutePath, modulePath, content });
69
+ }
70
+ return { files, skipped };
71
+ }
72
+ export function discoverFirebaseArtifacts(targetRoot, knownModulePaths = []) {
73
+ const { files, skipped } = discoverSecretSourceFiles(targetRoot, knownModulePaths);
74
+ const googleServicesFiles = [];
75
+ const firebaseJsonFiles = [];
76
+ const databaseRulesJsonFiles = [];
77
+ const gradleFiles = [];
78
+ const sourceFiles = [];
79
+ const manifestFiles = [];
80
+ const foundRelativePaths = new Set();
81
+ for (const file of files) {
82
+ const lower = file.relativePath.toLowerCase();
83
+ foundRelativePaths.add(file.relativePath);
84
+ if (lower.endsWith("google-services.json"))
85
+ googleServicesFiles.push(file);
86
+ else if (lower.endsWith("/firebase.json") || lower === "firebase.json")
87
+ firebaseJsonFiles.push(file);
88
+ else if (lower.endsWith("database.rules.json"))
89
+ databaseRulesJsonFiles.push(file);
90
+ else if (/\.(gradle|gradle\.kts)$/.test(lower))
91
+ gradleFiles.push(file);
92
+ else if (/\.(java|kt)$/.test(lower))
93
+ sourceFiles.push(file);
94
+ else if (lower.endsWith("androidmanifest.xml"))
95
+ manifestFiles.push(file);
96
+ }
97
+ // Root-level filenames (e.g. a repo-root database.rules.json) are not
98
+ // matched by "**/*.json"-style globs, which require at least one path
99
+ // separator (the same limitation Batch 4's discovery module documents) —
100
+ // explicit root-level literals fill that gap for Firebase's well-known
101
+ // conventional filenames, deduplicated against what discoverSecretSourceFiles
102
+ // already found.
103
+ const rc = readSupplemental(targetRoot, ["**/.firebaserc", ".firebaserc"], knownModulePaths);
104
+ const rules = readSupplemental(targetRoot, ["**/*.rules", "firestore.rules", "storage.rules", "database.rules"], knownModulePaths);
105
+ const rootJson = readSupplemental(targetRoot, ["google-services.json", "firebase.json", "database.rules.json"], knownModulePaths);
106
+ for (const file of rootJson.files) {
107
+ if (foundRelativePaths.has(file.relativePath))
108
+ continue;
109
+ const lower = file.relativePath.toLowerCase();
110
+ if (lower.endsWith("google-services.json"))
111
+ googleServicesFiles.push(file);
112
+ else if (lower.endsWith("firebase.json"))
113
+ firebaseJsonFiles.push(file);
114
+ else if (lower.endsWith("database.rules.json"))
115
+ databaseRulesJsonFiles.push(file);
116
+ }
117
+ return {
118
+ googleServicesFiles,
119
+ firebaseJsonFiles,
120
+ firebaseRcFiles: rc.files,
121
+ databaseRulesJsonFiles,
122
+ rulesFiles: rules.files,
123
+ gradleFiles,
124
+ sourceFiles,
125
+ manifestFiles,
126
+ skipped: [...skipped, ...rc.skipped, ...rules.skipped, ...rootJson.skipped],
127
+ };
128
+ }
@@ -0,0 +1 @@
1
+ export { auditAndroidFirebaseGoogleServices, ANDROID_FIREBASE_GOOGLE_SERVICES_AUDIT_CHECK_ID } from "./checkResult.js";
@@ -0,0 +1,55 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.1 Batch 6 — bounded local Realtime Database rules parser.
3
+ //
4
+ // Recursively walks the parsed JSON tree collecting .read/.write/.validate/
5
+ // .indexOn entries with their JSON path. This is not the Firebase rules
6
+ // evaluator: no runtime auth is evaluated, and a local rules file is never
7
+ // treated as proof of the deployed configuration.
8
+ // ---------------------------------------------------------------------------
9
+ const MAX_DEPTH = 40;
10
+ const MAX_NODES = 20_000;
11
+ const MAX_CONDITION_LENGTH = 200;
12
+ const RULE_KEYS = new Set([".read", ".write", ".validate", ".indexOn"]);
13
+ function summarize(value) {
14
+ const text = typeof value === "string" ? value : JSON.stringify(value);
15
+ return text.length > MAX_CONDITION_LENGTH ? `${text.slice(0, MAX_CONDITION_LENGTH)}...` : text;
16
+ }
17
+ export function parseDatabaseRules(file) {
18
+ let parsed;
19
+ try {
20
+ parsed = JSON.parse(file.content);
21
+ }
22
+ catch {
23
+ return { file, malformed: true, entries: [], boundsExceeded: false };
24
+ }
25
+ const root = parsed && typeof parsed === "object" ? parsed.rules ?? parsed : parsed;
26
+ const entries = [];
27
+ let nodeCount = 0;
28
+ let boundsExceeded = false;
29
+ function walk(node, jsonPath, depth) {
30
+ if (boundsExceeded)
31
+ return;
32
+ nodeCount += 1;
33
+ if (nodeCount > MAX_NODES || depth > MAX_DEPTH) {
34
+ boundsExceeded = true;
35
+ return;
36
+ }
37
+ if (node === null || typeof node !== "object" || Array.isArray(node))
38
+ return;
39
+ for (const [key, value] of Object.entries(node)) {
40
+ if (RULE_KEYS.has(key)) {
41
+ entries.push({
42
+ jsonPath,
43
+ operation: key,
44
+ literalBoolean: typeof value === "boolean" ? value : undefined,
45
+ conditionSummary: summarize(value),
46
+ depth,
47
+ });
48
+ continue;
49
+ }
50
+ walk(value, jsonPath === "/" ? `/${key}` : `${jsonPath}/${key}`, depth + 1);
51
+ }
52
+ }
53
+ walk(root, "/", 0);
54
+ return { file, malformed: false, entries, boundsExceeded };
55
+ }