Loading...
Skip to main content
Ecosystem Guide

Java Dependency Scanning: What OWASP Dependency-Check Misses

Sudhir P.
Last updated
22 min read

Java's dependency ecosystem is fragmented across Maven Central, Gradle, and enterprise repositories, with transitive trees that explode across 100-300 packages. OWASP Dependency-Check catches vulnerabilities and flags CISA KEV entries, but has no exploitation probability — layer EPSS on top of NVD advisories, and scan a pom.xml with explicit versions or a CycloneDX SBOM, not version ranges.

Your enterprise Java platform pulls in 240 transitive dependencies. Last week, a critical vulnerability dropped in a third-level transitive of Apache Commons. Your scanning tool flagged it as "High" severity. Your team spent 48 hours investigating whether it actually affected your code path, only to discover that the vulnerable function wasn't reachable in your application flow.

Welcome to Java dependency security in 2026. The ecosystem is mature, the tooling is mature, but the signal-to-noise ratio is brutal. A single Spring Boot application can depend on 150+ JAR files transitively. Maven Central hosts millions of packages, many abandoned. When a vulnerability disclosure hits, you need to know not just "do I have it?" but "can attackers actually exploit it in my app?" and "is anyone actually exploiting it right now?"

This is where most Java teams fail. They run OWASP Dependency-Check, get a list of 40 findings, treat CVSS 6.0 as equally critical as CVSS 9.0, and patch everything. Or worse, they ignore everything because the signal is too noisy. Neither approach scales. Scaling requires understanding what dependency vulnerability scanning actually does beyond producing a CVE list — the enrichment step is what turns 40 findings into a short, defensible one.

Reachability changes the calculus too: sometimes the vulnerable package is shaded into your JAR under a relocated package name, sometimes it's declared optional and never loaded, and sometimes you're simply not affected despite a CVSS 9.0 headline. None of that shows up in a bare CVE list — it only shows up once you trace the dependency path and cross-reference exploitation signals from FIRST's EPSS model and the CISA KEV catalog.

What We'll Cover

Why Java Dependency Security Is Different

Java dependency security is harder than npm or Python because of five compounding factors: transitive trees that explode past 200 packages, vulnerability data fragmented across Maven Central, NVD, and GitHub advisories, build-time dependency resolution, JAR shading that hides vulnerable code under relocated package names, and enterprise patch cycles that routinely run four to eight weeks.

1. Transitive dependency explosion

Spring Boot web app (Boot 3.0.0)
└── spring-boot-starter-web
    ├── spring-boot-starter
    │   ├── spring-boot (+ autoconfigure)
    │   ├── spring-core
    │   ├── snakeyaml
    │   └── spring-boot-starter-logging
    │       ├── logback-classic
    │       ├── log4j-to-slf4j (bridge)
    │       └── jul-to-slf4j
    ├── spring-boot-starter-json
    │   ├── jackson-databind ← many CVEs
    │   ├── jackson-datatype-jdk8
    │   ├── jackson-datatype-jsr310
    │   └── jackson-module-parameter-names
    ├── spring-boot-starter-tomcat
    │   ├── tomcat-embed-core
    │   └── tomcat-embed-el, -websocket
    ├── spring-web
    └── spring-webmvc

That one starter resolves to roughly 30 jars before you add anything else, and every starter, library and plugin you add brings its own tree. Each is a potential vulnerability vector.

2. Multiple artifact repositories with inconsistent vulnerability data

Java packages live in Maven Central, JCenter (deprecated), Gradle plugins portal, corporate nexus instances, and custom private repositories. Unlike npm's unified advisory database, Java's vulnerability data is fragmented:

  • NVD (NIST): Comprehensive but slow to propagate
  • GitHub Security Advisories: Fast but incomplete for Java-specific issues
  • CVE Details: Niche, sometimes contradicts NVD
  • Package maintainer security pages: Highly variable coverage
  • Apache security advisories: Often the first source, but outside NVD for weeks

3. Dependency resolution complexity

