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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +10 -61
  2. package/dist/scripts/security/validate.js +38 -5
  3. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/analyzeBackupConfiguration.js +300 -0
  4. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/checkResult.js +41 -0
  5. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/index.js +8 -0
  6. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/manifestEvidence.js +102 -0
  7. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/parseBackupRules.js +110 -0
  8. package/dist/src/mobile/android/advancedSecurity/backupConfiguration/types.js +27 -0
  9. package/dist/src/mobile/android/advancedSecurity/candidateEvidence.js +50 -0
  10. package/dist/src/mobile/android/advancedSecurity/clipboard/analyzeClipboardSecurity.js +166 -0
  11. package/dist/src/mobile/android/advancedSecurity/clipboard/checkResult.js +74 -0
  12. package/dist/src/mobile/android/advancedSecurity/clipboard/collectClipboardEvidence.js +82 -0
  13. package/dist/src/mobile/android/advancedSecurity/clipboard/index.js +1 -0
  14. package/dist/src/mobile/android/advancedSecurity/clipboard/types.js +1 -0
  15. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/checkResult.js +255 -0
  16. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/index.js +1 -0
  17. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintSarif.js +37 -0
  18. package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintXml.js +36 -0
  19. package/dist/src/mobile/android/advancedSecurity/externalTools/artifacts.js +95 -0
  20. package/dist/src/mobile/android/advancedSecurity/externalTools/boundedOutput.js +33 -0
  21. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/checkResult.js +173 -0
  22. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/command.js +33 -0
  23. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/index.js +1 -0
  24. package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/parseDependencyCheckJson.js +133 -0
  25. package/dist/src/mobile/android/advancedSecurity/externalTools/discoverExecutable.js +13 -0
  26. package/dist/src/mobile/android/advancedSecurity/externalTools/index.js +8 -0
  27. package/dist/src/mobile/android/advancedSecurity/externalTools/minimalEnvironment.js +26 -0
  28. package/dist/src/mobile/android/advancedSecurity/externalTools/mutation.js +77 -0
  29. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/checkResult.js +161 -0
  30. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/command.js +25 -0
  31. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/index.js +1 -0
  32. package/dist/src/mobile/android/advancedSecurity/externalTools/osv/parseOsvJson.js +136 -0
  33. package/dist/src/mobile/android/advancedSecurity/externalTools/runBoundedExternalTool.js +51 -0
  34. package/dist/src/mobile/android/advancedSecurity/externalTools/runRequestedAndroidExternalTools.js +54 -0
  35. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/checkResult.js +170 -0
  36. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/command.js +28 -0
  37. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/index.js +2 -0
  38. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/parseSemgrepJson.js +141 -0
  39. package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/rules.js +105 -0
  40. package/dist/src/mobile/android/advancedSecurity/externalTools/types.js +107 -0
  41. package/dist/src/mobile/android/advancedSecurity/fileProvider/checkResult.js +188 -0
  42. package/dist/src/mobile/android/advancedSecurity/fileProvider/index.js +1 -0
  43. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/analyzeFirebaseGoogleServices.js +197 -0
  44. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/checkResult.js +77 -0
  45. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/collectFirebaseServiceEvidence.js +193 -0
  46. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/discoverFirebaseArtifacts.js +128 -0
  47. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/index.js +1 -0
  48. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseDatabaseRules.js +55 -0
  49. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirebaseConfig.js +75 -0
  50. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirestoreStorageRules.js +88 -0
  51. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseGoogleServicesConfig.js +55 -0
  52. package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/types.js +6 -0
  53. package/dist/src/mobile/android/advancedSecurity/networkSecurity/analyzeNetworkSecurity.js +394 -0
  54. package/dist/src/mobile/android/advancedSecurity/networkSecurity/checkResult.js +47 -0
  55. package/dist/src/mobile/android/advancedSecurity/networkSecurity/deriveEffectiveNetworkPolicy.js +52 -0
  56. package/dist/src/mobile/android/advancedSecurity/networkSecurity/index.js +14 -0
  57. package/dist/src/mobile/android/advancedSecurity/networkSecurity/manifestEvidence.js +109 -0
  58. package/dist/src/mobile/android/advancedSecurity/networkSecurity/parseNetworkSecurityConfig.js +186 -0
  59. package/dist/src/mobile/android/advancedSecurity/networkSecurity/types.js +1 -0
  60. package/dist/src/mobile/android/advancedSecurity/ordering.js +44 -0
  61. package/dist/src/mobile/android/advancedSecurity/redaction.js +54 -0
  62. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/analyzeReleaseConfiguration.js +221 -0
  63. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/checkResult.js +39 -0
  64. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/index.js +7 -0
  65. package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/manifestEvidence.js +19 -0
  66. package/dist/src/mobile/android/advancedSecurity/resourceReference.js +69 -0
  67. package/dist/src/mobile/android/advancedSecurity/resourceResolution.js +118 -0
  68. package/dist/src/mobile/android/advancedSecurity/ruleIds.js +141 -0
  69. package/dist/src/mobile/android/advancedSecurity/secretCandidates/analyzeSecretCandidates.js +116 -0
  70. package/dist/src/mobile/android/advancedSecurity/secretCandidates/checkResult.js +80 -0
  71. package/dist/src/mobile/android/advancedSecurity/secretCandidates/discoverSecretSourceFiles.js +152 -0
  72. package/dist/src/mobile/android/advancedSecurity/secretCandidates/index.js +9 -0
  73. package/dist/src/mobile/android/advancedSecurity/secretCandidates/matchSecretCandidates.js +136 -0
  74. package/dist/src/mobile/android/advancedSecurity/secretCandidates/suppressValue.js +61 -0
  75. package/dist/src/mobile/android/advancedSecurity/secretCandidates/types.js +11 -0
  76. package/dist/src/mobile/android/advancedSecurity/sensitiveCategories.js +34 -0
  77. package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifyDirectExpression.js +99 -0
  78. package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifySensitiveIdentifier.js +111 -0
  79. package/dist/src/mobile/android/advancedSecurity/sensitiveData/index.js +3 -0
  80. package/dist/src/mobile/android/advancedSecurity/sensitiveData/localSourceContext.js +90 -0
  81. package/dist/src/mobile/android/advancedSecurity/sensitiveData/splitArguments.js +59 -0
  82. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/analyzeSensitiveLogging.js +117 -0
  83. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/checkResult.js +74 -0
  84. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/collectLoggingEvidence.js +87 -0
  85. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/index.js +1 -0
  86. package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/types.js +1 -0
  87. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/analyzeSensitiveStorage.js +182 -0
  88. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/checkResult.js +74 -0
  89. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/collectStorageEvidence.js +109 -0
  90. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/index.js +1 -0
  91. package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/types.js +4 -0
  92. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/analyzeSigningConfiguration.js +270 -0
  93. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/checkResult.js +119 -0
  94. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/discoverKeystoreCandidates.js +55 -0
  95. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/extractSigningConfigurations.js +81 -0
  96. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/index.js +8 -0
  97. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/types.js +20 -0
  98. package/dist/src/mobile/android/advancedSecurity/sourceLocation.js +15 -0
  99. package/dist/src/mobile/android/advancedSecurity/webview/checkResult.js +210 -0
  100. package/dist/src/mobile/android/advancedSecurity/webview/index.js +1 -0
  101. package/dist/src/mobile/android/gradle/moduleMetadataExtractor.js +123 -17
  102. package/dist/src/mobile/android/manifest/parseAndroidManifest.js +27 -4
  103. package/dist/src/mobile/android/manifest/xml/parseXml.js +25 -9
  104. package/dist/src/mobile/android/report/model.js +32 -1
  105. package/dist/src/mobile/android/report/renderAndroidReport.js +63 -0
  106. package/dist/src/mobile/android/validate/validateAndroidTarget.js +146 -3
  107. package/dist/src/mobile/android/validation/checkResult.js +7 -0
  108. package/dist/src/securityValidation/commandRunner.js +10 -1
  109. package/dist/src/securityValidation/validate/cliOptions.js +46 -0
  110. package/package.json +1 -1
  111. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/debug/res/xml/ambiguous_backup_rules.xml +4 -0
  112. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/ambiguous_backup_rules.xml +4 -0
  113. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/backup_rules.xml +5 -0
  114. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/broad_backup_rules.xml +4 -0
  115. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/data_extraction_rules.xml +10 -0
  116. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/unsupported_domain_rules.xml +4 -0
  117. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/malformed_backup_rules.xml +4 -0
  118. package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/unsupported_root.xml +4 -0
  119. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/debug/res/xml/ambiguous_config.xml +4 -0
  120. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/ambiguous_config.xml +4 -0
  121. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/base_cleartext_false.xml +8 -0
  122. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/broad_and_duplicate_domains.xml +8 -0
  123. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/empty_domain.xml +6 -0
  124. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/malformed_pin.xml +11 -0
  125. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/network_security_config.xml +26 -0
  126. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/malformed_network_security_config.xml +6 -0
  127. package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/unsupported_root.xml +4 -0
  128. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-dynamic.gradle +7 -0
  129. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-false.gradle +9 -0
  130. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-true.gradle +13 -0
  131. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-false.gradle.kts +7 -0
  132. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-true.gradle.kts +10 -0
  133. package/tests/fixtures/android/advanced-security-fixtures/release-configuration/missing-release-build-type.gradle.kts +7 -0
  134. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/debug/res/xml/network_security_config.xml +8 -0
  135. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/backup_rules.xml +4 -0
  136. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/network_security_config.xml +8 -0
  137. package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-b/src/main/res/xml/malformed_network_security_config.xml +7 -0
  138. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.java +8 -0
  139. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.kt +9 -0
  140. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle +11 -0
  141. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle.kts +11 -0
  142. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/google-services.json +16 -0
  143. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/local.properties +3 -0
  144. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/binary.properties +0 -0
  145. package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/oversized.json +1 -0
  146. package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/app/fake-release-keystore.jks +1 -0
  147. package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/build/generated-fake.keystore +1 -0
