@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,394 @@
|
|
|
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 { extractManifestNetworkSecurityEvidence } from "./manifestEvidence.js";
|
|
6
|
+
import { deriveEffectiveNetworkPolicy } from "./deriveEffectiveNetworkPolicy.js";
|
|
7
|
+
function loc(targetRoot, absolutePath, position) {
|
|
8
|
+
return buildAndroidSourceLocation(targetRoot, absolutePath, position);
|
|
9
|
+
}
|
|
10
|
+
function analyzeScopeCleartext(scope, sourceRelativePath) {
|
|
11
|
+
if (scope.cleartextTrafficPermitted !== true)
|
|
12
|
+
return undefined;
|
|
13
|
+
const isBase = scope.kind === "base";
|
|
14
|
+
const domainsText = scope.domains.map((d) => d.normalized).filter((d) => d.length > 0).join(", ");
|
|
15
|
+
return makeAndroidFinding({
|
|
16
|
+
ruleId: "android-network-cleartext-traffic",
|
|
17
|
+
title: isBase
|
|
18
|
+
? "Network Security Config base configuration permits cleartext traffic"
|
|
19
|
+
: `Network Security Config domain configuration permits cleartext traffic${domainsText ? ` (${domainsText})` : ""}`,
|
|
20
|
+
severity: isBase ? "major" : "minor",
|
|
21
|
+
confidence: "high",
|
|
22
|
+
description: isBase
|
|
23
|
+
? "The Network Security Config's <base-config> sets cleartextTrafficPermitted=\"true\", which is high-confidence static evidence that cleartext (unencrypted) HTTP traffic is permitted application-wide unless narrowed by a more specific domain-config. This does not prove cleartext traffic actually occurs at runtime."
|
|
24
|
+
: `A <domain-config> sets cleartextTrafficPermitted="true" for the scoped domain(s) above. This is high-confidence static evidence limited to that domain scope${scope.domains.some((d) => d.includeSubdomains) ? " (including subdomains for at least one entry)" : ""}, not proof that cleartext traffic actually occurs at runtime.`,
|
|
25
|
+
manifestPath: sourceRelativePath,
|
|
26
|
+
identity: scope.scopeId,
|
|
27
|
+
location: { line: scope.location.line, column: scope.location.column },
|
|
28
|
+
evidenceDetails: [
|
|
29
|
+
`scope=${scope.kind}`,
|
|
30
|
+
`cleartextTrafficSource=${scope.cleartextTrafficSource}`,
|
|
31
|
+
...(isBase ? [] : [`domains=${domainsText || "(none declared; scope has no resolvable domain list)"}`]),
|
|
32
|
+
],
|
|
33
|
+
recommendation: "Disable cleartext traffic unless a specific, documented use case requires it, and scope any exception as narrowly as possible.",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function analyzeScopeTrustAnchors(scope, sourceAbsolutePath, targetRoot, modulePath, sourceRelativePath) {
|
|
37
|
+
const candidates = [];
|
|
38
|
+
const findings = [];
|
|
39
|
+
if (scope.trustAnchorsChain.length === 0)
|
|
40
|
+
return { candidates, findings };
|
|
41
|
+
// The nearest entry in the chain is the one that actually governs this
|
|
42
|
+
// scope (own, or the closest ancestor it inherited from) — not every
|
|
43
|
+
// entry in the chain, which would double-count inherited certificates
|
|
44
|
+
// once per descendant scope.
|
|
45
|
+
const nearest = scope.trustAnchorsChain[0];
|
|
46
|
+
for (const cert of nearest.certificates) {
|
|
47
|
+
const location = loc(targetRoot, sourceAbsolutePath, { line: cert.location.line, column: cert.location.column });
|
|
48
|
+
if (cert.sourceKind === "user") {
|
|
49
|
+
findings.push(makeAndroidFinding({
|
|
50
|
+
ruleId: "android-network-user-added-trust-anchor",
|
|
51
|
+
title: `${scope.kind === "base" ? "Base" : "Domain-scoped"} configuration trusts user-added certificate authorities`,
|
|
52
|
+
severity: scope.kind === "base" ? "major" : "minor",
|
|
53
|
+
confidence: "high",
|
|
54
|
+
description: "The configuration includes <certificates src=\"user\">, meaning certificates a device user has added to the trust store are trusted for TLS connections in this scope. This is high-confidence static evidence of the trust configuration itself, not proof of an active interception (e.g. MITM) attack.",
|
|
55
|
+
manifestPath: sourceRelativePath,
|
|
56
|
+
identity: scope.scopeId,
|
|
57
|
+
location: { line: cert.location.line, column: cert.location.column },
|
|
58
|
+
evidenceDetails: [`scope=${scope.kind}`, `certificatesSrc=user`, cert.overridePinsRaw !== undefined ? `overridePins=${cert.overridePinsRaw}` : undefined].filter((v) => Boolean(v)),
|
|
59
|
+
recommendation: "Avoid trusting user-added CAs in release configuration unless a specific, reviewed use case (e.g. enterprise proxy/MDM) requires it.",
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
else if (cert.sourceKind === "raw-resource") {
|
|
63
|
+
candidates.push(makeCandidateEvidence({
|
|
64
|
+
ruleId: "android-network-user-added-trust-anchor",
|
|
65
|
+
category: "android-network-security",
|
|
66
|
+
confidence: "medium",
|
|
67
|
+
modulePath,
|
|
68
|
+
location,
|
|
69
|
+
summary: `Custom raw certificate authority referenced (@raw/${cert.rawResourceName ?? "?"}) in ${scope.kind} configuration`,
|
|
70
|
+
rawValue: cert.rawResourceName,
|
|
71
|
+
resolutionState: "resolved",
|
|
72
|
+
staticAnalysisLimitations: ["Certificate contents were not inspected or validated; a custom CA is not automatically unsafe."],
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
else if (cert.sourceKind === "unknown") {
|
|
76
|
+
candidates.push(makeCandidateEvidence({
|
|
77
|
+
ruleId: "android-network-user-added-trust-anchor",
|
|
78
|
+
category: "android-network-security",
|
|
79
|
+
confidence: "low",
|
|
80
|
+
modulePath,
|
|
81
|
+
location,
|
|
82
|
+
summary: `Unrecognized <certificates src="..."> value in ${scope.kind} configuration`,
|
|
83
|
+
rawValue: cert.srcRaw,
|
|
84
|
+
resolutionState: "unsupported",
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
// "system" is normal evidence, not a finding or candidate by itself.
|
|
88
|
+
}
|
|
89
|
+
return { candidates, findings };
|
|
90
|
+
}
|
|
91
|
+
function analyzeDebugOverrides(debugTrustAnchors, sourceAbsolutePath, targetRoot, modulePath) {
|
|
92
|
+
if (!debugTrustAnchors)
|
|
93
|
+
return [];
|
|
94
|
+
return debugTrustAnchors.certificates.map((cert) => makeCandidateEvidence({
|
|
95
|
+
ruleId: "android-network-debug-trust-override",
|
|
96
|
+
category: "android-network-security",
|
|
97
|
+
confidence: "low",
|
|
98
|
+
modulePath,
|
|
99
|
+
location: loc(targetRoot, sourceAbsolutePath, { line: cert.location.line, column: cert.location.column }),
|
|
100
|
+
summary: `Debug-only trust override: <certificates src="${cert.srcRaw ?? "unknown"}">`,
|
|
101
|
+
rawValue: cert.srcRaw,
|
|
102
|
+
resolutionState: "resolved",
|
|
103
|
+
staticAnalysisLimitations: [
|
|
104
|
+
"<debug-overrides> only applies to builds with android:debuggable=\"true\" and must never be treated as active release-build policy.",
|
|
105
|
+
],
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
const BROAD_DOMAIN_MAX_SINGLE_LABEL_LENGTH = 3;
|
|
109
|
+
function analyzeDomainBroadness(scope, sourceAbsolutePath, targetRoot, modulePath) {
|
|
110
|
+
const candidates = [];
|
|
111
|
+
for (const domain of scope.domains) {
|
|
112
|
+
const location = loc(targetRoot, sourceAbsolutePath, { line: domain.location.line, column: domain.location.column });
|
|
113
|
+
if (domain.malformed) {
|
|
114
|
+
candidates.push(makeCandidateEvidence({
|
|
115
|
+
ruleId: "android-network-broad-domain-config",
|
|
116
|
+
category: "android-network-security",
|
|
117
|
+
confidence: "low",
|
|
118
|
+
modulePath,
|
|
119
|
+
location,
|
|
120
|
+
summary: "Empty or malformed <domain> entry",
|
|
121
|
+
rawValue: domain.textRaw,
|
|
122
|
+
resolutionState: "malformed",
|
|
123
|
+
}));
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const isWildcardLike = domain.normalized.includes("*");
|
|
127
|
+
// A conservative "narrow" broadness signal only: a bare, very short
|
|
128
|
+
// single-label host (no dot at all) is unusual for a real public/
|
|
129
|
+
// internal domain and worth a low-confidence review candidate. Ordinary
|
|
130
|
+
// multi-label domains (including internal-looking ones) never match —
|
|
131
|
+
// per section 9.8, ambiguous/potentially-legitimate internal domains
|
|
132
|
+
// must remain unflagged rather than produce false positives.
|
|
133
|
+
const isSuspiciouslyShortSingleLabel = !domain.normalized.includes(".") && domain.normalized.length <= BROAD_DOMAIN_MAX_SINGLE_LABEL_LENGTH;
|
|
134
|
+
if (isWildcardLike || isSuspiciouslyShortSingleLabel) {
|
|
135
|
+
candidates.push(makeCandidateEvidence({
|
|
136
|
+
ruleId: "android-network-broad-domain-config",
|
|
137
|
+
category: "android-network-security",
|
|
138
|
+
confidence: "low",
|
|
139
|
+
modulePath,
|
|
140
|
+
location,
|
|
141
|
+
summary: `Potentially broad domain candidate: "${domain.normalized}"${domain.includeSubdomains ? " (includeSubdomains)" : ""}`,
|
|
142
|
+
rawValue: domain.textRaw,
|
|
143
|
+
resolutionState: "resolved",
|
|
144
|
+
staticAnalysisLimitations: [
|
|
145
|
+
"Domain registrability/ownership was not verified (no network or public-suffix-list access); this may be a legitimate internal or short hostname.",
|
|
146
|
+
],
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return candidates;
|
|
151
|
+
}
|
|
152
|
+
function analyzePinSet(pinSet, sourceAbsolutePath, targetRoot, modulePath) {
|
|
153
|
+
if (!pinSet)
|
|
154
|
+
return [];
|
|
155
|
+
const candidates = [];
|
|
156
|
+
candidates.push(makeCandidateEvidence({
|
|
157
|
+
ruleId: "android-network-pinning-metadata",
|
|
158
|
+
category: "android-network-security",
|
|
159
|
+
confidence: "high",
|
|
160
|
+
modulePath,
|
|
161
|
+
location: loc(targetRoot, sourceAbsolutePath, { line: pinSet.location.line, column: pinSet.location.column }),
|
|
162
|
+
summary: `Certificate pin-set present (${pinSet.pins.length} pin(s)${pinSet.expirationRaw ? `, expiration=${pinSet.expirationRaw}` : ", no expiration set"})`,
|
|
163
|
+
rawValue: pinSet.expirationRaw,
|
|
164
|
+
resolutionState: pinSet.expirationMalformed ? "malformed" : "resolved",
|
|
165
|
+
staticAnalysisLimitations: [
|
|
166
|
+
"Pin values were not validated against any live certificate or TLS connection; presence of pin metadata does not prove pinning is effective at runtime, and its absence is not automatically a vulnerability.",
|
|
167
|
+
...(pinSet.expirationMalformed ? ["expiration attribute is not in the expected YYYY-MM-DD format."] : []),
|
|
168
|
+
],
|
|
169
|
+
}));
|
|
170
|
+
for (const pin of pinSet.pins) {
|
|
171
|
+
if (pin.malformed || !pin.digestSupported) {
|
|
172
|
+
candidates.push(makeCandidateEvidence({
|
|
173
|
+
ruleId: "android-network-pinning-unresolved",
|
|
174
|
+
category: "android-network-security",
|
|
175
|
+
confidence: "medium",
|
|
176
|
+
modulePath,
|
|
177
|
+
location: loc(targetRoot, sourceAbsolutePath, { line: pin.location.line, column: pin.location.column }),
|
|
178
|
+
summary: pin.malformed ? `Malformed pin entry: ${pin.malformedReason}` : `Unsupported pin digest algorithm: "${pin.digestRaw}"`,
|
|
179
|
+
rawValue: pin.valuePreview,
|
|
180
|
+
resolutionState: pin.malformed ? "malformed" : "unsupported",
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return candidates;
|
|
185
|
+
}
|
|
186
|
+
function analyzeParsedFile(parseResult, fileRelativePath, targetRoot, modulePath) {
|
|
187
|
+
const fileAbsolutePath = path.join(targetRoot, fileRelativePath);
|
|
188
|
+
const policy = deriveEffectiveNetworkPolicy(parseResult);
|
|
189
|
+
const candidates = [];
|
|
190
|
+
const findings = [];
|
|
191
|
+
const scopes = [...(policy.baseScope ? [policy.baseScope] : []), ...policy.domainScopes];
|
|
192
|
+
for (const scope of scopes) {
|
|
193
|
+
const cleartextFinding = analyzeScopeCleartext(scope, fileRelativePath);
|
|
194
|
+
if (cleartextFinding)
|
|
195
|
+
findings.push(cleartextFinding);
|
|
196
|
+
const trustResult = analyzeScopeTrustAnchors(scope, fileAbsolutePath, targetRoot, modulePath, fileRelativePath);
|
|
197
|
+
candidates.push(...trustResult.candidates);
|
|
198
|
+
findings.push(...trustResult.findings);
|
|
199
|
+
candidates.push(...analyzeDomainBroadness(scope, fileAbsolutePath, targetRoot, modulePath));
|
|
200
|
+
candidates.push(...analyzePinSet(scope.pinSet, fileAbsolutePath, targetRoot, modulePath));
|
|
201
|
+
}
|
|
202
|
+
candidates.push(...analyzeDebugOverrides(policy.debugTrustAnchors, fileAbsolutePath, targetRoot, modulePath));
|
|
203
|
+
return { candidates, findings, baseCleartextExplicitFalse: policy.baseScope?.cleartextTrafficPermitted === false };
|
|
204
|
+
}
|
|
205
|
+
function analyzeResolvedOrAmbiguousFile(fileRelativePath, parseResult, targetRoot, modulePath) {
|
|
206
|
+
const fileAbsolutePath = path.join(targetRoot, fileRelativePath);
|
|
207
|
+
if (parseResult.state === "malformed-xml") {
|
|
208
|
+
return {
|
|
209
|
+
candidates: [
|
|
210
|
+
makeCandidateEvidence({
|
|
211
|
+
ruleId: "android-network-security-config",
|
|
212
|
+
category: "android-network-security",
|
|
213
|
+
confidence: "medium",
|
|
214
|
+
modulePath,
|
|
215
|
+
location: loc(targetRoot, fileAbsolutePath, parseResult.location ? { line: parseResult.location.line, column: parseResult.location.column } : undefined),
|
|
216
|
+
summary: `Referenced Network Security Config is malformed XML: ${parseResult.reason}`,
|
|
217
|
+
rawValue: parseResult.reason,
|
|
218
|
+
resolutionState: "malformed",
|
|
219
|
+
}),
|
|
220
|
+
],
|
|
221
|
+
findings: [],
|
|
222
|
+
baseCleartextExplicitFalse: false,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (parseResult.state === "unsupported-root") {
|
|
226
|
+
return {
|
|
227
|
+
candidates: [
|
|
228
|
+
makeCandidateEvidence({
|
|
229
|
+
ruleId: "android-network-security-config",
|
|
230
|
+
category: "android-network-security",
|
|
231
|
+
confidence: "medium",
|
|
232
|
+
modulePath,
|
|
233
|
+
location: loc(targetRoot, fileAbsolutePath),
|
|
234
|
+
summary: `Referenced XML file has an unsupported root element <${parseResult.rootTagName}>, not <network-security-config>`,
|
|
235
|
+
rawValue: parseResult.rootTagName,
|
|
236
|
+
resolutionState: "unsupported",
|
|
237
|
+
}),
|
|
238
|
+
],
|
|
239
|
+
findings: [],
|
|
240
|
+
baseCleartextExplicitFalse: false,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return analyzeParsedFile(parseResult, fileRelativePath, targetRoot, modulePath);
|
|
244
|
+
}
|
|
245
|
+
// Analyzes one already-parsed manifest's cleartext/Network Security Config
|
|
246
|
+
// evidence, without touching validateAndroidTarget or any active
|
|
247
|
+
// orchestration (this function is standalone until a later integration
|
|
248
|
+
// batch).
|
|
249
|
+
export function analyzeManifestNetworkSecurity(targetRoot, entry) {
|
|
250
|
+
const evidence = extractManifestNetworkSecurityEvidence(targetRoot, entry);
|
|
251
|
+
const candidates = [];
|
|
252
|
+
const findings = [];
|
|
253
|
+
const modulePath = entry.modulePath;
|
|
254
|
+
const manifestAbsolutePath = path.join(targetRoot, entry.manifestPath);
|
|
255
|
+
const nsc = evidence.networkSecurityConfig;
|
|
256
|
+
let anyResolvedNsc = false;
|
|
257
|
+
let anyResolvedBaseExplicitFalse = false;
|
|
258
|
+
if (nsc.state === "resolved") {
|
|
259
|
+
anyResolvedNsc = true;
|
|
260
|
+
const result = analyzeResolvedOrAmbiguousFile(nsc.candidate.relativePath, nsc.parseResult, targetRoot, modulePath);
|
|
261
|
+
candidates.push(...result.candidates);
|
|
262
|
+
findings.push(...result.findings);
|
|
263
|
+
anyResolvedBaseExplicitFalse = result.baseCleartextExplicitFalse;
|
|
264
|
+
}
|
|
265
|
+
else if (nsc.state === "ambiguous") {
|
|
266
|
+
anyResolvedNsc = true;
|
|
267
|
+
candidates.push(makeCandidateEvidence({
|
|
268
|
+
ruleId: "android-network-security-config",
|
|
269
|
+
category: "android-network-security",
|
|
270
|
+
confidence: "low",
|
|
271
|
+
modulePath,
|
|
272
|
+
location: loc(targetRoot, manifestAbsolutePath, evidence.applicationLocation),
|
|
273
|
+
summary: `networkSecurityConfig reference "${nsc.raw}" resolved to ${nsc.candidates.length} ambiguous candidates across source sets; no candidate was arbitrarily selected`,
|
|
274
|
+
rawValue: nsc.raw,
|
|
275
|
+
resolutionState: "unresolved",
|
|
276
|
+
staticAnalysisLimitations: ["Android resource-overlay precedence between the candidates was not evaluated."],
|
|
277
|
+
}));
|
|
278
|
+
let anyBaseFalse = false;
|
|
279
|
+
for (const candidate of nsc.candidates) {
|
|
280
|
+
const result = analyzeResolvedOrAmbiguousFile(candidate.candidate.relativePath, candidate.parseResult, targetRoot, modulePath);
|
|
281
|
+
candidates.push(...result.candidates);
|
|
282
|
+
findings.push(...result.findings);
|
|
283
|
+
anyBaseFalse = anyBaseFalse || result.baseCleartextExplicitFalse;
|
|
284
|
+
}
|
|
285
|
+
anyResolvedBaseExplicitFalse = anyBaseFalse;
|
|
286
|
+
}
|
|
287
|
+
else if (nsc.state === "missing") {
|
|
288
|
+
candidates.push(makeCandidateEvidence({
|
|
289
|
+
ruleId: "android-network-security-config",
|
|
290
|
+
category: "android-network-security",
|
|
291
|
+
confidence: "low",
|
|
292
|
+
modulePath,
|
|
293
|
+
location: loc(targetRoot, manifestAbsolutePath, evidence.applicationLocation),
|
|
294
|
+
summary: `networkSecurityConfig reference "${nsc.raw}" did not resolve to any file in the searched source sets`,
|
|
295
|
+
rawValue: nsc.raw,
|
|
296
|
+
resolutionState: "missing",
|
|
297
|
+
staticAnalysisLimitations: ["A missing referenced configuration is not automatically a vulnerability; runtime behavior in this case cannot be proven statically."],
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
else if (nsc.state === "placeholder") {
|
|
301
|
+
candidates.push(makeCandidateEvidence({
|
|
302
|
+
ruleId: "android-network-security-config",
|
|
303
|
+
category: "android-network-security",
|
|
304
|
+
confidence: "low",
|
|
305
|
+
modulePath,
|
|
306
|
+
location: loc(targetRoot, manifestAbsolutePath, evidence.applicationLocation),
|
|
307
|
+
summary: "networkSecurityConfig is an unresolved build-time placeholder",
|
|
308
|
+
rawValue: nsc.raw,
|
|
309
|
+
resolutionState: "unresolved",
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
312
|
+
else if (nsc.state === "malformed") {
|
|
313
|
+
candidates.push(makeCandidateEvidence({
|
|
314
|
+
ruleId: "android-network-security-config",
|
|
315
|
+
category: "android-network-security",
|
|
316
|
+
confidence: "low",
|
|
317
|
+
modulePath,
|
|
318
|
+
location: loc(targetRoot, manifestAbsolutePath, evidence.applicationLocation),
|
|
319
|
+
summary: `networkSecurityConfig reference is malformed: ${nsc.reason}`,
|
|
320
|
+
rawValue: nsc.raw,
|
|
321
|
+
resolutionState: "malformed",
|
|
322
|
+
}));
|
|
323
|
+
}
|
|
324
|
+
else if (nsc.state === "unsupported-type" || nsc.state === "package-qualified" || nsc.state === "module-unknown") {
|
|
325
|
+
candidates.push(makeCandidateEvidence({
|
|
326
|
+
ruleId: "android-network-security-config",
|
|
327
|
+
category: "android-network-security",
|
|
328
|
+
confidence: "low",
|
|
329
|
+
modulePath,
|
|
330
|
+
location: loc(targetRoot, manifestAbsolutePath, evidence.applicationLocation),
|
|
331
|
+
summary: nsc.state === "module-unknown"
|
|
332
|
+
? "networkSecurityConfig reference could not be resolved because this manifest's module could not be determined"
|
|
333
|
+
: `networkSecurityConfig reference is not a resolvable target-contained @xml/... resource (${nsc.state})`,
|
|
334
|
+
rawValue: nsc.raw,
|
|
335
|
+
resolutionState: "unsupported",
|
|
336
|
+
}));
|
|
337
|
+
}
|
|
338
|
+
if (evidence.usesCleartextTraffic.state === "malformed") {
|
|
339
|
+
candidates.push(makeCandidateEvidence({
|
|
340
|
+
ruleId: "android-network-cleartext-traffic",
|
|
341
|
+
category: "android-network-security",
|
|
342
|
+
confidence: "low",
|
|
343
|
+
modulePath,
|
|
344
|
+
location: loc(targetRoot, manifestAbsolutePath, evidence.applicationLocation),
|
|
345
|
+
summary: "android:usesCleartextTraffic has an unresolved or malformed value",
|
|
346
|
+
rawValue: evidence.usesCleartextTraffic.raw,
|
|
347
|
+
resolutionState: "malformed",
|
|
348
|
+
}));
|
|
349
|
+
}
|
|
350
|
+
else if (evidence.usesCleartextTraffic.state === "explicit-true") {
|
|
351
|
+
if (!anyResolvedNsc) {
|
|
352
|
+
findings.push(makeAndroidFinding({
|
|
353
|
+
ruleId: "android-network-cleartext-traffic",
|
|
354
|
+
title: "Manifest explicitly permits cleartext traffic (android:usesCleartextTraffic=\"true\")",
|
|
355
|
+
severity: "major",
|
|
356
|
+
confidence: "high",
|
|
357
|
+
description: "The manifest's <application> element sets android:usesCleartextTraffic=\"true\" with no resolvable Network Security Config to narrow it. This is high-confidence static evidence that cleartext (unencrypted) HTTP traffic is permitted application-wide; it is not proof that cleartext traffic actually occurs at runtime.",
|
|
358
|
+
manifestPath: entry.manifestPath,
|
|
359
|
+
location: evidence.applicationLocation,
|
|
360
|
+
evidenceDetails: ["source=manifest-attribute", "networkSecurityConfig=absent-or-unresolved"],
|
|
361
|
+
recommendation: "Set android:usesCleartextTraffic=\"false\" unless a specific, documented use case requires cleartext traffic.",
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
else if (anyResolvedBaseExplicitFalse) {
|
|
365
|
+
candidates.push(makeCandidateEvidence({
|
|
366
|
+
ruleId: "android-network-cleartext-traffic",
|
|
367
|
+
category: "android-network-security",
|
|
368
|
+
confidence: "medium",
|
|
369
|
+
modulePath,
|
|
370
|
+
location: loc(targetRoot, manifestAbsolutePath, evidence.applicationLocation),
|
|
371
|
+
summary: "android:usesCleartextTraffic=\"true\" conflicts with the resolved Network Security Config's <base-config cleartextTrafficPermitted=\"false\">",
|
|
372
|
+
rawValue: evidence.usesCleartextTraffic.raw,
|
|
373
|
+
resolutionState: "resolved",
|
|
374
|
+
staticAnalysisLimitations: [
|
|
375
|
+
"Precedence between the manifest attribute and the Network Security Config was not evaluated for a specific Android API level; this is conflicting static configuration, not a confirmed runtime state.",
|
|
376
|
+
],
|
|
377
|
+
}));
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
findings.push(makeAndroidFinding({
|
|
381
|
+
ruleId: "android-network-cleartext-traffic",
|
|
382
|
+
title: "Manifest explicitly permits cleartext traffic (android:usesCleartextTraffic=\"true\")",
|
|
383
|
+
severity: "major",
|
|
384
|
+
confidence: "high",
|
|
385
|
+
description: "The manifest's <application> element sets android:usesCleartextTraffic=\"true\". A Network Security Config is present but does not explicitly set cleartextTrafficPermitted=\"false\" at the base level, so this attribute's effect is not contradicted by static evidence.",
|
|
386
|
+
manifestPath: entry.manifestPath,
|
|
387
|
+
location: evidence.applicationLocation,
|
|
388
|
+
evidenceDetails: ["source=manifest-attribute", "networkSecurityConfig=resolved-non-contradicting"],
|
|
389
|
+
recommendation: "Set android:usesCleartextTraffic=\"false\" unless a specific, documented use case requires cleartext traffic.",
|
|
390
|
+
}));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return { evidence, candidates, findings };
|
|
394
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { buildAndroidManifestCheckResult } from "../../audit/checkResultBuilder.js";
|
|
2
|
+
import { sortCandidateEvidence } from "../ordering.js";
|
|
3
|
+
import { analyzeManifestNetworkSecurity } from "./analyzeNetworkSecurity.js";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// v0.4.1 Batch 2 — standalone Android network-security check result.
|
|
6
|
+
//
|
|
7
|
+
// Reuses the existing manifest-audit status-decision policy
|
|
8
|
+
// (buildAndroidManifestCheckResult, src/mobile/android/audit/
|
|
9
|
+
// checkResultBuilder.ts) rather than inventing a second one — this check
|
|
10
|
+
// operates over the same AndroidManifestParseEntry[] shape as the four
|
|
11
|
+
// v0.4.0 manifest audits.
|
|
12
|
+
//
|
|
13
|
+
// Deliberately standalone: this function is exercised directly by tests but
|
|
14
|
+
// is NOT called from validateAndroidTarget / any active orchestration, not
|
|
15
|
+
// rendered in text/JSON reports, and does not affect the CLI. A later
|
|
16
|
+
// integration batch (Batch 8 per the version plan) wires it in.
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
export const ANDROID_NETWORK_SECURITY_AUDIT_CHECK_ID = "android-network-security-audit";
|
|
19
|
+
export function auditAndroidNetworkSecurity(targetRoot, detection, manifests) {
|
|
20
|
+
const findings = [];
|
|
21
|
+
const candidateEvidence = [];
|
|
22
|
+
const evidence = [];
|
|
23
|
+
const warnings = [];
|
|
24
|
+
for (const entry of manifests) {
|
|
25
|
+
const result = analyzeManifestNetworkSecurity(targetRoot, entry);
|
|
26
|
+
findings.push(...result.findings);
|
|
27
|
+
candidateEvidence.push(...result.candidates);
|
|
28
|
+
evidence.push(`${entry.manifestPath}: usesCleartextTraffic=${result.evidence.usesCleartextTraffic.state}, networkSecurityConfig=${result.evidence.networkSecurityConfig.state}`);
|
|
29
|
+
}
|
|
30
|
+
const base = buildAndroidManifestCheckResult({
|
|
31
|
+
id: ANDROID_NETWORK_SECURITY_AUDIT_CHECK_ID,
|
|
32
|
+
category: "android-network-security",
|
|
33
|
+
title: "Android cleartext traffic and Network Security Config audit",
|
|
34
|
+
detection,
|
|
35
|
+
manifests,
|
|
36
|
+
findings,
|
|
37
|
+
evidence,
|
|
38
|
+
warnings: [
|
|
39
|
+
...warnings,
|
|
40
|
+
"Static analysis only: does not evaluate Gradle manifest merging, Android resource overlays, or runtime network/TLS behavior.",
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
...base,
|
|
45
|
+
candidateEvidence: sortCandidateEvidence(candidateEvidence),
|
|
46
|
+
};
|
|
47
|
+
}
|
package/dist/src/mobile/android/advancedSecurity/networkSecurity/deriveEffectiveNetworkPolicy.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { MAX_DOMAIN_CONFIG_DEPTH } from "./types.js";
|
|
2
|
+
function walkDomainConfig(node, scopeId, depth, inheritedDomains, inheritedTrustAnchorsChain, baseCleartextTrafficPermitted, out) {
|
|
3
|
+
if (depth > MAX_DOMAIN_CONFIG_DEPTH)
|
|
4
|
+
return;
|
|
5
|
+
const domains = node.domains.length > 0 ? node.domains : inheritedDomains;
|
|
6
|
+
const domainsInherited = node.domains.length === 0 && inheritedDomains.length > 0;
|
|
7
|
+
const cleartextTrafficPermitted = node.cleartextTrafficPermitted ?? baseCleartextTrafficPermitted;
|
|
8
|
+
const cleartextTrafficSource = node.cleartextTrafficPermitted !== undefined ? "explicit" : baseCleartextTrafficPermitted !== undefined ? "inherited-from-base" : "unspecified";
|
|
9
|
+
const trustAnchorsChain = node.trustAnchors ? [node.trustAnchors, ...inheritedTrustAnchorsChain] : inheritedTrustAnchorsChain;
|
|
10
|
+
const trustAnchorsSource = node.trustAnchors ? "own" : trustAnchorsChain.length > 0 ? "inherited" : "unspecified";
|
|
11
|
+
out.push({
|
|
12
|
+
scopeId,
|
|
13
|
+
kind: "domain",
|
|
14
|
+
depth,
|
|
15
|
+
domains,
|
|
16
|
+
domainsInherited,
|
|
17
|
+
cleartextTrafficPermitted,
|
|
18
|
+
cleartextTrafficSource,
|
|
19
|
+
trustAnchorsChain,
|
|
20
|
+
trustAnchorsSource,
|
|
21
|
+
pinSet: node.pinSet,
|
|
22
|
+
location: node.location,
|
|
23
|
+
});
|
|
24
|
+
node.nested.forEach((child, index) => {
|
|
25
|
+
walkDomainConfig(child, `${scopeId}>domain-config[${index}]`, depth + 1, domains, trustAnchorsChain, baseCleartextTrafficPermitted, out);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
// Derives deterministic effective policy from a "parsed" Network Security
|
|
29
|
+
// Config result. Callers must narrow to the "parsed" state first — a
|
|
30
|
+
// malformed/unsupported-root parse result has no policy to derive.
|
|
31
|
+
export function deriveEffectiveNetworkPolicy(parsed) {
|
|
32
|
+
const baseScope = parsed.baseConfig
|
|
33
|
+
? {
|
|
34
|
+
scopeId: "base-config",
|
|
35
|
+
kind: "base",
|
|
36
|
+
depth: 0,
|
|
37
|
+
domains: [],
|
|
38
|
+
domainsInherited: false,
|
|
39
|
+
cleartextTrafficPermitted: parsed.baseConfig.cleartextTrafficPermitted,
|
|
40
|
+
cleartextTrafficSource: parsed.baseConfig.cleartextTrafficPermitted !== undefined ? "explicit" : "unspecified",
|
|
41
|
+
trustAnchorsChain: parsed.baseConfig.trustAnchors ? [parsed.baseConfig.trustAnchors] : [],
|
|
42
|
+
trustAnchorsSource: parsed.baseConfig.trustAnchors ? "own" : "unspecified",
|
|
43
|
+
pinSet: parsed.baseConfig.pinSet,
|
|
44
|
+
location: parsed.baseConfig.location,
|
|
45
|
+
}
|
|
46
|
+
: undefined;
|
|
47
|
+
const domainScopes = [];
|
|
48
|
+
parsed.domainConfigs.forEach((node, index) => {
|
|
49
|
+
walkDomainConfig(node, `domain-config[${index}]`, 0, [], baseScope?.trustAnchorsChain ?? [], baseScope?.cleartextTrafficPermitted, domainScopes);
|
|
50
|
+
});
|
|
51
|
+
return { baseScope, domainScopes, debugTrustAnchors: parsed.debugOverrides?.trustAnchors };
|
|
52
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.1 Batch 2 — public exports for the standalone Android network-
|
|
3
|
+
// security (cleartext traffic / Network Security Config) audit.
|
|
4
|
+
//
|
|
5
|
+
// Exports only what a later integration batch (or a test) needs: the parser,
|
|
6
|
+
// the effective-policy deriver, the analyzer, and the standalone check-result
|
|
7
|
+
// builder. Internal helpers (attribute parsing, bounded previews, per-scope
|
|
8
|
+
// finding builders) stay unexported.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
export { parseNetworkSecurityConfig } from "./parseNetworkSecurityConfig.js";
|
|
11
|
+
export { deriveEffectiveNetworkPolicy } from "./deriveEffectiveNetworkPolicy.js";
|
|
12
|
+
export { extractManifestNetworkSecurityEvidence } from "./manifestEvidence.js";
|
|
13
|
+
export { analyzeManifestNetworkSecurity } from "./analyzeNetworkSecurity.js";
|
|
14
|
+
export { auditAndroidNetworkSecurity, ANDROID_NETWORK_SECURITY_AUDIT_CHECK_ID } from "./checkResult.js";
|
|
@@ -0,0 +1,109 @@
|
|
|
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 { parseNetworkSecurityConfig } from "./parseNetworkSecurityConfig.js";
|
|
6
|
+
function classifyUsesCleartextTraffic(entry) {
|
|
7
|
+
const app = entry.manifest.application;
|
|
8
|
+
if (app.usesCleartextTraffic === true)
|
|
9
|
+
return { state: "explicit-true", raw: app.usesCleartextTrafficRaw ?? "true" };
|
|
10
|
+
if (app.usesCleartextTraffic === false)
|
|
11
|
+
return { state: "explicit-false", raw: app.usesCleartextTrafficRaw ?? "false" };
|
|
12
|
+
if (app.usesCleartextTrafficRaw !== undefined)
|
|
13
|
+
return { state: "malformed", raw: app.usesCleartextTrafficRaw };
|
|
14
|
+
return { state: "missing" };
|
|
15
|
+
}
|
|
16
|
+
function parseNetworkSecurityConfigFile(absolutePath) {
|
|
17
|
+
let xmlText;
|
|
18
|
+
try {
|
|
19
|
+
xmlText = readFileSync(absolutePath, "utf8");
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return { state: "malformed-xml", reason: error instanceof Error ? error.message : "Unable to read the resolved Network Security Config file" };
|
|
23
|
+
}
|
|
24
|
+
return parseNetworkSecurityConfig(xmlText);
|
|
25
|
+
}
|
|
26
|
+
function classifyResolution(raw, resolution, targetRoot) {
|
|
27
|
+
switch (resolution.state) {
|
|
28
|
+
case "resolved": {
|
|
29
|
+
const candidate = resolution.candidates[0];
|
|
30
|
+
return {
|
|
31
|
+
state: "resolved",
|
|
32
|
+
raw,
|
|
33
|
+
modulePath: resolution.modulePath,
|
|
34
|
+
candidate,
|
|
35
|
+
parseResult: parseNetworkSecurityConfigFile(path.join(targetRoot, candidate.relativePath)),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
case "ambiguous":
|
|
39
|
+
return {
|
|
40
|
+
state: "ambiguous",
|
|
41
|
+
raw,
|
|
42
|
+
modulePath: resolution.modulePath,
|
|
43
|
+
candidates: resolution.candidates.map((candidate) => ({
|
|
44
|
+
candidate,
|
|
45
|
+
parseResult: parseNetworkSecurityConfigFile(path.join(targetRoot, candidate.relativePath)),
|
|
46
|
+
})),
|
|
47
|
+
};
|
|
48
|
+
case "missing":
|
|
49
|
+
return { state: "missing", raw, modulePath: resolution.modulePath, searchedSourceSets: resolution.searchedSourceSets };
|
|
50
|
+
case "malformed-reference":
|
|
51
|
+
return { state: "malformed", raw, reason: resolution.reason };
|
|
52
|
+
case "unsupported-reference":
|
|
53
|
+
// Unreachable in practice: this function only calls the resolver once
|
|
54
|
+
// parseAndroidResourceReference has already confirmed a "parsed"
|
|
55
|
+
// (xml-type) reference, so the resolver's own unsupported-reference
|
|
56
|
+
// branch (package-qualified/non-xml) cannot trigger from this path.
|
|
57
|
+
// Handled defensively rather than asserted unreachable.
|
|
58
|
+
return {
|
|
59
|
+
state: "unsupported-type",
|
|
60
|
+
raw,
|
|
61
|
+
resourceType: "type" in resolution.parsed ? resolution.parsed.type : "unknown",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Extracts and classifies cleartext-traffic/Network Security Config evidence
|
|
66
|
+
// for one already-parsed manifest. `targetRoot` must be the absolute Android
|
|
67
|
+
// target root (the same root the manifest/resource resolver operate within).
|
|
68
|
+
export function extractManifestNetworkSecurityEvidence(targetRoot, entry) {
|
|
69
|
+
const usesCleartextTraffic = classifyUsesCleartextTraffic(entry);
|
|
70
|
+
const raw = entry.manifest.application.networkSecurityConfigRef;
|
|
71
|
+
let networkSecurityConfig;
|
|
72
|
+
if (raw === undefined) {
|
|
73
|
+
networkSecurityConfig = { state: "absent" };
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const parsedRef = parseAndroidResourceReference(raw);
|
|
77
|
+
if (parsedRef.state === "placeholder") {
|
|
78
|
+
networkSecurityConfig = { state: "placeholder", raw };
|
|
79
|
+
}
|
|
80
|
+
else if (parsedRef.state === "malformed") {
|
|
81
|
+
networkSecurityConfig = { state: "malformed", raw, reason: parsedRef.reason };
|
|
82
|
+
}
|
|
83
|
+
else if (parsedRef.state === "empty") {
|
|
84
|
+
networkSecurityConfig = { state: "malformed", raw, reason: "Reference is empty" };
|
|
85
|
+
}
|
|
86
|
+
else if (parsedRef.state === "unsupported-type") {
|
|
87
|
+
networkSecurityConfig = { state: "unsupported-type", raw, resourceType: parsedRef.type };
|
|
88
|
+
}
|
|
89
|
+
else if (parsedRef.state === "package-qualified") {
|
|
90
|
+
networkSecurityConfig = { state: "package-qualified", raw, packageQualifier: parsedRef.packageQualifier };
|
|
91
|
+
}
|
|
92
|
+
else if (entry.modulePath === undefined) {
|
|
93
|
+
networkSecurityConfig = { state: "module-unknown", raw };
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
const moduleAbsolutePath = path.join(targetRoot, entry.modulePath);
|
|
97
|
+
const resolution = resolveAndroidXmlResourceReference(targetRoot, moduleAbsolutePath, raw);
|
|
98
|
+
networkSecurityConfig = classifyResolution(raw, resolution, targetRoot);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
manifestPath: entry.manifestPath,
|
|
103
|
+
modulePath: entry.modulePath,
|
|
104
|
+
sourceSetKind: entry.sourceSetKind,
|
|
105
|
+
applicationLocation: entry.manifest.application.location,
|
|
106
|
+
usesCleartextTraffic,
|
|
107
|
+
networkSecurityConfig,
|
|
108
|
+
};
|
|
109
|
+
}
|