Maven and Gradle resolve dependencies at build time, not install time. This means:

  • Version ranges ([1.0, 2.0)) are evaluated during build, not at definition time
  • Transitive dependency trees can be resolved differently based on build order, exclusions, and classloader hierarchy
  • BOM (Bill of Materials) imports can shadow or override direct dependencies
  • Maven's "nearest wins" strategy is different from npm's deterministic lock-file approach

4. Shading, relocation, and bundling

Java projects often "shade" dependencies—bundling them inside the JAR with relocated package names. Example:

<!-- In pom.xml -->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-shade-plugin</artifactId>
  <executions>
    <execution>
      <phase>package</phase>
      <goals><goal>shade</goal></goals>
      <configuration>
        <relocations>
          <relocation>
            <pattern>org.apache.commons</pattern>
            <shadedPattern>com.mycompany.shaded.commons</shadedPattern>
          </relocation>
        </relocations>
      </configuration>
    </execution>
  </executions>
</plugin>

Now the commons library is embedded, version-locked, and isolated. A vulnerable version inside the shade is still a problem, but it's invisible to standard classpath scanning. Tools need to understand shading to catch these.

5. Long patch cycles in enterprise

Enterprise Java teams have SOC2/compliance constraints. Patching a critical Spring Framework vulnerability might require:

  • Code review (1-2 weeks)
  • Testing in staging (1-2 weeks)
  • Change approval board (1-2 weeks)
  • Staged rollout (1-2 weeks)

That 4-8 week cycle means zero-day vulns in Java hit production before patches land. Knowing which vulns are actually being exploited (CISA KEV) vs. which are theoretical attacks (high CVSS, low EPSS) changes the urgency entirely.

What Dependency Files GeekWala Accepts for Java

GeekWala reads two kinds of Java input:

FileWhat it covers
pom.xmlThe dependencies declared in it with an explicit <version> (${property} placeholders are resolved). A dependency without a version — one managed by a parent POM or BOM, the usual Spring Boot setup — or with a version range stops the scan with an error asking for explicit versions. Transitive dependencies aren't included.
CycloneDX or SPDX JSON SBOM (bom.json, *.cdx.json, *.spdx.json)The full resolved tree, for Maven and Gradle builds alike.

Gradle build files (build.gradle, build.gradle.kts), gradle.lockfile and version catalogs aren't parsed directly — generate an SBOM with the CycloneDX Maven or Gradle plugin instead (see the setup steps below).

Best practice: Generate the SBOM in CI from the same build that produces your artifact, so the scan sees exactly the versions you ship. For Gradle, enable dependency locking and commit gradle.lockfile so builds — and the SBOM made from them — are reproducible.

A pom.xml on its own only shows the versions written in it; everything your build resolves beyond that goes unchecked until you scan the SBOM.

OWASP Dependency-Check vs GeekWala

OWASP Dependency-Check is the de facto standard for Java vulnerability scanning. It's open-source, free, integrated into Jenkins/GitLab/GitHub Actions, and widely trusted. But "standard" doesn't mean "sufficient." It scans every JAR in your classpath and cross-references NVD reliably, but beyond flagging CISA KEV entries it has no exploitation signal — no EPSS — and no sense of code-path reachability.

OWASP Dependency-Check's approach is conservative: it scans all JAR files in your classpath, including transitive dependencies, and cross-references them against NVD. It's thorough and catches nearly everything in its database. But it has three limitations:

  1. No exploitation probability: Its Known Exploited Vulnerability analyzer flags CVEs already on CISA's KEV list, but for everything else it tells you "Apache Commons Collections 3.2 has a deserialization vulnerability" and not "how likely is anyone to exploit this?" A vulnerability disclosed in 2014 might have CVSS 9.8 but EPSS 0.01 if no one has exploited it in years.

  2. Limited cross-database coverage: OWASP Dependency-Check relies primarily on NVD, which has gaps for Java-specific advisories that appear in GitHub Security Advisories or Apache security pages first.

  3. A static severity: EPSS scores change daily. A vulnerability might start at EPSS 0.05 and jump to 0.8 when a public PoC emerges; a CVSS-only report never moves, while a scanner that shows today's EPSS (GeekWala refreshes it daily) puts that finding back at the top on the next scan.

