A typical Spring Boot service declares a couple of dozen dependencies and ships with well over a hundred JARs once transitive dependencies are resolved. Any one of them can carry a published vulnerability, and nobody reads the advisories for all of them. OWASP Dependency-Check is a free, open-source tool that does this reading for you: it identifies each library in a build, looks up known vulnerabilities for it, writes a report and can fail the build above a severity threshold.

Using it well takes more than adding a plugin. The tool guesses identities from evidence, so it produces false positives; it depends on vulnerability data that must be downloaded and cached; and a team that cannot triage its findings quickly learns to ignore them. This article explains how matching works, configures the Maven and Gradle plugins and the CLI, handles the data feeds and API keys, works through triage and suppressions, and lists the failure modes to plan for. Configuration names were checked against the project documentation for version 13.0.0 on 2026-10-03.

What it does and what it does not

Dependency-Check is a software composition analysis (SCA) tool. Its job is narrow: given the components in a project, report the publicly known vulnerabilities, mainly CVEs, that apply to the versions you use. It does not tell you whether your code reaches the vulnerable function, it does not detect malicious packages that have no CVE, and it does not check licences. It complements, rather than replaces, the other supply-chain controls described in Software Supply Chain Security Architecture.

It needs a Java runtime (the minimum has been Java 11 since release 11.0.0; check the release notes of the version you adopt) and ships as a Maven plugin, a Gradle plugin, an Ant task and a command-line tool. Besides Java archives it has analyzers for other ecosystems, such as JavaScript through RetireJS and npm audit data, and .NET assemblies; this article stays with the JVM.

How a dependency becomes a finding

Understanding the pipeline explains nearly every confusing result. Analysis runs in phases. First, file-type analyzers collect evidence from each artifact: the JAR manifest, any embedded pom.xml or pom.properties, the file name, and package names. A hash lookup against Maven Central can confirm exact coordinates. Each piece of evidence becomes a candidate vendor, product or version with a confidence level.

Second, the CPE analyzer searches a local index of Common Platform Enumeration names, the naming scheme the NVD uses, for vendor and product pairs that match the evidence. The best matches become identifiers on the dependency. Third, the vulnerability lookup compares each identifier and version against the CPE version ranges in the local copy of NVD data. Separately, the OSS Index analyzer sends package URLs (purls) such as pkg:maven/org.example/lib@1.2.3 to Sonatype's service, which matches on exact coordinates. Finally, findings are enriched, for example flagged when they appear on the known-exploited vulnerabilities list, filtered by suppressions, written to the report and compared with the failure threshold.

Build classpathJARs, POMs, lockfilesAnalyzersmanifest, pom, hash, nameEvidencevendor/product/versionIdentifiersCPE + purlLocal DB (data dir)NVD CVEs + CPE rangesOSS Index APIlookup by purl (auth)Known-exploited listflags KEV entriesReport + gatesuppressions, failBuildOnCVSSmatch CPE + version rangepurlNVD API 2.0 updatesMost false positives enter at the evidence-to-CPE step; most false negatives come from components with no usable evidence.
Dependency-Check matching: evidence from each artifact becomes CPE and purl identifiers, which are matched against the local NVD-derived database and OSS Index before suppressions and the CVSS gate are applied.

The CPE step is fuzzy by design, because many artifacts do not declare which upstream product they are. That fuzziness is why a client library can be matched to a server product of the same name, and why a shaded or renamed JAR with stripped metadata can escape matching entirely.

Maven setup

In Maven, bind the check goal to the build and set a threshold. The default failBuildOnCVSS is 11, which can never be reached on a 0 to 10 scale, so out of the box the plugin reports but never fails. Use aggregate instead of check on a multi-module parent to get one report for all modules.

<plugin>
  <groupId>org.owasp</groupId>
  <artifactId>dependency-check-maven</artifactId>
  <version>13.0.0</version>
  <configuration>
    <nvdApiServerId>nvd</nvdApiServerId>          <!-- key held in settings.xml -->
    <ossIndexServerId>ossindex</ossIndexServerId>
    <failBuildOnCVSS>7</failBuildOnCVSS>
    <failBuildOnUnusedSuppressionRule>true</failBuildOnUnusedSuppressionRule>
    <suppressionFiles>
      <suppressionFile>dependency-check-suppressions.xml</suppressionFile>
    </suppressionFiles>
    <formats><format>HTML</format><format>JSON</format><format>JUNIT</format></formats>
  </configuration>
  <executions>
    <execution><goals><goal>check</goal></goals></execution>
  </executions>
