Skip to content

Maven SBOMs miss 93% of the dependency graph #47

Description

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:

MethodComponentsUnknown versionsGrype matches
Syft dir: scan (what the lanes do today)418
cyclonedx-maven-plugin:makeAggregateBom330119

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:

junit-jupiter @ 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions