Skip to content

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 -> test trivy-fs.
  • Gitleaks JSON -> Gitleaks Scan -> test gitleaks.

What is opt-in:

  • Kubeconform: set DD_IMPORT_KUBECONFORM=1 to render a per-overlay kubeconform JSON report and import it as Generic Findings Import -> test kubeconform. 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 send product_type_name=Homelab and auto_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, engagement ci-main / ci-dev, tests trivy-fs and gitleaks.

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 the Homelab product 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):

DD_STRICT=1 make defectdojo-import

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