@dailephd/my-dev-kit-lab 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -61
- package/dist/scripts/security/validate.js +38 -5
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/analyzeBackupConfiguration.js +300 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/checkResult.js +41 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/index.js +8 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/manifestEvidence.js +102 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/parseBackupRules.js +110 -0
- package/dist/src/mobile/android/advancedSecurity/backupConfiguration/types.js +27 -0
- package/dist/src/mobile/android/advancedSecurity/candidateEvidence.js +50 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/analyzeClipboardSecurity.js +166 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/checkResult.js +74 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/collectClipboardEvidence.js +82 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/clipboard/types.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/checkResult.js +255 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintSarif.js +37 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/androidLint/parseLintXml.js +36 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/artifacts.js +95 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/boundedOutput.js +33 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/checkResult.js +173 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/command.js +33 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/dependencyCheck/parseDependencyCheckJson.js +133 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/discoverExecutable.js +13 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/index.js +8 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/minimalEnvironment.js +26 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/mutation.js +77 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/osv/checkResult.js +161 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/osv/command.js +25 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/osv/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/osv/parseOsvJson.js +136 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/runBoundedExternalTool.js +51 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/runRequestedAndroidExternalTools.js +54 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/checkResult.js +170 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/command.js +28 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/index.js +2 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/parseSemgrepJson.js +141 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/semgrep/rules.js +105 -0
- package/dist/src/mobile/android/advancedSecurity/externalTools/types.js +107 -0
- package/dist/src/mobile/android/advancedSecurity/fileProvider/checkResult.js +188 -0
- package/dist/src/mobile/android/advancedSecurity/fileProvider/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/analyzeFirebaseGoogleServices.js +197 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/checkResult.js +77 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/collectFirebaseServiceEvidence.js +193 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/discoverFirebaseArtifacts.js +128 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseDatabaseRules.js +55 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirebaseConfig.js +75 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseFirestoreStorageRules.js +88 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/parseGoogleServicesConfig.js +55 -0
- package/dist/src/mobile/android/advancedSecurity/firebaseGoogle/types.js +6 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/analyzeNetworkSecurity.js +394 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/checkResult.js +47 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/deriveEffectiveNetworkPolicy.js +52 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/index.js +14 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/manifestEvidence.js +109 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/parseNetworkSecurityConfig.js +186 -0
- package/dist/src/mobile/android/advancedSecurity/networkSecurity/types.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/ordering.js +44 -0
- package/dist/src/mobile/android/advancedSecurity/redaction.js +54 -0
- package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/analyzeReleaseConfiguration.js +221 -0
- package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/checkResult.js +39 -0
- package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/index.js +7 -0
- package/dist/src/mobile/android/advancedSecurity/releaseConfiguration/manifestEvidence.js +19 -0
- package/dist/src/mobile/android/advancedSecurity/resourceReference.js +69 -0
- package/dist/src/mobile/android/advancedSecurity/resourceResolution.js +118 -0
- package/dist/src/mobile/android/advancedSecurity/ruleIds.js +141 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/analyzeSecretCandidates.js +116 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/checkResult.js +80 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/discoverSecretSourceFiles.js +152 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/index.js +9 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/matchSecretCandidates.js +136 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/suppressValue.js +61 -0
- package/dist/src/mobile/android/advancedSecurity/secretCandidates/types.js +11 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveCategories.js +34 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifyDirectExpression.js +99 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/classifySensitiveIdentifier.js +111 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/index.js +3 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/localSourceContext.js +90 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveData/splitArguments.js +59 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/analyzeSensitiveLogging.js +117 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/checkResult.js +74 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/collectLoggingEvidence.js +87 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveLogging/types.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/analyzeSensitiveStorage.js +182 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/checkResult.js +74 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/collectStorageEvidence.js +109 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/index.js +1 -0
- package/dist/src/mobile/android/advancedSecurity/sensitiveStorage/types.js +4 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/analyzeSigningConfiguration.js +270 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/checkResult.js +119 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/discoverKeystoreCandidates.js +55 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/extractSigningConfigurations.js +81 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/index.js +8 -0
- package/dist/src/mobile/android/advancedSecurity/signingConfiguration/types.js +20 -0
- package/dist/src/mobile/android/advancedSecurity/sourceLocation.js +15 -0
- package/dist/src/mobile/android/advancedSecurity/webview/checkResult.js +210 -0
- package/dist/src/mobile/android/advancedSecurity/webview/index.js +1 -0
- package/dist/src/mobile/android/gradle/moduleMetadataExtractor.js +123 -17
- package/dist/src/mobile/android/manifest/parseAndroidManifest.js +27 -4
- package/dist/src/mobile/android/manifest/xml/parseXml.js +25 -9
- package/dist/src/mobile/android/report/model.js +32 -1
- package/dist/src/mobile/android/report/renderAndroidReport.js +63 -0
- package/dist/src/mobile/android/validate/validateAndroidTarget.js +146 -3
- package/dist/src/mobile/android/validation/checkResult.js +7 -0
- package/dist/src/securityValidation/commandRunner.js +10 -1
- package/dist/src/securityValidation/validate/cliOptions.js +46 -0
- package/package.json +1 -1
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/debug/res/xml/ambiguous_backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/ambiguous_backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/backup_rules.xml +5 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/broad_backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/data_extraction_rules.xml +10 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-a/src/main/res/xml/unsupported_domain_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/malformed_backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/backup-configuration/module-b/src/main/res/xml/unsupported_root.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/debug/res/xml/ambiguous_config.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/ambiguous_config.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/base_cleartext_false.xml +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/broad_and_duplicate_domains.xml +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/empty_domain.xml +6 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/malformed_pin.xml +11 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-a/src/main/res/xml/network_security_config.xml +26 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/malformed_network_security_config.xml +6 -0
- package/tests/fixtures/android/advanced-security-fixtures/network-security/module-b/src/main/res/xml/unsupported_root.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-dynamic.gradle +7 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-false.gradle +9 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/groovy-release-debuggable-true.gradle +13 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-false.gradle.kts +7 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/kotlin-release-debuggable-true.gradle.kts +10 -0
- package/tests/fixtures/android/advanced-security-fixtures/release-configuration/missing-release-build-type.gradle.kts +7 -0
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/debug/res/xml/network_security_config.xml +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/backup_rules.xml +4 -0
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-a/src/main/res/xml/network_security_config.xml +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/resource-resolution/module-b/src/main/res/xml/malformed_network_security_config.xml +7 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.java +8 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/SampleActivity.kt +9 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle +11 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/build.gradle.kts +11 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/google-services.json +16 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates/local.properties +3 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/binary.properties +0 -0
- package/tests/fixtures/android/advanced-security-fixtures/secret-candidates-bounds/module/oversized.json +1 -0
- package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/app/fake-release-keystore.jks +1 -0
- package/tests/fixtures/android/advanced-security-fixtures/signing-configuration/build/generated-fake.keystore +1 -0
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { relativeWithinRoot, resolveWithinRoot } from "../../../../core/pathSafety.js";
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// v0.4.1 Batch 7 — bounded, contained artifact handling for raw external-tool
|
|
7
|
+
// output. New infrastructure: no existing owner copies/hashes generated
|
|
8
|
+
// Gradle or external-tool output (confirmed absent from
|
|
9
|
+
// src/mobile/android/gradle/validate/*). Reuses src/core/pathSafety.ts for
|
|
10
|
+
// containment rather than a second path-safety implementation.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
const MAX_ARTIFACT_BYTES = 32 * 1024 * 1024;
|
|
13
|
+
function mediaTypeFor(kind) {
|
|
14
|
+
switch (kind) {
|
|
15
|
+
case "json":
|
|
16
|
+
return "application/json";
|
|
17
|
+
case "xml":
|
|
18
|
+
return "application/xml";
|
|
19
|
+
case "sarif":
|
|
20
|
+
return "application/sarif+json";
|
|
21
|
+
case "text":
|
|
22
|
+
return "text/plain";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function sha256Of(buffer) {
|
|
26
|
+
return `sha256:${createHash("sha256").update(buffer).digest("hex")}`;
|
|
27
|
+
}
|
|
28
|
+
function toolArtifactDir(artifactRoot, toolId) {
|
|
29
|
+
return resolveWithinRoot(path.resolve(artifactRoot), toolId);
|
|
30
|
+
}
|
|
31
|
+
// Writes tool-generated content (e.g. a materialized Semgrep config, or
|
|
32
|
+
// content the adapter itself produced) under `<artifactRoot>/<toolId>/`.
|
|
33
|
+
// Containment is validated by resolveWithinRoot, which throws on traversal —
|
|
34
|
+
// callers must not catch that away.
|
|
35
|
+
export function writeExternalToolArtifact(artifactRoot, toolId, fileName, content, kind) {
|
|
36
|
+
const dir = toolArtifactDir(artifactRoot, toolId);
|
|
37
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
38
|
+
const absolutePath = resolveWithinRoot(dir, fileName);
|
|
39
|
+
const buffer = Buffer.from(content, "utf8");
|
|
40
|
+
const truncated = buffer.byteLength > MAX_ARTIFACT_BYTES;
|
|
41
|
+
const boundedBuffer = truncated ? buffer.subarray(0, MAX_ARTIFACT_BYTES) : buffer;
|
|
42
|
+
fs.writeFileSync(absolutePath, boundedBuffer);
|
|
43
|
+
return {
|
|
44
|
+
toolId,
|
|
45
|
+
artifactKind: kind,
|
|
46
|
+
relativePath: relativeWithinRoot(path.resolve(artifactRoot), absolutePath),
|
|
47
|
+
mediaType: mediaTypeFor(kind),
|
|
48
|
+
sizeBytes: boundedBuffer.byteLength,
|
|
49
|
+
sha256: sha256Of(boundedBuffer),
|
|
50
|
+
generatedByThisRun: true,
|
|
51
|
+
copiedFromTarget: false,
|
|
52
|
+
truncated,
|
|
53
|
+
parseStatus: "not-attempted",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Copies an existing target-generated report (e.g. a fresh Android Lint XML
|
|
57
|
+
// file) into the artifact root without modifying or deleting the original.
|
|
58
|
+
export function copyExternalToolArtifactFromTarget(artifactRoot, targetRoot, toolId, sourceAbsolutePath, kind) {
|
|
59
|
+
let stat;
|
|
60
|
+
try {
|
|
61
|
+
stat = fs.statSync(sourceAbsolutePath);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
if (!stat.isFile())
|
|
67
|
+
return undefined;
|
|
68
|
+
const truncated = stat.size > MAX_ARTIFACT_BYTES;
|
|
69
|
+
const buffer = truncated ? fs.readFileSync(sourceAbsolutePath).subarray(0, MAX_ARTIFACT_BYTES) : fs.readFileSync(sourceAbsolutePath);
|
|
70
|
+
const dir = toolArtifactDir(artifactRoot, toolId);
|
|
71
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
72
|
+
const destFileName = path.basename(sourceAbsolutePath);
|
|
73
|
+
const destAbsolutePath = resolveWithinRoot(dir, destFileName);
|
|
74
|
+
fs.writeFileSync(destAbsolutePath, buffer);
|
|
75
|
+
let sourceTargetRelativePath;
|
|
76
|
+
try {
|
|
77
|
+
sourceTargetRelativePath = relativeWithinRoot(path.resolve(targetRoot), sourceAbsolutePath);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
sourceTargetRelativePath = undefined;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
toolId,
|
|
84
|
+
artifactKind: kind,
|
|
85
|
+
relativePath: relativeWithinRoot(path.resolve(artifactRoot), destAbsolutePath),
|
|
86
|
+
mediaType: mediaTypeFor(kind),
|
|
87
|
+
sizeBytes: buffer.byteLength,
|
|
88
|
+
sha256: sha256Of(buffer),
|
|
89
|
+
generatedByThisRun: true,
|
|
90
|
+
copiedFromTarget: true,
|
|
91
|
+
sourceTargetRelativePath,
|
|
92
|
+
truncated,
|
|
93
|
+
parseStatus: "not-attempted",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.1 Batch 7 — bounded text/JSON handling for external-tool output.
|
|
3
|
+
//
|
|
4
|
+
// No shared "safe JSON parse" utility existed before this batch (each
|
|
5
|
+
// consumer, e.g. src/securityValidation/staticScans/semgrep.ts, inlines its
|
|
6
|
+
// own unbounded try/catch JSON.parse). This module is the one owner Batch 7
|
|
7
|
+
// adapters share — a truncated document is never handed to JSON.parse, since
|
|
8
|
+
// a truncated-but-syntactically-valid-looking prefix could otherwise parse
|
|
9
|
+
// successfully into a wrong/partial value instead of failing loudly.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
export const DEFAULT_MAX_STDOUT_BYTES = 16 * 1024 * 1024;
|
|
12
|
+
export const DEFAULT_MAX_STDERR_BYTES = 2 * 1024 * 1024;
|
|
13
|
+
export const DEFAULT_MAX_REPORT_BYTES = 32 * 1024 * 1024;
|
|
14
|
+
export const DEFAULT_MAX_MESSAGE_LENGTH = 1_000;
|
|
15
|
+
export const DEFAULT_MAX_SNIPPET_LENGTH = 240;
|
|
16
|
+
export function boundedText(text, maxLength) {
|
|
17
|
+
if (text.length <= maxLength)
|
|
18
|
+
return { text, truncated: false };
|
|
19
|
+
return { text: text.slice(0, maxLength), truncated: true };
|
|
20
|
+
}
|
|
21
|
+
// Bounds the input length BEFORE parsing (a truncated document is reported
|
|
22
|
+
// malformed/truncated, never parsed as if it were a smaller valid document).
|
|
23
|
+
export function safeJsonParse(text, maxBytes = DEFAULT_MAX_REPORT_BYTES) {
|
|
24
|
+
const truncated = text.length > maxBytes;
|
|
25
|
+
if (truncated)
|
|
26
|
+
return { ok: false, malformed: true, truncated: true };
|
|
27
|
+
try {
|
|
28
|
+
return { ok: true, value: JSON.parse(text), truncated: false };
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return { ok: false, malformed: true, truncated: false };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { sortCandidateEvidence } from "../../ordering.js";
|
|
4
|
+
import { discoverAllowlistedExecutable } from "../discoverExecutable.js";
|
|
5
|
+
import { buildMinimalEnvironment } from "../minimalEnvironment.js";
|
|
6
|
+
import { copyExternalToolArtifactFromTarget } from "../artifacts.js";
|
|
7
|
+
import { captureTargetSnapshot, buildExternalToolMutationReport } from "../mutation.js";
|
|
8
|
+
import { runBoundedExternalTool, buildCommandSummary, VERSION_PROBE_TIMEOUT_MS } from "../runBoundedExternalTool.js";
|
|
9
|
+
import { buildDependencyCheckArgs, DEPENDENCY_CHECK_REPORT_FILE_NAME, sanitizeDependencyCheckProjectName } from "./command.js";
|
|
10
|
+
import { parseDependencyCheckJson } from "./parseDependencyCheckJson.js";
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// v0.4.1 Batch 7 — standalone Android OWASP Dependency-Check check. Not
|
|
13
|
+
// called from validateAndroidTarget or any active orchestration; not
|
|
14
|
+
// rendered in reports; no CLI effect. Batch 8 integrates.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
export const ANDROID_DEPENDENCY_CHECK_AUDIT_CHECK_ID = "android-dependency-check-audit";
|
|
17
|
+
const DEPENDENCY_CHECK_CANDIDATES = ["dependency-check", "dependency-check.exe", "dependency-check.sh", "dependency-check.bat", "dependency-check.cmd"];
|
|
18
|
+
const DEPENDENCY_CHECK_TIMEOUT_MS = 15 * 60_000;
|
|
19
|
+
const DATABASE_UNAVAILABLE_PATTERN = /database.*(missing|unavailable|not found|corrupt)|no such file.*\.h2\.db/i;
|
|
20
|
+
function skippedCheck(reason, environmentRequirements = []) {
|
|
21
|
+
return {
|
|
22
|
+
id: ANDROID_DEPENDENCY_CHECK_AUDIT_CHECK_ID,
|
|
23
|
+
category: "android-dependency-check",
|
|
24
|
+
title: "Android OWASP Dependency-Check audit",
|
|
25
|
+
status: "skipped",
|
|
26
|
+
requirementLevel: "optional",
|
|
27
|
+
ran: false,
|
|
28
|
+
skipped: true,
|
|
29
|
+
skipInfo: {
|
|
30
|
+
checkId: ANDROID_DEPENDENCY_CHECK_AUDIT_CHECK_ID,
|
|
31
|
+
reason,
|
|
32
|
+
requirementLevel: "optional",
|
|
33
|
+
missingCapability: "dependency-check-executable-or-database",
|
|
34
|
+
verdictImpact: "does not apply until Batch 8 integration",
|
|
35
|
+
recommendedNextAction: "Install OWASP Dependency-Check locally with a populated local database, or run again after Batch 8 wires optional-tool selection into the CLI.",
|
|
36
|
+
},
|
|
37
|
+
evidence: [],
|
|
38
|
+
findings: [],
|
|
39
|
+
warnings: [],
|
|
40
|
+
errors: [],
|
|
41
|
+
sourcePaths: [],
|
|
42
|
+
confidence: "unknown",
|
|
43
|
+
environmentRequirements,
|
|
44
|
+
candidateEvidence: [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export async function auditAndroidDependencyCheck(options) {
|
|
48
|
+
const targetRoot = path.resolve(options.targetRoot);
|
|
49
|
+
if (options.javaAvailable === false) {
|
|
50
|
+
return skippedCheck("Java was not available; OWASP Dependency-Check requires a Java runtime.", ["Requires a Java runtime (JAVA_HOME or java on PATH)."]);
|
|
51
|
+
}
|
|
52
|
+
const discovered = (options.discover ?? (() => discoverAllowlistedExecutable(DEPENDENCY_CHECK_CANDIDATES, targetRoot)))();
|
|
53
|
+
if (!discovered.available) {
|
|
54
|
+
return skippedCheck("A Dependency-Check executable was not found on PATH.", ["Requires a Dependency-Check executable on PATH."]);
|
|
55
|
+
}
|
|
56
|
+
const env = buildMinimalEnvironment({ additionalAllowedKeys: ["JAVA_HOME"] });
|
|
57
|
+
const versionOutcome = await runBoundedExternalTool(options.executor, { command: discovered.command, args: ["--version"], cwd: targetRoot, timeoutMs: VERSION_PROBE_TIMEOUT_MS, env });
|
|
58
|
+
if (versionOutcome.result.timedOut || versionOutcome.result.exitCode === null) {
|
|
59
|
+
return skippedCheck("Dependency-Check version probe failed or timed out.");
|
|
60
|
+
}
|
|
61
|
+
const rawVersion = versionOutcome.stdout.trim();
|
|
62
|
+
const outDir = path.resolve(options.artifactRoot, "dependency-check", "raw");
|
|
63
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
64
|
+
const projectName = sanitizeDependencyCheckProjectName(path.basename(targetRoot));
|
|
65
|
+
const args = buildDependencyCheckArgs(projectName, targetRoot, outDir);
|
|
66
|
+
const commandSummary = buildCommandSummary(discovered.basename, args, targetRoot, path.resolve(options.artifactRoot));
|
|
67
|
+
const before = captureTargetSnapshot(targetRoot, true);
|
|
68
|
+
const analysisOutcome = await runBoundedExternalTool(options.executor, { command: discovered.command, args, cwd: targetRoot, timeoutMs: DEPENDENCY_CHECK_TIMEOUT_MS, env });
|
|
69
|
+
const after = captureTargetSnapshot(targetRoot, true);
|
|
70
|
+
const mutation = buildExternalToolMutationReport(before, after);
|
|
71
|
+
const warnings = [];
|
|
72
|
+
if (analysisOutcome.stdoutTruncated)
|
|
73
|
+
warnings.push("stdout was truncated.");
|
|
74
|
+
if (analysisOutcome.stderrTruncated)
|
|
75
|
+
warnings.push("stderr was truncated.");
|
|
76
|
+
if (!mutation.comparable)
|
|
77
|
+
warnings.push(`Target mutation evidence unavailable: ${mutation.reason ?? "unknown reason"}`);
|
|
78
|
+
if (mutation.unexpectedChanges.length > 0)
|
|
79
|
+
warnings.push(`Unexpected target change(s) observed: ${mutation.unexpectedChanges.join(", ")}`);
|
|
80
|
+
if (analysisOutcome.result.timedOut) {
|
|
81
|
+
return {
|
|
82
|
+
id: ANDROID_DEPENDENCY_CHECK_AUDIT_CHECK_ID,
|
|
83
|
+
category: "android-dependency-check",
|
|
84
|
+
title: "Android OWASP Dependency-Check audit",
|
|
85
|
+
status: "inconclusive",
|
|
86
|
+
requirementLevel: "optional",
|
|
87
|
+
ran: true,
|
|
88
|
+
skipped: false,
|
|
89
|
+
evidence: [`version=${rawVersion}`],
|
|
90
|
+
findings: [],
|
|
91
|
+
warnings: [...warnings, `Dependency-Check timed out after ${DEPENDENCY_CHECK_TIMEOUT_MS}ms.`],
|
|
92
|
+
errors: [],
|
|
93
|
+
sourcePaths: [],
|
|
94
|
+
confidence: "medium",
|
|
95
|
+
environmentRequirements: [],
|
|
96
|
+
candidateEvidence: [],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const reportPath = path.join(outDir, DEPENDENCY_CHECK_REPORT_FILE_NAME);
|
|
100
|
+
const reportExists = fs.existsSync(reportPath);
|
|
101
|
+
if (!reportExists) {
|
|
102
|
+
if (DATABASE_UNAVAILABLE_PATTERN.test(analysisOutcome.stderr)) {
|
|
103
|
+
return skippedCheck("The local OWASP Dependency-Check vulnerability database is unavailable; updates are disabled by policy, so the scan did not run.", [
|
|
104
|
+
"Requires a populated local Dependency-Check vulnerability database.",
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
id: ANDROID_DEPENDENCY_CHECK_AUDIT_CHECK_ID,
|
|
109
|
+
category: "android-dependency-check",
|
|
110
|
+
title: "Android OWASP Dependency-Check audit",
|
|
111
|
+
status: "inconclusive",
|
|
112
|
+
requirementLevel: "optional",
|
|
113
|
+
ran: true,
|
|
114
|
+
skipped: false,
|
|
115
|
+
evidence: [`version=${rawVersion}`, `exitCode=${analysisOutcome.result.exitCode}`, "No JSON report was produced."],
|
|
116
|
+
findings: [],
|
|
117
|
+
warnings,
|
|
118
|
+
errors: analysisOutcome.result.exitCode !== 0 ? ["Dependency-Check exited non-zero and produced no report."] : [],
|
|
119
|
+
sourcePaths: [],
|
|
120
|
+
confidence: "low",
|
|
121
|
+
environmentRequirements: [],
|
|
122
|
+
candidateEvidence: [],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const artifact = copyExternalToolArtifactFromTarget(options.artifactRoot, targetRoot, "dependency-check", reportPath, "json");
|
|
126
|
+
void artifact;
|
|
127
|
+
const rawJson = fs.readFileSync(reportPath, "utf8");
|
|
128
|
+
const parsed = parseDependencyCheckJson(rawJson, targetRoot);
|
|
129
|
+
const isFindingsExitCode = analysisOutcome.result.exitCode === 0 || analysisOutcome.result.exitCode === 1;
|
|
130
|
+
if (parsed.malformed) {
|
|
131
|
+
return {
|
|
132
|
+
id: ANDROID_DEPENDENCY_CHECK_AUDIT_CHECK_ID,
|
|
133
|
+
category: "android-dependency-check",
|
|
134
|
+
title: "Android OWASP Dependency-Check audit",
|
|
135
|
+
status: "inconclusive",
|
|
136
|
+
requirementLevel: "optional",
|
|
137
|
+
ran: true,
|
|
138
|
+
skipped: false,
|
|
139
|
+
evidence: [`version=${rawVersion}`, `exitCode=${analysisOutcome.result.exitCode}`],
|
|
140
|
+
findings: [],
|
|
141
|
+
warnings,
|
|
142
|
+
errors: [],
|
|
143
|
+
sourcePaths: [],
|
|
144
|
+
confidence: "low",
|
|
145
|
+
environmentRequirements: [],
|
|
146
|
+
candidateEvidence: [],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (parsed.truncated)
|
|
150
|
+
warnings.push("Dependency count exceeded the bounded limit; results were truncated.");
|
|
151
|
+
const findingsById = new Map(parsed.findings.map((f) => [f.id, f]));
|
|
152
|
+
const candidatesById = new Map(parsed.candidates.map((c) => [c.id, c]));
|
|
153
|
+
const findings = [...findingsById.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
154
|
+
const candidateEvidence = sortCandidateEvidence([...candidatesById.values()]);
|
|
155
|
+
const status = findings.length > 0 ? "failed" : isFindingsExitCode ? "passed" : "error";
|
|
156
|
+
return {
|
|
157
|
+
id: ANDROID_DEPENDENCY_CHECK_AUDIT_CHECK_ID,
|
|
158
|
+
category: "android-dependency-check",
|
|
159
|
+
title: "Android OWASP Dependency-Check audit",
|
|
160
|
+
status,
|
|
161
|
+
requirementLevel: "optional",
|
|
162
|
+
ran: true,
|
|
163
|
+
skipped: false,
|
|
164
|
+
evidence: [`version=${rawVersion}`, `dependencies=${parsed.dependencyCount}`, `command=${commandSummary}`],
|
|
165
|
+
findings,
|
|
166
|
+
warnings,
|
|
167
|
+
errors: status === "error" ? ["Dependency-Check exited with a non-findings error."] : [],
|
|
168
|
+
sourcePaths: [],
|
|
169
|
+
confidence: "high",
|
|
170
|
+
environmentRequirements: [],
|
|
171
|
+
candidateEvidence,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.1 Batch 7 — fixed OWASP Dependency-Check command planning.
|
|
3
|
+
//
|
|
4
|
+
// One fixed operation. No caller-provided flags, no database updates, no
|
|
5
|
+
// NVD API key. `--noupdate` is always present; the additional
|
|
6
|
+
// `--disable*` flags are fixed and only prevent additional network-capable
|
|
7
|
+
// analyzers (Node/Yarn/pnpm audit, RetireJS, known-exploited feed) from
|
|
8
|
+
// running, matching the "no unauthorized network access" boundary.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
export const DEPENDENCY_CHECK_REPORT_FILE_NAME = "dependency-check-report.json";
|
|
11
|
+
export function sanitizeDependencyCheckProjectName(rawName) {
|
|
12
|
+
const sanitized = rawName.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 100);
|
|
13
|
+
return sanitized.length > 0 ? sanitized : "android-target";
|
|
14
|
+
}
|
|
15
|
+
export function buildDependencyCheckArgs(projectName, targetRoot, outDir) {
|
|
16
|
+
return [
|
|
17
|
+
"--project",
|
|
18
|
+
sanitizeDependencyCheckProjectName(projectName),
|
|
19
|
+
"--scan",
|
|
20
|
+
targetRoot,
|
|
21
|
+
"--format",
|
|
22
|
+
"JSON",
|
|
23
|
+
"--out",
|
|
24
|
+
outDir,
|
|
25
|
+
"--noupdate",
|
|
26
|
+
"--disableAssembly",
|
|
27
|
+
"--disableNodeAudit",
|
|
28
|
+
"--disableYarnAudit",
|
|
29
|
+
"--disablePnpmAudit",
|
|
30
|
+
"--disableRetireJS",
|
|
31
|
+
"--disableKnownExploited",
|
|
32
|
+
];
|
|
33
|
+
}
|