diff --git a/packages/code-analyzer-sfge-engine/gradle/libs.versions.toml b/packages/code-analyzer-sfge-engine/gradle/libs.versions.toml index ab26b93f..423a8c57 100644 --- a/packages/code-analyzer-sfge-engine/gradle/libs.versions.toml +++ b/packages/code-analyzer-sfge-engine/gradle/libs.versions.toml @@ -10,11 +10,10 @@ hamcrest = "3.0" junit-jupiter = "5.13.4" commons-cli = "1.10.0" commons-collections4 = "4.5.0" -tinkergraph-gremlin = "4.0.0-beta.1" -gremlin-driver = "4.0.0-beta.1" +tinkergraph-gremlin = "3.7.7" antlr-runtime = "3.5.3" -log4j-api = "2.25.1" -log4j-core = "2.25.1" +log4j-api = "2.25.5" +log4j-core = "2.25.5" gson = "2.13.2" guava = "33.4.8-jre" jsr305 = "3.0.2" @@ -23,7 +22,12 @@ reflections = "0.10.2" asm = "9.8" mockito-core = "5.19.0" mockito-junit-jupiter = "5.19.0" -netty-all = "4.1.127.Final" +# Versions below force patched releases of dependencies that arrive transitively (via gremlin-core). See sfge/build.gradle.kts. +commons-configuration2 = "2.15.1" +commons-beanutils = "1.11.0" +commons-lang3 = "3.21.0" +commons-text = "1.15.0" +snakeyaml = "2.7" [libraries] hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" } @@ -31,7 +35,6 @@ junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "jun commons-cli = { module = "commons-cli:commons-cli", version.ref = "commons-cli" } commons-collections4 = { module = "org.apache.commons:commons-collections4", version.ref = "commons-collections4" } tinkergraph-gremlin = { module = "org.apache.tinkerpop:tinkergraph-gremlin", version.ref = "tinkergraph-gremlin" } -gremlin-driver = { module = "org.apache.tinkerpop:gremlin-driver", version.ref = "gremlin-driver" } antlr-runtime = { module = "org.antlr:antlr-runtime", version.ref = "antlr-runtime" } log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j-api" } log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j-core" } @@ -44,4 +47,8 @@ asm = { module = "org.ow2.asm:asm", version.ref = "asm" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito-core" } mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito-junit-jupiter" } -netty-all = { module = "io.netty:netty-all", version.ref = "netty-all" } +commons-configuration2 = { module = "org.apache.commons:commons-configuration2", version.ref = "commons-configuration2" } +commons-beanutils = { module = "commons-beanutils:commons-beanutils", version.ref = "commons-beanutils" } +commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commons-lang3" } +commons-text = { module = "org.apache.commons:commons-text", version.ref = "commons-text" } +snakeyaml = { module = "org.yaml:snakeyaml", version.ref = "snakeyaml" } diff --git a/packages/code-analyzer-sfge-engine/package.json b/packages/code-analyzer-sfge-engine/package.json index b9583ecc..2826a8d3 100644 --- a/packages/code-analyzer-sfge-engine/package.json +++ b/packages/code-analyzer-sfge-engine/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-sfge-engine", "description": "Plugin package that adds 'Salesforce Graph Engine' as an engine into Salesforce Code Analyzer", - "version": "0.25.0", + "version": "0.26.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", diff --git a/packages/code-analyzer-sfge-engine/sfge/build.gradle.kts b/packages/code-analyzer-sfge-engine/sfge/build.gradle.kts index f5d29853..d4076831 100644 --- a/packages/code-analyzer-sfge-engine/sfge/build.gradle.kts +++ b/packages/code-analyzer-sfge-engine/sfge/build.gradle.kts @@ -22,8 +22,18 @@ repositories { dependencies { implementation(libs.commons.cli) implementation(libs.commons.collections4) - implementation(libs.tinkergraph.gremlin) - implementation(libs.gremlin.driver) + implementation(libs.tinkergraph.gremlin) { + // commons-collections 3.x (pulled in by commons-beanutils) has known vulnerabilities and no patched release. + // SFGE and the Gremlin graph layer do not use it, so it is removed from the classpath entirely. + exclude(group = "commons-collections", module = "commons-collections") + // gremlin-shaded bundles (relocated) copies of Jackson and Kryo that are only used for graph import/export + // (GraphSON/Gryo/GraphML). SFGE only builds and traverses an in-memory graph, and the bundled Jackson version + // contains known vulnerabilities that cannot be patched via dependency constraints, so we do not ship it. + exclude(group = "org.apache.tinkerpop", module = "gremlin-shaded") + } + // These are used directly by SFGE source (they previously arrived only transitively), so declare them explicitly. + implementation(libs.commons.lang3) + implementation(libs.commons.text) implementation(libs.antlr.runtime) implementation(libs.log4j.api) implementation(libs.log4j.core) @@ -35,9 +45,16 @@ dependencies { } implementation(libs.reflections) implementation(libs.asm) + // Force patched versions of transitive dependencies of gremlin-core (embedded in-memory graph). constraints { - implementation(libs.netty.all) { - because("Force specific version of transitive dependency") + implementation(libs.commons.configuration2) { + because("Patched version of a transitive dependency of gremlin-core") + } + implementation(libs.commons.beanutils) { + because("Patched version of a transitive dependency of gremlin-core") + } + implementation(libs.snakeyaml) { + because("Patched version of a transitive dependency of gremlin-core") } } implementation(files("lib/apex-jorje-lsp-sfge.jar")) @@ -71,11 +88,53 @@ tasks.jar { tasks.register("copyDependencies") { from(configurations.runtimeClasspath) // This includes all runtime dependencies into(javaLibDir) // Target directory for dependencies + // The directory is also the output of the jar task (sfge-*.jar), so we can't use Sync. Instead, remove any stale + // dependency jars left over from earlier builds (e.g. ones since removed from the runtime classpath) so that an + // incremental build packs exactly the same dependency set as a clean build. + doFirst { + delete(fileTree(javaLibDir) { exclude("sfge-*.jar") }) + } } tasks.build { dependsOn("copyDependencies") // Ensure dependencies are copied during the build process } +// Fail the build if a library that is known to carry unpatchable vulnerabilities (or that SFGE has no need for) sneaks +// back onto the runtime classpath, e.g., through a Gremlin upgrade. Everything on the runtime classpath gets shipped. +val bannedRuntimeDependencies = mapOf( + "io.netty" to "Only needed by gremlin-driver (remote Gremlin client), which SFGE does not use", + "software.amazon.awssdk" to "Only needed by gremlin-driver (remote Gremlin client), which SFGE does not use", + "org.apache.tinkerpop:gremlin-driver" to "SFGE only uses the embedded in-memory graph", + "org.apache.tinkerpop:gremlin-util" to "SFGE only uses the embedded in-memory graph", + "org.apache.tinkerpop:gremlin-shaded" to "Bundles Jackson/Kryo with known vulnerabilities that cannot be patched via constraints", + "commons-collections:commons-collections" to "3.x has known vulnerabilities and no patched release (SFGE uses commons-collections4)" +) +tasks.register("verifyNoBannedDependencies") { + group = "verification" + description = "Fails if a banned dependency is on the runtime classpath (and therefore would be shipped)." + val resolvedModules = configurations.runtimeClasspath.map { config -> + config.incoming.resolutionResult.allComponents + .mapNotNull { it.moduleVersion } + .map { "${it.group}:${it.name}:${it.version}" } + } + doLast { + val violations = resolvedModules.get().flatMap { coordinates -> + val (group, name) = coordinates.split(":") + bannedRuntimeDependencies.filter { (banned, _) -> banned == group || banned == "$group:$name" } + .map { (_, reason) -> "$coordinates -> $reason" } + } + if (violations.isNotEmpty()) { + throw GradleException("Banned dependencies found on the runtime classpath:\n " + violations.joinToString("\n ")) + } + } +} +tasks.check { + dependsOn("verifyNoBannedDependencies") +} +tasks.build { + dependsOn("verifyNoBannedDependencies") +} + // ======== TEST RELATED TASKS ========================================================================================= jacoco { diff --git a/packages/code-analyzer-sfge-engine/sfge/src/main/java/org/apache/tinkerpop/shaded/jackson/core/JsonProcessingException.java b/packages/code-analyzer-sfge-engine/sfge/src/main/java/org/apache/tinkerpop/shaded/jackson/core/JsonProcessingException.java new file mode 100644 index 00000000..300ba775 --- /dev/null +++ b/packages/code-analyzer-sfge-engine/sfge/src/main/java/org/apache/tinkerpop/shaded/jackson/core/JsonProcessingException.java @@ -0,0 +1,23 @@ +package org.apache.tinkerpop.shaded.jackson.core; + +import java.io.IOException; + +/** + * Minimal stand-in for the relocated Jackson exception type that {@code TinkerGraph} references in a {@code catch} + * clause. + * + *

