Reporting

Otto Chriek’s compliance reporting requirements are, to use his own description, “thorough.” Mr. Bent’s quarterly review requirements are, to use a different word, “exhausting.” Both require consistent, accurate, and reproducible vulnerability metrics. DefectDojo provides the underlying data; this runbook describes how to extract it, format it, and deliver it in the forms each audience requires. It covers built-in report generation, API-based custom reports, the Royal Bank trend report, the monthly compliance PDF, and the Grafana dashboard for real-time visibility.

Built-in reports

DefectDojo’s built-in reporting is accessed via Reports in the navigation. The most useful standard reports are:

The Executive Summary report provides finding counts by severity over time, as a trend chart. It covers all products or a selected subset. Generate it for the past 90 days to show the trend to management:

  1. Navigate to Reports, Executive Summary

  2. Set the date range and select “All Products”

  3. Click “Generate Report”

  4. Export as PDF via the “Export PDF” button

The Product-Level Drill-Down report shows findings per product, broken down by severity, open/closed status, and SLA compliance rate. Generate this for each product individually for the monthly review.

Exporting reports via the API

For automated report generation (scheduled by cron), use the DefectDojo API. The reports API generates a PDF asynchronously:

curl -X POST "https://defectdojo.golemtrust.am/api/v2/reports/" \
  -H "Authorization: Token ${DEFECTDOJO_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Monthly Executive Summary",
    "type": "executive",
    "include_finding_notes": true,
    "include_finding_images": false,
    "options": {
      "include_table_of_contents": true
    }
  }'

Poll the returned id until the status is ready, then download:

curl "https://defectdojo.golemtrust.am/api/v2/reports/${REPORT_ID}/" \
  -H "Authorization: Token ${DEFECTDOJO_API_TOKEN}" \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['file'])"

Otto Chriek’s monthly compliance PDF

Otto’s monthly report is generated automatically on the first day of each month by a cron job. The report covers:

  • Total active findings by severity (snapshot at end of reporting period)

  • New findings opened in the period

  • Findings closed in the period (remediated)

  • SLA breach rate (percentage of findings that exceeded their SLA)

  • Number of active risk acceptances and their expiry dates

  • Any finding with no assigned owner

The cron job script:

#!/usr/bin/env python3
import requests
import datetime
import json

DEFECTDOJO_URL = "https://defectdojo.golemtrust.am"
TOKEN = open("/etc/defectdojo-report-token").read().strip()
PERIOD_END = datetime.date.today() - datetime.timedelta(days=1)
PERIOD_START = PERIOD_END.replace(day=1) - datetime.timedelta(days=1)
PERIOD_START = PERIOD_START.replace(day=1)

headers = {"Authorization": f"Token {TOKEN}"}

def get_count(params):
    r = requests.get(f"{DEFECTDOJO_URL}/api/v2/findings/", headers=headers, params={**params, "limit": 1})
    return r.json()["count"]

summary = {
    "period": f"{PERIOD_START} to {PERIOD_END}",
    "active_critical": get_count({"active": True, "severity": "Critical"}),
    "active_high": get_count({"active": True, "severity": "High"}),
    "active_medium": get_count({"active": True, "severity": "Medium"}),
    "active_low": get_count({"active": True, "severity": "Low"}),
    "opened_in_period": get_count({"date_gte": str(PERIOD_START), "date_lte": str(PERIOD_END)}),
    "closed_in_period": get_count({"mitigated_date_gte": str(PERIOD_START), "mitigated_date_lte": str(PERIOD_END), "is_Mitigated": True}),
    "sla_breaches": get_count({"outside_sla": True}),
    "risk_acceptances": get_count({"risk_accepted": True, "active": True}),
}

with open(f"/reports/monthly-compliance-{PERIOD_START:%Y%m}.json", "w") as f:
    json.dump(summary, f, indent=2)

print("Compliance data collected. Generate PDF from DefectDojo UI or template.")

Otto then uses the JSON data to populate the report template in the golem-trust/compliance-reports GitLab repository, which renders to a PDF via pandoc. The completed PDF is emailed to Adora Belle and retained in the Hetzner Object Storage bucket golemtrust-compliance-reports.

Mr. Bent’s quarterly Royal Bank trend report

Mr. Bent requires a trend report showing the vulnerability count in the Royal-Bank-Integration product over time, demonstrating that the number is decreasing (or, at minimum, that the Critical and High counts are decreasing). The report covers the 90-day period preceding the review.

Custom Python script for the Royal Bank trend report:

#!/usr/bin/env python3
import requests
import datetime
import csv
import io

DEFECTDOJO_URL = "https://defectdojo.golemtrust.am"
TOKEN = open("/etc/defectdojo-report-token").read().strip()
headers = {"Authorization": f"Token {TOKEN}"}

end_date = datetime.date.today()
start_date = end_date - datetime.timedelta(days=90)

response = requests.get(
    f"{DEFECTDOJO_URL}/api/v2/findings/",
    headers=headers,
    params={
        "product_name": "Royal-Bank-Integration",
        "date_gte": str(start_date),
        "date_lte": str(end_date),
        "limit": 1000,
    }
)
findings = response.json()["results"]

output = io.StringIO()
writer = csv.writer(output)
writer.writerow(["ID", "Title", "Severity", "Date", "Status", "SLA Deadline", "Mitigated Date"])
for f in findings:
    writer.writerow([
        f["id"],
        f["title"],
        f["severity"],
        f["date"],
        "Mitigated" if f["is_Mitigated"] else "Active",
        f.get("sla_expiration_date", ""),
        f.get("mitigated", ""),
    ])

with open(f"/reports/royal-bank-90day-{end_date:%Y%m%d}.csv", "w") as report_file:
    report_file.write(output.getvalue())

print(f"Royal Bank report: {len(findings)} findings")

This CSV is formatted as a table and emailed to the Royal Bank liaison. The email is sent via the Golem Trust mail relay using a Python smtplib call, with Otto Chriek and Cheery CC’d. The content of the email must not include raw finding details for vulnerabilities that are not yet patched; only finding counts and SLA compliance rates are shared until patching is complete.

Grafana dashboard for real-time metrics

The Grafana dashboard “Security Vulnerability Metrics” polls the DefectDojo API every 5 minutes via the Prometheus exporter (see the SLA setup runbook) and displays:

  • Total active findings by severity (gauge panels)

  • New findings per day over the past 30 days (time series)

  • Findings opened vs. closed per week (bar chart)

  • SLA compliance rate by severity (stat panels, with red threshold at 95%)

  • Top 10 affected products by open finding count (table)

The dashboard is provisioned from the golem-trust/grafana-dashboards GitLab repository. Cheery, Angua, Carrot, and Ponder all have read access. Adora Belle reviews the dashboard in her weekly leadership review.

CSV export for ad-hoc queries

For any query not covered by the above, the DefectDojo API supports flexible filtering. A common ad-hoc query is all active High and Critical findings with no assigned owner:

curl "https://defectdojo.golemtrust.am/api/v2/findings/?active=true&severity=Critical&assigned_to=null" \
  -H "Authorization: Token ${DEFECTDOJO_API_TOKEN}" \
  | python3 -m json.tool

Cheery runs this query as part of her Monday morning review. Any finding with no owner for more than 24 hours is manually assigned and the assignment logic reviewed to prevent recurrence.