Safeguard
Open Source Security

Auditing Spring Boot dependencies with OWASP Dependency-C...

A step-by-step spring boot dependency audit using OWASP Dependency-Check and Snyk, from Maven setup to CI automation and finding reconciliation.

Aman Khan
AppSec Engineer
Updated 7 min read

Spring Boot's dependency graph grows fast. A typical production service pulls in Spring Boot starters, Jackson, Netty, Tomcat, Hibernate, and dozens of transitive libraries you never explicitly chose — and any one of them can carry a known CVE that turns into an incident. A proper spring boot dependency audit catches these issues before they ship, giving you a defensible, repeatable process instead of a one-off scramble every time a new CVE makes headlines.

In this guide, you'll set up two complementary scanners — OWASP Dependency-Check and Snyk dependency scanning — against a Spring Boot / Maven project, interpret their output, reconcile overlapping findings, and wire the whole thing into CI so every pull request gets checked automatically. By the end, you'll have a working, automatable spring boot dependency audit pipeline plus a way to triage what actually matters versus noise.

Step 1: Build a Clean Dependency Inventory

Before scanning, get an accurate picture of what's actually on your classpath. Maven's dependency plugin gives you the full resolved tree, including transitive dependencies that don't appear in your pom.xml directly:

mvn dependency:tree -Dverbose -Doutput=dependency-tree.txt

Also generate a flat list of resolved versions, which is useful later when cross-referencing scanner output:

mvn dependency:list -DoutputFile=dependency-list.txt -Dsort=true

If you're on a multi-module project, run these at the parent POM level with -am so child module dependencies are captured too. This inventory step matters more than it sounds — most "false positive" disputes with security scanners actually come from not knowing which version of a transitive dependency is truly on the runtime classpath (Maven's nearest-wins conflict resolution can surprise you).

Step 2: Run OWASP Dependency-Check Maven Plugin

Add the OWASP Dependency-Check Maven plugin to your pom.xml. It cross-references your dependencies against the National Vulnerability Database (NVD) and flags known CVEs by CPE match:

<plugin>
    <groupId>org.owasp</groupId>
    <artifactId>dependency-check-maven</artifactId>
    <version>9.2.0</version>
    <configuration>
        <failBuildOnCVSS>7</failBuildOnCVSS>
        <formats>
            <format>HTML</format>
            <format>JSON</format>
        </formats>
        <suppressionFiles>
            <suppressionFile>dependency-check-suppressions.xml</suppressionFile>
        </suppressionFiles>
    </configuration>
</plugin>

Run it directly:

mvn org.owasp:dependency-check-maven:check

The first run downloads and caches the NVD data feed locally, which can take several minutes — budget for that in CI by caching the ~/.m2/repository/org/owasp/dependency-check-data directory between runs, or point dataDirectory at a persistent volume. Using owasp dependency-check maven this way gives you an offline-friendly, NVD-grounded baseline that doesn't depend on an external service being reachable at scan time, which matters for air-gapped or regulated environments.

Step 3: Interpret the Dependency-Check Report

Open target/dependency-check-report.html. Each finding lists the dependency, the matched CVE, CVSS score, and the CPE identifier Dependency-Check used to make the match. Read the CPE match carefully — Dependency-Check's biggest weakness is CPE mismatching, where it flags a library because its group/artifact name loosely resembles an unrelated product with a similar name.

For genuine false positives, suppress them explicitly rather than ignoring the report:

<suppress>
    <notes>False positive: CPE match against unrelated "commons" product</notes>
    <packageUrl regex="true">^pkg:maven/commons\-io/commons\-io@.*$</packageUrl>
    <cve>CVE-2021-XXXXX</cve>
</suppress>

Never suppress by wildcarding an entire artifact without a CVE-specific scope — that silently masks future, legitimate findings against the same library.

Step 4: Add Snyk Java Scanning for Broader Coverage

Dependency-Check is NVD-only, which means it misses vulnerabilities that are documented in vendor advisories, GitHub Security Advisories, or private feeds before they get an NVD CVE assigned. This is where Snyk java scanning fills the gap — Snyk maintains its own curated vulnerability database and typically surfaces issues faster, plus it understands Maven's dependency resolution well enough to suggest exact upgrade paths.

