@dailephd/my-dev-kit-lab 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +269 -47
- package/dist/scripts/audits/runAudit.js +28 -1
- package/dist/scripts/security/validate.js +38 -5
- package/dist/src/audits/core/auditConfig.js +16 -1
- package/dist/src/audits/core/auditRunner.js +6 -2
- package/dist/src/audits/report/auditReportModel.js +7 -0
- package/dist/src/audits/report/renderAuditJsonReport.js +1 -0
- package/dist/src/audits/report/renderAuditTextReport.js +37 -0
- package/dist/src/audits/security/androidAuditIntegration.js +145 -0
- package/dist/src/audits/security/mapAndroidSecurityFindingToAuditIssue.js +116 -0
- package/dist/src/audits/security/mapSecurityFindingToAuditIssue.js +15 -5
- package/dist/src/audits/security/securityAuditAdapter.js +20 -2
- package/dist/src/audits/security/securityAuditTypes.js +37 -0
- 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
package/dist/src/mobile/android/advancedSecurity/backupConfiguration/analyzeBackupConfiguration.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { makeAndroidFinding } from "../../audit/androidFinding.js";
|
|
3
|
+
import { buildAndroidSourceLocation } from "../sourceLocation.js";
|
|
4
|
+
import { makeCandidateEvidence } from "../candidateEvidence.js";
|
|
5
|
+
import { extractManifestBackupEvidence } from "./manifestEvidence.js";
|
|
6
|
+
function loc(targetRoot, absolutePath, position) {
|
|
7
|
+
return buildAndroidSourceLocation(targetRoot, absolutePath, position);
|
|
8
|
+
}
|
|
9
|
+
// A conservative, narrow "broad inclusion" signal: the entire root, or an
|
|
10
|
+
// entire sensitive domain with no path narrowing ("." — the whole domain).
|
|
11
|
+
function isBroadInclude(rule) {
|
|
12
|
+
if (rule.kind !== "include" || rule.malformed)
|
|
13
|
+
return false;
|
|
14
|
+
if (rule.domain === "root" || rule.domain === "device_root")
|
|
15
|
+
return true;
|
|
16
|
+
const sensitiveDomains = new Set(["sharedpref", "database", "external", "device_sharedpref", "device_database", "device_external"]);
|
|
17
|
+
return rule.domain !== undefined && sensitiveDomains.has(rule.domain) && rule.path === ".";
|
|
18
|
+
}
|
|
19
|
+
function analyzeRuleEntries(rules, ruleId, scopeLabel, sourceAbsolutePath, targetRoot, modulePath, sourceRelativePath) {
|
|
20
|
+
const candidates = [];
|
|
21
|
+
const evidenceText = [];
|
|
22
|
+
let hasBroadInclude = false;
|
|
23
|
+
for (const rule of rules) {
|
|
24
|
+
const location = loc(targetRoot, sourceAbsolutePath, { line: rule.location.line, column: rule.location.column });
|
|
25
|
+
if (rule.malformed) {
|
|
26
|
+
candidates.push(makeCandidateEvidence({
|
|
27
|
+
ruleId,
|
|
28
|
+
category: "android-backup-configuration",
|
|
29
|
+
confidence: "low",
|
|
30
|
+
modulePath,
|
|
31
|
+
location,
|
|
32
|
+
summary: `Malformed ${scopeLabel} rule: ${rule.malformedReason}`,
|
|
33
|
+
rawValue: `${rule.domainRaw ?? ""} ${rule.path ?? ""}`.trim(),
|
|
34
|
+
resolutionState: "malformed",
|
|
35
|
+
}));
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (rule.domain === undefined) {
|
|
39
|
+
candidates.push(makeCandidateEvidence({
|
|
40
|
+
ruleId,
|
|
41
|
+
category: "android-backup-configuration",
|
|
42
|
+
confidence: "low",
|
|
43
|
+
modulePath,
|
|
44
|
+
location,
|
|
45
|
+
summary: `Unsupported backup domain value in ${scopeLabel}: "${rule.domainRaw}"`,
|
|
46
|
+
rawValue: rule.domainRaw,
|
|
47
|
+
resolutionState: "unsupported",
|
|
48
|
+
}));
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (rule.kind === "exclude") {
|
|
52
|
+
evidenceText.push(`${sourceRelativePath}: ${scopeLabel} restrictive exclusion — domain=${rule.domain}, path=${rule.path}`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (isBroadInclude(rule)) {
|
|
56
|
+
hasBroadInclude = true;
|
|
57
|
+
candidates.push(makeCandidateEvidence({
|
|
58
|
+
ruleId,
|
|
59
|
+
category: "android-backup-configuration",
|
|
60
|
+
confidence: "medium",
|
|
61
|
+
modulePath,
|
|
62
|
+
location,
|
|
63
|
+
summary: `Broad ${scopeLabel} inclusion: domain=${rule.domain}, path=${rule.path}`,
|
|
64
|
+
rawValue: `${rule.domain}:${rule.path}`,
|
|
65
|
+
resolutionState: "resolved",
|
|
66
|
+
staticAnalysisLimitations: [
|
|
67
|
+
"Domain/path naming is conservative evidence only; it does not prove the included data is actually sensitive or that a backup is ever performed.",
|
|
68
|
+
],
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { candidates, evidenceText, hasBroadInclude };
|
|
73
|
+
}
|
|
74
|
+
function analyzeBackupRulesParseResult(parseResult, ruleId, sourceRelativePath, targetRoot, modulePath) {
|
|
75
|
+
const sourceAbsolutePath = path.join(targetRoot, sourceRelativePath);
|
|
76
|
+
if (parseResult.state === "malformed-xml") {
|
|
77
|
+
return {
|
|
78
|
+
candidates: [
|
|
79
|
+
makeCandidateEvidence({
|
|
80
|
+
ruleId,
|
|
81
|
+
category: "android-backup-configuration",
|
|
82
|
+
confidence: "medium",
|
|
83
|
+
modulePath,
|
|
84
|
+
location: loc(targetRoot, sourceAbsolutePath, parseResult.location ? { line: parseResult.location.line, column: parseResult.location.column } : undefined),
|
|
85
|
+
summary: `Referenced backup rules file is malformed XML: ${parseResult.reason}`,
|
|
86
|
+
rawValue: parseResult.reason,
|
|
87
|
+
resolutionState: "malformed",
|
|
88
|
+
}),
|
|
89
|
+
],
|
|
90
|
+
evidenceText: [],
|
|
91
|
+
hasBroadCloudOrLegacyInclude: false,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (parseResult.state === "unsupported-root") {
|
|
95
|
+
return {
|
|
96
|
+
candidates: [
|
|
97
|
+
makeCandidateEvidence({
|
|
98
|
+
ruleId,
|
|
99
|
+
category: "android-backup-configuration",
|
|
100
|
+
confidence: "medium",
|
|
101
|
+
modulePath,
|
|
102
|
+
location: loc(targetRoot, sourceAbsolutePath),
|
|
103
|
+
summary: `Referenced XML file has an unsupported root element <${parseResult.rootTagName}>`,
|
|
104
|
+
rawValue: parseResult.rootTagName,
|
|
105
|
+
resolutionState: "unsupported",
|
|
106
|
+
}),
|
|
107
|
+
],
|
|
108
|
+
evidenceText: [],
|
|
109
|
+
hasBroadCloudOrLegacyInclude: false,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (parseResult.state === "parsed-full-backup-content") {
|
|
113
|
+
const result = analyzeRuleEntries(parseResult.model.rules, ruleId, "full-backup-content", sourceAbsolutePath, targetRoot, modulePath, sourceRelativePath);
|
|
114
|
+
return { candidates: result.candidates, evidenceText: result.evidenceText, hasBroadCloudOrLegacyInclude: result.hasBroadInclude };
|
|
115
|
+
}
|
|
116
|
+
// parsed-data-extraction-rules
|
|
117
|
+
const cloudResult = parseResult.model.cloudBackup
|
|
118
|
+
? analyzeRuleEntries(parseResult.model.cloudBackup.rules, ruleId, "cloud-backup", sourceAbsolutePath, targetRoot, modulePath, sourceRelativePath)
|
|
119
|
+
: { candidates: [], evidenceText: [], hasBroadInclude: false };
|
|
120
|
+
const transferResult = parseResult.model.deviceTransfer
|
|
121
|
+
? analyzeRuleEntries(parseResult.model.deviceTransfer.rules, ruleId, "device-transfer", sourceAbsolutePath, targetRoot, modulePath, sourceRelativePath)
|
|
122
|
+
: { candidates: [], evidenceText: [], hasBroadInclude: false };
|
|
123
|
+
// Device-transfer broadness stays a review candidate only — it is local
|
|
124
|
+
// device-to-device transfer, not cloud exfiltration, so it never
|
|
125
|
+
// contributes to the allowBackup-level "major" escalation.
|
|
126
|
+
return {
|
|
127
|
+
candidates: [...cloudResult.candidates, ...transferResult.candidates],
|
|
128
|
+
evidenceText: [...cloudResult.evidenceText, ...transferResult.evidenceText],
|
|
129
|
+
hasBroadCloudOrLegacyInclude: cloudResult.hasBroadInclude,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function analyzeXmlReferenceEvidence(evidence, ruleId, targetRoot, manifestAbsolutePath, modulePath, applicationLocation) {
|
|
133
|
+
switch (evidence.state) {
|
|
134
|
+
case "absent":
|
|
135
|
+
return { candidates: [], evidenceText: [], hasBroadInclude: false };
|
|
136
|
+
case "resolved": {
|
|
137
|
+
const result = analyzeBackupRulesParseResult(evidence.parseResult, ruleId, evidence.candidate.relativePath, targetRoot, modulePath);
|
|
138
|
+
return { candidates: result.candidates, evidenceText: result.evidenceText, hasBroadInclude: result.hasBroadCloudOrLegacyInclude };
|
|
139
|
+
}
|
|
140
|
+
case "ambiguous": {
|
|
141
|
+
const candidates = [
|
|
142
|
+
makeCandidateEvidence({
|
|
143
|
+
ruleId,
|
|
144
|
+
category: "android-backup-configuration",
|
|
145
|
+
confidence: "low",
|
|
146
|
+
modulePath,
|
|
147
|
+
location: loc(targetRoot, manifestAbsolutePath, applicationLocation),
|
|
148
|
+
summary: `Backup rules reference "${evidence.raw}" resolved to ${evidence.candidates.length} ambiguous candidates across source sets; no candidate was arbitrarily selected`,
|
|
149
|
+
rawValue: evidence.raw,
|
|
150
|
+
resolutionState: "unresolved",
|
|
151
|
+
}),
|
|
152
|
+
];
|
|
153
|
+
let hasBroadInclude = false;
|
|
154
|
+
for (const candidate of evidence.candidates) {
|
|
155
|
+
const result = analyzeBackupRulesParseResult(candidate.parseResult, ruleId, candidate.candidate.relativePath, targetRoot, modulePath);
|
|
156
|
+
candidates.push(...result.candidates);
|
|
157
|
+
hasBroadInclude = hasBroadInclude || result.hasBroadCloudOrLegacyInclude;
|
|
158
|
+
}
|
|
159
|
+
return { candidates, evidenceText: [], hasBroadInclude };
|
|
160
|
+
}
|
|
161
|
+
case "missing":
|
|
162
|
+
return {
|
|
163
|
+
candidates: [
|
|
164
|
+
makeCandidateEvidence({
|
|
165
|
+
ruleId,
|
|
166
|
+
category: "android-backup-configuration",
|
|
167
|
+
confidence: "low",
|
|
168
|
+
modulePath,
|
|
169
|
+
location: loc(targetRoot, manifestAbsolutePath, applicationLocation),
|
|
170
|
+
summary: `Backup rules reference "${evidence.raw}" did not resolve to any file in the searched source sets`,
|
|
171
|
+
rawValue: evidence.raw,
|
|
172
|
+
resolutionState: "missing",
|
|
173
|
+
staticAnalysisLimitations: ["A missing referenced rules file is not automatically a vulnerability; runtime behavior cannot be proven statically."],
|
|
174
|
+
}),
|
|
175
|
+
],
|
|
176
|
+
evidenceText: [],
|
|
177
|
+
hasBroadInclude: false,
|
|
178
|
+
};
|
|
179
|
+
case "placeholder":
|
|
180
|
+
return {
|
|
181
|
+
candidates: [
|
|
182
|
+
makeCandidateEvidence({
|
|
183
|
+
ruleId,
|
|
184
|
+
category: "android-backup-configuration",
|
|
185
|
+
confidence: "low",
|
|
186
|
+
modulePath,
|
|
187
|
+
location: loc(targetRoot, manifestAbsolutePath, applicationLocation),
|
|
188
|
+
summary: "Backup rules reference is an unresolved build-time placeholder",
|
|
189
|
+
rawValue: evidence.raw,
|
|
190
|
+
resolutionState: "unresolved",
|
|
191
|
+
}),
|
|
192
|
+
],
|
|
193
|
+
evidenceText: [],
|
|
194
|
+
hasBroadInclude: false,
|
|
195
|
+
};
|
|
196
|
+
case "malformed":
|
|
197
|
+
return {
|
|
198
|
+
candidates: [
|
|
199
|
+
makeCandidateEvidence({
|
|
200
|
+
ruleId,
|
|
201
|
+
category: "android-backup-configuration",
|
|
202
|
+
confidence: "low",
|
|
203
|
+
modulePath,
|
|
204
|
+
location: loc(targetRoot, manifestAbsolutePath, applicationLocation),
|
|
205
|
+
summary: `Backup rules reference is malformed: ${evidence.reason}`,
|
|
206
|
+
rawValue: evidence.raw,
|
|
207
|
+
resolutionState: "malformed",
|
|
208
|
+
}),
|
|
209
|
+
],
|
|
210
|
+
evidenceText: [],
|
|
211
|
+
hasBroadInclude: false,
|
|
212
|
+
};
|
|
213
|
+
case "unsupported-type":
|
|
214
|
+
case "package-qualified":
|
|
215
|
+
case "module-unknown":
|
|
216
|
+
return {
|
|
217
|
+
candidates: [
|
|
218
|
+
makeCandidateEvidence({
|
|
219
|
+
ruleId,
|
|
220
|
+
category: "android-backup-configuration",
|
|
221
|
+
confidence: "low",
|
|
222
|
+
modulePath,
|
|
223
|
+
location: loc(targetRoot, manifestAbsolutePath, applicationLocation),
|
|
224
|
+
summary: evidence.state === "module-unknown"
|
|
225
|
+
? "Backup rules reference could not be resolved because this manifest's module could not be determined"
|
|
226
|
+
: `Backup rules reference is not a resolvable target-contained @xml/... resource (${evidence.state})`,
|
|
227
|
+
rawValue: evidence.raw,
|
|
228
|
+
resolutionState: "unsupported",
|
|
229
|
+
}),
|
|
230
|
+
],
|
|
231
|
+
evidenceText: [],
|
|
232
|
+
hasBroadInclude: false,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Analyzes one already-parsed manifest's backup/data-extraction evidence.
|
|
237
|
+
// Standalone until a later integration batch.
|
|
238
|
+
export function analyzeManifestBackupConfiguration(targetRoot, entry) {
|
|
239
|
+
const evidence = extractManifestBackupEvidence(targetRoot, entry);
|
|
240
|
+
const candidates = [];
|
|
241
|
+
const findings = [];
|
|
242
|
+
const evidenceText = [];
|
|
243
|
+
const modulePath = entry.modulePath;
|
|
244
|
+
const manifestAbsolutePath = path.join(targetRoot, entry.manifestPath);
|
|
245
|
+
const applicationLocation = evidence.applicationLocation;
|
|
246
|
+
let hasBroadInclude = false;
|
|
247
|
+
// fullBackupContent — legacy literal states never touch the resource
|
|
248
|
+
// resolver/parser (there is no file to analyze).
|
|
249
|
+
const fullBackupContent = evidence.fullBackupContent;
|
|
250
|
+
if (fullBackupContent.state === "legacy-literal-true" || fullBackupContent.state === "legacy-literal-false") {
|
|
251
|
+
evidenceText.push(`${entry.manifestPath}: fullBackupContent is a legacy literal (${fullBackupContent.raw})`);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
const result = analyzeXmlReferenceEvidence(fullBackupContent, "android-backup-full-backup-content", targetRoot, manifestAbsolutePath, modulePath, applicationLocation);
|
|
255
|
+
candidates.push(...result.candidates);
|
|
256
|
+
evidenceText.push(...result.evidenceText);
|
|
257
|
+
hasBroadInclude = hasBroadInclude || result.hasBroadInclude;
|
|
258
|
+
}
|
|
259
|
+
const dataExtractionResult = analyzeXmlReferenceEvidence(evidence.dataExtractionRules, "android-backup-data-extraction-rules", targetRoot, manifestAbsolutePath, modulePath, applicationLocation);
|
|
260
|
+
candidates.push(...dataExtractionResult.candidates);
|
|
261
|
+
evidenceText.push(...dataExtractionResult.evidenceText);
|
|
262
|
+
hasBroadInclude = hasBroadInclude || dataExtractionResult.hasBroadInclude;
|
|
263
|
+
// allowBackup analysis — the only source of an actual SecurityFinding in
|
|
264
|
+
// this analyzer (agents.txt Batch 3 section 9.7/9.17).
|
|
265
|
+
if (evidence.allowBackup.state === "explicit-true") {
|
|
266
|
+
findings.push(makeAndroidFinding({
|
|
267
|
+
ruleId: "android-backup-allow-backup",
|
|
268
|
+
title: hasBroadInclude
|
|
269
|
+
? "Application allows backup with a broad backup-rule inclusion"
|
|
270
|
+
: "Application manifest explicitly permits backup (android:allowBackup=\"true\")",
|
|
271
|
+
severity: hasBroadInclude ? "major" : "minor",
|
|
272
|
+
confidence: hasBroadInclude ? "high" : "medium",
|
|
273
|
+
description: hasBroadInclude
|
|
274
|
+
? "The manifest sets android:allowBackup=\"true\" and the resolved backup rules include a broad root/domain inclusion with no narrowing. This is high-confidence static evidence of a broad backup surface; it does not prove any specific file is actually backed up or that backup ever runs."
|
|
275
|
+
: "The manifest sets android:allowBackup=\"true\" with no broad-rule evidence found. This alone is not a vulnerability — it is the platform's own default behavior when the attribute is unset on old targetSdk versions, and is a common, legitimate configuration.",
|
|
276
|
+
manifestPath: entry.manifestPath,
|
|
277
|
+
location: applicationLocation,
|
|
278
|
+
evidenceDetails: [`source=manifest-attribute`, `broadRuleEvidence=${hasBroadInclude}`],
|
|
279
|
+
recommendation: hasBroadInclude
|
|
280
|
+
? "Narrow the backup rules to exclude sensitive domains, or set android:allowBackup=\"false\" if backup is not required."
|
|
281
|
+
: "Review whether backup should remain enabled and whether backup rules narrow the included data appropriately.",
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
else if (evidence.allowBackup.state === "malformed") {
|
|
285
|
+
candidates.push(makeCandidateEvidence({
|
|
286
|
+
ruleId: "android-backup-allow-backup",
|
|
287
|
+
category: "android-backup-configuration",
|
|
288
|
+
confidence: "low",
|
|
289
|
+
modulePath,
|
|
290
|
+
location: loc(targetRoot, manifestAbsolutePath, applicationLocation),
|
|
291
|
+
summary: "android:allowBackup has an unresolved or malformed value",
|
|
292
|
+
rawValue: evidence.allowBackup.raw,
|
|
293
|
+
resolutionState: "malformed",
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
else if (evidence.allowBackup.state === "explicit-false") {
|
|
297
|
+
evidenceText.push(`${entry.manifestPath}: allowBackup is explicitly false (restrictive)`);
|
|
298
|
+
}
|
|
299
|
+
return { evidence, candidates, findings, evidenceText };
|
|
300
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { buildAndroidManifestCheckResult } from "../../audit/checkResultBuilder.js";
|
|
2
|
+
import { sortCandidateEvidence } from "../ordering.js";
|
|
3
|
+
import { analyzeManifestBackupConfiguration } from "./analyzeBackupConfiguration.js";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// v0.4.1 Batch 3 — standalone Android backup/data-extraction check result.
|
|
6
|
+
//
|
|
7
|
+
// Reuses the existing manifest-audit status-decision policy
|
|
8
|
+
// (buildAndroidManifestCheckResult), mirroring Batch 2's checkResult.ts.
|
|
9
|
+
// Standalone: not called from validateAndroidTarget / any active
|
|
10
|
+
// orchestration, not rendered in reports, no CLI effect. Batch 8 integrates.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export const ANDROID_BACKUP_CONFIGURATION_AUDIT_CHECK_ID = "android-backup-configuration-audit";
|
|
13
|
+
export function auditAndroidBackupConfiguration(targetRoot, detection, manifests) {
|
|
14
|
+
const findings = [];
|
|
15
|
+
const candidateEvidence = [];
|
|
16
|
+
const evidence = [];
|
|
17
|
+
const warnings = [];
|
|
18
|
+
for (const entry of manifests) {
|
|
19
|
+
const result = analyzeManifestBackupConfiguration(targetRoot, entry);
|
|
20
|
+
findings.push(...result.findings);
|
|
21
|
+
candidateEvidence.push(...result.candidates);
|
|
22
|
+
evidence.push(`${entry.manifestPath}: allowBackup=${result.evidence.allowBackup.state}, fullBackupContent=${result.evidence.fullBackupContent.state}, dataExtractionRules=${result.evidence.dataExtractionRules.state}`, ...result.evidenceText);
|
|
23
|
+
}
|
|
24
|
+
const base = buildAndroidManifestCheckResult({
|
|
25
|
+
id: ANDROID_BACKUP_CONFIGURATION_AUDIT_CHECK_ID,
|
|
26
|
+
category: "android-backup-configuration",
|
|
27
|
+
title: "Android backup and data-extraction configuration audit",
|
|
28
|
+
detection,
|
|
29
|
+
manifests,
|
|
30
|
+
findings,
|
|
31
|
+
evidence,
|
|
32
|
+
warnings: [
|
|
33
|
+
...warnings,
|
|
34
|
+
"Static analysis only: does not evaluate Gradle manifest merging, Android resource overlays, or whether a backup actually occurs at runtime.",
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
...base,
|
|
39
|
+
candidateEvidence: sortCandidateEvidence(candidateEvidence),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.1 Batch 3 — public exports for the standalone Android backup/data-
|
|
3
|
+
// extraction audit. Mirrors Batch 2's networkSecurity/index.ts convention.
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
export { parseBackupRules } from "./parseBackupRules.js";
|
|
6
|
+
export { extractManifestBackupEvidence } from "./manifestEvidence.js";
|
|
7
|
+
export { analyzeManifestBackupConfiguration } from "./analyzeBackupConfiguration.js";
|
|
8
|
+
export { auditAndroidBackupConfiguration, ANDROID_BACKUP_CONFIGURATION_AUDIT_CHECK_ID } from "./checkResult.js";
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseAndroidResourceReference } from "../resourceReference.js";
|
|
4
|
+
import { resolveAndroidXmlResourceReference } from "../resourceResolution.js";
|
|
5
|
+
import { parseBackupRules } from "./parseBackupRules.js";
|
|
6
|
+
function classifyBooleanAttribute(value, raw) {
|
|
7
|
+
if (value === true)
|
|
8
|
+
return { state: "explicit-true", raw: raw ?? "true" };
|
|
9
|
+
if (value === false)
|
|
10
|
+
return { state: "explicit-false", raw: raw ?? "false" };
|
|
11
|
+
if (raw !== undefined)
|
|
12
|
+
return { state: "malformed", raw };
|
|
13
|
+
return { state: "missing" };
|
|
14
|
+
}
|
|
15
|
+
function parseBackupRulesFile(absolutePath) {
|
|
16
|
+
let xmlText;
|
|
17
|
+
try {
|
|
18
|
+
xmlText = readFileSync(absolutePath, "utf8");
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
return { state: "malformed-xml", reason: error instanceof Error ? error.message : "Unable to read the resolved backup rules file" };
|
|
22
|
+
}
|
|
23
|
+
return parseBackupRules(xmlText);
|
|
24
|
+
}
|
|
25
|
+
function classifyResolution(raw, resolution, targetRoot) {
|
|
26
|
+
switch (resolution.state) {
|
|
27
|
+
case "resolved": {
|
|
28
|
+
const candidate = resolution.candidates[0];
|
|
29
|
+
return {
|
|
30
|
+
state: "resolved",
|
|
31
|
+
raw,
|
|
32
|
+
modulePath: resolution.modulePath,
|
|
33
|
+
candidate,
|
|
34
|
+
parseResult: parseBackupRulesFile(path.join(targetRoot, candidate.relativePath)),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
case "ambiguous":
|
|
38
|
+
return {
|
|
39
|
+
state: "ambiguous",
|
|
40
|
+
raw,
|
|
41
|
+
modulePath: resolution.modulePath,
|
|
42
|
+
candidates: resolution.candidates.map((candidate) => ({
|
|
43
|
+
candidate,
|
|
44
|
+
parseResult: parseBackupRulesFile(path.join(targetRoot, candidate.relativePath)),
|
|
45
|
+
})),
|
|
46
|
+
};
|
|
47
|
+
case "missing":
|
|
48
|
+
return { state: "missing", raw, modulePath: resolution.modulePath, searchedSourceSets: resolution.searchedSourceSets };
|
|
49
|
+
case "malformed-reference":
|
|
50
|
+
return { state: "malformed", raw, reason: resolution.reason };
|
|
51
|
+
case "unsupported-reference":
|
|
52
|
+
// Unreachable in practice — see Batch 2's manifestEvidence.ts for the
|
|
53
|
+
// identical rationale (this function only calls the resolver once a
|
|
54
|
+
// "parsed" xml-type reference is confirmed).
|
|
55
|
+
return { state: "unsupported-type", raw, resourceType: "type" in resolution.parsed ? resolution.parsed.type : "unknown" };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function classifyXmlReference(targetRoot, entry, raw) {
|
|
59
|
+
if (raw === undefined)
|
|
60
|
+
return { state: "absent" };
|
|
61
|
+
const parsedRef = parseAndroidResourceReference(raw);
|
|
62
|
+
if (parsedRef.state === "placeholder")
|
|
63
|
+
return { state: "placeholder", raw };
|
|
64
|
+
if (parsedRef.state === "malformed")
|
|
65
|
+
return { state: "malformed", raw, reason: parsedRef.reason };
|
|
66
|
+
if (parsedRef.state === "empty")
|
|
67
|
+
return { state: "malformed", raw, reason: "Reference is empty" };
|
|
68
|
+
if (parsedRef.state === "unsupported-type")
|
|
69
|
+
return { state: "unsupported-type", raw, resourceType: parsedRef.type };
|
|
70
|
+
if (parsedRef.state === "package-qualified")
|
|
71
|
+
return { state: "package-qualified", raw, packageQualifier: parsedRef.packageQualifier };
|
|
72
|
+
if (entry.modulePath === undefined)
|
|
73
|
+
return { state: "module-unknown", raw };
|
|
74
|
+
const moduleAbsolutePath = path.join(targetRoot, entry.modulePath);
|
|
75
|
+
const resolution = resolveAndroidXmlResourceReference(targetRoot, moduleAbsolutePath, raw);
|
|
76
|
+
return classifyResolution(raw, resolution, targetRoot);
|
|
77
|
+
}
|
|
78
|
+
function classifyFullBackupContent(targetRoot, entry) {
|
|
79
|
+
const raw = entry.manifest.application.fullBackupContentRef;
|
|
80
|
+
if (raw === "true")
|
|
81
|
+
return { state: "legacy-literal-true", raw };
|
|
82
|
+
if (raw === "false")
|
|
83
|
+
return { state: "legacy-literal-false", raw };
|
|
84
|
+
return classifyXmlReference(targetRoot, entry, raw);
|
|
85
|
+
}
|
|
86
|
+
// Extracts and classifies backup/data-extraction/debuggable/testOnly
|
|
87
|
+
// evidence for one already-parsed manifest. `targetRoot` must be the
|
|
88
|
+
// absolute Android target root.
|
|
89
|
+
export function extractManifestBackupEvidence(targetRoot, entry) {
|
|
90
|
+
const app = entry.manifest.application;
|
|
91
|
+
return {
|
|
92
|
+
manifestPath: entry.manifestPath,
|
|
93
|
+
modulePath: entry.modulePath,
|
|
94
|
+
sourceSetKind: entry.sourceSetKind,
|
|
95
|
+
applicationLocation: app.location,
|
|
96
|
+
allowBackup: classifyBooleanAttribute(app.allowBackup, app.allowBackupRaw),
|
|
97
|
+
debuggable: classifyBooleanAttribute(app.debuggable, app.debuggableRaw),
|
|
98
|
+
testOnly: classifyBooleanAttribute(app.testOnly, app.testOnlyRaw),
|
|
99
|
+
fullBackupContent: classifyFullBackupContent(targetRoot, entry),
|
|
100
|
+
dataExtractionRules: classifyXmlReference(targetRoot, entry, app.dataExtractionRulesRef),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { findChildren, parseXmlDocument } from "../../manifest/xml/parseXml.js";
|
|
2
|
+
import { BACKUP_DOMAIN_VALUES, MAX_BACKUP_RULES_PER_SCOPE, } from "./types.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// v0.4.1 Batch 3 — dependency-free parser for the legacy
|
|
5
|
+
// <full-backup-content> and newer <data-extraction-rules> XML formats.
|
|
6
|
+
//
|
|
7
|
+
// Builds on the existing bounded XML tree parser
|
|
8
|
+
// (src/mobile/android/manifest/xml/parseXml.ts), mirroring Batch 2's
|
|
9
|
+
// parseNetworkSecurityConfig.ts layering — no second XML tokenizer. Parses
|
|
10
|
+
// exactly one already-resolved, target-contained file; never merges files,
|
|
11
|
+
// never evaluates resource overlays, never invokes Gradle/Android SDK/
|
|
12
|
+
// network. Malformed XML or an unsupported root never throws.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
function getUnprefixedAttribute(element, localName) {
|
|
15
|
+
return element.attributes.find((attr) => attr.prefix === undefined && attr.localName === localName)?.value;
|
|
16
|
+
}
|
|
17
|
+
function parseBooleanAttribute(element, localName) {
|
|
18
|
+
const raw = getUnprefixedAttribute(element, localName);
|
|
19
|
+
if (raw === undefined)
|
|
20
|
+
return {};
|
|
21
|
+
if (raw === "true")
|
|
22
|
+
return { value: true, raw };
|
|
23
|
+
if (raw === "false")
|
|
24
|
+
return { value: false, raw };
|
|
25
|
+
return { raw };
|
|
26
|
+
}
|
|
27
|
+
function isBackupDomainValue(value) {
|
|
28
|
+
return BACKUP_DOMAIN_VALUES.includes(value);
|
|
29
|
+
}
|
|
30
|
+
function parseRuleEntry(kind, element) {
|
|
31
|
+
const domainRaw = getUnprefixedAttribute(element, "domain");
|
|
32
|
+
const path = getUnprefixedAttribute(element, "path");
|
|
33
|
+
const requireFlagsRaw = getUnprefixedAttribute(element, "requireFlags");
|
|
34
|
+
const domain = domainRaw !== undefined && isBackupDomainValue(domainRaw) ? domainRaw : undefined;
|
|
35
|
+
const malformed = domainRaw === undefined || path === undefined || path.length === 0;
|
|
36
|
+
const malformedReason = domainRaw === undefined ? "Missing domain attribute" : path === undefined || path.length === 0 ? "Missing or empty path attribute" : undefined;
|
|
37
|
+
return {
|
|
38
|
+
kind,
|
|
39
|
+
domainRaw,
|
|
40
|
+
domain,
|
|
41
|
+
path,
|
|
42
|
+
requireFlagsRaw,
|
|
43
|
+
location: element.location,
|
|
44
|
+
malformed,
|
|
45
|
+
malformedReason,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function parseRuleEntries(scopeElement) {
|
|
49
|
+
const includeEntries = findChildren(scopeElement, "include").map((el) => parseRuleEntry("include", el));
|
|
50
|
+
const excludeEntries = findChildren(scopeElement, "exclude").map((el) => parseRuleEntry("exclude", el));
|
|
51
|
+
// Preserves document order (include/exclude interleaving is semantically
|
|
52
|
+
// meaningful in the real format) rather than grouping by kind — includes
|
|
53
|
+
// are collected first only because findChildren is kind-specific; the
|
|
54
|
+
// caller should not assume this array reflects source order for mixed
|
|
55
|
+
// kinds beyond what findChildren naturally preserves per kind.
|
|
56
|
+
return [...includeEntries, ...excludeEntries].slice(0, MAX_BACKUP_RULES_PER_SCOPE);
|
|
57
|
+
}
|
|
58
|
+
function parseFullBackupContent(root, warnings) {
|
|
59
|
+
const knownChildren = new Set(["include", "exclude"]);
|
|
60
|
+
const unsupportedChildren = root.children.filter((child) => !knownChildren.has(child.localName)).map((child) => `<${child.tagName}> at line ${child.location.line}`);
|
|
61
|
+
const rules = parseRuleEntries(root);
|
|
62
|
+
if (rules.length >= MAX_BACKUP_RULES_PER_SCOPE) {
|
|
63
|
+
warnings.push(`<full-backup-content> rule count reached the bounded maximum of ${MAX_BACKUP_RULES_PER_SCOPE}; further rules were not processed.`);
|
|
64
|
+
}
|
|
65
|
+
return { rules, unsupportedChildren };
|
|
66
|
+
}
|
|
67
|
+
function parseDataExtractionRuleSet(element) {
|
|
68
|
+
const disableResult = parseBooleanAttribute(element, "disableIfNoEncryptionCapabilities");
|
|
69
|
+
return {
|
|
70
|
+
disableIfNoEncryptionCapabilities: disableResult.value,
|
|
71
|
+
disableIfNoEncryptionCapabilitiesRaw: disableResult.raw,
|
|
72
|
+
rules: parseRuleEntries(element),
|
|
73
|
+
location: element.location,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function parseDataExtractionRules(root, warnings) {
|
|
77
|
+
const knownChildren = new Set(["cloud-backup", "device-transfer"]);
|
|
78
|
+
const unsupportedChildren = root.children.filter((child) => !knownChildren.has(child.localName)).map((child) => `<${child.tagName}> at line ${child.location.line}`);
|
|
79
|
+
const cloudBackupEl = findChildren(root, "cloud-backup")[0];
|
|
80
|
+
if (findChildren(root, "cloud-backup").length > 1) {
|
|
81
|
+
warnings.push("Multiple <cloud-backup> elements found; only the first was used.");
|
|
82
|
+
}
|
|
83
|
+
const deviceTransferEl = findChildren(root, "device-transfer")[0];
|
|
84
|
+
if (findChildren(root, "device-transfer").length > 1) {
|
|
85
|
+
warnings.push("Multiple <device-transfer> elements found; only the first was used.");
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
cloudBackup: cloudBackupEl ? parseDataExtractionRuleSet(cloudBackupEl) : undefined,
|
|
89
|
+
deviceTransfer: deviceTransferEl ? parseDataExtractionRuleSet(deviceTransferEl) : undefined,
|
|
90
|
+
unsupportedChildren,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// Parses either legacy full-backup-content or data-extraction-rules XML
|
|
94
|
+
// text, selecting the semantic model by the document's actual root element.
|
|
95
|
+
// Never throws for ordinary malformed external input.
|
|
96
|
+
export function parseBackupRules(xmlText) {
|
|
97
|
+
const parsed = parseXmlDocument(xmlText);
|
|
98
|
+
if (!parsed.ok) {
|
|
99
|
+
return { state: "malformed-xml", reason: parsed.error.message, location: parsed.error.location };
|
|
100
|
+
}
|
|
101
|
+
const root = parsed.root;
|
|
102
|
+
const warnings = parsed.warnings.map((w) => `${w.message} (line ${w.location.line})`);
|
|
103
|
+
if (root.localName === "full-backup-content") {
|
|
104
|
+
return { state: "parsed-full-backup-content", model: parseFullBackupContent(root, warnings), warnings };
|
|
105
|
+
}
|
|
106
|
+
if (root.localName === "data-extraction-rules") {
|
|
107
|
+
return { state: "parsed-data-extraction-rules", model: parseDataExtractionRules(root, warnings), warnings };
|
|
108
|
+
}
|
|
109
|
+
return { state: "unsupported-root", rootTagName: root.tagName };
|
|
110
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.1 Batch 3 — bounded backup/data-extraction XML parse-tree model.
|
|
3
|
+
//
|
|
4
|
+
// Covers both the legacy <full-backup-content> format (API <=30, still
|
|
5
|
+
// widely used) and the newer <data-extraction-rules> format (API 31+,
|
|
6
|
+
// separates <cloud-backup> from <device-transfer>). Local to Android
|
|
7
|
+
// advanced security, mirroring Batch 2's networkSecurity/types.ts pattern —
|
|
8
|
+
// not pushed into the generic XML tree model. Locations are line/column
|
|
9
|
+
// only; file/module/source-set identity is attached by the caller once a
|
|
10
|
+
// file is resolved (see manifestEvidence.ts).
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export const BACKUP_DOMAIN_VALUES = [
|
|
13
|
+
"root",
|
|
14
|
+
"file",
|
|
15
|
+
"database",
|
|
16
|
+
"sharedpref",
|
|
17
|
+
"external",
|
|
18
|
+
"device_root",
|
|
19
|
+
"device_file",
|
|
20
|
+
"device_database",
|
|
21
|
+
"device_sharedpref",
|
|
22
|
+
"device_external",
|
|
23
|
+
];
|
|
24
|
+
// Defensive bound on the number of rule entries processed from one file —
|
|
25
|
+
// mirrors Batch 2's MAX_DOMAIN_CONFIG_DEPTH bound philosophy (never let a
|
|
26
|
+
// pathological fixture/target cause unbounded work).
|
|
27
|
+
export const MAX_BACKUP_RULES_PER_SCOPE = 500;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { fingerprintCandidateValue, redactedPreviewForCandidate } from "./redaction.js";
|
|
2
|
+
// Resolution states for a static candidate's underlying evidence (e.g. did a
|
|
3
|
+
// referenced resource file resolve, was a signing config path found). Kept
|
|
4
|
+
// narrow and additive; overlaps deliberately with AndroidCheckStatus/XML
|
|
5
|
+
// resource-resolution states where the underlying concept is the same
|
|
6
|
+
// (resolved and missing/malformed/unsupported are shared vocabulary), but
|
|
7
|
+
// this is not a duplicate of AndroidCheckStatus — a candidate's resolution
|
|
8
|
+
// state describes one piece of referenced evidence, not an entire check.
|
|
9
|
+
export const CANDIDATE_RESOLUTION_STATES = ["resolved", "unresolved", "missing", "malformed", "unsupported", "not-applicable"];
|
|
10
|
+
function slug(value) {
|
|
11
|
+
return value
|
|
12
|
+
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
13
|
+
.replace(/^-+|-+$/g, "")
|
|
14
|
+
.toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
// Deterministic, content-derived id (no counter, no timestamp) so equivalent
|
|
17
|
+
// candidate evidence always produces the same id across repeated runs.
|
|
18
|
+
function buildCandidateEvidenceId(ruleId, location, fingerprint) {
|
|
19
|
+
const locationPart = [location.path, location.line !== undefined ? `l${location.line}` : undefined, location.column !== undefined ? `c${location.column}` : undefined]
|
|
20
|
+
.filter((part) => Boolean(part))
|
|
21
|
+
.join(":");
|
|
22
|
+
return [ruleId, slug(locationPart), fingerprint.slice(0, 16)].join("--");
|
|
23
|
+
}
|
|
24
|
+
// Never accepts a raw secret value as a plain string parameter name to
|
|
25
|
+
// discourage accidental logging at call sites — `rawValue` is redacted and
|
|
26
|
+
// fingerprinted here, once, and the raw value is never stored on the
|
|
27
|
+
// returned object.
|
|
28
|
+
export function makeCandidateEvidence(input) {
|
|
29
|
+
const fingerprint = fingerprintCandidateValue(input.rawValue);
|
|
30
|
+
const redactedPreview = redactedPreviewForCandidate(input.rawValue);
|
|
31
|
+
const id = buildCandidateEvidenceId(input.ruleId, input.location, fingerprint);
|
|
32
|
+
return {
|
|
33
|
+
id,
|
|
34
|
+
ruleId: input.ruleId,
|
|
35
|
+
category: input.category,
|
|
36
|
+
sensitiveDataCategory: input.sensitiveDataCategory,
|
|
37
|
+
confidence: input.confidence,
|
|
38
|
+
modulePath: input.modulePath,
|
|
39
|
+
location: input.location,
|
|
40
|
+
summary: input.summary,
|
|
41
|
+
redactedPreview,
|
|
42
|
+
fingerprint,
|
|
43
|
+
resolutionState: input.resolutionState,
|
|
44
|
+
staticAnalysisLimitations: input.staticAnalysisLimitations ?? [],
|
|
45
|
+
relatedManifestComponentIds: input.relatedManifestComponentIds,
|
|
46
|
+
relatedResourceReferences: input.relatedResourceReferences,
|
|
47
|
+
relatedFindingIds: input.relatedFindingIds,
|
|
48
|
+
relatedCheckIds: input.relatedCheckIds,
|
|
49
|
+
};
|
|
50
|
+
}
|