@dailephd/my-dev-kit-lab 0.3.4 → 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 (252) hide show
  1. package/README.md +12 -287
  2. package/dist/scripts/security/validate.js +210 -70
  3. package/dist/src/audits/codeRot/detectors/architectureDriftDetector.js +5 -2
  4. package/dist/src/audits/codeRot/detectors/securityValidationAssumptionRotDetector.js +5 -2
  5. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/analyzeBackupConfiguration.js +300 -0
  6. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/checkResult.js +41 -0
  7. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/index.js +8 -0
  8. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/manifestEvidence.js +102 -0
  9. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/parseBackupRules.js +110 -0
  10. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/types.js +27 -0
  11. package/dist/src/mobile/android/advancedSecurity/candidateEvidence.js +50 -0
  12. package/dist/src/mobile/android/advancedSecurity/clipboard/analyzeClipboardSecurity.js +166 -0
  13. package/dist/src/mobile/android/advancedSecurity/clipboard/checkResult.js +74 -0
  14. package/dist/src/mobile/android/advancedSecurity/clipboard/collectClipboardEvidence.js +82 -0
  15. package/dist/src/mobile/android/advancedSecurity/clipboard/index.js +1 -0
  16. package/dist/src/mobile/android/advancedSecurity/clipboard/types.js +1 -0
  17. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/checkResult.js +255 -0
  18. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/index.js +1 -0
  19. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintSarif.js +37 -0
  20. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintXml.js +36 -0
  21. package/dist/src/mobile/android/advancedSecurity/externalTools/artifacts.js +95 -0
  22. package/dist/src/mobile/android/advancedSecurity/externalTools/boundedOutput.js +33 -0
  23. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/checkResult.js +173 -0
  24. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/command.js +33 -0
  25. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/index.js +1 -0
  26. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/parseDependencyCheckJson.js +133 -0
  27. package/dist/src/mobile/android/advancedSecurity/externalTools/discoverExecutable.js +13 -0
  28. package/dist/src/mobile/android/advancedSecurity/externalTools/index.js +8 -0
  29. package/dist/src/mobile/android/advancedSecurity/externalTools/minimalEnvironment.js +26 -0
  30. package/dist/src/mobile/android/advancedSecurity/externalTools/mutation.js +77 -0
  31. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/checkResult.js +161 -0
  32. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/command.js +25 -0
  33. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/index.js +1 -0
  34. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/parseOsvJson.js +136 -0
  35. package/dist/src/mobile/android/advancedSecurity/externalTools/runBoundedExternalTool.js +51 -0
  36. package/dist/src/mobile/android/advancedSecurity/externalTools/runRequestedAndroidExternalTools.js +54 -0
  37. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/checkResult.js +170 -0
  38. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/command.js +28 -0
  39. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/index.js +2 -0
  40. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/parseSemgrepJson.js +141 -0
  41. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/rules.js +105 -0
  42. package/dist/src/mobile/android/advancedSecurity/externalTools/types.js +107 -0
  43. package/dist/src/mobile/android/advancedSecurity/fileProvider/checkResult.js +188 -0
  44. package/dist/src/mobile/android/advancedSecurity/fileProvider/index.js +1 -0
  45. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/analyzeFirebaseGoogleServices.js +197 -0
  46. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/checkResult.js +77 -0
  47. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/collectFirebaseServiceEvidence.js +193 -0
  48. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/discoverFirebaseArtifacts.js +128 -0
  49. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/index.js +1 -0
  50. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseDatabaseRules.js +55 -0
  51. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirebaseConfig.js +75 -0
  52. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirestoreStorageRules.js +88 -0
  53. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseGoogleServicesConfig.js +55 -0
  54. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/types.js +6 -0
  55. package/dist/src/mobile/android/advancedSecurity/networkSecurity/analyzeNetworkSecurity.js +394 -0
  56. package/dist/src/mobile/android/advancedSecurity/networkSecurity/checkResult.js +47 -0
  57. package/dist/src/mobile/android/advancedSecurity/networkSecurity/deriveEffectiveNetworkPolicy.js +52 -0
  58. package/dist/src/mobile/android/advancedSecurity/networkSecurity/index.js +14 -0
  59. package/dist/src/mobile/android/advancedSecurity/networkSecurity/manifestEvidence.js +109 -0
  60. package/dist/src/mobile/android/advancedSecurity/networkSecurity/parseNetworkSecurityConfig.js +186 -0
  61. package/dist/src/mobile/android/advancedSecurity/networkSecurity/types.js +1 -0
  62. package/dist/src/mobile/android/advancedSecurity/ordering.js +44 -0
  63. package/dist/src/mobile/android/advancedSecurity/redaction.js +54 -0
  64. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/analyzeReleaseConfiguration.js +221 -0
  65. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/checkResult.js +39 -0
  66. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/index.js +7 -0
  67. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/manifestEvidence.js +19 -0
  68. package/dist/src/mobile/android/advancedSecurity/resourceReference.js +69 -0
  69. package/dist/src/mobile/android/advancedSecurity/resourceResolution.js +118 -0
  70. package/dist/src/mobile/android/advancedSecurity/ruleIds.js +141 -0
  71. package/dist/src/mobile/android/advancedSecurity/secretCandidates/analyzeSecretCandidates.js +116 -0
  72. package/dist/src/mobile/android/advancedSecurity/secretCandidates/checkResult.js +80 -0
  73. package/dist/src/mobile/android/advancedSecurity/secretCandidates/discoverSecretSourceFiles.js +152 -0
  74. package/dist/src/mobile/android/advancedSecurity/secretCandidates/index.js +9 -0
  75. package/dist/src/mobile/android/advancedSecurity/secretCandidates/matchSecretCandidates.js +136 -0
  76. package/dist/src/mobile/android/advancedSecurity/secretCandidates/suppressValue.js +61 -0
  77. package/dist/src/mobile/android/advancedSecurity/secretCandidates/types.js +11 -0
  78. package/dist/src/mobile/android/advancedSecurity/sensitiveCategories.js +34 -0
  79. package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifyDirectExpression.js +99 -0
  80. package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifySensitiveIdentifier.js +111 -0
  81. package/dist/src/mobile/android/advancedSecurity/sensitiveData/index.js +3 -0
  82. package/dist/src/mobile/android/advancedSecurity/sensitiveData/localSourceContext.js +90 -0
  83. package/dist/src/mobile/android/advancedSecurity/sensitiveData/splitArguments.js +59 -0
  84. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/analyzeSensitiveLogging.js +117 -0
  85. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/checkResult.js +74 -0
  86. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/collectLoggingEvidence.js +87 -0
  87. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/index.js +1 -0
  88. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/types.js +1 -0
  89. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/analyzeSensitiveStorage.js +182 -0
  90. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/checkResult.js +74 -0
  91. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/collectStorageEvidence.js +109 -0
  92. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/index.js +1 -0
  93. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/types.js +4 -0
  94. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/analyzeSigningConfiguration.js +270 -0
  95. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/checkResult.js +119 -0
  96. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/discoverKeystoreCandidates.js +55 -0
  97. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/extractSigningConfigurations.js +81 -0
  98. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/index.js +8 -0
  99. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/types.js +20 -0
  100. package/dist/src/mobile/android/advancedSecurity/sourceLocation.js +15 -0
  101. package/dist/src/mobile/android/advancedSecurity/webview/checkResult.js +210 -0
  102. package/dist/src/mobile/android/advancedSecurity/webview/index.js +1 -0
  103. package/dist/src/mobile/android/audit/androidFinding.js +68 -0
  104. package/dist/src/mobile/android/audit/checkResultBuilder.js +97 -0
  105. package/dist/src/mobile/android/audit/deepLinkAudit.js +183 -0
  106. package/dist/src/mobile/android/audit/exportedComponentAudit.js +183 -0
  107. package/dist/src/mobile/android/audit/intentFilterAudit.js +128 -0
  108. package/dist/src/mobile/android/audit/permissionAudit.js +142 -0
  109. package/dist/src/mobile/android/detect/androidTargetDetection.js +18 -0
  110. package/dist/src/mobile/android/detect/buildFileEvidence.js +36 -0
  111. package/dist/src/mobile/android/detect/classify.js +150 -0
  112. package/dist/src/mobile/android/detect/detectAndroidProject.js +252 -0
  113. package/dist/src/mobile/android/detect/settingsModules.js +60 -0
  114. package/dist/src/mobile/android/detect/sourceEvidence.js +92 -0
  115. package/dist/src/mobile/android/detect/traversal.js +79 -0
  116. package/dist/src/mobile/android/detect/versionCatalogEvidence.js +37 -0
  117. package/dist/src/mobile/android/detection.js +28 -0
  118. package/dist/src/mobile/android/gradle/gradleMetadataCheckResult.js +120 -0
  119. package/dist/src/mobile/android/gradle/moduleMetadataExtractor.js +243 -0
  120. package/dist/src/mobile/android/gradle/pluginVersionExtractor.js +40 -0
  121. package/dist/src/mobile/android/gradle/readAndroidGradleMetadata.js +210 -0
  122. package/dist/src/mobile/android/gradle/releaseMetadataSummary.js +64 -0
  123. package/dist/src/mobile/android/gradle/settingsMetadata.js +20 -0
  124. package/dist/src/mobile/android/gradle/types.js +1 -0
  125. package/dist/src/mobile/android/gradle/validate/executor.js +9 -0
  126. package/dist/src/mobile/android/gradle/validate/operationCheckResult.js +99 -0
  127. package/dist/src/mobile/android/gradle/validate/operations.js +68 -0
  128. package/dist/src/mobile/android/gradle/validate/planner.js +49 -0
  129. package/dist/src/mobile/android/gradle/validate/runOptionalGradleValidation.js +113 -0
  130. package/dist/src/mobile/android/gradle/validate/targetMutation.js +45 -0
  131. package/dist/src/mobile/android/gradle/validate/taskListParser.js +24 -0
  132. package/dist/src/mobile/android/gradle/versionCatalogMetadata.js +94 -0
  133. package/dist/src/mobile/android/gradle/wrapperMetadata.js +32 -0
  134. package/dist/src/mobile/android/manifest/parseAndroidManifest.js +364 -0
  135. package/dist/src/mobile/android/manifest/types.js +29 -0
  136. package/dist/src/mobile/android/manifest/xml/parseXml.js +329 -0
  137. package/dist/src/mobile/android/profile.js +29 -0
  138. package/dist/src/mobile/android/report/model.js +135 -0
  139. package/dist/src/mobile/android/report/renderAndroidReport.js +286 -0
  140. package/dist/src/mobile/android/report/writeAndroidReportFiles.js +23 -0
  141. package/dist/src/mobile/android/targetMetadata.js +27 -0
  142. package/dist/src/mobile/android/validate/androidVerdict.js +248 -0
  143. package/dist/src/mobile/android/validate/detectionAndImmutabilityCheckResults.js +177 -0
  144. package/dist/src/mobile/android/validate/playReadinessChecklist.js +145 -0
  145. package/dist/src/mobile/android/validate/validateAndroidTarget.js +315 -0
  146. package/dist/src/mobile/android/validation/checkResult.js +58 -0
  147. package/dist/src/mobile/android/validation/result.js +25 -0
  148. package/dist/src/mobile/index.js +42 -0
  149. package/dist/src/mobile/types.js +27 -0
  150. package/dist/src/securityValidation/attackScenarios/attackProfile.js +2 -0
  151. package/dist/src/securityValidation/attackScenarios/profiles/androidProfile.js +20 -0
  152. package/dist/src/securityValidation/commandRunner.js +10 -1
  153. package/dist/src/securityValidation/validate/cliOptions.js +92 -2
  154. package/package.json +2 -1
  155. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/debug/res/xml/ambiguous_backup_rules.xml +4 -0
  156. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/ambiguous_backup_rules.xml +4 -0
  157. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/backup_rules.xml +5 -0
  158. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/broad_backup_rules.xml +4 -0
  159. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/data_extraction_rules.xml +10 -0
  160. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/unsupported_domain_rules.xml +4 -0
  161. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/malformed_backup_rules.xml +4 -0
  162. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/unsupported_root.xml +4 -0
  163. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/debug/res/xml/ambiguous_config.xml +4 -0
  164. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/ambiguous_config.xml +4 -0
  165. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/base_cleartext_false.xml +8 -0
  166. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/broad_and_duplicate_domains.xml +8 -0
  167. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/empty_domain.xml +6 -0
  168. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/malformed_pin.xml +11 -0
  169. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/network_security_config.xml +26 -0
  170. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/malformed_network_security_config.xml +6 -0
  171. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/unsupported_root.xml +4 -0
  172. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-dynamic.gradle +7 -0
  173. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-false.gradle +9 -0
  174. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-true.gradle +13 -0
  175. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-false.gradle.kts +7 -0
  176. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-true.gradle.kts +10 -0
  177. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/missing-release-build-type.gradle.kts +7 -0
  178. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/debug/res/xml/network_security_config.xml +8 -0
  179. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/backup_rules.xml +4 -0
  180. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/network_security_config.xml +8 -0
  181. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-b/src/main/res/xml/malformed_network_security_config.xml +7 -0
  182. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.java +8 -0
  183. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.kt +9 -0
  184. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle +11 -0
  185. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle.kts +11 -0
  186. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/google-services.json +16 -0
  187. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/local.properties +3 -0
  188. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/binary.properties +0 -0
  189. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/oversized.json +1 -0
  190. package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/app/fake-release-keystore.jks +1 -0
  191. package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/build/generated-fake.keystore +1 -0
  192. package/tests/fixtures/android/compose-app/app/build.gradle.kts +21 -0
  193. package/tests/fixtures/android/compose-app/app/src/main/AndroidManifest.xml +22 -0
  194. package/tests/fixtures/android/compose-app/app/src/main/java/com/example/composeapp/MainActivity.kt +11 -0
  195. package/tests/fixtures/android/compose-app/build.gradle.kts +4 -0
  196. package/tests/fixtures/android/compose-app/gradle/wrapper/gradle-wrapper.properties +4 -0
  197. package/tests/fixtures/android/compose-app/settings.gradle.kts +2 -0
  198. package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/build.gradle.kts +3 -0
  199. package/tests/fixtures/android/gradle-metadata-fixtures/catalog-only-plugin/gradle/libs.versions.toml +5 -0
  200. package/tests/fixtures/android/gradle-metadata-fixtures/comment-safety.gradle.kts +21 -0
  201. package/tests/fixtures/android/gradle-metadata-fixtures/dynamic-values.gradle.kts +17 -0
  202. package/tests/fixtures/android/gradle-metadata-fixtures/groovy-literal.gradle +23 -0
  203. package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-malformed.properties +4 -0
  204. package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-missing-url.properties +3 -0
  205. package/tests/fixtures/android/gradle-metadata-fixtures/wrapper-with-checksum.properties +5 -0
  206. package/tests/fixtures/android/java-app/app/build.gradle +16 -0
  207. package/tests/fixtures/android/java-app/app/src/main/AndroidManifest.xml +19 -0
  208. package/tests/fixtures/android/java-app/app/src/main/java/com/example/javaapp/MainActivity.java +4 -0
  209. package/tests/fixtures/android/java-app/build.gradle +3 -0
  210. package/tests/fixtures/android/java-app/gradle/wrapper/gradle-wrapper.properties +4 -0
  211. package/tests/fixtures/android/java-app/settings.gradle +2 -0
  212. package/tests/fixtures/android/library/build.gradle.kts +3 -0
  213. package/tests/fixtures/android/library/gradle/wrapper/gradle-wrapper.properties +4 -0
  214. package/tests/fixtures/android/library/mylibrary/build.gradle.kts +13 -0
  215. package/tests/fixtures/android/library/mylibrary/src/main/AndroidManifest.xml +2 -0
  216. package/tests/fixtures/android/library/settings.gradle.kts +2 -0
  217. package/tests/fixtures/android/manifest-audit-fixtures/incomplete-intent-filter.xml +14 -0
  218. package/tests/fixtures/android/manifest-audit-fixtures/malformed-exported.xml +10 -0
  219. package/tests/fixtures/android/manifest-audit-fixtures/malformed.xml +5 -0
  220. package/tests/fixtures/android/manifest-audit-fixtures/missing-application.xml +7 -0
  221. package/tests/fixtures/android/manifest-audit-fixtures/missing-component-name.xml +11 -0
  222. package/tests/fixtures/android/manifest-audit-fixtures/permission-coverage.xml +29 -0
  223. package/tests/fixtures/android/manifest-audit-fixtures/risky-components.xml +62 -0
  224. package/tests/fixtures/android/manifest-audit-fixtures/risky-deep-links.xml +45 -0
  225. package/tests/fixtures/android/manifest-audit-fixtures/unresolved-placeholder.xml +14 -0
  226. package/tests/fixtures/android/manifest-audit-fixtures/unusual-namespace-prefix.xml +11 -0
  227. package/tests/fixtures/android/multi-module/app/build.gradle.kts +22 -0
  228. package/tests/fixtures/android/multi-module/app/src/androidTest/java/com/example/multimodule/app/ExampleInstrumentedTest.kt +3 -0
  229. package/tests/fixtures/android/multi-module/app/src/main/AndroidManifest.xml +11 -0
  230. package/tests/fixtures/android/multi-module/app/src/test/java/com/example/multimodule/app/ExampleUnitTest.kt +3 -0
  231. package/tests/fixtures/android/multi-module/build.gradle.kts +5 -0
  232. package/tests/fixtures/android/multi-module/core/build.gradle.kts +14 -0
  233. package/tests/fixtures/android/multi-module/core/src/main/AndroidManifest.xml +2 -0
  234. package/tests/fixtures/android/multi-module/feature-login/build.gradle.kts +18 -0
  235. package/tests/fixtures/android/multi-module/feature-login/src/main/AndroidManifest.xml +2 -0
  236. package/tests/fixtures/android/multi-module/gradle/libs.versions.toml +8 -0
  237. package/tests/fixtures/android/multi-module/gradle/wrapper/gradle-wrapper.properties +4 -0
  238. package/tests/fixtures/android/multi-module/settings.gradle.kts +4 -0
  239. package/tests/fixtures/android/non-android-gradle/build.gradle.kts +3 -0
  240. package/tests/fixtures/android/non-android-gradle/core/build.gradle.kts +3 -0
  241. package/tests/fixtures/android/non-android-gradle/core/src/main/java/com/example/core/Core.java +4 -0
  242. package/tests/fixtures/android/non-android-gradle/gradle/wrapper/gradle-wrapper.properties +4 -0
  243. package/tests/fixtures/android/non-android-gradle/settings.gradle.kts +2 -0
  244. package/tests/fixtures/android/partial/app/build.gradle.kts +7 -0
  245. package/tests/fixtures/android/partial/settings.gradle.kts +2 -0
  246. package/tests/fixtures/android/xml-view-app/app/build.gradle.kts +17 -0
  247. package/tests/fixtures/android/xml-view-app/app/src/main/AndroidManifest.xml +31 -0
  248. package/tests/fixtures/android/xml-view-app/app/src/main/kotlin/com/example/xmlviewapp/MainActivity.kt +7 -0
  249. package/tests/fixtures/android/xml-view-app/app/src/main/res/layout/activity_main.xml +4 -0
  250. package/tests/fixtures/android/xml-view-app/build.gradle.kts +4 -0
  251. package/tests/fixtures/android/xml-view-app/gradle/wrapper/gradle-wrapper.properties +4 -0
  252. package/tests/fixtures/android/xml-view-app/settings.gradle.kts +2 -0
