PROJECTIntermediate

OWASP DefectDojo: Self-Hosted Vulnerability Management Platform

Aggregate, deduplicate, and track security findings from 100+ scanners in a unified dashboard. Build a production-grade vulnerability management pipeline...

Dylan H.

Projects

July 1, 2026
10 min read
3-5 hours
OWASP DefectDojo: Self-Hosted Vulnerability Management Platform

Tools & Technologies

DockerDocker ComposeDefectDojoTrivySemgrepcurl / HTTPie

Overview

Security teams using multiple scanners — SAST, DAST, container image scanning, dependency auditing — quickly end up drowning in disconnected CSV exports and PDF reports. Findings sit in silos, duplicates multiply across tools, and nothing tracks remediation SLAs.

OWASP DefectDojo solves this. It is a mature, open-source vulnerability management platform that ingests findings from over 100 security tools, deduplicates them, maps them to your product hierarchy, and drives a full remediation workflow complete with risk scoring, SLA tracking, and CI/CD integration via its REST API.

This guide walks through deploying DefectDojo with Docker Compose, wiring up Trivy and Semgrep as live scanners, and integrating it into a GitHub Actions pipeline so every pull request feeds scan results directly into your vulnerability backlog.

Why DefectDojo?

FeatureValue
100+ scanner parsersTrivy, Nessus, Burp Suite, Semgrep, Bandit, OWASP ZAP, Grype, and more
Smart deduplicationHashes findings across tests so re-runs don't multiply noise
Product / engagement modelOrganises findings by application, sprint, and test type
SLA policiesConfigurable remediation deadlines by severity (Critical: 7d, High: 30d)
REST API & webhooksNative CI/CD integration — push findings from any pipeline
RBACFine-grained roles: superuser, product owner, API user

Architecture

┌─────────────────────────────────────────────────────────┐
│                    DefectDojo Stack                      │
│                                                         │
│  ┌──────────┐   ┌──────────┐   ┌──────────────────┐    │
│  │  Nginx   │──▶│  Django  │──▶│   PostgreSQL 16   │    │
│  │ :80/:443 │   │  :8080   │   │   (defectdojo)    │    │
│  └──────────┘   └────┬─────┘   └──────────────────┘    │
│                      │                                   │
│               ┌──────▼──────┐   ┌──────────────────┐    │
│               │   Celery    │──▶│   Redis 7        │    │
│               │  Workers    │   │  (message broker) │    │
│               └─────────────┘   └──────────────────┘    │
└─────────────────────────────────────────────────────────┘
         ▲                    ▲
         │                    │
┌────────┴───────┐   ┌────────┴───────┐
│  CI/CD Pipline │   │ Scanner Import │
│  (GitHub Acts) │   │ (Trivy, DAST)  │
└────────────────┘   └────────────────┘

DefectDojo uses a Django application server, Celery workers for async import and notification tasks, Redis as the broker, and PostgreSQL for persistent storage. Nginx fronts everything.

Product Hierarchy

DefectDojo organises data in a three-tier model:

Product Type (e.g. "Web Applications")
  └── Product (e.g. "My API Service")
        └── Engagement (e.g. "Sprint 42 Scan" or "CI/CD")
              └── Test (e.g. "Trivy image scan 2026-07-01")
                    └── Finding (CVE-2024-XXXX — Critical)

This hierarchy means you can compare findings across releases, track fix rates per product, and report risk posture by product type.


Prerequisites

  • Linux host with Docker Engine 24+ and Docker Compose v2
  • 4 GB RAM minimum (8 GB recommended for comfortable operation)
  • 20 GB disk (findings database grows over time)
  • Ports 80 and 443 available, or a reverse proxy (Traefik/Nginx) in front

Step 1 — Clone and Configure

git clone https://github.com/DefectDojo/django-DefectDojo.git
cd django-DefectDojo

Copy the environment template and edit credentials:

cp docker/environments/postgres-redis.env.example \
   docker/environments/postgres-redis.env

Open docker/environments/postgres-redis.env and set strong values:

# Database
DD_DATABASE_URL=postgresql://defectdojo:CHANGEME_DB_PASS@postgres:5432/defectdojo
 
