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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +269 -47
  2. package/dist/scripts/audits/runAudit.js +28 -1
  3. package/dist/scripts/security/validate.js +38 -5
  4. package/dist/src/audits/core/auditConfig.js +16 -1
  5. package/dist/src/audits/core/auditRunner.js +6 -2
  6. package/dist/src/audits/report/auditReportModel.js +7 -0
  7. package/dist/src/audits/report/renderAuditJsonReport.js +1 -0
  8. package/dist/src/audits/report/renderAuditTextReport.js +37 -0
  9. package/dist/src/audits/security/androidAuditIntegration.js +145 -0
  10. package/dist/src/audits/security/mapAndroidSecurityFindingToAuditIssue.js +116 -0
  11. package/dist/src/audits/security/mapSecurityFindingToAuditIssue.js +15 -5
  12. package/dist/src/audits/security/securityAuditAdapter.js +20 -2
  13. package/dist/src/audits/security/securityAuditTypes.js +37 -0
  14. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/analyzeBackupConfiguration.js +300 -0
  15. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/checkResult.js +41 -0
  16. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/index.js +8 -0
  17. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/manifestEvidence.js +102 -0
  18. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/parseBackupRules.js +110 -0
  19. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/types.js +27 -0
  20. package/dist/src/mobile/android/advancedSecurity/candidateEvidence.js +50 -0
  21. package/dist/src/mobile/android/advancedSecurity/clipboard/analyzeClipboardSecurity.js +166 -0
  22. package/dist/src/mobile/android/advancedSecurity/clipboard/checkResult.js +74 -0
  23. package/dist/src/mobile/android/advancedSecurity/clipboard/collectClipboardEvidence.js +82 -0
  24. package/dist/src/mobile/android/advancedSecurity/clipboard/index.js +1 -0
  25. package/dist/src/mobile/android/advancedSecurity/clipboard/types.js +1 -0
  26. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/checkResult.js +255 -0
  27. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/index.js +1 -0
  28. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintSarif.js +37 -0
  29. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintXml.js +36 -0
  30. package/dist/src/mobile/android/advancedSecurity/externalTools/artifacts.js +95 -0
  31. package/dist/src/mobile/android/advancedSecurity/externalTools/boundedOutput.js +33 -0
  32. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/checkResult.js +173 -0
  33. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/command.js +33 -0
  34. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/index.js +1 -0
  35. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/parseDependencyCheckJson.js +133 -0
  36. package/dist/src/mobile/android/advancedSecurity/externalTools/discoverExecutable.js +13 -0
  37. package/dist/src/mobile/android/advancedSecurity/externalTools/index.js +8 -0
  38. package/dist/src/mobile/android/advancedSecurity/externalTools/minimalEnvironment.js +26 -0
  39. package/dist/src/mobile/android/advancedSecurity/externalTools/mutation.js +77 -0
  40. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/checkResult.js +161 -0
  41. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/command.js +25 -0
  42. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/index.js +1 -0
  43. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/parseOsvJson.js +136 -0
  44. package/dist/src/mobile/android/advancedSecurity/externalTools/runBoundedExternalTool.js +51 -0
  45. package/dist/src/mobile/android/advancedSecurity/externalTools/runRequestedAndroidExternalTools.js +54 -0
  46. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/checkResult.js +170 -0
  47. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/command.js +28 -0
  48. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/index.js +2 -0
  49. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/parseSemgrepJson.js +141 -0
  50. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/rules.js +105 -0
  51. package/dist/src/mobile/android/advancedSecurity/externalTools/types.js +107 -0
  52. package/dist/src/mobile/android/advancedSecurity/fileProvider/checkResult.js +188 -0
  53. package/dist/src/mobile/android/advancedSecurity/fileProvider/index.js +1 -0
  54. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/analyzeFirebaseGoogleServices.js +197 -0
  55. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/checkResult.js +77 -0
  56. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/collectFirebaseServiceEvidence.js +193 -0
  57. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/discoverFirebaseArtifacts.js +128 -0
  58. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/index.js +1 -0
  59. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseDatabaseRules.js +55 -0
  60. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirebaseConfig.js +75 -0
  61. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirestoreStorageRules.js +88 -0
  62. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseGoogleServicesConfig.js +55 -0
  63. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/types.js +6 -0
  64. package/dist/src/mobile/android/advancedSecurity/networkSecurity/analyzeNetworkSecurity.js +394 -0
  65. package/dist/src/mobile/android/advancedSecurity/networkSecurity/checkResult.js +47 -0
  66. package/dist/src/mobile/android/advancedSecurity/networkSecurity/deriveEffectiveNetworkPolicy.js +52 -0
  67. package/dist/src/mobile/android/advancedSecurity/networkSecurity/index.js +14 -0
  68. package/dist/src/mobile/android/advancedSecurity/networkSecurity/manifestEvidence.js +109 -0
  69. package/dist/src/mobile/android/advancedSecurity/networkSecurity/parseNetworkSecurityConfig.js +186 -0
  70. package/dist/src/mobile/android/advancedSecurity/networkSecurity/types.js +1 -0
  71. package/dist/src/mobile/android/advancedSecurity/ordering.js +44 -0
  72. package/dist/src/mobile/android/advancedSecurity/redaction.js +54 -0
  73. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/analyzeReleaseConfiguration.js +221 -0
  74. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/checkResult.js +39 -0
  75. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/index.js +7 -0
  76. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/manifestEvidence.js +19 -0
  77. package/dist/src/mobile/android/advancedSecurity/resourceReference.js +69 -0
  78. package/dist/src/mobile/android/advancedSecurity/resourceResolution.js +118 -0
  79. package/dist/src/mobile/android/advancedSecurity/ruleIds.js +141 -0
  80. package/dist/src/mobile/android/advancedSecurity/secretCandidates/analyzeSecretCandidates.js +116 -0
  81. package/dist/src/mobile/android/advancedSecurity/secretCandidates/checkResult.js +80 -0
  82. package/dist/src/mobile/android/advancedSecurity/secretCandidates/discoverSecretSourceFiles.js +152 -0
  83. package/dist/src/mobile/android/advancedSecurity/secretCandidates/index.js +9 -0
  84. package/dist/src/mobile/android/advancedSecurity/secretCandidates/matchSecretCandidates.js +136 -0
  85. package/dist/src/mobile/android/advancedSecurity/secretCandidates/suppressValue.js +61 -0
  86. package/dist/src/mobile/android/advancedSecurity/secretCandidates/types.js +11 -0
  87. package/dist/src/mobile/android/advancedSecurity/sensitiveCategories.js +34 -0
  88. package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifyDirectExpression.js +99 -0
  89. package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifySensitiveIdentifier.js +111 -0
  90. package/dist/src/mobile/android/advancedSecurity/sensitiveData/index.js +3 -0
  91. package/dist/src/mobile/android/advancedSecurity/sensitiveData/localSourceContext.js +90 -0
  92. package/dist/src/mobile/android/advancedSecurity/sensitiveData/splitArguments.js +59 -0
  93. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/analyzeSensitiveLogging.js +117 -0
  94. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/checkResult.js +74 -0
  95. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/collectLoggingEvidence.js +87 -0
  96. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/index.js +1 -0
  97. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/types.js +1 -0
  98. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/analyzeSensitiveStorage.js +182 -0
  99. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/checkResult.js +74 -0
  100. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/collectStorageEvidence.js +109 -0
  101. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/index.js +1 -0
  102. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/types.js +4 -0
  103. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/analyzeSigningConfiguration.js +270 -0
  104. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/checkResult.js +119 -0
  105. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/discoverKeystoreCandidates.js +55 -0
  106. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/extractSigningConfigurations.js +81 -0
  107. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/index.js +8 -0
  108. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/types.js +20 -0
  109. package/dist/src/mobile/android/advancedSecurity/sourceLocation.js +15 -0
  110. package/dist/src/mobile/android/advancedSecurity/webview/checkResult.js +210 -0
  111. package/dist/src/mobile/android/advancedSecurity/webview/index.js +1 -0
  112. package/dist/src/mobile/android/gradle/moduleMetadataExtractor.js +123 -17
  113. package/dist/src/mobile/android/manifest/parseAndroidManifest.js +27 -4
  114. package/dist/src/mobile/android/manifest/xml/parseXml.js +25 -9
  115. package/dist/src/mobile/android/report/model.js +32 -1
  116. package/dist/src/mobile/android/report/renderAndroidReport.js +63 -0
  117. package/dist/src/mobile/android/validate/validateAndroidTarget.js +146 -3
  118. package/dist/src/mobile/android/validation/checkResult.js +7 -0
  119. package/dist/src/securityValidation/commandRunner.js +10 -1
  120. package/dist/src/securityValidation/validate/cliOptions.js +46 -0
  121. package/package.json +1 -1
  122. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/debug/res/xml/ambiguous_backup_rules.xml +4 -0
  123. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/ambiguous_backup_rules.xml +4 -0
  124. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/backup_rules.xml +5 -0
  125. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/broad_backup_rules.xml +4 -0
  126. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/data_extraction_rules.xml +10 -0
  127. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/unsupported_domain_rules.xml +4 -0
  128. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/malformed_backup_rules.xml +4 -0
  129. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/unsupported_root.xml +4 -0
  130. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/debug/res/xml/ambiguous_config.xml +4 -0
  131. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/ambiguous_config.xml +4 -0
  132. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/base_cleartext_false.xml +8 -0
  133. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/broad_and_duplicate_domains.xml +8 -0
  134. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/empty_domain.xml +6 -0
  135. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/malformed_pin.xml +11 -0
  136. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/network_security_config.xml +26 -0
  137. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/malformed_network_security_config.xml +6 -0
  138. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/unsupported_root.xml +4 -0
  139. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-dynamic.gradle +7 -0
  140. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-false.gradle +9 -0
  141. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-true.gradle +13 -0
  142. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-false.gradle.kts +7 -0
  143. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-true.gradle.kts +10 -0
  144. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/missing-release-build-type.gradle.kts +7 -0
  145. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/debug/res/xml/network_security_config.xml +8 -0
  146. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/backup_rules.xml +4 -0
  147. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/network_security_config.xml +8 -0
  148. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-b/src/main/res/xml/malformed_network_security_config.xml +7 -0
  149. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.java +8 -0
  150. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.kt +9 -0
  151. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle +11 -0
  152. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle.kts +11 -0
  153. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/google-services.json +16 -0
  154. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/local.properties +3 -0
  155. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/binary.properties +0 -0
  156. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/oversized.json +1 -0
  157. package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/app/fake-release-keystore.jks +1 -0
  158. package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/build/generated-fake.keystore +1 -0
