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?
- A1: Maven
- A2: I wasted hours of my day trying to scan my code with
dependency-check-maven
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:

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] ------------------------------------------------------------------------
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:

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.