# Secret key — generate with: python3 -c "import secrets; print(secrets.token_hex(50))"
DD_SECRET_KEY=CHANGEME_SECRET_KEY_50_CHARS
 
# Admin credentials (first-run only)
DD_ADMIN_USER=admin
DD_ADMIN_MAIL=admin@example.com
DD_ADMIN_FIRST_NAME=Admin
DD_ADMIN_LAST_NAME=User
DD_ADMIN_PASSWORD=CHANGEME_ADMIN_PASS
 
# Optional: SMTP for email notifications
DD_EMAIL_URL=smtp://user:pass@smtp.example.com:587

Step 2 — Launch the Stack

DefectDojo ships a production-ready compose file. The initializer service handles migrations and the first admin user:

docker compose \
  --env-file docker/environments/postgres-redis.env \
  -f docker-compose.yml \
  -f docker-compose.override.yml \
  up -d

Watch the initializer complete (takes 2–4 minutes on first run):

docker compose logs -f initializer
# Look for: "Admin user created successfully"

Once done, the UI is available at http://localhost. Log in with the credentials from your env file.

Production tip: Mount a named volume for PostgreSQL data and set restart: unless-stopped on all services. For TLS, place DefectDojo behind Traefik or Nginx with a Let's Encrypt certificate.


Step 3 — Create Your First Product

DefectDojo organises everything by product. Create one via the API (you can also use the UI):

# Get an API token
TOKEN=$(curl -s -X POST http://localhost/api/v2/api-token-auth/ \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"CHANGEME_ADMIN_PASS"}' \
  | jq -r '.token')
 
echo "Token: $TOKEN"
 
# Create a Product Type
curl -s -X POST http://localhost/api/v2/product_types/ \
  -H "Authorization: Token $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Container Images","critical_product":false,"key_product":false}' \
  | jq .
 
# Create a Product
curl -s -X POST http://localhost/api/v2/products/ \
  -H "Authorization: Token $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My API Service",
    "description": "Main backend API",
    "prod_type": 1
  }' | jq .

Step 4 — Run Trivy and Import Results

4a — Install Trivy

# On the host or in a pipeline runner
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
  | sh -s -- -b /usr/local/bin v0.54.0
 
trivy --version

4b — Scan a Container Image

# Scan and output DefectDojo-compatible JSON
trivy image --format json --output trivy-results.json \
  nginx:latest

4c — Create an Engagement and Import

Every scan lives inside an engagement — a time-boxed testing event. Create one, then import:

# Create a CI/CD engagement (continuous type = always open)
ENGAGEMENT_ID=$(curl -s -X POST http://localhost/api/v2/engagements/ \
  -H "Authorization: Token $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Container Scan - CI/CD",
    "product": 1,
    "target_start": "2026-07-01",
    "target_end": "2026-12-31",
    "engagement_type": "CI/CD",
    "status": "In Progress",
    "deduplication_on_engagement": false
  }' | jq -r '.id')
 
echo "Engagement ID: $ENGAGEMENT_ID"
 
# Import the Trivy JSON report
curl -s -X POST http://localhost/api/v2/import-scan/ \
  -H "Authorization: Token $TOKEN" \
  -F "scan_type=Trivy Scan" \
  -F "engagement=$ENGAGEMENT_ID" \
  -F "file=@trivy-results.json" \
  -F "minimum_severity=Low" \
  -F "active=true" \
  -F "verified=false" \
  | jq '{test_id: .test_id, findings_count: .findings}'

Navigate to Products → My API Service → Engagements → Container Scan in the UI to see your findings, grouped by severity.


Step 5 — Add a SAST Scanner (Semgrep)

# Install Semgrep
pip install semgrep
 
# Run against your codebase
semgrep --config=auto --json --output semgrep-results.json ./src/
 
# Import into the same engagement
curl -s -X POST http://localhost/api/v2/import-scan/ \
  -H "Authorization: Token $TOKEN" \
  -F "scan_type=Semgrep JSON Report" \
  -F "engagement=$ENGAGEMENT_ID" \
  -F "file=@semgrep-results.json" \
  -F "minimum_severity=Low" \
  -F "active=true" \
  | jq '{test_id: .test_id, findings_count: .findings}'

