# OWASP Dependency-Track Cheat Sheet

> **Tool:** OWASP Dependency-Track
> **Category:** Security & Compliance
> **Verified against:** Official docs — docs.dependencytrack.org/getting-started/deploy-docker,
> docs.dependencytrack.org/usage/cicd, and the CycloneDX `cyclonedx-py`/`cyclonedx-bom` tool docs —
> 2026-09-05 (a server application, not a local CLI — verified against current official docs and the
> project's own docker-compose deployment guide rather than a `--help` run)
> **Official docs:** https://docs.dependencytrack.org/

## 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](/tutorials/devsecops/05-cicd-pipeline-security-and-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

```mermaid
sequenceDiagram
    participant CI as CI Pipeline (many teams)
    participant DT as Dependency-Track
    participant NVD as Vulnerability feeds (NVD, OSV, GitHub Advisories...)

    CI->>DT: Upload SBOM after every build (ongoing, many projects)
    Note over DT: Component inventory accumulates<br/>across EVERY team's builds over time
    NVD->>DT: New CVE published, today
    DT->>DT: Match new CVE against EVERY stored<br/>component inventory — no re-scanning needed
    DT-->>CI: Instant alert: "Project A AND Project B<br/>both affected" — zero new scans triggered

    Note over DT,NVD: This step is the entire point — a scanner that<br/>only runs at build time can't catch a NEW CVE<br/>against an artifact that was already shipped.
```

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

```bash
# 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
```

```bash
# 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

| 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

```bash
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

```bash
# 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):

```json
{
  "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 |

```bash
# 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 |
| Email | 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 |

```json
// 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

```bash
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:

```bash
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

```yaml
# .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

```bash
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.
