Secure the PipelineSCAStep 9 of 31

Catch Vulnerable Dependencies for Free (OWASP Dependency-Check)

difficulty intermediatehands-on 25 min hands-on

29% complete

prereqs · what-a-pipeline-is

concepts · NVD API key · CPE matching · database build cost · suppression files · tool operating cost

Every SCA tool in this chapter answers the same question — which of my dependencies are vulnerable — and mostly agrees on the answer. What separates them is not detection. It is what they cost to operate, and OWASP Dependency-Check is the clearest lesson in that, because its operating cost is high, unavoidable, and entirely invisible until you run it.

This lesson runs it against the Range, lands on a number identical to Trivy's, and spends most of its time on the two hours it took to get there.

Verified against OWASP Dependency-Check 13.0.0 on 14 August 2026, scanning app/package-lock.json in the Range. Every number is from a real run.

Step 1 — It will not run without an NVD API key

Install it and point it at the app, the obvious way:

bash
brew install dependency-check

dependency-check \
  --project aiopsone-range \
  --scan app/ \
  --format JSON --format HTML

It fails immediately:

text
[ERROR] Error updating the NVD Data
NvdApiException: Invalid API Key, length of 0 too short to provide a masked partial key
[ERROR] No documents exist

This is not a misconfiguration you can skip. Since v9, the National Vulnerability Database retired the old bulk JSON feeds, and Dependency-Check now builds its database exclusively from the NVD API 2.0 — which requires a key. Without one, the update fails and there is nothing to scan against.

Get a free key at nvd.nist.gov/developers/request-an-api-key. Then — and this is the step that catches everyone — click the activation link NVD emails you. An unactivated key returns 404/Invalid API Key, indistinguishable from a wrong one. Confirm the key works before blaming the tool:

bash
curl -s -o /dev/null -w '%{http_code}' \
  -H "apiKey: $NVD_API_KEY" \
  "https://services.nvd.nist.gov/rest/json/cves/2.0?resultsPerPage=1"
# 200 = live and activated · 404 = not activated / wrong key

Step 2 — Then it downloads 377,000 CVEs

With a valid key, run it again — and wait:

bash
dependency-check \
  --project aiopsone-range \
  --scan app/ \
  --data ./dc-data \
  --nvdApiKey "$NVD_API_KEY" \
  --format JSON --format HTML
text
[INFO] Checking for updates
[INFO] Downloaded  50,000/377,185 (13%)
[INFO] Downloaded 190,000/377,185 (50%)
[INFO] Downloaded 377,185/377,185 (100%)

The first sync downloaded all 377,185 CVEs in the NVD and took about two hours — even with a key, because the NVD API is rate-limited and Dependency-Check paginates through the entire dataset to build a local H2 database. Point --data at somewhere with a couple of gigabytes free; the database is not small.

This two-hour first run is the whole point of the lesson. Trivy ships its vulnerability database inside the binary and updates a delta in seconds. Grype does the same. Snyk queries a hosted service. Dependency-Check makes you build and host the database — which means every fresh CI runner without a warm cache pays a version of this cost. Subsequent runs are fast (it updates only the delta), but "subsequent" is doing a lot of work in that sentence.

Step 3 — The findings, which are genuinely good

Once the database exists, the scan itself takes seconds:

text
Dependencies scanned:   13
Vulnerable:             12
Vulnerabilities:        54   (CRITICAL 3, HIGH 22, MEDIUM ~25, LOW 4)

By package, the worst offenders:

Package Findings
axios@0.21.0 25
lodash@4.17.4 10
jsonwebtoken@8.1.0 3
path-to-regexp@0.1.7 3
ejs, express, body-parser, minimist, qs 2 each

Now put it beside the tools from the Trivy and Snyk lessons, on the identical package-lock.json:

Tool Findings Database First-run cost
Dependency-Check 13.0.0 54 you build it from the NVD ~2 hours + an API key
Trivy 0.72.0 54 shipped in the binary seconds
Snyk 1.1306.3 63 hosted service account sign-in

