From ad74c3afa5b84868750e144617b65be7b8921039 Mon Sep 17 00:00:00 2001 From: nikhil-mittal-165 Date: Tue, 27 Oct 2026 11:35:21 +0530 Subject: [PATCH 1/3] FIX Remediate SFGE transitive dependency vulnerabilities Removes all known-vulnerable libraries from the SFGE Java runtime classpath (osv-scanner on dist/java-lib: 84 advisories across 24 artifacts -> 0; 97 JARs -> 35). - Remove unused gremlin-driver (and the netty-all constraint). It was the sole source of Netty and AWS SDK JARs. - Move Gremlin from 4.0.0-beta.1 to stable 3.7.7. Every 4.0 beta bundles Jackson 2.15.2 inside gremlin-shaded. (3.8.2 changes traversal behavior and breaks tests.) - Exclude gremlin-shaded (only used for GraphSON/Gryo/GraphML IO, which SFGE does not use; its bundled Jackson cannot be patched via constraints). Add a minimal JsonProcessingException stand-in because TinkerGraph references the type. - Exclude commons-collections 3.x (no patched release; SFGE uses collections4). - Upgrade log4j to 2.25.5; force patched commons-configuration2, commons-beanutils and snakeyaml; declare commons-lang3 and commons-text explicitly. - Add verifyNoBannedDependencies build check and NoVulnerableBundledLibrariesTest to prevent regressions. Co-Authored-By: Claude Sonnet 5.5 --- .../gradle/libs.versions.toml | 21 ++++--- .../sfge/build.gradle.kts | 61 +++++++++++++++++-- .../jackson/core/JsonProcessingException.java | 23 +++++++ .../ops/NoVulnerableBundledLibrariesTest.java | 43 +++++++++++++ 4 files changed, 137 insertions(+), 11 deletions(-) create mode 100644 packages/code-analyzer-sfge-engine/sfge/src/main/java/org/apache/tinkerpop/shaded/jackson/core/JsonProcessingException.java create mode 100644 packages/code-analyzer-sfge-engine/sfge/src/test/java/com/salesforce/graph/ops/NoVulnerableBundledLibrariesTest.java 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/sfge/build.gradle.kts b/packages/code-analyzer-sfge-engine/sfge/build.gradle.kts index f5d29853..a1619f18 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")) @@ -76,6 +93,42 @@ 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")); + } +} From 6629d6b9865f60144521dd2ed200fe2120fab006 Mon Sep 17 00:00:00 2001 From: nikhil-mittal-165 Date: Tue, 27 Oct 2026 12:27:14 +0530 Subject: [PATCH 2/3] Bump SFGE engine version to 0.26.0-SNAPSHOT Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/code-analyzer-sfge-engine/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From 41605b91642fd6be2a6c93986511c0bf54bd4f94 Mon Sep 17 00:00:00 2001 From: nikhil-mittal-165 Date: Tue, 3 Nov 2026 11:44:23 +0530 Subject: [PATCH 3/3] FIX Remove stale dependency jars before copying SFGE runtime dependencies copyDependencies only added files to dist/java-lib, so an incremental build after switching branches could keep jars that were removed from the runtime classpath (Netty, AWS, shaded Jackson, etc.) and still pass the banned dependency check. Delete everything except sfge-*.jar first so the packed directory always matches a clean build. Co-Authored-By: Claude Sonnet 5.5 --- packages/code-analyzer-sfge-engine/sfge/build.gradle.kts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/code-analyzer-sfge-engine/sfge/build.gradle.kts b/packages/code-analyzer-sfge-engine/sfge/build.gradle.kts index a1619f18..d4076831 100644 --- a/packages/code-analyzer-sfge-engine/sfge/build.gradle.kts +++ b/packages/code-analyzer-sfge-engine/sfge/build.gradle.kts @@ -88,6 +88,12 @@ 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