#!/usr/bin/env python3 """Vulnerability dashboard builder using DefectDojo API. Queries DefectDojo REST API v2 for findings, products, and engagements to build vulnerability management dashboards and metrics. """ import json import datetime import os import collections try: import requests HAS_REQUESTS = True except ImportError: HAS_REQUESTS = False class DefectDojoClient: """Client for DefectDojo REST API v2.""" def __init__(self, url=None, api_key=None): self.url = (url or os.environ.get("DEFECTDOJO_URL", "http://localhost:8080")).rstrip("/") self.api_key = api_key or os.environ.get("DEFECTDOJO_API_KEY", "") self.headers = { "Authorization": "Token " + self.api_key, "Content-Type": "application/json", } def _get(self, endpoint, params=None): if not HAS_REQUESTS or not self.api_key: return {"error": "requests not available or no API key"} try: resp = requests.get( self.url + "/api/v2/" + endpoint, headers=self.headers, params=params, timeout=15 ) if resp.status_code == 200: return resp.json() return {"error": "HTTP {}".format(resp.status_code)} except Exception as e: return {"error": str(e)} def get_findings(self, severity=None, active=True, limit=100): params = {"active": active, "limit": limit} if severity: params["severity"] = severity return self._get("findings/", params) def get_products(self, limit=100): return self._get("products/", {"limit": limit}) def get_engagements(self, product_id=None, limit=100): params = {"limit": limit} if product_id: params["product"] = product_id return self._get("engagements/", params) def get_finding_count_by_severity(self): result = {} for sev in ["Critical", "High", "Medium", "Low", "Info"]: data = self._get("findings/", {"severity": sev, "active": True, "limit": 1}) if isinstance(data, dict) and "count" in data: result[sev] = data["count"] return result def import_scan(self, product_id, engagement_id, scan_type, file_path): if not HAS_REQUESTS or not self.api_key: return {"error": "requests not available or no API key"} try: with open(file_path, "rb") as f: resp = requests.post( self.url + "/api/v2/import-scan/", headers={"Authorization": "Token " + self.api_key}, data={ "product": product_id, "engagement": engagement_id, "scan_type": scan_type, "active": True, "verified": False, }, files={"file": f}, timeout=60, ) if resp.status_code in (200, 201): return resp.json() return {"error": "HTTP {}".format(resp.status_code)} except Exception as e: return {"error": str(e)} def build_dashboard_data(findings): """Build dashboard metrics from findings list.""" if not isinstance(findings, dict) or "results" not in findings: return {"error": "Invalid findings data"} results = findings["results"] severity_counts = collections.Counter() product_counts = collections.Counter() age_sum = 0 overdue_count = 0 now = datetime.datetime.now(datetime.timezone.utc) for f in results: severity_counts[f.get("severity", "Unknown")] += 1 product_counts[f.get("test", {}).get("engagement", {}).get("product", {}).get("name", "Unknown")] += 1 if f.get("date"): try: created = datetime.datetime.fromisoformat(f["date"]) if created.tzinfo is None: created = created.replace(tzinfo=datetime.timezone.utc) age = (now - created).days age_sum += age sla = {"Critical": 7, "High": 30, "Medium": 90, "Low": 180}.get(f.get("severity", ""), 999) if age > sla: overdue_count += 1 except ValueError: pass total = len(results) return { "total_active_findings": total, "by_severity": dict(severity_counts), "by_product": dict(product_counts.most_common(10)), "avg_age_days": round(age_sum / max(total, 1), 1), "overdue_count": overdue_count, "sla_compliance_pct": round((total - overdue_count) / max(total, 1) * 100, 1), } SUPPORTED_SCAN_TYPES = [ "Nessus Scan", "Qualys Scan", "Burp REST API", "ZAP Scan", "Trivy Scan", "Snyk Scan", "Semgrep JSON Report", "SARIF", "Generic Findings Import", "Anchore Grype", "Nuclei Scan", "Checkov Scan", ] if __name__ == "__main__": print("=" * 60) print("Vulnerability Dashboard with DefectDojo") print("REST API v2 queries, severity metrics, SLA tracking") print("=" * 60) print(" requests available: {}".format(HAS_REQUESTS)) client = DefectDojoClient() print("\n--- Supported Scan Types ---") for st in SUPPORTED_SCAN_TYPES: print(" - {}".format(st)) print("\n--- API Endpoints ---") endpoints = [ ("GET", "/api/v2/findings/", "List findings"), ("GET", "/api/v2/products/", "List products"), ("GET", "/api/v2/engagements/", "List engagements"), ("POST", "/api/v2/import-scan/", "Import scan results"), ("POST", "/api/v2/reimport-scan/", "Re-import scan results"), ] for method, path, desc in endpoints: print(" {} {:30s} {}".format(method, path, desc)) demo_findings = { "count": 5, "results": [ {"severity": "Critical", "title": "SQL Injection", "date": "2025-01-10", "test": {"engagement": {"product": {"name": "WebApp"}}}}, {"severity": "High", "title": "XSS", "date": "2025-01-15", "test": {"engagement": {"product": {"name": "WebApp"}}}}, {"severity": "Medium", "title": "Missing Headers", "date": "2024-12-01", "test": {"engagement": {"product": {"name": "API"}}}}, {"severity": "Low", "title": "Cookie flag", "date": "2025-02-01", "test": {"engagement": {"product": {"name": "API"}}}}, {"severity": "Critical", "title": "RCE", "date": "2025-02-20", "test": {"engagement": {"product": {"name": "WebApp"}}}}, ], } dashboard = build_dashboard_data(demo_findings) print("\n--- Dashboard ---") for k, v in dashboard.items(): print(" {}: {}".format(k, v)) print("\n" + json.dumps({"findings_analyzed": demo_findings["count"]}, indent=2))