@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,182 @@
|
|
|
1
|
+
import { makeAndroidFinding } from "../../audit/androidFinding.js";
|
|
2
|
+
import { makeCandidateEvidence } from "../candidateEvidence.js";
|
|
3
|
+
import { buildAndroidSourceLocation } from "../sourceLocation.js";
|
|
4
|
+
import { classifyDirectExpression } from "../sensitiveData/classifyDirectExpression.js";
|
|
5
|
+
import { classifySensitiveIdentifier } from "../sensitiveData/classifySensitiveIdentifier.js";
|
|
6
|
+
import { splitIdentifierWords } from "../sensitiveData/classifySensitiveIdentifier.js";
|
|
7
|
+
import { collectStorageEvidence } from "./collectStorageEvidence.js";
|
|
8
|
+
const SENSITIVE_FILE_NAME_WORDS = new Set([
|
|
9
|
+
"credentials",
|
|
10
|
+
"password",
|
|
11
|
+
"passwords",
|
|
12
|
+
"token",
|
|
13
|
+
"tokens",
|
|
14
|
+
"secret",
|
|
15
|
+
"secrets",
|
|
16
|
+
"session",
|
|
17
|
+
"auth",
|
|
18
|
+
"cookies",
|
|
19
|
+
"privatekey",
|
|
20
|
+
"account",
|
|
21
|
+
"userprofile",
|
|
22
|
+
]);
|
|
23
|
+
function literalOf(expression) {
|
|
24
|
+
if (expression === undefined)
|
|
25
|
+
return undefined;
|
|
26
|
+
const classification = classifyDirectExpression(expression);
|
|
27
|
+
return classification.kind === "string-literal" ? classification.literalValue : undefined;
|
|
28
|
+
}
|
|
29
|
+
function isIdentifierExpression(expression) {
|
|
30
|
+
if (expression === undefined)
|
|
31
|
+
return undefined;
|
|
32
|
+
const classification = classifyDirectExpression(expression);
|
|
33
|
+
return classification.kind === "identifier" || classification.kind === "member-access" ? expression.replace(/^.*\./, "") : undefined;
|
|
34
|
+
}
|
|
35
|
+
function fileNameLooksSensitive(pathHint) {
|
|
36
|
+
const literal = literalOf(pathHint);
|
|
37
|
+
const candidate = literal ?? pathHint;
|
|
38
|
+
if (!candidate)
|
|
39
|
+
return false;
|
|
40
|
+
const words = splitIdentifierWords(candidate.replace(/\.[a-z0-9]+$/i, ""));
|
|
41
|
+
return words.some((word) => SENSITIVE_FILE_NAME_WORDS.has(word));
|
|
42
|
+
}
|
|
43
|
+
function hasWorldMode(modeExpression) {
|
|
44
|
+
return modeExpression !== undefined && /MODE_WORLD_READABLE|MODE_WORLD_WRITEABLE/.test(modeExpression);
|
|
45
|
+
}
|
|
46
|
+
function hasPrivateMode(modeExpression) {
|
|
47
|
+
return modeExpression !== undefined && /MODE_PRIVATE/.test(modeExpression) && !hasWorldMode(modeExpression);
|
|
48
|
+
}
|
|
49
|
+
function sensitiveIdentifierFor(expression) {
|
|
50
|
+
if (expression === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
const literal = literalOf(expression);
|
|
53
|
+
if (literal !== undefined)
|
|
54
|
+
return classifySensitiveIdentifier(literal);
|
|
55
|
+
const identifier = isIdentifierExpression(expression);
|
|
56
|
+
return identifier !== undefined ? classifySensitiveIdentifier(identifier) : undefined;
|
|
57
|
+
}
|
|
58
|
+
export function analyzeSensitiveStorageFile(targetRoot, file) {
|
|
59
|
+
const candidates = [];
|
|
60
|
+
const findings = [];
|
|
61
|
+
const matches = collectStorageEvidence(file.content);
|
|
62
|
+
const location = (m) => buildAndroidSourceLocation(targetRoot, file.absolutePath, { line: m.line });
|
|
63
|
+
const limitations = ["Bounded lexical source analysis; does not evaluate runtime storage contents, filesystem permissions, or scoped-storage behavior for a specific device or API level."];
|
|
64
|
+
const finding = (ruleId, title, m, evidenceDetails, severity = "major") => findings.push(makeAndroidFinding({
|
|
65
|
+
ruleId,
|
|
66
|
+
title,
|
|
67
|
+
severity,
|
|
68
|
+
confidence: "high",
|
|
69
|
+
description: `${title}. Static source evidence only; does not prove data was stored at runtime or that another application can access it.`,
|
|
70
|
+
manifestPath: file.relativePath,
|
|
71
|
+
identity: `${m.api}:${m.line}`,
|
|
72
|
+
location: { line: m.line },
|
|
73
|
+
evidenceDetails,
|
|
74
|
+
recommendation: "Avoid storing sensitive data in plaintext; use EncryptedSharedPreferences/EncryptedFile or an equivalent platform-provided encryption wrapper.",
|
|
75
|
+
}));
|
|
76
|
+
const candidate = (ruleId, m, summary, rawValue, confidence, resolutionState = "resolved") => candidates.push(makeCandidateEvidence({
|
|
77
|
+
ruleId,
|
|
78
|
+
category: "android-sensitive-storage",
|
|
79
|
+
confidence,
|
|
80
|
+
modulePath: file.modulePath,
|
|
81
|
+
location: location(m),
|
|
82
|
+
summary,
|
|
83
|
+
rawValue,
|
|
84
|
+
resolutionState,
|
|
85
|
+
staticAnalysisLimitations: limitations,
|
|
86
|
+
}));
|
|
87
|
+
for (const m of matches) {
|
|
88
|
+
if (m.kind === "shared-preferences-get") {
|
|
89
|
+
if (hasWorldMode(m.modeExpression)) {
|
|
90
|
+
finding("android-sensitive-preferences", "SharedPreferences opened with a world-readable/world-writable mode", m, [`mode=${m.modeExpression}`]);
|
|
91
|
+
}
|
|
92
|
+
else if (hasPrivateMode(m.modeExpression)) {
|
|
93
|
+
candidate("android-sensitive-preferences", m, "SharedPreferences opened with MODE_PRIVATE (restrictive, not encryption)", m.modeExpression, "low", "not-applicable");
|
|
94
|
+
}
|
|
95
|
+
else if (m.modeExpression !== undefined && classifyDirectExpression(m.modeExpression).kind !== "string-literal") {
|
|
96
|
+
candidate("android-sensitive-preferences", m, "SharedPreferences mode is dynamic or unresolved", m.modeExpression, "low", "unresolved");
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (m.kind === "shared-preferences-write") {
|
|
101
|
+
const sensitive = sensitiveIdentifierFor(m.keyExpression);
|
|
102
|
+
if (!sensitive || m.valueExpression === undefined)
|
|
103
|
+
continue;
|
|
104
|
+
const valueClassification = classifyDirectExpression(m.valueExpression);
|
|
105
|
+
if (valueClassification.kind === "string-literal") {
|
|
106
|
+
finding("android-sensitive-preferences", `Direct ${sensitive.sensitiveDataCategory} written to plaintext SharedPreferences`, m, [`key=${m.keyExpression}`]);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
candidate("android-sensitive-preferences", m, `Sensitive key "${m.keyExpression}" written with a dynamic SharedPreferences value`, m.valueExpression, "medium", "unresolved");
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (m.kind === "datastore-write") {
|
|
114
|
+
const sensitive = sensitiveIdentifierFor(m.keyExpression);
|
|
115
|
+
if (!sensitive)
|
|
116
|
+
continue;
|
|
117
|
+
candidate("android-sensitive-preferences", m, `Sensitive DataStore key "${m.keyExpression}" written`, m.valueExpression, "medium", "unresolved");
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (m.kind === "internal-file-write") {
|
|
121
|
+
if (hasWorldMode(m.modeExpression)) {
|
|
122
|
+
finding("android-sensitive-unsafe-file-database-storage", "Internal file opened with a world-readable/world-writable mode", m, [`mode=${m.modeExpression}`]);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const nameSensitive = fileNameLooksSensitive(m.pathHint) || (m.receiver !== "(unresolved)" && Boolean(sensitiveIdentifierFor(m.receiver)));
|
|
126
|
+
const valueSensitive = sensitiveIdentifierFor(m.valueExpression);
|
|
127
|
+
if (!nameSensitive && !valueSensitive)
|
|
128
|
+
continue;
|
|
129
|
+
const valueClassification = m.valueExpression !== undefined ? classifyDirectExpression(m.valueExpression) : undefined;
|
|
130
|
+
if (valueSensitive && valueClassification?.kind === "string-literal") {
|
|
131
|
+
finding("android-sensitive-unsafe-file-database-storage", "Direct sensitive value written to a plaintext internal file", m, [`file=${m.pathHint ?? m.receiver}`]);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
candidate("android-sensitive-unsafe-file-database-storage", m, "Sensitive internal file write candidate", m.valueExpression ?? m.pathHint, "medium", "unresolved");
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (m.kind === "external-file-write") {
|
|
139
|
+
const isPublic = /getExternalStorageDirectory|getExternalStoragePublicDirectory/.test(m.api);
|
|
140
|
+
const isCache = /ExternalCacheDir/.test(m.api);
|
|
141
|
+
const nameSensitive = fileNameLooksSensitive(m.pathHint);
|
|
142
|
+
if (!nameSensitive)
|
|
143
|
+
continue;
|
|
144
|
+
if (isPublic) {
|
|
145
|
+
finding("android-sensitive-external-storage", "Sensitive-looking path written to shared public external storage", m, [`path=${m.pathHint}`]);
|
|
146
|
+
}
|
|
147
|
+
else if (isCache) {
|
|
148
|
+
candidate("android-sensitive-external-storage", m, "Sensitive-looking path written to external cache", m.pathHint, "medium");
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
candidate("android-sensitive-external-storage", m, "Sensitive-looking path written to app-specific external storage", m.pathHint, "medium");
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (m.kind === "database-write") {
|
|
156
|
+
if (m.api === "ContentValues.put") {
|
|
157
|
+
const sensitive = sensitiveIdentifierFor(m.keyExpression);
|
|
158
|
+
if (!sensitive)
|
|
159
|
+
continue;
|
|
160
|
+
const valueClassification = m.valueExpression !== undefined ? classifyDirectExpression(m.valueExpression) : undefined;
|
|
161
|
+
if (valueClassification?.kind === "string-literal") {
|
|
162
|
+
finding("android-sensitive-unsafe-file-database-storage", "Direct sensitive value inserted into a local database column", m, [`column=${m.keyExpression}`]);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
candidate("android-sensitive-unsafe-file-database-storage", m, `Sensitive database column "${m.keyExpression}" candidate`, m.valueExpression, "medium", "unresolved");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (m.api === "execSQL" && m.valueExpression !== undefined) {
|
|
169
|
+
const words = splitIdentifierWords(m.valueExpression);
|
|
170
|
+
if (words.some((word) => classifySensitiveIdentifier(word))) {
|
|
171
|
+
candidate("android-sensitive-unsafe-file-database-storage", m, "Sensitive-looking column name referenced in a raw SQL statement", m.valueExpression, "low");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (m.kind === "encrypted-storage-reference") {
|
|
177
|
+
candidate("android-sensitive-preferences", m, `Protective encrypted-storage API referenced: ${m.api}`, m.api, "low", "not-applicable");
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { candidates, findings };
|
|
182
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { sortCandidateEvidence } from "../ordering.js";
|
|
2
|
+
import { discoverSecretSourceFiles } from "../secretCandidates/discoverSecretSourceFiles.js";
|
|
3
|
+
import { analyzeSensitiveStorageFile } from "./analyzeSensitiveStorage.js";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// v0.4.1 Batch 6 — standalone Android sensitive-storage check result.
|
|
6
|
+
// Follows the exact Batch 4/5 standalone pattern: not called from
|
|
7
|
+
// validateAndroidTarget / any active orchestration, not rendered in reports,
|
|
8
|
+
// no CLI effect. Batch 8 integrates.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
export const ANDROID_SENSITIVE_STORAGE_AUDIT_CHECK_ID = "android-sensitive-storage-audit";
|
|
11
|
+
export function auditAndroidSensitiveStorage(targetRoot, detection) {
|
|
12
|
+
if (detection.projectKind === "non-android") {
|
|
13
|
+
return {
|
|
14
|
+
id: ANDROID_SENSITIVE_STORAGE_AUDIT_CHECK_ID,
|
|
15
|
+
category: "android-sensitive-storage",
|
|
16
|
+
title: "Android sensitive-storage audit",
|
|
17
|
+
status: "unsupported",
|
|
18
|
+
requirementLevel: "optional",
|
|
19
|
+
ran: false,
|
|
20
|
+
skipped: true,
|
|
21
|
+
skipInfo: {
|
|
22
|
+
checkId: ANDROID_SENSITIVE_STORAGE_AUDIT_CHECK_ID,
|
|
23
|
+
reason: "Target was not detected as an Android project.",
|
|
24
|
+
requirementLevel: "optional",
|
|
25
|
+
missingCapability: "android-detection",
|
|
26
|
+
verdictImpact: "does not apply to a non-Android target",
|
|
27
|
+
recommendedNextAction: "Re-run against an Android Gradle project, or verify project detection.",
|
|
28
|
+
},
|
|
29
|
+
evidence: [],
|
|
30
|
+
findings: [],
|
|
31
|
+
warnings: [],
|
|
32
|
+
errors: [],
|
|
33
|
+
sourcePaths: [],
|
|
34
|
+
confidence: "unknown",
|
|
35
|
+
environmentRequirements: [],
|
|
36
|
+
candidateEvidence: [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const modulePaths = detection.modules.map((m) => m.path);
|
|
40
|
+
const { files, skipped } = discoverSecretSourceFiles(targetRoot, modulePaths);
|
|
41
|
+
const sourceFiles = files.filter((file) => /\.(java|kt)$/.test(file.relativePath));
|
|
42
|
+
const findingsById = new Map();
|
|
43
|
+
const candidatesById = new Map();
|
|
44
|
+
for (const file of sourceFiles) {
|
|
45
|
+
const result = analyzeSensitiveStorageFile(targetRoot, file);
|
|
46
|
+
for (const f of result.findings)
|
|
47
|
+
findingsById.set(f.id, f);
|
|
48
|
+
for (const c of result.candidates)
|
|
49
|
+
candidatesById.set(c.id, c);
|
|
50
|
+
}
|
|
51
|
+
const findings = [...findingsById.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
52
|
+
const candidateEvidence = sortCandidateEvidence([...candidatesById.values()]);
|
|
53
|
+
const hasReportableFinding = findings.some((f) => f.severity !== "informational");
|
|
54
|
+
return {
|
|
55
|
+
id: ANDROID_SENSITIVE_STORAGE_AUDIT_CHECK_ID,
|
|
56
|
+
category: "android-sensitive-storage",
|
|
57
|
+
title: "Android sensitive-storage audit",
|
|
58
|
+
status: hasReportableFinding ? "failed" : "passed",
|
|
59
|
+
requirementLevel: "required",
|
|
60
|
+
ran: true,
|
|
61
|
+
skipped: false,
|
|
62
|
+
evidence: [`${sourceFiles.length} Java/Kotlin file(s) scanned`],
|
|
63
|
+
findings,
|
|
64
|
+
warnings: [
|
|
65
|
+
...skipped.map((s) => `${s.relativePath}: skipped (${s.reason}${s.detail ? `: ${s.detail}` : ""})`),
|
|
66
|
+
"Static analysis only: does not inspect runtime storage contents, filesystem permissions, or database records.",
|
|
67
|
+
],
|
|
68
|
+
errors: [],
|
|
69
|
+
sourcePaths: sourceFiles.map((f) => f.relativePath),
|
|
70
|
+
confidence: skipped.length > 0 ? "medium" : "high",
|
|
71
|
+
environmentRequirements: [],
|
|
72
|
+
candidateEvidence,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { executableMask, lineForOffset } from "../sensitiveData/localSourceContext.js";
|
|
2
|
+
import { splitTopLevelArguments } from "../sensitiveData/splitArguments.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// v0.4.1 Batch 6 — bounded direct evidence collection for sensitive-storage
|
|
5
|
+
// APIs (SharedPreferences, DataStore, internal/external file writes,
|
|
6
|
+
// database/ContentValues writes, and encrypted-storage references).
|
|
7
|
+
//
|
|
8
|
+
// Purely lexical: call shapes are matched over comment/string-masked text
|
|
9
|
+
// (so nothing inside a comment or string literal is matched as executable
|
|
10
|
+
// code), but every captured group offset is then re-sliced out of the
|
|
11
|
+
// ORIGINAL, unmasked content (via regex match "indices", the `d` flag) —
|
|
12
|
+
// masked text has string-literal interiors blanked out, so reading argument
|
|
13
|
+
// text directly from a masked capture group would silently lose every
|
|
14
|
+
// string-literal key/value. Nothing in this file constructs CandidateEvidence
|
|
15
|
+
// or SecurityFinding, and nothing here is exported outside sensitiveStorage/.
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
const PUT_CALL = /([A-Za-z_][\w.]*)\.(?:edit\(\)\.)?(putString|putStringSet|putInt|putLong|putFloat|putBoolean|remove|clear|apply|commit)\s*\(([^;\n]*?)\)/gd;
|
|
18
|
+
const GET_PREFS = /(?:([A-Za-z_][\w.]*)\.)?(getSharedPreferences|PreferenceManager\.getDefaultSharedPreferences)\s*\(([^;\n]*?)\)/gd;
|
|
19
|
+
const DATASTORE_EDIT = /([A-Za-z_][\w.]*)\[\s*([A-Za-z_][\w.]*)\s*\]\s*=\s*([^;\n]+)/gd;
|
|
20
|
+
const INTERNAL_FILE_WRITE = /(?:([A-Za-z_][\w.]*)\.)?(openFileOutput|writeText|appendText)\s*\(([^;\n]*?)\)/gd;
|
|
21
|
+
const FILE_STREAM_CTOR = /new\s+(FileOutputStream|FileWriter|BufferedWriter)\s*\(([^;\n]*?)\)/gd;
|
|
22
|
+
const EXTERNAL_PATH_API = /(Environment\.getExternalStorageDirectory|Environment\.getExternalStoragePublicDirectory|[A-Za-z_][\w.]*\.getExternalFilesDir|[A-Za-z_][\w.]*\.externalFilesDir|[A-Za-z_][\w.]*\.getExternalCacheDir|[A-Za-z_][\w.]*\.externalCacheDir)\s*\(?([^;\n)]*)\)?/gd;
|
|
23
|
+
const CONTENT_VALUES_PUT = /([A-Za-z_][\w.]*)\.put\s*\(([^;\n]*?)\)/gd;
|
|
24
|
+
const EXEC_SQL = /([A-Za-z_][\w.]*)\.execSQL\s*\(([^;\n]*?)\)/gd;
|
|
25
|
+
const ENCRYPTED_STORAGE_REFERENCE = /\b(EncryptedSharedPreferences|EncryptedFile|MasterKey|AndroidKeyStore|SQLCipher)\b/gd;
|
|
26
|
+
function realGroup(content, match, group) {
|
|
27
|
+
const span = match.indices?.[group];
|
|
28
|
+
if (!span)
|
|
29
|
+
return match[group];
|
|
30
|
+
return content.slice(span[0], span[1]);
|
|
31
|
+
}
|
|
32
|
+
function pushMatch(list, content, kind, api, offset, extra) {
|
|
33
|
+
list.push({ kind, api, receiver: extra.receiver ?? "(unresolved)", offset, line: lineForOffset(content, offset), ...extra });
|
|
34
|
+
}
|
|
35
|
+
export function collectStorageEvidence(content) {
|
|
36
|
+
const mask = executableMask(content);
|
|
37
|
+
const matches = [];
|
|
38
|
+
for (const match of mask.matchAll(PUT_CALL)) {
|
|
39
|
+
const offset = match.index ?? 0;
|
|
40
|
+
const args = splitTopLevelArguments(realGroup(content, match, 3) ?? "");
|
|
41
|
+
pushMatch(matches, content, "shared-preferences-write", `SharedPreferences.Editor.${match[2]}`, offset, {
|
|
42
|
+
receiver: match[1],
|
|
43
|
+
keyExpression: args[0],
|
|
44
|
+
valueExpression: args[1],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
for (const match of mask.matchAll(GET_PREFS)) {
|
|
48
|
+
const offset = match.index ?? 0;
|
|
49
|
+
const args = splitTopLevelArguments(realGroup(content, match, 3) ?? "");
|
|
50
|
+
pushMatch(matches, content, "shared-preferences-get", match[2], offset, {
|
|
51
|
+
receiver: match[1] ?? match[2],
|
|
52
|
+
pathHint: args[0],
|
|
53
|
+
modeExpression: args[1],
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
for (const match of mask.matchAll(DATASTORE_EDIT)) {
|
|
57
|
+
const offset = match.index ?? 0;
|
|
58
|
+
pushMatch(matches, content, "datastore-write", "DataStore.edit[key]=", offset, {
|
|
59
|
+
receiver: match[1],
|
|
60
|
+
keyExpression: match[2],
|
|
61
|
+
valueExpression: realGroup(content, match, 3),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
for (const match of mask.matchAll(INTERNAL_FILE_WRITE)) {
|
|
65
|
+
const offset = match.index ?? 0;
|
|
66
|
+
const args = splitTopLevelArguments(realGroup(content, match, 3) ?? "");
|
|
67
|
+
pushMatch(matches, content, "internal-file-write", match[2], offset, {
|
|
68
|
+
receiver: match[1],
|
|
69
|
+
pathHint: match[2] === "openFileOutput" ? args[0] : undefined,
|
|
70
|
+
modeExpression: match[2] === "openFileOutput" ? args[1] : undefined,
|
|
71
|
+
valueExpression: match[2] !== "openFileOutput" ? args[0] : undefined,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
for (const match of mask.matchAll(FILE_STREAM_CTOR)) {
|
|
75
|
+
const offset = match.index ?? 0;
|
|
76
|
+
const args = splitTopLevelArguments(realGroup(content, match, 2) ?? "");
|
|
77
|
+
pushMatch(matches, content, "internal-file-write", `new ${match[1]}`, offset, {
|
|
78
|
+
pathHint: args[0],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
for (const match of mask.matchAll(EXTERNAL_PATH_API)) {
|
|
82
|
+
const offset = match.index ?? 0;
|
|
83
|
+
pushMatch(matches, content, "external-file-write", match[1], offset, {
|
|
84
|
+
pathHint: realGroup(content, match, 2),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
for (const match of mask.matchAll(CONTENT_VALUES_PUT)) {
|
|
88
|
+
const offset = match.index ?? 0;
|
|
89
|
+
const args = splitTopLevelArguments(realGroup(content, match, 2) ?? "");
|
|
90
|
+
pushMatch(matches, content, "database-write", "ContentValues.put", offset, {
|
|
91
|
+
receiver: match[1],
|
|
92
|
+
keyExpression: args[0],
|
|
93
|
+
valueExpression: args[1],
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
for (const match of mask.matchAll(EXEC_SQL)) {
|
|
97
|
+
const offset = match.index ?? 0;
|
|
98
|
+
const args = splitTopLevelArguments(realGroup(content, match, 2) ?? "");
|
|
99
|
+
pushMatch(matches, content, "database-write", "execSQL", offset, {
|
|
100
|
+
receiver: match[1],
|
|
101
|
+
valueExpression: args[0],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
for (const match of mask.matchAll(ENCRYPTED_STORAGE_REFERENCE)) {
|
|
105
|
+
const offset = match.index ?? 0;
|
|
106
|
+
pushMatch(matches, content, "encrypted-storage-reference", match[1], offset, {});
|
|
107
|
+
}
|
|
108
|
+
return matches.sort((a, b) => a.offset - b.offset);
|
|
109
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { auditAndroidSensitiveStorage, ANDROID_SENSITIVE_STORAGE_AUDIT_CHECK_ID } from "./checkResult.js";
|
package/dist/src/mobile/android/advancedSecurity/signingConfiguration/analyzeSigningConfiguration.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { makeAndroidFinding } from "../../audit/androidFinding.js";
|
|
4
|
+
import { buildAndroidSourceLocation } from "../sourceLocation.js";
|
|
5
|
+
import { makeCandidateEvidence } from "../candidateEvidence.js";
|
|
6
|
+
import { extractSigningConfigurations } from "./extractSigningConfigurations.js";
|
|
7
|
+
function loc(targetRoot, absolutePath, position) {
|
|
8
|
+
return buildAndroidSourceLocation(targetRoot, absolutePath, position);
|
|
9
|
+
}
|
|
10
|
+
function analyzeCredential(fieldLabel, value, configName, modulePath, location, buildFileRelativePath) {
|
|
11
|
+
if (value.state === "literal" && value.fingerprint !== undefined) {
|
|
12
|
+
return {
|
|
13
|
+
candidates: [],
|
|
14
|
+
findings: [
|
|
15
|
+
makeAndroidFinding({
|
|
16
|
+
ruleId: "android-signing-password-literal",
|
|
17
|
+
title: `Literal ${fieldLabel} in signing configuration "${configName}"`,
|
|
18
|
+
severity: "major",
|
|
19
|
+
confidence: "high",
|
|
20
|
+
description: `The signing configuration "${configName}" sets a literal ${fieldLabel} value. This is high-confidence static evidence of a hardcoded signing credential; it is not proof the credential unlocks any specific keystore.`,
|
|
21
|
+
manifestPath: buildFileRelativePath,
|
|
22
|
+
identity: value.fingerprint,
|
|
23
|
+
location: { line: location.line },
|
|
24
|
+
evidenceDetails: [`signingConfig=${configName}`, `field=${fieldLabel}`, `redactedPreview=${value.redactedPreview}`],
|
|
25
|
+
recommendation: "Remove the literal credential and load it from an environment variable or a local, untracked properties file instead.",
|
|
26
|
+
}),
|
|
27
|
+
],
|
|
28
|
+
evidenceText: [],
|
|
29
|
+
hasLiteral: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (value.state === "environment-reference" || value.state === "gradle-property-reference" || value.state === "local-property-reference") {
|
|
33
|
+
return {
|
|
34
|
+
candidates: [],
|
|
35
|
+
findings: [],
|
|
36
|
+
evidenceText: [`${configName}.${fieldLabel} is a ${value.state} (informational, not a hardcoded credential)`],
|
|
37
|
+
hasLiteral: false,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (value.state === "variable-reference" || value.state === "method-call" || value.state === "dynamic") {
|
|
41
|
+
return {
|
|
42
|
+
candidates: [
|
|
43
|
+
makeCandidateEvidence({
|
|
44
|
+
ruleId: "android-signing-password-literal",
|
|
45
|
+
category: "android-signing-configuration",
|
|
46
|
+
confidence: "low",
|
|
47
|
+
modulePath,
|
|
48
|
+
location,
|
|
49
|
+
summary: `Signing configuration "${configName}"'s ${fieldLabel} is a dynamic/unresolved expression`,
|
|
50
|
+
rawValue: value.rawExpression,
|
|
51
|
+
resolutionState: "unresolved",
|
|
52
|
+
}),
|
|
53
|
+
],
|
|
54
|
+
findings: [],
|
|
55
|
+
evidenceText: [],
|
|
56
|
+
hasLiteral: false,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return { candidates: [], findings: [], evidenceText: [], hasLiteral: false };
|
|
60
|
+
}
|
|
61
|
+
function analyzeStoreFile(targetRoot, config, modulePath, moduleDirRelative, location) {
|
|
62
|
+
const { storeFile } = config;
|
|
63
|
+
if (storeFile.state === "missing")
|
|
64
|
+
return { candidates: [], evidenceText: [`${config.name}.storeFile is not set`] };
|
|
65
|
+
if (storeFile.state !== "literal" || storeFile.literalValue === undefined) {
|
|
66
|
+
return {
|
|
67
|
+
candidates: [
|
|
68
|
+
makeCandidateEvidence({
|
|
69
|
+
ruleId: "android-signing-keystore-candidate",
|
|
70
|
+
category: "android-signing-configuration",
|
|
71
|
+
confidence: "low",
|
|
72
|
+
modulePath,
|
|
73
|
+
location,
|
|
74
|
+
summary: `Signing configuration "${config.name}"'s storeFile is a dynamic/unresolved expression`,
|
|
75
|
+
rawValue: storeFile.rawExpression,
|
|
76
|
+
resolutionState: "unresolved",
|
|
77
|
+
}),
|
|
78
|
+
],
|
|
79
|
+
evidenceText: [],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const literalPath = storeFile.literalValue;
|
|
83
|
+
if (path.isAbsolute(literalPath)) {
|
|
84
|
+
return {
|
|
85
|
+
candidates: [
|
|
86
|
+
makeCandidateEvidence({
|
|
87
|
+
ruleId: "android-signing-path-leakage",
|
|
88
|
+
category: "android-signing-configuration",
|
|
89
|
+
confidence: "medium",
|
|
90
|
+
modulePath,
|
|
91
|
+
location,
|
|
92
|
+
summary: `Signing configuration "${config.name}" references an absolute local storeFile path`,
|
|
93
|
+
rawValue: literalPath,
|
|
94
|
+
resolutionState: "resolved",
|
|
95
|
+
staticAnalysisLimitations: ["The absolute path may reveal local workstation directory structure; it was not resolved or opened."],
|
|
96
|
+
}),
|
|
97
|
+
],
|
|
98
|
+
evidenceText: [],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const resolvedAbsolute = path.join(targetRoot, moduleDirRelative, literalPath);
|
|
102
|
+
const exists = fileExistsSafely(resolvedAbsolute);
|
|
103
|
+
if (!exists) {
|
|
104
|
+
return {
|
|
105
|
+
candidates: [
|
|
106
|
+
makeCandidateEvidence({
|
|
107
|
+
ruleId: "android-signing-keystore-candidate",
|
|
108
|
+
category: "android-signing-configuration",
|
|
109
|
+
confidence: "low",
|
|
110
|
+
modulePath,
|
|
111
|
+
location,
|
|
112
|
+
summary: `Signing configuration "${config.name}" references a keystore file that was not found in the target`,
|
|
113
|
+
rawValue: literalPath,
|
|
114
|
+
resolutionState: "missing",
|
|
115
|
+
staticAnalysisLimitations: ["A missing referenced keystore is review evidence, not proof that signing is broken."],
|
|
116
|
+
}),
|
|
117
|
+
],
|
|
118
|
+
evidenceText: [],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return { candidates: [], evidenceText: [`${config.name}.storeFile resolves to a target-contained file: ${literalPath}`] };
|
|
122
|
+
}
|
|
123
|
+
function fileExistsSafely(absolutePath) {
|
|
124
|
+
try {
|
|
125
|
+
return fs.existsSync(absolutePath);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function extractReleaseSigningConfigName(signingConfigRef) {
|
|
132
|
+
const getByNameMatch = signingConfigRef.match(/signingConfigs\.getByName\(\s*["'](\w[\w-]*)["']\s*\)/);
|
|
133
|
+
if (getByNameMatch)
|
|
134
|
+
return getByNameMatch[1];
|
|
135
|
+
const dotMatch = signingConfigRef.match(/signingConfigs\.(\w[\w-]*)/);
|
|
136
|
+
if (dotMatch)
|
|
137
|
+
return dotMatch[1];
|
|
138
|
+
const namedMatch = signingConfigRef.match(/signingConfigs\.named\(\s*["'](\w[\w-]*)["']\s*\)/);
|
|
139
|
+
if (namedMatch)
|
|
140
|
+
return namedMatch[1];
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
export function analyzeModuleSigningConfiguration(targetRoot, gradleModule, buildFileText, allKeystoreCandidates) {
|
|
144
|
+
const candidates = [];
|
|
145
|
+
const findings = [];
|
|
146
|
+
const evidenceText = [];
|
|
147
|
+
const modulePath = gradleModule.path;
|
|
148
|
+
const isLibraryOnlyModule = gradleModule.isLibrary === true && gradleModule.isApplication !== true;
|
|
149
|
+
const buildFileRelativePath = gradleModule.buildFilePath ?? modulePath;
|
|
150
|
+
const buildFileAbsolutePath = path.join(targetRoot, buildFileRelativePath);
|
|
151
|
+
const location = loc(targetRoot, buildFileAbsolutePath);
|
|
152
|
+
const signingConfigs = extractSigningConfigurations(buildFileText);
|
|
153
|
+
const literalCredentialConfigNames = new Set();
|
|
154
|
+
for (const config of signingConfigs) {
|
|
155
|
+
const storePasswordResult = analyzeCredential("storePassword", config.storePassword, config.name, modulePath, location, buildFileRelativePath);
|
|
156
|
+
const keyPasswordResult = analyzeCredential("keyPassword", config.keyPassword, config.name, modulePath, location, buildFileRelativePath);
|
|
157
|
+
const storeFileResult = analyzeStoreFile(targetRoot, config, modulePath, modulePath, location);
|
|
158
|
+
if (storePasswordResult.hasLiteral || keyPasswordResult.hasLiteral)
|
|
159
|
+
literalCredentialConfigNames.add(config.name);
|
|
160
|
+
if (isLibraryOnlyModule) {
|
|
161
|
+
// Downgrade findings to candidates for library-only modules — never
|
|
162
|
+
// suppress the evidence entirely, just its release-signing weight.
|
|
163
|
+
for (const finding of [...storePasswordResult.findings, ...keyPasswordResult.findings]) {
|
|
164
|
+
candidates.push(makeCandidateEvidence({
|
|
165
|
+
ruleId: finding.id.split("--")[0],
|
|
166
|
+
category: "android-signing-configuration",
|
|
167
|
+
confidence: "low",
|
|
168
|
+
modulePath,
|
|
169
|
+
location,
|
|
170
|
+
summary: `${finding.title} (library module — not an installable application)`,
|
|
171
|
+
rawValue: undefined,
|
|
172
|
+
resolutionState: "not-applicable",
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
findings.push(...storePasswordResult.findings, ...keyPasswordResult.findings);
|
|
178
|
+
}
|
|
179
|
+
candidates.push(...storePasswordResult.candidates, ...keyPasswordResult.candidates, ...storeFileResult.candidates);
|
|
180
|
+
evidenceText.push(...storePasswordResult.evidenceText, ...keyPasswordResult.evidenceText, ...storeFileResult.evidenceText);
|
|
181
|
+
// keyAlias is metadata only — never a finding/candidate by itself.
|
|
182
|
+
if (config.keyAlias.state === "literal") {
|
|
183
|
+
evidenceText.push(`${config.name}.keyAlias=${config.keyAlias.literalValue}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// --- release build-type <-> signingConfig correlation ---
|
|
187
|
+
const releaseBuildTypes = (gradleModule.buildTypeDetails ?? []).filter((bt) => bt.name === "release");
|
|
188
|
+
for (const buildType of releaseBuildTypes) {
|
|
189
|
+
if (buildType.signingConfigRef === undefined)
|
|
190
|
+
continue;
|
|
191
|
+
const referencedName = extractReleaseSigningConfigName(buildType.signingConfigRef);
|
|
192
|
+
if (referencedName === undefined) {
|
|
193
|
+
candidates.push(makeCandidateEvidence({
|
|
194
|
+
ruleId: "android-signing-debug-in-release",
|
|
195
|
+
category: "android-signing-configuration",
|
|
196
|
+
confidence: "low",
|
|
197
|
+
modulePath,
|
|
198
|
+
location,
|
|
199
|
+
summary: `Release build type's signingConfig is a dynamic/unresolved expression: ${buildType.signingConfigRef}`,
|
|
200
|
+
rawValue: buildType.signingConfigRef,
|
|
201
|
+
resolutionState: "unresolved",
|
|
202
|
+
}));
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (referencedName === "debug") {
|
|
206
|
+
const debugFinding = makeAndroidFinding({
|
|
207
|
+
ruleId: "android-signing-debug-in-release",
|
|
208
|
+
title: 'Release build type explicitly uses signingConfigs.debug',
|
|
209
|
+
severity: "major",
|
|
210
|
+
confidence: "high",
|
|
211
|
+
description: "The release build type's signingConfig references signingConfigs.debug. This is high-confidence risky release-configuration evidence; it does not prove a shipped APK was actually signed with the debug key (variant/flavor selection is not evaluated).",
|
|
212
|
+
manifestPath: buildFileRelativePath,
|
|
213
|
+
identity: "release-debug-signing",
|
|
214
|
+
evidenceDetails: [`buildType=release`, `signingConfigRef=${buildType.signingConfigRef}`],
|
|
215
|
+
recommendation: "Configure a dedicated release signing configuration and remove the reference to signingConfigs.debug.",
|
|
216
|
+
});
|
|
217
|
+
if (isLibraryOnlyModule) {
|
|
218
|
+
candidates.push(makeCandidateEvidence({
|
|
219
|
+
ruleId: "android-signing-debug-in-release",
|
|
220
|
+
category: "android-signing-configuration",
|
|
221
|
+
confidence: "low",
|
|
222
|
+
modulePath,
|
|
223
|
+
location,
|
|
224
|
+
summary: `${debugFinding.title} (library module — not an installable application)`,
|
|
225
|
+
rawValue: undefined,
|
|
226
|
+
resolutionState: "not-applicable",
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
findings.push(debugFinding);
|
|
231
|
+
}
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const referencedConfigExists = signingConfigs.some((c) => c.name === referencedName);
|
|
235
|
+
if (!referencedConfigExists) {
|
|
236
|
+
candidates.push(makeCandidateEvidence({
|
|
237
|
+
ruleId: "android-signing-debug-in-release",
|
|
238
|
+
category: "android-signing-configuration",
|
|
239
|
+
confidence: "low",
|
|
240
|
+
modulePath,
|
|
241
|
+
location,
|
|
242
|
+
summary: `Release build type references signing config "${referencedName}", which has no matching definition in this build file`,
|
|
243
|
+
rawValue: referencedName,
|
|
244
|
+
resolutionState: "missing",
|
|
245
|
+
}));
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
evidenceText.push(`release build type references named signing config "${referencedName}" (ordinary metadata)`);
|
|
249
|
+
// --- committed-keystore + literal-credential + release correlation ---
|
|
250
|
+
if (literalCredentialConfigNames.has(referencedName)) {
|
|
251
|
+
const referencedConfig = signingConfigs.find((c) => c.name === referencedName);
|
|
252
|
+
const storeFileName = referencedConfig.storeFile.state === "literal" ? path.basename(referencedConfig.storeFile.literalValue ?? "") : undefined;
|
|
253
|
+
const committedKeystore = storeFileName ? allKeystoreCandidates.find((k) => path.basename(k.relativePath) === storeFileName) : undefined;
|
|
254
|
+
if (committedKeystore && !isLibraryOnlyModule) {
|
|
255
|
+
findings.push(makeAndroidFinding({
|
|
256
|
+
ruleId: "android-signing-keystore-candidate",
|
|
257
|
+
title: `Release signing configuration "${referencedName}" correlates with a committed keystore and a literal credential`,
|
|
258
|
+
severity: "major",
|
|
259
|
+
confidence: "high",
|
|
260
|
+
description: `The release build type references signing configuration "${referencedName}", which has a literal password and a matching committed keystore file (${committedKeystore.relativePath}) in the target. This is a high-confidence aligned static signal; it does not prove the credential is valid or that this keystore is used in a published artifact.`,
|
|
261
|
+
manifestPath: committedKeystore.relativePath,
|
|
262
|
+
identity: `${referencedName}-committed-keystore-correlation`,
|
|
263
|
+
evidenceDetails: [`signingConfig=${referencedName}`, `keystore=${committedKeystore.relativePath}`],
|
|
264
|
+
recommendation: "Remove the committed keystore and credential from source control; use a secure, out-of-repo signing mechanism (e.g. CI secret store).",
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return { candidates, findings, evidenceText };
|
|
270
|
+
}
|