@@ -11,7 +11,10 @@ import { extractBuildFileEvidence } from "../detect/buildFileEvidence.js";
11
11
  // excerpt" strategy for each key. Comments are stripped first so a
12
12
  // commented-out assignment never becomes active metadata (ANDROID-B4-05).
13
13
  // ---------------------------------------------------------------------------
14
- function stripComments(text) {
14
+ // v0.4.1 Batch 4 — exported (was module-private) so the new signing-
15
+ // configuration module can reuse the exact same comment-stripping/literal-
16
+ // extraction/brace-matching technique instead of a second implementation.
17
+ export function stripComments(text) {
15
18
  // Replaces comment characters with spaces (not empty string) so any
16
19
  // surviving line/column-independent offsets are not shifted — this batch
17
20
  // does not track per-field locations, but keeping length stable is cheap
@@ -21,7 +24,7 @@ function stripComments(text) {
21
24
  .replace(/\/\/[^\n]*/g, (match) => " ".repeat(match.length));
22
25
  }
23
26
  const STRING_OR_INT = `(?:'([^']*)'|"([^"]*)"|(-?\\d+))`;
24
- function extractLiteral(text, keyAlternatives) {
27
+ export function extractLiteral(text, keyAlternatives) {
25
28
  const keyPattern = keyAlternatives.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
26
29
  // Anchored to the start of a line (allowing leading indentation) so a key
27
30
  // name that merely appears as a word inside an unrelated string literal —
@@ -46,32 +49,45 @@ function extractLiteral(text, keyAlternatives) {
46
49
  }
47
50
  return {};
48
51
  }
49
- // Finds the top-level named entries inside a `<blockKeyword> { ... }` block
50
- // (e.g. `buildTypes { debug { ... } release { ... } }` or
51
- // `sourceSets { main { ... } androidTest { ... } }`), supporting both the
52
- // bare-identifier Groovy/Kotlin-DSL form and the Kotlin DSL
53
- // getByName/create/named(...) form. Bounded by brace-depth matching, not a
54
- // full parser — nested unrelated blocks inside are not descended into for
55
- // name extraction beyond the immediate children.
56
- function findNamedBlockEntries(text, blockKeyword) {
52
+ // Returns the inner text of the first `<blockKeyword> { ... }` block found,
53
+ // bounded by brace-depth matching (not a full parser). Shared by
54
+ // findNamedBlockEntries (names only) and extractNamedSubBlocks (v0.4.1
55
+ // Batch 3 — names plus each entry's own block content).
56
+ export function findBlockBody(text, blockKeyword) {
57
57
  const blockStart = text.search(new RegExp(`\\b${blockKeyword}\\s*\\{`));
58
58
  if (blockStart === -1)
59
- return [];
59
+ return undefined;
60
60
  const openBraceIndex = text.indexOf("{", blockStart);
61
+ return sliceBlockAt(text, openBraceIndex);
62
+ }
63
+ // Given the index of an opening `{`, returns the text between it and its
64
+ // matching `}` (brace-depth bounded), or undefined if unterminated.
65
+ export function sliceBlockAt(text, openBraceIndex) {
66
+ if (text[openBraceIndex] !== "{")
67
+ return undefined;
61
68
  let depth = 0;
62
- let endIndex = text.length;
63
69
  for (let i = openBraceIndex; i < text.length; i++) {
64
70
  if (text[i] === "{")
65
71
  depth += 1;
66
72
  else if (text[i] === "}") {
67
73
  depth -= 1;
68
- if (depth === 0) {
69
- endIndex = i;
70
- break;
71
- }
74
+ if (depth === 0)
75
+ return text.slice(openBraceIndex + 1, i);
72
76
  }
73
77
  }
74
- const block = text.slice(openBraceIndex + 1, endIndex);
78
+ return undefined;
79
+ }
80
+ // Finds the top-level named entries inside a `<blockKeyword> { ... }` block
81
+ // (e.g. `buildTypes { debug { ... } release { ... } }` or
82
+ // `sourceSets { main { ... } androidTest { ... } }`), supporting both the
83
+ // bare-identifier Groovy/Kotlin-DSL form and the Kotlin DSL
84
+ // getByName/create/named(...) form. Bounded by brace-depth matching, not a
85
+ // full parser — nested unrelated blocks inside are not descended into for
86
+ // name extraction beyond the immediate children.
87
+ function findNamedBlockEntries(text, blockKeyword) {
88
+ const block = findBlockBody(text, blockKeyword);
89
+ if (block === undefined)
90
+ return [];
75
91
  const names = new Set();
76
92
  for (const match of block.matchAll(/\b([A-Za-z_][A-Za-z0-9_]*)\s*\{/g)) {
77
93
  names.add(match[1]);
@@ -81,6 +97,95 @@ function findNamedBlockEntries(text, blockKeyword) {
81
97
  }
82
98
  return [...names].sort((a, b) => a.localeCompare(b));
83
99
  }
100
+ // v0.4.1 Batch 3 — like findNamedBlockEntries, but also returns each named
101
+ // entry's own block content so per-build-type literal fields (debuggable,
102
+ // minifyEnabled, etc.) can be extracted. Reuses the exact same bounded
103
+ // brace-matching technique and the same two name-pattern forms — not a
104
+ // second parser, just returning more of what was already being scanned.
105
+ export function extractNamedSubBlocks(text, blockKeyword) {
106
+ const block = findBlockBody(text, blockKeyword);
107
+ if (block === undefined)
108
+ return [];
109
+ const results = [];
110
+ const seen = new Set();
111
+ for (const match of block.matchAll(/\b([A-Za-z_][A-Za-z0-9_]*)\s*\{/g)) {
112
+ const name = match[1];
113
+ if (seen.has(name))
114
+ continue;
115
+ const openBraceIndex = match.index + match[0].length - 1;
116
+ const content = sliceBlockAt(block, openBraceIndex);
117
+ if (content !== undefined) {
118
+ results.push({ name, content });
119
+ seen.add(name);
120
+ }
121
+ }
122
+ for (const match of block.matchAll(/\b(?:getByName|create|named)\(\s*["']([\w-]+)["']\s*\)\s*\{/g)) {
123
+ const name = match[1];
124
+ if (seen.has(name))
125
+ continue;
126
+ const openBraceIndex = match.index + match[0].length - 1;
127
+ const content = sliceBlockAt(block, openBraceIndex);
128
+ if (content !== undefined) {
129
+ results.push({ name, content });
130
+ seen.add(name);
131
+ }
132
+ }
133
+ return results.sort((a, b) => a.name.localeCompare(b.name));
134
+ }
135
+ const BOOLEAN_TOKEN = `(true|false)`;
136
+ // v0.4.1 Batch 3 — boolean-literal counterpart to extractLiteral (which only
137
+ // matches quoted-string/integer literals — Gradle boolean assignments like
138
+ // `debuggable true` or `isMinifyEnabled = true` are bare tokens, not quoted).
139
+ // Same anchoring/fallback strategy as extractLiteral for consistency.
140
+ export function extractBooleanLiteral(text, keyAlternatives) {
141
+ const keyPattern = keyAlternatives.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
142
+ const literalPattern = new RegExp(`^[ \\t]*(?:${keyPattern})\\b\\s*[=(]?\\s*${BOOLEAN_TOKEN}\\)?`, "m");
143
+ const literalMatch = text.match(literalPattern);
144
+ if (literalMatch) {
145
+ return { value: literalMatch[1] === "true" };
146
+ }
147
+ const rawPattern = new RegExp(`^[ \\t]*(?:${keyPattern})\\b\\s*[=(]?\\s*([^\\n]{1,120})`, "m");
148
+ const rawMatch = text.match(rawPattern);
149
+ if (rawMatch) {
150
+ const raw = rawMatch[1].trim();
151
+ if (raw.length > 0)
152
+ return { raw };
153
+ }
154
+ return {};
155
+ }
156
+ function booleanLiteralState(result) {
157
+ if (result.value !== undefined)
158
+ return result.value ? "literal-true" : "literal-false";
159
+ if (result.raw !== undefined)
160
+ return "dynamic";
161
+ return "missing";
162
+ }
163
+ // v0.4.1 Batch 3 — builds bounded literal evidence for one named build-type
164
+ // block. Never evaluates Gradle; signingConfig/matchingFallbacks/initWith
165
+ // are always reference expressions, so only a bounded raw excerpt is kept.
166
+ function buildBuildTypeInfo(name, content) {
167
+ const debuggable = extractBooleanLiteral(content, ["debuggable", "isDebuggable"]);
168
+ const minifyEnabled = extractBooleanLiteral(content, ["minifyEnabled", "isMinifyEnabled"]);
169
+ const shrinkResources = extractBooleanLiteral(content, ["shrinkResources", "isShrinkResources"]);
170
+ const signingConfig = extractLiteral(content, ["signingConfig"]);
171
+ const matchingFallbacks = extractLiteral(content, ["matchingFallbacks"]);
172
+ const initWith = extractLiteral(content, ["initWith"]);
173
+ return {
174
+ name,
175
+ debuggable: debuggable.value,
176
+ debuggableRaw: debuggable.raw,
177
+ debuggableState: booleanLiteralState(debuggable),
178
+ minifyEnabled: minifyEnabled.value,
179
+ minifyEnabledRaw: minifyEnabled.raw,
180
+ minifyEnabledState: booleanLiteralState(minifyEnabled),
181
+ shrinkResources: shrinkResources.value,
182
+ shrinkResourcesRaw: shrinkResources.raw,
183
+ shrinkResourcesState: booleanLiteralState(shrinkResources),
184
+ signingConfigRef: signingConfig.value ?? signingConfig.raw,
185
+ matchingFallbacksRaw: matchingFallbacks.value ?? matchingFallbacks.raw,
186
+ initWithRaw: initWith.value ?? initWith.raw,
187
+ };
188
+ }
84
189
  // Extracts bounded static metadata for one module's build file text. `path`
85
190
  // is intentionally omitted from the returned info — the caller (the
86
191
  // aggregator, which already knows the module path from detection) attaches
@@ -128,6 +233,7 @@ export function extractModuleGradleMetadata(rawText) {
128
233
  compileSdk: compileSdk.value,
129
234
  compileSdkRaw: compileSdk.raw,
130
235
  buildTypes: findNamedBlockEntries(text, "buildTypes"),
236
+ buildTypeDetails: extractNamedSubBlocks(text, "buildTypes").map(({ name, content }) => buildBuildTypeInfo(name, content)),
131
237
  composeEnabled: evidence.composeBuildFeatureEvidence || evidence.composeDependencyEvidence || undefined,
132
238
  sourceSetEvidence: findNamedBlockEntries(text, "sourceSets").map((name) => `declared sourceSet: ${name}`),
133
239
  testSourceSetEvidence: [],
@@ -108,8 +108,17 @@ function parseComponent(kind, element, warnings) {
108
108
  component.readPermission = androidAttr(element, "readPermission");
109
109
  component.writePermission = androidAttr(element, "writePermission");
110
110
  const authoritiesRaw = androidAttr(element, "authorities");
111
+ component.authoritiesRaw = authoritiesRaw;
111
112
  component.authorities = authoritiesRaw ? authoritiesRaw.split(";").map((s) => s.trim()).filter(Boolean) : undefined;
112
- component.grantUriPermissions = parseBooleanAttr(element, "grantUriPermissions", warnings, context).value;
113
+ const grantUriPermissions = parseBooleanAttr(element, "grantUriPermissions", warnings, context);
114
+ component.grantUriPermissions = grantUriPermissions.value;
115
+ component.grantUriPermissionsRaw = grantUriPermissions.raw;
116
+ component.metadata = findChildren(element, "meta-data").map((metadata) => ({
117
+ name: androidAttr(metadata, "name"),
118
+ resource: androidAttr(metadata, "resource"),
119
+ value: androidAttr(metadata, "value"),
120
+ location: toLocation(metadata.location),
121
+ }));
113
122
  }
114
123
  return component;
115
124
  }
@@ -202,6 +211,12 @@ export function parseAndroidManifestSource(xmlText, manifestPath) {
202
211
  if (applicationElements.length > 1) {
203
212
  parseWarnings.push(`Manifest declares ${applicationElements.length} <application> elements; only the first was used.`);
204
213
  }
214
+ const usesCleartextTrafficResult = applicationEl
215
+ ? parseBooleanAttr(applicationEl, "usesCleartextTraffic", parseWarnings, "<application>")
216
+ : undefined;
217
+ const allowBackupResult = applicationEl ? parseBooleanAttr(applicationEl, "allowBackup", parseWarnings, "<application>") : undefined;
218
+ const debuggableResult = applicationEl ? parseBooleanAttr(applicationEl, "debuggable", parseWarnings, "<application>") : undefined;
219
+ const testOnlyResult = applicationEl ? parseBooleanAttr(applicationEl, "testOnly", parseWarnings, "<application>") : undefined;
205
220
  const application = applicationEl
206
221
  ? {
207
222
  name: androidAttr(applicationEl, "name"),
@@ -209,10 +224,18 @@ export function parseAndroidManifestSource(xmlText, manifestPath) {
209
224
  iconRef: androidAttr(applicationEl, "icon"),
210
225
  enabled: parseBooleanAttr(applicationEl, "enabled", parseWarnings, "<application>").value,
211
226
  permission: androidAttr(applicationEl, "permission"),
212
- allowBackup: parseBooleanAttr(applicationEl, "allowBackup", parseWarnings, "<application>").value,
213
- debuggable: parseBooleanAttr(applicationEl, "debuggable", parseWarnings, "<application>").value,
214
- usesCleartextTraffic: parseBooleanAttr(applicationEl, "usesCleartextTraffic", parseWarnings, "<application>").value,
227
+ allowBackup: allowBackupResult?.value,
228
+ allowBackupRaw: allowBackupResult?.raw,
229
+ debuggable: debuggableResult?.value,
230
+ debuggableRaw: debuggableResult?.raw,
231
+ usesCleartextTraffic: usesCleartextTrafficResult?.value,
232
+ usesCleartextTrafficRaw: usesCleartextTrafficResult?.raw,
215
233
  networkSecurityConfigRef: androidAttr(applicationEl, "networkSecurityConfig"),
234
+ fullBackupContentRef: androidAttr(applicationEl, "fullBackupContent"),
235
+ dataExtractionRulesRef: androidAttr(applicationEl, "dataExtractionRules"),
236
+ testOnly: testOnlyResult?.value,
237
+ testOnlyRaw: testOnlyResult?.raw,
238
+ location: toLocation(applicationEl.location),
216
239
  }
217
240
  : {};
218
241
  const activities = applicationEl ? findChildren(applicationEl, "activity").map((el) => parseComponent("activity", el, parseWarnings)) : [];
@@ -92,6 +92,18 @@ class Scanner {
92
92
  }
93
93
  this.expect(literal);
94
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
+ }
95
107
  }
96
108
  function resolveNamespace(prefix, scope) {
97
109
  return scope.get(prefix ?? "");
@@ -224,12 +236,13 @@ function parseElement(scanner, parentScope, warnings) {
224
236
  scanner.skipWhitespace();
225
237
  if (scanner.startsWith("/>")) {
226
238
  scanner.expect("/>");
227
- return { tagName, prefix, localName, namespaceUri, attributes, children: [], location };
239
+ return { tagName, prefix, localName, namespaceUri, attributes, children: [], location, textContent: "" };
228
240
  }
229
241
  scanner.expect(">");
230
242
  const children = [];
243
+ const textParts = [];
231
244
  for (;;) {
232
- skipTextAndMisc(scanner, warnings);
245
+ skipTextAndMisc(scanner, warnings, textParts);
233
246
  if (scanner.startsWith("</")) {
234
247
  scanner.expect("</");
235
248
  const closingName = readName(scanner);
@@ -251,16 +264,22 @@ function parseElement(scanner, parentScope, warnings) {
251
264
  // element/closing-tag pattern) is treated as malformed input.
252
265
  throw new XmlSyntaxError("Unexpected content while looking for a child element or closing tag", scanner.loc());
253
266
  }
254
- return { tagName, prefix, localName, namespaceUri, attributes, children, location };
267
+ return { tagName, prefix, localName, namespaceUri, attributes, children, location, textContent: decodeXmlEntities(textParts.join("")) };
255
268
  }
256
- function skipTextAndMisc(scanner, warnings) {
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) {
257
275
  for (;;) {
258
276
  if (scanner.startsWith("<!--")) {
259
277
  scanner.skipUntil("-->");
260
278
  continue;
261
279
  }
262
280
  if (scanner.startsWith("<![CDATA[")) {
263
- scanner.skipUntil("]]>");
281
+ scanner.expect("<![CDATA[");
282
+ textParts.push(scanner.captureUntil("]]>"));
264
283
  continue;
265
284
  }
266
285
  if (scanner.startsWith("<?")) {
@@ -271,10 +290,7 @@ function skipTextAndMisc(scanner, warnings) {
271
290
  break;
272
291
  if (scanner.eof())
273
292
  break;
274
- // Plain text content between elements — not meaningful for manifest
275
- // structure, so it is discarded rather than stored.
276
- scanner.advance();
277
- continue;
293
+ textParts.push(scanner.advance());
278
294
  }
279
295
  }
280
296
  // Parses `xmlText` into a small element tree. Never throws: all failures are
@@ -1,10 +1,29 @@
1
1
  import { androidVerdictToHumanLabel } from "../validate/androidVerdict.js";
2
2
  const GRADLE_OPERATION_CHECK_PREFIX = "android-gradle-";
3
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"];
4
23
  const STATIC_ANALYSIS_LIMITATIONS = [
5
24
  "Static Android project analysis does not prove runtime behavior.",
6
25
  "Manifests are parsed independently and are not merged.",
7
- "Manifest placeholders and resource references are not resolved.",
26
+ "Selected local resource references are resolved conservatively; manifest placeholders, full resource merging, and overlay precedence are not resolved.",
8
27
  "Gradle files are not evaluated (no Groovy/Kotlin execution).",
9
28
  "Dynamic Gradle values may remain unresolved.",
10
29
  "Same-file duplicate Gradle assignments use first-match extraction (a known limitation — see agents.txt Batch 4/5 notes).",
@@ -33,6 +52,10 @@ export function toAndroidReportModel(result, options = {}) {
33
52
  const deepLinkCount = result.manifests.reduce((sum, m) => sum + m.deepLinks.length, 0);
34
53
  const gradleOperationResults = result.checks.filter((c) => c.id.startsWith(GRADLE_OPERATION_CHECK_PREFIX) && c.id !== GRADLE_METADATA_CHECK_ID);
35
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);
36
59
  const verdict = String(result.verdict);
37
60
  const isRealVerdict = result.verdict !== "not-calculated";
38
61
  return {
@@ -42,6 +65,8 @@ export function toAndroidReportModel(result, options = {}) {
42
65
  target: result.target,
43
66
  profile: options.profile ?? "android",
44
67
  requestedGradleOperations: options.requestedGradleOperations ?? [],
68
+ requestedExternalTools: options.requestedExternalTools ?? [],
69
+ externalNetworkPolicy: options.externalNetworkPolicy ?? "deny",
45
70
  generatedAt: result.finishedAt,
46
71
  totalDurationMs: result.durationMs,
47
72
  },
@@ -65,6 +90,12 @@ export function toAndroidReportModel(result, options = {}) {
65
90
  },
66
91
  gradleCheckSummary: gradleMetadataCheck ? { title: "Gradle metadata check", summary: `status: ${gradleMetadataCheck.status}` } : undefined,
67
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
+ },
68
99
  releaseMetadataSummary: {
69
100
  title: "Release metadata",
70
101
  summary: result.releaseMetadata
@@ -17,6 +17,27 @@ function formatDuration(ms) {
17
17
  return `${ms}ms`;
18
18
  return `${(ms / 1000).toFixed(1)}s`;
19
19
  }
20
+ // v0.4.1 Batch 8 — shared per-check rendering for the new ADVANCED SECURITY
21
+ // CHECKS and EXTERNAL TOOL RESULTS sections. Bounded: prints counts and
22
+ // identifiers, never raw stdout/stderr/environment/complete source (that
23
+ // bound is already enforced by each check builder before evidence reaches
24
+ // this report — this function does not itself receive raw tool output).
25
+ function renderCheckSummaryLines(lines, c) {
26
+ lines.push(` ${c.id} [${c.status}] — ${sanitize(c.title)}`);
27
+ lines.push(` category: ${c.category} findings: ${c.findings.length} candidates: ${c.candidateEvidence?.length ?? 0}`);
28
+ if (c.evidence.length > 0)
29
+ lines.push(` evidence: ${c.evidence.map(sanitize).join(" | ")}`);
30
+ if (c.durationMs !== undefined)
31
+ lines.push(` duration: ${formatDuration(c.durationMs)}`);
32
+ if (c.skipInfo)
33
+ lines.push(` skip reason: ${sanitize(c.skipInfo.reason)}`);
34
+ for (const w of c.warnings)
35
+ lines.push(` warning: ${sanitize(w)}`);
36
+ for (const e of c.errors)
37
+ lines.push(` error: ${sanitize(e)}`);
38
+ if (c.environmentRequirements.length > 0)
39
+ lines.push(` limitations: ${c.environmentRequirements.map(sanitize).join(" | ")}`);
40
+ }
20
41
  export function renderAndroidTextReport(model) {
21
42
  const lines = [];
22
43
  // 1. Summary
@@ -143,6 +164,48 @@ export function renderAndroidTextReport(model) {
143
164
  }
144
165
  }
145
166
  lines.push("");
167
+ // 15b. Advanced security checks (v0.4.1 Batch 8)
168
+ lines.push(divider());
169
+ lines.push("ADVANCED SECURITY CHECKS");
170
+ lines.push(divider());
171
+ if (model.advancedSecurityChecks.length === 0) {
172
+ lines.push("(none ran — not applicable to this target)");
173
+ }
174
+ else {
175
+ for (const c of model.advancedSecurityChecks) {
176
+ renderCheckSummaryLines(lines, c);
177
+ }
178
+ }
179
+ lines.push("");
180
+ // 15c. External tool results (v0.4.1 Batch 8)
181
+ lines.push(divider());
182
+ lines.push("EXTERNAL TOOL RESULTS");
183
+ lines.push(divider());
184
+ lines.push(`Requested external tools: ${model.metadata.requestedExternalTools.length > 0 ? model.metadata.requestedExternalTools.join(", ") : "(none — zero external-tool process execution)"}`);
185
+ if (model.metadata.requestedExternalTools.length > 0) {
186
+ lines.push(`Effective network policy: ${model.metadata.externalNetworkPolicy}`);
187
+ }
188
+ if (model.externalToolChecks.length === 0) {
189
+ lines.push("No external tools were requested.");
190
+ }
191
+ else {
192
+ for (const c of model.externalToolChecks) {
193
+ renderCheckSummaryLines(lines, c);
194
+ }
195
+ }
196
+ lines.push("");
197
+ // 15d. Candidate evidence (review required, distinct from confirmed findings)
198
+ lines.push(divider());
199
+ lines.push("CANDIDATE EVIDENCE (REVIEW REQUIRED — NOT CONFIRMED FINDINGS)");
200
+ lines.push(divider());
201
+ lines.push(sanitize(model.candidateEvidenceSummary.summary));
202
+ for (const c of [...model.advancedSecurityChecks, ...model.externalToolChecks]) {
203
+ for (const candidate of c.candidateEvidence ?? []) {
204
+ lines.push(` [${candidate.confidence}/${candidate.resolutionState}] ${candidate.ruleId} — ${sanitize(candidate.summary)}`);
205
+ lines.push(` location: ${candidate.location.path}${candidate.location.line !== undefined ? `:${candidate.location.line}` : ""}`);
206
+ }
207
+ }
208
+ lines.push("");
146
209
  // 16. Release metadata
147
210
  lines.push(divider());
148
211
  lines.push("RELEASE METADATA");
@@ -18,6 +18,80 @@ import { buildAndroidPlayReadinessChecklist } from "./playReadinessChecklist.js"
18
18
  import { calculateAndroidVerdict } from "./androidVerdict.js";
19
19
  import { buildAndroidDetectionCheckResult, buildAndroidManifestParsingCheckResult, buildAndroidTargetImmutabilityCheckResult, } from "./detectionAndImmutabilityCheckResults.js";
20
20
  import { ANDROID_VALIDATION_ARTIFACT_TYPE, ANDROID_VALIDATION_SCHEMA_VERSION, } from "../validation/result.js";
21
+ import { auditAndroidNetworkSecurity } from "../advancedSecurity/networkSecurity/checkResult.js";
22
+ import { auditAndroidBackupConfiguration } from "../advancedSecurity/backupConfiguration/checkResult.js";
23
+ import { auditAndroidReleaseConfiguration } from "../advancedSecurity/releaseConfiguration/checkResult.js";
24
+ import { auditAndroidSecretCandidates } from "../advancedSecurity/secretCandidates/checkResult.js";
25
+ import { auditAndroidSigningConfiguration } from "../advancedSecurity/signingConfiguration/checkResult.js";
26
+ import { auditAndroidWebViewSecurity } from "../advancedSecurity/webview/checkResult.js";
27
+ import { auditAndroidFileProviders } from "../advancedSecurity/fileProvider/checkResult.js";
28
+ import { auditAndroidSensitiveStorage } from "../advancedSecurity/sensitiveStorage/checkResult.js";
29
+ import { auditAndroidSensitiveLogging } from "../advancedSecurity/sensitiveLogging/checkResult.js";
30
+ import { auditAndroidClipboardSecurity } from "../advancedSecurity/clipboard/checkResult.js";
31
+ import { auditAndroidFirebaseGoogleServices } from "../advancedSecurity/firebaseGoogle/checkResult.js";
32
+ import { runRequestedAndroidExternalTools } from "../advancedSecurity/externalTools/runRequestedAndroidExternalTools.js";
33
+ import { createRealExternalToolExecutor } from "../advancedSecurity/externalTools/runBoundedExternalTool.js";
34
+ import { createRealGradleCommandExecutor } from "../gradle/validate/executor.js";
35
+ function runIsolatedCheck(descriptor) {
36
+ try {
37
+ return descriptor.run();
38
+ }
39
+ catch (error) {
40
+ const message = error instanceof Error ? error.message : String(error);
41
+ return {
42
+ id: descriptor.id,
43
+ category: descriptor.category,
44
+ title: descriptor.title,
45
+ status: "error",
46
+ requirementLevel: "optional",
47
+ ran: false,
48
+ skipped: false,
49
+ evidence: [],
50
+ findings: [],
51
+ warnings: [],
52
+ errors: [`Advanced check raised an unexpected error: ${message}`],
53
+ sourcePaths: [],
54
+ confidence: "unknown",
55
+ environmentRequirements: [],
56
+ candidateEvidence: [],
57
+ };
58
+ }
59
+ }
60
+ function buildAdvancedSecurityChecks(targetRoot, detection, manifestEntries, gradleModules) {
61
+ // Fixed order (agents.txt Batch 8 section 9.1) — never alphabetical, never
62
+ // derived from filesystem/object iteration.
63
+ const descriptors = [
64
+ { id: "android-network-security-audit", category: "android-network-security", title: "Android Network Security Config audit", run: () => auditAndroidNetworkSecurity(targetRoot, detection, manifestEntries) },
65
+ { id: "android-backup-configuration-audit", category: "android-backup-configuration", title: "Android backup configuration audit", run: () => auditAndroidBackupConfiguration(targetRoot, detection, manifestEntries) },
66
+ { id: "android-release-configuration-audit", category: "android-release-configuration", title: "Android release configuration audit", run: () => auditAndroidReleaseConfiguration(targetRoot, detection, manifestEntries, gradleModules) },
67
+ { id: "android-secret-candidates-audit", category: "android-secret-candidates", title: "Android hardcoded secret-candidate audit", run: () => auditAndroidSecretCandidates(targetRoot, detection) },
68
+ { id: "android-signing-configuration-audit", category: "android-signing-configuration", title: "Android signing configuration audit", run: () => auditAndroidSigningConfiguration(targetRoot, detection, gradleModules) },
69
+ { id: "android-webview-security-audit", category: "android-webview", title: "Android WebView security audit", run: () => auditAndroidWebViewSecurity(targetRoot, detection) },
70
+ { id: "android-file-provider-audit", category: "android-file-provider", title: "Android FileProvider security audit", run: () => auditAndroidFileProviders(targetRoot, detection, manifestEntries) },
71
+ { id: "android-sensitive-storage-audit", category: "android-sensitive-storage", title: "Android sensitive-storage audit", run: () => auditAndroidSensitiveStorage(targetRoot, detection) },
72
+ { id: "android-sensitive-logging-audit", category: "android-sensitive-logging", title: "Android sensitive-logging audit", run: () => auditAndroidSensitiveLogging(targetRoot, detection) },
73
+ { id: "android-clipboard-security-audit", category: "android-clipboard", title: "Android clipboard security audit", run: () => auditAndroidClipboardSecurity(targetRoot, detection) },
74
+ { id: "android-firebase-google-services-audit", category: "android-firebase-google-services", title: "Android Firebase/Google services audit", run: () => auditAndroidFirebaseGoogleServices(targetRoot, detection) },
75
+ ];
76
+ return descriptors.map(runIsolatedCheck);
77
+ }
78
+ // Memoizes by Gradle operation id: the fixed allowlist (operations.ts) maps
79
+ // each operation id to fixed args, so within one validation run the same
80
+ // operation id always means the same command — caching by id is safe and
81
+ // prevents running `lintDebug` twice when both --android-gradle-operations
82
+ // lint-debug and --android-external-tools android-lint are requested
83
+ // together (agents.txt Batch 8 section 10.6).
84
+ function createDedupingGradleExecutor(base) {
85
+ const cache = new Map();
86
+ return (plan) => {
87
+ const cached = cache.get(plan.operationId);
88
+ if (cached)
89
+ return cached;
90
+ const promise = base(plan);
91
+ cache.set(plan.operationId, promise);
92
+ return promise;
93
+ };
94
+ }
21
95
  function readToolPackageMetadata(toolRoot) {
22
96
  try {
23
97
  const pkgRaw = fs.readFileSync(path.join(toolRoot, "package.json"), "utf8");
@@ -75,6 +149,11 @@ export async function validateAndroidTarget(options) {
75
149
  // batch owner).
76
150
  const detectionCheck = buildAndroidDetectionCheckResult(detection);
77
151
  const manifestParsingCheck = buildAndroidManifestParsingCheckResult(detection, manifestEntries);
152
+ // Shared, deduping Gradle executor: reused for both --android-gradle-
153
+ // operations and (if android-lint is also externally requested) the
154
+ // Batch 7 Android Lint adapter, so `lintDebug` never runs twice for one
155
+ // validation run (agents.txt Batch 8 section 10.6).
156
+ const sharedGradleExecutor = createDedupingGradleExecutor(options.gradleExecutor ?? createRealGradleCommandExecutor());
78
157
  // 15-16. Optional Gradle operations — never run unless explicitly requested.
79
158
  const requestedOperationIds = options.requestedGradleOperationIds ?? [];
80
159
  const gradleOperationResults = requestedOperationIds.length > 0
@@ -83,17 +162,79 @@ export async function validateAndroidTarget(options) {
83
162
  targetRoot: resolvedTargetRoot,
84
163
  detection,
85
164
  operationIds: requestedOperationIds,
86
- executor: options.gradleExecutor,
165
+ executor: sharedGradleExecutor,
87
166
  allowWithoutTaskDiscovery: options.allowGradleWithoutTaskDiscovery,
88
167
  platform: options.platform,
89
168
  })
90
169
  : [];
91
- // 17. Target mutation evidence — captured regardless of whether Gradle ran,
170
+ // v0.4.1 Batch 8 — eleven internal advanced Android checks, active by
171
+ // default (agents.txt Batch 8 section 9.1). Sequential, fixed order,
172
+ // isolated per-check via runIsolatedCheck. Never executes a process or
173
+ // touches the network.
174
+ const advancedSecurityChecks = buildAdvancedSecurityChecks(resolvedTargetRoot, detection, manifestEntries, gradle.modules);
175
+ // v0.4.1 Batch 8 — optional external-tool integration. Never executes
176
+ // unless requestedExternalToolIds is non-empty (same "no request, no
177
+ // execution" convention as Gradle operations). Wrapped in try/catch so a
178
+ // dispatcher-level failure (e.g. an unknown tool id slipping past CLI
179
+ // validation in a programmatic caller) cannot discard the checks already
180
+ // gathered above.
181
+ const requestedExternalToolIds = options.requestedExternalToolIds ?? [];
182
+ let externalToolChecks = [];
183
+ if (requestedExternalToolIds.length > 0) {
184
+ try {
185
+ externalToolChecks = await runRequestedAndroidExternalTools({
186
+ request: {
187
+ requestedTools: requestedExternalToolIds,
188
+ targetRoot: resolvedTargetRoot,
189
+ artifactRoot: options.externalToolArtifactRoot ?? path.join(options.toolRoot, "reports", "security", "external-tools"),
190
+ networkPolicy: options.externalNetworkPolicy ?? "deny",
191
+ },
192
+ detection,
193
+ executors: {
194
+ semgrep: options.externalToolExecutors?.semgrep ?? createRealExternalToolExecutor(),
195
+ osv: options.externalToolExecutors?.osv ?? createRealExternalToolExecutor(),
196
+ androidLint: options.externalToolExecutors?.androidLint ?? sharedGradleExecutor,
197
+ dependencyCheck: options.externalToolExecutors?.dependencyCheck ?? createRealExternalToolExecutor(),
198
+ },
199
+ discover: options.externalToolDiscover,
200
+ javaAvailable: options.javaAvailable,
201
+ lintTaskAvailable: options.lintTaskAvailable,
202
+ });
203
+ }
204
+ catch (error) {
205
+ const message = error instanceof Error ? error.message : String(error);
206
+ externalToolChecks = [
207
+ {
208
+ id: "android-external-tools-dispatch",
209
+ category: "android-semgrep",
210
+ title: "External security-tool dispatch",
211
+ status: "error",
212
+ requirementLevel: "optional",
213
+ ran: false,
214
+ skipped: false,
215
+ evidence: [],
216
+ findings: [],
217
+ warnings: [],
218
+ errors: [`External-tool dispatch raised an unexpected error: ${message}`],
219
+ sourcePaths: [],
220
+ confidence: "unknown",
221
+ environmentRequirements: [],
222
+ candidateEvidence: [],
223
+ },
224
+ ];
225
+ }
226
+ }
227
+ // 17. Target mutation evidence — captured after every process this run may
228
+ // have executed (Gradle operations, advanced checks, and external tools),
92
229
  // so the static-only path also proves it made no changes.
93
230
  const snapshotAfter = captureTargetSnapshot(resolvedTargetRoot, hasGit);
94
231
  const targetMutation = buildTargetMutationReport(snapshotBefore, snapshotAfter);
95
232
  const immutabilityCheck = buildAndroidTargetImmutabilityCheckResult(targetMutation);
96
- // 18. Assemble checks/findings in deterministic order (agents.txt section 10.3).
233
+ // 18. Assemble checks/findings in deterministic order (agents.txt Batch 8
234
+ // section 11.1): the existing v0.4.0 check order is preserved exactly
235
+ // (ending with immutabilityCheck, as before), and the eleven advanced
236
+ // checks and any requested external tools are APPENDED after it — never
237
+ // interleaved into the existing order.
97
238
  const checks = [
98
239
  detectionCheck,
99
240
  manifestParsingCheck,
@@ -104,6 +245,8 @@ export async function validateAndroidTarget(options) {
104
245
  gradleMetadataCheck,
105
246
  ...gradleOperationResults,
106
247
  immutabilityCheck,
248
+ ...advancedSecurityChecks,
249
+ ...externalToolChecks,
107
250
  ];
108
251
  const findingsById = new Map();
109
252
  for (const check of checks) {