Skip to content

Architecture

draventis is a small Python CLI plus a Helm chart. The CLI is the orchestrator + uploader; the scanning is done by external binaries (ZAP, Nuclei) that live in the container image.

Data flow

targets.yaml ──► config.load_config ──► Config (model.py)
                                          │
                 for each selected target │
                                          ▼
   zap.run_scan ──► zap.sh -autorun <plan> ──► /zap/wrk/zap-*.xml ─┐
   nuclei.run_scan ──► nuclei ...        ──► /zap/wrk/nuclei-*.jsonl ─┤
                                                                      ▼
                                          defectdojo.reimport ──► POST /api/v2/reimport-scan/

Per-target failures are isolated: one target failing (or one upload failing) does not stop the rest.

Modules (src/draventis/)

Module Responsibility Runtime deps
model.py Typed config dataclasses: Target, AuthProfile, DefectDojoConfig, Config, and the ScanPolicy / Schedule enums. stdlib only
config.py Load & validate targets.yaml (or JSON) into a Config; apply defaults; reject dupes/missing fields. PyYAML (lazy)
zap.py Pick the AF plan for a policy, export ${DRAVENTIS_*} env, build & run zap.sh, return the report path. stdlib only
nuclei.py Build & run the nuclei command (JSONL output; -dast for non-baseline). stdlib only
defectdojo.py reimport-scan client: build multipart, POST via urllib, failure-isolated. stdlib only
__main__.py The draventis run CLI: parse args, select targets, orchestrate scan + upload. None

Design properties

  • The DefectDojo client is stdlib urllib and never raises. reimport returns a ReimportResult(ok=…); the orchestrator logs and continues. A flaky DefectDojo never fails a scan.
  • Subprocess calls are injectable. zap.run_scan / nuclei.run_scan take a runner callable (default subprocess.run), so command/env construction is unit-tested without invoking the real binaries. --dry-run builds everything and executes nothing.
  • Secrets never touch config or code paths that log. Credentials are read from the environment by name (auth.user_env etc.); draventis passes the env through to ZAP but does not read or print the values.
  • Policy → plan → report is a single mapping in zap.py (_PLAN_BY_POLICY), so adding a policy is one table entry + one plan file.

The CLI

draventis run [--all | --target NAME] [--schedule nightly|weekly]
             [--config PATH] [--plans-dir DIR] [--workdir DIR]
             [--no-nuclei] [--dry-run]

Exit codes: 0 all selected targets attempted OK, 1 at least one scan failed to produce a report, 2 bad arguments or config.

The CronJobs run draventis run --schedule <name>, so each schedule scans only its own targets.

Automation plans (automation/)

One ZAP Automation Framework plan per policy. Each ends in a report job that writes the traditional XML DefectDojo's ZAP Scan parser ingests. The target URL (and, for authenticated plans, the login URL and credentials) is substituted from the environment at plan-load time (${DRAVENTIS_TARGET_URL}, ${ZAP_USER}, …).

The chart (charts/draventis/)

Renders targets.yaml into a ConfigMap, provisions the secret backend (plain Secret or ExternalSecret), and creates a CronJob per enabled schedule. See Deployment. The chart-rendered targets.yaml is the exact shape config.load_config parses. The two are kept in sync.

Testing

tests/ mirrors the modules: config parsing & validation, the DefectDojo multipart builder + failure isolation, ZAP/Nuclei command construction, and the end-to-end CLI in --dry-run. Run uv run pytest -q.