Java Dependency Scanner
Log4Shell shipped inside thousands of builds that never called Log4j directly. This scanner reads pom.xml and build.gradle as plain text, pulls out each group:artifact and its version, and checks it against a table of twelve known-bad coordinates. No build, no network, no API key.
Reading pom.xml
The actor is the scanner alone. It walks every file in the repository snapshot, and any file named pom.xml (at any depth) goes to a Maven routine. The routine never resolves a parent POM or a property; it sees only the text in front of it.
1A pom.xml declares libraries in <dependency> blocks. Three typical ones: a literal version, a version held in a property, and a version left to a BOM.
2One regex finds every block that has groupId, then artifactId, then an optional <version> right after. The coordinate is group:artifact, and the line number is the count of newlines before the match.
3Guard: an empty version, or one starting with $, is skipped. The scanner cannot resolve ${guava.version} or a BOM-managed version, so it reports nothing rather than guess. A vulnerable Guava hidden behind a property is therefore invisible to this scanner.
4Only the log4j block, with a literal 2.14.1, continues to the table lookup.
Lookup and version compare
The table is a fixed dictionary of 12 group:artifact keys. A coordinate that is not a key is never reported, at any version.
1The surviving coordinate is a dictionary lookup. Log4j-core has one entry: vulnerable below 2.17.1, severity Critical, the Log4Shell CVE.
2Installed version and limit are each split on ., the first three parts are read as integers, and short versions are padded with zeros.
3The rule is installed < limit as a list compare: [2,14,1] < [2,17,1]. The middle numbers differ and 14 is less than 17, so the library is vulnerable. Only the < range form exists in the table.
4A match becomes a Critical finding at confidence 0.85, with the manifest path, the line, and group:artifact:version as the snippet. The fix text is generic: upgrade to the latest version.
5Edge cases. 2.17.0 is still below 2.17.1. 2.21.0 passes, which is the fix. A version with a suffix such as -SNAPSHOT, or a wildcard like 2.14.+, cannot be parsed to integers, and the scanner treats that as not vulnerable, not as an error.
When Struts gives two findings
Most coordinates have one entry. org.apache.struts:struts2-core has two Critical ranges, and each range that matches yields its own finding for the same line.
1Two entries share one coordinate. The scanner tests every entry for the coordinate; it does not stop at the first match.
22.3.30 is below both limits, so it gets two findings, one per CVE, on the same line.
32.4.0 is below 2.5.33 but not below 2.3.37, so one finding.
42.5.33 equals the first limit. The compare is strict less-than, so nothing is reported.
Gradle declarations
build.gradle and build.gradle.kts go through one regex over the whole file. The table and the compare are the same as for Maven.
1One regex runs over the file text. It needs one of five configuration keywords, an opening quote or parenthesis, and a group:artifact:version string.
2The Groovy single-quoted form matches. Log4j-core 2.14.1 is below 2.17.1, so Critical.
3The Kotlin DSL form implementation("…") matches the same regex. Commons Text 1.9 is below 1.10.0 (Text4Shell), so Critical.
4Two quiet cases. A variable version is captured as text, fails the integer parse and is dropped. A configuration such as annotationProcessor is not in the keyword list, so the line is never seen.
Why it matters
Java dependency vulnerabilities have caused some of the largest breaches in history, because one library sits under thousands of applications. Log4Shell (2021), CVE-2021-44228, allowed remote code execution through a log message. Apache Struts (2017), CVE-2017-5638, caused the Equifax breach. Text4Shell (2022), CVE-2022-42889 in Commons Text, allowed remote code execution through string interpolation.
Equifax (2017). 147 million records were stolen because of an unpatched Apache Struts dependency. The patch had been available for two months. Equifax paid $700 million in settlements. Log4Shell (2021) affected hundreds of thousands of applications, including Apple, Amazon, Cloudflare and Steam.
Vulnerable vs fixed
<!-- Maven: Log4Shell-era version -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
// Gradle: same coordinate
implementation 'org.apache.logging.log4j:log4j-core:2.14.1'<!-- Maven: update to safe versions -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.21.0</version> <!-- Fixed -->
</dependency>
// Gradle: update to safe versions
implementation 'org.apache.logging.log4j:log4j-core:2.21.0'Limits and what it stays quiet about
| Coordinate | Limit | Severity | Issue |
|---|---|---|---|
org.apache.logging.log4j:log4j-core | <2.17.1 | Critical | CVE-2021-44228 (Log4Shell) |
org.apache.struts:struts2-core | <2.5.33 and <2.3.37 | Critical | CVE-2023-50164; CVE-2017-5638 |
org.apache.commons:commons-text | <1.10.0 | Critical | CVE-2022-42889 (Text4Shell) |
org.springframework:spring-webmvc | <5.3.28 | High | CVE-2023-34036, DoS |
org.springframework.boot:spring-boot-starter | <3.1.5 | High | CVE-2023-34055, DoS via HTTP/2 |
org.springframework.security:spring-security-core | <5.8.9 | High | CVE-2023-34042, authorization bypass |
com.fasterxml.jackson.core:jackson-databind | <2.15.3 | High | CVE-2023-35116, DoS |
io.jsonwebtoken:jjwt | <0.11.5 | High | Algorithm confusion |
org.apache.tomcat.embed:tomcat-embed-core | <10.1.16 | High | CVE-2023-46589, request smuggling |
mysql:mysql-connector-java | <8.0.33 | High | CVE-2023-21971, unauthorized access |
commons-io:commons-io | <2.14.0 | Medium | CVE-2024-47554, path traversal |
com.google.guava:guava | <32.0.0 | Medium | CVE-2023-2976, temp directory |
The table is hand-maintained and holds one or two entries per library, so a library or a CVE not in it is never reported; the OSV scanner covers Maven coordinates with a live database. Property versions in pom.xml (${…}) and BOM-managed versions with no <version> tag are skipped. A version that does not parse as three integers, such as a -SNAPSHOT suffix, a wildcard or a Maven range, counts as not vulnerable. Gradle version catalogs and unlisted configurations such as annotationProcessor are not read.