Verified11 commandsAI-assisted

OWASP Dependency-Track

.md

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#

Diagram

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 server

Open 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#

ConceptWhat it means
ProjectOne logical application/service — every SBOM you upload targets a specific project (by UUID, or by name+version)
ComponentOne dependency inside a project's SBOM — Dependency-Track tracks it across every project it appears in
FindingA component matched against a known vulnerability — the core unit continuous monitoring produces
PolicyA rule engine that can auto-triage findings, fail a build, or route an alert based on severity/license/age conditions
PortfolioThe 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:

ConditionExample
SeverityCRITICAL or HIGH
Component ageOlder than a defined threshold with no update
LicenseA copyleft license the org's legal team hasn't approved
Vulnerability IDA 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:

PublisherTypical use
Slack / Microsoft Teams webhookReal-time alert to a security or team channel
EmailDigest-style alerts to a distribution list
Webhook (generic JSON)Feed findings into an internal ticketing/SIEM pipeline
JiraAuto-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.

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.json

Using ${{ 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 of NOT_SET findings, 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 main branch, even without a fresh release — cyclonedx-py (Python), cyclonedx-bom (Node via @cyclonedx/cyclonedx-npm), or syft (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=true so 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 urllib3 scenario 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.