@@ -0,0 +1,210 @@
1
+ import { makeAndroidFinding } from "../../audit/androidFinding.js";
2
+ import { makeCandidateEvidence } from "../candidateEvidence.js";
3
+ import { sortCandidateEvidence } from "../ordering.js";
4
+ import { buildAndroidSourceLocation } from "../sourceLocation.js";
5
+ import { discoverSecretSourceFiles } from "../secretCandidates/discoverSecretSourceFiles.js";
6
+ export const ANDROID_WEBVIEW_SECURITY_AUDIT_CHECK_ID = "android-webview-security-audit";
7
+ const CALL = /(?:([A-Za-z_][\w.]*(?:\.getSettings\(\))?)\.)?(setJavaScriptEnabled|addJavascriptInterface|setAllowFileAccess|setAllowContentAccess|setAllowFileAccessFromFileURLs|setAllowUniversalAccessFromFileURLs|setMixedContentMode|setWebContentsDebuggingEnabled|loadUrl|postUrl|loadData|loadDataWithBaseURL|proceed|cancel)\s*\(([^\n)]*)\)/g;
8
+ const PROPERTY = /([A-Za-z_][\w.]*)\.(javaScriptEnabled|allowFileAccess|allowContentAccess|allowFileAccessFromFileURLs|allowUniversalAccessFromFileURLs|mixedContentMode)\s*=\s*([^;\n}]+)/g;
9
+ const SETTINGS_ALIAS = /(?:\bWebSettings\s+|\b(?:val|var)\s+)([A-Za-z_]\w*)\s*=\s*([A-Za-z_][\w.]*)\s*(?:\.getSettings\(\)|\.settings)/g;
10
+ const MAX_ARGUMENT = 240;
11
+ function lineAt(text, offset) { return text.slice(0, offset).split("\n").length; }
12
+ // Masks comments and string *contents* while preserving offsets/newlines.
13
+ // Calls are matched only in code; the original text is then used for the
14
+ // bounded argument summary. This intentionally local scanner is not a Java
15
+ // or Kotlin parser and never attempts type/provenance resolution.
16
+ function executableMask(source) {
17
+ let out = "";
18
+ let i = 0;
19
+ let state = "code";
20
+ let quote = "";
21
+ while (i < source.length) {
22
+ const ch = source[i], next = source[i + 1];
23
+ if (state === "code" && ch === "/" && next === "/") {
24
+ out += " ";
25
+ i += 2;
26
+ state = "line";
27
+ continue;
28
+ }
29
+ if (state === "code" && ch === "/" && next === "*") {
30
+ out += " ";
31
+ i += 2;
32
+ state = "block";
33
+ continue;
34
+ }
35
+ if (state === "code" && source.slice(i, i + 3) === '\"\"\"') {
36
+ out += '\"\"\"';
37
+ i += 3;
38
+ state = "raw";
39
+ continue;
40
+ }
41
+ if (state === "code" && (ch === '"' || ch === "'")) {
42
+ quote = ch;
43
+ out += ch;
44
+ i++;
45
+ state = "quote";
46
+ continue;
47
+ }
48
+ if (state === "line") {
49
+ out += ch === "\n" ? "\n" : " ";
50
+ if (ch === "\n")
51
+ state = "code";
52
+ i++;
53
+ continue;
54
+ }
55
+ if (state === "block") {
56
+ out += ch === "\n" ? "\n" : " ";
57
+ if (ch === "*" && next === "/") {
58
+ out = out.slice(0, -1) + " ";
59
+ i += 2;
60
+ state = "code";
61
+ }
62
+ else
63
+ i++;
64
+ continue;
65
+ }
66
+ if (state === "raw") {
67
+ if (source.slice(i, i + 3) === '\"\"\"') {
68
+ out += '\"\"\"';
69
+ i += 3;
70
+ state = "code";
71
+ }
72
+ else {
73
+ out += ch === "\n" ? "\n" : " ";
74
+ i++;
75
+ }
76
+ continue;
77
+ }
78
+ if (state === "quote") {
79
+ if (ch === "\\") {
80
+ out += " ";
81
+ i += 2;
82
+ continue;
83
+ }
84
+ out += ch === "\n" ? "\n" : (ch === quote ? ch : " ");
85
+ if (ch === quote)
86
+ state = "code";
87
+ i++;
88
+ continue;
89
+ }
90
+ out += ch;
91
+ i++;
92
+ }
93
+ return out;
94
+ }
95
+ function braceEnd(mask, open) { let depth = 0; for (let i = open; i < mask.length; i++) {
96
+ if (mask[i] === "{")
97
+ depth++;
98
+ else if (mask[i] === "}" && --depth === 0)
99
+ return i;
100
+ } return mask.length; }
101
+ function scopeRanges(mask) { const ranges = []; const header = /(?:\bfun\s+([A-Za-z_]\w*)\s*\([^)]*\)|\b(?:public|private|protected|override|static|final|suspend|void|[A-Z][\w<>?\[\]]*)[\s\w<>?\[\]]*?\s+([A-Za-z_]\w*)\s*\([^)]*\))\s*\{/g; for (const match of mask.matchAll(header)) {
102
+ const from = (match.index ?? 0);
103
+ const open = from + match[0].length - 1;
104
+ ranges.push({ from, to: braceEnd(mask, open), id: `${match[1] ?? match[2] ?? "scope"}@${from}` });
105
+ } return ranges; }
106
+ function sslRanges(mask) { const ranges = []; for (const match of mask.matchAll(/onReceivedSslError\s*\([^)]*\)\s*\{/g)) {
107
+ const start = (match.index ?? 0) + match[0].length - 1;
108
+ ranges.push([start, braceEnd(mask, start)]);
109
+ } return ranges; }
110
+ function literal(argument) {
111
+ const trimmed = argument.trim();
112
+ return trimmed === "true" ? "true" : trimmed === "false" ? "false" : undefined;
113
+ }
114
+ function finding(ruleId, title, file, line, identity, severity) {
115
+ return makeAndroidFinding({ ruleId, title, severity, confidence: "high", description: `${title}. This is static source evidence only and does not prove runtime execution or exploitability.`, manifestPath: file, identity, location: { line }, recommendation: "Review the configuration and keep WebView capabilities limited to documented requirements." });
116
+ }
117
+ function candidate(ruleId, targetRoot, file, line, summary, rawValue) {
118
+ return makeCandidateEvidence({ ruleId, category: "android-webview", confidence: "medium", modulePath: file.modulePath, location: buildAndroidSourceLocation(targetRoot, file.absolutePath, { line }), summary, rawValue: rawValue.slice(0, MAX_ARGUMENT), resolutionState: "resolved", staticAnalysisLimitations: ["Bounded lexical source analysis; receiver types, call ordering, and dynamic values are not resolved."] });
119
+ }
120
+ export function auditAndroidWebViewSecurity(targetRoot, detection) {
121
+ if (detection.projectKind === "non-android")
122
+ return { id: ANDROID_WEBVIEW_SECURITY_AUDIT_CHECK_ID, category: "android-webview", title: "Android WebView security audit", status: "unsupported", requirementLevel: "optional", ran: false, skipped: true, skipInfo: { checkId: ANDROID_WEBVIEW_SECURITY_AUDIT_CHECK_ID, reason: "Target was not detected as Android.", requirementLevel: "optional", verdictImpact: "does not apply", recommendedNextAction: "Run against an Android project." }, evidence: [], findings: [], warnings: [], errors: [], sourcePaths: [], confidence: "unknown", environmentRequirements: [], candidateEvidence: [] };
123
+ const discovered = discoverSecretSourceFiles(targetRoot, detection.modules.map((m) => m.path));
124
+ const files = discovered.files.filter((file) => /\.(java|kt)$/.test(file.relativePath));
125
+ const findings = [];
126
+ const candidates = [];
127
+ for (const file of files) {
128
+ const mask = executableMask(file.content);
129
+ const ssl = sslRanges(mask);
130
+ const scopes = scopeRanges(mask);
131
+ const calls = [];
132
+ const contextAt = (offset) => scopes.filter((scope) => offset >= scope.from && offset <= scope.to).sort((a, b) => a.to - a.from - (b.to - b.from))[0]?.id ?? "(unresolved-scope)";
133
+ const aliases = new Map();
134
+ for (const match of mask.matchAll(SETTINGS_ALIAS))
135
+ aliases.set(`${contextAt(match.index ?? 0)}:${match[1]}`, match[2]);
136
+ const normalize = (receiver, method, context) => { const stripped = receiver.replace(/\.getSettings\(\)$|\.settings$/, ""); if (stripped !== receiver)
137
+ return stripped; if (method.startsWith("set") && aliases.has(`${context}:${receiver}`))
138
+ return aliases.get(`${context}:${receiver}`); return receiver; };
139
+ CALL.lastIndex = 0;
140
+ for (let match; (match = CALL.exec(mask));) {
141
+ const start = match.index;
142
+ const original = file.content.slice(start, start + match[0].length);
143
+ const argument = original.slice(original.indexOf("(") + 1, -1).slice(0, MAX_ARGUMENT);
144
+ const line = lineAt(file.content, start);
145
+ const context = contextAt(start);
146
+ calls.push({ receiver: normalize(match[1] ?? "(unresolved)", match[2], context), method: match[2], argument, line, context, inSslCallback: ssl.some(([from, to]) => start > from && start < to) });
147
+ }
148
+ PROPERTY.lastIndex = 0;
149
+ for (let match; (match = PROPERTY.exec(mask));) {
150
+ const start = match.index;
151
+ const original = file.content.slice(start, start + match[0].length);
152
+ const eq = original.indexOf("=");
153
+ const propertyToMethod = { javaScriptEnabled: "setJavaScriptEnabled", allowFileAccess: "setAllowFileAccess", allowContentAccess: "setAllowContentAccess", allowFileAccessFromFileURLs: "setAllowFileAccessFromFileURLs", allowUniversalAccessFromFileURLs: "setAllowUniversalAccessFromFileURLs", mixedContentMode: "setMixedContentMode" };
154
+ const method = propertyToMethod[match[2]], context = contextAt(start);
155
+ calls.push({ receiver: normalize(match[1], method, context), method, argument: original.slice(eq + 1).trim().slice(0, MAX_ARGUMENT), line: lineAt(file.content, start), context, inSslCallback: ssl.some(([from, to]) => start > from && start < to) });
156
+ }
157
+ for (const call of calls) {
158
+ const value = literal(call.argument);
159
+ if (call.method === "setAllowUniversalAccessFromFileURLs" && value === "true")
160
+ findings.push(finding("android-webview-universal-file-url-access", "WebView universal file URL access is explicitly enabled", file.relativePath, call.line, call.context, "major"));
161
+ else if (call.method === "setMixedContentMode" && /MIXED_CONTENT_ALWAYS_ALLOW|0\b/.test(call.argument))
162
+ findings.push(finding("android-webview-mixed-content", "WebView mixed content is explicitly always allowed", file.relativePath, call.line, call.context, "major"));
163
+ else if (call.method === "setWebContentsDebuggingEnabled" && value === "true" && /(?:^|\.)WebView$/.test(call.receiver))
164
+ findings.push(finding("android-webview-debugging-enabled", "WebView debugging is explicitly enabled", file.relativePath, call.line, call.context, "minor"));
165
+ else if (call.method === "proceed" && call.inSslCallback)
166
+ findings.push(finding("android-webview-ssl-error-proceed", "SSL error callback proceeds despite the certificate error", file.relativePath, call.line, call.context, "major"));
167
+ else if (call.method === "setJavaScriptEnabled" && value !== "false")
168
+ candidates.push(candidate("android-webview-javascript-enabled", targetRoot, file, call.line, value === "true" ? "JavaScript is explicitly enabled" : "JavaScript enablement is dynamic or unresolved", call.argument));
169
+ else if (["setAllowFileAccess", "setAllowContentAccess", "setAllowFileAccessFromFileURLs", "setAllowUniversalAccessFromFileURLs"].includes(call.method) && value !== "false")
170
+ candidates.push(candidate(call.method === "setAllowUniversalAccessFromFileURLs" ? "android-webview-universal-file-url-access" : "android-webview-file-content-access", targetRoot, file, call.line, `WebView ${call.method} is ${value === "true" ? "enabled" : "dynamic or unresolved"}`, call.argument));
171
+ else if (call.method === "addJavascriptInterface")
172
+ candidates.push(candidate("android-webview-javascript-interface-exposure", targetRoot, file, call.line, "JavaScript interface registration source evidence", call.argument));
173
+ else if (call.method === "setMixedContentMode" && /MIXED_CONTENT_COMPATIBILITY_MODE|1\b/.test(call.argument))
174
+ candidates.push(candidate("android-webview-mixed-content", targetRoot, file, call.line, "WebView mixed-content compatibility mode is configured", call.argument));
175
+ else if (call.method === "setMixedContentMode" && !/MIXED_CONTENT_NEVER_ALLOW|2\b/.test(call.argument))
176
+ candidates.push(candidate("android-webview-mixed-content", targetRoot, file, call.line, "WebView mixed-content mode is dynamic or unresolved", call.argument));
177
+ else if (call.method === "setWebContentsDebuggingEnabled" && value !== "false")
178
+ candidates.push(candidate("android-webview-debugging-enabled", targetRoot, file, call.line, "WebView debugging state is dynamic or receiver context is unresolved", call.argument));
179
+ else if (["loadUrl", "postUrl", "loadData", "loadDataWithBaseURL"].includes(call.method)) {
180
+ if (/^\s*["']javascript:/i.test(call.argument))
181
+ candidates.push(candidate("android-webview-suspicious-url-loading", targetRoot, file, call.line, "javascript: WebView URL loading evidence", call.argument));
182
+ else if (/^\s*["'](?:http|file|content):/i.test(call.argument))
183
+ candidates.push(candidate("android-webview-suspicious-url-loading", targetRoot, file, call.line, "WebView loads a review-oriented literal scheme", call.argument));
184
+ else if (!/^\s*["']https:/i.test(call.argument))
185
+ candidates.push(candidate("android-webview-suspicious-url-loading", targetRoot, file, call.line, "Dynamic or unsupported WebView content loading argument", call.argument));
186
+ }
187
+ }
188
+ for (const [from, to] of ssl) {
189
+ const contained = calls.filter((call) => call.inSslCallback && call.method === "proceed" || call.inSslCallback && call.method === "cancel");
190
+ if (contained.length === 0)
191
+ candidates.push(candidate("android-webview-ssl-error-proceed", targetRoot, file, lineAt(file.content, from), mask.slice(from, Math.min(to, from + MAX_ARGUMENT)).trim() ? "SSL error callback delegates or has unresolved local behavior" : "SSL error callback is empty", file.content.slice(from, Math.min(to, from + MAX_ARGUMENT))));
192
+ }
193
+ const same = (a, b) => a.receiver !== "(unresolved)" && a.receiver === b.receiver && a.context === b.context;
194
+ for (const js of calls.filter((c) => c.method === "setJavaScriptEnabled" && literal(c.argument) === "true"))
195
+ for (const bridge of calls.filter((c) => c.method === "addJavascriptInterface" && same(c, js)))
196
+ findings.push(finding("android-webview-javascript-interface-exposure", "JavaScript and addJavascriptInterface occur on the same receiver in one method", file.relativePath, bridge.line, `${js.receiver}:${js.context}`, "major"));
197
+ for (const js of calls.filter((c) => c.method === "setJavaScriptEnabled" && literal(c.argument) === "true"))
198
+ for (const access of calls.filter((c) => ["setAllowFileAccessFromFileURLs", "setAllowUniversalAccessFromFileURLs"].includes(c.method) && literal(c.argument) === "true" && same(c, js)))
199
+ findings.push(finding("android-webview-file-content-access", "JavaScript and file URL access occur on the same receiver in one method", file.relativePath, access.line, `${js.receiver}:${js.context}`, "major"));
200
+ for (const js of calls.filter((c) => c.method === "setJavaScriptEnabled" && literal(c.argument) === "true"))
201
+ for (const url of calls.filter((c) => c.method === "loadUrl" && /^\s*["']javascript:/i.test(c.argument) && same(c, js)))
202
+ findings.push(finding("android-webview-suspicious-url-loading", "JavaScript is enabled and a javascript: URL is loaded on the same receiver", file.relativePath, url.line, `${js.receiver}:${js.context}`, "major"));
203
+ for (const mixed of calls.filter((c) => c.method === "setMixedContentMode" && /MIXED_CONTENT_ALWAYS_ALLOW|0\b/.test(c.argument)))
204
+ for (const url of calls.filter((c) => c.method === "loadUrl" && /^\s*["']http:\/\//i.test(c.argument) && same(c, mixed)))
205
+ findings.push(finding("android-webview-mixed-content", "Always-allowed mixed content and HTTP loading occur on the same receiver", file.relativePath, url.line, `${mixed.receiver}:${mixed.context}`, "major"));
206
+ }
207
+ const uniqueFindings = [...new Map(findings.map((item) => [item.id, item])).values()].sort((a, b) => a.id.localeCompare(b.id));
208
+ const uniqueCandidates = [...new Map(candidates.map((item) => [item.id, item])).values()];
209
+ return { id: ANDROID_WEBVIEW_SECURITY_AUDIT_CHECK_ID, category: "android-webview", title: "Android WebView security audit", status: uniqueFindings.length ? "failed" : "passed", requirementLevel: "required", ran: true, skipped: false, evidence: [`${files.length} Java/Kotlin file(s) scanned`], findings: uniqueFindings, warnings: [...discovered.skipped.map((item) => `${item.relativePath}: skipped (${item.reason})`), "Static analysis only: no compiler, runtime, URL provenance, or whole-program analysis was used."], errors: [], sourcePaths: files.map((file) => file.relativePath), confidence: discovered.skipped.length ? "medium" : "high", environmentRequirements: [], candidateEvidence: sortCandidateEvidence(uniqueCandidates) };
210
+ }
@@ -0,0 +1 @@
1
+ export { auditAndroidWebViewSecurity, ANDROID_WEBVIEW_SECURITY_AUDIT_CHECK_ID } from "./checkResult.js";
@@ -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