</plugin>

The server ids point at entries in settings.xml, where the API key or token is the server's password, so it can be encrypted with Maven's password encryption and never appears in the POM. Two defaults are worth knowing: skipTestScope is true and skipProvidedScope is false, so test-only libraries are ignored but provided-scope APIs are scanned. Apache Maven, in depth explains how scopes and dependency mediation decide which versions end up on the classpath being scanned.

Gradle and the command line

The Gradle plugin id is org.owasp.dependencycheck. Its tasks are dependencyCheckAnalyze, dependencyCheckAggregate for multi-project builds, dependencyCheckUpdate and dependencyCheckPurge.

plugins {
    id "org.owasp.dependencycheck" version "13.0.0"
}

dependencyCheck {
    failBuildOnCVSS = 7
    formats = ["HTML", "JSON"]
    suppressionFiles = ["dependency-check-suppressions.xml"]
    nvd {
        apiKey = System.getenv("NVD_API_KEY")   // never commit the key
    }
    analyzers.ossIndex {
        password = System.getenv("OSSINDEX_TOKEN")
    }
}
check.dependsOn dependencyCheckAnalyze

Gradle resolves configurations lazily, so make sure the configurations you care about, usually the runtime classpaths, are the ones scanned; Gradle, in depth covers how resolution works. For anything else there is the CLI, which takes --project, --scan, --out, --format, --failOnCVSS, --suppression, --nvdApiKey and --data, plus --updateonly and --noupdate for splitting data refresh from analysis.

The data problem: NVD, caching and OSS Index

The tool keeps a local database built from the NVD's CVE API 2.0. Building it from scratch downloads the whole CVE history and is slow; without an API key, the client waits longer between calls (the Maven docs list a default delay of 8,000 ms without a key and 3,500 ms with one). Request a free NVD API key, which only raises the rate limit, and supply it through nvdApiServerId, an environment variable or a secret store rather than a command line that ends up in logs. Later runs only fetch changes, and by default the plugin skips the check entirely if the data is under four hours old (nvdValidForHours).

In CI the database is the main operational concern. Three patterns work. Cache the data directory between runs, keyed on the plugin's major version. Run a scheduled job with update-only that refreshes a shared cache, and run builds with auto-update off against it. Or, for large organisations, point every build at a central database through connectionString or at an internal mirror of the NVD feed with nvdDatafeedUrl. Whichever you choose, monitor the age of the data, because a build that scans against stale data passes quietly.

OSS Index changed in September 2025: Sonatype began requiring authentication, and the project's changelog for 12.1.5 warns that the analyzer needs a free account from 22 September 2025. Current versions take a Sonatype token through ossIndexPassword or ossIndexServerId; ossIndexUsername is deprecated. Without credentials, either disable the analyzer or set ossIndexWarnOnlyOnRemoteErrors so a remote failure does not break builds, accepting that you lose its purl-based matches.

Worked example: triaging two findings

Worked example. A build fails with two findings above CVSS 7. For each, open the HTML report and read three things: the identifiers and their confidence, the evidence that produced them, and the CVE's affected versions.

Finding one is on a small HTTP client JAR, matched to a CPE for a web server product with a similar name. The evidence shows the match came from the word 'server' in the manifest title and the confidence is low; the CVE describes a request-handling bug in the server, which the client does not contain. This is a false positive, and the right fix is a narrow suppression of that CPE for that package.

Finding two is a JSON library at a version inside the affected range, reached transitively through a framework starter. Run mvn dependency:tree -Dincludes=<groupId> to see which dependency brings it in. If the framework has a release with the fixed version, upgrade the framework. If not, pin the fixed version in <dependencyManagement> (or a Gradle constraint), run the tests, and leave a comment naming the CVE so the pin is removed when the framework catches up. Only if no fixed version exists and the vulnerable feature is unused do you accept the risk, with an expiring suppression.

Suppressions that do not rot

Suppressions are XML rules that hide specific findings. Each rule identifies a dependency, ideally by package URL, and names what to suppress: a CPE, a CVE, a vulnerability name, or every finding below a CVSS score. The until attribute makes a rule expire, after which the finding returns.

