@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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.1 Batch 6 — bounded, safe firebase.json and .firebaserc parsing.
|
|
3
|
+
// Only the sections relevant to Android security review (rules file
|
|
4
|
+
// references, project aliases) are extracted; hosting/functions/emulators
|
|
5
|
+
// sections are intentionally ignored (they are unrelated to Android
|
|
6
|
+
// security findings per agents.txt Batch 6 section 13.7).
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
function asRecord(value) {
|
|
9
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
function asArray(value) {
|
|
12
|
+
return Array.isArray(value) ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
function asString(value) {
|
|
15
|
+
return typeof value === "string" ? value : undefined;
|
|
16
|
+
}
|
|
17
|
+
function collectRulesPaths(section) {
|
|
18
|
+
const paths = [];
|
|
19
|
+
const record = asRecord(section);
|
|
20
|
+
if (record) {
|
|
21
|
+
const rules = asString(record.rules);
|
|
22
|
+
if (rules)
|
|
23
|
+
paths.push(rules);
|
|
24
|
+
return paths;
|
|
25
|
+
}
|
|
26
|
+
const array = asArray(section);
|
|
27
|
+
if (array) {
|
|
28
|
+
for (const entry of array) {
|
|
29
|
+
const entryRecord = asRecord(entry);
|
|
30
|
+
const rules = asString(entryRecord?.rules);
|
|
31
|
+
if (rules)
|
|
32
|
+
paths.push(rules);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return paths;
|
|
36
|
+
}
|
|
37
|
+
export function parseFirebaseJson(file) {
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(file.content);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return { file, malformed: true, databaseRulesPaths: [], firestoreRulesPaths: [], storageRulesPaths: [] };
|
|
44
|
+
}
|
|
45
|
+
const root = asRecord(parsed);
|
|
46
|
+
if (!root)
|
|
47
|
+
return { file, malformed: true, databaseRulesPaths: [], firestoreRulesPaths: [], storageRulesPaths: [] };
|
|
48
|
+
return {
|
|
49
|
+
file,
|
|
50
|
+
malformed: false,
|
|
51
|
+
databaseRulesPaths: collectRulesPaths(root.database),
|
|
52
|
+
firestoreRulesPaths: collectRulesPaths(root.firestore),
|
|
53
|
+
storageRulesPaths: collectRulesPaths(root.storage),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function parseFirebaseRc(file) {
|
|
57
|
+
let parsed;
|
|
58
|
+
try {
|
|
59
|
+
parsed = JSON.parse(file.content);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return { file, malformed: true, aliases: {} };
|
|
63
|
+
}
|
|
64
|
+
const root = asRecord(parsed);
|
|
65
|
+
const projects = asRecord(root?.projects);
|
|
66
|
+
if (!projects)
|
|
67
|
+
return { file, malformed: false, aliases: {} };
|
|
68
|
+
const aliases = {};
|
|
69
|
+
for (const [alias, value] of Object.entries(projects)) {
|
|
70
|
+
const projectId = asString(value);
|
|
71
|
+
if (projectId)
|
|
72
|
+
aliases[alias] = projectId;
|
|
73
|
+
}
|
|
74
|
+
return { file, malformed: false, defaultProject: aliases.default, aliases };
|
|
75
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { executableMask, lineForOffset } from "../sensitiveData/localSourceContext.js";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// v0.4.1 Batch 6 — bounded local Firestore/Storage security-rules parser.
|
|
4
|
+
//
|
|
5
|
+
// A brace-aware, comment/string-masked local scanner over `match { ... }` /
|
|
6
|
+
// `allow ...: if ...;` syntax — not a full Firebase rules-language compiler.
|
|
7
|
+
// Functions, imports, and complex boolean expressions are preserved only as
|
|
8
|
+
// a bounded condition summary; this module never evaluates a condition and
|
|
9
|
+
// never claims a rule is (or is not) the deployed configuration.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
const MAX_MATCH_BLOCKS = 500;
|
|
12
|
+
const MAX_ALLOW_STATEMENTS = 1000;
|
|
13
|
+
const MAX_CONDITION_LENGTH = 300;
|
|
14
|
+
const MATCH_HEADER = /\bmatch\s+([^\n{]+?)\s*\{/g;
|
|
15
|
+
const ALLOW_STATEMENT = /\ballow\s+([A-Za-z,\s]+?)\s*(?::\s*if\s+([^;]+?))?\s*;/g;
|
|
16
|
+
const TEST_MODE_TIME = /request\.time\s*[<>]=?\s*timestamp\.(date|value)\s*\(/;
|
|
17
|
+
function braceEnd(mask, openBraceIndex) {
|
|
18
|
+
let depth = 0;
|
|
19
|
+
for (let i = openBraceIndex; i < mask.length; i += 1) {
|
|
20
|
+
if (mask[i] === "{")
|
|
21
|
+
depth += 1;
|
|
22
|
+
else if (mask[i] === "}") {
|
|
23
|
+
depth -= 1;
|
|
24
|
+
if (depth === 0)
|
|
25
|
+
return i;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return mask.length - 1;
|
|
29
|
+
}
|
|
30
|
+
function boundedCondition(condition) {
|
|
31
|
+
if (condition === undefined)
|
|
32
|
+
return "(no condition — always allowed)";
|
|
33
|
+
const trimmed = condition.trim();
|
|
34
|
+
return trimmed.length > MAX_CONDITION_LENGTH ? `${trimmed.slice(0, MAX_CONDITION_LENGTH)}...` : trimmed;
|
|
35
|
+
}
|
|
36
|
+
export function parseFirestoreOrStorageRules(file, service) {
|
|
37
|
+
const mask = executableMask(file.content);
|
|
38
|
+
let boundsExceeded = false;
|
|
39
|
+
const blocks = [];
|
|
40
|
+
for (const match of mask.matchAll(MATCH_HEADER)) {
|
|
41
|
+
if (blocks.length >= MAX_MATCH_BLOCKS) {
|
|
42
|
+
boundsExceeded = true;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
const from = match.index ?? 0;
|
|
46
|
+
const openBrace = from + match[0].length - 1;
|
|
47
|
+
blocks.push({ pattern: match[1].trim(), from, to: braceEnd(mask, openBrace) });
|
|
48
|
+
}
|
|
49
|
+
function enclosingPattern(offset) {
|
|
50
|
+
let best;
|
|
51
|
+
for (const block of blocks) {
|
|
52
|
+
if (offset >= block.from && offset <= block.to) {
|
|
53
|
+
if (best === undefined || block.to - block.from < best.to - best.from)
|
|
54
|
+
best = block;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return best?.pattern ?? "(unresolved match scope)";
|
|
58
|
+
}
|
|
59
|
+
const matches = [];
|
|
60
|
+
let allowCount = 0;
|
|
61
|
+
for (const match of mask.matchAll(ALLOW_STATEMENT)) {
|
|
62
|
+
if (allowCount >= MAX_ALLOW_STATEMENTS) {
|
|
63
|
+
boundsExceeded = true;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
allowCount += 1;
|
|
67
|
+
const offset = match.index ?? 0;
|
|
68
|
+
const operations = match[1]
|
|
69
|
+
.split(",")
|
|
70
|
+
.map((op) => op.trim())
|
|
71
|
+
.filter(Boolean);
|
|
72
|
+
const conditionRaw = match[2]?.trim();
|
|
73
|
+
const isLiteralTrue = conditionRaw === undefined || conditionRaw === "true";
|
|
74
|
+
const isLiteralFalse = conditionRaw === "false";
|
|
75
|
+
matches.push({
|
|
76
|
+
service,
|
|
77
|
+
matchPath: enclosingPattern(offset),
|
|
78
|
+
operations,
|
|
79
|
+
conditionSummary: boundedCondition(conditionRaw),
|
|
80
|
+
isLiteralTrue,
|
|
81
|
+
isLiteralFalse,
|
|
82
|
+
hasAuthCondition: conditionRaw !== undefined && /request\.auth/.test(conditionRaw),
|
|
83
|
+
isTestModeTimeCondition: conditionRaw !== undefined && TEST_MODE_TIME.test(conditionRaw),
|
|
84
|
+
line: lineForOffset(file.content, offset),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return { file, malformed: false, matches, boundsExceeded };
|
|
88
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.1 Batch 6 — bounded, safe google-services.json parsing.
|
|
3
|
+
//
|
|
4
|
+
// A bare Firebase API key is not a private secret by format (see
|
|
5
|
+
// sensitiveCategories.ts) — this parser only records bounded presence
|
|
6
|
+
// metadata (a count) for oauth_client/api_key entries, never the values
|
|
7
|
+
// themselves. Malformed JSON is handled safely (never throws to the
|
|
8
|
+
// caller); unsupported/unexpected shapes degrade to an empty client list
|
|
9
|
+
// rather than crashing.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
function asRecord(value) {
|
|
12
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
function asArray(value) {
|
|
15
|
+
return Array.isArray(value) ? value : undefined;
|
|
16
|
+
}
|
|
17
|
+
function asString(value) {
|
|
18
|
+
return typeof value === "string" ? value : undefined;
|
|
19
|
+
}
|
|
20
|
+
export function parseGoogleServicesConfig(file) {
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(file.content);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { file, malformed: true, clients: [] };
|
|
27
|
+
}
|
|
28
|
+
const root = asRecord(parsed);
|
|
29
|
+
if (!root)
|
|
30
|
+
return { file, malformed: true, clients: [] };
|
|
31
|
+
const projectInfo = asRecord(root.project_info);
|
|
32
|
+
const clientsRaw = asArray(root.client) ?? [];
|
|
33
|
+
const clients = clientsRaw.map((entry) => {
|
|
34
|
+
const clientRecord = asRecord(entry) ?? {};
|
|
35
|
+
const clientInfo = asRecord(clientRecord.client_info);
|
|
36
|
+
const androidClientInfo = asRecord(clientInfo?.android_client_info);
|
|
37
|
+
const oauthClient = asArray(clientRecord.oauth_client) ?? [];
|
|
38
|
+
const apiKey = asArray(clientRecord.api_key) ?? [];
|
|
39
|
+
return {
|
|
40
|
+
packageName: asString(androidClientInfo?.package_name),
|
|
41
|
+
mobileSdkAppId: asString(clientInfo?.mobilesdk_app_id),
|
|
42
|
+
apiKeyCount: apiKey.length,
|
|
43
|
+
oauthClientCount: oauthClient.length,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
file,
|
|
48
|
+
malformed: false,
|
|
49
|
+
projectId: asString(projectInfo?.project_id),
|
|
50
|
+
projectNumber: asString(projectInfo?.project_number),
|
|
51
|
+
storageBucket: asString(projectInfo?.storage_bucket),
|
|
52
|
+
firebaseUrl: asString(projectInfo?.firebase_url),
|
|
53
|
+
clients,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.1 Batch 6 — Firebase/Google services semantic contracts. Local to
|
|
3
|
+
// this feature; does not duplicate Batch 1's CandidateEvidence/
|
|
4
|
+
// SecurityFinding/AndroidCheckResult.
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
export {};
|
|
@@ -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
|
+
}
|