geoffwilliams@home:~$

Java and Maven - NVD and SBOM

TL;DR

Maven NVD scanning is broken due to NIST NVD infrastructure, build an SBOM instead and gate the build with Grype or some other tool/process.

Q: Why was I up all night yesterday?

Q: What is dependency-check-maven

  • A: It’s a Maven plugin to scan for dependencies with CVEs, with the power to fail builds if severity is high enough

NIST NVD Service - outage

All joking aside, at least yesterday, the NIST NVD service dependency-check-maven relies on is just hopelessly broken. Downloading without an API key takes hours, if it works at all.

Yesterday, only approx 15% of the database was downloaded after a couple of hours.

At this point, I caved and requested an API key. Even this process took about half an hour, with multiple failures before a working API key was released.

I’ll save you the trouble of fighting for an API key: On my workstation at least, the database downloaded just as slowly as without a key at all. In fact, leaving Maven trying to download the NVD database with a valid key resulted in a 50x error from the server after an hour or so, with the same result after several attempts.

Best-practice is to setup a local (corporate) mirror of NVD, however:

  • There are no actual instructions on how to do so, just a rough outline of the components needed
  • Even if there were, it relies on upstream NVD server working for the initial download

I’m pretty sure that for every team who diligently figures out correct mirroring, there’s another ten who just let their CI servers rip on the NVD upstream servers 24/7 for git pushes, so here we are with an NVD system that’s under so much load it’s unusable.

In reality, few developers will fight through all of this to scan their dependencies for CVEs, so the outcome is skipped tests and/or permanently removed security checks from build pipelines.

Solution? SBOM

This is not an isolated outage, the NVD service has been failing for a while now. To prevent Maven build failures, there’s really no option but to disable/remove the plugin unless you’re hosting your own NVD mirror.

So what do we do instead? Many developers are turning to Software Bill of Materials - SBOM.

In the case of a Java Maven app, this means capturing the full dependency tree. There’s a plugin for this: cyclonedx-maven-plugin.

Adding the plugin to your Maven project results in capturing dependency information as JSON and XML under the /target directory.

Now just hand over your favourite flavour of structured data file to your security scanner/team and deploy as usual right?

If you have a true enterprise build and deploy CI pipeline, with integrated SBOM check before deployment, then yes, you may actually be finished - otherwise, you may just be giving yourself a false sense of security.

The great thing about dependency-check-maven is that it fails the build if severe enough CVEs are detected, and you would have to consciously disable the check to produce a .jar file. With just SBOM generation, you have a list of libraries but the build will still produce executables unless you add something to your build to act on this information.

To put it another way, just producing the SBOM does not magically improve security posture, you must act on this data somehow.

Grype

Enter Grype:

A vulnerability scanner for container images and filesystems.

Grype has built-in support for reading SBOM files created by cyclonedx-maven-plugin. It’s approximately five years old, so still somewhat new. Apply your due diligence to determine whether Grype is right for your own organisation.

Worked Example

Aim: Integrate Grype into a project with multiple high-severity CVEs and force build failure at the Maven package phase.

Step 1 - Simple Java app

I created a simple Maven Java project from scratch with Cursor featuring just a simple CLI app. It works great: wargames app

Step 2 - SBOM

Enable SBOM creation during Maven package phase in pom.xml:

            <plugin>
                <groupId>org.cyclonedx</groupId>
                <artifactId>cyclonedx-maven-plugin</artifactId>
                <version>2.9.2</version>
                <executions>
                    <execution>
                        <id>cyclonedx-bom</id>
                        <phase>package</phase>
                        <goals>
                            <goal>makeBom</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <projectType>application</projectType>
                    <schemaVersion>1.6</schemaVersion>
                    <outputFormat>all</outputFormat>
                    <outputName>bom</outputName>
                </configuration>
            </plugin>

This will result in target/bom.json and target/bom.xml being produced. If you have a multi-module Maven project, you should produce the SBOM once, in the parent POM (with makeAggregateBom).

Step 3 - Try out Grype

We can run grype in a container, to avoid installation on the host system. If you don’t have podman, you can just run the same command with docker:

# if using my example repo as-is
# mvn package -Dgrype.skip=true

# You must create ~/.cache/grype on your HOST _first_
podman run --rm  -v ~/.cache/grype/:/.cache/grype -v ./target:/target docker.io/anchore/grype sbom:/target/bom.json

When run from the top level of the project directory, this will mount the ./target directory containing the SBOM files into the container and run the SBOM scan on the JSON version.

The /.cache/grype directory is volume mounted in the container to avoid approximately 2GB download on every startup (we don’t want another NVD). Interestingly, pulling the entire database takes only 1 minute, not 4+ hours.

Since I started from scratch with no dependencies, Grype printed:

No vulnerabilities found

And took approximately 1 minute to run initially, and around 10 seconds thereafter.

Step 4 - Add dependencies with CVEs

I asked Cursor to add a bunch of dependencies with severe CVEs and he delivered! A ton of really bad libraries (log4shell, and co) were added to pom.xml. Regenerating the SBOM and re-scanning set off some very loud alarms in Grype:

