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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +10 -61
  2. package/dist/scripts/security/validate.js +38 -5
  3. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/analyzeBackupConfiguration.js +300 -0
  4. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/checkResult.js +41 -0
  5. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/index.js +8 -0
  6. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/manifestEvidence.js +102 -0
  7. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/parseBackupRules.js +110 -0
  8. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/types.js +27 -0
  9. package/dist/src/mobile/android/advancedSecurity/candidateEvidence.js +50 -0
  10. package/dist/src/mobile/android/advancedSecurity/clipboard/analyzeClipboardSecurity.js +166 -0
  11. package/dist/src/mobile/android/advancedSecurity/clipboard/checkResult.js +74 -0
  12. package/dist/src/mobile/android/advancedSecurity/clipboard/collectClipboardEvidence.js +82 -0
  13. package/dist/src/mobile/android/advancedSecurity/clipboard/index.js +1 -0
  14. package/dist/src/mobile/android/advancedSecurity/clipboard/types.js +1 -0
  15. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/checkResult.js +255 -0
  16. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/index.js +1 -0
  17. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintSarif.js +37 -0
  18. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintXml.js +36 -0
  19. package/dist/src/mobile/android/advancedSecurity/externalTools/artifacts.js +95 -0
  20. package/dist/src/mobile/android/advancedSecurity/externalTools/boundedOutput.js +33 -0
  21. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/checkResult.js +173 -0
  22. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/command.js +33 -0
  23. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/index.js +1 -0
  24. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/parseDependencyCheckJson.js +133 -0
  25. package/dist/src/mobile/android/advancedSecurity/externalTools/discoverExecutable.js +13 -0
  26. package/dist/src/mobile/android/advancedSecurity/externalTools/index.js +8 -0
  27. package/dist/src/mobile/android/advancedSecurity/externalTools/minimalEnvironment.js +26 -0
  28. package/dist/src/mobile/android/advancedSecurity/externalTools/mutation.js +77 -0
  29. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/checkResult.js +161 -0
  30. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/command.js +25 -0
  31. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/index.js +1 -0
  32. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/parseOsvJson.js +136 -0
  33. package/dist/src/mobile/android/advancedSecurity/externalTools/runBoundedExternalTool.js +51 -0
  34. package/dist/src/mobile/android/advancedSecurity/externalTools/runRequestedAndroidExternalTools.js +54 -0
  35. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/checkResult.js +170 -0
  36. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/command.js +28 -0
  37. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/index.js +2 -0
  38. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/parseSemgrepJson.js +141 -0
  39. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/rules.js +105 -0
  40. package/dist/src/mobile/android/advancedSecurity/externalTools/types.js +107 -0
  41. package/dist/src/mobile/android/advancedSecurity/fileProvider/checkResult.js +188 -0
  42. package/dist/src/mobile/android/advancedSecurity/fileProvider/index.js +1 -0
  43. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/analyzeFirebaseGoogleServices.js +197 -0
  44. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/checkResult.js +77 -0
  45. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/collectFirebaseServiceEvidence.js +193 -0
  46. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/discoverFirebaseArtifacts.js +128 -0
  47. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/index.js +1 -0
  48. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseDatabaseRules.js +55 -0
  49. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirebaseConfig.js +75 -0
  50. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirestoreStorageRules.js +88 -0
  51. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseGoogleServicesConfig.js +55 -0
  52. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/types.js +6 -0
  53. package/dist/src/mobile/android/advancedSecurity/networkSecurity/analyzeNetworkSecurity.js +394 -0
  54. package/dist/src/mobile/android/advancedSecurity/networkSecurity/checkResult.js +47 -0
  55. package/dist/src/mobile/android/advancedSecurity/networkSecurity/deriveEffectiveNetworkPolicy.js +52 -0
  56. package/dist/src/mobile/android/advancedSecurity/networkSecurity/index.js +14 -0
  57. package/dist/src/mobile/android/advancedSecurity/networkSecurity/manifestEvidence.js +109 -0
  58. package/dist/src/mobile/android/advancedSecurity/networkSecurity/parseNetworkSecurityConfig.js +186 -0
  59. package/dist/src/mobile/android/advancedSecurity/networkSecurity/types.js +1 -0
  60. package/dist/src/mobile/android/advancedSecurity/ordering.js +44 -0
  61. package/dist/src/mobile/android/advancedSecurity/redaction.js +54 -0
  62. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/analyzeReleaseConfiguration.js +221 -0
  63. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/checkResult.js +39 -0
  64. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/index.js +7 -0
  65. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/manifestEvidence.js +19 -0
  66. package/dist/src/mobile/android/advancedSecurity/resourceReference.js +69 -0
  67. package/dist/src/mobile/android/advancedSecurity/resourceResolution.js +118 -0
  68. package/dist/src/mobile/android/advancedSecurity/ruleIds.js +141 -0
  69. package/dist/src/mobile/android/advancedSecurity/secretCandidates/analyzeSecretCandidates.js +116 -0
  70. package/dist/src/mobile/android/advancedSecurity/secretCandidates/checkResult.js +80 -0
  71. package/dist/src/mobile/android/advancedSecurity/secretCandidates/discoverSecretSourceFiles.js +152 -0
  72. package/dist/src/mobile/android/advancedSecurity/secretCandidates/index.js +9 -0
  73. package/dist/src/mobile/android/advancedSecurity/secretCandidates/matchSecretCandidates.js +136 -0
  74. package/dist/src/mobile/android/advancedSecurity/secretCandidates/suppressValue.js +61 -0
  75. package/dist/src/mobile/android/advancedSecurity/secretCandidates/types.js +11 -0
  76. package/dist/src/mobile/android/advancedSecurity/sensitiveCategories.js +34 -0
  77. package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifyDirectExpression.js +99 -0
  78. package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifySensitiveIdentifier.js +111 -0
  79. package/dist/src/mobile/android/advancedSecurity/sensitiveData/index.js +3 -0
  80. package/dist/src/mobile/android/advancedSecurity/sensitiveData/localSourceContext.js +90 -0
  81. package/dist/src/mobile/android/advancedSecurity/sensitiveData/splitArguments.js +59 -0
  82. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/analyzeSensitiveLogging.js +117 -0
  83. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/checkResult.js +74 -0
  84. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/collectLoggingEvidence.js +87 -0
  85. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/index.js +1 -0
  86. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/types.js +1 -0
  87. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/analyzeSensitiveStorage.js +182 -0
  88. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/checkResult.js +74 -0
  89. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/collectStorageEvidence.js +109 -0
  90. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/index.js +1 -0
  91. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/types.js +4 -0
  92. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/analyzeSigningConfiguration.js +270 -0
  93. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/checkResult.js +119 -0
  94. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/discoverKeystoreCandidates.js +55 -0
  95. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/extractSigningConfigurations.js +81 -0
  96. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/index.js +8 -0
  97. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/types.js +20 -0
  98. package/dist/src/mobile/android/advancedSecurity/sourceLocation.js +15 -0
  99. package/dist/src/mobile/android/advancedSecurity/webview/checkResult.js +210 -0
  100. package/dist/src/mobile/android/advancedSecurity/webview/index.js +1 -0
  101. package/dist/src/mobile/android/gradle/moduleMetadataExtractor.js +123 -17
  102. package/dist/src/mobile/android/manifest/parseAndroidManifest.js +27 -4
  103. package/dist/src/mobile/android/manifest/xml/parseXml.js +25 -9
  104. package/dist/src/mobile/android/report/model.js +32 -1
  105. package/dist/src/mobile/android/report/renderAndroidReport.js +63 -0
  106. package/dist/src/mobile/android/validate/validateAndroidTarget.js +146 -3
  107. package/dist/src/mobile/android/validation/checkResult.js +7 -0
  108. package/dist/src/securityValidation/commandRunner.js +10 -1
  109. package/dist/src/securityValidation/validate/cliOptions.js +46 -0
  110. package/package.json +1 -1
  111. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/debug/res/xml/ambiguous_backup_rules.xml +4 -0
  112. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/ambiguous_backup_rules.xml +4 -0
  113. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/backup_rules.xml +5 -0
  114. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/broad_backup_rules.xml +4 -0
  115. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/data_extraction_rules.xml +10 -0
  116. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/unsupported_domain_rules.xml +4 -0
  117. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/malformed_backup_rules.xml +4 -0
  118. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/unsupported_root.xml +4 -0
  119. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/debug/res/xml/ambiguous_config.xml +4 -0
  120. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/ambiguous_config.xml +4 -0
  121. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/base_cleartext_false.xml +8 -0
  122. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/broad_and_duplicate_domains.xml +8 -0
  123. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/empty_domain.xml +6 -0
  124. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/malformed_pin.xml +11 -0
  125. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/network_security_config.xml +26 -0
  126. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/malformed_network_security_config.xml +6 -0
  127. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/unsupported_root.xml +4 -0
  128. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-dynamic.gradle +7 -0
  129. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-false.gradle +9 -0
  130. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-true.gradle +13 -0
  131. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-false.gradle.kts +7 -0
  132. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-true.gradle.kts +10 -0
  133. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/missing-release-build-type.gradle.kts +7 -0
  134. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/debug/res/xml/network_security_config.xml +8 -0
  135. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/backup_rules.xml +4 -0
  136. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/network_security_config.xml +8 -0
  137. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-b/src/main/res/xml/malformed_network_security_config.xml +7 -0
  138. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.java +8 -0
  139. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.kt +9 -0
  140. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle +11 -0
  141. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle.kts +11 -0
  142. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/google-services.json +16 -0
  143. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/local.properties +3 -0
  144. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/binary.properties +0 -0
  145. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/oversized.json +1 -0
  146. package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/app/fake-release-keystore.jks +1 -0
  147. package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/build/generated-fake.keystore +1 -0
