@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,329 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.0 Batch 3 — bounded, deterministic, dependency-free XML parser for
3
+ // AndroidManifest.xml.
4
+ //
5
+ // No XML parsing dependency exists in this repository (see package.json —
6
+ // zero runtime dependencies), so this hand-written recursive-descent scanner
7
+ // avoids adding one, per agents.txt Batch 3 section 7.2 ("prefer a small
8
+ // bounded parser implementation ... Do not use brittle global regular
9
+ // expressions as the primary XML parser"). This is a manual character
10
+ // scanner (no regex-based tokenization), tracks line/column for every
11
+ // element and attribute, and resolves XML namespace URIs from `xmlns`/
12
+ // `xmlns:*` declarations rather than assuming any particular prefix — so an
13
+ // `android:` attribute is recognized even if a manifest (unusually)
14
+ // re-prefixes the Android namespace.
15
+ //
16
+ // Deliberately NOT implemented (out of scope for a manifest-only parser):
17
+ // DTD/entity resolution beyond skipping DOCTYPE, full XML 1.1 conformance,
18
+ // processing-instruction data, and CDATA content extraction (CDATA is
19
+ // skipped as opaque text — AndroidManifest.xml has no meaningful use of it).
20
+ // ---------------------------------------------------------------------------
21
+ export const ANDROID_NAMESPACE_URI = "http://schemas.android.com/apk/res/android";
22
+ class XmlSyntaxError extends Error {
23
+ location;
24
+ constructor(message, location) {
25
+ super(message);
26
+ this.location = location;
27
+ }
28
+ }
29
+ const NAME_START_PATTERN = /[A-Za-z_:]/;
30
+ const NAME_CHAR_PATTERN = /[A-Za-z0-9_.:-]/;
31
+ function isNameStartChar(ch) {
32
+ return ch !== undefined && NAME_START_PATTERN.test(ch);
33
+ }
34
+ function isNameChar(ch) {
35
+ return ch !== undefined && NAME_CHAR_PATTERN.test(ch);
36
+ }
37
+ function isWhitespace(ch) {
38
+ return ch === " " || ch === "\t" || ch === "\n" || ch === "\r";
39
+ }
40
+ class Scanner {
41
+ text;
42
+ pos = 0;
43
+ line = 1;
44
+ column = 1;
45
+ constructor(text) {
46
+ // Strip a UTF-8 BOM if present so it doesn't count as document content.
47
+ this.text = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
48
+ }
49
+ loc() {
50
+ return { line: this.line, column: this.column };
51
+ }
52
+ eof() {
53
+ return this.pos >= this.text.length;
54
+ }
55
+ peek(offset = 0) {
56
+ return this.text[this.pos + offset];
57
+ }
58
+ startsWith(literal) {
59
+ return this.text.startsWith(literal, this.pos);
60
+ }
61
+ advance() {
62
+ const ch = this.text[this.pos];
63
+ if (ch === undefined) {
64
+ throw new XmlSyntaxError("Unexpected end of document", this.loc());
65
+ }
66
+ this.pos += 1;
67
+ if (ch === "\n") {
68
+ this.line += 1;
69
+ this.column = 1;
70
+ }
71
+ else {
72
+ this.column += 1;
73
+ }
74
+ return ch;
75
+ }
76
+ skipWhitespace() {
77
+ while (!this.eof() && isWhitespace(this.peek()))
78
+ this.advance();
79
+ }
80
+ expect(literal) {
81
+ if (!this.startsWith(literal)) {
82
+ throw new XmlSyntaxError(`Expected "${literal}"`, this.loc());
83
+ }
84
+ for (let i = 0; i < literal.length; i++)
85
+ this.advance();
86
+ }
87
+ skipUntil(literal) {
88
+ while (!this.eof() && !this.startsWith(literal))
89
+ this.advance();
90
+ if (this.eof()) {
91
+ throw new XmlSyntaxError(`Unterminated construct, expected "${literal}"`, this.loc());
92
+ }
93
+ this.expect(literal);
94
+ }
95
+ // Like skipUntil, but returns the consumed text before `literal` instead of
96
+ // discarding it. Used to capture CDATA section content as element text.
97
+ captureUntil(literal) {
98
+ let captured = "";
99
+ while (!this.eof() && !this.startsWith(literal))
100
+ captured += this.advance();
101
+ if (this.eof()) {
102
+ throw new XmlSyntaxError(`Unterminated construct, expected "${literal}"`, this.loc());
103
+ }
104
+ this.expect(literal);
105
+ return captured;
106
+ }
107
+ }
108
+ function resolveNamespace(prefix, scope) {
109
+ return scope.get(prefix ?? "");
110
+ }
111
+ function splitQualifiedName(qualifiedName) {
112
+ const colonIndex = qualifiedName.indexOf(":");
113
+ if (colonIndex === -1)
114
+ return { localName: qualifiedName };
115
+ return { prefix: qualifiedName.slice(0, colonIndex), localName: qualifiedName.slice(colonIndex + 1) };
116
+ }
117
+ function readName(scanner) {
118
+ const start = scanner.loc();
119
+ if (!isNameStartChar(scanner.peek())) {
120
+ throw new XmlSyntaxError("Expected a name", start);
121
+ }
122
+ let name = "";
123
+ while (isNameChar(scanner.peek()))
124
+ name += scanner.advance();
125
+ return name;
126
+ }
127
+ function readQuotedValue(scanner) {
128
+ const quote = scanner.peek();
129
+ if (quote !== `"` && quote !== `'`) {
130
+ throw new XmlSyntaxError("Expected a quoted attribute value", scanner.loc());
131
+ }
132
+ scanner.advance();
133
+ let value = "";
134
+ while (!scanner.eof() && scanner.peek() !== quote) {
135
+ value += scanner.advance();
136
+ }
137
+ if (scanner.eof()) {
138
+ throw new XmlSyntaxError("Unterminated attribute value", scanner.loc());
139
+ }
140
+ scanner.advance();
141
+ return decodeXmlEntities(value);
142
+ }
143
+ function decodeXmlEntities(raw) {
144
+ return raw
145
+ .replace(/&lt;/g, "<")
146
+ .replace(/&gt;/g, ">")
147
+ .replace(/&apos;/g, "'")
148
+ .replace(/&quot;/g, '"')
149
+ .replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number(dec)))
150
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
151
+ .replace(/&amp;/g, "&");
152
+ }
153
+ function skipMisc(scanner, warnings) {
154
+ // Skips whitespace, comments, the XML declaration/processing instructions,
155
+ // and DOCTYPE declarations that may appear between/around elements.
156
+ for (;;) {
157
+ scanner.skipWhitespace();
158
+ if (scanner.startsWith("<!--")) {
159
+ scanner.skipUntil("-->");
160
+ continue;
161
+ }
162
+ if (scanner.startsWith("<?")) {
163
+ scanner.skipUntil("?>");
164
+ continue;
165
+ }
166
+ if (scanner.startsWith("<!DOCTYPE") || scanner.startsWith("<!doctype")) {
167
+ warnings.push({ message: "DOCTYPE declaration encountered and skipped (not evaluated)", location: scanner.loc() });
168
+ scanner.skipUntil(">");
169
+ continue;
170
+ }
171
+ if (scanner.startsWith("<![CDATA[")) {
172
+ scanner.skipUntil("]]>");
173
+ continue;
174
+ }
175
+ break;
176
+ }
177
+ }
178
+ function parseAttributes(scanner) {
179
+ const rawAttributes = [];
180
+ for (;;) {
181
+ scanner.skipWhitespace();
182
+ const next = scanner.peek();
183
+ if (next === undefined || next === "/" || next === ">")
184
+ break;
185
+ const location = scanner.loc();
186
+ const rawName = readName(scanner);
187
+ scanner.skipWhitespace();
188
+ scanner.expect("=");
189
+ scanner.skipWhitespace();
190
+ const value = readQuotedValue(scanner);
191
+ rawAttributes.push({ rawName, value, location });
192
+ }
193
+ // Build the namespace scope contributed by this element's own xmlns/xmlns:*
194
+ // attributes so attribute namespace resolution can use it immediately
195
+ // (XML allows an element to both declare and use a namespace prefix).
196
+ const localScope = new Map();
197
+ for (const attr of rawAttributes) {
198
+ if (attr.rawName === "xmlns") {
199
+ localScope.set("", attr.value);
200
+ }
201
+ else if (attr.rawName.startsWith("xmlns:")) {
202
+ localScope.set(attr.rawName.slice("xmlns:".length), attr.value);
203
+ }
204
+ }
205
+ return { rawAttributes, localScope };
206
+ }
207
+ // Resolves each raw attribute's namespace URI against the combined
208
+ // (ancestor + this element's own) namespace scope. `xmlns`/`xmlns:*`
209
+ // attributes themselves are preserved as regular attributes (some manifest
210
+ // tooling inspects them) but are not namespace-resolved.
211
+ function resolveAttributeNamespaces(rawAttributes, scope, warnings) {
212
+ return rawAttributes.map(({ rawName, value, location }) => {
213
+ const { prefix, localName } = splitQualifiedName(rawName);
214
+ if (rawName === "xmlns" || rawName.startsWith("xmlns:")) {
215
+ return { rawName, localName, value, location };
216
+ }
217
+ if (prefix === undefined) {
218
+ return { rawName, localName, value, location };
219
+ }
220
+ const namespaceUri = resolveNamespace(prefix, scope);
221
+ if (namespaceUri === undefined) {
222
+ warnings.push({ message: `Unresolved namespace prefix "${prefix}" on attribute "${rawName}"`, location });
223
+ }
224
+ return { rawName, prefix, localName, namespaceUri, value, location };
225
+ });
226
+ }
227
+ function parseElement(scanner, parentScope, warnings) {
228
+ const location = scanner.loc();
229
+ scanner.expect("<");
230
+ const tagName = readName(scanner);
231
+ const { rawAttributes, localScope } = parseAttributes(scanner);
232
+ const scope = localScope.size > 0 ? new Map([...parentScope, ...localScope]) : parentScope;
233
+ const attributes = resolveAttributeNamespaces(rawAttributes, scope, warnings);
234
+ const { prefix, localName } = splitQualifiedName(tagName);
235
+ const namespaceUri = prefix !== undefined ? resolveNamespace(prefix, scope) : resolveNamespace(undefined, scope);
236
+ scanner.skipWhitespace();
237
+ if (scanner.startsWith("/>")) {
238
+ scanner.expect("/>");
239
+ return { tagName, prefix, localName, namespaceUri, attributes, children: [], location, textContent: "" };
240
+ }
241
+ scanner.expect(">");
242
+ const children = [];
243
+ const textParts = [];
244
+ for (;;) {
245
+ skipTextAndMisc(scanner, warnings, textParts);
246
+ if (scanner.startsWith("</")) {
247
+ scanner.expect("</");
248
+ const closingName = readName(scanner);
249
+ scanner.skipWhitespace();
250
+ scanner.expect(">");
251
+ if (closingName !== tagName) {
252
+ throw new XmlSyntaxError(`Mismatched closing tag: expected "</${tagName}>" but found "</${closingName}>"`, location);
253
+ }
254
+ break;
255
+ }
256
+ if (scanner.eof()) {
257
+ throw new XmlSyntaxError(`Unterminated element "<${tagName}>" (missing closing tag)`, location);
258
+ }
259
+ if (scanner.peek() === "<" && isNameStartChar(scanner.peek(1))) {
260
+ children.push(parseElement(scanner, scope, warnings));
261
+ continue;
262
+ }
263
+ // Any other content (unexpected `<` not matching a comment/PI/CDATA/
264
+ // element/closing-tag pattern) is treated as malformed input.
265
+ throw new XmlSyntaxError("Unexpected content while looking for a child element or closing tag", scanner.loc());
266
+ }
267
+ return { tagName, prefix, localName, namespaceUri, attributes, children, location, textContent: decodeXmlEntities(textParts.join("")) };
268
+ }
269
+ // v0.4.1 Batch 2 — `textParts` accumulates direct text/CDATA content so
270
+ // callers needing element text content (e.g. Network Security Config's
271
+ // <domain>/<pin> elements) can read it via XmlElement.textContent. Comments,
272
+ // processing instructions, and DOCTYPE are still skipped, never counted as
273
+ // text — only true character data and CDATA sections are captured.
274
+ function skipTextAndMisc(scanner, warnings, textParts) {
275
+ for (;;) {
276
+ if (scanner.startsWith("<!--")) {
277
+ scanner.skipUntil("-->");
278
+ continue;
279
+ }
280
+ if (scanner.startsWith("<![CDATA[")) {
281
+ scanner.expect("<![CDATA[");
282
+ textParts.push(scanner.captureUntil("]]>"));
283
+ continue;
284
+ }
285
+ if (scanner.startsWith("<?")) {
286
+ scanner.skipUntil("?>");
287
+ continue;
288
+ }
289
+ if (scanner.peek() === "<")
290
+ break;
291
+ if (scanner.eof())
292
+ break;
293
+ textParts.push(scanner.advance());
294
+ }
295
+ }
296
+ // Parses `xmlText` into a small element tree. Never throws: all failures are
297
+ // reported via the `ok: false` branch with a bounded message and location.
298
+ export function parseXmlDocument(xmlText) {
299
+ const warnings = [];
300
+ try {
301
+ const scanner = new Scanner(xmlText);
302
+ skipMisc(scanner, warnings);
303
+ if (scanner.eof() || scanner.peek() !== "<") {
304
+ return { ok: false, error: { message: "No root element found", location: scanner.loc() }, warnings };
305
+ }
306
+ const root = parseElement(scanner, new Map(), warnings);
307
+ skipMisc(scanner, warnings);
308
+ if (!scanner.eof()) {
309
+ warnings.push({ message: "Unexpected trailing content after the root element was ignored", location: scanner.loc() });
310
+ }
311
+ return { ok: true, root, warnings };
312
+ }
313
+ catch (error) {
314
+ if (error instanceof XmlSyntaxError) {
315
+ return { ok: false, error: { message: error.message, location: error.location }, warnings };
316
+ }
317
+ return {
318
+ ok: false,
319
+ error: { message: error instanceof Error ? error.message : "Unknown XML parse error", location: { line: 1, column: 1 } },
320
+ warnings,
321
+ };
322
+ }
323
+ }
324
+ export function findChildren(element, localName, namespaceUri) {
325
+ return element.children.filter((child) => child.localName === localName && (namespaceUri === undefined || child.namespaceUri === namespaceUri));
326
+ }
327
+ export function getAttribute(element, localName, namespaceUri = ANDROID_NAMESPACE_URI) {
328
+ return element.attributes.find((attr) => attr.localName === localName && attr.namespaceUri === namespaceUri);
329
+ }
@@ -0,0 +1,29 @@
1
+ // ---------------------------------------------------------------------------
2
+ // v0.4.0 Batch 1 — Android profile identity.
3
+ //
4
+ // A single canonical profile id ("android") is used for all Android Gradle
5
+ // projects. Application vs. library vs. Compose-vs-XML/View distinctions are
6
+ // represented as classification data on AndroidDetectionResult (see
7
+ // detection.ts), not as separate profile ids — this keeps the public profile
8
+ // surface minimal, per the batch's "no unnecessary user-facing aliases" rule.
9
+ // ---------------------------------------------------------------------------
10
+ export const ANDROID_PLATFORM = "android";
11
+ export const ANDROID_PROFILE_ID = "android";
12
+ export function createAndroidProfile(partial) {
13
+ return {
14
+ id: ANDROID_PROFILE_ID,
15
+ displayName: "Android",
16
+ platform: ANDROID_PLATFORM,
17
+ projectType: partial.projectType,
18
+ projectSubtype: partial.projectSubtype,
19
+ profileVersion: partial.profileVersion,
20
+ detectionConfidence: partial.detectionConfidence,
21
+ evidence: partial.evidence ?? [],
22
+ unsupportedNotes: partial.unsupportedNotes ?? [],
23
+ supportedCapabilities: partial.supportedCapabilities ?? [],
24
+ limitations: partial.limitations ?? [],
25
+ };
26
+ }
27
+ export function isAndroidProfile(profile) {
28
+ return profile.platform === ANDROID_PLATFORM && profile.id === ANDROID_PROFILE_ID;
29
+ }
@@ -0,0 +1,135 @@
1
+ import { androidVerdictToHumanLabel } from "../validate/androidVerdict.js";
2
+ const GRADLE_OPERATION_CHECK_PREFIX = "android-gradle-";
3
+ const GRADLE_METADATA_CHECK_ID = "android-gradle-metadata";
4
+ // v0.4.1 Batch 8 — fixed check-id order for the eleven internal advanced
5
+ // checks and four external tools (agents.txt Batch 8 sections 9.1/10.5).
6
+ // Re-declared here (not imported from each check's checkResult.ts) to avoid
7
+ // a report -> advancedSecurity/externalTools import-fan-out; each id is a
8
+ // stable literal already asserted by that check's own regression tests.
9
+ const ADVANCED_SECURITY_CHECK_ID_ORDER = [
10
+ "android-network-security-audit",
11
+ "android-backup-configuration-audit",
12
+ "android-release-configuration-audit",
13
+ "android-secret-candidates-audit",
14
+ "android-signing-configuration-audit",
15
+ "android-webview-security-audit",
16
+ "android-file-provider-audit",
17
+ "android-sensitive-storage-audit",
18
+ "android-sensitive-logging-audit",
19
+ "android-clipboard-security-audit",
20
+ "android-firebase-google-services-audit",
21
+ ];
22
+ const EXTERNAL_TOOL_CHECK_ID_ORDER = ["android-semgrep-audit", "android-osv-audit", "android-lint-audit", "android-dependency-check-audit"];
23
+ const STATIC_ANALYSIS_LIMITATIONS = [
24
+ "Static Android project analysis does not prove runtime behavior.",
25
+ "Manifests are parsed independently and are not merged.",
26
+ "Selected local resource references are resolved conservatively; manifest placeholders, full resource merging, and overlay precedence are not resolved.",
27
+ "Gradle files are not evaluated (no Groovy/Kotlin execution).",
28
+ "Dynamic Gradle values may remain unresolved.",
29
+ "Same-file duplicate Gradle assignments use first-match extraction (a known limitation — see agents.txt Batch 4/5 notes).",
30
+ "Optional Gradle execution is not performed unless explicitly requested via --android-gradle-operations.",
31
+ "A successful debug build does not prove release build, signing, runtime security, or Play readiness.",
32
+ "Android SDK or dependency availability may limit optional checks.",
33
+ "Digital Asset Links are not verified.",
34
+ "Domain ownership is not verified.",
35
+ "Current Google Play policy is not checked by this tool.",
36
+ "APK/AAB contents and signing are not inspected.",
37
+ ];
38
+ function groupFindingsBySeverity(findings) {
39
+ const grouped = {};
40
+ for (const finding of findings) {
41
+ if (!grouped[finding.severity])
42
+ grouped[finding.severity] = [];
43
+ grouped[finding.severity].push(finding);
44
+ }
45
+ return grouped;
46
+ }
47
+ // Builds the report model from a complete AndroidValidationResult (Batch 5).
48
+ export function toAndroidReportModel(result, options = {}) {
49
+ const permissionCount = result.manifests.reduce((sum, m) => sum + m.permissions.length, 0);
50
+ const componentCount = result.manifests.reduce((sum, m) => sum + m.activities.length + (m.activityAliases?.length ?? 0) + m.services.length + m.receivers.length + m.providers.length, 0);
51
+ const intentFilterCount = result.manifests.reduce((sum, m) => sum + [...m.activities, ...(m.activityAliases ?? []), ...m.services, ...m.receivers].reduce((s, c) => s + c.intentFilters.length, 0), 0);
52
+ const deepLinkCount = result.manifests.reduce((sum, m) => sum + m.deepLinks.length, 0);
53
+ const gradleOperationResults = result.checks.filter((c) => c.id.startsWith(GRADLE_OPERATION_CHECK_PREFIX) && c.id !== GRADLE_METADATA_CHECK_ID);
54
+ const gradleMetadataCheck = result.checks.find((c) => c.id === GRADLE_METADATA_CHECK_ID);
55
+ const checksById = new Map(result.checks.map((c) => [c.id, c]));
56
+ const advancedSecurityChecks = ADVANCED_SECURITY_CHECK_ID_ORDER.map((id) => checksById.get(id)).filter((c) => Boolean(c));
57
+ const externalToolChecks = EXTERNAL_TOOL_CHECK_ID_ORDER.map((id) => checksById.get(id)).filter((c) => Boolean(c));
58
+ const candidateEvidenceCount = result.checks.reduce((sum, c) => sum + (c.candidateEvidence?.length ?? 0), 0);
59
+ const verdict = String(result.verdict);
60
+ const isRealVerdict = result.verdict !== "not-calculated";
61
+ return {
62
+ executiveSummary: `Android security-validation summary for ${result.target.local.targetRoot} — ${result.manifests.length} manifest(s), ${result.checks.length} check(s), ${result.findings.length} finding(s).`,
63
+ metadata: {
64
+ tool: result.tool,
65
+ target: result.target,
66
+ profile: options.profile ?? "android",
67
+ requestedGradleOperations: options.requestedGradleOperations ?? [],
68
+ requestedExternalTools: options.requestedExternalTools ?? [],
69
+ externalNetworkPolicy: options.externalNetworkPolicy ?? "deny",
70
+ generatedAt: result.finishedAt,
71
+ totalDurationMs: result.durationMs,
72
+ },
73
+ detectionSummary: {
74
+ title: "Detection",
75
+ summary: result.detection.detected
76
+ ? `Detected (confidence: ${result.detection.confidence})`
77
+ : `Not detected (confidence: ${result.detection.confidence})`,
78
+ },
79
+ androidProjectSummary: { title: "Android project classification", summary: result.target.classification.projectKind },
80
+ uiToolkitSummary: { title: "UI toolkit classification", summary: result.target.classification.uiToolkit },
81
+ moduleSummary: result.detection.modules.map((m) => ({ path: m.path, kind: m.kind, uiToolkit: m.uiToolkit })),
82
+ manifestSummary: { title: "Manifests", summary: `${result.manifests.length} manifest(s) parsed independently (not merged)` },
83
+ permissionsSummary: { title: "Permissions", summary: `${permissionCount} declared permission(s) across all manifests` },
84
+ componentSummary: { title: "Components", summary: `${componentCount} component(s) across all manifests` },
85
+ intentFilterSummary: { title: "Intent filters", summary: `${intentFilterCount} intent-filter(s) across all manifests` },
86
+ deepLinkSummary: { title: "Deep links", summary: `${deepLinkCount} deep-link data entr(y/ies) across all manifests` },
87
+ gradleMetadataSummary: {
88
+ title: "Static Gradle metadata",
89
+ summary: `${result.gradle.modules.length} module(s); confidence: ${result.gradle.metadataConfidence}`,
90
+ },
91
+ gradleCheckSummary: gradleMetadataCheck ? { title: "Gradle metadata check", summary: `status: ${gradleMetadataCheck.status}` } : undefined,
92
+ gradleOperationResults,
93
+ advancedSecurityChecks,
94
+ externalToolChecks,
95
+ candidateEvidenceSummary: {
96
+ title: "Candidate evidence (review required, not confirmed findings)",
97
+ summary: `${candidateEvidenceCount} candidate evidence item(s) across all checks`,
98
+ },
99
+ releaseMetadataSummary: {
100
+ title: "Release metadata",
101
+ summary: result.releaseMetadata
102
+ ? result.releaseMetadata.applicationModuleSelectionNote
103
+ : result.releaseMetadataSummary.note,
104
+ },
105
+ playReadinessSummary: {
106
+ title: "Play readiness",
107
+ summary: result.playReadiness ? result.playReadiness.note : result.playReadinessSummary.note,
108
+ },
109
+ playReadinessItems: result.playReadiness?.items.map((i) => ({ id: i.id, title: i.title, status: i.status, detail: i.detail })) ?? [],
110
+ findingsBySeverity: groupFindingsBySeverity(result.findings),
111
+ checks: result.checks,
112
+ skippedChecks: result.skippedChecks,
113
+ environmentLimitations: result.target.environmentCapabilities.notes,
114
+ targetMutationSummary: {
115
+ title: "Target mutation evidence",
116
+ summary: result.targetMutationEvidence
117
+ ? result.targetMutationEvidence.comparable
118
+ ? `${result.targetMutationEvidence.unexpectedChanges.length} unexpected change(s), ${result.targetMutationEvidence.expectedGeneratedChanges.length} expected generated output(s)`
119
+ : `unavailable: ${result.targetMutationEvidence.reason ?? "unknown reason"}`
120
+ : "not captured",
121
+ },
122
+ staticAnalysisLimitations: STATIC_ANALYSIS_LIMITATIONS,
123
+ verdict,
124
+ verdictHumanLabel: isRealVerdict ? androidVerdictToHumanLabel(result.verdict) : "not calculated",
125
+ verdictReasons: result.verdictReasons ?? [],
126
+ recommendedNextStep: result.recommendedNextStep ?? "",
127
+ linkedArtifactReferences: result.reportReferences,
128
+ };
129
+ }
130
+ // Minimal deterministic JSON serializer — object literals above are built
131
+ // with a fixed key order, so equivalent inputs always serialize identically
132
+ // (JSON.stringify preserves string-key insertion order).
133
+ export function serializeAndroidReportModel(model) {
134
+ return JSON.stringify(model, null, 2);
135
+ }