瀏覽代碼

[8.19] [Gradle] Configure transitive dependencies via ComponentMetadataRules (#134169) (#134714)

* [Gradle] Configure transitive dependencies via ComponentMetadataRules (#134169)

This introduces ComponentMetadataRulesPlugin that contains declarative logic for dealing with transitive dependencies on a per dependency level.
Ulitmately we want more finegrained control over our dependencies without loosing information about transitive dependencies.

The initial list of the applied component metadata rules will be more finegrained over time. Initially this is mostly a reflection of how we brought in dependencies before by basically making the transitive dependencies we identified as required where added as direct dependency.

I started looking through the existing dependencies applyging the following pattern:

if no problematic transitive dependency detected, do not apply any component meta data rule.
if only non group dependencies have been problematic, use ExcludeOtherGroupsTransitiveRule which allows transitive dependencies brought with the same groupId as the parent but excludes all others.
Otherwise exclude all transitive dependencies by applying ExcludeAllTransitivesRule
We will add more specific rules in the future as we see the need to "fix' component metadata of thirdparty dependencies.

This change replaces our plain transitive = false approach for non elasticsearch dependencies

Historically we have solved dealing with transitive dependencies and component metadata in this regard by just ignoring it and bringing in dependencies explicitly. This results in

weaker control what we bring in and why
loose information why a dependency is needed and how its tight ot another dependency on the classpath
transitive behavior differed in different context as we only have applied transitivity
Furthermore the way we have configured transitive = false for each dependency resulted in other problems when using other newer Gradle APIs like test suites.

(cherry picked from commit a0e6ea160922c425f12da5398fd448858d0c1e38)

# Conflicts:
#	test/fixtures/url-fixture/build.gradle

* Fix merge conflicts
Rene Groeschke 3 周之前
父節點
當前提交
cb2f88d0c9

+ 64 - 0
BUILDING.md

@@ -97,6 +97,70 @@ will have the `origin` attribute been set to `Generated by Gradle`.
 > If you want to add a level of verification you can manually confirm the checksum (e.g. by looking it up on the website of the library)
 > Please replace the content of the `origin` attribute by `official site` in that case.
 
+##### Handling transitive dependencies
+
+Dependency management is a critical aspect of maintaining a secure and reliable build system, requiring explicit control over what we rely on. The Elasticsearch build mainly uses component metadata rules declared in the `ComponentMetadataRulesPlugin`
+plugin to manage transitive dependencies and avoid version conflicts.
+This approach ensures we have explicit control over all dependencies used in the build.
+
+###### General Guidelines
+
+1. **Avoid unused transitive dependencies** - Dependencies that are not actually used by our code should be excluded to reduce the attack surface and avoid potential conflicts.
+
+2. **Prefer versions declared in `build-tools-internal/version.properties`** - All dependency versions should be centrally managed in this file to ensure consistency across the entire build.
+
+3. **Libraries required to compile our code should be direct dependencies** - If we directly use a library in our source code, it should be declared as a direct dependency rather than relying on it being transitively available.
+
+###### Component Metadata Rules
+
+We use two main types of component metadata rules at this point to manage transitive dependencies:
+
+- **`ExcludeAllTransitivesRule`** - Excludes all transitive dependencies for libraries where we want complete control over dependencies or the transitive dependencies are unused.
+
+- **`ExcludeOtherGroupsTransitiveRule`** - Excludes transitive dependencies that don't belong to the same group as the direct dependency, while keeping same-group dependencies.
+-
+- **`ExcludeByGroup`** - Excludes transitive dependencies that match a specific groupId while keeping all other transitive dependencies with different groupIds.
+
+Examples from the `ComponentMetadataRulesPlugin`:
+
+```gradle
+// Exclude all transitives - used when transitive deps are unused or problematic
+components.withModule("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor", ExcludeAllTransitivesRule.class);
+
+// Exclude other groups - used when we want same-group deps but not external ones
+components.withModule("com.azure:azure-core", ExcludeOtherGroupsTransitiveRule.class);
+
+// Exclude only specific groups - used when we want exclude specific group of transitive deps.
+components.withModule("org.apache.logging.log4j:log4j-api", ExcludeByGroup.class, rule -> {
+    rule.params(List.of("biz.aQute.bnd", "org.osgi"));
+});
+```
+
+###### Common Scenarios
+
+**Version Conflicts**: When a transitive dependency brings in a different version than what we use:
+```gradle
+// brings in jackson-databind and jackson-annotations not used
+components.withModule("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor", ExcludeAllTransitivesRule.class);
+```
+
+**Unused Dependencies**: When transitive dependencies are not actually used:
+```gradle
+// brings in azure-core-http-netty. not used
+components.withModule("com.azure:azure-core-http-netty", ExcludeAllTransitivesRule.class);
+```
+
+**Mismatching Version Dependencies**: When other versions are required:
+```gradle
+// brings in org.slf4j:slf4j-api:1.7.25. We use 2.0.6
+components.withModule("org.apache.directory.api:api-asn1-ber", ExcludeOtherGroupsTransitiveRule.class);
+```
+
+When adding or updating dependencies, ensure that any required transitive dependencies are either:
+1. Already available as direct dependencies with compatible versions
+2. Added as direct dependencies if they're actually used by our code
+3. Properly excluded if they're not needed
+
 #### Custom plugin and task implementations
 
 Build logic that is used across multiple subprojects should be considered to be moved into a Gradle plugin with according Gradle task implementation.

+ 3 - 0
build-conventions/build.gradle

@@ -72,6 +72,9 @@ repositories {
 }
 
 dependencies {
+  constraints {
+    api("org.slf4j:slf4j-api:2.0.6")
+  }
   api buildLibs.maven.model
   api buildLibs.shadow.plugin
   api buildLibs.apache.rat

+ 7 - 0
build-tools-internal/build.gradle

@@ -40,6 +40,10 @@ gradlePlugin {
       id = 'elasticsearch.build-complete'
       implementationClass = 'org.elasticsearch.gradle.internal.ElasticsearchBuildCompletePlugin'
     }
+    componentMetadataRules {
+      id = 'elasticsearch.component-metadata-rules'
+      implementationClass = 'org.elasticsearch.gradle.internal.dependencies.rules.ComponentMetadataRulesPlugin'
+    }
     distro {
       id = 'elasticsearch.distro'
       implementationClass = 'org.elasticsearch.gradle.internal.distribution.ElasticsearchDistributionPlugin'
@@ -281,6 +285,9 @@ dependencies {
     testImplementation buildLibs.asm
     integTestImplementation buildLibs.asm
     api(buildLibs.snakeyaml)
+    api("org.slf4j:slf4j-api:2.0.6") {
+      because("Align with what we use in production")
+    }
   }
   // Forcefully downgrade the jackson platform as used in production
   api enforcedPlatform(buildLibs.jackson.platform)

+ 0 - 54
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaBasePlugin.java

@@ -16,18 +16,14 @@ import org.elasticsearch.gradle.internal.info.GlobalBuildInfoPlugin;
 import org.elasticsearch.gradle.internal.test.MutedTestPlugin;
 import org.elasticsearch.gradle.internal.test.TestUtil;
 import org.elasticsearch.gradle.test.SystemPropertyCommandLineArgumentProvider;
-import org.elasticsearch.gradle.util.GradleUtils;
 import org.gradle.api.JavaVersion;
 import org.gradle.api.Plugin;
 import org.gradle.api.Project;
 import org.gradle.api.artifacts.Configuration;
-import org.gradle.api.artifacts.ResolutionStrategy;
 import org.gradle.api.file.FileCollection;
 import org.gradle.api.plugins.JavaBasePlugin;
 import org.gradle.api.plugins.JavaPluginExtension;
 import org.gradle.api.provider.Provider;
-import org.gradle.api.tasks.SourceSet;
-import org.gradle.api.tasks.SourceSetContainer;
 import org.gradle.api.tasks.compile.AbstractCompile;
 import org.gradle.api.tasks.compile.CompileOptions;
 import org.gradle.api.tasks.compile.GroovyCompile;
@@ -67,8 +63,6 @@ public class ElasticsearchJavaBasePlugin implements Plugin<Project> {
         project.getPluginManager().apply(ElasticsearchTestBasePlugin.class);
         project.getPluginManager().apply(PrecommitTaskPlugin.class);
         project.getPluginManager().apply(MutedTestPlugin.class);
-
-        configureConfigurations(project);
         configureCompile(project);
         configureInputNormalization(project);
         configureNativeLibraryPath(project);
@@ -77,54 +71,6 @@ public class ElasticsearchJavaBasePlugin implements Plugin<Project> {
         project.getExtensions().getExtraProperties().set("versions", VersionProperties.getVersions());
     }
 
-    /**
-     * Makes dependencies non-transitive.
-     * <p>
-     * Gradle allows setting all dependencies as non-transitive very easily.
-     * Sadly this mechanism does not translate into maven pom generation. In order
-     * to effectively make the pom act as if it has no transitive dependencies,
-     * we must exclude each transitive dependency of each direct dependency.
-     * <p>
-     * Determining the transitive deps of a dependency which has been resolved as
-     * non-transitive is difficult because the process of resolving removes the
-     * transitive deps. To sidestep this issue, we create a configuration per
-     * direct dependency version. This specially named and unique configuration
-     * will contain all of the transitive dependencies of this particular
-     * dependency. We can then use this configuration during pom generation
-     * to iterate the transitive dependencies and add excludes.
-     */
-    public static void configureConfigurations(Project project) {
-        // we are not shipping these jars, we act like dumb consumers of these things
-        if (project.getPath().startsWith(":test:fixtures") || project.getPath().equals(":build-tools")) {
-            return;
-        }
-        // fail on any conflicting dependency versions
-        project.getConfigurations().all(configuration -> {
-            if (configuration.getName().endsWith("Fixture")) {
-                // just a self contained test-fixture configuration, likely transitive and hellacious
-                return;
-            }
-            configuration.resolutionStrategy(ResolutionStrategy::failOnVersionConflict);
-        });
-
-        // disable transitive dependency management
-        SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
-        sourceSets.all(sourceSet -> disableTransitiveDependenciesForSourceSet(project, sourceSet));
-    }
-
-    private static void disableTransitiveDependenciesForSourceSet(Project project, SourceSet sourceSet) {
-        List<String> sourceSetConfigurationNames = List.of(
-            sourceSet.getApiConfigurationName(),
-            sourceSet.getImplementationConfigurationName(),
-            sourceSet.getCompileOnlyConfigurationName(),
-            sourceSet.getRuntimeOnlyConfigurationName()
-        );
-
-        project.getConfigurations()
-            .matching(c -> sourceSetConfigurationNames.contains(c.getName()))
-            .configureEach(GradleUtils::disableTransitiveDependencies);
-    }
-
     /**
      * Adds compiler settings to the project
      */

+ 6 - 4
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaModulePathPlugin.java

@@ -21,6 +21,7 @@ import org.gradle.api.artifacts.component.ProjectComponentIdentifier;
 import org.gradle.api.artifacts.result.ResolvedComponentResult;
 import org.gradle.api.artifacts.result.ResolvedDependencyResult;
 import org.gradle.api.attributes.LibraryElements;
+import org.gradle.api.attributes.Usage;
 import org.gradle.api.file.FileCollection;
 import org.gradle.api.logging.Logger;
 import org.gradle.api.plugins.JavaPlugin;
@@ -75,12 +76,13 @@ public abstract class ElasticsearchJavaModulePathPlugin implements Plugin<Projec
             it.extendsFrom(compileClasspath);
             it.setCanBeResolved(true);
             it.setCanBeConsumed(false); // we don't want this configuration used by dependent projects
-            it.attributes(
-                attrs -> attrs.attribute(
+            it.attributes(attrs -> {
+                attrs.attribute(
                     LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
                     project.getObjects().named(LibraryElements.class, LibraryElements.CLASSES)
-                )
-            );
+                );
+                attrs.attribute(Usage.USAGE_ATTRIBUTE, project.getObjects().named(Usage.class, Usage.JAVA_API));
+            });
         }).getIncoming().artifactView(it -> {
             it.componentFilter(cf -> {
                 var visited = new HashSet<ComponentIdentifier>();

+ 501 - 0
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/dependencies/rules/ComponentMetadataRulesPlugin.java

@@ -0,0 +1,501 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.gradle.internal.dependencies.rules;
+
+import org.gradle.api.Plugin;
+import org.gradle.api.artifacts.dsl.ComponentMetadataHandler;
+import org.gradle.api.initialization.Settings;
+
+import java.util.List;
+
+/**
+ * A settings plugin that configures component metadata rules for dependency management.
+ * This plugin centralizes the configuration of transitive dependency exclusion rules.
+ * Since we want to use the same rules in serverless an stack setup we cannot just put
+ * this into settings.gradle but encapsulate it in a plugin that can be reused in the
+ * context of serverless.
+ */
+public class ComponentMetadataRulesPlugin implements Plugin<Settings> {
+
+    @Override
+    public void apply(Settings settings) {
+        ComponentMetadataHandler components = settings.getDependencyResolutionManagement().getComponents();
+
+        // Azure dependencies
+        components.withModule("com.azure:azure-core", ExcludeOtherGroupsTransitiveRule.class);
+        // brings in azure-core-http-netty. not used
+        components.withModule("com.azure:azure-core-http-netty", ExcludeAllTransitivesRule.class);
+        components.withModule("com.azure:azure-core-http-okhttp", ExcludeOtherGroupsTransitiveRule.class);
+
+        // brings in net.java.dev.jna:jna-platform:5.6.0. We use 5.12.1.
+        // brings in com.azure:azure-core-http-netty:1.15.1
+        components.withModule("com.azure:azure-identity", ExcludeAllTransitivesRule.class);
+
+        // brings in com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.2. We use 2.15.0
+        components.withModule("com.azure:azure-storage-blob", ExcludeOtherGroupsTransitiveRule.class);
+        components.withModule("com.azure:azure-storage-blob-batch", ExcludeOtherGroupsTransitiveRule.class);
+        components.withModule("com.azure:azure-storage-common", ExcludeOtherGroupsTransitiveRule.class);
+        components.withModule("com.azure:azure-storage-internal-avro", ExcludeAllTransitivesRule.class);
+
+        // Testing dependencies
+        components.withModule("com.carrotsearch.randomizedtesting:randomizedtesting-runner", ExcludeAllTransitivesRule.class);
+
+        // Jackson dependencies
+        components.withModule("com.fasterxml.jackson.core:jackson-core", ExcludeOtherGroupsTransitiveRule.class);
+        // brings in jackson-databind and jackson-annotations not used
+        components.withModule("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor", ExcludeAllTransitivesRule.class);
+        components.withModule("com.fasterxml.jackson.dataformat:jackson-dataformat-smile", ExcludeAllTransitivesRule.class);
+
+        // brings woodstox-core 6.7.0, we use 6.5.1
+        components.withModule("com.fasterxml.jackson.dataformat:jackson-dataformat-xml", ExcludeOtherGroupsTransitiveRule.class);
+
+        // brings in snakeyaml with wrong version
+        components.withModule("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", ExcludeOtherGroupsTransitiveRule.class);
+
+        components.withModule("com.fasterxml.jackson.module:jackson-module-jaxb-annotations", ExcludeOtherGroupsTransitiveRule.class);
+
+        // Google dependencies
+        components.withModule("com.google.api-client:google-api-client", ExcludeAllTransitivesRule.class);
+        components.withModule("com.google.api.grpc:proto-google-cloud-storage-v2", ExcludeOtherGroupsTransitiveRule.class);
+
+        // brings com.google.api.grpc:proto-google-common-protos:2.9.2 we
+        components.withModule("com.google.api.grpc:proto-google-iam-v1", ExcludeAllTransitivesRule.class);
+        components.withModule("com.google.api:api-common", ExcludeAllTransitivesRule.class);
+        components.withModule("com.google.api:gax", ExcludeAllTransitivesRule.class);
+        components.withModule("com.google.api:gax-httpjson", ExcludeAllTransitivesRule.class);
+        components.withModule("com.google.apis:google-api-services-storage", ExcludeAllTransitivesRule.class);
+        components.withModule("com.google.auth:google-auth-library-oauth2-http", ExcludeAllTransitivesRule.class);
+        components.withModule("com.google.cloud:google-cloud-core", ExcludeAllTransitivesRule.class);
+        components.withModule("com.google.cloud:google-cloud-core-http", ExcludeAllTransitivesRule.class);
+        components.withModule("com.google.cloud:google-cloud-storage", ExcludeAllTransitivesRule.class);
+        components.withModule("com.google.code.gson:gson", ExcludeAllTransitivesRule.class);
+        components.withModule("com.google.guava:guava", ExcludeAllTransitivesRule.class);
+        components.withModule("com.google.http-client:google-http-client", ExcludeAllTransitivesRule.class);
+        components.withModule("com.google.http-client:google-http-client-appengine", ExcludeAllTransitivesRule.class);
+        components.withModule("com.google.http-client:google-http-client-gson", ExcludeAllTransitivesRule.class);
+        components.withModule("com.google.http-client:google-http-client-jackson2", ExcludeAllTransitivesRule.class);
+        components.withModule("com.google.jimfs:jimfs", ExcludeAllTransitivesRule.class);
+        components.withModule("com.google.oauth-client:google-oauth-client", ExcludeAllTransitivesRule.class);
+        components.withModule("com.google.protobuf:protobuf-java-util", ExcludeAllTransitivesRule.class);
+        components.withModule("com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer", ExcludeAllTransitivesRule.class);
+
+        // Other dependencies
+        components.withModule("com.maxmind.geoip2:geoip2", ExcludeAllTransitivesRule.class);
+
+        // Microsoft dependencies
+        components.withModule("com.microsoft.azure:azure-core", ExcludeAllTransitivesRule.class);
+        components.withModule("com.microsoft.azure:azure-svc-mgmt-compute", ExcludeAllTransitivesRule.class);
+        components.withModule("com.microsoft.azure:msal4j", ExcludeAllTransitivesRule.class);
+        components.withModule("com.microsoft.azure:msal4j-persistence-extension", ExcludeAllTransitivesRule.class);
+        components.withModule("com.microsoft.graph:microsoft-graph", ExcludeAllTransitivesRule.class);
+        components.withModule("com.microsoft.graph:microsoft-graph-core", ExcludeAllTransitivesRule.class);
+        components.withModule("com.microsoft.kiota:microsoft-kiota-abstractions", ExcludeAllTransitivesRule.class);
+        components.withModule("com.microsoft.kiota:microsoft-kiota-authentication-azure", ExcludeAllTransitivesRule.class);
+        components.withModule("com.microsoft.kiota:microsoft-kiota-http-okHttp", ExcludeAllTransitivesRule.class);
+        components.withModule("com.microsoft.kiota:microsoft-kiota-serialization-form", ExcludeAllTransitivesRule.class);
+        components.withModule("com.microsoft.kiota:microsoft-kiota-serialization-json", ExcludeAllTransitivesRule.class);
+        components.withModule("com.microsoft.kiota:microsoft-kiota-serialization-multipart", ExcludeAllTransitivesRule.class);
+        components.withModule("com.microsoft.kiota:microsoft-kiota-serialization-text", ExcludeAllTransitivesRule.class);
+
+        // Network dependencies
+        components.withModule("com.networknt:json-schema-validator", ExcludeAllTransitivesRule.class);
+        components.withModule("com.nimbusds:oauth2-oidc-sdk", ExcludeAllTransitivesRule.class);
+        components.withModule("com.squareup.okhttp3:okhttp", ExcludeAllTransitivesRule.class);
+        components.withModule("com.squareup.okio:okio", ExcludeAllTransitivesRule.class);
+        components.withModule("com.squareup.okio:okio-jvm", ExcludeAllTransitivesRule.class);
+
+        // Jakarta/javax mail dependencies
+        components.withModule("com.sun.mail:jakarta.mail", ExcludeAllTransitivesRule.class);
+        components.withModule("com.sun.xml.bind:jaxb-impl", ExcludeAllTransitivesRule.class);
+
+        // Mapbox dependencies
+        components.withModule("com.wdtinc:mapbox-vector-tile", ExcludeAllTransitivesRule.class);
+
+        // Dropwizard dependencies
+        components.withModule("io.dropwizard.metrics:metrics-core", ExcludeAllTransitivesRule.class);
+
+        // gRPC dependencies
+        components.withModule("io.grpc:grpc-api", ExcludeAllTransitivesRule.class);
+
+        // Netty dependencies
+        components.withModule("io.netty:netty-buffer", ExcludeAllTransitivesRule.class);
+        components.withModule("io.netty:netty-codec", ExcludeAllTransitivesRule.class);
+        components.withModule("io.netty:netty-codec-dns", ExcludeAllTransitivesRule.class);
+        components.withModule("io.netty:netty-codec-http", ExcludeAllTransitivesRule.class);
+        components.withModule("io.netty:netty-codec-http2", ExcludeAllTransitivesRule.class);
+        components.withModule("io.netty:netty-codec-socks", ExcludeAllTransitivesRule.class);
+        components.withModule("io.netty:netty-handler", ExcludeAllTransitivesRule.class);
+        components.withModule("io.netty:netty-handler-proxy", ExcludeAllTransitivesRule.class);
+        components.withModule("io.netty:netty-resolver", ExcludeAllTransitivesRule.class);
+        components.withModule("io.netty:netty-resolver-dns", ExcludeAllTransitivesRule.class);
+        components.withModule("io.netty:netty-transport", ExcludeAllTransitivesRule.class);
+        components.withModule("io.netty:netty-transport-classes-epoll", ExcludeAllTransitivesRule.class);
+        components.withModule("io.netty:netty-transport-native-unix-common", ExcludeAllTransitivesRule.class);
+
+        // OpenCensus dependencies
+        components.withModule("io.opencensus:opencensus-api", ExcludeAllTransitivesRule.class);
+        components.withModule("io.opencensus:opencensus-contrib-http-util", ExcludeAllTransitivesRule.class);
+
+        // OpenTelemetry dependencies
+        components.withModule("io.opentelemetry:opentelemetry-api", ExcludeAllTransitivesRule.class);
+        components.withModule("io.opentelemetry:opentelemetry-context", ExcludeAllTransitivesRule.class);
+        components.withModule("io.opentelemetry:opentelemetry-exporter-common", ExcludeAllTransitivesRule.class);
+        components.withModule("io.opentelemetry:opentelemetry-exporter-otlp", ExcludeAllTransitivesRule.class);
+        components.withModule("io.opentelemetry:opentelemetry-exporter-otlp-common", ExcludeAllTransitivesRule.class);
+        components.withModule("io.opentelemetry:opentelemetry-exporter-sender-jdk", ExcludeAllTransitivesRule.class);
+        components.withModule("io.opentelemetry:opentelemetry-sdk", ExcludeAllTransitivesRule.class);
+        components.withModule("io.opentelemetry:opentelemetry-sdk-common", ExcludeAllTransitivesRule.class);
+        components.withModule("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure", ExcludeAllTransitivesRule.class);
+        components.withModule("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi", ExcludeAllTransitivesRule.class);
+        components.withModule("io.opentelemetry:opentelemetry-sdk-metrics", ExcludeAllTransitivesRule.class);
+        components.withModule("io.opentelemetry:opentelemetry-semconv", ExcludeAllTransitivesRule.class);
+
+        // Project Reactor dependencies
+        components.withModule("io.projectreactor.netty:reactor-netty-core", ExcludeAllTransitivesRule.class);
+        components.withModule("io.projectreactor.netty:reactor-netty-http", ExcludeAllTransitivesRule.class);
+        components.withModule("io.projectreactor:reactor-core", ExcludeAllTransitivesRule.class);
+
+        // S2 Geometry dependencies
+        components.withModule("io.sgr:s2-geometry-library-java", ExcludeAllTransitivesRule.class);
+
+        // Jakarta XML dependencies
+        components.withModule("jakarta.xml.bind:jakarta.xml.bind-api", ExcludeAllTransitivesRule.class);
+
+        // javax mail dependencies
+        components.withModule("javax.mail:mail", ExcludeAllTransitivesRule.class);
+        components.withModule("javax.xml.bind:jaxb-api", ExcludeAllTransitivesRule.class);
+
+        // Testing dependencies
+        components.withModule("junit:junit", ExcludeAllTransitivesRule.class);
+
+        // JNA dependencies
+        components.withModule("net.java.dev.jna:jna-platform", ExcludeAllTransitivesRule.class);
+
+        // JSON dependencies
+        components.withModule("net.minidev:accessors-smart", ExcludeAllTransitivesRule.class);
+        components.withModule("net.minidev:json-smart", ExcludeAllTransitivesRule.class);
+
+        // EhCache dependencies
+        components.withModule("net.sf.ehcache:ehcache", ExcludeAllTransitivesRule.class);
+
+        // Shibboleth dependencies
+        components.withModule("net.shibboleth.utilities:java-support", ExcludeAllTransitivesRule.class);
+
+        // Apache Arrow dependencies
+        components.withModule("org.apache.arrow:arrow-format", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.arrow:arrow-memory-core", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.arrow:arrow-vector", ExcludeAllTransitivesRule.class);
+
+        // Apache Commons dependencies
+        components.withModule("org.apache.commons:commons-compress", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.commons:commons-text", ExcludeAllTransitivesRule.class);
+
+        // org.apache.directory.api:api-asn1-ber brings in org.slf4j:slf4j-api:1.7.25. We use 2.0.6
+        components.withModule("org.apache.directory.api:api-asn1-ber", ExcludeOtherGroupsTransitiveRule.class);
+
+        // org.apache.directory.api:api-ldap-client-api brings in org.apache.mina:mina-core:2.0.16. We use 2.2.4
+        components.withModule("org.apache.directory.api:api-ldap-client-api", ExcludeOtherGroupsTransitiveRule.class);
+        components.withModule("org.apache.directory.api:api-ldap-codec-core", ExcludeAllTransitivesRule.class);
+
+        // "org.apache.directory.api:api-ldap-codec-standalone brings in org.apache.mina:mina-core:2.0.16. We use 2.2.4
+        components.withModule("org.apache.directory.api:api-ldap-codec-standalone", ExcludeOtherGroupsTransitiveRule.class);
+
+        // TODO: For org.apache.directory.api dependencies we use partially 1.0.1 and partially 1.0.0. We should align these.
+        components.withModule("org.apache.directory.api:api-ldap-extras-aci", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.directory.api:api-ldap-schema-data", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.directory.api:api-ldap-extras-codec-api", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.directory.api:api-ldap-extras-sp", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.directory.api:api-ldap-extras-util", ExcludeAllTransitivesRule.class);
+
+        // org.apache.directory.api:api-ldap-model brings in org.apache.mina:mina-core:2.0.17. We use 2.2.4
+        // org.apache.directory.api:api-ldap-model brings in org.apache.servicemix.bundles:org.apache.servicemix.bundles.antlr:2.7.7_5. We
+        // use 2.7.7_5.
+        // org.apache.directory.api:api-ldap-model brings in commons-codec:commons-lang:commons-lang:2.6. We use 2.6.
+        // org.apache.directory.api:api-ldap-model brings in commons-collections:commons-collections:3.2.2. We use 3.3.2.
+        // org.apache.directory.api:api-ldap-model brings in commons-codec:commons-codec:1.10. We use 1.15.
+        // TODO exclude matching third party deps from being excluded
+        components.withModule("org.apache.directory.api:api-ldap-model", ExcludeOtherGroupsTransitiveRule.class);
+
+        // org.apache.directory.api:api-ldap-model brings in org.apache.mina:mina-core:2.0.17. We use 2.2.4
+        components.withModule("org.apache.directory.api:api-ldap-net-mina", ExcludeOtherGroupsTransitiveRule.class);
+
+        // org.apache.directory.api:api-asn1-ber brings in org.slf4j:slf4j-api:1.7.25. We use 2.0.6
+        // TODO: For org.apache.directory.api dependencies we use partially 1.0.1 and partially 1.0.0. We should align these.
+        components.withModule("org.apache.directory.api:api-util", ExcludeAllTransitivesRule.class);
+
+        // Apache Directory Server dependencies
+        components.withModule("org.apache.directory.jdbm:apacheds-jdbm1", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.directory.mavibot:mavibot", ExcludeAllTransitivesRule.class);
+        // org.apache.directory.server:apacheds-core-annotations brings in org.apache.directory.api:api-ldap-model:1.0.0. We use 1.0.1.
+        // org.apache.directory.server:apacheds-core-annotations brings in org.apache.directory.api:api-util:1.0.0. We use 1.0.1.
+        components.withModule("org.apache.directory.server:apacheds-core-annotations", ExcludeAllTransitivesRule.class);
+
+        // brings in org.slf4j:slf4j-api:1.7.25. We use 2.0.6
+        components.withModule("org.apache.directory.server:apacheds-core", ExcludeAllTransitivesRule.class);
+        // brings in org.apache.directory.server:apacheds-core:2.0.0-M24
+        components.withModule("org.apache.directory.server:apacheds-interceptor-kerberos", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.directory.server:apacheds-core-shared", ExcludeOtherGroupsTransitiveRule.class);
+        // brings in org.apache.directory.server:apacheds-core-shared:2.0.0-M24
+        components.withModule("org.apache.directory.server:apacheds-ldif-partition", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.directory.server:apacheds-protocol-shared", ExcludeOtherGroupsTransitiveRule.class);
+        components.withModule("org.apache.directory.server:ldap-client-test", ExcludeOtherGroupsTransitiveRule.class);
+
+        // org.apache.directory.server:apacheds-core-api brings in org.apache.directory.api:api-asn1-api:1.0.0. We use 1.0.1.
+        // org.apache.directory.server:apacheds-core-api brings in org.apache.directory.api:api-i18n:1.0.0. We use 1.0.1.
+        // org.apache.directory.server:apacheds-core-api brings in org.apache.directory.api:api-ldap-model:1.0.0. We use 1.0.1.
+        // org.apache.directory.server:apacheds-core-api brings in org.apache.directory.api:api-util:1.0.0. We use 1.0.1.
+        components.withModule("org.apache.directory.server:apacheds-core-api", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.directory.server:apacheds-i18n", ExcludeAllTransitivesRule.class);
+        // org.apache.directory.server:apacheds-jdbm-partition brings in org.apache.directory.api:api-ldap-model:1.0.0. We use 1.0.1.
+        // org.apache.directory.server:apacheds-jdbm-partition brings in org.apache.directory.api:api-util:1.0.0. We use 1.0.1.
+        components.withModule("org.apache.directory.server:apacheds-jdbm-partition", ExcludeAllTransitivesRule.class);
+        // org.apache.directory.server:apacheds-kerberos-codec brings in org.apache.directory.api:api-asn1-api:1.0.0. We use 1.0.1.
+        // org.apache.directory.server:apacheds-kerberos-codec brings in org.apache.directory.api:api-asn1-ber:1.0.0. We use 1.0.1.
+        // org.apache.directory.server:apacheds-kerberos-codec brings in org.apache.directory.api:api-i18n:1.0.0. We use 1.0.1.
+        // org.apache.directory.server:apacheds-kerberos-codec brings in org.apache.directory.api:api-ldap-model:1.0.0. We use 1.0.1.
+        // org.apache.directory.server:apacheds-kerberos-codec brings in org.apache.directory.api:api-util:1.0.0. We use 1.0.1.
+        components.withModule("org.apache.directory.server:apacheds-kerberos-codec", ExcludeAllTransitivesRule.class);
+        // org.apache.directory.server:apacheds-mavibot-partition brings in org.apache.directory.api:api-ldap-model:1.0.0. We use 1.0.1.
+        // org.apache.directory.server:apacheds-mavibot-partition brings in org.apache.directory.api:api-util:1.0.0. We use 1.0.1.
+        components.withModule("org.apache.directory.server:apacheds-mavibot-partition", ExcludeAllTransitivesRule.class);
+        // org.apache.directory.server:apacheds-protocol-kerberos brings in org.apache.directory.api:api-asn1-api:1.0.0. We use 1.0.1.
+        // org.apache.directory.server:apacheds-protocol-kerberos brings in org.apache.directory.api:api-ldap-model:1.0.0. We use 1.0.1.
+        components.withModule("org.apache.directory.server:apacheds-protocol-kerberos", ExcludeAllTransitivesRule.class);
+        // org.apache.directory.server:apacheds-protocol-ldap brings in org.apache.directory.api:api-asn1-ber:1.0.0. We use 1.0.1.
+        // org.apache.directory.server:apacheds-protocol-ldap brings in org.apache.directory.api:api-ldap-model:1.0.0. We use 1.0.1.
+        // org.apache.directory.server:apacheds-protocol-ldap brings in org.apache.directory.api:api-util:1.0.0. We use 1.0.1.
+        components.withModule("org.apache.directory.server:apacheds-protocol-ldap", ExcludeAllTransitivesRule.class);
+        // org.apache.directory.server:apacheds-server-annotations brings in org.apache.directory.api:api-ldap-model:1.0.0. We use 1.0.1.
+        components.withModule("org.apache.directory.server:apacheds-server-annotations", ExcludeAllTransitivesRule.class);
+        // org.apache.directory.server:apacheds-test-framework brings in org.apache.directory.api:api-ldap-model:1.0.0. We use 1.0.1.
+        components.withModule("org.apache.directory.server:apacheds-test-framework", ExcludeAllTransitivesRule.class);
+        // org.apache.directory.server:apacheds-xdbm-partition brings in org.apache.directory.api:api-ldap-model:1.0.0. We use 1.0.1.
+        // org.apache.directory.server:apacheds-xdbm-partition brings in org.apache.directory.api:api-util:1.0.0. We use 1.0.1.
+        components.withModule("org.apache.directory.server:apacheds-xdbm-partition", ExcludeAllTransitivesRule.class);
+
+        // org.apache.directory.api:api-ldap-client-api brings in org.apache.mina:mina-core:2.0.16. We use 2.2.4
+        components.withModule("org.apache.directory.server:apacheds-interceptors-authn", ExcludeAllTransitivesRule.class);
+
+        // Hadoop dependencies
+        components.withModule("org.apache.hadoop:hadoop-client-api", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.hadoop:hadoop-client-runtime", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.hadoop:hadoop-common", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.hadoop:hadoop-hdfs", ExcludeAllTransitivesRule.class);
+
+        // Apache HTTP dependencies
+        components.withModule("org.apache.httpcomponents.client5:httpclient5", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.httpcomponents:fluent-hc", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.httpcomponents:httpasyncclient", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.httpcomponents:httpclient", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.httpcomponents:httpclient-cache", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.httpcomponents:httpcore-nio", ExcludeAllTransitivesRule.class);
+
+        // Apache James dependencies
+        components.withModule("org.apache.james:apache-mime4j-core", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.james:apache-mime4j-dom", ExcludeAllTransitivesRule.class);
+
+        // Apache Kerby dependencies
+        components.withModule("org.apache.kerby:kerb-admin", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.kerby:kerb-client", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.kerby:kerb-common", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.kerby:kerb-identity", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.kerby:kerb-server", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.kerby:kerb-simplekdc", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.kerby:kerb-util", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.kerby:kerby-config", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.kerby:kerby-pkix", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.kerby:ldap-backend", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.kerby:token-provider", ExcludeAllTransitivesRule.class);
+
+        // Apache Log4j dependencies
+        // We want to remove log4j-api compile only dependency on biz.aQute.bnd and org.osgi group but
+        // keep other compile only dependencies like spotbugs, errorprone and jspecify
+        components.withModule("org.apache.logging.log4j:log4j-api", ExcludeByGroup.class, config -> {
+            config.params(List.of("biz.aQute.bnd", "org.osgi"));
+        });
+        components.withModule("org.apache.logging.log4j:log4j-1.2-api", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.logging.log4j:log4j-core", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.logging.log4j:log4j-jcl", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.logging.log4j:log4j-slf4j-impl", ExcludeAllTransitivesRule.class);
+
+        // lucene-analysis-morfologik brings in org.carrot2:morfologik-stemming:2.1.9. we use 2.1.1
+        // lucene-analysis-morfologik brings in org.carrot2:morfologik-polish:2.1.9. we use none.
+        // lucene-analysis-morfologik brings in ua.net.nlp:morfologik-ukrainian-search:4.9.1 we use 3.7.5.
+        components.withModule("org.apache.lucene:lucene-analysis-morfologik", ExcludeOtherGroupsTransitiveRule.class);
+
+        // lucene-analysis-icu brings in com.ibm.icu:icu4j:74.2 we use 68.2.
+        components.withModule("org.apache.lucene:lucene-analysis-icu", ExcludeOtherGroupsTransitiveRule.class);
+
+        // lucene-analysis-phonetic brings in commons-codec:1.17. We use 1.15
+        components.withModule("org.apache.lucene:lucene-analysis-phonetic", ExcludeOtherGroupsTransitiveRule.class);
+
+        // lucene-spatial-extras brings in different version of spatial4j
+        // lucene-spatial-extras brings in different version of s2-geometry-library-java
+        components.withModule("org.apache.lucene:lucene-spatial-extras", ExcludeOtherGroupsTransitiveRule.class);
+
+        // lucene-expressions brings in org.antlr:antlr4-runtime:4.13.2
+        // lucene-expressions brings in org.ow2.asm:asm:9.6
+        // lucene-expressions brings in org.ow2.asm:asm-commons:9.6
+        components.withModule("org.apache.lucene:lucene-expressions", ExcludeOtherGroupsTransitiveRule.class);
+        components.withModule("org.apache.lucene:lucene-test-framework", ExcludeOtherGroupsTransitiveRule.class);
+
+        // Apache Mina dependencies
+        components.withModule("org.apache.mina:mina-core", ExcludeAllTransitivesRule.class);
+
+        // Apache PDFBox dependencies
+        components.withModule("org.apache.pdfbox:fontbox", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.pdfbox:pdfbox", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.pdfbox:pdfbox-io", ExcludeAllTransitivesRule.class);
+
+        // Apache POI dependencies
+        components.withModule("org.apache.poi:poi", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.poi:poi-ooxml", ExcludeAllTransitivesRule.class);
+        components.withModule("org.apache.poi:poi-scratchpad", ExcludeAllTransitivesRule.class);
+
+        // Apache Santuario dependencies
+        components.withModule("org.apache.santuario:xmlsec", ExcludeAllTransitivesRule.class);
+
+        // org.apache.tika:tika-core brings in org.slf4j:slf4j-api:2.0.17. We use 2.0.6
+        // org.apache.tika:tika-core brings in commons-io:commons-io:2.20.0. We use 2.5
+        components.withModule("org.apache.tika:tika-core", ExcludeOtherGroupsTransitiveRule.class);
+
+        // org.apache.tika:tika-langdetect-tika brings in com.optimaize.languagedetector:language-detector:0.6.
+        components.withModule("org.apache.tika:tika-langdetect-tika", ExcludeOtherGroupsTransitiveRule.class);
+        // org.apache.tika:tika-parser-apple-module brings in com.googlecode.plist:dd-plist:1.28.
+        components.withModule("org.apache.tika:tika-parser-apple-module", ExcludeOtherGroupsTransitiveRule.class);
+        // org.apache.tika:tika-parser-microsoft-module brings in com.healthmarketscience.jackcess:jackcess-encrypt:4.0.3.
+        // org.apache.tika:tika-parser-microsoft-module brings in com.healthmarketscience.jackcess:jackcess:4.0.8.
+        // org.apache.tika:tika-parser-microsoft-module brings in com.pff:java-libpst:0.9.3.
+        // org.apache.tika:tika-parser-microsoft-module brings in commons-logging:commons-logging:1.3.5. We use 1.2.
+        // org.apache.tika:tika-parser-microsoft-module brings in org.bouncycastle:bcjmail-jdk18on:1.81.
+        // org.apache.tika:tika-parser-microsoft-module brings in org.bouncycastle:bcprov-jdk18on:1.81. We use 1.78.1/1.79.
+        // org.apache.tika:tika-parser-microsoft-module brings in tika-parser-mail-commons.
+        components.withModule("org.apache.tika:tika-parser-microsoft-module", ExcludeAllTransitivesRule.class);
+        // org.apache.tika:tika-parser-miscoffice-module brings in org.glassfish.jaxb:jaxb-runtime:4.0.5.
+        components.withModule("org.apache.tika:tika-parser-miscoffice-module", ExcludeOtherGroupsTransitiveRule.class);
+        // org.apache.tika:tika-parser-pdf-module brings in org.apache.pdfbox:pdfbox-tools:3.0.5.
+        // org.apache.tika:tika-parser-pdf-module brings in org.bouncycastle:bcjmail-jdk18on:1.81. Closest we use is
+        // bcprov-jdk18on:1.78.1/1.79.
+        // org.apache.tika:tika-parser-pdf-module brings in org.bouncycastle:bcprov-jdk18on:1.81. We use 1.78.1/1.79.
+        // org.apache.tika:tika-parser-pdf-module brings in org.glassfish.jaxb:jaxb-runtime:4.0.5.
+        components.withModule("org.apache.tika:tika-parser-pdf-module", ExcludeOtherGroupsTransitiveRule.class);
+        // org.apache.tika:tika-parser-text-module brings in com.github.albfernandez:juniversalchardet:2.5.0..
+        // org.apache.tika:tika-parser-text-module brings in org.apache.commons:commons-csv:1.14.1. We use 1.0.
+        components.withModule("org.apache.tika:tika-parser-text-module", ExcludeOtherGroupsTransitiveRule.class);
+        // org.apache.tika:tika-parser-xmp-commons brings in org.apache.pdfbox:xmpbox:3.0.5.
+        components.withModule("org.apache.tika:tika-parser-xmp-commons", ExcludeOtherGroupsTransitiveRule.class);
+
+        // Apache XMLBeans dependencies
+        components.withModule("org.apache.xmlbeans:xmlbeans", ExcludeAllTransitivesRule.class);
+
+        // BouncyCastle dependencies
+        components.withModule("org.bouncycastle:bcpg-fips", ExcludeAllTransitivesRule.class);
+        components.withModule("org.bouncycastle:bcpkix-jdk18on", ExcludeAllTransitivesRule.class);
+        components.withModule("org.bouncycastle:bcutil-jdk18on", ExcludeAllTransitivesRule.class);
+
+        // Carrot2 dependencies
+        components.withModule("org.carrot2:morfologik-stemming", ExcludeAllTransitivesRule.class);
+
+        // Kotlin dependencies
+        components.withModule("org.jetbrains.kotlin:kotlin-stdlib", ExcludeAllTransitivesRule.class);
+
+        // JONI dependencies
+        components.withModule("org.jruby.joni:joni", ExcludeAllTransitivesRule.class);
+
+        // JUnit dependencies
+        components.withModule("org.junit.jupiter:junit-jupiter", ExcludeAllTransitivesRule.class);
+
+        // Mockito dependencies
+        components.withModule("org.mockito:mockito-core", ExcludeAllTransitivesRule.class);
+        components.withModule("org.mockito:mockito-subclass", ExcludeAllTransitivesRule.class);
+
+        // JMH dependencies
+        components.withModule("org.openjdk.jmh:jmh-core", ExcludeAllTransitivesRule.class);
+
+        // OpenSAML dependencies
+        components.withModule("org.opensaml:opensaml-core", ExcludeAllTransitivesRule.class);
+        components.withModule("org.opensaml:opensaml-messaging-api", ExcludeAllTransitivesRule.class);
+        components.withModule("org.opensaml:opensaml-messaging-impl", ExcludeAllTransitivesRule.class);
+        components.withModule("org.opensaml:opensaml-profile-api", ExcludeAllTransitivesRule.class);
+        components.withModule("org.opensaml:opensaml-profile-impl", ExcludeAllTransitivesRule.class);
+        components.withModule("org.opensaml:opensaml-saml-api", ExcludeAllTransitivesRule.class);
+        components.withModule("org.opensaml:opensaml-saml-impl", ExcludeAllTransitivesRule.class);
+        components.withModule("org.opensaml:opensaml-security-api", ExcludeAllTransitivesRule.class);
+        components.withModule("org.opensaml:opensaml-security-impl", ExcludeAllTransitivesRule.class);
+        components.withModule("org.opensaml:opensaml-soap-api", ExcludeAllTransitivesRule.class);
+        components.withModule("org.opensaml:opensaml-soap-impl", ExcludeAllTransitivesRule.class);
+        components.withModule("org.opensaml:opensaml-storage-api", ExcludeAllTransitivesRule.class);
+        components.withModule("org.opensaml:opensaml-storage-impl", ExcludeAllTransitivesRule.class);
+        components.withModule("org.opensaml:opensaml-xmlsec-api", ExcludeAllTransitivesRule.class);
+        components.withModule("org.opensaml:opensaml-xmlsec-impl", ExcludeAllTransitivesRule.class);
+
+        // OrbisGIS dependencies
+        components.withModule("org.orbisgis:cts", ExcludeAllTransitivesRule.class);
+        components.withModule("org.orbisgis:h2gis", ExcludeAllTransitivesRule.class);
+        components.withModule("org.orbisgis:h2gis-utilities", ExcludeAllTransitivesRule.class);
+
+        // ASM dependencies
+        components.withModule("org.ow2.asm:asm-analysis", ExcludeAllTransitivesRule.class);
+        components.withModule("org.ow2.asm:asm-commons", ExcludeAllTransitivesRule.class);
+        components.withModule("org.ow2.asm:asm-tree", ExcludeAllTransitivesRule.class);
+        components.withModule("org.ow2.asm:asm-util", ExcludeAllTransitivesRule.class);
+
+        // Reactive Streams dependencies
+        components.withModule("org.reactivestreams:reactive-streams-tck", ExcludeAllTransitivesRule.class);
+
+        // SLF4J dependencies
+        components.withModule("org.slf4j:jcl-over-slf4j", ExcludeAllTransitivesRule.class);
+        components.withModule("org.slf4j:slf4j-nop", ExcludeAllTransitivesRule.class);
+        components.withModule("org.slf4j:slf4j-simple", ExcludeAllTransitivesRule.class);
+
+        // SubEtha SMTP dependencies
+        components.withModule("org.subethamail:subethasmtp", ExcludeAllTransitivesRule.class);
+
+        // Testcontainers dependencies
+        components.withModule("org.testcontainers:testcontainers", ExcludeAllTransitivesRule.class);
+
+        // AWS SDK dependencies
+        components.withModule("com.amazonaws:aws-java-sdk-core", ExcludeAllTransitivesRule.class);
+        components.withModule("com.amazonaws:aws-java-sdk-s3", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:bedrockruntime", ExcludeAllTransitivesRule.class);
+
+        components.withModule("software.amazon.awssdk:apache-client", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:arns", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:auth", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:aws-core", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:aws-json-protocol", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:aws-query-protocol", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:aws-xml-protocol", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:checksums", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:checksums-spi", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:ec2", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:endpoints-spi", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:http-auth", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:http-auth-aws", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:http-auth-aws-eventstream", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:http-auth-spi", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:http-client-spi", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:identity-spi", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:imds", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:json-utils", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:metrics-spi", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:netty-nio-client", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:profiles", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:protocol-core", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:regions", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:retries", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:retries-spi", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:s3", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:sdk-core", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:services", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:sts", ExcludeAllTransitivesRule.class);
+        components.withModule("software.amazon.awssdk:utils", ExcludeAllTransitivesRule.class);
+    }
+}

+ 43 - 0
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/dependencies/rules/ExcludeAllTransitivesRule.java

@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.gradle.internal.dependencies.rules;
+
+import org.gradle.api.artifacts.CacheableRule;
+import org.gradle.api.artifacts.ComponentMetadataContext;
+import org.gradle.api.artifacts.ComponentMetadataRule;
+
+/**
+ * A Gradle component metadata rule that excludes all transitive dependencies.
+ *
+ * <p>The rule operates on all variants of a component.</p>
+ *
+ * <h3>Example Usage:</h3>
+ * <pre>{@code
+ * dependencies {
+ *     components {
+ *        withModule("com.azure:azure-core-http-netty"", ExcludeAllTransitivesRule)
+ *     }
+ * }
+ * }</pre>
+ *
+ */
+@CacheableRule
+public abstract class ExcludeAllTransitivesRule implements ComponentMetadataRule {
+
+    @Override
+    public void execute(ComponentMetadataContext context) {
+        context.getDetails().allVariants(variant -> {
+            variant.withDependencies(dependencies -> {
+                // Exclude all transitive dependencies
+                dependencies.clear();
+            });
+        });
+    }
+}

+ 52 - 0
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/dependencies/rules/ExcludeByGroup.java

@@ -0,0 +1,52 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.gradle.internal.dependencies.rules;
+
+import org.gradle.api.artifacts.CacheableRule;
+import org.gradle.api.artifacts.ComponentMetadataContext;
+import org.gradle.api.artifacts.ComponentMetadataRule;
+
+import java.util.List;
+
+import javax.inject.Inject;
+
+/**
+ * A Gradle component metadata rule that excludes transitive dependencies of a specific groups.
+ *
+ * <p>The rule operates on all variants of a component.</p>
+ *
+ * <h3>Example Usage:</h3>
+ * <pre>{@code
+ * dependencies {
+ *     components {
+ *        withModule("org.apache.logging.log4j:log4j-api", ExcludeByGroup) {
+ *            params(["biz.aQute.bnd", "org.osgi"])
+ *        }
+ *     }
+ * }
+ * }</pre>
+ *
+ */
+@CacheableRule
+public class ExcludeByGroup implements ComponentMetadataRule {
+
+    private final List<String> groupIds;
+
+    @Inject
+    public ExcludeByGroup(List<String> groupIds) {
+        this.groupIds = groupIds;
+    }
+
+    @Override
+    public void execute(ComponentMetadataContext context) {
+        context.getDetails()
+            .allVariants(v -> v.withDependencies(dependencies -> dependencies.removeIf(dep -> groupIds.contains(dep.getGroup()))));
+    }
+}

+ 58 - 0
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/dependencies/rules/ExcludeOtherGroupsTransitiveRule.java

@@ -0,0 +1,58 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.gradle.internal.dependencies.rules;
+
+import org.gradle.api.artifacts.CacheableRule;
+import org.gradle.api.artifacts.ComponentMetadataContext;
+import org.gradle.api.artifacts.ComponentMetadataRule;
+
+/**
+ * A Gradle component metadata rule that excludes transitive dependencies with different group IDs
+ * than the parent component.
+ *
+ * <p>This rule helps prevent dependency conflicts and reduces the dependency tree size by ensuring
+ * that only dependencies from the same Maven group ID are included transitively. This is particularly
+ * useful for large projects where different modules should not pull in external dependencies from
+ * other organizations or groups.</p>
+ *
+ * <p>The rule operates on all variants of a component and removes any transitive dependency whose
+ * group ID differs from the component's own group ID.</p>
+ *
+ * <h3>Example Usage:</h3>
+ * <pre>{@code
+ * dependencies {
+ *     components {
+ *        withModule("com.azure:azure-core", ExcludeOtherGroupsTransitiveRule)
+ *     }
+ * }
+ * }</pre>
+ *
+ * <h3>Example Behavior:</h3>
+ * <p>If component {@code com.example:my-lib:1.0} has transitive dependencies:</p>
+ * <ul>
+ *   <li>{@code com.example:another-lib:1.0} - KEPT (same group)</li>
+ *   <li>{@code org.apache.commons:commons-lang3:3.12.0} - REMOVED (different group)</li>
+ *   <li>{@code com.example:utils:1.5} - KEPT (same group)</li>
+ * </ul>
+ *
+ */
+@CacheableRule
+public abstract class ExcludeOtherGroupsTransitiveRule implements ComponentMetadataRule {
+
+    @Override
+    public void execute(ComponentMetadataContext context) {
+        context.getDetails().allVariants(variant -> {
+            variant.withDependencies(dependencies -> {
+                // Exclude transitive dependencies with a different groupId than the parent
+                dependencies.removeIf(dep -> dep.getGroup().equals(context.getDetails().getId().getGroup()) == false);
+            });
+        });
+    }
+}

+ 0 - 12
build-tools/src/main/java/org/elasticsearch/gradle/util/GradleUtils.java

@@ -14,8 +14,6 @@ import org.gradle.api.Project;
 import org.gradle.api.Task;
 import org.gradle.api.UnknownTaskException;
 import org.gradle.api.artifacts.Configuration;
-import org.gradle.api.artifacts.ModuleDependency;
-import org.gradle.api.artifacts.ProjectDependency;
 import org.gradle.api.plugins.JavaBasePlugin;
 import org.gradle.api.plugins.JavaPlugin;
 import org.gradle.api.plugins.JavaPluginExtension;
@@ -195,16 +193,6 @@ public abstract class GradleUtils {
         return projectPath.contains("modules:") || projectPath.startsWith(":x-pack:plugin");
     }
 
-    public static void disableTransitiveDependencies(Configuration config) {
-        config.getDependencies().all(dep -> {
-            if (dep instanceof ModuleDependency
-                && dep instanceof ProjectDependency == false
-                && dep.getGroup().startsWith("org.elasticsearch") == false) {
-                ((ModuleDependency) dep).setTransitive(false);
-            }
-        });
-    }
-
     public static String projectPath(String taskPath) {
         return taskPath.lastIndexOf(':') == 0 ? ":" : taskPath.substring(0, taskPath.lastIndexOf(':'));
     }

+ 7 - 0
distribution/packages/build.gradle

@@ -43,6 +43,13 @@ import java.util.regex.Pattern
  *    dpkg -c path/to/elasticsearch.deb
  */
 
+buildscript {
+  dependencies {
+    constraints {
+      classpath "org.slf4j:slf4j-api:2.0.6"
+    }
+  }
+}
 plugins {
   alias(buildLibs.plugins.ospackage)
 }

+ 0 - 30
gradle/verification-metadata.xml

@@ -2175,11 +2175,6 @@
             <sha256 value="e955772e53448ecec3450da0e43097e8fe3f1e684eed4617453ff42b76fa75b5" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <component group="org.apache.directory.api" name="api-asn1-api" version="1.0.0-M20">
-         <artifact name="api-asn1-api-1.0.0-M20.jar">
-            <sha256 value="484aaf4b888b0eb699d95bea265c2d5b6ebec951d70e5c5f7691cd52dd4c8298" origin="Generated by Gradle"/>
-         </artifact>
-      </component>
       <component group="org.apache.directory.api" name="api-asn1-api" version="1.0.1">
          <artifact name="api-asn1-api-1.0.1.jar">
             <sha256 value="e558865b92fcd7211e18d5d34d3cff9e51b172f291be074497811dc3b5ba197c" origin="Generated by Gradle"/>
@@ -4686,36 +4681,11 @@
             <sha256 value="affd06771589ebfe454bb11315a4f466ecaa135b95f3e7939534cf1d2fd7064c" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <component group="org.slf4j" name="slf4j-api" version="1.7.10">
-         <artifact name="slf4j-api-1.7.10.jar">
-            <sha256 value="3863e27005740d4d1289bf87b113efea115e9a22408a7d623be8004991232bfe" origin="Generated by Gradle"/>
-         </artifact>
-      </component>
       <component group="org.slf4j" name="slf4j-api" version="1.7.25">
          <artifact name="slf4j-api-1.7.25.jar">
             <sha256 value="18c4a0095d5c1da6b817592e767bb23d29dd2f560ad74df75ff3961dbde25b79" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <component group="org.slf4j" name="slf4j-api" version="1.7.30">
-         <artifact name="slf4j-api-1.7.30.jar">
-            <sha256 value="cdba07964d1bb40a0761485c6b1e8c2f8fd9eb1d19c53928ac0d7f9510105c57" origin="Generated by Gradle"/>
-         </artifact>
-      </component>
-      <component group="org.slf4j" name="slf4j-api" version="1.7.32">
-         <artifact name="slf4j-api-1.7.32.jar">
-            <sha256 value="3624f8474c1af46d75f98bc097d7864a323c81b3808aa43689a6e1c601c027be" origin="Generated by Gradle"/>
-         </artifact>
-      </component>
-      <component group="org.slf4j" name="slf4j-api" version="1.7.36">
-         <artifact name="slf4j-api-1.7.36.jar">
-            <sha256 value="d3ef575e3e4979678dc01bf1dcce51021493b4d11fb7f1be8ad982877c16a1c0" origin="Generated by Gradle"/>
-         </artifact>
-      </component>
-      <component group="org.slf4j" name="slf4j-api" version="1.7.5">
-         <artifact name="slf4j-api-1.7.5.jar">
-            <sha256 value="fe30825245d2336c859dc38d60c0fc5f3668dbf29cd586828d2b5667ec355b91" origin="Generated by Gradle"/>
-         </artifact>
-      </component>
       <component group="org.slf4j" name="slf4j-api" version="2.0.17">
          <artifact name="slf4j-api-2.0.17.jar">
             <sha256 value="7b751d952061954d5abfed7181c1f645d336091b679891591d63329c622eb832" origin="Generated by Gradle"/>

+ 3 - 3
settings.gradle

@@ -1,7 +1,6 @@
-import org.elasticsearch.gradle.internal.toolchain.OracleOpenJdkToolchainResolver
-import org.elasticsearch.gradle.internal.toolchain.ArchivedOracleJdkToolchainResolver
 import org.elasticsearch.gradle.internal.toolchain.AdoptiumJdkToolchainResolver
-import org.elasticsearch.gradle.internal.toolchain.EarlyAccessCatalogJdkToolchainResolver
+import org.elasticsearch.gradle.internal.toolchain.ArchivedOracleJdkToolchainResolver
+import org.elasticsearch.gradle.internal.toolchain.OracleOpenJdkToolchainResolver
 
 pluginManagement {
   repositories {
@@ -17,6 +16,7 @@ pluginManagement {
 plugins {
   id "com.gradle.develocity" version "4.1.1"
   id 'elasticsearch.java-toolchain'
+  id 'elasticsearch.component-metadata-rules'
 }
 
 enableFeaturePreview "STABLE_CONFIGURATION_CACHE"

+ 69 - 25
test/fixtures/hdfs-fixture/build.gradle

@@ -17,11 +17,13 @@ def patched = Attribute.of('patched', Boolean)
 def hdfsVersionAttr = Attribute.of('hdfs.major.version', Integer)
 configurations {
   hdfs2 {
+    transitive = false
     attributes {
       attribute(patched, true)
     }
   }
   hdfs3 {
+    transitive = false
     attributes {
       attribute(patched, true)
     }
@@ -62,8 +64,22 @@ dependencies {
       matchingArtifacts = ["hadoop-common"]
     }
   }
+  implementation("org.slf4j:slf4j-api:${versions.slf4j}")
 
-  compileOnly("org.apache.hadoop:hadoop-minicluster:2.8.5")
+  compileOnly("commons-io:commons-io:2.16.1")
+  compileOnly("org.apache.hadoop:hadoop-common:2.8.5")
+  compileOnly("org.apache.hadoop:hadoop-annotations:2.8.5")
+  compileOnly("org.apache.hadoop:hadoop-hdfs:2.8.5")
+  compileOnly("org.apache.hadoop:hadoop-hdfs:2.8.5:tests")
+  compileOnly("org.apache.hadoop:hadoop-hdfs-client:2.8.5") {
+    exclude group: "org.apache.hadoop", module: "hadoop-common"
+  }
+  compileOnly("org.apache.hadoop:hadoop-hdfs:2.8.5:tests") {
+    transitive = false
+  }
+  compileOnly("org.apache.hadoop:hadoop-minicluster:2.8.5") {
+    transitive = false
+  }
   api("com.carrotsearch.randomizedtesting:randomizedtesting-runner:${versions.randomizedrunner}") {
     transitive = false
   }
@@ -104,30 +120,58 @@ dependencies {
     [group: "org.slf4j", module: "slf4j-api"],
   ]
 
-  hdfs2("org.apache.hadoop:hadoop-minicluster:2.8.5") {
-    commonExcludes.each { exclude it }
-    exclude group: "org.apache.commons", module: "commons-math3"
-    exclude group: "xmlenc", module: "xmlenc"
-    exclude group: "net.java.dev.jets3t", module: "jets3t"
-    exclude group: "org.apache.directory.server", module: "apacheds-i18n"
-    exclude group: "xerces", module: "xercesImpl"
-  }
-
-  hdfs3("org.apache.hadoop:hadoop-minicluster:3.3.1") {
-    commonExcludes.each { exclude it }
-    exclude group: "dnsjava", module: "dnsjava"
-    exclude group: "com.google.inject.extensions", module: "guice-servlet"
-    exclude group: "com.google.inject", module: "guice"
-    exclude group: "com.microsoft.sqlserver", module: "mssql-jdbc"
-    exclude group: "com.sun.jersey.contribs", module: "jersey-guice"
-    exclude group: "com.zaxxer", module: "HikariCP-java7"
-    exclude group: "com.sun.jersey", module: "jersey-server"
-    exclude group: "org.bouncycastle", module: "bcpkix-jdk15on"
-    exclude group: "org.bouncycastle", module: "bcprov-jdk15on"
-    exclude group: "org.ehcache", module: "ehcache"
-    exclude group: "org.apache.geronimo.specs", module: "geronimo-jcache_1.0_spec"
-    exclude group: "org.xerial.snappy", module: "snappy-java"
-  }
+    // port this to a component metadata rule for minicluster 2 and 3
+    hdfs2("commons-io:commons-io:2.16.1")
+    hdfs2("com.google.protobuf:protobuf-java:3.25.5")
+    hdfs2("org.apache.htrace:htrace-core4:4.0.1-incubating")
+    hdfs2("org.mortbay.jetty:jetty:6.1.26")
+    hdfs2("org.mortbay.jetty:jetty-util:6.1.26")
+    hdfs2("org.mortbay.jetty:jetty-sslengine:6.1.26")
+    hdfs2("org.mortbay.jetty:servlet-api:2.5-20081211")
+    hdfs2("org.codehaus.jackson:jackson-mapper-asl:1.9.13")
+    hdfs2("org.codehaus.jackson:jackson-core-asl:1.9.13")
+    hdfs2("com.sun.jersey:jersey-servlet:1.19")
+    hdfs2("com.sun.jersey:jersey-server:1.19")
+    hdfs2("com.sun.jersey:jersey-core:1.19")
+    hdfs2("commons-collections:commons-collections:3.2.1")
+    hdfs2("commons-configuration:commons-configuration:1.6")
+    hdfs2("commons-lang:commons-lang:2.6")
+    hdfs2("org.apache.hadoop:hadoop-auth:2.8.5")
+    hdfs2("org.apache.hadoop:hadoop-common:2.8.5")
+    hdfs2("org.apache.hadoop:hadoop-common:2.8.5:tests")
+    hdfs2("org.apache.hadoop:hadoop-hdfs:2.8.5")
+    hdfs2("org.apache.hadoop:hadoop-hdfs:2.8.5:tests")
+    hdfs2("org.apache.hadoop:hadoop-annotations:2.8.5")
+    hdfs2("org.apache.hadoop:hadoop-hdfs-client:2.8.5")
+    hdfs2("org.apache.hadoop:hadoop-minicluster:2.8.5")
+
+    hdfs3("org.apache.hadoop:hadoop-minicluster:3.3.1")
+    hdfs3("org.apache.hadoop:hadoop-common:3.3.1")
+    hdfs3("org.apache.hadoop:hadoop-common:3.3.1:tests")
+    hdfs3("org.apache.hadoop:hadoop-hdfs:3.3.1")
+    hdfs3("org.apache.hadoop:hadoop-hdfs:3.3.1:tests")
+    hdfs3("org.apache.hadoop:hadoop-hdfs-client:3.3.1")
+    hdfs3("org.apache.hadoop:hadoop-yarn-api:3.3.1")
+    hdfs3("org.apache.hadoop:hadoop-auth:3.3.1")
+    hdfs3("org.apache.hadoop.thirdparty:hadoop-shaded-guava:1.1.1")
+    hdfs3("org.apache.hadoop.thirdparty:hadoop-shaded-protobuf_3_7:1.1.1")
+    hdfs3("org.apache.htrace:htrace-core4:4.1.0-incubating")
+    hdfs3("commons-collections:commons-collections:3.2.2")
+    hdfs3("commons-io:commons-io:2.8.0")
+    hdfs3("org.apache.commons:commons-configuration2:2.1.1")
+    hdfs3("org.apache.commons:commons-lang3:3.7")
+    hdfs3("org.apache.commons:commons-text:1.4")
+    hdfs3("com.fasterxml.woodstox:woodstox-core:6.7.0")
+    hdfs3("org.codehaus.woodstox:stax2-api:4.2.2")
+    hdfs3("org.eclipse.jetty:jetty-servlet:9.4.40.v20210413")
+    hdfs3("org.eclipse.jetty:jetty-server:9.4.40.v20210413")
+    hdfs3("org.eclipse.jetty:jetty-util:9.4.40.v20210413")
+    hdfs3("org.eclipse.jetty:jetty-webapp:9.4.40.v20210413")
+    hdfs3("org.eclipse.jetty:jetty-http:9.4.40.v20210413")
+    hdfs3("org.eclipse.jetty:jetty-security:9.4.40.v20210413")
+    hdfs3("org.eclipse.jetty:jetty-io:9.4.40.v20210413")
+    hdfs3("javax.servlet:javax.servlet-api:3.1.0")
+    hdfs3("com.sun.jersey:jersey-servlet:1.19")
 }
 
 tasks.named("shadowJar").configure {

+ 2 - 2
test/fixtures/url-fixture/build.gradle

@@ -10,6 +10,6 @@ apply plugin: 'elasticsearch.java'
 description = 'Fixture for URL external service'
 
 dependencies {
-  api project(':server')
-  api project(':test:framework')
+  implementation project(':server')
+  implementation project(':test:framework')
 }