Java applications rely heavily on third-party libraries pulled from Maven Central and other repositories. The Log4Shell vulnerability in December 2021 was a painful reminder of how deep and impactful a single compromised dependency can be. This guide covers the practical steps for securing your Java dependency chain in both Maven and Gradle projects.
The Java Dependency Problem
A typical enterprise Java application has hundreds of dependencies. Spring Boot starters alone pull in dozens of transitive libraries. Maven and Gradle resolve these trees automatically, which is convenient but opaque. Most developers have no idea what is actually in their dependency tree.
The risks are familiar:
- Known vulnerabilities in transitive dependencies. Your direct dependencies are probably fine. Their dependencies' dependencies are where the risk hides.
- Dependency confusion. If you use internal Maven repositories alongside public ones, the resolution order matters.
- Compromised artifacts. Maven Central requires PGP signatures, but verification is not enforced by default in either Maven or Gradle.
- Abandoned libraries. Java's enterprise ecosystem is littered with libraries that haven't seen a release in years but still have known CVEs.
Maven: Securing Your Build
Enforcing Dependency Versions
The Maven Enforcer Plugin prevents unexpected dependency changes:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<id>enforce</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<requireMavenVersion>
<version>3.8.0</version>
</requireMavenVersion>
<dependencyConvergence/>
<banDuplicateClasses/>
</rules>
</configuration>
</execution>
</executions>
</plugin>
The dependencyConvergence rule fails the build if different versions of the same dependency appear in the tree. This catches a class of subtle bugs and ensures you know exactly which version is being used.
Dependency Management Section
Use <dependencyManagement> to pin transitive dependency versions explicitly:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.21.1</version>
</dependency>
</dependencies>
</dependencyManagement>
This overrides whatever version a transitive dependency might request.
Repository Configuration
Lock down which repositories Maven can pull from. In your settings.xml:
<mirrors>
<mirror>
<id>company-repo</id>
<mirrorOf>*</mirrorOf>
<url>https://nexus.yourcompany.com/repository/maven-public/</url>
</mirror>
</mirrors>
The mirrorOf>*</mirrorOf> ensures all artifact requests go through your corporate proxy repository. This gives you a single point of control for blocking malicious packages and caching approved artifacts.
PGP Signature Verification
Maven 4.x will enforce PGP signature verification by default. For Maven 3.x, you can use the pgpverify-maven-plugin:
<plugin>
<groupId>org.simplify4u.plugins</groupId>
<artifactId>pgpverify-maven-plugin</artifactId>
<version>1.17.0</version>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
This verifies that every artifact in your build was signed by its expected maintainer.
Gradle: Securing Your Build
Dependency Verification
Gradle 6.2+ supports built-in dependency verification. Generate a verification metadata file:
gradle --write-verification-metadata sha256,pgp
This creates gradle/verification-metadata.xml containing checksums and PGP key IDs for every dependency:
<components>
<component group="com.google.guava" name="guava" version="32.1.3-jre">
<artifact name="guava-32.1.3-jre.jar">
<sha256 value="abc123..." origin="Generated by Gradle"/>
<pgp value="AABBCCDD"/>
</artifact>
</component>
</components>
Commit this file. Gradle will verify every dependency on subsequent builds and fail if anything changes.
Dependency Locking
Gradle dependency locking pins resolved versions across builds:
dependencyLocking {
lockAllConfigurations()
}
Generate lock files:
gradle dependencies --write-locks
This creates per-configuration lock files that ensure reproducible builds.
Repository Filtering
Restrict which repositories can serve which dependencies:
repositories {
maven {
url = "https://nexus.yourcompany.com/repository/maven-releases/"
content {
includeGroupByRegex("com\\.yourcompany\\..*")
}
}
mavenCentral() {
content {
excludeGroupByRegex("com\\.yourcompany\\..*")
}
}
}
This prevents dependency confusion by ensuring internal packages can only come from your private repository.
Vulnerability Scanning
OWASP Dependency-Check
The OWASP Dependency-Check plugin scans your dependency tree against the NVD:
Maven:
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>9.0.6</version>
<configuration>
<failBuildOnCVSS>7</failBuildOnCVSS>
</configuration>
</plugin>
Gradle:
plugins {
id 'org.owasp.dependencycheck' version '9.0.6'
}
dependencyCheck {
failBuildOnCVSS = 7.0f
}
Viewing Your Dependency Tree
Before you can secure your dependencies, you need to see them:
# Maven
mvn dependency:tree
# Gradle
gradle dependencies
Pipe the output through grep to find specific libraries. Pay special attention to transitive dependencies you did not explicitly choose.
SBOM Generation
Generate CycloneDX SBOMs as part of your build:
Maven:
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.7.10</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>makeAggregateBom</goal>
</goals>
</execution>
</executions>
</plugin>
Gradle:
plugins {
id 'org.cyclonedx.bom' version '1.7.4'
}
Include SBOM generation in your CI pipeline and publish the artifacts alongside your JARs.
How Safeguard.sh Helps
Safeguard.sh integrates with your Java build pipelines to provide continuous dependency monitoring. It consumes the CycloneDX SBOMs generated by your Maven and Gradle builds, tracks every library version across your services, and alerts you when new CVEs are published that affect your stack. When the next Log4Shell hits, Safeguard.sh tells you within minutes which of your applications are affected and which specific versions need upgrading. It replaces the manual triage process with automated, prioritized remediation guidance.