@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,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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { auditAndroidDependencyCheck, ANDROID_DEPENDENCY_CHECK_AUDIT_CHECK_ID } from "./checkResult.js";
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { makeAndroidFinding } from "../../../audit/androidFinding.js";
|
|
3
|
+
import { makeCandidateEvidence } from "../../candidateEvidence.js";
|
|
4
|
+
import { buildAndroidSourceLocation } from "../../sourceLocation.js";
|
|
5
|
+
import { safeJsonParse, DEFAULT_MAX_MESSAGE_LENGTH } from "../boundedOutput.js";
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// v0.4.1 Batch 7 — bounded OWASP Dependency-Check JSON normalization.
|
|
8
|
+
//
|
|
9
|
+
// Only the one Batch 1 Dependency-Check rule id
|
|
10
|
+
// (android-optional-tool-dependency-check-evidence) is used internally; the
|
|
11
|
+
// vulnerability id (CVE/GHSA/etc.) is preserved as a public identifier.
|
|
12
|
+
// External cache paths (dependencies resolved from ~/.gradle, ~/.m2, etc.)
|
|
13
|
+
// are never used as a source location — only target-contained paths are.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const MAX_DEPENDENCIES = 5_000;
|
|
16
|
+
function boundedText(value) {
|
|
17
|
+
const text = typeof value === "string" ? value : "";
|
|
18
|
+
return text.length > DEFAULT_MAX_MESSAGE_LENGTH ? `${text.slice(0, DEFAULT_MAX_MESSAGE_LENGTH)}...` : text;
|
|
19
|
+
}
|
|
20
|
+
function severityFromCvss(vuln) {
|
|
21
|
+
const cvssv3 = vuln.cvssv3;
|
|
22
|
+
const cvssv2 = vuln.cvssv2;
|
|
23
|
+
const v3Score = typeof cvssv3?.baseScore === "number" ? cvssv3.baseScore : undefined;
|
|
24
|
+
if (v3Score !== undefined) {
|
|
25
|
+
return { severity: v3Score >= 7 ? "major" : v3Score >= 4 ? "minor" : v3Score > 0 ? "informational" : undefined, cvssVersion: "3", score: v3Score };
|
|
26
|
+
}
|
|
27
|
+
const v2Score = typeof cvssv2?.score === "number" ? cvssv2.score : undefined;
|
|
28
|
+
if (v2Score !== undefined) {
|
|
29
|
+
return { severity: v2Score >= 7 ? "major" : v2Score >= 4 ? "minor" : v2Score > 0 ? "informational" : undefined, cvssVersion: "2", score: v2Score };
|
|
30
|
+
}
|
|
31
|
+
const toolSeverity = typeof vuln.severity === "string" ? vuln.severity.toUpperCase() : undefined;
|
|
32
|
+
if (toolSeverity === "CRITICAL" || toolSeverity === "HIGH")
|
|
33
|
+
return { severity: "major" };
|
|
34
|
+
if (toolSeverity === "MEDIUM")
|
|
35
|
+
return { severity: "minor" };
|
|
36
|
+
if (toolSeverity === "LOW")
|
|
37
|
+
return { severity: "informational" };
|
|
38
|
+
return { severity: undefined };
|
|
39
|
+
}
|
|
40
|
+
function normalizeDependencyPath(targetRoot, filePath) {
|
|
41
|
+
if (!filePath)
|
|
42
|
+
return { isExternal: true };
|
|
43
|
+
const absolute = path.isAbsolute(filePath) ? filePath : path.resolve(targetRoot, filePath);
|
|
44
|
+
const normalizedRoot = path.resolve(targetRoot).replace(/\\/g, "/");
|
|
45
|
+
const normalizedAbsolute = absolute.replace(/\\/g, "/");
|
|
46
|
+
if (normalizedAbsolute.startsWith(`${normalizedRoot}/`)) {
|
|
47
|
+
try {
|
|
48
|
+
return { relativePath: buildAndroidSourceLocation(targetRoot, absolute).path, isExternal: false };
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { isExternal: true };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { isExternal: true };
|
|
55
|
+
}
|
|
56
|
+
export function parseDependencyCheckJson(rawJson, targetRoot) {
|
|
57
|
+
const parsed = safeJsonParse(rawJson);
|
|
58
|
+
if (!parsed.ok)
|
|
59
|
+
return { malformed: true, findings: [], candidates: [], dependencyCount: 0, truncated: parsed.truncated };
|
|
60
|
+
const dependencies = Array.isArray(parsed.value.dependencies) ? parsed.value.dependencies : [];
|
|
61
|
+
const findings = [];
|
|
62
|
+
const candidates = [];
|
|
63
|
+
let truncated = dependencies.length > MAX_DEPENDENCIES;
|
|
64
|
+
for (const rawDependency of dependencies.slice(0, MAX_DEPENDENCIES)) {
|
|
65
|
+
const dependency = rawDependency;
|
|
66
|
+
const fileName = typeof dependency.fileName === "string" ? dependency.fileName : "(unknown-dependency)";
|
|
67
|
+
const filePath = typeof dependency.filePath === "string" ? dependency.filePath : undefined;
|
|
68
|
+
const { relativePath, isExternal } = normalizeDependencyPath(targetRoot, filePath);
|
|
69
|
+
const vulnerabilities = Array.isArray(dependency.vulnerabilities) ? dependency.vulnerabilities : [];
|
|
70
|
+
const suppressed = Array.isArray(dependency.suppressedVulnerabilities) ? dependency.suppressedVulnerabilities : [];
|
|
71
|
+
const suppressedNames = new Set(suppressed.map((v) => (typeof v?.name === "string" ? v.name : undefined)).filter(Boolean));
|
|
72
|
+
for (const rawVuln of vulnerabilities) {
|
|
73
|
+
const vuln = rawVuln;
|
|
74
|
+
const vulnName = typeof vuln.name === "string" ? vuln.name : "(unknown-vulnerability)";
|
|
75
|
+
const cwes = Array.isArray(vuln.cwes) ? vuln.cwes.filter((c) => typeof c === "string") : [];
|
|
76
|
+
const description = boundedText(vuln.description);
|
|
77
|
+
const { severity, cvssVersion, score } = severityFromCvss(vuln);
|
|
78
|
+
const identity = `dependency-check:${vulnName}:${fileName}:${relativePath ?? "(external)"}`;
|
|
79
|
+
const sourcePath = relativePath ?? `(external-cache) ${fileName}`;
|
|
80
|
+
if (suppressedNames.has(vulnName)) {
|
|
81
|
+
candidates.push(makeCandidateEvidence({
|
|
82
|
+
ruleId: "android-optional-tool-dependency-check-evidence",
|
|
83
|
+
category: "android-dependency-check",
|
|
84
|
+
confidence: "low",
|
|
85
|
+
location: relativePath ? buildAndroidSourceLocation(targetRoot, path.resolve(targetRoot, relativePath)) : { path: sourcePath },
|
|
86
|
+
summary: `Dependency-Check reports ${vulnName} as suppressed for ${fileName} (tool-reported; not independently verified)`,
|
|
87
|
+
rawValue: undefined,
|
|
88
|
+
resolutionState: "not-applicable",
|
|
89
|
+
staticAnalysisLimitations: ["Suppression is tool-reported only; this analysis does not verify the suppression is justified."],
|
|
90
|
+
}));
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (isExternal) {
|
|
94
|
+
candidates.push(makeCandidateEvidence({
|
|
95
|
+
ruleId: "android-optional-tool-dependency-check-evidence",
|
|
96
|
+
category: "android-dependency-check",
|
|
97
|
+
confidence: "low",
|
|
98
|
+
location: { path: sourcePath },
|
|
99
|
+
summary: `Dependency-Check vulnerability ${vulnName} affects ${fileName} (resolved from an external cache, not a target-contained path)`,
|
|
100
|
+
rawValue: undefined,
|
|
101
|
+
resolutionState: "unresolved",
|
|
102
|
+
staticAnalysisLimitations: ["Dependency resolved outside the target; not used as a target source location."],
|
|
103
|
+
}));
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (severity === undefined) {
|
|
107
|
+
candidates.push(makeCandidateEvidence({
|
|
108
|
+
ruleId: "android-optional-tool-dependency-check-evidence",
|
|
109
|
+
category: "android-dependency-check",
|
|
110
|
+
confidence: "medium",
|
|
111
|
+
location: buildAndroidSourceLocation(targetRoot, path.resolve(targetRoot, relativePath)),
|
|
112
|
+
summary: `Dependency-Check vulnerability ${vulnName} (unknown severity) affects ${fileName}`,
|
|
113
|
+
rawValue: description,
|
|
114
|
+
resolutionState: "resolved",
|
|
115
|
+
staticAnalysisLimitations: [`cwes=${cwes.join(",")}`, "Dependency-Check evidence only; does not prove the dependency is packaged into the final APK."],
|
|
116
|
+
}));
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
findings.push(makeAndroidFinding({
|
|
120
|
+
ruleId: "android-optional-tool-dependency-check-evidence",
|
|
121
|
+
title: `Dependency-Check vulnerability ${vulnName} in ${fileName}`,
|
|
122
|
+
severity,
|
|
123
|
+
confidence: "medium",
|
|
124
|
+
description: description || `Dependency-Check reported a vulnerability for this dependency.`,
|
|
125
|
+
manifestPath: relativePath,
|
|
126
|
+
identity,
|
|
127
|
+
evidenceDetails: [`cwes=${cwes.join(",") || "(none)"}`, cvssVersion ? `cvssV${cvssVersion}=${score}` : undefined].filter((v) => Boolean(v)),
|
|
128
|
+
recommendation: "Upgrade the affected dependency to a patched version.",
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { malformed: false, findings, candidates, dependencyCount: dependencies.length, truncated };
|
|
133
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { resolveCommand } from "../../../../core/resolveCommand.js";
|
|
2
|
+
// PATH resolution itself legitimately needs the real environment (a
|
|
3
|
+
// minimized env would defeat PATH search); only the later *analysis* spawn
|
|
4
|
+
// uses the minimized environment built by minimalEnvironment.ts.
|
|
5
|
+
export function discoverAllowlistedExecutable(candidateBasenames, cwd) {
|
|
6
|
+
for (const basename of candidateBasenames) {
|
|
7
|
+
const resolved = resolveCommand(basename, { cwd, env: process.env, allowPowerShellShim: false });
|
|
8
|
+
if (resolved.resolutionKind !== "unavailable") {
|
|
9
|
+
return { available: true, command: basename, basename };
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return { available: false };
|
|
13
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { ANDROID_EXTERNAL_TOOL_IDS, ANDROID_EXTERNAL_TOOL_ORDER, ANDROID_EXTERNAL_TOOL_NETWORK_POLICIES, ANDROID_EXTERNAL_TOOL_STATUSES, isAndroidExternalSecurityToolId, normalizeAndroidExternalToolRequest, notRequestedResult, } from "./types.js";
|
|
2
|
+
export { discoverAllowlistedExecutable } from "./discoverExecutable.js";
|
|
3
|
+
export { buildMinimalEnvironment } from "./minimalEnvironment.js";
|
|
4
|
+
export { boundedText, safeJsonParse, DEFAULT_MAX_STDOUT_BYTES, DEFAULT_MAX_STDERR_BYTES, DEFAULT_MAX_REPORT_BYTES, DEFAULT_MAX_MESSAGE_LENGTH, DEFAULT_MAX_SNIPPET_LENGTH } from "./boundedOutput.js";
|
|
5
|
+
export { writeExternalToolArtifact, copyExternalToolArtifactFromTarget } from "./artifacts.js";
|
|
6
|
+
export { captureTargetSnapshot, buildExternalToolMutationReport, findFreshFiles } from "./mutation.js";
|
|
7
|
+
export { runBoundedExternalTool, buildCommandSummary, createRealExternalToolExecutor, VERSION_PROBE_TIMEOUT_MS } from "./runBoundedExternalTool.js";
|
|
8
|
+
export { runRequestedAndroidExternalTools } from "./runRequestedAndroidExternalTools.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// v0.4.1 Batch 7 — minimal child-process environment construction.
|
|
3
|
+
//
|
|
4
|
+
// Neither existing process runner (runSecurityCommand, runMeasuredCommand)
|
|
5
|
+
// minimizes the environment — both inherit the full caller process.env.
|
|
6
|
+
// External tools are the first Batch 7 use case that must NOT propagate
|
|
7
|
+
// arbitrary caller credentials (SEMGREP_APP_TOKEN, GITHUB_TOKEN, NVD_API_KEY,
|
|
8
|
+
// cloud credentials, ...) to a spawned child, so this is new, additive
|
|
9
|
+
// infrastructure rather than a correction to either existing runner.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
const ALWAYS_ALLOWED_KEYS = ["PATH", "Path", "SystemRoot", "ComSpec", "COMSPEC", "PATHEXT", "TEMP", "TMP", "HOME", "USERPROFILE", "LANG", "LC_ALL"];
|
|
12
|
+
// Never returns a value that was not explicitly allowlisted — this is a
|
|
13
|
+
// security invariant (agents.txt Batch 7 section 9.10/23.15), not a
|
|
14
|
+
// convenience default. A key present in the source env but not in the
|
|
15
|
+
// allowlist is silently omitted, never partially masked or renamed.
|
|
16
|
+
export function buildMinimalEnvironment(options = {}) {
|
|
17
|
+
const source = options.source ?? process.env;
|
|
18
|
+
const allowedKeys = new Set([...ALWAYS_ALLOWED_KEYS, ...(options.additionalAllowedKeys ?? [])]);
|
|
19
|
+
const env = {};
|
|
20
|
+
for (const key of allowedKeys) {
|
|
21
|
+
const value = source[key];
|
|
22
|
+
if (value !== undefined)
|
|
23
|
+
env[key] = value;
|
|
24
|
+
}
|
|
25
|
+
return env;
|
|
26
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { captureTargetSnapshot, diffTargetSnapshots } from "../../../../securityValidation/attackScenarios/targetSnapshot.js";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// v0.4.1 Batch 7 — target mutation evidence for external-tool execution.
|
|
6
|
+
//
|
|
7
|
+
// Reuses the existing generic snapshot/diff primitives
|
|
8
|
+
// (src/securityValidation/attackScenarios/targetSnapshot.ts) exactly as
|
|
9
|
+
// src/mobile/android/gradle/validate/targetMutation.ts does — no second
|
|
10
|
+
// target-status model. Semgrep, OSV, and Dependency-Check are expected to
|
|
11
|
+
// produce zero target mutation (all their output goes to the external
|
|
12
|
+
// artifact root); Android Lint reuses the existing Android-generated-path
|
|
13
|
+
// classifier since it runs through the existing Gradle lint-debug operation.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
export { captureTargetSnapshot };
|
|
16
|
+
export function buildExternalToolMutationReport(before, after, isExpectedGeneratedPath = () => false) {
|
|
17
|
+
const diff = diffTargetSnapshots(before, after);
|
|
18
|
+
if (!diff.comparable) {
|
|
19
|
+
return { comparable: false, reason: diff.reason, expectedGeneratedChanges: [], unexpectedChanges: [] };
|
|
20
|
+
}
|
|
21
|
+
const expectedGeneratedChanges = [];
|
|
22
|
+
const unexpectedChanges = [];
|
|
23
|
+
for (const entry of diff.newEntries) {
|
|
24
|
+
const isUntrackedOrAdded = entry.code === "??" || entry.code.includes("A");
|
|
25
|
+
if (isUntrackedOrAdded && isExpectedGeneratedPath(entry.path)) {
|
|
26
|
+
expectedGeneratedChanges.push(entry.path);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
unexpectedChanges.push(entry.path);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
comparable: true,
|
|
34
|
+
expectedGeneratedChanges: expectedGeneratedChanges.sort((a, b) => a.localeCompare(b)),
|
|
35
|
+
unexpectedChanges: unexpectedChanges.sort((a, b) => a.localeCompare(b)),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// Bounded, best-effort discovery of files under `dir` that did not exist (or
|
|
39
|
+
// had a different mtime) before the run — used by the Android Lint adapter
|
|
40
|
+
// to find fresh XML/SARIF reports without trusting a stale pre-existing
|
|
41
|
+
// report. Never follows symlinks outside `dir`, never recurses unbounded.
|
|
42
|
+
export function findFreshFiles(dir, extensions, sinceEpochMs, maxDepth = 6) {
|
|
43
|
+
const results = [];
|
|
44
|
+
function walk(current, depth) {
|
|
45
|
+
if (depth > maxDepth)
|
|
46
|
+
return;
|
|
47
|
+
let entries;
|
|
48
|
+
try {
|
|
49
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
const full = path.join(current, entry.name);
|
|
56
|
+
if (entry.isDirectory()) {
|
|
57
|
+
walk(full, depth + 1);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!entry.isFile())
|
|
61
|
+
continue;
|
|
62
|
+
if (!extensions.some((ext) => entry.name.toLowerCase().endsWith(ext)))
|
|
63
|
+
continue;
|
|
64
|
+
let stat;
|
|
65
|
+
try {
|
|
66
|
+
stat = fs.statSync(full);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (stat.mtimeMs >= sinceEpochMs)
|
|
72
|
+
results.push(full);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
walk(dir, 0);
|
|
76
|
+
return results.sort((a, b) => a.localeCompare(b));
|
|
77
|
+
}
|