@@ -0,0 +1,4 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.1 Batch 6 — sensitive-storage evidence contracts.
3
+ // ---------------------------------------------------------------------------
4
+ export {};
@@ -0,0 +1,270 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { makeAndroidFinding } from "../../audit/androidFinding.js";
4
+ import { buildAndroidSourceLocation } from "../sourceLocation.js";
5
+ import { makeCandidateEvidence } from "../candidateEvidence.js";
6
+ import { extractSigningConfigurations } from "./extractSigningConfigurations.js";
7
+ function loc(targetRoot, absolutePath, position) {
8
+ return buildAndroidSourceLocation(targetRoot, absolutePath, position);
9
+ }
10
+ function analyzeCredential(fieldLabel, value, configName, modulePath, location, buildFileRelativePath) {
11
+ if (value.state === "literal" && value.fingerprint !== undefined) {
12
+ return {
13
+ candidates: [],
14
+ findings: [
15
+ makeAndroidFinding({
16
+ ruleId: "android-signing-password-literal",
17
+ title: `Literal ${fieldLabel} in signing configuration "${configName}"`,
18
+ severity: "major",
19
+ confidence: "high",
20
+ description: `The signing configuration "${configName}" sets a literal ${fieldLabel} value. This is high-confidence static evidence of a hardcoded signing credential; it is not proof the credential unlocks any specific keystore.`,
21
+ manifestPath: buildFileRelativePath,
22
+ identity: value.fingerprint,
23
+ location: { line: location.line },
24
+ evidenceDetails: [`signingConfig=${configName}`, `field=${fieldLabel}`, `redactedPreview=${value.redactedPreview}`],
25
+ recommendation: "Remove the literal credential and load it from an environment variable or a local, untracked properties file instead.",
26
+ }),
27
+ ],
28
+ evidenceText: [],
29
+ hasLiteral: true,
30
+ };
31
+ }
32
+ if (value.state === "environment-reference" || value.state === "gradle-property-reference" || value.state === "local-property-reference") {
33
+ return {
34
+ candidates: [],
35
+ findings: [],
36
+ evidenceText: [`${configName}.${fieldLabel} is a ${value.state} (informational, not a hardcoded credential)`],
37
+ hasLiteral: false,
38
+ };
39
+ }
40
+ if (value.state === "variable-reference" || value.state === "method-call" || value.state === "dynamic") {
41
+ return {
42
+ candidates: [
43
+ makeCandidateEvidence({
44
+ ruleId: "android-signing-password-literal",
45
+ category: "android-signing-configuration",
46
+ confidence: "low",
47
+ modulePath,
48
+ location,
49
+ summary: `Signing configuration "${configName}"'s ${fieldLabel} is a dynamic/unresolved expression`,
50
+ rawValue: value.rawExpression,
51
+ resolutionState: "unresolved",
52
+ }),
53
+ ],
54
+ findings: [],
55
+ evidenceText: [],
56
+ hasLiteral: false,
57
+ };
58
+ }
59
+ return { candidates: [], findings: [], evidenceText: [], hasLiteral: false };
60
+ }
61
+ function analyzeStoreFile(targetRoot, config, modulePath, moduleDirRelative, location) {
62
+ const { storeFile } = config;
63
+ if (storeFile.state === "missing")
64
+ return { candidates: [], evidenceText: [`${config.name}.storeFile is not set`] };
65
+ if (storeFile.state !== "literal" || storeFile.literalValue === undefined) {
66
+ return {
67
+ candidates: [
68
+ makeCandidateEvidence({
69
+ ruleId: "android-signing-keystore-candidate",
70
+ category: "android-signing-configuration",
71
+ confidence: "low",
72
+ modulePath,
73
+ location,
74
+ summary: `Signing configuration "${config.name}"'s storeFile is a dynamic/unresolved expression`,
75
+ rawValue: storeFile.rawExpression,
76
+ resolutionState: "unresolved",
77
+ }),
78
+ ],
79
+ evidenceText: [],
80
+ };
81
+ }
82
+ const literalPath = storeFile.literalValue;
83
+ if (path.isAbsolute(literalPath)) {
84
+ return {
85
+ candidates: [
86
+ makeCandidateEvidence({
87
+ ruleId: "android-signing-path-leakage",
88
+ category: "android-signing-configuration",
89
+ confidence: "medium",
90
+ modulePath,
91
+ location,
92
+ summary: `Signing configuration "${config.name}" references an absolute local storeFile path`,
93
+ rawValue: literalPath,
94
+ resolutionState: "resolved",
95
+ staticAnalysisLimitations: ["The absolute path may reveal local workstation directory structure; it was not resolved or opened."],
96
+ }),
97
+ ],
98
+ evidenceText: [],
99
+ };
100
+ }
101
+ const resolvedAbsolute = path.join(targetRoot, moduleDirRelative, literalPath);
102
+ const exists = fileExistsSafely(resolvedAbsolute);
103
+ if (!exists) {
104
+ return {
105
+ candidates: [
106
+ makeCandidateEvidence({
107
+ ruleId: "android-signing-keystore-candidate",
108
+ category: "android-signing-configuration",
109
+ confidence: "low",
110
+ modulePath,
111
+ location,
112
+ summary: `Signing configuration "${config.name}" references a keystore file that was not found in the target`,
113
+ rawValue: literalPath,
114
+ resolutionState: "missing",
115
+ staticAnalysisLimitations: ["A missing referenced keystore is review evidence, not proof that signing is broken."],
116
+ }),
117
+ ],
118
+ evidenceText: [],
119
+ };
120
+ }
121
+ return { candidates: [], evidenceText: [`${config.name}.storeFile resolves to a target-contained file: ${literalPath}`] };
122
+ }
123
+ function fileExistsSafely(absolutePath) {
124
+ try {
125
+ return fs.existsSync(absolutePath);
126
+ }
127
+ catch {
128
+ return false;
129
+ }
130
+ }
131
+ function extractReleaseSigningConfigName(signingConfigRef) {
132
+ const getByNameMatch = signingConfigRef.match(/signingConfigs\.getByName\(\s*["'](\w[\w-]*)["']\s*\)/);
133
+ if (getByNameMatch)
134
+ return getByNameMatch[1];
135
+ const dotMatch = signingConfigRef.match(/signingConfigs\.(\w[\w-]*)/);
136
+ if (dotMatch)
137
+ return dotMatch[1];
138
+ const namedMatch = signingConfigRef.match(/signingConfigs\.named\(\s*["'](\w[\w-]*)["']\s*\)/);
139
+ if (namedMatch)
140
+ return namedMatch[1];
141
+ return undefined;
142
+ }
143
+ export function analyzeModuleSigningConfiguration(targetRoot, gradleModule, buildFileText, allKeystoreCandidates) {
144
+ const candidates = [];
145
+ const findings = [];
146
+ const evidenceText = [];
147
+ const modulePath = gradleModule.path;
148
+ const isLibraryOnlyModule = gradleModule.isLibrary === true && gradleModule.isApplication !== true;
149
+ const buildFileRelativePath = gradleModule.buildFilePath ?? modulePath;
150
+ const buildFileAbsolutePath = path.join(targetRoot, buildFileRelativePath);
151
+ const location = loc(targetRoot, buildFileAbsolutePath);
152
+ const signingConfigs = extractSigningConfigurations(buildFileText);
153
+ const literalCredentialConfigNames = new Set();
154
+ for (const config of signingConfigs) {
155
+ const storePasswordResult = analyzeCredential("storePassword", config.storePassword, config.name, modulePath, location, buildFileRelativePath);
156
+ const keyPasswordResult = analyzeCredential("keyPassword", config.keyPassword, config.name, modulePath, location, buildFileRelativePath);
157
+ const storeFileResult = analyzeStoreFile(targetRoot, config, modulePath, modulePath, location);
158
+ if (storePasswordResult.hasLiteral || keyPasswordResult.hasLiteral)
159
+ literalCredentialConfigNames.add(config.name);
160
+ if (isLibraryOnlyModule) {
161
+ // Downgrade findings to candidates for library-only modules — never
162
+ // suppress the evidence entirely, just its release-signing weight.
163
+ for (const finding of [...storePasswordResult.findings, ...keyPasswordResult.findings]) {
164
+ candidates.push(makeCandidateEvidence({
165
+ ruleId: finding.id.split("--")[0],
166
+ category: "android-signing-configuration",
167
+ confidence: "low",
168
+ modulePath,
169
+ location,
170
+ summary: `${finding.title} (library module — not an installable application)`,
171
+ rawValue: undefined,
172
+ resolutionState: "not-applicable",
173
+ }));
174
+ }
175
+ }
176
+ else {
177
+ findings.push(...storePasswordResult.findings, ...keyPasswordResult.findings);
178
+ }
179
+ candidates.push(...storePasswordResult.candidates, ...keyPasswordResult.candidates, ...storeFileResult.candidates);
180
+ evidenceText.push(...storePasswordResult.evidenceText, ...keyPasswordResult.evidenceText, ...storeFileResult.evidenceText);
181
+ // keyAlias is metadata only — never a finding/candidate by itself.
182
+ if (config.keyAlias.state === "literal") {
183
+ evidenceText.push(`${config.name}.keyAlias=${config.keyAlias.literalValue}`);
184
+ }
185
+ }
186
+ // --- release build-type <-> signingConfig correlation ---
187
+ const releaseBuildTypes = (gradleModule.buildTypeDetails ?? []).filter((bt) => bt.name === "release");
188
+ for (const buildType of releaseBuildTypes) {
189
+ if (buildType.signingConfigRef === undefined)
190
+ continue;
191
+ const referencedName = extractReleaseSigningConfigName(buildType.signingConfigRef);
192
+ if (referencedName === undefined) {
193
+ candidates.push(makeCandidateEvidence({
194
+ ruleId: "android-signing-debug-in-release",
195
+ category: "android-signing-configuration",
196
+ confidence: "low",
197
+ modulePath,
198
+ location,
199
+ summary: `Release build type's signingConfig is a dynamic/unresolved expression: ${buildType.signingConfigRef}`,
200
+ rawValue: buildType.signingConfigRef,
201
+ resolutionState: "unresolved",
202
+ }));
203
+ continue;
204
+ }
205
+ if (referencedName === "debug") {
206
+ const debugFinding = makeAndroidFinding({
207
+ ruleId: "android-signing-debug-in-release",
208
+ title: 'Release build type explicitly uses signingConfigs.debug',
209
+ severity: "major",
210
+ confidence: "high",
211
+ description: "The release build type's signingConfig references signingConfigs.debug. This is high-confidence risky release-configuration evidence; it does not prove a shipped APK was actually signed with the debug key (variant/flavor selection is not evaluated).",
212
+ manifestPath: buildFileRelativePath,
213
+ identity: "release-debug-signing",
214
+ evidenceDetails: [`buildType=release`, `signingConfigRef=${buildType.signingConfigRef}`],
215
+ recommendation: "Configure a dedicated release signing configuration and remove the reference to signingConfigs.debug.",
216
+ });
217
+ if (isLibraryOnlyModule) {
218
+ candidates.push(makeCandidateEvidence({
219
+ ruleId: "android-signing-debug-in-release",
220
+ category: "android-signing-configuration",
221
+ confidence: "low",
222
+ modulePath,
223
+ location,
224
+ summary: `${debugFinding.title} (library module — not an installable application)`,
225
+ rawValue: undefined,
226
+ resolutionState: "not-applicable",
227
+ }));
228
+ }
229
+ else {
230
+ findings.push(debugFinding);
231
+ }
232
+ continue;
233
+ }
234
+ const referencedConfigExists = signingConfigs.some((c) => c.name === referencedName);
235
+ if (!referencedConfigExists) {
236
+ candidates.push(makeCandidateEvidence({
237
+ ruleId: "android-signing-debug-in-release",
238
+ category: "android-signing-configuration",
239
+ confidence: "low",
240
+ modulePath,
241
+ location,
242
+ summary: `Release build type references signing config "${referencedName}", which has no matching definition in this build file`,
243
+ rawValue: referencedName,
244
+ resolutionState: "missing",
245
+ }));
246
+ continue;
247
+ }
248
+ evidenceText.push(`release build type references named signing config "${referencedName}" (ordinary metadata)`);
249
+ // --- committed-keystore + literal-credential + release correlation ---
250
+ if (literalCredentialConfigNames.has(referencedName)) {
251
+ const referencedConfig = signingConfigs.find((c) => c.name === referencedName);
252
+ const storeFileName = referencedConfig.storeFile.state === "literal" ? path.basename(referencedConfig.storeFile.literalValue ?? "") : undefined;
253
+ const committedKeystore = storeFileName ? allKeystoreCandidates.find((k) => path.basename(k.relativePath) === storeFileName) : undefined;
254
+ if (committedKeystore && !isLibraryOnlyModule) {
255
+ findings.push(makeAndroidFinding({
256
+ ruleId: "android-signing-keystore-candidate",
257
+ title: `Release signing configuration "${referencedName}" correlates with a committed keystore and a literal credential`,
258
+ severity: "major",
259
+ confidence: "high",
260
+ description: `The release build type references signing configuration "${referencedName}", which has a literal password and a matching committed keystore file (${committedKeystore.relativePath}) in the target. This is a high-confidence aligned static signal; it does not prove the credential is valid or that this keystore is used in a published artifact.`,
261
+ manifestPath: committedKeystore.relativePath,
262
+ identity: `${referencedName}-committed-keystore-correlation`,
263
+ evidenceDetails: [`signingConfig=${referencedName}`, `keystore=${committedKeystore.relativePath}`],
264
+ recommendation: "Remove the committed keystore and credential from source control; use a secure, out-of-repo signing mechanism (e.g. CI secret store).",
265
+ }));
266
+ }
267
+ }
268
+ }
269
+ return { candidates, findings, evidenceText };
270
+ }
@@ -0,0 +1,119 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { buildAndroidSourceLocation } from "../sourceLocation.js";
4
+ import { makeCandidateEvidence } from "../candidateEvidence.js";
5
+ import { sortCandidateEvidence } from "../ordering.js";
6
+ import { discoverKeystoreCandidates } from "./discoverKeystoreCandidates.js";
7
+ import { analyzeModuleSigningConfiguration } from "./analyzeSigningConfiguration.js";
8
+ // ---------------------------------------------------------------------------
9
+ // v0.4.1 Batch 4 — standalone Android signing-configuration check result.
10
+ //
11
+ // Operates over Gradle module metadata rather than parsed manifests, so it
12
+ // cannot reuse buildAndroidManifestCheckResult. Standalone: not called from
13
+ // validateAndroidTarget / any active orchestration, not rendered in reports,
14
+ // no CLI effect. Batch 8 integrates.
15
+ // ---------------------------------------------------------------------------
16
+ export const ANDROID_SIGNING_CONFIGURATION_AUDIT_CHECK_ID = "android-signing-configuration-audit";
17
+ export function auditAndroidSigningConfiguration(targetRoot, detection, gradleModules) {
18
+ if (detection.projectKind === "non-android") {
19
+ return {
20
+ id: ANDROID_SIGNING_CONFIGURATION_AUDIT_CHECK_ID,
21
+ category: "android-signing-configuration",
22
+ title: "Android signing configuration audit",
23
+ status: "unsupported",
24
+ requirementLevel: "optional",
25
+ ran: false,
26
+ skipped: true,
27
+ skipInfo: {
28
+ checkId: ANDROID_SIGNING_CONFIGURATION_AUDIT_CHECK_ID,
29
+ reason: "Target was not detected as an Android project.",
30
+ requirementLevel: "optional",
31
+ missingCapability: "android-detection",
32
+ verdictImpact: "does not apply to a non-Android target",
33
+ recommendedNextAction: "Re-run against an Android Gradle project, or verify project detection.",
34
+ },
35
+ evidence: [],
36
+ findings: [],
37
+ warnings: [],
38
+ errors: [],
39
+ sourcePaths: [],
40
+ confidence: "unknown",
41
+ environmentRequirements: [],
42
+ candidateEvidence: [],
43
+ };
44
+ }
45
+ const modulePaths = gradleModules.map((m) => m.path);
46
+ const keystoreCandidates = discoverKeystoreCandidates(targetRoot, modulePaths);
47
+ const findingsById = new Map();
48
+ const candidatesById = new Map();
49
+ const evidence = [];
50
+ const warnings = [];
51
+ const sourcePaths = [];
52
+ const correlatedKeystorePaths = new Set();
53
+ for (const module of gradleModules) {
54
+ if (module.buildFilePath === undefined) {
55
+ warnings.push(`${module.path}: no build file path known; signing configuration was not analyzed for this module`);
56
+ continue;
57
+ }
58
+ const absolutePath = path.join(targetRoot, module.buildFilePath);
59
+ let text;
60
+ try {
61
+ text = fs.readFileSync(absolutePath, "utf8");
62
+ }
63
+ catch {
64
+ warnings.push(`${module.buildFilePath}: could not be read; signing configuration was not analyzed for this module`);
65
+ continue;
66
+ }
67
+ sourcePaths.push(module.buildFilePath);
68
+ const result = analyzeModuleSigningConfiguration(targetRoot, module, text, keystoreCandidates);
69
+ for (const finding of result.findings) {
70
+ findingsById.set(finding.id, finding);
71
+ if (finding.affectedFiles)
72
+ for (const p of finding.affectedFiles)
73
+ correlatedKeystorePaths.add(p);
74
+ }
75
+ for (const candidate of result.candidates)
76
+ candidatesById.set(candidate.id, candidate);
77
+ evidence.push(`${module.path}: ${result.evidenceText.length} evidence note(s), ${result.findings.length} finding(s)`, ...result.evidenceText);
78
+ }
79
+ // Baseline review evidence for every committed keystore not already
80
+ // covered by a higher-confidence correlation finding above.
81
+ for (const keystore of keystoreCandidates) {
82
+ if (correlatedKeystorePaths.has(keystore.relativePath))
83
+ continue;
84
+ const absolutePath = path.join(targetRoot, keystore.relativePath);
85
+ candidatesById.set(`keystore-candidate--${keystore.relativePath}`, makeCandidateEvidence({
86
+ ruleId: "android-signing-keystore-candidate",
87
+ category: "android-signing-configuration",
88
+ confidence: "low",
89
+ modulePath: keystore.modulePath,
90
+ location: buildAndroidSourceLocation(targetRoot, absolutePath),
91
+ summary: `Committed keystore-like file found: ${keystore.relativePath}`,
92
+ rawValue: undefined,
93
+ resolutionState: "resolved",
94
+ staticAnalysisLimitations: [
95
+ "File contents were not read, opened, or validated; this does not prove the file contains a production release key.",
96
+ ],
97
+ }));
98
+ }
99
+ const findings = [...findingsById.values()].sort((a, b) => a.id.localeCompare(b.id));
100
+ const candidateEvidence = sortCandidateEvidence([...candidatesById.values()]);
101
+ const hasReportableFinding = findings.some((f) => f.severity !== "informational");
102
+ return {
103
+ id: ANDROID_SIGNING_CONFIGURATION_AUDIT_CHECK_ID,
104
+ category: "android-signing-configuration",
105
+ title: "Android signing configuration audit",
106
+ status: hasReportableFinding ? "failed" : "passed",
107
+ requirementLevel: "required",
108
+ ran: true,
109
+ skipped: false,
110
+ evidence,
111
+ findings,
112
+ warnings: [...warnings, "Static analysis only: does not open keystores, validate credentials, evaluate Gradle, or inspect APK/AAB signing."],
113
+ errors: [],
114
+ sourcePaths,
115
+ confidence: warnings.length > 0 ? "medium" : "high",
116
+ environmentRequirements: [],
117
+ candidateEvidence,
118
+ };
119
+ }
@@ -0,0 +1,55 @@
1
+ import { collectFilesForGlobs } from "../../../../core/fileGlobs.js";
2
+ // ---------------------------------------------------------------------------
3
+ // v0.4.1 Batch 4 — committed keystore-file candidate discovery.
4
+ //
5
+ // Reuses the existing target-contained glob collector (src/core/fileGlobs.ts)
6
+ // for traversal — no second traversal engine. Discovery is by file
7
+ // path/extension ONLY: this module never opens, reads, or inspects file
8
+ // contents (per agents.txt Batch 4 section 9.16).
9
+ // ---------------------------------------------------------------------------
10
+ const KEYSTORE_EXTENSIONS = [".jks", ".keystore", ".p12", ".pfx"];
11
+ const KEYSTORE_GLOBS = ["**/*.jks", "**/*.keystore", "**/*.p12", "**/*.pfx"];
12
+ const EXCLUDED_PATH_SEGMENTS = [".gradle/", "build/", "out/", "reports/"];
13
+ function isExcludedPath(relativePath) {
14
+ const normalized = `${relativePath.replace(/\\/g, "/")}/`;
15
+ return EXCLUDED_PATH_SEGMENTS.some((segment) => normalized.includes(`/${segment}`) || normalized.startsWith(segment));
16
+ }
17
+ function inferModulePath(relativePath, knownModulePaths) {
18
+ const normalized = relativePath.replace(/\\/g, "/");
19
+ let best;
20
+ for (const modulePath of knownModulePaths) {
21
+ const prefix = `${modulePath.replace(/\\/g, "/")}/`;
22
+ if (normalized.startsWith(prefix) && (best === undefined || modulePath.length > best.length)) {
23
+ best = modulePath;
24
+ }
25
+ }
26
+ return best;
27
+ }
28
+ // Discovers target-contained files whose extension matches a conventional
29
+ // Android/Java signing-container format. Path/metadata only — content is
30
+ // never read, opened, or validated.
31
+ export function discoverKeystoreCandidates(targetRoot, knownModulePaths = []) {
32
+ const candidateMap = new Map();
33
+ for (const glob of KEYSTORE_GLOBS) {
34
+ let matches;
35
+ try {
36
+ matches = collectFilesForGlobs(targetRoot, [glob]);
37
+ }
38
+ catch {
39
+ continue;
40
+ }
41
+ for (const match of matches) {
42
+ const normalizedRel = match.relativePath.replace(/\\/g, "/");
43
+ if (isExcludedPath(normalizedRel))
44
+ continue;
45
+ candidateMap.set(normalizedRel, match.absolutePath);
46
+ }
47
+ }
48
+ return [...candidateMap.keys()]
49
+ .sort((a, b) => a.localeCompare(b))
50
+ .map((relativePath) => ({
51
+ relativePath,
52
+ modulePath: inferModulePath(relativePath, knownModulePaths),
53
+ extension: KEYSTORE_EXTENSIONS.find((ext) => relativePath.toLowerCase().endsWith(ext)) ?? "",
54
+ }));
55
+ }
@@ -0,0 +1,81 @@
1
+ import { extractBooleanLiteral, extractLiteral, extractNamedSubBlocks, stripComments } from "../../gradle/moduleMetadataExtractor.js";
2
+ import { fingerprintCandidateValue, redactedPreviewForCandidate } from "../redaction.js";
3
+ function classifyExpressionState(result) {
4
+ if (result.value !== undefined)
5
+ return "literal";
6
+ if (result.raw === undefined)
7
+ return "missing";
8
+ const raw = result.raw;
9
+ if (/\bSystem\.getenv\b|\bproviders\.environmentVariable\b/.test(raw))
10
+ return "environment-reference";
11
+ if (/\bproviders\.gradleProperty\b|\bproject\.findProperty\b|\bfindProperty\(|(?<![A-Za-z0-9_.])\bproperty\(/.test(raw))
12
+ return "gradle-property-reference";
13
+ if (/\blocalProperties\b/i.test(raw))
14
+ return "local-property-reference";
15
+ if (/[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(raw))
16
+ return "method-call";
17
+ if (/^[A-Za-z_][A-Za-z0-9_.]*$/.test(raw))
18
+ return "variable-reference";
19
+ return "dynamic";
20
+ }
21
+ // Bounded raw-expression preview for non-literal states. These are
22
+ // expressions (e.g. `System.getenv("STORE_PASSWORD")`), not secrets
23
+ // themselves, but still bounded/control-char-stripped for safety and
24
+ // consistency with the rest of the codebase's evidence conventions.
25
+ function boundedRawExpression(raw) {
26
+ return redactedPreviewForCandidate(raw, { maxLength: 120, edgeLength: 60 });
27
+ }
28
+ function extractPathValue(content, keys) {
29
+ const result = extractLiteral(content, keys);
30
+ const state = classifyExpressionState(result);
31
+ if (state === "literal")
32
+ return { state, literalValue: result.value };
33
+ if (result.raw !== undefined)
34
+ return { state, rawExpression: boundedRawExpression(result.raw) };
35
+ return { state: "missing" };
36
+ }
37
+ // storeFile is conventionally wrapped as `storeFile file("name.jks")` /
38
+ // `storeFile = file("name.jks")` in both DSLs — the plain extractLiteral
39
+ // literal-match never fires here (a call expression intervenes before the
40
+ // quoted string), so the literal path is unwrapped explicitly first.
41
+ function extractStoreFileValue(content) {
42
+ const fileWrapped = content.match(/\bstoreFile\s*=?\s*file\(\s*["']([^"']*)["']\s*\)/);
43
+ if (fileWrapped)
44
+ return { state: "literal", literalValue: fileWrapped[1] };
45
+ return extractPathValue(content, ["storeFile"]);
46
+ }
47
+ function extractCredentialValue(content, keys) {
48
+ const result = extractLiteral(content, keys);
49
+ const state = classifyExpressionState(result);
50
+ if (state === "literal" && result.value !== undefined) {
51
+ return { state, redactedPreview: redactedPreviewForCandidate(result.value), fingerprint: fingerprintCandidateValue(result.value) };
52
+ }
53
+ if (result.raw !== undefined)
54
+ return { state, rawExpression: boundedRawExpression(result.raw) };
55
+ return { state: "missing" };
56
+ }
57
+ function extractSigningConfigEntry(name, content) {
58
+ const v1 = extractBooleanLiteral(content, ["enableV1Signing", "v1SigningEnabled"]);
59
+ const v2 = extractBooleanLiteral(content, ["enableV2Signing", "v2SigningEnabled"]);
60
+ const v3 = extractBooleanLiteral(content, ["enableV3Signing", "v3SigningEnabled"]);
61
+ const v4 = extractBooleanLiteral(content, ["enableV4Signing", "v4SigningEnabled"]);
62
+ return {
63
+ name,
64
+ storeFile: extractStoreFileValue(content),
65
+ storePassword: extractCredentialValue(content, ["storePassword"]),
66
+ keyAlias: extractPathValue(content, ["keyAlias"]),
67
+ keyPassword: extractCredentialValue(content, ["keyPassword"]),
68
+ enableV1Signing: v1.value,
69
+ enableV2Signing: v2.value,
70
+ enableV3Signing: v3.value,
71
+ enableV4Signing: v4.value,
72
+ };
73
+ }
74
+ // Extracts every named entry inside a module build file's `signingConfigs { }`
75
+ // block. Returns an empty array (never throws) when no such block exists.
76
+ export function extractSigningConfigurations(rawBuildFileText) {
77
+ const text = stripComments(rawBuildFileText);
78
+ return extractNamedSubBlocks(text, "signingConfigs")
79
+ .map(({ name, content }) => extractSigningConfigEntry(name, content))
80
+ .sort((a, b) => a.name.localeCompare(b.name));
81
+ }
@@ -0,0 +1,8 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.1 Batch 4 — public exports for the standalone Android signing-
3
+ // configuration audit.
4
+ // ---------------------------------------------------------------------------
5
+ export { extractSigningConfigurations } from "./extractSigningConfigurations.js";
6
+ export { discoverKeystoreCandidates } from "./discoverKeystoreCandidates.js";
7
+ export { analyzeModuleSigningConfiguration } from "./analyzeSigningConfiguration.js";
8
+ export { auditAndroidSigningConfiguration, ANDROID_SIGNING_CONFIGURATION_AUDIT_CHECK_ID } from "./checkResult.js";
@@ -0,0 +1,20 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.1 Batch 4 — signing-configuration model.
3
+ //
4
+ // Deliberately a NEW, separate type — not an additive field on the shared,
5
+ // already-actively-rendered AndroidGradleModuleInfo/AndroidGradleMetadata
6
+ // (src/mobile/android/gradle/types.ts). That type flows into the real,
7
+ // currently-rendered v0.4.0 Android JSON/text report via the active
8
+ // android-gradle-metadata check; adding a raw or even redacted-preview
9
+ // credential field to it would put secret-shaped data on a path that already
10
+ // renders today, before this batch's own non-disclosure guarantees can be
11
+ // verified end-to-end by Batch 8. Keeping this type local to the standalone,
12
+ // unregistered signingConfiguration feature means it can only ever reach a
13
+ // report once Batch 8 deliberately wires it in.
14
+ //
15
+ // storeFile/keyAlias are not secrets (a path and an alias name), so their
16
+ // literal value is retained directly. storePassword/keyPassword literal
17
+ // values are NEVER retained here — only a redacted preview and fingerprint
18
+ // (Batch 1 owners), computed immediately at extraction time.
19
+ // ---------------------------------------------------------------------------
20
+ export {};
@@ -0,0 +1,15 @@
1
+ import { relativeWithinRoot } from "../../../core/pathSafety.js";
2
+ // `absolutePath` must already be an absolute, filesystem-resolved path (the
3
+ // same convention relativeWithinRoot's existing callers use, e.g. the
4
+ // manifest reader) — a relative path here would be resolved against the
5
+ // process cwd, not root, and silently produce the wrong location.
6
+ export function buildAndroidSourceLocation(root, absolutePath, position) {
7
+ const relativePath = relativeWithinRoot(root, absolutePath);
8
+ return {
9
+ path: relativePath,
10
+ line: position?.line,
11
+ column: position?.column,
12
+ startLine: position?.startLine,
13
+ endLine: position?.endLine,
14
+ };
15
+ }