DefectDojo Findings Ingestion¶
DefectDojo on the local cluster (https://defectdojo.mutana.fr) is the
central vulnerability and security-findings intake for the homelab. This page
describes every ingestion path, what is automated, what is manual, and how to
troubleshoot it.
Related: DefectDojo service page, DefectDojo runbook, SecOps Workflow.
Architecture¶
[Forgejo CI (k8s repo)] [Homelab host (Docker)]
| make secops-*-scan | Nessus scanner
| -> .secops-reports/{trivy,gitleaks}/*.json | -> .nessus XML v2
v v
make defectdojo-import nessus_to_defectdojo.sh
| dd_import_ci.sh -> dd_import.py | dd_import.py
`------> POST https://defectdojo.mutana.fr/api/v2/reimport-scan/ ------+
v
[DefectDojo on local cluster]
(django + celery + postgres + valkey)
The API route https://defectdojo.mutana.fr/api/v2/... bypasses the Authelia
forward-auth middleware (high-priority /api IngressRoute route). Automation
authenticates with a DefectDojo v2 API token: Authorization: Token <key>.
Taxonomy¶
Stable product / engagement / test names keep reimport idempotent. The same names are used by every ingestion path.
| Product | Engagement | Tests | Source |
|---|---|---|---|
homelab-k8s |
ci-main (or ci-dev on the dev branch) |
trivy-fs, gitleaks, kubeconform |
k8s repo CI |
nessus-homelab |
nessus-scheduled |
nessus-scan |
Nessus (homelab Docker) |
homelab-ansible |
ci-main (or ci-dev) |
trivy-fs, gitleaks |
Ansible repo CI (separate repo; out of scope here) |
homelab-infra |
scheduled-scans |
(reserved) | future infrastructure scans |
scan_type strings must match DefectDojo parser names exactly:
| Tool | scan_type |
File format |
|---|---|---|
| Trivy | Trivy Scan |
Trivy native JSON (-f json) |
| Gitleaks | Gitleaks Scan |
Gitleaks JSON (-r report.json) |
| Kubeconform | Generic Findings Import |
JSON wrapped in the Generic schema |
| Nessus | Nessus Scan |
Nessus .nessus XML v2 export |
Required secrets / variables¶
In-cluster (DefectDojo runtime)¶
Created from defectdojo/overlays/local/.defectdojo-*.env via Kustomize
secretGenerator. See the service page and the .example files. Never commit
real values.
CI (Forgejo repo/org secrets)¶
| Secret name | Purpose | Example |
|---|---|---|
DEFECTDOJO_API_URL |
DefectDojo base URL | https://defectdojo.mutana.fr |
DEFECTDOJO_API_TOKEN |
v2 API token (mapped to DD_API_TOKEN in the workflow) |
(generate in DefectDojo UI) |
The Forgejo workflow maps these to DD_API_URL / DD_API_TOKEN for the
make defectdojo-import step. If either is unset, the upload step no-ops.
The shared API client sends a browser-compatible automation User-Agent because
Cloudflare Browser Integrity Check rejects Python urllib's default signature
with error 1010. Set DD_USER_AGENT only if the Cloudflare policy expects a
different value.
Nessus importer (homelab host env)¶
| Variable | Purpose |
|---|---|
NESSUS_HOST |
Nessus base URL on the home network |
NESSUS_ACCESS_KEY / NESSUS_SECRET_KEY |
Nessus API keys |
DD_API_URL / DD_API_TOKEN |
DefectDojo API (same token as CI) |
CI ingestion (Trivy / Gitleaks / Kubeconform)¶
The Forgejo workflow .forgejo/workflows/validate.yaml runs the SecOps scans,
then runs a make defectdojo-import step that is continue-on-error: true
(non-blocking). The Makefile target calls defectdojo/scripts/dd_import_ci.sh,
which reimports each available report via defectdojo/scripts/dd_import.py.
The upload runs only for pushes to main or dev; pull-request validation does
not reimport branch findings into either shared engagement. Import failures mark
the non-blocking step as failed so they remain visible in the workflow result.
What is automated:
- Trivy JSON ->
Trivy Scan-> testtrivy-fs. - Gitleaks JSON ->
Gitleaks Scan-> testgitleaks.
What is opt-in:
- Kubeconform: set
DD_IMPORT_KUBECONFORM=1to render a per-overlay kubeconform JSON report and import it asGeneric Findings Import-> testkubeconform. Off by default until the generic schema is reviewed.
What is manual:
- Creating the API token in DefectDojo (Profile -> API v2 Key -> Generate).
- Adding the two Forgejo secrets.
- Optionally seeding the products/engagements once with
dd_bootstrap.py. Name-based reimports sendproduct_type_name=Homelabandauto_create_context=true, so the first push can create the hierarchy when the API user has permission.
Local test:
# Produce reports locally
make secops-trivy-scan TRIVY_REPORT_DIR=.secops-reports/trivy
make secops-secret-scan GITLEAKS_REPORT_PATH=.secops-reports/gitleaks/gitleaks.json
# Import into DefectDojo
export DD_API_URL=https://defectdojo.mutana.fr
export DD_API_TOKEN=<your v2 token>
make defectdojo-import SECOPS_REPORT_DIR=.secops-reports
Import a single report by hand:
python3 defectdojo/scripts/dd_import.py \
--file .secops-reports/trivy/trivy-fs.json \
--scan-type "Trivy Scan" \
--product homelab-k8s --engagement ci-main --test trivy-fs \
--minimum-severity Medium --close-old-findings
Nessus ingestion¶
The Nessus scanner runs in Docker on the homelab. DefectDojo is on the remote
local cluster. The script runs on the homelab side and pushes to the
public DefectDojo API — this is the natural direction because the homelab can
reach the internet, while the cluster cannot reach the home network without a
tunnel.
Connectivity required:
- Outbound HTTPS from the homelab host to
https://defectdojo.mutana.fr. - No inbound ports and no cluster -> homelab path.
- No NAT/firewall changes on the cluster side; ensure the homelab host can
resolve and reach
defectdojo.mutana.fr(443).
Run once manually:
export NESSUS_HOST=https://nessus.home.mutana.fr:8834
export NESSUS_ACCESS_KEY=...
export NESSUS_SECRET_KEY=...
export DD_API_URL=https://defectdojo.mutana.fr
export DD_API_TOKEN=...
bash defectdojo/scripts/nessus_to_defectdojo.sh
Schedule it on the homelab host (systemd timer example, not committed here):
# /etc/systemd/system/nessus-to-defectdojo.service
[Service]
Type=oneshot
EnvironmentFile=/etc/nessus-to-defectdojo.env
ExecStart=/home/kardudu/k8s/defectdojo/scripts/nessus_to_defectdojo.sh
# /etc/systemd/system/nessus-to-defectdojo.timer
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
The script exports the latest scan (or NESSUS_SCAN_NAME) in Nessus .nessus
XML v2 format, polls the export until ready, downloads it, and reimports it as
Nessus Scan into nessus-homelab / nessus-scheduled / nessus-scan.
Ansible repo ingestion¶
The Ansible project is a separate repository (/home/kardudu/ansible) and is
not scanned by this repo's workflow. To send its Trivy/Gitleaks findings to the
same DefectDojo, copy or vendor defectdojo/scripts/dd_import.py and
dd_api.py into the Ansible repo (or submodule-reference them) and add a
Forgejo step that posts its reports to:
- product
homelab-ansible, engagementci-main/ci-dev, teststrivy-fsandgitleaks.
This is intentionally left to the Ansible repo's own CI to avoid a cross-repo
filesystem dependency (recorded decision in docs/secops.md "Ansible Boundary").
Deduplication and avoiding duplicates¶
- Every path uses
/api/v2/reimport-scan/, which reuses the latest matching Test in the engagement and updates findings in place. - Name-based imports send
auto_create_context=true; the first run creates theHomelabproduct type, Product, Engagement, and Test when needed, and subsequent runs update the matching Test. --close-old-findings(default in CI) closes findings absent from the new report so the test reflects current state.- For cross-test dedup, configure deduplication scope on the product in DefectDojo (UI -> Product -> Settings) after the first import.
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
| CI upload step no-ops | DEFECTDOJO_API_URL / DEFECTDOJO_API_TOKEN not set in Forgejo |
Add the two secrets |
| Upload returns Cloudflare 403 / error 1010 | Cloudflare rejected the HTTP client signature | Confirm the updated importer is running; if the edge policy requires a specific signature, set DD_USER_AGENT, or exempt the token-authenticated /api/v2/ path from Browser Integrity Check |
| Upload returns 400 for missing Product or Engagement | API user cannot auto-create context, or the product type differs | Grant create permissions or pre-seed the hierarchy with dd_bootstrap.py; override DD_PRODUCT_TYPE if needed |
| Upload returns HTML 401 (Authelia page) | API route missing or lower priority than UI route | Verify IngressRoute /api route has priority: 100 and no authelia@file |
| Upload returns JSON 401 | Bad/expired token | Regenerate v2 token, update the Forgejo secret |
| Upload returns 400 scan_type | Wrong scan_type spelling |
Use exact strings from the table above |
| Findings not visible | Imported into wrong engagement / test | Check product/engagement names; reimport auto-creates them |
| Nessus download fails | Keys wrong / scan not finished | Check NESSUS_ACCESS_KEY/SECRET_KEY, run a scan first |
| Reimport keeps creating new tests | test_title differs between runs |
Use the stable test title (trivy-fs, etc.) |
Verbose local re-run (strict mode, fails on import error):
Manual API smoke test¶
# Auth + reachability
curl -sS -H "Authorization: Token ${DD_API_TOKEN}" \
https://defectdojo.mutana.fr/api/v2/userinfo/
# List scan types to confirm a parser name
curl -sS -H "Authorization: Token ${DD_API_TOKEN}" \
https://defectdojo.mutana.fr/api/v2/metadata/findings/ | python3 -m json.tool | head