Install the CLI and authenticate:

npm install -g snyk
snyk auth

Run a scan against your Spring Boot project:

snyk test --all-projects --severity-threshold=high

For a continuously monitored baseline (rather than a one-time test), register the project so Snyk alerts you when new vulnerabilities are disclosed against dependencies you already ship:

snyk monitor --org=your-org --project-name=spring-boot-service

Snyk java scanning — a form of Snyk dependency scanning tuned to Maven's resolution model — also supports --fix suggestions and, in many cases, automatic PRs that bump a vulnerable transitive dependency via a <dependencyManagement> override — worth enabling once you trust the signal-to-noise ratio on your repo.

Step 5: Reconcile Findings Between Both Tools

You'll now have two reports that overlap but rarely match exactly. Build a simple reconciliation habit:

  • Findings flagged by both tools are near-certain true positives — prioritize these first.
  • Findings unique to Dependency-Check are often CPE mismatches — verify the CPE before acting.
  • Findings unique to Snyk are often advisory-only issues without an NVD CVE yet — treat these as early warning, not noise.

A lightweight way to diff the two JSON outputs is to normalize both to groupId:artifactId:version -> CVE pairs and compare:

jq -r '.dependencies[].vulnerabilities[]? | "\(.name) \(.severity)"' target/dependency-check-report.json | sort -u > odc-findings.txt
snyk test --json | jq -r '.vulnerabilities[] | "\(.packageName) \(.severity)"' | sort -u > snyk-findings.txt
comm -12 odc-findings.txt snyk-findings.txt

This kind of cross-referencing is the core of mature java vulnerability management: no single scanner is authoritative, and the value comes from triangulating multiple sources rather than trusting one tool's severity score blindly.

Step 6: Automate the Spring Boot Dependency Audit in CI

A manual audit is a snapshot; an automated one is a control. Add both scanners to your CI pipeline so every build enforces the policy. Here's a GitHub Actions example:

name: dependency-audit
on: [pull_request]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
      - name: OWASP Dependency-Check
        run: mvn org.owasp:dependency-check-maven:check
      - name: Snyk scan
        uses: snyk/actions/maven@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high

Set failBuildOnCVSS conservatively at first (e.g., 9, critical-only) to avoid blocking every PR on day one, then tighten the threshold as your suppression list and triage backlog stabilize.

Troubleshooting and Verification

Dependency-Check hangs or times out on first run. The NVD data download is large and NVD's API has been rate-limited since 2023. Request a free NVD API key and set it via -DnvdApiKey=$NVD_API_KEY — this dramatically speeds up the initial feed sync.

Snyk reports a vulnerability with no available fix. Check whether the vulnerable code path is actually reachable (snyk test --print-deps combined with manual review of the call path). If it's a dev-only or test-scope dependency, it's lower priority than a runtime dependency shipped in your artifact.

Findings don't match between local runs and CI. Confirm both environments resolve the same dependency versions — a stale local .m2 cache or a floating version range ([1.0,)) in a transitive dependency will produce different resolved trees. Pin ranges explicitly in dependencyManagement.

To verify your audit setup is actually working, intentionally add a dependency with a known CVE (e.g., an old jackson-databind version) to a throwaway branch and confirm both tools flag it and CI fails the build. Remove it once confirmed — this is the fastest way to prove your pipeline isn't silently passing.

How Safeguard Helps

Running OWASP Dependency-Check and Snyk side by side is a strong foundation, but stitching their output into a single source of truth, tracking suppressions over time, and proving continuous coverage to auditors is its own ongoing effort. Safeguard's software supply chain security platform ingests findings from both tools (and others) into one normalized view, automatically deduplicates overlapping CVEs, and maintains an audit trail of every suppression decision with its justification — exactly the evidence SOC 2 and similar frameworks expect.

Safeguard also tracks your Spring Boot dependency tree continuously rather than at scan time only, so a newly disclosed CVE against a library you shipped six months ago surfaces immediately instead of waiting for your next scheduled scan. If you're building out java vulnerability management as a repeatable program rather than a one-time exercise, Safeguard turns the manual reconciliation described above into an automated, always-current control you can point to during a security review.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.