DefectDojo
DefectDojo is an open-source, self-hosted vulnerability-management and aggregation platform — an OWASP Flagship Project — built to solve exactly one problem: a pipeline running more than one scanner produces more findings than any human can triage as a flat list, and a meaningful share of them are the same underlying bug reported two or three different ways. DefectDojo ingests scan output from well over a hundred supported tools, in whatever native format each one emits, and normalizes all of it into one hierarchy of Products, Engagements, Tests, and Findings — deduplicating across tools by rule ID or a computed hash, tracking each finding's status through a defined lifecycle, and attaching a severity-driven SLA clock that turns "we have 4,000 open findings" into a scheduled, auditable backlog. It's the tool this course's own CDP blueprint closes on, and the reason is structural: every scanner covered earlier in this course produces a report, and DefectDojo is what happens to that report next. By the end of this page you should know its architecture, its data model, exactly how its deduplication logic works, the commands and API calls you'll actually run against it, and where a narrower tool like Dependency-Track or a commercial ASPM platform earns its keep instead.
Imagine a school where four different teachers each grade the same stack of essays for a different thing — spelling, grammar, plagiarism, and factual accuracy — and each one hands back a separate pile of sticky notes stuck to the same essays. If you just dump all four piles on one desk, you get sticky notes that say the same thing twice, notes with no due date, and no way to tell which essay needs the most urgent rewrite. DefectDojo is the one office assistant who takes all four piles, notices when two notes are actually about the same sentence, throws away the duplicate, stamps a deadline on what's left based on how serious it is, and hands you back one single list you can actually work through.
What DefectDojo is, and the problem it names directly
☺ Like you're 10: It started as one company's internal spreadsheet-killer and grew into the open-source tool most pipelines reach for once "just read every scan report by hand" stops being possible.
DefectDojo began inside Rackspace's security engineering team — credited to Greg Anderson and Jay Paz — as an internal tool for tracking findings across the company's own application-security testing, before being open-sourced and, over time, donated into the OWASP ecosystem, where it's maintained today as a community project distributed under the BSD 3-Clause license and carries OWASP's Flagship designation, the tier reserved for projects that have demonstrated both strategic value and active, ongoing maintenance. That lineage matters for a practical reason: DefectDojo wasn't designed around one scanner's output format and generalized later. It was designed from day one to be format-agnostic, because Rackspace's own security team was already running more than one tool and drowning in more than one report shape.
The problem it names directly is the one this course's closing CDP blueprint chapter spends an entire chapter on: Semgrep and gitleaks at commit, OWASP Dependency-Check or Trivy at build, Checkov against the same Terraform a container scan already covers, OWASP ZAP against staging — run daily across even a modest number of services and the raw finding count stops being something a person reads end to end. DefectDojo's answer isn't a smarter scanner. It's a normalization and bookkeeping layer that sits downstream of every scanner this course teaches, which is exactly why it's the one tool page in this course with no scan target of its own — its input is other tools' output.
DefectDojo detects nothing. It has no rule engine, no vulnerability feed of its own, no crawler. Its entire value is in what happens after a scanner already produced a finding: getting it into one place, recognizing when two tools reported the same bug, and keeping a durable, auditable status on it across every rescan that follows. That's a different kind of tool than everything else in this course's tool landscape, and it's worth holding that distinction clearly before the rest of this page.
Architecture: Django, PostgreSQL, and where the import pipeline actually runs
☺ Like you're 10: It's a website with a filing cabinet behind it, plus a second, slower worker in the back room who does the actual sorting so the website up front never has to freeze while a big import happens.
DefectDojo is a Django application — Python, a relational schema, a standard web-app shape — backed by PostgreSQL as its system of record. That alone would be enough for a small deployment, but a scan import can be a multi-thousand-line JSON or XML file, and parsing, deduplicating, and writing that many rows synchronously inside an HTTP request is exactly the kind of thing that times out a request or blocks the UI for every other user. DefectDojo solves that with an async task queue: Celery workers, coordinated through Redis as the broker, pick up import and reimport jobs off a queue and process them in the background, so the API call that submits a scan returns quickly while the actual parsing, matching, and deduplication happen on a worker process that can be scaled independently of the web tier. Celery beat handles scheduled work — periodic reimports, SLA-breach notification sweeps, scheduled reports — on its own clock, separate from anything a user triggered.
Two deployment paths cover most real installs. The official Docker Compose quick-start (docker-compose up against the project's own compose file, seeded with a generated admin password on first boot) is the fastest way to a working instance and what most teams evaluate the tool with first. For production, the project publishes a Helm chart for a Kubernetes deployment — separate Deployments for the Django app, the Celery workers, and Celery beat, backed by an external or in-cluster PostgreSQL and Redis, which is what lets the worker tier scale independently under real import volume rather than fighting the web tier for the same pod's resources. A bare-metal or manually-provisioned install is documented too, but nearly every team either runs the Docker Compose stack for evaluation or the Helm chart for anything that has to survive real traffic.
The data model: Product Type, Product, Engagement, Test, Finding
☺ Like you're 10: Before anything can be filed neatly, you need labeled drawers — a broad one, a specific one, a time window, one scanner's output, and finally the single issue itself.
Every finding DefectDojo holds lives inside a strict hierarchy, and knowing where a finding sits in it is most of what "using DefectDojo correctly" actually means. This course's blueprint chapter covers the full reasoning behind each level and how the triage workflow built on top of it operates day to day; this page's version is the tool-level summary worth having cold.
| Level | Represents | Worked example |
|---|---|---|
| Product Type | A broad grouping — a business unit, a portfolio, a compliance boundary | "Payments" |
| Product | One deployable thing — usually a 1:1 mapping to a repository or a service | "checkout-service" |
| Engagement | A bounded testing window against that product — a CI run, a sprint, a pen-test | "checkout-service · main · build #4821" |
| Test | One scanner's output within that engagement | "Trivy Scan," "ZAP Scan" |
| Finding | One specific issue inside a Test | "CVE-2024-XXXXX in follow-redirects@1.14.9" |
A sixth object sits alongside that chain rather than inside it: an Endpoint — a URL, a host, or a host-plus-port pair — is what a DAST or API-security finding attaches to in addition to its Test, which is what lets DefectDojo answer "which of our forty staging endpoints still has this reflected-XSS finding open" rather than just "this Engagement has an open XSS finding somewhere." A finding from OWASP ZAP or Burp Suite's Dastardly scanner typically carries one or more Endpoints; a finding from a source-code scanner like Semgrep typically doesn't, since a file path inside the Finding record already answers the equivalent question.
Getting a Product Type and Product created is a one-time setup step, done once per repository or service rather than per scan — everything after that is Engagements and Tests arriving continuously as the pipeline runs.
Ingesting scanner output: the import/reimport API and the parser library
☺ Like you're 10: One doorway takes in a brand-new report; a second doorway updates a report you've already filed, so the same folder gets thicker over time instead of a new folder appearing every single day.
DefectDojo ships parsers for well over a hundred scanners — covering essentially every tool named across this course's SAST, DAST, SCA, secrets, container, IaC, and compliance pages — reachable through a REST API v2 exposed at /api/v2/, documented interactively at /api/v2/doc/ via its own Swagger/OpenAPI schema. Two endpoints do nearly all the work, and which one a pipeline calls matters more than almost anything else on this page:
# first run of a given Test in a given Engagement — creates it fresh
curl -s -X POST "https://defectdojo.internal.example.com/api/v2/import-scan/" \
-H "Authorization: Token $DD_API_TOKEN" \
-F "product_name=checkout-service" \
-F "engagement_name=checkout-service · main · build #4821" \
-F "scan_type=Trivy Scan" \
-F "file=@trivy-report.json" \
-F "minimum_severity=Low" \
-F "active=true"
# every run after that — updates the SAME Test instead of creating a new one
curl -s -X POST "https://defectdojo.internal.example.com/api/v2/reimport-scan/" \
-H "Authorization: Token $DD_API_TOKEN" \
-F "product_name=checkout-service" \
-F "engagement_name=checkout-service · main · build #4821" \
-F "test_id=$DD_TRIVY_TEST_ID" \
-F "scan_type=Trivy Scan" \
-F "file=@trivy-report.json"import-scan creates a new Test and its findings from a blank slate; reimport-scan updates an existing Test, which is the mechanism behind auto-closing a finding that a later scan no longer reproduces, and behind a triager's False Positive marking surviving into next week's scan instead of resetting every single run. Pointing a nightly pipeline at import-scan on every run is one of the single most common DefectDojo misconfigurations — it silently produces a fresh, disconnected Test every night instead of one Test with real history, which quietly breaks reimport-driven auto-close and SLA aging without ever throwing an error.
For a tool with no dedicated parser — an internal script, a niche scanner nobody's written support for yet — DefectDojo ships a Generic Findings Import format: a defined JSON (or CSV) schema with fields like title, severity, description, cwe, and file_path, which any script can emit and hand to the same import endpoint using scan_type=Generic Findings Import. And any SARIF-emitting tool — CodeQL and most modern SAST tools among them — has a direct escape hatch, since scan_type=SARIF is a generic parser DefectDojo maintains against the SARIF spec itself rather than against any one vendor's output quirks. Parser names shift and multiply between DefectDojo releases as new tools are added and old ones renamed, so treat any specific scan_type string as illustrative and confirm the current name against your installed version's Import Scan form or the /api/v2/doc/ schema before wiring a pipeline to it.
| Tool covered elsewhere in this course | Typical scan_type |
|---|---|
| Semgrep | "Semgrep JSON Report" |
| SonarQube | "SonarQube Scan" |
| gitleaks / TruffleHog | "Gitleaks Scan" / "Trufflehog Scan" |
| OWASP Dependency-Check | "Dependency Check Scan" |
| OWASP ZAP | "ZAP Scan" |
| Trivy / Grype | "Trivy Scan" / "Anchore Grype" |
| Checkov / tfsec | "Checkov Scan" / "Tfsec Scan" |
| OpenSCAP | an OpenSCAP-family parser, or generic ARF/XCCDF import |
| Anything SARIF-emitting | "SARIF" — the generic escape hatch |
Because parsing and deduplication run asynchronously on a Celery worker, a successful 201 Created response from import-scan means the job was queued, not that the findings have finished landing in PostgreSQL — under real import volume, or with a worker pool sized too small for the queue depth, there can be a real, non-trivial gap between "the API accepted the file" and "the findings are queryable." A pipeline that immediately queries the finding count right after the import call, expecting it to already reflect the new scan, is checking before the work is actually done.
Deduplication logic across tools
☺ Like you're 10: Two different inspectors can write up the exact same crack in the wall using different words — dedup is teaching the filing system to recognize it's the same crack either way.
DefectDojo's deduplication engine decides whether a newly-imported finding is genuinely new or a repeat of something already on file, and the algorithm used is configurable per scan type — a setting a team can override in System Settings, though the shipped defaults per parser are what most installs run unmodified:
hash_code— a hash computed from a configurable set of fields, commonly title, CWE, description, and file path or endpoint. The default for most SAST- and DAST-style parsers, where the tool itself supplies no single natural external identifier to key off.unique_id_from_tool— keys off an identifier the scanning tool itself supplies, such as a CVE plus a package coordinate, or a rule ID plus a stable location. More precise than a computed hash whenever the source tool actually provides one — the usual default for most SCA and container-scan parsers.unique_id_from_tool_or_hash_code— tries the tool's own ID first and falls back to the computed hash, a pragmatic middle ground for tools that only sometimes supply a stable ID.
Scope matters as much as algorithm: by default, dedup compares findings within one Engagement — a Trivy Test and an OWASP Dependency-Check Test scanning the same image, in the same build, get compared against each other. A "deduplicate across engagements" option, set at the Product level, extends that comparison across time — matching today's build against a finding still open from last week's — which is what actually makes SLA aging and mean-time-to-remediate mean anything; without it, every new build would silently spawn a fresh copy of every still-unfixed finding instead of recognizing it as the same one, still open. When two findings are judged identical, the older one stays Active and the newer one is automatically filed as Duplicate under it — one ticket, not two, for one bug.
If a scan type's hash_code configuration includes something as volatile as an exact line number, an unrelated formatting change or an added import two lines above a finding shifts every line number below it — and every one of those findings reimports as "new," silently discarding every prior False Positive and Risk Accepted marking that had already been decided on it. Favor dedup keys built from stable identifiers — a CWE, a rule ID, a package coordinate, a normalized path — over anything that shifts when unrelated code moves.
Day-to-day operations
☺ Like you're 10: Spin it up, make an account, hand it a report, and check what it decided — a handful of commands cover almost all of it.
# evaluate locally — the official quick-start
$ git clone https://github.com/DefectDojo/django-DefectDojo && cd django-DefectDojo
$ docker-compose up -d
$ docker-compose logs initializer | grep "Admin password:" # generated on first boot
# Django management commands, run inside the uwsgi container
$ docker-compose exec uwsgi ./manage.py createsuperuser
$ docker-compose exec uwsgi ./manage.py migrate # after an upgrade
$ docker-compose exec uwsgi ./manage.py dbshell # a raw psql prompt, for real trouble
# generate a personal API token from the UI (or the API itself) before scripting anything
# then confirm the API is reachable and see exactly what scan_type strings your version ships
$ curl -s -H "Authorization: Token $DD_API_TOKEN" \
https://defectdojo.internal.example.com/api/v2/test_types/ | jq '.results[].name' | head
# create a Product via the API, so a pipeline's first-ever import has somewhere to land
$ curl -s -X POST "https://defectdojo.internal.example.com/api/v2/products/" \
-H "Authorization: Token $DD_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"checkout-service","description":"Checkout API","prod_type":1}'
# a Risk Acceptance always needs an expiry — an undated one is just a finding nobody revisits
$ curl -s -X POST "https://defectdojo.internal.example.com/api/v2/risk_acceptance/" \
-H "Authorization: Token $DD_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Accepted: bastion-only exposure","expiration_date":"2026-11-01","accepted_findings":[4821]}'Most day-to-day interaction after initial setup is the import/reimport calls shown earlier, wired into whatever CI system already runs the scanners — a step at the end of the SAST, SCA, DAST, or IaC stage that curls or POSTs the just-produced report file straight to DefectDojo before the pipeline continues. Jira, Slack, Microsoft Teams, and email integrations are configured once in System Settings and then fire automatically off severity or SLA thresholds — worth setting up early, since a finding nobody's notified about is functionally the same as a finding nobody triaged.
Gotchas and failure modes
☺ Like you're 10: Most surprises trace back to one of three things — the worker queue got backed up, the wrong endpoint got called, or a setting that looked fine for one product turned out to be wrong for a different one.
- Worker pool sized for the wrong volume. A single Celery worker handling a burst of large imports across many products at once queues behind itself; findings that should be visible in seconds can lag by minutes with no error surfaced anywhere. Scale the worker replica count with actual import volume, not with the web tier's traffic — they're independent axes.
import-scancalled on every run instead ofreimport-scan. Creates a fresh, disconnected Test each time instead of one Test with real history — silently breaks auto-close on fix, breaks SLA aging, and produces "new" findings that were never actually new. This is the single most common DefectDojo misconfiguration in a CI pipeline.- Product-per-repo sprawl with no Product Type discipline. A hundred repositories each becoming their own Product with no consistent Product Type grouping makes cross-product reporting — "which business unit has the worst MTTR this quarter" — effectively impossible to answer later. Decide the Product Type taxonomy before onboarding services at scale, not after.
- Risk Acceptances with no expiry. An acceptance record with no review date functions exactly like a finding nobody ever revisits — the underlying risk is real and permanently suppressed. Every Risk Acceptance should carry an expiration date that forces a re-review.
- Role and permission sprawl. DefectDojo's permission model layers global roles (Superuser, Staff-level roles) on top of per-Product-Type and per-Product roles (Reader, Writer, Maintainer, Owner, and a scan-only API-importer role); granting broad global roles by default because scoping per-product felt like extra setup is a common shortcut that becomes a real access-control problem once the instance holds findings across every team in the org.
- Upgrades carry real Django migrations. A version upgrade is a schema migration against a database that may hold years of finding history, not a stateless container swap — read the release notes for breaking changes and back up the database before every upgrade, the same discipline any Django app with a production database demands.
A "7-day" Critical SLA configured without adjusting for business-day calculation counts weekends and holidays the same as any other day — a Critical finding filed at 5pm on a Friday can breach its SLA by Monday morning purely on the calendar, before anyone with the ability to fix it was even back online. Decide deliberately whether a Product's SLA configuration should count calendar days or business days, rather than inheriting whichever default shipped with the instance.
DefectDojo vs. Dependency-Track, GitLab, and a commercial ASPM
☺ Like you're 10: Every alternative trades DefectDojo's "ingest anything, from any tool" breadth for something narrower and, in exchange, usually smoother in that one specific lane.
| Tool | Scope | Strength | Trade-off |
|---|---|---|---|
| DefectDojo | General aggregation across SAST, DAST, SCA, secrets, IaC, and compliance — over a hundred parsers | One system of record for a genuinely multi-tool pipeline; free, self-hosted, no vendor lock-in on the data itself | You run and scale it yourself — Django, Postgres, Celery, Redis, upgrades, and all |
| OWASP Dependency-Track | SBOM-centric — ingests CycloneDX SBOMs specifically and continuously monitors them against vulnerability feeds | Purpose-built for the "what's in this build, and does that change tomorrow" question; lighter-weight than a general aggregator for SCA alone | Not a general SAST/DAST/IaC aggregation platform — it's one job done well, not DefectDojo's whole hierarchy |
| GitLab Vulnerability Management (Ultimate tier) | Built into GitLab's own CI, for scanners GitLab runs itself | Zero extra infrastructure if you're already all-in on GitLab CI; findings appear directly in the merge-request UI | Tied to GitLab's own scanner integrations — a tool outside that ecosystem, or a pipeline split across CI systems, doesn't feed in cleanly |
| ThreadFix / Faraday | Similar aggregation space — ThreadFix from Denim Group, Faraday oriented toward pentest workflow aggregation | Established alternatives with their own parser libraries and workflow assumptions | Smaller open-source communities and slower parser growth than DefectDojo's, in practice |
| Commercial ASPM platforms (e.g. application security posture management vendors) | Aggregation plus automated risk scoring, code-to-cloud correlation, and reachability analysis | Correlation and prioritization logic DefectDojo leaves to a team's own SLA configuration and manual triage | Licensing cost, and your finding data now lives in a vendor's cloud rather than infrastructure you fully control |
The practical pattern most teams land on: DefectDojo (or Dependency-Track, for SCA-only shops) as the free, self-hosted backbone that every scanner in the pipeline reports into, with a commercial ASPM layered on top only once a team has decided the extra correlation and reachability analysis is worth its licensing cost — the same "breadth from a free tool, depth from a paid one where it earns its keep" trade this course keeps returning to across its tool pages. See Vulnerability Management & Triage for the full triage workflow built on top of whichever platform a team chooses, and The Tool Landscape for how DefectDojo sits among every other tool this course covers.
Nutty: Queued the Trivy import twenty minutes ago and the finding count still hasn't moved. Something's stuck.
Recon the Robot: Check the worker pool before you assume it's broken. A 201 from the API only means the job was accepted — it doesn't mean a Celery worker has actually picked it up yet.
Nutty: ...one worker replica, six products importing on the same schedule. That would do it.
Benny the Beaver: While you're fixing that — can I just point my nightly pipeline at import-scan every run? Simpler than tracking a test ID.
Timmy: Simpler today, and it quietly breaks every rescan after it. A fresh Test every night means no auto-close when you fix something, and no memory of what was already marked False Positive. Use reimport-scan and keep the same test ID.
Pip the Hummingbird: And while everyone's in there — does the SBOM importer even exist for us, or are we hand-writing Generic Findings Import JSON for the tools nobody's built a parser for yet?
Nutty: Checked the parser list this morning — CycloneDX and SPDX both have real support now. Generic import is the fallback, not the default. I keep the parser catalogue current for exactly this reason.
1. What does DefectDojo actually detect on its own, and what does that imply about where it sits relative to every other tool in this course? 2. Name the five levels of DefectDojo's data hierarchy, and what an Endpoint is for. 3. What's the practical difference between the import-scan and reimport-scan API endpoints, and what silently breaks if a pipeline always calls the former? 4. Name the three deduplication algorithms and when each is the better default. 5. Give one concrete reason a team might run Dependency-Track alongside DefectDojo rather than relying on DefectDojo alone.
Check your answers
- Nothing — it has no scanner, rule engine, or vulnerability feed of its own. It's a normalization, deduplication, and triage layer that sits downstream of every scanner in the pipeline, which is why it's the one tool page in this course with no scan target of its own.
- Product Type, Product, Engagement, Test, and Finding, from broadest to most specific. An Endpoint is a URL, host, or host-plus-port that a DAST or API-security finding attaches to alongside its Test, so a finding can be tracked against the specific reachable location it applies to, not just the Engagement it was found in.
import-scancreates a brand-new Test from scratch every time it's called.reimport-scanupdates an existing Test, preserving its history — which is what lets DefectDojo auto-close a finding once a rescan stops reproducing it and lets a triage marking like False Positive survive into the next scan. Always callingimport-scancreates a disconnected Test every run, silently breaking auto-close, SLA aging, and finding history.hash_code(a computed hash from fields like title, CWE, and location — the default when a tool supplies no natural external ID, typical for SAST/DAST),unique_id_from_tool(keyed off an identifier the scanner itself supplies — more precise when available, typical for SCA/container scans), andunique_id_from_tool_or_hash_code(tries the tool's ID first, falls back to the hash).- Any reasonable answer citing Dependency-Track's SBOM-specific focus: for example, continuous monitoring of a CycloneDX SBOM against newly-published vulnerabilities as they're disclosed, which is a narrower and often faster-moving workflow than treating SCA as just one more scan type feeding DefectDojo's general-purpose import pipeline.