Problem
The Maven and Gradle lanes generate their SBOM by running Syft against a source checkout that has not been built. For Java this misses the overwhelming majority of the dependency graph, so the downstream Grype scan reports almost nothing and passes green.
Measurement
Reproduced locally with Syft v1.46 and Grype, against a two-dependency pom.xml declaring log4j-core:2.14.1 (Log4Shell) and spring-boot-starter-web:2.5.4:
| Method | Components | Unknown versions | Grype matches |
|---|
Syft dir: scan (what the lanes do today) | 4 | 1 | 8 |
cyclonedx-maven-plugin:makeAggregateBom | 33 | 0 | 119 |
The current lane misses 111 of 119 matches — 93.3%.
Critical and High findings present in the resolved graph but invisible to the lane today:
jackson-core, jackson-databind, logback-classic, logback-core, snakeyaml,
spring-beans, spring-boot, spring-boot-autoconfigure, spring-context,
spring-core, spring-expression, spring-web, spring-webmvc, tomcat-embed-core
Of those, the source scan sees only log4j-core and spring-boot-starter-web — the two dependencies written literally in the pom.xml. Everything reached transitively is absent, including KEV-listed criticals in tomcat-embed-core, spring-beans and spring-webmvc.
Two distinct causes
1. No transitive resolution. Syft's java-pom-cataloger parses pom.xml as text. It does not resolve the dependency graph, so a single spring-boot-starter-web entry stays one component instead of expanding to the ~30 artifacts Maven actually puts on the classpath. Most CVEs live in transitives.
2. Managed versions resolve to UNKNOWN. Where a version comes from dependencyManagement or an imported BOM, Syft emits the package with version UNKNOWN:
Grype cannot match a package without a version, so these are silently unscannable. This matters more than the count suggests — centrally managed versions via a parent POM or the Spring Boot / JUnit BOM are the norm in enterprise Java, and ONAP projects use parent POMs heavily. A project that manages every version centrally would produce an SBOM in which every dependency is unscannable.
Confirmed against test-maven-project: 24 components, of which 19 are GitHub Actions from .github/workflows/, 4 are the project's own modules, and exactly one is a third-party dependency — junit-jupiter, with version UNKNOWN. Zero scannable third-party Java dependencies. Grype output: No vulnerabilities found.
⚠️ Note for anyone reproducing this: Syft caches aggressively and will return components from previously scanned trees. Set SYFT_CACHE_DIR to a fresh directory or the numbers will be wrong. My first run was contaminated this way.
Suggested fix
Generate the Java SBOM from the resolved dependency graph rather than from source text.
Maven — verified working locally, and it requires no modification to the consumer's pom.xml:
mvn -B org.cyclonedx:cyclonedx-maven-plugin:2.9.1:makeAggregateBom \
-DoutputFormat=json -DoutputName=sbom-cyclonedx
Direct plugin invocation, exit 0, emits target/sbom-cyclonedx.json. makeAggregateBom handles multi-module reactors, which matters for the ONAP-style projects these lanes target. Output is CycloneDX JSON, so it drops into the existing grype-scan-action contract unchanged.
Gradle — needs more thought. cyclonedx-gradle-plugin normally requires apply plugin: in the build script, which the lanes must not do to consumer projects. Injecting it via gradle --init-script is a standard Gradle mechanism and should work, but is not documented by the CycloneDX project and would need prototyping before being committed to a reusable workflow.
Sequencing note
This changes where the SBOM comes from, so it interacts with the sbom job's current design: that job deliberately runs even when build fails, because it does its own checkout and needs no build artefacts. A resolved-graph SBOM needs a working Maven/Gradle resolution step, so that property has to be revisited — probably by keeping the job independent but accepting that it fails when resolution fails, which is arguably the correct signal anyway.
Why this is filed instead of #46
#46 asked whether Java needs a dependency-audit lane like pip-audit / govulncheck / npm audit. The answer was no — four of the five candidate tools re-query the same GitHub Security Advisory data Grype already uses. That issue is closed.
But verifying the premise behind closing it ("Grype-on-SBOM already covers Java") is what surfaced this. The premise was false, and not for the reason anyone expected: the gap is not a missing scanner, it is SBOM fidelity. Adding a fifth scanner would not have fixed it — every tool in that evaluation reads either the same unresolved pom.xml or a gradle.lockfile most projects do not have.
Expected impact: projects currently passing the Java Grype gate cleanly will start reporting findings once this lands. That is the point, but it should be rolled out with grype_permit_fail / NO_BLOCK_AUDIT_FAIL available so the first run does not block every Java project at once.
Problem
The Maven and Gradle lanes generate their SBOM by running Syft against a source checkout that has not been built. For Java this misses the overwhelming majority of the dependency graph, so the downstream Grype scan reports almost nothing and passes green.
Measurement
Reproduced locally with Syft v1.46 and Grype, against a two-dependency
pom.xmldeclaringlog4j-core:2.14.1(Log4Shell) andspring-boot-starter-web:2.5.4:dir:scan (what the lanes do today)cyclonedx-maven-plugin:makeAggregateBomThe current lane misses 111 of 119 matches — 93.3%.
Critical and High findings present in the resolved graph but invisible to the lane today:
Of those, the source scan sees only
log4j-coreandspring-boot-starter-web— the two dependencies written literally in thepom.xml. Everything reached transitively is absent, including KEV-listed criticals intomcat-embed-core,spring-beansandspring-webmvc.Two distinct causes
1. No transitive resolution. Syft's
java-pom-catalogerparsespom.xmlas text. It does not resolve the dependency graph, so a singlespring-boot-starter-webentry stays one component instead of expanding to the ~30 artifacts Maven actually puts on the classpath. Most CVEs live in transitives.2. Managed versions resolve to
UNKNOWN. Where a version comes fromdependencyManagementor an imported BOM, Syft emits the package with versionUNKNOWN:Grype cannot match a package without a version, so these are silently unscannable. This matters more than the count suggests — centrally managed versions via a parent POM or the Spring Boot / JUnit BOM are the norm in enterprise Java, and ONAP projects use parent POMs heavily. A project that manages every version centrally would produce an SBOM in which every dependency is unscannable.
Confirmed against
test-maven-project: 24 components, of which 19 are GitHub Actions from.github/workflows/, 4 are the project's own modules, and exactly one is a third-party dependency —junit-jupiter, with versionUNKNOWN. Zero scannable third-party Java dependencies. Grype output:No vulnerabilities found.Suggested fix
Generate the Java SBOM from the resolved dependency graph rather than from source text.
Maven — verified working locally, and it requires no modification to the consumer's
pom.xml:Direct plugin invocation, exit 0, emits
target/sbom-cyclonedx.json.makeAggregateBomhandles multi-module reactors, which matters for the ONAP-style projects these lanes target. Output is CycloneDX JSON, so it drops into the existinggrype-scan-actioncontract unchanged.Gradle — needs more thought.
cyclonedx-gradle-pluginnormally requiresapply plugin:in the build script, which the lanes must not do to consumer projects. Injecting it viagradle --init-scriptis a standard Gradle mechanism and should work, but is not documented by the CycloneDX project and would need prototyping before being committed to a reusable workflow.Sequencing note
This changes where the SBOM comes from, so it interacts with the
sbomjob's current design: that job deliberately runs even whenbuildfails, because it does its own checkout and needs no build artefacts. A resolved-graph SBOM needs a working Maven/Gradle resolution step, so that property has to be revisited — probably by keeping the job independent but accepting that it fails when resolution fails, which is arguably the correct signal anyway.Why this is filed instead of #46
#46 asked whether Java needs a dependency-audit lane like
pip-audit/govulncheck/npm audit. The answer was no — four of the five candidate tools re-query the same GitHub Security Advisory data Grype already uses. That issue is closed.But verifying the premise behind closing it ("Grype-on-SBOM already covers Java") is what surfaced this. The premise was false, and not for the reason anyone expected: the gap is not a missing scanner, it is SBOM fidelity. Adding a fifth scanner would not have fixed it — every tool in that evaluation reads either the same unresolved
pom.xmlor agradle.lockfilemost projects do not have.Expected impact: projects currently passing the Java Grype gate cleanly will start reporting findings once this lands. That is the point, but it should be rolled out with
grype_permit_fail/NO_BLOCK_AUDIT_FAILavailable so the first run does not block every Java project at once.