<?xml version="1.0" encoding="UTF-8"?>
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.4.xsd">
  <!-- False positive: client JAR matched to a server product's CPE. -->
  <suppress>
    <notes>HTTP client, not the server product. Reviewed by A. Dev, 2026-10-03.</notes>
    <packageUrl regex="true">^pkg:maven/org\.example/http-client@.*$</packageUrl>
    <cpe>cpe:/a:example:example_server</cpe>
  </suppress>
  <!-- Accepted risk: no fix released; feature disabled. Re-review at expiry. -->
  <suppress until="2026-12-31Z">
    <notes>Vulnerable parser mode unused; see ticket SEC-1234.</notes>
    <packageUrl regex="true">^pkg:maven/org\.example/json-lib@2\.4\..*$</packageUrl>
    <cve>CVE-0000-00000</cve>
  </suppress>
</suppressions>

The identifiers in this file are placeholders. Keep rules narrow: a false-positive rule should name the CPE, not the CVE, so a real CVE against the true product still surfaces; an accepted-risk rule should name the CVE and a version range, so an upgrade into another vulnerable version is caught. Turn on failBuildOnUnusedSuppressionRule so rules for removed dependencies are deleted instead of piling up, and review the file like code.

Wiring results into CI and prioritising

Pick report formats for their readers. HTML is for the person triaging. JSON feeds dashboards and lets a script compare this build's findings with the last release's. The JUNIT format makes findings appear as failed tests in CI systems that already render test results; its own threshold, junitFailOnCVSS, defaults to 0, so every finding becomes a failed test case even when the build gate is higher, which is useful for visibility but noisy if nobody expects it.

Severity alone is a weak priority. Order work by three signals together: whether the finding is on the known-exploited list, which Dependency-Check checks by default; whether the vulnerable component is on the runtime classpath of an internet-facing service; and the CVSS score. A medium-severity CVE that is actively exploited in a public endpoint deserves attention before a critical one in a batch tool behind a firewall. Record the decision in the suppression note or the fix commit so the next reviewer can see why.

Failure modes

  • Threshold never set. With the default failBuildOnCVSS of 11 the build never fails; the report exists and nobody reads it.
  • Rate-limited or slow updates. Builds without an API key or shared cache spend minutes on data refresh and time out. Separate refresh from analysis.
  • Stale data. Auto-update disabled and the refresh job broken: every build passes against old data. Alert on data age.
  • Database incompatibility after an upgrade. Major versions have changed the schema, and the changelog notes that H2 databases from older releases did not work with 11.0.0. Purge and rebuild, or upgrade a central database deliberately.
  • Concurrent builds sharing one H2 file. Parallel jobs writing the same local database can lock or corrupt it. Share read-only, refresh in one job.
  • Gaps in upstream data. A CVE whose NVD record lacks CPE configuration data cannot be matched by CPE; purl-based sources partly cover this.
  • Suppression sprawl. Broad regex rules written to make a build green also hide future real findings.

Trade-offs

Dependency-Check's strengths are that it is free, runs offline once the database exists, covers several ecosystems and integrates with every Java build tool. Its weaknesses come from CPE matching: more false positives than tools that match purls against ecosystem advisory databases such as OSV or GitHub's advisory database, and more operational work around the data. Many teams run it alongside a purl-based scanner or a hosted dependency-update service and use whichever finding arrives first; the important part is a single triage process and owner, not the number of scanners.

What to do next

  1. Add the plugin to one service, run it locally and read the report before setting any threshold.
  2. Request an NVD API key and a Sonatype token, store them in settings.xml servers or CI secrets, and never pass them in a logged command line.
  3. Set failBuildOnCVSS to 7 (or your policy), and plan to fix or suppress today's findings before enforcing it.
  4. Move data refresh to a scheduled update-only job with a shared cache, and alert when the data is more than a day old.
  5. Create a suppression file with notes and owners, use until for accepted risk, and turn on failBuildOnUnusedSuppressionRule.
  6. Fix transitive findings by upgrading the parent dependency first, pinning only when needed.
  7. Re-run with a fresh database after each plugin major upgrade.
Key takeaway: Dependency-Check matches build artifacts to known vulnerabilities by turning evidence into CPE and purl identifiers, so expect some false positives and some blind spots. Set a real failBuildOnCVSS threshold, supply an NVD API key and a Sonatype token from secret storage, refresh the database in a separate job and alert on its age, triage by reading identifiers and evidence, fix transitive issues by upgrading or pinning, and keep suppressions narrow, annotated and expiring.