Dependency-Check found exactly what Trivy found — 54. Same axios@0.21.0 at the top, same lodash. On detection, it is right there with the tool that costs nothing to run. The gap is entirely operational.

Step 4 — CPE matching, and why it needs a suppression file

Dependency-Check identifies a dependency by inferring its CPE (Common Platform Enumeration — the NVD's naming scheme, cpe:2.3:a:vendor:product:version) and matching that against every CVE for that CPE. For npm it augments this with the Node Audit analyzer, but the CPE core is where its reputation for false positives comes from: a library whose name collides with an unrelated product's CPE inherits that product's CVEs.

So the tool ships with a first-class suppression mechanism, and using it honestly is part of operating it:

xml
<!-- dependency-check-suppression.xml -->
<suppress>
  <notes>False positive: our internal 'range-utils' is not the
    unrelated PyPI 'range' package this CVE refers to. Confirmed
    against the advisory 2026-08-14.</notes>
  <packageUrl regex="true">^pkg:npm/range\-utils@.*$</packageUrl>
  <cve>CVE-2019-xxxxx</cve>
</suppress>
bash
dependency-check --scan app/ --suppression dependency-check-suppression.xml ...

Suppress by CVE with a note and a date, never with a blanket exclusion. A suppression is a decision that a finding does not apply to you; it should read like one. <notes> is not optional documentation — it is the difference between a reviewed exception and a silenced scanner, and it is what an auditor reads when they ask why a High was ignored.

Step 5 — Gate it (and warm the cache)

The gate is the same shape as every other SCA tool — fail on severity — but the CI job has to solve the database problem or every run pays the two hours:

yaml
# .github/workflows/sca.yml
- name: OWASP Dependency-Check
  uses: dependency-check/Dependency-Check_Action@main
  with:
    project: aiopsone-range
    path: app/
    format: SARIF
    args: >
      --failOnCVSS 7
      --nvdApiKey ${{ secrets.NVD_API_KEY }}
      --suppression dependency-check-suppression.xml

- name: Cache the NVD database
  uses: actions/cache@v4
  with:
    path: ~/.dependency-check/data
    key: dependency-check-nvd

Two lines carry the operational reality: --nvdApiKey from a secret (never hard-code it), and caching ~/.dependency-check/data so only the first CI run builds the database and the rest update a delta. Without that cache, this job is the slowest thing in your pipeline by an order of magnitude.

Step 6 — So should you use it

On findings alone, it is a peer of Trivy — 54 to 54, the same top packages. If the tool were free to operate, this would be an easy yes.

It is not free to operate:

  • You own the database. An API key, its activation, a multi-hour first build, a couple of gigabytes of storage, and a CI cache to maintain. Trivy and Grype hand you all of that inside a binary.
  • CPE matching is noisier, so you also own a suppression file and the triage that fills it.

Reach for it when the operating cost is actually a feature: an air-gapped or regulated environment where you want the vulnerability database on infrastructure you control rather than pulled from a vendor on every run, or where OWASP provenance is what a compliance reviewer expects to see. For everyone else, Trivy finds the same 54 in seconds with nothing to operate — and that, not the finding count, is the decision.

What you learned

  • It will not run without a free NVD API key — and the key must be activated via NVD's email, or it returns Invalid API Key exactly like a wrong one.
  • The first sync downloads all ~377,000 NVD CVEs and takes ~2 hours, because you build and host the database. That operating cost, not detection, is what sets it apart.
  • It found 54 vulnerabilities (3 critical, 22 high) on the Range's npm deps — the same 54 as Trivy, below Snyk's 63. On detection it is a peer.
  • CPE matching produces false positives, so a suppression file with per-CVE notes and dates is part of running it honestly.
  • Cache ~/.dependency-check/data in CI or every run pays the full build.
  • Use it when you want to own the database (air-gapped, regulated, OWASP provenance). Otherwise Trivy finds the same thing in seconds with nothing to operate.