DefectDojo will deduplicate any finding that matches by hash across the Trivy and Semgrep tests, so you won't see the same vulnerability counted twice.


Step 6 — Configure SLA Policies

SLA policies enforce remediation timelines. Navigate to Configuration → SLA Configuration in the UI, or use the API:

curl -s -X POST http://localhost/api/v2/sla_configuration/ \
  -H "Authorization: Token $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Standard SLA",
    "description": "Default remediation SLAs by severity",
    "critical": 7,
    "high": 30,
    "medium": 90,
    "low": 180
  }' | jq .

Assign this SLA to your product. DefectDojo will flag findings that breach their deadline in red on the dashboard.


Step 7 — GitHub Actions CI Integration

Add a workflow that scans on every PR and pushes findings to DefectDojo automatically:

# .github/workflows/security-scan.yml
name: Security Scan
 
on:
  pull_request:
    branches: [main]
  push:
    branches: [main]
 
jobs:
  trivy-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Build image
        run: docker build -t app:${{ github.sha }} .
 
      - name: Run Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: app:${{ github.sha }}
          format: json
          output: trivy-results.json
          severity: CRITICAL,HIGH,MEDIUM
 
      - name: Upload to DefectDojo
        env:
          DD_URL: ${{ secrets.DEFECTDOJO_URL }}
          DD_TOKEN: ${{ secrets.DEFECTDOJO_TOKEN }}
          DD_ENGAGEMENT: ${{ secrets.DEFECTDOJO_ENGAGEMENT_ID }}
        run: |
          curl -s -X POST "$DD_URL/api/v2/import-scan/" \
            -H "Authorization: Token $DD_TOKEN" \
            -F "scan_type=Trivy Scan" \
            -F "engagement=$DD_ENGAGEMENT" \
            -F "file=@trivy-results.json" \
            -F "minimum_severity=Low" \
            -F "active=true" \
            -F "verified=false" \
            -F "commit_hash=${{ github.sha }}" \
            -F "branch_tag=${{ github.ref_name }}"
 
  semgrep-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Run Semgrep
        uses: returntocorp/semgrep-action@v1
        with:
          config: auto
          generateSarif: "0"
        env:
          SEMGREP_RULES: auto
 
      - name: Export JSON
        run: semgrep --config=auto --json --output semgrep-results.json .
 
      - name: Upload to DefectDojo
        env:
          DD_URL: ${{ secrets.DEFECTDOJO_URL }}
          DD_TOKEN: ${{ secrets.DEFECTDOJO_TOKEN }}
          DD_ENGAGEMENT: ${{ secrets.DEFECTDOJO_ENGAGEMENT_ID }}
        run: |
          curl -s -X POST "$DD_URL/api/v2/import-scan/" \
            -H "Authorization: Token $DD_TOKEN" \
            -F "scan_type=Semgrep JSON Report" \
            -F "engagement=$DD_ENGAGEMENT" \
            -F "file=@semgrep-results.json" \
            -F "minimum_severity=Low" \
            -F "active=true"

Add DEFECTDOJO_URL, DEFECTDOJO_TOKEN, and DEFECTDOJO_ENGAGEMENT_ID to your repository secrets.


Step 8 — Enable Smart Deduplication

DefectDojo's deduplication engine prevents findings from multiplying across scans. Configure it per product under Product → Edit → Deduplication. The recommended algorithm is unique_id_from_tool_or_hash_code, which uses the tool's native CVE/rule ID if present, falling back to a content hash.

Check deduplication status from the API:

# List findings and check for duplicates
curl -s "http://localhost/api/v2/findings/?duplicate=true&limit=20" \
  -H "Authorization: Token $TOKEN" \
  | jq '[.results[] | {id, title, severity, duplicate_finding}]'

Testing the Setup

Verify the Stack is Healthy

# All containers should be running
docker compose ps --format "table {{.Name}}\t{{.Status}}"
 
# Django health check
curl -s http://localhost/api/v2/system_settings/ \
  -H "Authorization: Token $TOKEN" | jq '.system_settings_url'