@@ -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";
@@ -0,0 +1,68 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.0 Batch 3 — Android SecurityFinding factory.
3
+ //
4
+ // Reuses SecurityFinding as-is (no parallel Android finding schema). Android
5
+ // findings use category "static-scan" — an existing SecurityCheckCategory
6
+ // value that already covers static-analysis-derived findings (CodeQL,
7
+ // Semgrep) and accurately describes manifest analysis, so no widening of the
8
+ // shared, heavily-tested SecurityCheckCategory union was needed. Per-finding
9
+ // audit family is instead carried by the owning AndroidCheckResult.category
10
+ // (android-permissions/android-components/android-intent-filters/
11
+ // android-deep-links) and by the finding id/title.
12
+ //
13
+ // SecurityFinding has no `confidence` or structured `location` field (the
14
+ // existing codebase convention, e.g. AttackResult, keeps confidence on the
15
+ // *check result*, not the individual finding — see
16
+ // src/securityValidation/attackScenarios/attackResult.ts). Android check
17
+ // results already carry `confidence: MobileConfidence` (Batch 1). Per-finding
18
+ // confidence and location are therefore folded into the free-text `evidence`
19
+ // field here, consistent with how existing scenario code joins evidence
20
+ // previews into that same field.
21
+ // ---------------------------------------------------------------------------
22
+ function slug(value) {
23
+ return value
24
+ .replace(/[^a-zA-Z0-9]+/g, "-")
25
+ .replace(/^-+|-+$/g, "")
26
+ .toLowerCase();
27
+ }
28
+ // Deterministic, content-derived id — never depends on a counter, timestamp,
29
+ // or enumeration order, so equivalent evidence always yields the same id.
30
+ function buildFindingId(ruleId, manifestPath, identity, location) {
31
+ const parts = [ruleId, slug(manifestPath), identity ? slug(identity) : undefined, location?.line !== undefined ? `l${location.line}` : undefined].filter((part) => Boolean(part));
32
+ return parts.join("--");
33
+ }
34
+ function defaultReleaseImpact(severity) {
35
+ switch (severity) {
36
+ case "blocker":
37
+ return "Must fix before release";
38
+ case "major":
39
+ return "Should fix before release";
40
+ case "minor":
41
+ return "Review recommended before release";
42
+ case "informational":
43
+ return "No release impact; informational only";
44
+ case "skipped":
45
+ return "Not evaluated";
46
+ }
47
+ }
48
+ export function makeAndroidFinding(input) {
49
+ const id = buildFindingId(input.ruleId, input.manifestPath, input.identity, input.location);
50
+ const locationSuffix = input.location?.line !== undefined ? `:${input.location.line}` : "";
51
+ const evidenceParts = [
52
+ `confidence=${input.confidence}`,
53
+ input.identity ? `identity=${input.identity}` : undefined,
54
+ `location=${input.manifestPath}${locationSuffix}`,
55
+ ...(input.evidenceDetails ?? []),
56
+ ].filter((part) => Boolean(part));
57
+ return {
58
+ id,
59
+ title: input.title,
60
+ severity: input.severity,
61
+ category: "static-scan",
62
+ description: input.description,
63
+ evidence: evidenceParts.join(" | "),
64
+ affectedFiles: [input.manifestPath],
65
+ recommendation: input.recommendation,
66
+ releaseImpact: input.releaseImpact ?? defaultReleaseImpact(input.severity),
67
+ };
68
+ }
@@ -0,0 +1,97 @@
1
+ function isMalformed(entry) {
2
+ return entry.manifest.parseWarnings.some((w) => w.startsWith("Malformed XML:"));
3
+ }
4
+ export function buildAndroidManifestCheckResult(input) {
5
+ const { id, category, title, detection, manifests, findings, evidence, warnings } = input;
6
+ const sourcePaths = manifests.map((m) => m.manifestPath);
7
+ if (detection.projectKind === "non-android") {
8
+ return {
9
+ id,
10
+ category,
11
+ title,
12
+ status: "unsupported",
13
+ requirementLevel: "optional",
14
+ ran: false,
15
+ skipped: true,
16
+ skipInfo: {
17
+ checkId: id,
18
+ reason: "Target was not detected as an Android project.",
19
+ requirementLevel: "optional",
20
+ missingCapability: "android-detection",
21
+ verdictImpact: "does not apply to a non-Android target",
22
+ recommendedNextAction: "Re-run against an Android Gradle project, or verify project detection.",
23
+ },
24
+ evidence: [],
25
+ findings: [],
26
+ warnings: [],
27
+ errors: [],
28
+ sourcePaths: [],
29
+ confidence: "unknown",
30
+ environmentRequirements: [],
31
+ };
32
+ }
33
+ if (manifests.length === 0) {
34
+ return {
35
+ id,
36
+ category,
37
+ title,
38
+ status: "inconclusive",
39
+ requirementLevel: "required",
40
+ ran: true,
41
+ skipped: false,
42
+ evidence,
43
+ findings: [],
44
+ warnings: [...warnings, "No AndroidManifest.xml was found for this target; audit could not run."],
45
+ errors: [],
46
+ sourcePaths: [],
47
+ confidence: "unknown",
48
+ environmentRequirements: [],
49
+ };
50
+ }
51
+ const malformedEntries = manifests.filter(isMalformed);
52
+ const allMalformed = malformedEntries.length === manifests.length;
53
+ if (allMalformed) {
54
+ return {
55
+ id,
56
+ category,
57
+ title,
58
+ status: "error",
59
+ requirementLevel: "required",
60
+ ran: true,
61
+ skipped: false,
62
+ evidence,
63
+ findings: [],
64
+ warnings,
65
+ errors: malformedEntries.flatMap((entry) => entry.manifest.parseWarnings.filter((w) => w.startsWith("Malformed XML:"))),
66
+ sourcePaths,
67
+ confidence: "unknown",
68
+ environmentRequirements: [],
69
+ };
70
+ }
71
+ const confidence = malformedEntries.length > 0 ? "medium" : "high";
72
+ // Informational-only findings (e.g. "standard launcher activity is
73
+ // exported, as expected") are evidence/inventory, not a reportable issue —
74
+ // only minor/major/blocker findings cause a "failed" status, matching
75
+ // section 7.17's "completed and found no reportable issues may pass".
76
+ const hasReportableFinding = findings.some((finding) => finding.severity !== "informational");
77
+ const status = hasReportableFinding ? "failed" : "passed";
78
+ return {
79
+ id,
80
+ category,
81
+ title,
82
+ status,
83
+ requirementLevel: "required",
84
+ ran: true,
85
+ skipped: false,
86
+ evidence,
87
+ findings,
88
+ warnings: [
89
+ ...warnings,
90
+ ...malformedEntries.flatMap((entry) => entry.manifest.parseWarnings.filter((w) => w.startsWith("Malformed XML:"))),
91
+ ],
92
+ errors: [],
93
+ sourcePaths,
94
+ confidence,
95
+ environmentRequirements: [],
96
+ };
97
+ }
@@ -0,0 +1,183 @@
1
+ import { makeAndroidFinding } from "./androidFinding.js";
2
+ import { buildAndroidManifestCheckResult } from "./checkResultBuilder.js";
3
+ // ---------------------------------------------------------------------------
4
+ // v0.4.0 Batch 3 — initial deep-link audit.
5
+ //
6
+ // Distinguishes web links (http/https) from custom-scheme deep links, flags
7
+ // missing host/scheme restrictions and broad path patterns, and treats
8
+ // BROWSABLE-without-VIEW and VIEW-without-BROWSABLE as separate "incomplete
9
+ // filter" observations (agents.txt Batch 3 section 7.16). Never claims
10
+ // Digital Asset Links verification, domain ownership, or exploitability —
11
+ // findings are framed as static exposure/configuration review, at most
12
+ // "major" for the narrow high-confidence case of an explicitly exported,
13
+ // unprotected, wildcard-path activity.
14
+ // ---------------------------------------------------------------------------
15
+ const VIEW_ACTION = "android.intent.action.VIEW";
16
+ const BROWSABLE_CATEGORY = "android.intent.category.BROWSABLE";
17
+ const WILDCARD_PATTERN = /[*.]{1,2}\*|\.\*/;
18
+ function isPlaceholder(value) {
19
+ return value !== undefined && (value.includes("${") || value.startsWith("@"));
20
+ }
21
+ function classifyScheme(dataElements) {
22
+ const schemes = dataElements.map((d) => d.dataScheme).filter((s) => Boolean(s));
23
+ if (schemes.length === 0)
24
+ return "none";
25
+ if (schemes.some((s) => s === "http" || s === "https"))
26
+ return "web";
27
+ return "custom";
28
+ }
29
+ function auditDeepLinkFilter(entry, kind, component, filter, findings) {
30
+ const identity = component.name || "(unnamed component)";
31
+ const actions = filter.actions ?? [];
32
+ const categories = filter.categories ?? [];
33
+ const dataElements = filter.dataElements ?? filter.filterData;
34
+ const hasView = actions.includes(VIEW_ACTION);
35
+ const hasBrowsable = categories.includes(BROWSABLE_CATEGORY);
36
+ if (!hasView && !hasBrowsable)
37
+ return;
38
+ if (hasBrowsable && !hasView) {
39
+ findings.push(makeAndroidFinding({
40
+ ruleId: "android-deep-link-browsable-without-view",
41
+ title: `BROWSABLE category without a VIEW action on ${kind} "${identity}"`,
42
+ severity: "informational",
43
+ confidence: "high",
44
+ description: "This <intent-filter> declares category BROWSABLE without action VIEW, so it will not actually be treated as a browsable web/deep link by the platform.",
45
+ manifestPath: entry.manifestPath,
46
+ identity,
47
+ location: filter.location,
48
+ }));
49
+ return;
50
+ }
51
+ if (hasView && !hasBrowsable && dataElements.length === 0) {
52
+ // Plain VIEW with no data and no BROWSABLE — not a deep link, nothing to audit here.
53
+ return;
54
+ }
55
+ const exportedExplicitTrue = component.exported === true;
56
+ const unprotected = !component.permission;
57
+ const scheme = classifyScheme(dataElements);
58
+ const hasHost = dataElements.some((d) => Boolean(d.dataHost));
59
+ const placeholderEvidence = dataElements.some((d) => isPlaceholder(d.dataScheme) || isPlaceholder(d.dataHost) || isPlaceholder(d.dataPath) || isPlaceholder(d.dataPathPrefix) || isPlaceholder(d.dataPathPattern));
60
+ const broadPattern = dataElements.some((d) => (d.dataPathPattern && WILDCARD_PATTERN.test(d.dataPathPattern)) || d.dataPathPattern === ".*" || d.dataPathPattern === "*");
61
+ const evidenceDetails = [
62
+ `scheme=${scheme}`,
63
+ `hasHost=${hasHost}`,
64
+ `exported=${component.exported ?? "unspecified"}`,
65
+ `hasComponentPermission=${!unprotected}`,
66
+ `dataElementCount=${dataElements.length}`,
67
+ ];
68
+ if (placeholderEvidence) {
69
+ findings.push(makeAndroidFinding({
70
+ ruleId: "android-deep-link-placeholder-value",
71
+ title: `Deep-link data on ${kind} "${identity}" contains an unresolved placeholder or resource reference`,
72
+ severity: "informational",
73
+ confidence: "low",
74
+ description: "One or more deep-link data attributes (scheme/host/path) reference an unresolved manifest placeholder (e.g. ${applicationId}) or Android resource (e.g. @string/...). Effective exposure cannot be assessed statically.",
75
+ manifestPath: entry.manifestPath,
76
+ identity,
77
+ location: filter.location,
78
+ evidenceDetails,
79
+ }));
80
+ return;
81
+ }
82
+ if (broadPattern) {
83
+ const strongEvidence = exportedExplicitTrue && unprotected;
84
+ findings.push(makeAndroidFinding({
85
+ ruleId: "android-deep-link-broad-path-pattern",
86
+ title: `Broad/wildcard deep-link path pattern on ${kind} "${identity}"`,
87
+ severity: strongEvidence ? "major" : "minor",
88
+ confidence: strongEvidence ? "high" : "medium",
89
+ description: strongEvidence
90
+ ? "This deep-link filter uses a highly permissive pathPattern, the component is explicitly exported, and no component-level permission protects it — a strong static combination of broad matching and open reachability."
91
+ : "This deep-link filter uses a broad/wildcard-like pathPattern. Broad path matching is not automatically exploitable, but widens the set of URLs the app will handle.",
92
+ manifestPath: entry.manifestPath,
93
+ identity,
94
+ location: filter.location,
95
+ evidenceDetails,
96
+ recommendation: "Narrow the pathPattern to only the routes the app actually needs to handle, and validate any path/query data before use.",
97
+ }));
98
+ return;
99
+ }
100
+ if (scheme === "web" && !hasHost) {
101
+ findings.push(makeAndroidFinding({
102
+ ruleId: "android-deep-link-web-link-without-host",
103
+ title: `HTTP/HTTPS deep link on ${kind} "${identity}" has no host restriction`,
104
+ severity: "minor",
105
+ confidence: "medium",
106
+ description: "This web-link intent filter declares an http/https scheme without a host, so it is not restricted to a specific domain.",
107
+ manifestPath: entry.manifestPath,
108
+ identity,
109
+ location: filter.location,
110
+ evidenceDetails,
111
+ recommendation: "Add android:host to restrict which domains this filter matches.",
112
+ }));
113
+ return;
114
+ }
115
+ if (scheme === "custom" && !hasHost) {
116
+ findings.push(makeAndroidFinding({
117
+ ruleId: "android-deep-link-custom-scheme-without-host",
118
+ title: `Custom-scheme deep link on ${kind} "${identity}" has no host restriction`,
119
+ severity: "minor",
120
+ confidence: "medium",
121
+ description: "This custom-scheme intent filter declares no host restriction. Custom schemes are not automatically vulnerabilities, but an unrestricted scheme accepts any host/path combination.",
122
+ manifestPath: entry.manifestPath,
123
+ identity,
124
+ location: filter.location,
125
+ evidenceDetails,
126
+ recommendation: "Consider restricting the host or validating incoming URIs defensively in the handling code.",
127
+ }));
128
+ return;
129
+ }
130
+ // Bounded/safe deep link: explicit scheme + host (+ optional path
131
+ // restriction), no wildcard pattern, no placeholders — recorded for
132
+ // inventory purposes at informational severity only (ANDROID-B3-24).
133
+ findings.push(makeAndroidFinding({
134
+ ruleId: "android-deep-link-bounded",
135
+ title: `Deep link declared on ${kind} "${identity}"`,
136
+ severity: "informational",
137
+ confidence: "high",
138
+ description: "This deep-link filter declares an explicit scheme and host with bounded path evidence. No static exposure concern was identified.",
139
+ manifestPath: entry.manifestPath,
140
+ identity,
141
+ location: filter.location,
142
+ evidenceDetails,
143
+ }));
144
+ }
145
+ function componentGroups(entry) {
146
+ return [
147
+ { kind: "activity", components: entry.manifest.activities },
148
+ { kind: "activity-alias", components: entry.manifest.activityAliases ?? [] },
149
+ ];
150
+ }
151
+ export function auditAndroidDeepLinks(detection, manifests) {
152
+ const findings = [];
153
+ const evidence = [];
154
+ const warnings = [];
155
+ for (const entry of manifests) {
156
+ let candidateCount = 0;
157
+ for (const group of componentGroups(entry)) {
158
+ for (const component of group.components) {
159
+ for (const filter of component.intentFilters) {
160
+ const actions = filter.actions ?? [];
161
+ const categories = filter.categories ?? [];
162
+ if (actions.includes(VIEW_ACTION) || categories.includes(BROWSABLE_CATEGORY)) {
163
+ candidateCount += 1;
164
+ auditDeepLinkFilter(entry, group.kind, component, filter, findings);
165
+ }
166
+ }
167
+ }
168
+ }
169
+ if (candidateCount > 0) {
170
+ evidence.push(`${entry.manifestPath}: ${candidateCount} deep-link candidate intent-filter(s) inspected`);
171
+ }
172
+ }
173
+ return buildAndroidManifestCheckResult({
174
+ id: "android-deep-links-audit",
175
+ category: "android-deep-links",
176
+ title: "Android deep-link audit",
177
+ detection,
178
+ manifests,
179
+ findings,
180
+ evidence,
181
+ warnings,
182
+ });
183
+ }