Why this exists: {@code gremlin-shaded} bundles (relocated) copies of Jackson and Kryo. The Jackson version it + * bundles contains known vulnerabilities that cannot be fixed with dependency constraints, and SFGE does not use it + * since SFGE only builds and traverses an in-memory graph (no GraphSON/Gryo/GraphML import or export). So we exclude + * {@code gremlin-shaded} from the runtime classpath (see {@code build.gradle.kts}). However, the JVM must still be able + * to load this one exception class when it verifies {@code TinkerGraph}, even though no Jackson code ever runs. + * + *

This class intentionally contains no Jackson code. Remove it (and re-evaluate the {@code gremlin-shaded} exclusion) + * when upgrading to a Gremlin release whose bundled Jackson is patched. {@code NoVulnerableBundledLibrariesTest} + * guards this behavior. + */ +public class JsonProcessingException extends IOException { + public JsonProcessingException(String message) { + super(message); + } +} diff --git a/packages/code-analyzer-sfge-engine/sfge/src/test/java/com/salesforce/graph/ops/NoVulnerableBundledLibrariesTest.java b/packages/code-analyzer-sfge-engine/sfge/src/test/java/com/salesforce/graph/ops/NoVulnerableBundledLibrariesTest.java new file mode 100644 index 00000000..679f3532 --- /dev/null +++ b/packages/code-analyzer-sfge-engine/sfge/src/test/java/com/salesforce/graph/ops/NoVulnerableBundledLibrariesTest.java @@ -0,0 +1,43 @@ +package com.salesforce.graph.ops; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +/** + * Guards the dependency-hygiene decisions documented in {@code build.gradle.kts}: SFGE only needs an in-memory + * TinkerGraph, and must not ship libraries that carry known vulnerabilities. + */ +public class NoVulnerableBundledLibrariesTest { + @Test + public void inMemoryGraphWorksWithoutGremlinShaded() { + // Fails with NoClassDefFoundError if TinkerGraph needs anything from gremlin-shaded beyond what we provide. + assertNotNull(GraphUtil.getGraph()); + } + + @Test + public void gremlinShadedJacksonImplementationIsNotOnClasspath() { + assertThrows( + ClassNotFoundException.class, + () -> Class.forName("org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper")); + assertThrows( + ClassNotFoundException.class, + () -> Class.forName("org.apache.tinkerpop.shaded.jackson.core.JsonFactory")); + } + + @Test + public void commonsCollections3IsNotOnClasspath() { + assertThrows( + ClassNotFoundException.class, + () -> Class.forName("org.apache.commons.collections.FastHashMap")); + } + + @Test + public void nettyAndGremlinDriverAreNotOnClasspath() { + assertThrows(ClassNotFoundException.class, () -> Class.forName("io.netty.channel.Channel")); + assertThrows( + ClassNotFoundException.class, + () -> Class.forName("org.apache.tinkerpop.gremlin.driver.Cluster")); + } +}