Verify Findings Appear

# Count findings by severity
curl -s "http://localhost/api/v2/findings/?limit=1" \
  -H "Authorization: Token $TOKEN" | jq '.count'
 
# List Critical findings
curl -s "http://localhost/api/v2/findings/?severity=Critical&active=true" \
  -H "Authorization: Token $TOKEN" \
  | jq '[.results[] | {id, title, cve, component_name}]'

Test the Deduplication

Import the same Trivy report twice and confirm the second import produces 0 new findings (all deduplicated):

# Import again
RESPONSE=$(curl -s -X POST http://localhost/api/v2/import-scan/ \
  -H "Authorization: Token $TOKEN" \
  -F "scan_type=Trivy Scan" \
  -F "engagement=$ENGAGEMENT_ID" \
  -F "file=@trivy-results.json" \
  -F "minimum_severity=Low" \
  -F "active=true")
 
echo "New findings: $(echo $RESPONSE | jq '.findings')"
echo "Closed findings: $(echo $RESPONSE | jq '.closed_findings')"

A healthy deduplication result shows findings: 0 (all matched existing records) and closed_findings: 0.


Deployment Considerations

Persistent Storage

Add named volumes to your compose file to survive container restarts:

volumes:
  defectdojo-media:
  postgres-data:
 
services:
  postgres:
    volumes:
      - postgres-data:/var/lib/postgresql/data
  django:
    volumes:
      - defectdojo-media:/app/media

Reverse Proxy with TLS

If running behind Traefik, add labels to the nginx service:

services:
  nginx:
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.defectdojo.rule=Host(`dojo.example.com`)"
      - "traefik.http.routers.defectdojo.entrypoints=websecure"
      - "traefik.http.routers.defectdojo.tls.certresolver=letsencrypt"
      - "traefik.http.services.defectdojo.loadbalancer.server.port=80"

User Management

Create scoped API users for each CI/CD system rather than sharing the admin token:

  1. Navigate to Settings → Users → Add User
  2. Assign the API User role to the product(s) it should write to
  3. Generate a personal API token under that user's profile

Extensions and Next Steps

Add More Scanners

DefectDojo has parsers for all of the following — add them to your pipelines with the same import-scan API call:

  • DAST: OWASP ZAP, Burp Suite, Nuclei
  • Container: Grype, Snyk Container, AWS ECR findings
  • IaC: Checkov, tfsec, Terrascan
  • Dependency SCA: OWASP Dependency-Check, Grype SBOM, npm audit
  • Cloud: AWS Inspector, Microsoft Defender for Cloud export

SBOM Integration

Generate a Software Bill of Materials with Syft, then import it for SCA:

syft nginx:latest -o cyclonedx-json > sbom.json
 
curl -s -X POST http://localhost/api/v2/import-scan/ \
  -H "Authorization: Token $TOKEN" \
  -F "scan_type=CycloneDX Scan" \
  -F "engagement=$ENGAGEMENT_ID" \
  -F "file=@sbom.json" \
  -F "minimum_severity=Low" \
  -F "active=true"

Notifications and Jira Integration

DefectDojo can push finding notifications to Slack, MS Teams, and create Jira tickets automatically when new Critical/High findings are imported. Configure under Configuration → System Settings → Notifications and Configuration → JIRA.

Metrics with Grafana

DefectDojo exposes finding metrics via its API. Pair it with Grafana using a Prometheus push-gateway or build a custom data source to visualise your vulnerability trend over time — open findings by severity, mean time to remediation (MTTR), and SLA compliance rate.


Summary

You now have a self-hosted vulnerability management platform that:

  • Ingests findings from Trivy, Semgrep, and 100+ other tools
  • Deduplicates intelligently so your backlog stays clean
  • Enforces SLAs with configurable timelines per severity
  • Integrates with CI/CD via REST API, tagging findings to commits and branches
  • Scales with Celery workers for async processing of large scan results

DefectDojo bridges the gap between running security tools and actually managing the vulnerabilities they find — turning disconnected scan outputs into an auditable remediation workflow.