@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,141 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { makeAndroidFinding } from "../../../audit/androidFinding.js";
|
|
3
|
+
import { makeCandidateEvidence } from "../../candidateEvidence.js";
|
|
4
|
+
import { buildAndroidSourceLocation } from "../../sourceLocation.js";
|
|
5
|
+
import { safeJsonParse, DEFAULT_MAX_MESSAGE_LENGTH } from "../boundedOutput.js";
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// v0.4.1 Batch 7 — bounded Semgrep JSON normalization.
|
|
8
|
+
//
|
|
9
|
+
// Only the one Batch 1 Semgrep rule id (android-optional-tool-semgrep-
|
|
10
|
+
// evidence) is used internally; the external Semgrep rule id is preserved
|
|
11
|
+
// separately as `externalRuleId`. Full source snippets (extra.lines) and
|
|
12
|
+
// complete metavariable values are never retained.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const MAX_RESULTS = 5_000;
|
|
15
|
+
const MAX_SKIPPED_PATHS = 200;
|
|
16
|
+
function severityToProjectSeverity(severity) {
|
|
17
|
+
switch (severity) {
|
|
18
|
+
case "ERROR":
|
|
19
|
+
return "major";
|
|
20
|
+
case "WARNING":
|
|
21
|
+
return "minor";
|
|
22
|
+
default:
|
|
23
|
+
return "informational";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function boundedMessage(message) {
|
|
27
|
+
const text = typeof message === "string" ? message : "";
|
|
28
|
+
return text.length > DEFAULT_MAX_MESSAGE_LENGTH ? `${text.slice(0, DEFAULT_MAX_MESSAGE_LENGTH)}...` : text;
|
|
29
|
+
}
|
|
30
|
+
export function parseSemgrepJson(rawStdout, targetRoot, configFingerprint) {
|
|
31
|
+
const parsed = safeJsonParse(rawStdout);
|
|
32
|
+
if (!parsed.ok) {
|
|
33
|
+
return { malformed: true, findings: [], candidates: [], scannedCount: 0, skippedPaths: [], parserErrors: [], truncated: parsed.truncated };
|
|
34
|
+
}
|
|
35
|
+
const doc = parsed.value;
|
|
36
|
+
const toolVersion = typeof doc.version === "string" ? doc.version : undefined;
|
|
37
|
+
const results = Array.isArray(doc.results) ? doc.results : [];
|
|
38
|
+
const errors = Array.isArray(doc.errors) ? doc.errors : [];
|
|
39
|
+
const pathsSection = doc.paths;
|
|
40
|
+
const scanned = Array.isArray(pathsSection?.scanned) ? pathsSection.scanned : [];
|
|
41
|
+
const skipped = Array.isArray(pathsSection?.skipped) ? pathsSection.skipped : [];
|
|
42
|
+
const findings = [];
|
|
43
|
+
const candidates = [];
|
|
44
|
+
let truncated = false;
|
|
45
|
+
for (const rawResult of results.slice(0, MAX_RESULTS)) {
|
|
46
|
+
if (results.length > MAX_RESULTS)
|
|
47
|
+
truncated = true;
|
|
48
|
+
const result = rawResult;
|
|
49
|
+
const checkId = typeof result.check_id === "string" ? result.check_id : "(unknown-rule)";
|
|
50
|
+
const relativePath = typeof result.path === "string" ? result.path : undefined;
|
|
51
|
+
const start = result.start;
|
|
52
|
+
const line = typeof start?.line === "number" ? start.line : undefined;
|
|
53
|
+
const extra = result.extra ?? {};
|
|
54
|
+
const severity = typeof extra.severity === "string" ? extra.severity : undefined;
|
|
55
|
+
const message = boundedMessage(extra.message);
|
|
56
|
+
const projectSeverity = severityToProjectSeverity(severity);
|
|
57
|
+
if (!relativePath) {
|
|
58
|
+
candidates.push(makeCandidateEvidence({
|
|
59
|
+
ruleId: "android-optional-tool-semgrep-evidence",
|
|
60
|
+
category: "android-semgrep",
|
|
61
|
+
confidence: "low",
|
|
62
|
+
location: { path: "(unknown)" },
|
|
63
|
+
summary: `Semgrep result "${checkId}" had no reportable path`,
|
|
64
|
+
rawValue: undefined,
|
|
65
|
+
resolutionState: "unresolved",
|
|
66
|
+
staticAnalysisLimitations: ["Semgrep static analysis only; does not prove runtime exploitability."],
|
|
67
|
+
}));
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const absolutePath = path.resolve(targetRoot, relativePath);
|
|
71
|
+
let location;
|
|
72
|
+
try {
|
|
73
|
+
location = buildAndroidSourceLocation(targetRoot, absolutePath, { line });
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
candidates.push(makeCandidateEvidence({
|
|
77
|
+
ruleId: "android-optional-tool-semgrep-evidence",
|
|
78
|
+
category: "android-semgrep",
|
|
79
|
+
confidence: "low",
|
|
80
|
+
location: { path: relativePath },
|
|
81
|
+
summary: `Semgrep result "${checkId}" reported a path outside the target`,
|
|
82
|
+
rawValue: relativePath,
|
|
83
|
+
resolutionState: "unresolved",
|
|
84
|
+
staticAnalysisLimitations: ["Semgrep static analysis only; does not prove runtime exploitability."],
|
|
85
|
+
}));
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (projectSeverity === "informational" || severity === undefined) {
|
|
89
|
+
candidates.push(makeCandidateEvidence({
|
|
90
|
+
ruleId: "android-optional-tool-semgrep-evidence",
|
|
91
|
+
category: "android-semgrep",
|
|
92
|
+
confidence: "medium",
|
|
93
|
+
location,
|
|
94
|
+
summary: `Semgrep ${severity ?? "unknown-severity"} result: ${checkId} — ${message}`,
|
|
95
|
+
rawValue: message,
|
|
96
|
+
resolutionState: "resolved",
|
|
97
|
+
staticAnalysisLimitations: [`External rule: ${checkId}`, "Semgrep static analysis only; does not prove runtime exploitability."],
|
|
98
|
+
}));
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
findings.push(makeAndroidFinding({
|
|
102
|
+
ruleId: "android-optional-tool-semgrep-evidence",
|
|
103
|
+
title: `Semgrep finding: ${checkId}`,
|
|
104
|
+
severity: projectSeverity,
|
|
105
|
+
confidence: "medium",
|
|
106
|
+
description: message || "Semgrep reported a finding for this rule.",
|
|
107
|
+
manifestPath: relativePath,
|
|
108
|
+
identity: `semgrep:${checkId}:${configFingerprint.slice(0, 16)}`,
|
|
109
|
+
location: { line },
|
|
110
|
+
evidenceDetails: [`externalRuleId=${checkId}`, `toolSeverity=${severity}`],
|
|
111
|
+
recommendation: "Review the Semgrep rule pack entry and remediate the matched pattern.",
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
for (const rawSkip of skipped.slice(0, MAX_SKIPPED_PATHS)) {
|
|
115
|
+
const skip = rawSkip;
|
|
116
|
+
const skippedPath = typeof skip === "string" ? skip : typeof skip.path === "string" ? skip.path : undefined;
|
|
117
|
+
if (skippedPath) {
|
|
118
|
+
candidates.push(makeCandidateEvidence({
|
|
119
|
+
ruleId: "android-optional-tool-semgrep-evidence",
|
|
120
|
+
category: "android-semgrep",
|
|
121
|
+
confidence: "low",
|
|
122
|
+
location: { path: skippedPath },
|
|
123
|
+
summary: "Semgrep skipped this path during the scan",
|
|
124
|
+
rawValue: undefined,
|
|
125
|
+
resolutionState: "not-applicable",
|
|
126
|
+
staticAnalysisLimitations: ["Semgrep static analysis only."],
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const parserErrors = errors.slice(0, 50).map((e) => (typeof e === "object" && e !== null && "message" in e ? boundedMessage(e.message) : "Semgrep reported an error."));
|
|
131
|
+
return {
|
|
132
|
+
malformed: false,
|
|
133
|
+
toolVersion,
|
|
134
|
+
findings,
|
|
135
|
+
candidates,
|
|
136
|
+
scannedCount: scanned.length,
|
|
137
|
+
skippedPaths: skipped.map((s) => (typeof s === "string" ? s : typeof s?.path === "string" ? s.path : "")).filter(Boolean),
|
|
138
|
+
parserErrors,
|
|
139
|
+
truncated,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.1 Batch 7 — package-owned, bounded Android-focused Semgrep rule pack.
|
|
3
|
+
//
|
|
4
|
+
// Small, stable supplement to the internal checks — NOT a replacement.
|
|
5
|
+
// Deliberately excludes hardcoded-secret rules (owned by Batch 4's
|
|
6
|
+
// secretCandidates analyzer) and manifest-attribute rules (owned by the
|
|
7
|
+
// internal manifest parser); covers source-level patterns those analyzers
|
|
8
|
+
// do not reach. Caller-provided configuration is never accepted — this is
|
|
9
|
+
// the only rule pack Semgrep is ever invoked with.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
export const ANDROID_SEMGREP_RULE_PACK = [
|
|
12
|
+
{
|
|
13
|
+
id: "android-semgrep-trust-all-hostname-verifier",
|
|
14
|
+
languages: ["java", "kotlin"],
|
|
15
|
+
message: "A HostnameVerifier implementation unconditionally returns true, disabling TLS hostname verification.",
|
|
16
|
+
severity: "ERROR",
|
|
17
|
+
cwe: ["CWE-297"],
|
|
18
|
+
owasp: ["A02:2021"],
|
|
19
|
+
patterns: ["new HostnameVerifier() { ... boolean verify(...) { return true; } ... }"],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "android-semgrep-permissive-x509-trust-manager",
|
|
23
|
+
languages: ["java", "kotlin"],
|
|
24
|
+
message: "An X509TrustManager implementation has an empty or no-op certificate validation method, accepting any certificate chain.",
|
|
25
|
+
severity: "ERROR",
|
|
26
|
+
cwe: ["CWE-295"],
|
|
27
|
+
owasp: ["A02:2021"],
|
|
28
|
+
patterns: [
|
|
29
|
+
"class $T : X509TrustManager { ... override fun checkServerTrusted(...) { } ... }",
|
|
30
|
+
"new X509TrustManager() { ... public void checkServerTrusted(...) { } ... }",
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "android-semgrep-ssl-error-handler-proceed",
|
|
35
|
+
languages: ["java", "kotlin"],
|
|
36
|
+
message: "onReceivedSslError calls handler.proceed(), ignoring the certificate error and continuing the connection.",
|
|
37
|
+
severity: "ERROR",
|
|
38
|
+
cwe: ["CWE-295"],
|
|
39
|
+
patterns: ["... onReceivedSslError(...) { ... $HANDLER.proceed() ... }"],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "android-semgrep-webview-universal-file-url-access",
|
|
43
|
+
languages: ["java", "kotlin"],
|
|
44
|
+
message: "setAllowUniversalAccessFromFileURLs(true) allows file:// content to access any origin.",
|
|
45
|
+
severity: "WARNING",
|
|
46
|
+
cwe: ["CWE-668"],
|
|
47
|
+
patterns: ["$WEBVIEW.settings.allowUniversalAccessFromFileURLs = true", "$SETTINGS.setAllowUniversalAccessFromFileURLs(true)"],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "android-semgrep-webview-debugging-enabled",
|
|
51
|
+
languages: ["java", "kotlin"],
|
|
52
|
+
message: "WebView.setWebContentsDebuggingEnabled(true) is set unconditionally, which may ship to release builds.",
|
|
53
|
+
severity: "INFO",
|
|
54
|
+
patterns: ["WebView.setWebContentsDebuggingEnabled(true)"],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "android-semgrep-world-readable-writable-mode",
|
|
58
|
+
languages: ["java", "kotlin"],
|
|
59
|
+
message: "A file or preferences API is opened with MODE_WORLD_READABLE or MODE_WORLD_WRITEABLE.",
|
|
60
|
+
severity: "ERROR",
|
|
61
|
+
cwe: ["CWE-732"],
|
|
62
|
+
patterns: ["$CTX.getSharedPreferences($NAME, Context.MODE_WORLD_READABLE)", "$CTX.getSharedPreferences($NAME, Context.MODE_WORLD_WRITEABLE)", "$CTX.openFileOutput($NAME, Context.MODE_WORLD_READABLE)", "$CTX.openFileOutput($NAME, Context.MODE_WORLD_WRITEABLE)"],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "android-semgrep-weak-digest-md5-sha1",
|
|
66
|
+
languages: ["java", "kotlin"],
|
|
67
|
+
message: "MessageDigest.getInstance uses a weak digest algorithm (MD5 or SHA-1) for security-sensitive hashing.",
|
|
68
|
+
severity: "WARNING",
|
|
69
|
+
cwe: ["CWE-327"],
|
|
70
|
+
patterns: ['MessageDigest.getInstance("MD5")', 'MessageDigest.getInstance("SHA-1")', 'MessageDigest.getInstance("SHA1")'],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "android-semgrep-insecure-cipher-transformation",
|
|
74
|
+
languages: ["java", "kotlin"],
|
|
75
|
+
message: "Cipher.getInstance uses an insecure transformation (ECB mode, DES, or RC4).",
|
|
76
|
+
severity: "ERROR",
|
|
77
|
+
cwe: ["CWE-327"],
|
|
78
|
+
patterns: ['Cipher.getInstance("...ECB...")', 'Cipher.getInstance("DES")', 'Cipher.getInstance("RC4")'],
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
export function assertUniqueSemgrepRuleIds() {
|
|
82
|
+
const ids = ANDROID_SEMGREP_RULE_PACK.map((rule) => rule.id);
|
|
83
|
+
const unique = new Set(ids);
|
|
84
|
+
if (unique.size !== ids.length) {
|
|
85
|
+
throw new Error("Duplicate Semgrep rule id detected in ANDROID_SEMGREP_RULE_PACK.");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Deterministic YAML-compatible serialization (JSON is valid YAML; Semgrep
|
|
89
|
+
// accepts a .yaml/.yml config file). Key order is fixed so repeated calls
|
|
90
|
+
// produce byte-identical output (config fingerprinting depends on this).
|
|
91
|
+
export function serializeSemgrepRulePack() {
|
|
92
|
+
const rules = ANDROID_SEMGREP_RULE_PACK.map((rule) => ({
|
|
93
|
+
id: rule.id,
|
|
94
|
+
languages: [...rule.languages],
|
|
95
|
+
message: rule.message,
|
|
96
|
+
severity: rule.severity,
|
|
97
|
+
metadata: {
|
|
98
|
+
...(rule.cwe ? { cwe: [...rule.cwe] } : {}),
|
|
99
|
+
...(rule.owasp ? { owasp: [...rule.owasp] } : {}),
|
|
100
|
+
"android-package-owned": true,
|
|
101
|
+
},
|
|
102
|
+
"pattern-either": rule.patterns.map((pattern) => ({ pattern })),
|
|
103
|
+
}));
|
|
104
|
+
return JSON.stringify({ rules }, null, 2);
|
|
105
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.1 Batch 7 — common contract for optional external Android
|
|
3
|
+
// security-tool evidence (Semgrep, OSV-Scanner, Android Lint,
|
|
4
|
+
// OWASP Dependency-Check).
|
|
5
|
+
//
|
|
6
|
+
// This module defines identities and request/result shapes only. No process
|
|
7
|
+
// ever executes because a type exists here. Every tool remains disconnected
|
|
8
|
+
// from validateAndroidTarget, the CLI, verdict calculation, and active
|
|
9
|
+
// reports until Batch 8 — see each adapter's checkResult.ts for the
|
|
10
|
+
// standalone AndroidCheckResult-compatible builder.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export const ANDROID_EXTERNAL_TOOL_IDS = ["semgrep", "osv", "android-lint", "dependency-check"];
|
|
13
|
+
export function isAndroidExternalSecurityToolId(value) {
|
|
14
|
+
return ANDROID_EXTERNAL_TOOL_IDS.includes(value);
|
|
15
|
+
}
|
|
16
|
+
// Deterministic dispatch order — never concurrent, never caller-controlled.
|
|
17
|
+
export const ANDROID_EXTERNAL_TOOL_ORDER = ["semgrep", "osv", "android-lint", "dependency-check"];
|
|
18
|
+
export const ANDROID_EXTERNAL_TOOL_NETWORK_POLICIES = ["deny", "allow-for-requested-tool"];
|
|
19
|
+
const MIN_TIMEOUT_OVERRIDE_MS = 5_000;
|
|
20
|
+
const MAX_TIMEOUT_OVERRIDE_MS = 30 * 60_000;
|
|
21
|
+
// Deduplicates and orders requested tool ids deterministically, and rejects
|
|
22
|
+
// any id outside the closed ANDROID_EXTERNAL_TOOL_IDS union at this single
|
|
23
|
+
// contract boundary — no caller can smuggle an arbitrary tool identity past
|
|
24
|
+
// this point.
|
|
25
|
+
export function normalizeAndroidExternalToolRequest(request) {
|
|
26
|
+
if (!request || !Array.isArray(request.requestedTools) || request.requestedTools.length === 0) {
|
|
27
|
+
return {
|
|
28
|
+
ok: true,
|
|
29
|
+
value: {
|
|
30
|
+
tools: [],
|
|
31
|
+
targetRoot: request?.targetRoot ?? "",
|
|
32
|
+
artifactRoot: request?.artifactRoot ?? "",
|
|
33
|
+
networkPolicy: request?.networkPolicy ?? "deny",
|
|
34
|
+
timeoutOverrideMs: request?.timeoutOverrideMs,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const unknown = request.requestedTools.filter((id) => !isAndroidExternalSecurityToolId(id));
|
|
39
|
+
if (unknown.length > 0) {
|
|
40
|
+
return { ok: false, error: `Unknown external security tool id(s): ${unknown.join(", ")}` };
|
|
41
|
+
}
|
|
42
|
+
if (!request.targetRoot)
|
|
43
|
+
return { ok: false, error: "targetRoot is required." };
|
|
44
|
+
if (!request.artifactRoot)
|
|
45
|
+
return { ok: false, error: "artifactRoot is required." };
|
|
46
|
+
if (request.timeoutOverrideMs !== undefined) {
|
|
47
|
+
if (request.timeoutOverrideMs < MIN_TIMEOUT_OVERRIDE_MS || request.timeoutOverrideMs > MAX_TIMEOUT_OVERRIDE_MS) {
|
|
48
|
+
return { ok: false, error: `timeoutOverrideMs must be between ${MIN_TIMEOUT_OVERRIDE_MS} and ${MAX_TIMEOUT_OVERRIDE_MS}.` };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const requestedSet = new Set(request.requestedTools);
|
|
52
|
+
const tools = ANDROID_EXTERNAL_TOOL_ORDER.filter((id) => requestedSet.has(id));
|
|
53
|
+
return {
|
|
54
|
+
ok: true,
|
|
55
|
+
value: {
|
|
56
|
+
tools,
|
|
57
|
+
targetRoot: request.targetRoot,
|
|
58
|
+
artifactRoot: request.artifactRoot,
|
|
59
|
+
networkPolicy: request.networkPolicy ?? "deny",
|
|
60
|
+
timeoutOverrideMs: request.timeoutOverrideMs,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Artifact references — Batch 7 introduces this concept new (no existing
|
|
66
|
+
// owner copies/hashes generated tool output); Batch 8 decides where active
|
|
67
|
+
// reports place these.
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
export const ANDROID_EXTERNAL_TOOL_ARTIFACT_KINDS = ["json", "xml", "sarif", "text"];
|
|
70
|
+
export const ANDROID_EXTERNAL_TOOL_PARSE_STATUSES = ["parsed", "malformed", "truncated", "not-attempted"];
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Common per-tool execution result — every adapter's checkResult.ts folds
|
|
73
|
+
// this (plus tool-specific findings/candidates) into a standalone
|
|
74
|
+
// AndroidCheckResult, exactly as the existing Gradle operations fold a
|
|
75
|
+
// CommandExecutionResult via buildGradleOperationCheckResult.
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
export const ANDROID_EXTERNAL_TOOL_STATUSES = [
|
|
78
|
+
"not-requested",
|
|
79
|
+
"skipped",
|
|
80
|
+
"completed-without-findings",
|
|
81
|
+
"completed-with-findings",
|
|
82
|
+
"inconclusive",
|
|
83
|
+
"failed",
|
|
84
|
+
];
|
|
85
|
+
export function notRequestedResult(toolId, targetRoot, networkPolicy) {
|
|
86
|
+
return {
|
|
87
|
+
toolId,
|
|
88
|
+
requested: false,
|
|
89
|
+
available: false,
|
|
90
|
+
executed: false,
|
|
91
|
+
status: "not-requested",
|
|
92
|
+
commandSummary: "(not requested)",
|
|
93
|
+
workingDirectory: targetRoot,
|
|
94
|
+
networkPolicy,
|
|
95
|
+
durationMs: 0,
|
|
96
|
+
exitCode: null,
|
|
97
|
+
timedOut: false,
|
|
98
|
+
stdoutTruncated: false,
|
|
99
|
+
stderrTruncated: false,
|
|
100
|
+
artifacts: [],
|
|
101
|
+
warnings: [],
|
|
102
|
+
errors: [],
|
|
103
|
+
skips: [],
|
|
104
|
+
mutationEvidence: { comparable: true, expectedGeneratedChanges: [], unexpectedChanges: [] },
|
|
105
|
+
limitations: [],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseXmlDocument } from "../../manifest/xml/parseXml.js";
|
|
4
|
+
import { makeAndroidFinding } from "../../audit/androidFinding.js";
|
|
5
|
+
import { makeCandidateEvidence } from "../candidateEvidence.js";
|
|
6
|
+
import { sortCandidateEvidence } from "../ordering.js";
|
|
7
|
+
import { resolveAndroidXmlResourceReference } from "../resourceResolution.js";
|
|
8
|
+
import { buildAndroidSourceLocation } from "../sourceLocation.js";
|
|
9
|
+
import { discoverSecretSourceFiles } from "../secretCandidates/discoverSecretSourceFiles.js";
|
|
10
|
+
export const ANDROID_FILE_PROVIDER_AUDIT_CHECK_ID = "android-file-provider-audit";
|
|
11
|
+
const FILE_PROVIDER_METADATA = "android.support.FILE_PROVIDER_PATHS";
|
|
12
|
+
const PATH_TAGS = new Set(["root-path", "files-path", "cache-path", "external-path", "external-files-path", "external-cache-path", "external-media-path"]);
|
|
13
|
+
const EXTERNAL_TAGS = new Set(["external-path", "external-files-path", "external-cache-path", "external-media-path"]);
|
|
14
|
+
const MAX_PATH_ENTRIES = 256;
|
|
15
|
+
const MAX_EVIDENCE_VALUE = 240;
|
|
16
|
+
function sourceLocation(root, relativePath, line) {
|
|
17
|
+
return buildAndroidSourceLocation(root, path.join(root, relativePath), { line });
|
|
18
|
+
}
|
|
19
|
+
function standardProvider(name) {
|
|
20
|
+
return name === "androidx.core.content.FileProvider" || name === "android.support.v4.content.FileProvider";
|
|
21
|
+
}
|
|
22
|
+
function directCustomProviders(root, detection) {
|
|
23
|
+
const byModule = new Map();
|
|
24
|
+
const discovery = discoverSecretSourceFiles(root, detection.modules.map((module) => module.path));
|
|
25
|
+
for (const file of discovery.files.filter((candidate) => /\.(java|kt)$/.test(candidate.relativePath))) {
|
|
26
|
+
const moduleKey = file.modulePath ?? "";
|
|
27
|
+
const names = byModule.get(moduleKey) ?? new Set();
|
|
28
|
+
const executable = file.content.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\r\n]*/g, " ");
|
|
29
|
+
for (const match of executable.matchAll(/\bclass\s+([A-Za-z_]\w*)\s*(?:extends\s+|:\s*)(?:androidx\.core\.content\.|android\.support\.v4\.content\.)?FileProvider\s*(?:\(\s*\))?/g))
|
|
30
|
+
names.add(match[1]);
|
|
31
|
+
byModule.set(moduleKey, names);
|
|
32
|
+
}
|
|
33
|
+
return byModule;
|
|
34
|
+
}
|
|
35
|
+
function finding(ruleId, title, artifactPath, line, identity, severity) {
|
|
36
|
+
return makeAndroidFinding({ ruleId, title, severity, confidence: "high", description: `${title}. This is static manifest/XML evidence and does not prove runtime reachability, URI access, or file exposure.`, manifestPath: artifactPath, identity, location: line === undefined ? undefined : { line }, recommendation: "Keep FileProvider non-exported and restrict configured roots to the narrowest required paths." });
|
|
37
|
+
}
|
|
38
|
+
function candidate(ruleId, root, artifactPath, modulePath, line, summary, raw, state = "resolved", confidence = "medium") {
|
|
39
|
+
return makeCandidateEvidence({ ruleId, category: "android-file-provider", confidence, modulePath, location: sourceLocation(root, artifactPath, line), summary, rawValue: (raw ?? "(missing)").slice(0, MAX_EVIDENCE_VALUE), resolutionState: state, staticAnalysisLimitations: ["Manifest sources are not merged; resource overlays, installed authorities, URI grants, permission protection levels, and runtime filesystem behavior are not evaluated."] });
|
|
40
|
+
}
|
|
41
|
+
function protectedProvider(provider) {
|
|
42
|
+
return Boolean(provider.permission || provider.readPermission || provider.writePermission);
|
|
43
|
+
}
|
|
44
|
+
function broadPath(tag, rawPath) {
|
|
45
|
+
const value = (rawPath ?? "").trim().replace(/\\/g, "/").replace(/\/{2,}/g, "/");
|
|
46
|
+
return tag === "root-path" || value === "" || value === "." || value === "/" || /(^|\/)\.\.(\/|$)/.test(value) || /^[A-Za-z]:\//.test(value) || value.startsWith("/") || (EXTERNAL_TAGS.has(tag) && value.split("/").filter(Boolean).length <= 1);
|
|
47
|
+
}
|
|
48
|
+
function analyzeAuthorities(root, entry, provider, seen, out) {
|
|
49
|
+
const raw = provider.authoritiesRaw;
|
|
50
|
+
if (raw === undefined || raw.trim() === "") {
|
|
51
|
+
out.push(candidate("android-file-provider-exported", root, entry.manifestPath, entry.modulePath, provider.location?.line, "FileProvider authority is missing or empty", raw, raw === undefined ? "missing" : "malformed"));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const local = new Set();
|
|
55
|
+
for (const authority of raw.split(";")) {
|
|
56
|
+
const normalized = authority.trim();
|
|
57
|
+
if (!normalized) {
|
|
58
|
+
out.push(candidate("android-file-provider-exported", root, entry.manifestPath, entry.modulePath, provider.location?.line, "FileProvider authority contains an empty entry", authority, "malformed"));
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (/\$\{[^}]+\}/.test(normalized)) {
|
|
62
|
+
out.push(candidate("android-file-provider-exported", root, entry.manifestPath, entry.modulePath, provider.location?.line, "FileProvider authority contains an unresolved placeholder", normalized, "unresolved"));
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(normalized)) {
|
|
66
|
+
out.push(candidate("android-file-provider-exported", root, entry.manifestPath, entry.modulePath, provider.location?.line, "FileProvider authority is malformed", normalized, "malformed"));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const key = normalized.toLowerCase();
|
|
70
|
+
if (local.has(key) || seen.has(key))
|
|
71
|
+
out.push(candidate("android-file-provider-exported", root, entry.manifestPath, entry.modulePath, provider.location?.line, `Duplicate FileProvider authority${seen.has(key) ? " across providers/modules" : " within provider"}`, normalized, "unresolved"));
|
|
72
|
+
local.add(key);
|
|
73
|
+
seen.set(key, `${entry.modulePath ?? ""}:${entry.manifestPath}:${provider.name}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function auditAndroidFileProviders(targetRoot, detection, manifests) {
|
|
77
|
+
if (detection.projectKind === "non-android")
|
|
78
|
+
return { id: ANDROID_FILE_PROVIDER_AUDIT_CHECK_ID, category: "android-file-provider", title: "Android FileProvider audit", status: "unsupported", requirementLevel: "optional", ran: false, skipped: true, skipInfo: { checkId: ANDROID_FILE_PROVIDER_AUDIT_CHECK_ID, reason: "Target was not detected as Android.", requirementLevel: "optional", verdictImpact: "does not apply", recommendedNextAction: "Run against an Android project." }, evidence: [], findings: [], warnings: [], errors: [], sourcePaths: [], confidence: "unknown", environmentRequirements: [], candidateEvidence: [] };
|
|
79
|
+
const findings = [];
|
|
80
|
+
const candidates = [];
|
|
81
|
+
const sources = new Set();
|
|
82
|
+
const seenAuthorities = new Map();
|
|
83
|
+
const custom = directCustomProviders(targetRoot, detection);
|
|
84
|
+
for (const entry of manifests)
|
|
85
|
+
for (const provider of entry.manifest.providers) {
|
|
86
|
+
const simpleName = provider.name.split(".").at(-1) ?? provider.name;
|
|
87
|
+
const confirmed = standardProvider(provider.name) || (custom.get(entry.modulePath ?? "")?.has(simpleName) ?? false);
|
|
88
|
+
if (!confirmed) {
|
|
89
|
+
if (/FileProvider$/i.test(provider.name))
|
|
90
|
+
candidates.push(candidate("android-file-provider-exported", targetRoot, entry.manifestPath, entry.modulePath, provider.location?.line, "FileProvider-like class is not a directly proven FileProvider subclass", provider.name, "unresolved", "low"));
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
sources.add(entry.manifestPath);
|
|
94
|
+
const library = entry.modulePath !== undefined && detection.libraryModules.includes(entry.modulePath);
|
|
95
|
+
analyzeAuthorities(targetRoot, entry, provider, seenAuthorities, candidates);
|
|
96
|
+
if (provider.exported === true) {
|
|
97
|
+
if (library)
|
|
98
|
+
candidates.push(candidate("android-file-provider-exported", targetRoot, entry.manifestPath, entry.modulePath, provider.location?.line, "Library manifest explicitly exports a FileProvider; application merged behavior requires review", provider.name, "resolved"));
|
|
99
|
+
else
|
|
100
|
+
findings.push(finding("android-file-provider-exported", "Confirmed FileProvider is explicitly exported", entry.manifestPath, provider.location?.line, provider.name, "major"));
|
|
101
|
+
// The generic exported-component audit already owns the exact unprotected-provider finding.
|
|
102
|
+
if (!protectedProvider(provider))
|
|
103
|
+
candidates.push(candidate("android-file-provider-missing-protection", targetRoot, entry.manifestPath, entry.modulePath, provider.location?.line, "Exported FileProvider has no provider/read/write permission (generic exported audit owns the equivalent finding)", provider.name, "resolved", "high"));
|
|
104
|
+
}
|
|
105
|
+
else if (provider.exported === undefined)
|
|
106
|
+
candidates.push(candidate("android-file-provider-exported", targetRoot, entry.manifestPath, entry.modulePath, provider.location?.line, "FileProvider exported state is missing or unresolved", provider.exportedRaw, provider.exportedRaw === undefined ? "missing" : "unresolved"));
|
|
107
|
+
if (provider.grantUriPermissions === undefined)
|
|
108
|
+
candidates.push(candidate("android-file-provider-exported", targetRoot, entry.manifestPath, entry.modulePath, provider.location?.line, "FileProvider grantUriPermissions is missing or unresolved", provider.grantUriPermissionsRaw, provider.grantUriPermissionsRaw === undefined ? "missing" : "unresolved", "low"));
|
|
109
|
+
for (const value of [provider.permission, provider.readPermission, provider.writePermission].filter((item) => item !== undefined))
|
|
110
|
+
if (value.trim() === "" || /\$\{[^}]+\}/.test(value))
|
|
111
|
+
candidates.push(candidate("android-file-provider-missing-protection", targetRoot, entry.manifestPath, entry.modulePath, provider.location?.line, "Provider permission value is empty or unresolved", value, value.trim() === "" ? "malformed" : "unresolved"));
|
|
112
|
+
const metadata = (provider.metadata ?? []).filter((item) => item.name === FILE_PROVIDER_METADATA);
|
|
113
|
+
if (metadata.length === 0) {
|
|
114
|
+
candidates.push(candidate("android-file-provider-missing-paths-xml", targetRoot, entry.manifestPath, entry.modulePath, provider.location?.line, "FileProvider has no FILE_PROVIDER_PATHS metadata", undefined, "missing"));
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const references = [...new Set(metadata.map((item) => item.resource ?? item.value ?? ""))];
|
|
118
|
+
if (references.length > 1) {
|
|
119
|
+
candidates.push(candidate("android-file-provider-unresolved-reference", targetRoot, entry.manifestPath, entry.modulePath, metadata[0].location?.line, "Conflicting FILE_PROVIDER_PATHS metadata entries were not selected arbitrarily", references.join(";"), "unresolved"));
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const reference = references[0];
|
|
123
|
+
if (!reference || metadata.some((item) => !item.resource)) {
|
|
124
|
+
candidates.push(candidate("android-file-provider-missing-paths-xml", targetRoot, entry.manifestPath, entry.modulePath, metadata[0].location?.line, "FILE_PROVIDER_PATHS metadata has no concrete android:resource", reference, reference ? "unsupported" : "missing"));
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const resolution = resolveAndroidXmlResourceReference(targetRoot, path.join(targetRoot, entry.modulePath ?? "."), reference);
|
|
128
|
+
if (resolution.state !== "resolved") {
|
|
129
|
+
const state = resolution.state === "missing" ? "missing" : resolution.state === "malformed-reference" ? (/\$\{/.test(reference) ? "unresolved" : "malformed") : resolution.state === "unsupported-reference" ? "unsupported" : "unresolved";
|
|
130
|
+
candidates.push(candidate(resolution.state === "missing" ? "android-file-provider-missing-paths-xml" : "android-file-provider-unresolved-reference", targetRoot, entry.manifestPath, entry.modulePath, metadata[0].location?.line, `FILE_PROVIDER_PATHS resource is ${resolution.state}${resolution.state === "ambiguous" ? ` (${resolution.candidates.length} candidates)` : ""}`, reference, state));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const resource = resolution.candidates[0];
|
|
134
|
+
sources.add(resource.relativePath);
|
|
135
|
+
let xml;
|
|
136
|
+
try {
|
|
137
|
+
xml = fs.readFileSync(path.join(targetRoot, resource.relativePath), "utf8");
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
candidates.push(candidate("android-file-provider-missing-paths-xml", targetRoot, resource.relativePath, entry.modulePath, undefined, "Resolved paths XML could not be read", resource.relativePath, "missing"));
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const parsed = parseXmlDocument(xml);
|
|
144
|
+
if (!parsed.ok) {
|
|
145
|
+
candidates.push(candidate("android-file-provider-malformed-paths-xml", targetRoot, resource.relativePath, entry.modulePath, parsed.error.location.line, "FileProvider paths XML is malformed", parsed.error.message, "malformed"));
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (parsed.root.localName !== "paths") {
|
|
149
|
+
candidates.push(candidate("android-file-provider-malformed-paths-xml", targetRoot, resource.relativePath, entry.modulePath, parsed.root.location.line, `Unsupported FileProvider paths root <${parsed.root.tagName}>`, parsed.root.tagName, "unsupported"));
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (parsed.root.children.length > MAX_PATH_ENTRIES)
|
|
153
|
+
candidates.push(candidate("android-file-provider-malformed-paths-xml", targetRoot, resource.relativePath, entry.modulePath, parsed.root.location.line, `FileProvider paths XML exceeds the ${MAX_PATH_ENTRIES}-entry analysis bound`, String(parsed.root.children.length), "unsupported"));
|
|
154
|
+
const identities = new Map();
|
|
155
|
+
const exact = new Set();
|
|
156
|
+
for (const child of parsed.root.children.slice(0, MAX_PATH_ENTRIES)) {
|
|
157
|
+
const name = child.attributes.find((attribute) => attribute.localName === "name")?.value;
|
|
158
|
+
const rawPath = child.attributes.find((attribute) => attribute.localName === "path")?.value;
|
|
159
|
+
if (!PATH_TAGS.has(child.localName)) {
|
|
160
|
+
candidates.push(candidate("android-file-provider-broad-paths", targetRoot, resource.relativePath, entry.modulePath, child.location.line, "Unsupported FileProvider path element", child.tagName, "unsupported"));
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (name === undefined || name.trim() === "")
|
|
164
|
+
candidates.push(candidate("android-file-provider-broad-paths", targetRoot, resource.relativePath, entry.modulePath, child.location.line, "FileProvider path entry has a missing or empty name", name, "malformed"));
|
|
165
|
+
if (rawPath === undefined)
|
|
166
|
+
candidates.push(candidate("android-file-provider-broad-paths", targetRoot, resource.relativePath, entry.modulePath, child.location.line, "FileProvider path entry is missing path", undefined, "malformed"));
|
|
167
|
+
const normalized = (rawPath ?? "").trim().replace(/\\/g, "/").replace(/\/{2,}/g, "/").slice(0, MAX_EVIDENCE_VALUE);
|
|
168
|
+
const exactKey = `${child.localName}:${name ?? ""}:${normalized}`;
|
|
169
|
+
const prior = identities.get(name ?? "");
|
|
170
|
+
if (exact.has(exactKey))
|
|
171
|
+
candidates.push(candidate("android-file-provider-broad-paths", targetRoot, resource.relativePath, entry.modulePath, child.location.line, "Duplicate FileProvider path entry", exactKey, "unresolved"));
|
|
172
|
+
else if (name !== undefined && prior !== undefined && prior !== `${child.localName}:${normalized}`)
|
|
173
|
+
candidates.push(candidate("android-file-provider-broad-paths", targetRoot, resource.relativePath, entry.modulePath, child.location.line, "Conflicting FileProvider path entries share the same name", `${name}:${prior}:${child.localName}:${normalized}`, "unresolved"));
|
|
174
|
+
exact.add(exactKey);
|
|
175
|
+
if (name !== undefined)
|
|
176
|
+
identities.set(name, `${child.localName}:${normalized}`);
|
|
177
|
+
if (broadPath(child.localName, rawPath)) {
|
|
178
|
+
if (provider.exported === true && !library)
|
|
179
|
+
findings.push(finding("android-file-provider-broad-paths", `Exported FileProvider has broad <${child.localName}> path`, resource.relativePath, child.location.line, `${entry.modulePath ?? ""}:${provider.name}:${name ?? ""}:${normalized}`, "major"));
|
|
180
|
+
else
|
|
181
|
+
candidates.push(candidate("android-file-provider-broad-paths", targetRoot, resource.relativePath, entry.modulePath, child.location.line, `Potentially broad <${child.localName}> path`, normalized, "resolved"));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const uniqueFindings = [...new Map(findings.map((item) => [item.id, item])).values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
186
|
+
const uniqueCandidates = [...new Map(candidates.map((item) => [item.id, item])).values()];
|
|
187
|
+
return { id: ANDROID_FILE_PROVIDER_AUDIT_CHECK_ID, category: "android-file-provider", title: "Android FileProvider audit", status: uniqueFindings.length ? "failed" : "passed", requirementLevel: "required", ran: true, skipped: false, evidence: [`${sources.size} manifest/XML artifact(s) inspected`], findings: uniqueFindings, warnings: ["Static analysis only: manifests are not merged; resource overlays, runtime URI grants, installed authorities, and provider-backed filesystem access are not evaluated."], errors: [], sourcePaths: [...sources].sort((a, b) => a.localeCompare(b)), confidence: "high", environmentRequirements: [], candidateEvidence: sortCandidateEvidence(uniqueCandidates) };
|
|
188
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { auditAndroidFileProviders, ANDROID_FILE_PROVIDER_AUDIT_CHECK_ID } from "./checkResult.js";
|