@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,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
+ }
@@ -0,0 +1,210 @@
1
+ import { makeAndroidFinding } from "../../audit/androidFinding.js";
2
+ import { makeCandidateEvidence } from "../candidateEvidence.js";
3
+ import { sortCandidateEvidence } from "../ordering.js";
4
+ import { buildAndroidSourceLocation } from "../sourceLocation.js";
5
+ import { discoverSecretSourceFiles } from "../secretCandidates/discoverSecretSourceFiles.js";
6
+ export const ANDROID_WEBVIEW_SECURITY_AUDIT_CHECK_ID = "android-webview-security-audit";
7
+ const CALL = /(?:([A-Za-z_][\w.]*(?:\.getSettings\(\))?)\.)?(setJavaScriptEnabled|addJavascriptInterface|setAllowFileAccess|setAllowContentAccess|setAllowFileAccessFromFileURLs|setAllowUniversalAccessFromFileURLs|setMixedContentMode|setWebContentsDebuggingEnabled|loadUrl|postUrl|loadData|loadDataWithBaseURL|proceed|cancel)\s*\(([^\n)]*)\)/g;
8
+ const PROPERTY = /([A-Za-z_][\w.]*)\.(javaScriptEnabled|allowFileAccess|allowContentAccess|allowFileAccessFromFileURLs|allowUniversalAccessFromFileURLs|mixedContentMode)\s*=\s*([^;\n}]+)/g;
9
+ const SETTINGS_ALIAS = /(?:\bWebSettings\s+|\b(?:val|var)\s+)([A-Za-z_]\w*)\s*=\s*([A-Za-z_][\w.]*)\s*(?:\.getSettings\(\)|\.settings)/g;
10
+ const MAX_ARGUMENT = 240;
11
+ function lineAt(text, offset) { return text.slice(0, offset).split("\n").length; }
12
+ // Masks comments and string *contents* while preserving offsets/newlines.
13
+ // Calls are matched only in code; the original text is then used for the
14
+ // bounded argument summary. This intentionally local scanner is not a Java
15
+ // or Kotlin parser and never attempts type/provenance resolution.
16
+ function executableMask(source) {
17
+ let out = "";
18
+ let i = 0;
19
+ let state = "code";
20
+ let quote = "";
21
+ while (i < source.length) {
22
+ const ch = source[i], next = source[i + 1];
23
+ if (state === "code" && ch === "/" && next === "/") {
24
+ out += " ";
25
+ i += 2;
26
+ state = "line";
27
+ continue;
28
+ }
29
+ if (state === "code" && ch === "/" && next === "*") {
30
+ out += " ";
31
+ i += 2;
32
+ state = "block";
33
+ continue;
34
+ }
35
+ if (state === "code" && source.slice(i, i + 3) === '\"\"\"') {
36
+ out += '\"\"\"';
37
+ i += 3;
38
+ state = "raw";
39
+ continue;
40
+ }
41
+ if (state === "code" && (ch === '"' || ch === "'")) {
42
+ quote = ch;
43
+ out += ch;
44
+ i++;
45
+ state = "quote";
46
+ continue;
47
+ }
48
+ if (state === "line") {
49
+ out += ch === "\n" ? "\n" : " ";
50
+ if (ch === "\n")
51
+ state = "code";
52
+ i++;
53
+ continue;
54
+ }
55
+ if (state === "block") {
56
+ out += ch === "\n" ? "\n" : " ";
57
+ if (ch === "*" && next === "/") {
58
+ out = out.slice(0, -1) + " ";
59
+ i += 2;
60
+ state = "code";
61
+ }
62
+ else
63
+ i++;
64
+ continue;
65
+ }
66
+ if (state === "raw") {
67
+ if (source.slice(i, i + 3) === '\"\"\"') {
68
+ out += '\"\"\"';
69
+ i += 3;
70
+ state = "code";
71
+ }
72
+ else {
73
+ out += ch === "\n" ? "\n" : " ";
74
+ i++;
75
+ }
76
+ continue;
77
+ }
78
+ if (state === "quote") {
79
+ if (ch === "\\") {
80
+ out += " ";
81
+ i += 2;
82
+ continue;
83
+ }
84
+ out += ch === "\n" ? "\n" : (ch === quote ? ch : " ");
85
+ if (ch === quote)
86
+ state = "code";
87
+ i++;
88
+ continue;
89
+ }
90
+ out += ch;
91
+ i++;
92
+ }
93
+ return out;
94
+ }
95
+ function braceEnd(mask, open) { let depth = 0; for (let i = open; i < mask.length; i++) {
96
+ if (mask[i] === "{")
97
+ depth++;
98
+ else if (mask[i] === "}" && --depth === 0)
99
+ return i;
100
+ } return mask.length; }
101
+ function scopeRanges(mask) { const ranges = []; const header = /(?:\bfun\s+([A-Za-z_]\w*)\s*\([^)]*\)|\b(?:public|private|protected|override|static|final|suspend|void|[A-Z][\w<>?\[\]]*)[\s\w<>?\[\]]*?\s+([A-Za-z_]\w*)\s*\([^)]*\))\s*\{/g; for (const match of mask.matchAll(header)) {
102
+ const from = (match.index ?? 0);
103
+ const open = from + match[0].length - 1;
104
+ ranges.push({ from, to: braceEnd(mask, open), id: `${match[1] ?? match[2] ?? "scope"}@${from}` });
105
+ } return ranges; }
106
+ function sslRanges(mask) { const ranges = []; for (const match of mask.matchAll(/onReceivedSslError\s*\([^)]*\)\s*\{/g)) {
107
+ const start = (match.index ?? 0) + match[0].length - 1;
108
+ ranges.push([start, braceEnd(mask, start)]);
109
+ } return ranges; }
110
+ function literal(argument) {
111
+ const trimmed = argument.trim();
112
+ return trimmed === "true" ? "true" : trimmed === "false" ? "false" : undefined;
113
+ }
114
+ function finding(ruleId, title, file, line, identity, severity) {
115
+ return makeAndroidFinding({ ruleId, title, severity, confidence: "high", description: `${title}. This is static source evidence only and does not prove runtime execution or exploitability.`, manifestPath: file, identity, location: { line }, recommendation: "Review the configuration and keep WebView capabilities limited to documented requirements." });
116
+ }
117
+ function candidate(ruleId, targetRoot, file, line, summary, rawValue) {
118
+ return makeCandidateEvidence({ ruleId, category: "android-webview", confidence: "medium", modulePath: file.modulePath, location: buildAndroidSourceLocation(targetRoot, file.absolutePath, { line }), summary, rawValue: rawValue.slice(0, MAX_ARGUMENT), resolutionState: "resolved", staticAnalysisLimitations: ["Bounded lexical source analysis; receiver types, call ordering, and dynamic values are not resolved."] });
119
+ }
120
+ export function auditAndroidWebViewSecurity(targetRoot, detection) {
121
+ if (detection.projectKind === "non-android")
122
+ return { id: ANDROID_WEBVIEW_SECURITY_AUDIT_CHECK_ID, category: "android-webview", title: "Android WebView security audit", status: "unsupported", requirementLevel: "optional", ran: false, skipped: true, skipInfo: { checkId: ANDROID_WEBVIEW_SECURITY_AUDIT_CHECK_ID, reason: "Target was not detected as Android.", requirementLevel: "optional", verdictImpact: "does not apply", recommendedNextAction: "Run against an Android project." }, evidence: [], findings: [], warnings: [], errors: [], sourcePaths: [], confidence: "unknown", environmentRequirements: [], candidateEvidence: [] };
123
+ const discovered = discoverSecretSourceFiles(targetRoot, detection.modules.map((m) => m.path));
124
+ const files = discovered.files.filter((file) => /\.(java|kt)$/.test(file.relativePath));
125
+ const findings = [];
126
+ const candidates = [];
127
+ for (const file of files) {
128
+ const mask = executableMask(file.content);
129
+ const ssl = sslRanges(mask);
130
+ const scopes = scopeRanges(mask);
131
+ const calls = [];
132
+ const contextAt = (offset) => scopes.filter((scope) => offset >= scope.from && offset <= scope.to).sort((a, b) => a.to - a.from - (b.to - b.from))[0]?.id ?? "(unresolved-scope)";
133
+ const aliases = new Map();
134
+ for (const match of mask.matchAll(SETTINGS_ALIAS))
135
+ aliases.set(`${contextAt(match.index ?? 0)}:${match[1]}`, match[2]);
136
+ const normalize = (receiver, method, context) => { const stripped = receiver.replace(/\.getSettings\(\)$|\.settings$/, ""); if (stripped !== receiver)
137
+ return stripped; if (method.startsWith("set") && aliases.has(`${context}:${receiver}`))
138
+ return aliases.get(`${context}:${receiver}`); return receiver; };
139
+ CALL.lastIndex = 0;
140
+ for (let match; (match = CALL.exec(mask));) {
141
+ const start = match.index;
142
+ const original = file.content.slice(start, start + match[0].length);
143
+ const argument = original.slice(original.indexOf("(") + 1, -1).slice(0, MAX_ARGUMENT);
144
+ const line = lineAt(file.content, start);
145
+ const context = contextAt(start);
146
+ calls.push({ receiver: normalize(match[1] ?? "(unresolved)", match[2], context), method: match[2], argument, line, context, inSslCallback: ssl.some(([from, to]) => start > from && start < to) });
147
+ }
148
+ PROPERTY.lastIndex = 0;
149
+ for (let match; (match = PROPERTY.exec(mask));) {
150
+ const start = match.index;
151
+ const original = file.content.slice(start, start + match[0].length);
152
+ const eq = original.indexOf("=");
153
+ const propertyToMethod = { javaScriptEnabled: "setJavaScriptEnabled", allowFileAccess: "setAllowFileAccess", allowContentAccess: "setAllowContentAccess", allowFileAccessFromFileURLs: "setAllowFileAccessFromFileURLs", allowUniversalAccessFromFileURLs: "setAllowUniversalAccessFromFileURLs", mixedContentMode: "setMixedContentMode" };
154
+ const method = propertyToMethod[match[2]], context = contextAt(start);
155
+ calls.push({ receiver: normalize(match[1], method, context), method, argument: original.slice(eq + 1).trim().slice(0, MAX_ARGUMENT), line: lineAt(file.content, start), context, inSslCallback: ssl.some(([from, to]) => start > from && start < to) });
156
+ }
157
+ for (const call of calls) {
158
+ const value = literal(call.argument);
159
+ if (call.method === "setAllowUniversalAccessFromFileURLs" && value === "true")
160
+ findings.push(finding("android-webview-universal-file-url-access", "WebView universal file URL access is explicitly enabled", file.relativePath, call.line, call.context, "major"));
161
+ else if (call.method === "setMixedContentMode" && /MIXED_CONTENT_ALWAYS_ALLOW|0\b/.test(call.argument))
162
+ findings.push(finding("android-webview-mixed-content", "WebView mixed content is explicitly always allowed", file.relativePath, call.line, call.context, "major"));
163
+ else if (call.method === "setWebContentsDebuggingEnabled" && value === "true" && /(?:^|\.)WebView$/.test(call.receiver))
164
+ findings.push(finding("android-webview-debugging-enabled", "WebView debugging is explicitly enabled", file.relativePath, call.line, call.context, "minor"));
165
+ else if (call.method === "proceed" && call.inSslCallback)
166
+ findings.push(finding("android-webview-ssl-error-proceed", "SSL error callback proceeds despite the certificate error", file.relativePath, call.line, call.context, "major"));
167
+ else if (call.method === "setJavaScriptEnabled" && value !== "false")
168
+ candidates.push(candidate("android-webview-javascript-enabled", targetRoot, file, call.line, value === "true" ? "JavaScript is explicitly enabled" : "JavaScript enablement is dynamic or unresolved", call.argument));
169
+ else if (["setAllowFileAccess", "setAllowContentAccess", "setAllowFileAccessFromFileURLs", "setAllowUniversalAccessFromFileURLs"].includes(call.method) && value !== "false")
170
+ candidates.push(candidate(call.method === "setAllowUniversalAccessFromFileURLs" ? "android-webview-universal-file-url-access" : "android-webview-file-content-access", targetRoot, file, call.line, `WebView ${call.method} is ${value === "true" ? "enabled" : "dynamic or unresolved"}`, call.argument));
171
+ else if (call.method === "addJavascriptInterface")
172
+ candidates.push(candidate("android-webview-javascript-interface-exposure", targetRoot, file, call.line, "JavaScript interface registration source evidence", call.argument));
173
+ else if (call.method === "setMixedContentMode" && /MIXED_CONTENT_COMPATIBILITY_MODE|1\b/.test(call.argument))
174
+ candidates.push(candidate("android-webview-mixed-content", targetRoot, file, call.line, "WebView mixed-content compatibility mode is configured", call.argument));
175
+ else if (call.method === "setMixedContentMode" && !/MIXED_CONTENT_NEVER_ALLOW|2\b/.test(call.argument))
176
+ candidates.push(candidate("android-webview-mixed-content", targetRoot, file, call.line, "WebView mixed-content mode is dynamic or unresolved", call.argument));
177
+ else if (call.method === "setWebContentsDebuggingEnabled" && value !== "false")
178
+ candidates.push(candidate("android-webview-debugging-enabled", targetRoot, file, call.line, "WebView debugging state is dynamic or receiver context is unresolved", call.argument));
179
+ else if (["loadUrl", "postUrl", "loadData", "loadDataWithBaseURL"].includes(call.method)) {
180
+ if (/^\s*["']javascript:/i.test(call.argument))
181
+ candidates.push(candidate("android-webview-suspicious-url-loading", targetRoot, file, call.line, "javascript: WebView URL loading evidence", call.argument));
182
+ else if (/^\s*["'](?:http|file|content):/i.test(call.argument))
183
+ candidates.push(candidate("android-webview-suspicious-url-loading", targetRoot, file, call.line, "WebView loads a review-oriented literal scheme", call.argument));
184
+ else if (!/^\s*["']https:/i.test(call.argument))
185
+ candidates.push(candidate("android-webview-suspicious-url-loading", targetRoot, file, call.line, "Dynamic or unsupported WebView content loading argument", call.argument));
186
+ }
187
+ }
188
+ for (const [from, to] of ssl) {
189
+ const contained = calls.filter((call) => call.inSslCallback && call.method === "proceed" || call.inSslCallback && call.method === "cancel");
190
+ if (contained.length === 0)
191
+ candidates.push(candidate("android-webview-ssl-error-proceed", targetRoot, file, lineAt(file.content, from), mask.slice(from, Math.min(to, from + MAX_ARGUMENT)).trim() ? "SSL error callback delegates or has unresolved local behavior" : "SSL error callback is empty", file.content.slice(from, Math.min(to, from + MAX_ARGUMENT))));
192
+ }
193
+ const same = (a, b) => a.receiver !== "(unresolved)" && a.receiver === b.receiver && a.context === b.context;
194
+ for (const js of calls.filter((c) => c.method === "setJavaScriptEnabled" && literal(c.argument) === "true"))
195
+ for (const bridge of calls.filter((c) => c.method === "addJavascriptInterface" && same(c, js)))
196
+ findings.push(finding("android-webview-javascript-interface-exposure", "JavaScript and addJavascriptInterface occur on the same receiver in one method", file.relativePath, bridge.line, `${js.receiver}:${js.context}`, "major"));
197
+ for (const js of calls.filter((c) => c.method === "setJavaScriptEnabled" && literal(c.argument) === "true"))
198
+ for (const access of calls.filter((c) => ["setAllowFileAccessFromFileURLs", "setAllowUniversalAccessFromFileURLs"].includes(c.method) && literal(c.argument) === "true" && same(c, js)))
199
+ findings.push(finding("android-webview-file-content-access", "JavaScript and file URL access occur on the same receiver in one method", file.relativePath, access.line, `${js.receiver}:${js.context}`, "major"));
200
+ for (const js of calls.filter((c) => c.method === "setJavaScriptEnabled" && literal(c.argument) === "true"))
201
+ for (const url of calls.filter((c) => c.method === "loadUrl" && /^\s*["']javascript:/i.test(c.argument) && same(c, js)))
202
+ findings.push(finding("android-webview-suspicious-url-loading", "JavaScript is enabled and a javascript: URL is loaded on the same receiver", file.relativePath, url.line, `${js.receiver}:${js.context}`, "major"));
203
+ for (const mixed of calls.filter((c) => c.method === "setMixedContentMode" && /MIXED_CONTENT_ALWAYS_ALLOW|0\b/.test(c.argument)))
204
+ for (const url of calls.filter((c) => c.method === "loadUrl" && /^\s*["']http:\/\//i.test(c.argument) && same(c, mixed)))
205
+ findings.push(finding("android-webview-mixed-content", "Always-allowed mixed content and HTTP loading occur on the same receiver", file.relativePath, url.line, `${mixed.receiver}:${mixed.context}`, "major"));
206
+ }
207
+ const uniqueFindings = [...new Map(findings.map((item) => [item.id, item])).values()].sort((a, b) => a.id.localeCompare(b.id));
208
+ const uniqueCandidates = [...new Map(candidates.map((item) => [item.id, item])).values()];
209
+ return { id: ANDROID_WEBVIEW_SECURITY_AUDIT_CHECK_ID, category: "android-webview", title: "Android WebView security audit", status: uniqueFindings.length ? "failed" : "passed", requirementLevel: "required", ran: true, skipped: false, evidence: [`${files.length} Java/Kotlin file(s) scanned`], findings: uniqueFindings, warnings: [...discovered.skipped.map((item) => `${item.relativePath}: skipped (${item.reason})`), "Static analysis only: no compiler, runtime, URL provenance, or whole-program analysis was used."], errors: [], sourcePaths: files.map((file) => file.relativePath), confidence: discovered.skipped.length ? "medium" : "high", environmentRequirements: [], candidateEvidence: sortCandidateEvidence(uniqueCandidates) };
210
+ }
@@ -0,0 +1 @@
1
+ export { auditAndroidWebViewSecurity, ANDROID_WEBVIEW_SECURITY_AUDIT_CHECK_ID } from "./checkResult.js";