@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.
- package/README.md +10 -61
- package/dist/scripts/security/validate.js +38 -5
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/analyzeBackupConfiguration.js +300 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/checkResult.js +41 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/index.js +8 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/manifestEvidence.js +102 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/parseBackupRules.js +110 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/types.js +27 -0
- package/dist/src/mobile/android/advancedSecurity/candidateEvidence.js +50 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/analyzeClipboardSecurity.js +166 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/checkResult.js +74 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/collectClipboardEvidence.js +82 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/types.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/checkResult.js +255 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintSarif.js +37 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintXml.js +36 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/artifacts.js +95 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/boundedOutput.js +33 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/checkResult.js +173 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/command.js +33 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/parseDependencyCheckJson.js +133 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/discoverExecutable.js +13 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/index.js +8 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/minimalEnvironment.js +26 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/mutation.js +77 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/osv/checkResult.js +161 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/osv/command.js +25 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/osv/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/osv/parseOsvJson.js +136 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/runBoundedExternalTool.js +51 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/runRequestedAndroidExternalTools.js +54 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/checkResult.js +170 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/command.js +28 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/index.js +2 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/parseSemgrepJson.js +141 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/rules.js +105 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/types.js +107 -0
- package/dist/src/mobile/android/advancedSecurity/fileProvider/checkResult.js +188 -0
- package/dist/src/mobile/android/advancedSecurity/fileProvider/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/analyzeFirebaseGoogleServices.js +197 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/checkResult.js +77 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/collectFirebaseServiceEvidence.js +193 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/discoverFirebaseArtifacts.js +128 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseDatabaseRules.js +55 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirebaseConfig.js +75 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirestoreStorageRules.js +88 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseGoogleServicesConfig.js +55 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/types.js +6 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/analyzeNetworkSecurity.js +394 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/checkResult.js +47 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/deriveEffectiveNetworkPolicy.js +52 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/index.js +14 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/manifestEvidence.js +109 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/parseNetworkSecurityConfig.js +186 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/types.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/ordering.js +44 -0
- package/dist/src/mobile/android/advancedSecurity/redaction.js +54 -0
- package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/analyzeReleaseConfiguration.js +221 -0
- package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/checkResult.js +39 -0
- package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/index.js +7 -0
- package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/manifestEvidence.js +19 -0
- package/dist/src/mobile/android/advancedSecurity/resourceReference.js +69 -0
- package/dist/src/mobile/android/advancedSecurity/resourceResolution.js +118 -0
- package/dist/src/mobile/android/advancedSecurity/ruleIds.js +141 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/analyzeSecretCandidates.js +116 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/checkResult.js +80 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/discoverSecretSourceFiles.js +152 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/index.js +9 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/matchSecretCandidates.js +136 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/suppressValue.js +61 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/types.js +11 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveCategories.js +34 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifyDirectExpression.js +99 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifySensitiveIdentifier.js +111 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/index.js +3 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/localSourceContext.js +90 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/splitArguments.js +59 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/analyzeSensitiveLogging.js +117 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/checkResult.js +74 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/collectLoggingEvidence.js +87 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/types.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/analyzeSensitiveStorage.js +182 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/checkResult.js +74 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/collectStorageEvidence.js +109 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/types.js +4 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/analyzeSigningConfiguration.js +270 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/checkResult.js +119 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/discoverKeystoreCandidates.js +55 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/extractSigningConfigurations.js +81 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/index.js +8 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/types.js +20 -0
- package/dist/src/mobile/android/advancedSecurity/sourceLocation.js +15 -0
- package/dist/src/mobile/android/advancedSecurity/webview/checkResult.js +210 -0
- package/dist/src/mobile/android/advancedSecurity/webview/index.js +1 -0
- package/dist/src/mobile/android/gradle/moduleMetadataExtractor.js +123 -17
- package/dist/src/mobile/android/manifest/parseAndroidManifest.js +27 -4
- package/dist/src/mobile/android/manifest/xml/parseXml.js +25 -9
- package/dist/src/mobile/android/report/model.js +32 -1
- package/dist/src/mobile/android/report/renderAndroidReport.js +63 -0
- package/dist/src/mobile/android/validate/validateAndroidTarget.js +146 -3
- package/dist/src/mobile/android/validation/checkResult.js +7 -0
- package/dist/src/securityValidation/commandRunner.js +10 -1
- package/dist/src/securityValidation/validate/cliOptions.js +46 -0
- package/package.json +1 -1
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/debug/res/xml/ambiguous_backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/ambiguous_backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/backup_rules.xml +5 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/broad_backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/data_extraction_rules.xml +10 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/unsupported_domain_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/malformed_backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/unsupported_root.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/debug/res/xml/ambiguous_config.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/ambiguous_config.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/base_cleartext_false.xml +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/broad_and_duplicate_domains.xml +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/empty_domain.xml +6 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/malformed_pin.xml +11 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/network_security_config.xml +26 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/malformed_network_security_config.xml +6 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/unsupported_root.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-dynamic.gradle +7 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-false.gradle +9 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-true.gradle +13 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-false.gradle.kts +7 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-true.gradle.kts +10 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/missing-release-build-type.gradle.kts +7 -0
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/debug/res/xml/network_security_config.xml +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/network_security_config.xml +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-b/src/main/res/xml/malformed_network_security_config.xml +7 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.java +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.kt +9 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle +11 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle.kts +11 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/google-services.json +16 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/local.properties +3 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/binary.properties +0 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/oversized.json +1 -0
- package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/app/fake-release-keystore.jks +1 -0
- 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
|
-
|
|
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
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
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
|
-
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
74
|
+
if (depth === 0)
|
|
75
|
+
return text.slice(openBraceIndex + 1, i);
|
|
72
76
|
}
|
|
73
77
|
}
|
|
74
|
-
|
|
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
|
-
|
|
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:
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|