OWASP Dependency-Track
Verified against Official docs — docs.dependencytrack.org/getting-started/deploy-docker, · official docs
What it is and where it fits 🎯#
OWASP Dependency-Track is a continuous SBOM monitoring platform — you ingest a Software Bill of
Materials (SBOM) from every build, and it keeps every one of those SBOMs checked against newly-published
vulnerability data, indefinitely, without re-scanning anything. This is a precise, deliberate distinction
from Syft and Trivy's SBOM-generation capability (both covered elsewhere in this series): those
tools answer "what's inside this artifact, right now, at build time." Dependency-Track answers the
continuously-updating question this series'
CI/CD Pipeline Security & Supply Chain
chapter poses directly: "of everything we've EVER built and stored an SBOM for, what's affected by
vulnerability data published just today?" — turning what would otherwise be a manual grep across
scattered SBOM files into an automated, continuously-running, alerting system.
The practical payoff is real and specific: two unrelated teams' services can both pin the exact same vulnerable dependency version without either team knowing the other did — a single newly-published CVE lookup in Dependency-Track instantly flags both projects simultaneously, with zero new scans of either codebase, because the matching happens against already-collected component inventories the moment new vulnerability intelligence arrives.
How continuous monitoring differs from a point-in-time scan#
A per-build scanner (Trivy, Grype, run only in CI) is blind to a vulnerability published the day after your last build — Dependency-Track's continuously-running matching engine is what closes that specific gap.
Installation — Docker Compose deployment#
# The officially documented method — API server + frontend as two containers
curl -LO https://dependencytrack.org/docker-compose.yml
docker compose -f docker-compose.yml up -d
# First boot can take several minutes while the vulnerability database initializes — poll until ready
curl -s http://localhost:8081/api/version# Ports in the standard compose file
# 8080 → frontend (web UI)
# 8081 → API serverOpen http://localhost:8080, log in with the default admin/admin (you're forced to change it
immediately on first login), then generate an API key under Administration → Access Management →
Teams — every programmatic upload below needs this key.
Core concepts#
| Concept | What it means |
|---|---|
| Project | One logical application/service — every SBOM you upload targets a specific project (by UUID, or by name+version) |
| Component | One dependency inside a project's SBOM — Dependency-Track tracks it across every project it appears in |
| Finding | A component matched against a known vulnerability — the core unit continuous monitoring produces |
| Policy | A rule engine that can auto-triage findings, fail a build, or route an alert based on severity/license/age conditions |
| Portfolio | The aggregate view across every project ever ingested — this is what makes cross-team, cross-project matching possible |
Generating and uploading an SBOM#
pip install cyclonedx-bom # the CycloneDX Python SBOM generator
cd shipping-rates && cyclonedx-py requirements -o sbom.json && cd .. # generate a CycloneDX SBOM from requirements.txt
# Upload via the REST API — creates the project automatically if it doesn't exist yet
curl -X PUT "http://localhost:8081/api/v1/bom" \
-H "X-Api-Key: $DTRACK_API_KEY" \
-H "Content-Type: multipart/form-data" \
-F "autoCreate=true" \
-F "projectName=shipping-rates" \
-F "projectVersion=1.0.0" \
-F "bom=@shipping-rates/sbom.json"autoCreate=true matters for a CI pipeline specifically — without it, the very first upload for a
brand-new service fails because the project doesn't exist in Dependency-Track yet, which would otherwise
require a manual UI step before the pipeline could run unattended.
Querying findings via the API#
# List every project Dependency-Track currently tracks
curl -s "http://localhost:8081/api/v1/project" -H "X-Api-Key: $DTRACK_API_KEY" | jq '.[].name'
# Get every finding for one project, by UUID
curl -s "http://localhost:8081/api/v1/finding/project/<project-uuid>" \
-H "X-Api-Key: $DTRACK_API_KEY" | jq '.[] | {component: .component.name, vuln: .vulnerability.vulnId, severity: .vulnerability.severity}'Sample finding shape (representative — real UUIDs/vulnerability IDs vary per environment and scan date):
{
"component": { "name": "urllib3", "version": "1.26.4" },
"vulnerability": { "vulnId": "CVE-2023-45803", "severity": "MEDIUM" },
"analysis": { "state": "NOT_SET" }
}analysis.state is the field that tracks whether a human has triaged this finding yet (NOT_SET,
EXPLOITABLE, FALSE_POSITIVE, NOT_AFFECTED, RESOLVED) — the same false-positive/accepted-risk
triage workflow every scanner in this series needs, done centrally across the whole portfolio here rather
than per repository.
Policy configuration — codifying "fail the build" rules centrally#
Policies are configured through the UI (Policy Management) or the REST API; conceptually, a policy combines conditions like these:
| Condition | Example |
|---|---|
| Severity | CRITICAL or HIGH |
| Component age | Older than a defined threshold with no update |
| License | A copyleft license the org's legal team hasn't approved |
| Vulnerability ID | A specific CVE the org has decided is unacceptable regardless of severity score |
# Query whether a project currently has any unresolved policy violations
curl -s "http://localhost:8081/api/v1/violation/project/<project-uuid>" \
-H "X-Api-Key: $DTRACK_API_KEY" | jq 'length'Tip
Centralize policy in Dependency-Track rather than duplicating severity thresholds across every team's CI config. A single policy change (e.g. "CRITICAL findings now block a release") applies instantly across every project already reporting into the platform, instead of requiring a coordinated edit to dozens of individual pipeline YAML files.
Notifications — routing alerts without polling the API#
Configuring an outbound notification publisher (UI: Administration → Notifications) means new findings reach a team the moment they're discovered, instead of requiring someone to remember to check the portfolio:
| Publisher | Typical use |
|---|---|
| Slack / Microsoft Teams webhook | Real-time alert to a security or team channel |
| Digest-style alerts to a distribution list | |
| Webhook (generic JSON) | Feed findings into an internal ticketing/SIEM pipeline |
| Jira | Auto-create a ticket for a new CRITICAL finding |
// Example webhook payload Dependency-Track sends on a NEW_VULNERABILITY notification
{
"notification": {
"level": "LEVEL_ERROR",
"scope": "PORTFOLIO",
"group": "NEW_VULNERABILITY",
"subject": {
"component": { "name": "urllib3", "version": "1.26.4" },
"vulnerability": { "vulnId": "CVE-2023-45803", "severity": "MEDIUM" },
"affectedProjects": [{ "name": "shipping-rates" }, { "name": "invoice-generator" }]
}
}
}Note affectedProjects carries both projects in a single notification when a shared component is
newly flagged — this is the notification-side view of the exact cross-team matching capability this tool
exists for.
Portfolio metrics — tracking exposure trends over time#
curl -s "http://localhost:8081/api/v1/metrics/portfolio/current" \
-H "X-Api-Key: $DTRACK_API_KEY" | jq '{critical: .critical, high: .high, vulnerableComponents: .vulnerableComponents}'Dependency-Track retains a daily metrics snapshot per project and for the whole portfolio — this is what lets you produce a genuine trend line ("vulnerable component count dropped 40% this quarter") rather than only ever reporting a single point-in-time snapshot, which is exactly the kind of longitudinal evidence a SOC 2 Type II report's "operated effectively over a period of time" requirement is actually asking for.
Real-world scenario: catching a shared vulnerable dependency across two teams#
Directly reproducing the scenario this series' supply-chain chapter builds toward — shipping-rates and
invoice-generator, owned by two unrelated teams, both pin the same vulnerable urllib3 version without
either team knowing:
cd shipping-rates && cyclonedx-py requirements -o sbom.json && cd ..
cd invoice-generator && cyclonedx-py requirements -o sbom.json && cd ..
# Upload both as separate projects
curl -X PUT "http://localhost:8081/api/v1/bom" -H "X-Api-Key: $DTRACK_API_KEY" \
-F "autoCreate=true" -F "projectName=shipping-rates" -F "projectVersion=1.0.0" -F "bom=@shipping-rates/sbom.json"
curl -X PUT "http://localhost:8081/api/v1/bom" -H "X-Api-Key: $DTRACK_API_KEY" \
-F "autoCreate=true" -F "projectName=invoice-generator" -F "projectVersion=1.0.0" -F "bom=@invoice-generator/sbom.json"
# Search the whole portfolio for the shared component — this is the actual point of the exercise
curl -s "http://localhost:8081/api/v1/component/name/urllib3" \
-H "X-Api-Key: $DTRACK_API_KEY" | jq '.[] | {project: .project.name, version: .version}'Both projects surface from a single query — zero new scans of either codebase were needed to discover the shared exposure, since the matching engine already had both components in its portfolio.
CI/CD integration recipe — upload after every build#
# .github/workflows/sbom-upload.yml
name: Generate and upload SBOM to Dependency-Track
on:
push:
branches: [main]
jobs:
sbom:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install cyclonedx-bom && cyclonedx-py requirements -o sbom.json
- name: Upload to Dependency-Track
uses: DependencyTrack/gh-upload-sbom@v3
with:
serverHostname: dtrack.internal.example.com
apiKey: ${{ secrets.DTRACK_API_KEY }}
projectName: shipping-rates
projectVersion: ${{ github.sha }}
bomFilename: sbom.jsonUsing ${{ github.sha }} as projectVersion keeps every build's SBOM as its own distinct, queryable
version in Dependency-Track's history — useful for tracing exactly which commit introduced a component
that later turned out to be vulnerable.
Common pitfalls#
- Treating Dependency-Track as a replacement for build-time scanning instead of a complement to it. It only knows about vulnerabilities after an SBOM is uploaded and after a CVE is published in its feeds — it doesn't catch something a build-time scanner (Trivy, Snyk) would flag before merge.
- Never triaging findings'
analysis.state. An unreviewed portfolio quickly accumulates thousands ofNOT_SETfindings, burying genuinely new/urgent ones in noise — treat triage as an ongoing team habit, not a one-time setup step. - Uploading an SBOM without a stable
projectVersion. Reusing the same version string across builds silently overwrites the previous SBOM's history for that version instead of tracking each build separately. - Under-provisioning the API server. Dependency-Track's own docs recommend 8-12GB RAM for the API server at real portfolio scale — an under-resourced instance can fall behind on vulnerability feed ingestion, which quietly degrades how current its "continuous" matching actually is.
Real-world scenario: onboarding a legacy portfolio all at once#
A platform team inheriting 40 existing services with no prior SBOM practice doesn't need to wait for the next release cycle of each one to start getting value:
- Generate an SBOM for each service's current
mainbranch, even without a fresh release —cyclonedx-py(Python),cyclonedx-bom(Node via@cyclonedx/cyclonedx-npm), orsyft(any ecosystem, see its own cheat sheet) all work against an existing checkout, not just a build artifact. - Bulk-upload all 40 SBOMs with
autoCreate=trueso every project registers in one pass. - Immediately run a portfolio-wide finding query — this alone often surfaces the first genuinely
shared, previously-unknown exposure across teams, exactly like the
urllib3scenario above. - Wire each service's CI pipeline to re-upload on every subsequent build going forward, so the portfolio stays current rather than freezing at this one-time baseline snapshot.
This "retroactive bulk onboarding" pattern is a genuinely common, high-leverage first project for a team introducing Dependency-Track — the value shows up immediately, before any team even changes their CI.
Removing a decommissioned project from the portfolio#
curl -X DELETE "http://localhost:8081/api/v1/project/<project-uuid>" \
-H "X-Api-Key: $DTRACK_API_KEY"Deleting a project that's genuinely been decommissioned matters for report accuracy — a stale project still counting toward portfolio-wide vulnerability metrics after the service it represents was shut down quietly inflates every trend report a security team relies on for prioritization.
Shell completion and scripting notes#
Dependency-Track has no CLI binary of its own to add shell completion for — every interaction shown in
this cheat sheet is either the web UI or the REST API, so "scripting Dependency-Track" in practice means
wrapping curl/jq (as above) or one of the community CLI wrappers (dtrack-cli) rather than a
first-party command-line tool.
Exit codes and when to reach for something else#
Dependency-Track is a long-running server, not a CLI with a per-invocation exit code — pipeline gating is
built from querying its API for policy violations/finding severity, as shown above (the gh-upload-sbom
GitHub Action also supports failing the workflow directly on policy violations, via its own inputs). If
the actual need is generating an SBOM in the first place — the input this tool consumes — reach for
Syft or Trivy, both covered in this series. For point-in-time vulnerability scanning of a single
artifact rather than continuous portfolio-wide monitoring, Grype or Trivy remain the right first
tool; Dependency-Track is what you add once "we build many services and want to know instantly when any
of them are newly affected" becomes the actual operational question.