NAME                 INSTALLED   FIXED IN        TYPE          VULNERABILITY        SEVERITY  EPSS            RISK          
log4j-core           2.14.1      2.15.0          java-archive  GHSA-jfh8-c2jp-5v3q  Critical  100.0% (100th)  100.0  (kev)  
struts2-core         2.3.30      2.3.32          java-archive  GHSA-j77q-2qqg-6989  Critical  100.0% (99th)   100.0  (kev)  
log4j-core           2.14.1      2.16.0          java-archive  GHSA-7rjr-3q55-vv33  Critical  100.0% (99th)   99.0   (kev)  
spring-beans         5.3.17      5.3.18          java-archive  GHSA-36p3-wjmg-h94x  Critical  99.7% (99th)    98.7   (kev)  
spring-webmvc        5.3.17      5.3.18          java-archive  GHSA-36p3-wjmg-h94x  Critical  99.7% (99th)    98.7   (kev)  
...

Step 5 - Integrate grype into the build

I asked Cursor to integrate a Grype check into pom.xml and fail the build if necessary. He used the exec-maven-plugin to run the native grype command on the host (no containers - more on how it got installed later):

            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>3.5.0</version>
                <executions>
                    <!--
                      There is no official Anchore Grype Maven plugin on Maven Central.
                      This runs the Grype CLI against the CycloneDX SBOM produced above
                      and fails the build when severity >= ${grype.failOn}.
                      Requires `grype` on PATH (https://github.com/anchore/grype).
                    -->
                    <execution>
                        <id>grype-sbom</id>
                        <phase>package</phase>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                        <configuration>
                            <skip>${grype.skip}</skip>
                            <executable>grype</executable>
                            <arguments>
                                <argument>sbom:${project.build.directory}/bom.json</argument>
                                <argument>--fail-on</argument>
                                <argument>${grype.failOn}</argument>
                            </arguments>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

The arguments are sourced from <properties> that he also added:

    <properties>
        <!-- Fail the build when Grype finds findings at/above this severity. -->
        <grype.failOn>critical</grype.failOn>
        <grype.skip>false</grype.skip>
    </properties>

These combine with the --fail-on above to tell Grype to return a non-zero exit status to the shell when needed:

set the return code to 2 if a vulnerability is found with a severity >= the given severity, options=[negligible low medium high critical]

Step 6 - mvn package

Running mvn package does indeed fail the build with a list of bad dependencies, as we wanted:

[INFO] --- exec:3.5.0:exec (grype-sbom) @ java-do-not-run ---
NAME                 INSTALLED   FIXED IN        TYPE          VULNERABILITY        SEVERITY  EPSS            RISK          
log4j-core           2.14.1      2.15.0          java-archive  GHSA-jfh8-c2jp-5v3q  Critical  100.0% (100th)  100.0  (kev)  
struts2-core         2.3.30      2.3.32          java-archive  GHSA-j77q-2qqg-6989  Critical  100.0% (99th)   100.0  (kev)  
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------

Full output

A desperate developer can still force the build to succeed by bypassing Grype with -Dgrype.skip=true

Here’s the error we want, in all it’s glory:

Maven Grype failed build

Try it yourself

Working demo project released on GitHub: https://github.com/GeoffWilliams/java-do-not-run

Where did the grype command come from?

The astute reader will notice a grype command suddenly appeared on my system without me personally requesting it.

You’re not doing DEVOPS security right if you’re not piping curl output to a root shell, and Grype is no different. While I wasn’t looking (or rather, while I was blindly clicking approve), Cursor found the Grype installation instructions and diligently ran:

curl -sSfL https://get.anchore.io/grype | sudo sh -s -- -b /usr/local/bin

For me. Confirmed by the presence of a file at /usr/local/bin/grype.

Thankfully this is a sandbox VM I have for testing out AI. I can’t say I recommend installing a security system this way(!) A grype package for Debian, installable with apt would be much better.

That said, the binary got installed, it worked, and my “real” system is fine. I finished my SBOM experimenting and can enjoy (what’s left of) my weekend. You’re not welcome.

How does Grype work? (by Cursor)

The following notes are from Cursor

How Grype works (in short): it matches what you already have (packages / SBOM) against a local vulnerability database. It does not upload your code or SBOM to Anchore’s cloud for scanning.

Security / privacy

  • Privacy: Scan targets and results stay local. Anchore’s docs state Grype doesn’t send scan data to external services.
  • What does leave the box: DB update checks/downloads (and image pulls / Maven lookups only if you enable those).
  • Trust surface: You’re trusting Anchore’s published DB and the Grype binary (open source, Apache-2.0). Same class of risk as any SCA tool that syncs a vulnerability feed.
  • False positives / coverage: Matching isn’t perfect (esp. with incomplete SBOM metadata). That’s a quality concern more than a privacy one.
  • Your project: Intentional Log4Shell-era deps mean lots of Critical hits — expected; not a Grype “phone home” issue.

Who’s behind it / buying support

Anchore builds and maintains Grype (and Syft for SBOMs).

  • Open source: Grype itself is free; the public GrypeDB is published at no cost.
  • Commercial: Anchore Enterprise — hosted/enterprise product with support, richer policy/UI, and a larger/enriched dataset than the open GrypeDB alone. That’s the usual “buy support from the vendor” path.

For your Maven gate: Grype only needs network for DB freshness; the SBOM never has to leave the build machine.

Post comment

Markdown is allowed, HTML is not. All comments are moderated.