@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,166 @@
|
|
|
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, splitIdentifierWords } from "../sensitiveData/classifySensitiveIdentifier.js";
|
|
6
|
+
import { executableMask } from "../sensitiveData/localSourceContext.js";
|
|
7
|
+
import { splitTopLevelArguments } from "../sensitiveData/splitArguments.js";
|
|
8
|
+
import { collectClipboardEvidence } from "./collectClipboardEvidence.js";
|
|
9
|
+
const USER_TRIGGER_METHOD_NAMES = /^(onClick|onLongClick|onMenuItemClick|copy|copyToClipboard|handleCopy|share)$/i;
|
|
10
|
+
const BACKGROUND_LIKE_CLASS_SUFFIX = /(Service|Receiver|Worker)$/;
|
|
11
|
+
function literalOf(expression) {
|
|
12
|
+
if (expression === undefined)
|
|
13
|
+
return undefined;
|
|
14
|
+
const classification = classifyDirectExpression(expression);
|
|
15
|
+
return classification.kind === "string-literal" ? classification.literalValue : undefined;
|
|
16
|
+
}
|
|
17
|
+
function labelSensitive(labelExpression) {
|
|
18
|
+
if (labelExpression === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
const literal = literalOf(labelExpression);
|
|
21
|
+
if (literal !== undefined) {
|
|
22
|
+
const words = splitIdentifierWords(literal);
|
|
23
|
+
for (const word of words) {
|
|
24
|
+
const result = classifySensitiveIdentifier(word);
|
|
25
|
+
if (result)
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const identifier = classifyDirectExpression(labelExpression).kind === "identifier" ? labelExpression : undefined;
|
|
31
|
+
return identifier ? classifySensitiveIdentifier(identifier) : undefined;
|
|
32
|
+
}
|
|
33
|
+
function valueSensitive(valueExpression) {
|
|
34
|
+
if (valueExpression === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
const classification = classifyDirectExpression(valueExpression);
|
|
37
|
+
if (classification.kind !== "identifier" && classification.kind !== "member-access")
|
|
38
|
+
return undefined;
|
|
39
|
+
const name = valueExpression.replace(/^.*\./, "");
|
|
40
|
+
return classifySensitiveIdentifier(name);
|
|
41
|
+
}
|
|
42
|
+
function nearestBefore(mask, offset, pattern) {
|
|
43
|
+
const window = mask.slice(Math.max(0, offset - 4000), offset);
|
|
44
|
+
let last = null;
|
|
45
|
+
let match;
|
|
46
|
+
const re = new RegExp(pattern.source, "g");
|
|
47
|
+
while ((match = re.exec(window)) !== null)
|
|
48
|
+
last = match;
|
|
49
|
+
return last?.[1];
|
|
50
|
+
}
|
|
51
|
+
export function analyzeClipboardSecurityFile(targetRoot, file) {
|
|
52
|
+
const candidates = [];
|
|
53
|
+
const findings = [];
|
|
54
|
+
const mask = executableMask(file.content);
|
|
55
|
+
const matches = collectClipboardEvidence(file.content);
|
|
56
|
+
const creates = matches.filter((m) => m.kind === "clip-create");
|
|
57
|
+
const markerOffsets = matches.filter((m) => m.kind === "sensitive-marker").map((m) => m.offset);
|
|
58
|
+
const clearOffsetsByScope = new Map();
|
|
59
|
+
for (const m of matches.filter((m) => m.kind === "clip-clear")) {
|
|
60
|
+
const list = clearOffsetsByScope.get(m.scopeId) ?? [];
|
|
61
|
+
list.push(m.offset);
|
|
62
|
+
clearOffsetsByScope.set(m.scopeId, list);
|
|
63
|
+
}
|
|
64
|
+
const limitations = [
|
|
65
|
+
"Bounded lexical source analysis; does not prove clipboard content was actually set or read, that another application accessed the clipboard, or that a UI callback executed.",
|
|
66
|
+
];
|
|
67
|
+
const location = (m) => buildAndroidSourceLocation(targetRoot, file.absolutePath, { line: m.line });
|
|
68
|
+
const finding = (m, title, evidenceDetails) => findings.push(makeAndroidFinding({
|
|
69
|
+
ruleId: "android-sensitive-clipboard-write",
|
|
70
|
+
title,
|
|
71
|
+
severity: "major",
|
|
72
|
+
confidence: "high",
|
|
73
|
+
description: `${title} via ${m.api}. Static source evidence only; does not prove clipboard content was set at runtime or read by another application.`,
|
|
74
|
+
manifestPath: file.relativePath,
|
|
75
|
+
identity: `${m.api}:${m.line}`,
|
|
76
|
+
location: { line: m.line },
|
|
77
|
+
evidenceDetails,
|
|
78
|
+
recommendation: "Avoid copying credentials or recovery codes to the system clipboard; mark unavoidable sensitive clips with ClipDescription.EXTRA_IS_SENSITIVE and clear them promptly.",
|
|
79
|
+
}));
|
|
80
|
+
const candidate = (ruleId, m, summary, rawValue, confidence = "medium", resolutionState = "resolved") => candidates.push(makeCandidateEvidence({
|
|
81
|
+
ruleId,
|
|
82
|
+
category: "android-clipboard",
|
|
83
|
+
confidence,
|
|
84
|
+
modulePath: file.modulePath,
|
|
85
|
+
location: location(m),
|
|
86
|
+
summary,
|
|
87
|
+
rawValue,
|
|
88
|
+
resolutionState,
|
|
89
|
+
staticAnalysisLimitations: limitations,
|
|
90
|
+
}));
|
|
91
|
+
function resolveClip(argumentExpression, scopeId) {
|
|
92
|
+
if (argumentExpression === undefined)
|
|
93
|
+
return undefined;
|
|
94
|
+
const inline = /^ClipData\.(?:newPlainText|newHtmlText|newUri)\(([\s\S]*)\)$/.exec(argumentExpression.trim());
|
|
95
|
+
if (inline) {
|
|
96
|
+
const args = splitTopLevelArguments(inline[1]);
|
|
97
|
+
return { label: args[0], value: args[1], markerNearby: false };
|
|
98
|
+
}
|
|
99
|
+
const alias = classifyDirectExpression(argumentExpression).kind === "identifier" ? argumentExpression : undefined;
|
|
100
|
+
if (alias !== undefined) {
|
|
101
|
+
const source = creates.find((c) => c.assignedVariable === alias && c.scopeId === scopeId);
|
|
102
|
+
if (source)
|
|
103
|
+
return { label: source.labelExpression, value: source.valueExpression, markerNearby: false };
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
for (const m of matches) {
|
|
108
|
+
if (m.kind === "clip-set") {
|
|
109
|
+
const resolved = resolveClip(m.argumentExpression, m.scopeId);
|
|
110
|
+
const nearMarker = markerOffsets.some((offset) => Math.abs(offset - m.offset) < 400);
|
|
111
|
+
const clearedNearby = (clearOffsetsByScope.get(m.scopeId) ?? []).some((offset) => offset > m.offset);
|
|
112
|
+
const methodName = nearestBefore(mask, m.offset, /(?:fun|void)\s+([A-Za-z_]\w*)\s*\(/);
|
|
113
|
+
const userTriggered = methodName !== undefined && USER_TRIGGER_METHOD_NAMES.test(methodName);
|
|
114
|
+
if (!resolved) {
|
|
115
|
+
candidate("android-sensitive-clipboard-write", m, "Dynamic ClipData object set on the clipboard; contents not resolved", m.argumentExpression, "low", "unresolved");
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const sensitive = labelSensitive(resolved.label) ?? valueSensitive(resolved.value);
|
|
119
|
+
if (!sensitive)
|
|
120
|
+
continue;
|
|
121
|
+
const valueClassification = resolved.value !== undefined ? classifyDirectExpression(resolved.value) : undefined;
|
|
122
|
+
if (valueClassification?.kind === "string-literal" || valueClassification?.kind === "identifier" || valueClassification?.kind === "member-access") {
|
|
123
|
+
const title = nearMarker
|
|
124
|
+
? `Sensitive clipboard write with an EXTRA_IS_SENSITIVE marker present nearby (${sensitive.sensitiveDataCategory})`
|
|
125
|
+
: `Direct ${sensitive.sensitiveDataCategory} copied to the system clipboard`;
|
|
126
|
+
if (nearMarker || userTriggered) {
|
|
127
|
+
candidate("android-sensitive-clipboard-write", m, title, resolved.value, "medium");
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
finding(m, title, [`label=${resolved.label ?? "(none)"}`, `marker=${nearMarker}`, `cleared=${clearedNearby}`]);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
candidate("android-sensitive-clipboard-write", m, `Sensitive-looking clipboard label with a dynamic payload (${sensitive.sensitiveDataCategory})`, resolved.value, "medium", "unresolved");
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (m.kind === "legacy-set-text") {
|
|
139
|
+
const sensitive = valueSensitive(m.valueExpression);
|
|
140
|
+
if (!sensitive)
|
|
141
|
+
continue;
|
|
142
|
+
const classification = m.valueExpression !== undefined ? classifyDirectExpression(m.valueExpression) : undefined;
|
|
143
|
+
if (classification?.kind === "string-literal" || classification?.kind === "identifier") {
|
|
144
|
+
finding(m, `Direct ${sensitive.sensitiveDataCategory} copied via legacy ClipboardManager.setText`, [`value=${m.valueExpression}`]);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
candidate("android-sensitive-clipboard-write", m, "Sensitive-looking legacy clipboard write with a dynamic value", m.valueExpression, "medium", "unresolved");
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (m.kind === "clip-get") {
|
|
152
|
+
const className = nearestBefore(mask, m.offset, /class\s+([A-Za-z_]\w*)/);
|
|
153
|
+
const methodName = nearestBefore(mask, m.offset, /(?:fun|void)\s+([A-Za-z_]\w*)\s*\(/);
|
|
154
|
+
const backgroundLike = className !== undefined && BACKGROUND_LIKE_CLASS_SUFFIX.test(className);
|
|
155
|
+
const userTriggered = methodName !== undefined && USER_TRIGGER_METHOD_NAMES.test(methodName);
|
|
156
|
+
if (backgroundLike) {
|
|
157
|
+
candidate("android-sensitive-clipboard-read-review", m, `Clipboard read from a background-like class (${className})`, undefined, "medium", "unresolved");
|
|
158
|
+
}
|
|
159
|
+
else if (!userTriggered) {
|
|
160
|
+
candidate("android-sensitive-clipboard-read-review", m, "Clipboard read outside a recognized user-triggered paste flow", undefined, "low", "unresolved");
|
|
161
|
+
}
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { candidates, findings };
|
|
166
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { sortCandidateEvidence } from "../ordering.js";
|
|
2
|
+
import { discoverSecretSourceFiles } from "../secretCandidates/discoverSecretSourceFiles.js";
|
|
3
|
+
import { analyzeClipboardSecurityFile } from "./analyzeClipboardSecurity.js";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// v0.4.1 Batch 6 — standalone Android clipboard-security 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_CLIPBOARD_SECURITY_AUDIT_CHECK_ID = "android-clipboard-security-audit";
|
|
11
|
+
export function auditAndroidClipboardSecurity(targetRoot, detection) {
|
|
12
|
+
if (detection.projectKind === "non-android") {
|
|
13
|
+
return {
|
|
14
|
+
id: ANDROID_CLIPBOARD_SECURITY_AUDIT_CHECK_ID,
|
|
15
|
+
category: "android-clipboard",
|
|
16
|
+
title: "Android clipboard security audit",
|
|
17
|
+
status: "unsupported",
|
|
18
|
+
requirementLevel: "optional",
|
|
19
|
+
ran: false,
|
|
20
|
+
skipped: true,
|
|
21
|
+
skipInfo: {
|
|
22
|
+
checkId: ANDROID_CLIPBOARD_SECURITY_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 = analyzeClipboardSecurityFile(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_CLIPBOARD_SECURITY_AUDIT_CHECK_ID,
|
|
56
|
+
category: "android-clipboard",
|
|
57
|
+
title: "Android clipboard security 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 prove clipboard content was set or read, or that another application accessed the clipboard.",
|
|
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,82 @@
|
|
|
1
|
+
import { executableMask, lineForOffset, methodScopes, scopeAt } from "../sensitiveData/localSourceContext.js";
|
|
2
|
+
import { splitTopLevelArguments } from "../sensitiveData/splitArguments.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// v0.4.1 Batch 6 — bounded direct evidence collection for clipboard APIs
|
|
5
|
+
// (ClipData creation, setPrimaryClip/getPrimaryClip/clearPrimaryClip, legacy
|
|
6
|
+
// android.text.ClipboardManager, and the EXTRA_IS_SENSITIVE marker). Purely
|
|
7
|
+
// lexical over comment/string-masked text, argument text re-sliced from
|
|
8
|
+
// original content via regex match indices — see
|
|
9
|
+
// sensitiveStorage/collectStorageEvidence.ts for the same pattern.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Greedy (not lazy) argument capture: setPrimaryClip's single argument is
|
|
12
|
+
// frequently itself a nested ClipData.newPlainText(...) call, and a lazy
|
|
13
|
+
// quantifier would stop at that inner call's closing paren instead of the
|
|
14
|
+
// outer call's. Still bounded to one statement by excluding newlines/`;`.
|
|
15
|
+
const CLIP_CREATE = /(?:(?:val|var|ClipData)\s+([A-Za-z_]\w*)\s*=\s*)?ClipData\.(newPlainText|newHtmlText|newUri)\s*\(([^;\n]*)\)/gd;
|
|
16
|
+
const CLIP_SET = /([A-Za-z_][\w.]*)\.setPrimaryClip\s*\(([^;\n]*)\)/gd;
|
|
17
|
+
const LEGACY_SET_TEXT = /([A-Za-z_][\w.]*[Cc]lipboard[A-Za-z_]*)\.setText\s*\(([^;\n]*)\)/gd;
|
|
18
|
+
const CLIP_GET_CALL = /([A-Za-z_][\w.]*)\.(getPrimaryClip|hasPrimaryClip|getItemAt|coerceToText)\s*\(([^;\n]*?)\)/gd;
|
|
19
|
+
const CLIP_GET_PROPERTY = /([A-Za-z_][\w.]*[Cc]lipboard[A-Za-z_]*)\.(primaryClip|hasText)\b(?!\s*\()/gd;
|
|
20
|
+
const LEGACY_GET_TEXT = /([A-Za-z_][\w.]*[Cc]lipboard[A-Za-z_]*)\.getText\s*\(\s*\)/gd;
|
|
21
|
+
const CLIP_CLEAR = /([A-Za-z_][\w.]*)\.clearPrimaryClip\s*\(\s*\)/gd;
|
|
22
|
+
const SENSITIVE_MARKER = /\bClipDescription\.EXTRA_IS_SENSITIVE\b/g;
|
|
23
|
+
function realGroup(content, match, group) {
|
|
24
|
+
const span = match.indices?.[group];
|
|
25
|
+
if (!span)
|
|
26
|
+
return match[group];
|
|
27
|
+
return content.slice(span[0], span[1]);
|
|
28
|
+
}
|
|
29
|
+
function push(list, content, scopes, kind, api, offset, extra) {
|
|
30
|
+
list.push({ kind, api, offset, line: lineForOffset(content, offset), scopeId: scopeAt(scopes, offset)?.id, ...extra });
|
|
31
|
+
}
|
|
32
|
+
export function collectClipboardEvidence(content) {
|
|
33
|
+
const mask = executableMask(content);
|
|
34
|
+
const scopes = methodScopes(mask);
|
|
35
|
+
const matches = [];
|
|
36
|
+
for (const match of mask.matchAll(CLIP_CREATE)) {
|
|
37
|
+
const offset = match.index ?? 0;
|
|
38
|
+
const args = splitTopLevelArguments(realGroup(content, match, 3) ?? "");
|
|
39
|
+
push(matches, content, scopes, "clip-create", `ClipData.${match[2]}`, offset, {
|
|
40
|
+
assignedVariable: match[1],
|
|
41
|
+
labelExpression: args[0],
|
|
42
|
+
valueExpression: args[1],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
for (const match of mask.matchAll(CLIP_SET)) {
|
|
46
|
+
const offset = match.index ?? 0;
|
|
47
|
+
const args = splitTopLevelArguments(realGroup(content, match, 2) ?? "");
|
|
48
|
+
push(matches, content, scopes, "clip-set", "ClipboardManager.setPrimaryClip", offset, {
|
|
49
|
+
receiver: match[1],
|
|
50
|
+
argumentExpression: args[0],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
for (const match of mask.matchAll(LEGACY_SET_TEXT)) {
|
|
54
|
+
const offset = match.index ?? 0;
|
|
55
|
+
const args = splitTopLevelArguments(realGroup(content, match, 2) ?? "");
|
|
56
|
+
push(matches, content, scopes, "legacy-set-text", "ClipboardManager.setText", offset, {
|
|
57
|
+
receiver: match[1],
|
|
58
|
+
valueExpression: args[0],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
for (const match of mask.matchAll(CLIP_GET_CALL)) {
|
|
62
|
+
const offset = match.index ?? 0;
|
|
63
|
+
push(matches, content, scopes, "clip-get", `ClipboardManager.${match[2]}`, offset, { receiver: match[1] });
|
|
64
|
+
}
|
|
65
|
+
for (const match of mask.matchAll(CLIP_GET_PROPERTY)) {
|
|
66
|
+
const offset = match.index ?? 0;
|
|
67
|
+
push(matches, content, scopes, "clip-get", `ClipboardManager.${match[2]}`, offset, { receiver: match[1] });
|
|
68
|
+
}
|
|
69
|
+
for (const match of mask.matchAll(LEGACY_GET_TEXT)) {
|
|
70
|
+
const offset = match.index ?? 0;
|
|
71
|
+
push(matches, content, scopes, "clip-get", "ClipboardManager.getText", offset, { receiver: match[1] });
|
|
72
|
+
}
|
|
73
|
+
for (const match of mask.matchAll(CLIP_CLEAR)) {
|
|
74
|
+
const offset = match.index ?? 0;
|
|
75
|
+
push(matches, content, scopes, "clip-clear", "ClipboardManager.clearPrimaryClip", offset, { receiver: match[1] });
|
|
76
|
+
}
|
|
77
|
+
for (const match of mask.matchAll(SENSITIVE_MARKER)) {
|
|
78
|
+
const offset = match.index ?? 0;
|
|
79
|
+
push(matches, content, scopes, "sensitive-marker", "ClipDescription.EXTRA_IS_SENSITIVE", offset, {});
|
|
80
|
+
}
|
|
81
|
+
return matches.sort((a, b) => a.offset - b.offset);
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { auditAndroidClipboardSecurity, ANDROID_CLIPBOARD_SECURITY_AUDIT_CHECK_ID } from "./checkResult.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { makeAndroidFinding } from "../../../audit/androidFinding.js";
|
|
4
|
+
import { makeCandidateEvidence } from "../../candidateEvidence.js";
|
|
5
|
+
import { buildAndroidSourceLocation } from "../../sourceLocation.js";
|
|
6
|
+
import { sortCandidateEvidence } from "../../ordering.js";
|
|
7
|
+
import { buildGradleCommandPlan } from "../../../gradle/validate/planner.js";
|
|
8
|
+
import { captureTargetSnapshot, isExpectedAndroidGeneratedPath } from "../../../gradle/validate/targetMutation.js";
|
|
9
|
+
import { buildExternalToolMutationReport, findFreshFiles } from "../mutation.js";
|
|
10
|
+
import { copyExternalToolArtifactFromTarget } from "../artifacts.js";
|
|
11
|
+
import { parseLintXml } from "./parseLintXml.js";
|
|
12
|
+
import { parseLintSarif } from "./parseLintSarif.js";
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// v0.4.1 Batch 7 — standalone Android Lint check. Reuses the existing
|
|
15
|
+
// lint-debug Gradle operation as-is (its fixed args already include
|
|
16
|
+
// --offline/--no-daemon/--console=plain — see operations.ts) rather than
|
|
17
|
+
// creating a second Gradle task/operation system. Not called from
|
|
18
|
+
// validateAndroidTarget or any active orchestration; not rendered in
|
|
19
|
+
// reports; no CLI effect. Batch 8 integrates.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
export const ANDROID_LINT_AUDIT_CHECK_ID = "android-lint-audit";
|
|
22
|
+
const LINT_REPORT_EXTENSIONS = [".xml", ".sarif"];
|
|
23
|
+
function skippedCheck(reason, environmentRequirements = []) {
|
|
24
|
+
return {
|
|
25
|
+
id: ANDROID_LINT_AUDIT_CHECK_ID,
|
|
26
|
+
category: "android-lint",
|
|
27
|
+
title: "Android Lint audit (offline)",
|
|
28
|
+
status: "skipped",
|
|
29
|
+
requirementLevel: "optional",
|
|
30
|
+
ran: false,
|
|
31
|
+
skipped: true,
|
|
32
|
+
skipInfo: {
|
|
33
|
+
checkId: ANDROID_LINT_AUDIT_CHECK_ID,
|
|
34
|
+
reason,
|
|
35
|
+
requirementLevel: "optional",
|
|
36
|
+
missingCapability: "gradle-lint-debug",
|
|
37
|
+
verdictImpact: "does not apply until Batch 8 integration",
|
|
38
|
+
recommendedNextAction: "Ensure a Gradle wrapper, Java, Android SDK, and cached dependencies are available, or run again after Batch 8 wires optional-tool selection into the CLI.",
|
|
39
|
+
},
|
|
40
|
+
evidence: [],
|
|
41
|
+
findings: [],
|
|
42
|
+
warnings: [],
|
|
43
|
+
errors: [],
|
|
44
|
+
sourcePaths: [],
|
|
45
|
+
confidence: "unknown",
|
|
46
|
+
environmentRequirements,
|
|
47
|
+
candidateEvidence: [],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function severityToProject(severity) {
|
|
51
|
+
const normalized = severity.toLowerCase();
|
|
52
|
+
if (normalized === "fatal" || normalized === "error")
|
|
53
|
+
return "major";
|
|
54
|
+
if (normalized === "warning")
|
|
55
|
+
return "minor";
|
|
56
|
+
if (normalized === "information" || normalized === "informational")
|
|
57
|
+
return "informational";
|
|
58
|
+
if (normalized === "ignore")
|
|
59
|
+
return undefined;
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
export async function auditAndroidLint(options) {
|
|
63
|
+
const targetRoot = path.resolve(options.targetRoot);
|
|
64
|
+
if (options.taskAvailable === false) {
|
|
65
|
+
return skippedCheck("The lintDebug Gradle task was not found for this target.");
|
|
66
|
+
}
|
|
67
|
+
const plan = buildGradleCommandPlan("lint-debug", targetRoot, options.detection);
|
|
68
|
+
if (plan.rejected) {
|
|
69
|
+
return skippedCheck(plan.reason);
|
|
70
|
+
}
|
|
71
|
+
const before = captureTargetSnapshot(targetRoot, true);
|
|
72
|
+
const startedAtMs = Date.now();
|
|
73
|
+
const result = await options.executor(plan);
|
|
74
|
+
const after = captureTargetSnapshot(targetRoot, true);
|
|
75
|
+
const mutation = buildExternalToolMutationReport(before, after, isExpectedAndroidGeneratedPath);
|
|
76
|
+
const warnings = [];
|
|
77
|
+
if (!mutation.comparable)
|
|
78
|
+
warnings.push(`Target mutation evidence unavailable: ${mutation.reason ?? "unknown reason"}`);
|
|
79
|
+
if (mutation.unexpectedChanges.length > 0)
|
|
80
|
+
warnings.push(`Unexpected target change(s) observed: ${mutation.unexpectedChanges.join(", ")}`);
|
|
81
|
+
if (result.timedOut) {
|
|
82
|
+
return {
|
|
83
|
+
id: ANDROID_LINT_AUDIT_CHECK_ID,
|
|
84
|
+
category: "android-lint",
|
|
85
|
+
title: "Android Lint audit (offline)",
|
|
86
|
+
status: "inconclusive",
|
|
87
|
+
requirementLevel: "optional",
|
|
88
|
+
ran: true,
|
|
89
|
+
skipped: false,
|
|
90
|
+
evidence: [`exitCode=${result.exitCode ?? "null"}`],
|
|
91
|
+
findings: [],
|
|
92
|
+
warnings: [...warnings, `lintDebug timed out after ${plan.timeoutMs}ms.`],
|
|
93
|
+
errors: [],
|
|
94
|
+
sourcePaths: [],
|
|
95
|
+
confidence: "medium",
|
|
96
|
+
environmentRequirements: plan.environmentLimitations,
|
|
97
|
+
candidateEvidence: [],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (result.exitCode === null) {
|
|
101
|
+
return skippedCheck("Gradle could not be spawned or resolved for this target (missing Java, wrapper, or environment issue).");
|
|
102
|
+
}
|
|
103
|
+
// Discover fresh (created-or-modified-by-this-run) lint report files
|
|
104
|
+
// rather than trusting a stale pre-existing report from an earlier run.
|
|
105
|
+
// A small backward epsilon absorbs filesystem mtime rounding/clock-tick
|
|
106
|
+
// granularity (observed on some Windows filesystems) without meaningfully
|
|
107
|
+
// weakening the stale-report exclusion, since a genuinely stale report is
|
|
108
|
+
// from a prior run (seconds/minutes old), not a few tens of milliseconds.
|
|
109
|
+
const FRESHNESS_EPSILON_MS = 250;
|
|
110
|
+
const freshFiles = findFreshFiles(targetRoot, LINT_REPORT_EXTENSIONS, startedAtMs - FRESHNESS_EPSILON_MS).filter((f) => /lint-results/i.test(path.basename(f)));
|
|
111
|
+
const findingsById = new Map();
|
|
112
|
+
const candidatesById = new Map();
|
|
113
|
+
const limitations = ["Reflects the debug variant lint run only; does not prove the published release variant's lint result.", "Gradle/plugin/report format may drift; unsupported report shapes are represented conservatively."];
|
|
114
|
+
const finding = (issueId, severity, title, message, relativePath, line, module) => findingsById.set(`lint:${issueId}:${relativePath ?? "(no-path)"}:${line ?? 0}`, makeAndroidFinding({
|
|
115
|
+
ruleId: "android-optional-tool-lint-evidence",
|
|
116
|
+
title,
|
|
117
|
+
severity,
|
|
118
|
+
confidence: "medium",
|
|
119
|
+
description: message || `Android Lint reported issue ${issueId}.`,
|
|
120
|
+
manifestPath: relativePath ?? "(unresolved)",
|
|
121
|
+
identity: `lint:${issueId}:${module ?? "(root)"}`,
|
|
122
|
+
location: { line },
|
|
123
|
+
evidenceDetails: [`externalRuleId=${issueId}`, module ? `module=${module}` : undefined].filter((v) => Boolean(v)),
|
|
124
|
+
recommendation: "Review the Android Lint issue and remediate or explicitly suppress it with justification.",
|
|
125
|
+
}));
|
|
126
|
+
const candidate = (issueId, summary, relativePath, line, confidence = "low") => candidatesById.set(`${issueId}:${relativePath ?? "(no-path)"}:${line ?? 0}:${summary.slice(0, 40)}`, makeCandidateEvidence({
|
|
127
|
+
ruleId: "android-optional-tool-lint-evidence",
|
|
128
|
+
category: "android-lint",
|
|
129
|
+
confidence,
|
|
130
|
+
location: relativePath ? (() => { try {
|
|
131
|
+
return buildAndroidSourceLocation(targetRoot, path.resolve(targetRoot, relativePath), { line });
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return { path: relativePath, line };
|
|
135
|
+
} })() : { path: "(unknown)" },
|
|
136
|
+
summary,
|
|
137
|
+
rawValue: undefined,
|
|
138
|
+
resolutionState: "resolved",
|
|
139
|
+
staticAnalysisLimitations: limitations,
|
|
140
|
+
}));
|
|
141
|
+
const seenIssueKeys = new Set();
|
|
142
|
+
let sawAnyReport = false;
|
|
143
|
+
for (const filePath of freshFiles) {
|
|
144
|
+
const content = readFileSafe(filePath);
|
|
145
|
+
if (content === undefined)
|
|
146
|
+
continue;
|
|
147
|
+
const moduleGuess = moduleForPath(targetRoot, filePath, options.detection.modules.map((m) => m.path));
|
|
148
|
+
const artifact = copyExternalToolArtifactFromTarget(options.artifactRoot, targetRoot, "android-lint", filePath, filePath.toLowerCase().endsWith(".sarif") ? "sarif" : "xml");
|
|
149
|
+
void artifact;
|
|
150
|
+
if (filePath.toLowerCase().endsWith(".xml")) {
|
|
151
|
+
const parsedXml = parseLintXml(content);
|
|
152
|
+
if (parsedXml.malformed) {
|
|
153
|
+
warnings.push(`Malformed lint XML report: ${path.basename(filePath)}`);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
sawAnyReport = true;
|
|
157
|
+
for (const issue of parsedXml.issues) {
|
|
158
|
+
const dedupeKey = `${issue.id}:${issue.file}:${issue.line}`;
|
|
159
|
+
if (seenIssueKeys.has(dedupeKey))
|
|
160
|
+
continue;
|
|
161
|
+
seenIssueKeys.add(dedupeKey);
|
|
162
|
+
const relativePath = issue.file ? relativeIfContained(targetRoot, issue.file) : undefined;
|
|
163
|
+
const projectSeverity = severityToProject(issue.severity);
|
|
164
|
+
if (projectSeverity === "major" || projectSeverity === "minor") {
|
|
165
|
+
finding(issue.id, projectSeverity, `Android Lint: ${issue.id}`, issue.message, relativePath, issue.line, moduleGuess);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
candidate(issue.id, `Android Lint ${issue.severity} issue: ${issue.id} — ${issue.message}`, relativePath, issue.line, projectSeverity === "informational" ? "medium" : "low");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
const parsedSarif = parseLintSarif(content);
|
|
174
|
+
if (parsedSarif.malformed) {
|
|
175
|
+
warnings.push(`Malformed lint SARIF report: ${path.basename(filePath)}`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
sawAnyReport = true;
|
|
179
|
+
for (const sarifResult of parsedSarif.results) {
|
|
180
|
+
const dedupeKey = `${sarifResult.ruleId}:${sarifResult.artifactUri}:${sarifResult.line}`;
|
|
181
|
+
if (seenIssueKeys.has(dedupeKey))
|
|
182
|
+
continue;
|
|
183
|
+
seenIssueKeys.add(dedupeKey);
|
|
184
|
+
const relativePath = sarifResult.artifactUri ? relativeIfContained(targetRoot, sarifResult.artifactUri) : undefined;
|
|
185
|
+
candidate(sarifResult.ruleId, `Android Lint SARIF ${sarifResult.level} result: ${sarifResult.ruleId} — ${sarifResult.message}`, relativePath, sarifResult.line, "low");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const findings = [...findingsById.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
190
|
+
const candidateEvidence = sortCandidateEvidence([...candidatesById.values()]);
|
|
191
|
+
if (!sawAnyReport) {
|
|
192
|
+
const status = result.exitCode === 0 ? "inconclusive" : "inconclusive";
|
|
193
|
+
return {
|
|
194
|
+
id: ANDROID_LINT_AUDIT_CHECK_ID,
|
|
195
|
+
category: "android-lint",
|
|
196
|
+
title: "Android Lint audit (offline)",
|
|
197
|
+
status,
|
|
198
|
+
requirementLevel: "optional",
|
|
199
|
+
ran: true,
|
|
200
|
+
skipped: false,
|
|
201
|
+
evidence: [`exitCode=${result.exitCode}`, "No fresh structured lint report was discovered."],
|
|
202
|
+
findings: [],
|
|
203
|
+
warnings,
|
|
204
|
+
errors: result.exitCode !== 0 ? ["lintDebug exited non-zero and no structured report was produced."] : [],
|
|
205
|
+
sourcePaths: [],
|
|
206
|
+
confidence: "low",
|
|
207
|
+
environmentRequirements: plan.environmentLimitations,
|
|
208
|
+
candidateEvidence: [],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const status = findings.length > 0 ? "failed" : "passed";
|
|
212
|
+
if (result.exitCode !== 0) {
|
|
213
|
+
warnings.push(`lintDebug exited with code ${result.exitCode}; a structured report was still discovered and parsed.`);
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
id: ANDROID_LINT_AUDIT_CHECK_ID,
|
|
217
|
+
category: "android-lint",
|
|
218
|
+
title: "Android Lint audit (offline)",
|
|
219
|
+
status,
|
|
220
|
+
requirementLevel: "optional",
|
|
221
|
+
ran: true,
|
|
222
|
+
skipped: false,
|
|
223
|
+
evidence: [`exitCode=${result.exitCode}`, `reportsDiscovered=${freshFiles.length}`, `expectedGeneratedPaths=${plan.expectedGeneratedPaths.join(",") || "(none)"}`],
|
|
224
|
+
findings,
|
|
225
|
+
warnings,
|
|
226
|
+
errors: [],
|
|
227
|
+
sourcePaths: freshFiles.map((f) => relativeIfContained(targetRoot, f) ?? f),
|
|
228
|
+
confidence: "high",
|
|
229
|
+
environmentRequirements: plan.environmentLimitations,
|
|
230
|
+
targetModificationObserved: mutation.comparable ? mutation.unexpectedChanges.length > 0 : undefined,
|
|
231
|
+
candidateEvidence,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function readFileSafe(filePath) {
|
|
235
|
+
try {
|
|
236
|
+
return fs.readFileSync(filePath, "utf8");
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function moduleForPath(targetRoot, filePath, modulePaths) {
|
|
243
|
+
const relative = relativeIfContained(targetRoot, filePath);
|
|
244
|
+
if (!relative)
|
|
245
|
+
return undefined;
|
|
246
|
+
return modulePaths.find((m) => relative.startsWith(`${m}/`));
|
|
247
|
+
}
|
|
248
|
+
function relativeIfContained(root, absoluteOrRelative) {
|
|
249
|
+
const absolute = path.isAbsolute(absoluteOrRelative) ? absoluteOrRelative : path.resolve(root, absoluteOrRelative);
|
|
250
|
+
const normalizedRoot = path.resolve(root).replace(/\\/g, "/");
|
|
251
|
+
const normalizedAbsolute = absolute.replace(/\\/g, "/");
|
|
252
|
+
if (!normalizedAbsolute.startsWith(`${normalizedRoot}/`))
|
|
253
|
+
return undefined;
|
|
254
|
+
return normalizedAbsolute.slice(normalizedRoot.length + 1);
|
|
255
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { auditAndroidLint, ANDROID_LINT_AUDIT_CHECK_ID } from "./checkResult.js";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { safeJsonParse, DEFAULT_MAX_MESSAGE_LENGTH } from "../boundedOutput.js";
|
|
2
|
+
const MAX_RESULTS = 5_000;
|
|
3
|
+
function boundedMessage(text) {
|
|
4
|
+
const value = typeof text === "string" ? text : "";
|
|
5
|
+
return value.length > DEFAULT_MAX_MESSAGE_LENGTH ? `${value.slice(0, DEFAULT_MAX_MESSAGE_LENGTH)}...` : value;
|
|
6
|
+
}
|
|
7
|
+
export function parseLintSarif(sarifText) {
|
|
8
|
+
const parsed = safeJsonParse(sarifText);
|
|
9
|
+
if (!parsed.ok)
|
|
10
|
+
return { malformed: true, results: [] };
|
|
11
|
+
const runs = Array.isArray(parsed.value.runs) ? parsed.value.runs : [];
|
|
12
|
+
const firstRun = runs[0];
|
|
13
|
+
if (!firstRun)
|
|
14
|
+
return { malformed: false, results: [] };
|
|
15
|
+
const driver = firstRun.tool?.driver;
|
|
16
|
+
const toolVersion = typeof driver?.version === "string" ? driver.version : undefined;
|
|
17
|
+
const rawResults = Array.isArray(firstRun.results) ? firstRun.results : [];
|
|
18
|
+
const results = [];
|
|
19
|
+
for (const rawResult of rawResults.slice(0, MAX_RESULTS)) {
|
|
20
|
+
const result = rawResult;
|
|
21
|
+
const message = result.message?.text;
|
|
22
|
+
const locations = Array.isArray(result.locations) ? result.locations : [];
|
|
23
|
+
const firstLocation = locations[0];
|
|
24
|
+
const physicalLocation = firstLocation?.physicalLocation;
|
|
25
|
+
const artifactLocation = physicalLocation?.artifactLocation;
|
|
26
|
+
const region = physicalLocation?.region;
|
|
27
|
+
results.push({
|
|
28
|
+
ruleId: typeof result.ruleId === "string" ? result.ruleId : "(unknown-rule)",
|
|
29
|
+
level: typeof result.level === "string" ? result.level : "(unknown)",
|
|
30
|
+
message: boundedMessage(message),
|
|
31
|
+
artifactUri: typeof artifactLocation?.uri === "string" ? artifactLocation.uri : undefined,
|
|
32
|
+
line: typeof region?.startLine === "number" ? region.startLine : undefined,
|
|
33
|
+
column: typeof region?.startColumn === "number" ? region.startColumn : undefined,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return { malformed: false, toolVersion, results };
|
|
37
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { parseXmlDocument, findChildren } from "../../../manifest/xml/parseXml.js";
|
|
2
|
+
import { DEFAULT_MAX_MESSAGE_LENGTH } from "../boundedOutput.js";
|
|
3
|
+
function attr(element, name) {
|
|
4
|
+
return element.attributes.find((a) => a.localName === name)?.value;
|
|
5
|
+
}
|
|
6
|
+
function boundedMessage(text) {
|
|
7
|
+
if (!text)
|
|
8
|
+
return "";
|
|
9
|
+
return text.length > DEFAULT_MAX_MESSAGE_LENGTH ? `${text.slice(0, DEFAULT_MAX_MESSAGE_LENGTH)}...` : text;
|
|
10
|
+
}
|
|
11
|
+
export function parseLintXml(xmlText) {
|
|
12
|
+
const parsed = parseXmlDocument(xmlText);
|
|
13
|
+
if (!parsed.ok)
|
|
14
|
+
return { malformed: true, issues: [] };
|
|
15
|
+
const root = parsed.root;
|
|
16
|
+
if (root.localName !== "issues")
|
|
17
|
+
return { malformed: true, issues: [] };
|
|
18
|
+
const issues = [];
|
|
19
|
+
for (const issueElement of findChildren(root, "issue")) {
|
|
20
|
+
const locations = findChildren(issueElement, "location");
|
|
21
|
+
const primaryLocation = locations[0];
|
|
22
|
+
const lineRaw = primaryLocation ? attr(primaryLocation, "line") : undefined;
|
|
23
|
+
const columnRaw = primaryLocation ? attr(primaryLocation, "column") : undefined;
|
|
24
|
+
issues.push({
|
|
25
|
+
id: attr(issueElement, "id") ?? "(unknown-issue)",
|
|
26
|
+
severity: attr(issueElement, "severity") ?? "(unknown)",
|
|
27
|
+
message: boundedMessage(attr(issueElement, "message") ?? attr(issueElement, "summary")),
|
|
28
|
+
category: attr(issueElement, "category"),
|
|
29
|
+
priority: attr(issueElement, "priority"),
|
|
30
|
+
file: primaryLocation ? attr(primaryLocation, "file") : undefined,
|
|
31
|
+
line: lineRaw ? Number.parseInt(lineRaw, 10) : undefined,
|
|
32
|
+
column: columnRaw ? Number.parseInt(columnRaw, 10) : undefined,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return { malformed: false, issues, formatVersion: attr(root, "format") };
|
|
36
|
+
}
|