CapabilityOWASP Dependency-CheckGeekWala
Advisory sourcesNVD primaryOSV (includes GitHub Security Advisories)
JAR file scanning✓ (analyzes bytecode + checksums)✗ (reads pom.xml or an SBOM)
Transitive dependency analysis✓ (comprehensive)✓ from an SBOM; ✗ from pom.xml alone
Exploitation signals (EPSS)✗✓ EPSS score for each finding
Actively exploited tracking (KEV)✓ flags CISA KEV entries (no EPSS)✓ CISA KEV status, ranked first
Shading detectionLimited (checksum-based)✗
Gradle support✓ (scans JAR output)Via a CycloneDX or SPDX SBOM
BOM/version catalog support✗✗ (BOM-managed versions need an SBOM)
Historical trend tracking✗✓ (per-project vulnerability history; EPSS shown is today's)
Local vs. continuousBest for CI/CD automationBest for production monitoring

Use OWASP Dependency-Check for local development and CI/CD gates—it's thorough, free, offline-capable, and integrates everywhere. Build it into your pipeline.

Use GeekWala for production monitoring and risk-based prioritization—when you need to know not just "what's vulnerable?" but "what's being exploited right now?" Scan your Maven or Gradle dependencies →

Java's Transitive Dependency Explosion

Here's where Java differs most from npm or Python: transitive dependency trees can exceed 300 packages in a single application. Even one Spring Boot starter brings in roughly 30 jars you never explicitly chose or audited.

A vulnerable package rarely sits at the top of your dependency graph — it's usually several levels down, pulled in by something you did choose. The chain below shows the shape of that problem: your project depends on a direct dependency, which depends on a transitive dependency, which depends on the actually-vulnerable package three levels deep.

Transitive Dependency Tree: a vulnerable artifact (ByteBuddy) reached three levels deep — your project depends on spring-boot-starter-data-jpa, which depends on Hibernate, which depends on the vulnerable ByteBuddy artifact. You never declared ByteBuddy directly, but it still ships on your classpath.

This isn't a Java-specific quirk — it's the general shape of transitive dependency risk: the packages you didn't choose are the ones a scanner spends most of its time on, and the ones most likely to carry a CVE you didn't know you shipped. This explosion is why effective scanning requires:

  1. Accurate dependency resolution: Tools must understand Maven version ranges and Gradle constraint resolution
  2. Transitive path tracing: When a vulnerability is found 4 levels deep, you need to see the exact chain: my-app → spring-boot-starter-data-jpa → Hibernate → ByteBuddy → CVE-2023-xxx
  3. Scope filtering: Understanding which dependencies are compile, runtime, test, or optional — test dependencies in production vulnerabilities are less critical

Real-World Java Vulnerability Examples

Java's vulnerability landscape has evolved dramatically. Log4Shell showed how one ubiquitous logging library can compromise nearly every enterprise Java stack at once; Jackson's deserialization CVE showed how a flagged vulnerability can still be safe absent the right gadget chain; Spring4Shell showed CVSS alone can't tell you whether your actual code path is exposed.

Example 1: Log4Shell — CVE-2021-44228

One of the most damaging Java vulnerabilities on record. In log4j-core from 2.0-beta9 up to 2.15.0 (2.12.2 and 2.3.1 are patched backports), an attacker who controls a logged string can trigger a JNDI lookup that loads and runs code from a server they control:

your-app/
├── pom.xml
│   └── log4j-core:2.14.1  ← vulnerable
│       └── 🔴 CVE-2021-44228 · on KEV
│           CVSS 10.0 · EPSS 0.99+
└── ...

The cascade was massive because log4j-core is the logging backend of a long list of Java servers and frameworks (Elasticsearch and Apache Solr both shipped advisories), and it is often bundled inside other jars where mvn dependency:tree won't show it. Spring Boot's default Logback setup was not exposed — it ships only the log4j-to-slf4j bridge — but apps that switched to spring-boot-starter-log4j2 were.

Impact: Within hours of disclosure, threat actors exploited unpatched systems. CISA added it to KEV on December 10, 2021, a day after disclosure, and its EPSS score is still above 0.99. Within 48 hours, patching became an emergency. Teams with continuous vulnerability scanning caught it before attackers; teams with quarterly scans got breached.

Example 2: Jackson Databind Deserialization — CVE-2017-7525

Jackson-databind allows arbitrary code execution through polymorphic deserialization if certain gadget chains are in the classpath:

your-app/
├── pom.xml
│   └── jackson-databind:2.8.8
│       └── 🔴 CVE-2017-7525 · not on KEV
│           CVSS 9.8 · EPSS 0.38
│
└── Exploitable only with polymorphic
    typing enabled and a usable gadget
    class on the classpath

This vulnerability required a gadget chain in the classpath. Your app might have CVE-2017-7525 flagged but still be safe if you don't include the vulnerable gadget chain. OWASP Dependency-Check flags it as critical (CVSS 9.8). GeekWala ranks it below anything on CISA KEV but above CVSS-only findings: its EPSS of about 0.38 puts it in the 30–70% band of the ladder (patch within two weeks), and whether your classpath carries a gadget chain tells you whether to move faster.

Example 3: Spring4Shell — CVE-2022-22965

Spring Framework before 5.2.20, and 5.3.0 through 5.3.17, has a remote code execution flaw in data binding. The published exploit needs JDK 9+ and a WAR deployed on Tomcat:

your-app/
├── pom.xml
│   └── spring-webmvc:5.3.10
│       └── 🔴 CVE-2022-22965 · on KEV
│           CVSS 9.8 · EPSS 0.99+
│
└── Risk depends on:
    ├── JDK 9+ and a WAR on Tomcat?
    ├── Request params bound to POJOs?
    ├── If both: critical, patch now.
    └── If neither: still patch, less
        urgently.

The signal-to-noise problem: CVSS 9.8 looks like universal emergency. But exploitation requires specific code patterns. EPSS helps: at 0.99+ it signals real attack activity, and CISA lists it on KEV. But context matters — a Spring Boot app run as an executable jar (the default) is not exposed to the published exploit, and many REST APIs never bind request parameters onto POJOs.

Major Java Vulnerabilities That Changed the Game

Java's vulnerability landscape shifted after Log4Shell. Before 2021, enterprise teams patched on quarterly cycles. Log4Shell showed that Java ecosystems could have zero-day exploits in the wild within hours. That single event reset expectations across the industry — executives now expect detection in days rather than months, and CVSS-only triage has given way to EPSS- and KEV-weighted prioritization as the default.

This changed three things:

  1. Enterprise monitoring expectations: Executives now expect vulnerability detection to happen in days, not months
  2. Transitive dependency visibility: Teams realized they couldn't manually track 200+ transitive dependencies
  3. Prioritization matured: CVSS alone became insufficient; EPSS and KEV became table stakes

The pattern: Major Java vulns now spike to EPSS 80-95+ and hit CISA KEV within 72 hours if exploitable. Tools that only look at CVSS flag everything equally. Tools that include EPSS and KEV let you sort 40 findings down to the 3-4 that actually require emergency response.

How to Prioritize Java Vulnerabilities

Effective Java vulnerability prioritization requires abandoning CVSS as your primary sort key and adopting a multi-signal approach: check CISA KEV first — a listed CVE is confirmed active exploitation and overrides everything else. Then weight EPSS score, vulnerability category, and whether the affected code sits on a public attack surface before assigning a patch-within-24-hours versus routine-maintenance priority.

Finding: vulnerability in a transitive dependency.

  1. CISA KEV (actively exploited)?
    • Yes: PRIORITY 🔴 CRITICAL (patch within 24 hours).
    • No: continue to EPSS.
  2. EPSS 0.7 or higher (70%+ probability of exploitation)?
    • Yes: PRIORITY 🟠 HIGH (patch within 1 week; above 0.9 it is CRITICAL).
    • No: continue to context.
  3. Vulnerability category:
    • Deserialization (Jackson, YAML, Kryo): escalate to 🟠 HIGH if EPSS > 0.4.
    • Remote code execution (Spring, log4j): escalate to 🟠 HIGH if EPSS > 0.3.
    • Authentication bypass (Spring Security): escalate to 🟠 HIGH if EPSS > 0.5.
    • Information disclosure (low EPSS): keep as 🟡 MEDIUM if EPSS < 0.3.
    • Denial of service (regex, XML bomb): 🟡 MEDIUM (depends on public exposure).
    • Other: continue to context.
  4. Attack surface:
    • Vulnerability in public API (Spring controller): escalate one level.
    • Vulnerability in internal lib (only used internally): defer one level.
    • Vulnerability in test dependency: defer to maintenance cycle (🟢 LOW).
    • Vulnerability in optional feature (disabled): defer to next maintenance window.
  5. Final priority assignment:
    • 🔴 CRITICAL (CISA KEV or EPSS above 0.9): patch within 24 hours.
    • 🟠 HIGH (EPSS 0.3–0.9 or high category): patch within 1 week (EPSS 0.7+) or 2 weeks (EPSS 0.3–0.7).
    • 🟡 MEDIUM (EPSS 0.1–0.3 or medium category): patch within 30 days.
    • 🟢 LOW (EPSS below 0.1 and low category): patch at the next release.

This decision list is a Java-flavored version of the default ladder and framework in our vulnerability prioritization guide: the deadlines are the guide's (KEV or EPSS above 90% within 24 hours, 70–90% within 1 week, 30–70% within 2 weeks, 10–30% within 30 days, below 10% routine), and what is Java-specific is the category and attack-surface rules that can move a finding up or down a tier. The KEV-then-EPSS ordering and the "high CVSS, low real risk" pattern hold across every ecosystem, not just Maven.

First example (internal library, low EPSS):

FieldDetail
Findingorg.apache.commons:commons-collections:3.2 → CVE-2015-6420
CVSS9.8 (arbitrary code execution)
EPSS0.05 (5% exploitation probability)
KEVNot listed (not actively exploited)
CategoryDeserialization (normally high)
ContextUsed only internally by custom audit logging, not in the main request path
Decision🟡 MEDIUM → patch within 30 days, in the next maintenance release, not an emergency
ReasoningHigh CVSS but a historical vulnerability with minimal active exploitation

Second example (actively exploited):

FieldDetail
Findingorg.springframework:spring-webmvc:5.3.10 → CVE-2022-22965
CVSS9.8 (remote code execution)
EPSS0.96 (96% exploitation probability)
KEVYes (CISA actively exploited list)
CategoryRemote code execution
ContextDirect public API, handles untrusted user input
Decision🔴 CRITICAL → patch within 24 hours
ReasoningRCE, actively exploited, accessible from the internet

Setting Up Java Dependency Scanning

Setting up Java dependency scanning takes five steps: export your full resolved dependency list as an SBOM, upload it to GeekWala, read CVSS/EPSS/KEV context on each finding, trace the dependency path to the vulnerable package with mvn dependency:tree, and wire up scheduled scans so new disclosures surface on each re-scan.

Step 1: Export your full dependency list

GeekWala can read pom.xml directly, but only the dependencies it declares with an explicit version (${property} placeholders are resolved). Transitive dependencies, and versions inherited from a parent POM or BOM such as spring-boot-starter-parent, aren't in that file, so a pom.xml scan covers only part of the tree. For the full resolved tree, generate a CycloneDX SBOM:

# Maven: writes target/bom.json with every resolved dependency
mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom

# Gradle: apply the org.cyclonedx.bom plugin, then
gradle cyclonedxBom

# To read the tree yourself
mvn dependency:tree

Step 2: Upload to GeekWala

Upload bom.json on the scan page — CycloneDX and SPDX JSON SBOMs are accepted for every ecosystem — or paste your pom.xml on GeekWala's Java scanning page for a quick check of the declared dependencies. Gradle build files aren't parsed directly; use the SBOM. Anonymous scans cover the first 10 packages; sign in free to scan up to 500 packages per scan.

GeekWala checks each package against OSV (which includes GitHub Security Advisories) and adds EPSS and CISA KEV to every finding.

Step 3: Interpret the results

GeekWala enriches each finding with:

  • CVSS: Severity (how bad is it if exploited)
  • EPSS: Exploitation probability (how likely is actual exploitation)
  • CISA KEV: Active exploitation confirmation
  • Fixed version: The release that patches it, when the advisory names one

Work them KEV first, then EPSS descending (GeekWala's default ranking already follows that order). Ignore CVSS as your primary sort key.

For Java specifically, pay special attention to vulnerabilities in:

High-impact Java packages:

  • Spring Framework (core, security, data, cloud)
  • Log4j (logging framework)
  • Jackson (JSON databinding — deserialization risks)
  • Hibernate (ORM — SQL injection, deserialization)
  • Apache Commons (collections, lang, io — widely used)
  • Tomcat (servlet container)
  • Jetty (servlet container)
  • Kafka (message broker — often handles untrusted data)
  • Elasticsearch (search — injection risks)
  • OpenJDK/JDK modules (cryptography, XML parsing, etc.)

Step 4: Trace the dependency path

Run mvn dependency:tree -Dincludes=<groupId>:<artifactId> to see which of your dependencies brings the vulnerable artifact in. Understanding the chain matters:

  • Scenario A: your-app → spring-boot-starter-security → spring-security → commons-codec → CVE. Fix: update spring-security (it pulls in the patched commons-codec).
  • Scenario B: your-app → custom-internal-lib → commons-codec → CVE. Fix: either update custom-internal-lib, or add a direct exclusion/override in pom.xml.
  • Scenario C: your-app → spring-boot-starter-web → jackson-databind → CVE. But: you only use Jackson for REST parsing, not deserialization. Risk: Medium (unlikely to hit the vulnerable code path, but still patch).

Step 5: Set up scheduled scanning

For production, set up scheduled scans:

<!-- Maven: pom.xml for CI integration -->
<plugin>
  <groupId>org.owasp</groupId>
  <artifactId>dependency-check-maven</artifactId>
  <version>9.0.0</version>
  <executions>
    <execution>
      <goals>
        <goal>check</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Or use GeekWala's scheduled scans:

  • Daily or weekly automatic scans (Pro)
  • A scan-complete email and a scan.completed webhook list new and resolved vulnerabilities
  • Custom thresholds in the GitHub Action (e.g., "fail CI if any KEV finding is detected" via fail-on-kev)

Java-Specific Challenges: Shading, Relocation, and Bundling

One of Java's quirks: developers can shade (bundle and relocate) dependencies to avoid classpath conflicts. This makes scanning harder. A shaded dependency gets bundled inside a JAR under a relocated package name, so a scanner that only inspects the compiled classpath can miss it entirely. What a manifest- or SBOM-based scanner like GeekWala sees depends on whose JAR did the shading.

Example: a library shaded inside another JAR (illustrative)

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-shade-plugin</artifactId>
  <configuration>
    <relocations>
      <relocation>
        <pattern>org.apache.commons</pattern>
        <shadedPattern>org.apache.kafka.common.utils.org.apache.commons</shadedPattern>
      </relocation>
    </relocations>
  </configuration>
</plugin>

Now the Commons classes are embedded inside that JAR with a relocated package name. If your build does the shading, the library is still a declared dependency of yours, so it appears in your pom.xml and your SBOM and GeekWala checks it. If a third-party JAR shaded it, your build only declares that JAR: neither your SBOM nor GeekWala sees the library inside it, and you depend on the vendor's own advisories — or a binary scanner such as OWASP Dependency-Check, Trivy or Grype run against the built artifact.

Handling shading in your scanning workflow:

  1. Scan your declared dependencies — your pom.xml, or better, a CycloneDX SBOM of the build — so libraries you shade yourself are covered
  2. For third-party JARs known to shade dependencies, follow the vendor's advisories, or run a binary scanner on the built artifact
  3. When patching, remember that shaded vulnerabilities are "version-locked" — if a library you use shades a vulnerable commons-codec internally, you must upgrade that library to a version that shades a patched commons-codec

Java Dependency Security Best Practices

Ten practices separate teams that catch Java vulnerabilities early from teams that get breached: centralize versions with a Maven BOM or Gradle catalog, always commit lock files, scan continuously rather than only before release, and understand how Maven's nearest-wins resolution can silently reintroduce a patched vulnerability.

1. Use Maven BOM or Gradle dependency catalog for consistency

Instead of hardcoding versions across multiple pom.xml files, use a BOM:

<!-- parent-pom.xml or shared bom -->
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>3.2.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

Or in Gradle:

// gradle/libs.versions.toml
[versions]
spring-boot = "3.2.0"
jackson = "2.17.0"

[libraries]
spring-boot-web = { group = "org.springframework.boot", name = "spring-boot-starter-web", version.ref = "spring-boot" }

This centralizes version management — when a vulnerability is found, you update one version and it propagates across all projects.

2. Always commit lock files

Maven: there is no built-in lock file — generate a CycloneDX SBOM in CI (Step 1 above) so every scan sees the resolved versions. Gradle: enable dependency locking and always commit gradle.lockfile.

Without lock files, your build might resolve to different versions in different environments. Scanning becomes guesswork.

# Gradle: Lock dependencies
./gradlew dependencies --write-locks
git add gradle.lockfile
git commit -m "Update dependency locks"

3. Scan continuously, not just before release

EPSS scores spike when public exploits emerge. A vulnerability at EPSS 0.1 on Monday might be at EPSS 0.85 by Wednesday. Weekly scans catch rising threats early.

Set up the scan.completed webhook (or rely on the scan email) so your team sees newly matched vulnerabilities as soon as each scheduled scan completes.

4. Understand Maven's dependency resolution strategy

Maven uses "nearest wins" — if multiple versions of a library are in your tree, the closest to your application wins. This can lead to surprising resolution:

your-app
├── jackson-databind 2.9.8  ← declared
└── spring-boot-starter-json
    └── jackson-databind 2.14.1

Result: 2.9.8 wins (nearer to your app)

If 2.14.1 has a fix and 2.9.8 is vulnerable, your explicit dependency just pinned the vulnerable version. Use mvn dependency:tree to audit this.

5. Review optional dependencies carefully

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-orm</artifactId>
  <optional>true</optional>
</dependency>

Optional dependencies are in your classpath during development but might not be in production. If you're not using the optional feature, don't include it. If you do use it, patch vulnerabilities in it just as aggressively as non-optional dependencies.

6. Keep Spring Framework and Log4j up to date

Both are high-velocity ecosystems with frequent security patches. Subscribe to their mailing lists:

Check weekly. When a new version drops, prioritize testing and upgrading.

7. Test updates in staging before production

A newer version of Spring Security might fix a vulnerability but break an integration point. Test in staging first. For critical vulnerabilities (CISA KEV), you might skip this and patch production directly, but normally: test first.

8. Monitor for transitive dependency updates

Dependabot and Renovate can automatically create PRs to update transitive dependencies. This is valuable — it catches vulnerabilities in packages you didn't directly specify:

# Dependabot PR: "Bump commons-collections from 3.2 to 3.2.2"
# (dependency of dependency of your code)

Review and merge these automatically. They're usually low-risk patches to deep transitive dependencies.

9. Understand the difference between "patched" and "patched for your code path"

A deserialization vulnerability in Jackson exists but might only be exploitable if you:

  1. Deserialize untrusted JSON
  2. Enable polymorph type handling
  3. Have specific gadget chains in your classpath

If none of these apply, you're technically safe, but still patch. Supply chain attacks could inject new code later.

10. Keep dependency tree depth in check

Deep dependency trees are vulnerability multiplication. Every additional layer is a potential supply chain risk. When you have the option:

  • Use libraries with fewer transitive dependencies
  • Disable optional features you don't use
  • Break large monoliths into separate services with smaller dependency footprints

FAQ

Why does CVSS say 9.8 but EPSS says 0.1?

CVSS measures severity if exploited. EPSS measures likelihood of exploitation. A vulnerability might be severe (CVSS 9.8) but hard to exploit (EPSS 0.1) due to:

  • Specific preconditions required
  • No public exploit available
  • Difficult attack vector
  • Limited real-world impact

Example: YAML deserialization in PyYAML has CVSS 9.8 (arbitrary code execution) but EPSS 0.05 (most users aren't deserializing untrusted YAML).

Can I scan a Gradle project?

Yes, through an SBOM. GeekWala doesn't parse build.gradle or gradle.lockfile directly: apply the CycloneDX Gradle plugin (org.cyclonedx.bom), run gradle cyclonedxBom, and upload the JSON it writes. Commit gradle.lockfile (./gradlew dependencies --write-locks) so the SBOM matches what you ship.

My JAR has a shaded dependency. How do I scan it?

Scan the dependencies that went into the JAR, not the JAR itself: an SBOM from the CycloneDX Maven or Gradle plugin lists them, including the ones the shade plugin bundles. GeekWala doesn't inspect JAR contents, so a library copied in without a declared dependency won't appear.

A vulnerability was published, but my scanner didn't catch it for 48 hours. Why?

Java's advisory fragmentation is real, and it runs in the direction people don't expect: advisories frequently land before the CVE record. We measured Maven's NVD-to-advisory interval at a 0.05-day median (p90 0.10 days, n=671), with 12.8% of matched advisories preceding their own CVE. GeekWala queries OSV, which aggregates several sources, because coverage differs between them, not because propagation is slow. Subscribing to the Spring and Apache mailing lists is still worth doing — for the advisories that never get a CVE at all.

I have 60+ vulnerabilities found. Where do I start?

Start with the CISA KEV findings, then work down by EPSS (GeekWala's default ranking already puts findings in that order). For the full prioritization framework, see our EPSS deep dive.

For Java specifically, prioritize vulnerabilities in:

  1. Remote code execution findings (RCE)
  2. Authentication/authorization bypass
  3. Deserialization vulnerabilities (Jackson, Kryo, etc.)
  4. Injection vulnerabilities (SQL, YAML, expression language)

Then sort by EPSS within each category.

Does GeekWala work with private Maven repositories?

Not as such. GeekWala doesn't authenticate to private registries and never downloads packages: it matches package coordinates and versions against public advisory data (OSV), so an artifact that exists only in a private repository has no public advisory and shows no findings. If your private package wraps a public library, vulnerabilities in the public library are still detected when that library appears in your SBOM or pom.xml. Enterprise private registry support is planned.

Can I patch a transitive dependency without updating its parent?

Only by adding a direct dependency override in your pom.xml or build.gradle. But be cautious:

<!-- pom.xml: Force newer version of transitive dependency -->
<dependency>
  <groupId>commons-codec</groupId>
  <artifactId>commons-codec</artifactId>
  <version>1.16</version>
</dependency>

This works but can break compatibility if the parent library expects the old version. Patch the parent library instead when possible.

For a side-by-side comparison of all ecosystem scanners, see 7 Dependency Vulnerability Scanners Compared.


Discover which Java vulnerabilities are actually being exploited — not just which ones exist.

Upload your pom.xml or a CycloneDX SBOM → — get results enriched with EPSS exploitation scores and CISA KEV active exploitation status in under a minute. No account needed for up to 10 packages.