Skip to content

Commit 454c569

Browse files
authored
Add report API formats (#15148)
* Add report API formats * polish-reports
1 parent 4f24d4c commit 454c569

8 files changed

Lines changed: 193 additions & 22 deletions

File tree

dojo/api_v2/serializers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -998,10 +998,22 @@ class Meta:
998998

999999

10001000
class ReportGenerateOptionSerializer(serializers.Serializer):
1001+
REPORT_TYPE_CHOICES = (
1002+
("JSON", "JSON"),
1003+
("HTML", "HTML"),
1004+
("CSV", "CSV"),
1005+
("Excel", "Excel"),
1006+
)
1007+
10011008
include_finding_notes = serializers.BooleanField(default=False)
10021009
include_finding_images = serializers.BooleanField(default=False)
10031010
include_executive_summary = serializers.BooleanField(default=False)
10041011
include_table_of_contents = serializers.BooleanField(default=False)
1012+
report_type = serializers.ChoiceField(
1013+
choices=REPORT_TYPE_CHOICES,
1014+
default="JSON",
1015+
help_text="Format for the generated report.",
1016+
)
10051017

10061018

10071019
class ExecutiveSummarySerializer(serializers.Serializer):

dojo/api_v2/views.py

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django.core.exceptions import ValidationError
99
from django.db import IntegrityError
1010
from django.db.models.query import QuerySet as DjangoQuerySet
11+
from django.shortcuts import render
1112
from django.utils import timezone
1213
from django_filters.rest_framework import DjangoFilterBackend
1314
from drf_spectacular.renderers import OpenApiJsonRenderer2
@@ -35,10 +36,12 @@
3536
from dojo.authorization import api_permissions as permissions
3637
from dojo.authorization.authorization import user_has_permission_or_403
3738
from dojo.endpoint.ui.views import get_endpoint_ids
39+
from dojo.engagement.queries import get_authorized_engagements
3840
from dojo.filters import (
3941
ApiAppAnalysisFilter,
4042
ApiDojoMetaFilter,
4143
)
44+
from dojo.finding.queries import get_authorized_findings
4245
from dojo.finding.ui.filters import (
4346
ReportFindingFilter,
4447
ReportFindingFilterWithoutObjectLookups,
@@ -69,6 +72,8 @@
6972
get_authorized_products,
7073
)
7174
from dojo.reports.ui.views import (
75+
CSVExportView,
76+
ExcelExportView,
7277
prefetch_related_findings_for_report,
7378
report_url_resolver,
7479
)
@@ -525,8 +530,8 @@ def perform_create(self, serializer):
525530
pghistory.context(test_id=test_id_from_response)
526531

527532

528-
from dojo.note_type.api.views import NoteTypeViewSet # noqa: E402, F401 -- re-export; urls.py imports by name
529-
from dojo.notes.api.views import NotesViewSet # noqa: E402, F401 -- re-export; urls.py imports by name
533+
from dojo.note_type.api.views import NoteTypeViewSet # noqa: E402, F401
534+
from dojo.notes.api.views import NotesViewSet # noqa: E402, F401
530535

531536

532537
def report_generate(request, obj, options):
@@ -537,6 +542,9 @@ def report_generate(request, obj, options):
537542
test = None
538543
endpoint = None
539544
endpoints = None
545+
report_context = {}
546+
report_template = None
547+
report_title = "Generate Report"
540548

541549
include_finding_notes = False
542550
include_finding_images = False
@@ -560,14 +568,16 @@ def report_generate(request, obj, options):
560568

561569
if type(obj).__name__ == "Product_Type":
562570
product_type = obj
571+
report_template = "dojo/product_type_pdf_report.html"
572+
report_title = labels.ORG_REPORT_LABEL
563573

564574
report_name = labels.ORG_REPORT_WITH_NAME_TITLE % {"name": str(product_type)}
565575

566576
findings = report_finding_filter_class(
567577
request.GET,
568578
prod_type=product_type,
569579
queryset=prefetch_related_findings_for_report(
570-
Finding.objects.filter(
580+
get_authorized_findings("view").filter(
571581
test__engagement__product__prod_type=product_type,
572582
),
573583
),
@@ -586,9 +596,26 @@ def report_generate(request, obj, options):
586596
months_between = (r.years * 12) + r.months
587597
# include current month
588598
months_between += 1
599+
report_context = {
600+
"products": get_authorized_products("view").filter(
601+
prod_type=product_type,
602+
engagement__test__finding__in=findings.qs,
603+
).distinct(),
604+
"engagements": get_authorized_engagements("view").filter(
605+
product__prod_type=product_type,
606+
test__finding__in=findings.qs,
607+
).distinct(),
608+
"tests": get_authorized_tests("view").filter(
609+
engagement__product__prod_type=product_type,
610+
finding__in=findings.qs,
611+
).distinct(),
612+
"months_between": months_between,
613+
}
589614

590615
elif type(obj).__name__ == "Product":
591616
product = obj
617+
report_template = "dojo/product_pdf_report.html"
618+
report_title = labels.ASSET_REPORT_LABEL
592619

593620
report_name = labels.ASSET_REPORT_WITH_NAME_TITLE % {"name": str(product)}
594621

@@ -606,6 +633,8 @@ def report_generate(request, obj, options):
606633

607634
elif type(obj).__name__ == "Engagement":
608635
engagement = obj
636+
report_template = "dojo/engagement_pdf_report.html"
637+
report_title = "Engagement Report"
609638
findings = report_finding_filter_class(
610639
request.GET,
611640
engagement=engagement,
@@ -623,6 +652,8 @@ def report_generate(request, obj, options):
623652

624653
elif type(obj).__name__ == "Test":
625654
test = obj
655+
report_template = "dojo/test_pdf_report.html"
656+
report_title = "Test Report"
626657
findings = report_finding_filter_class(
627658
request.GET,
628659
engagement=test.engagement,
@@ -634,6 +665,8 @@ def report_generate(request, obj, options):
634665

635666
elif type(obj).__name__ == "Endpoint":
636667
endpoint = obj
668+
report_template = "dojo/endpoint_pdf_report.html"
669+
report_title = "Endpoint Report"
637670
host = endpoint.host
638671
report_name = "Endpoint Report: " + host
639672
endpoints = Endpoint.objects.filter(
@@ -648,6 +681,8 @@ def report_generate(request, obj, options):
648681

649682
elif isinstance(obj, DjangoQuerySet):
650683
# Support any Django QuerySet (including Tagulous CastTaggedQuerySet)
684+
report_template = "dojo/finding_pdf_report.html"
685+
report_title = "Finding Report"
651686
findings = report_finding_filter_class(
652687
request.GET,
653688
queryset=prefetch_related_findings_for_report(obj).distinct(),
@@ -670,12 +705,20 @@ def report_generate(request, obj, options):
670705
"endpoint": endpoint,
671706
"endpoints": endpoints,
672707
"findings": findings.qs.order_by("numerical_severity"),
708+
"include_finding_notes": include_finding_notes,
709+
"include_finding_images": include_finding_images,
710+
"include_executive_summary": include_executive_summary,
673711
"include_table_of_contents": include_table_of_contents,
712+
"include_disclaimer": get_system_setting("disclaimer_reports_forced", 0),
713+
"disclaimer": get_system_setting("disclaimer_reports"),
674714
"user": user,
675715
"team_name": settings.TEAM_NAME,
676-
"title": "Generate Report",
716+
"title": report_title,
677717
"user_id": request.user.id,
678718
"host": report_url_resolver(request),
719+
"host_view": False,
720+
"context": report_context,
721+
"report_template": report_template,
679722
}
680723

681724
finding_notes = []
@@ -792,6 +835,43 @@ def report_generate(request, obj, options):
792835
return result
793836

794837

838+
def _report_findings_filename(obj, extension):
839+
object_id = getattr(obj, "id", None)
840+
if object_id is None:
841+
return f"findings.{extension}"
842+
843+
object_type = type(obj).__name__.lower()
844+
return f"{object_type}_{object_id}_findings.{extension}"
845+
846+
847+
def report_generate_response(request, obj, options):
848+
report_type = options.get("report_type", "JSON")
849+
850+
if report_type not in {"CSV", "Excel", "HTML", "JSON"}:
851+
msg = f"Unsupported report_type: {report_type}"
852+
raise ValidationError(msg)
853+
854+
data = report_generate(request, obj, options)
855+
856+
if report_type == "JSON":
857+
report = serializers.ReportGenerateSerializer(data)
858+
return Response(report.data)
859+
860+
if report_type == "HTML":
861+
return render(request, data["report_template"], data)
862+
863+
if report_type == "CSV":
864+
return CSVExportView().build_response(
865+
data["findings"],
866+
filename=_report_findings_filename(obj, "csv"),
867+
)
868+
869+
return ExcelExportView().build_response(
870+
data["findings"],
871+
filename=_report_findings_filename(obj, "xlsx"),
872+
)
873+
874+
795875
class CeleryViewSet(viewsets.ViewSet):
796876
permission_classes = (permissions.IsSuperUser, DjangoModelPermissions)
797877
queryset = System_Settings.objects.none()

dojo/engagement/api/views.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from dojo.api_v2 import serializers as api_v2_serializers
1313
from dojo.api_v2.prefetch.prefetcher import _Prefetcher
14-
from dojo.api_v2.views import DojoModelViewSet, PrefetchDojoModelViewSet, report_generate, schema_with_prefetch
14+
from dojo.api_v2.views import DojoModelViewSet, PrefetchDojoModelViewSet, report_generate_response, schema_with_prefetch
1515
from dojo.authorization import api_permissions as permissions
1616
from dojo.celery_dispatch import dojo_dispatch_task
1717
from dojo.engagement.api.filters import ApiEngagementFilter
@@ -131,14 +131,13 @@ def generate_report(self, request, pk=None):
131131
options[
132132
"include_table_of_contents"
133133
] = report_options.validated_data["include_table_of_contents"]
134+
options["report_type"] = report_options.validated_data["report_type"]
134135
else:
135136
return Response(
136137
report_options.errors, status=status.HTTP_400_BAD_REQUEST,
137138
)
138139

139-
data = report_generate(request, engagement, options)
140-
report = api_v2_serializers.ReportGenerateSerializer(data)
141-
return Response(report.data)
140+
return report_generate_response(request, engagement, options)
142141

143142
@extend_schema(
144143
methods=["GET"],

dojo/finding/api/views.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@
3333
from dojo.api_v2 import (
3434
serializers as api_v2_serializers,
3535
)
36-
from dojo.api_v2.views import DojoModelViewSet, get_request_boolean, report_generate
36+
from dojo.api_v2.views import (
37+
DojoModelViewSet,
38+
get_request_boolean,
39+
report_generate_response,
40+
)
3741
from dojo.authorization import api_permissions as permissions
3842
from dojo.finding.api.filters import ApiFindingFilter, ApiTemplateFindingFilter
3943
from dojo.finding.api.serializer import (
@@ -702,14 +706,13 @@ def generate_report(self, request):
702706
options[
703707
"include_table_of_contents"
704708
] = report_options.validated_data["include_table_of_contents"]
709+
options["report_type"] = report_options.validated_data["report_type"]
705710
else:
706711
return Response(
707712
report_options.errors, status=status.HTTP_400_BAD_REQUEST,
708713
)
709714

710-
data = report_generate(request, findings, options)
711-
report = api_v2_serializers.ReportGenerateSerializer(data)
712-
return Response(report.data)
715+
return report_generate_response(request, findings, options)
713716

714717
def _get_metadata(self, request, finding):
715718
metadata = DojoMeta.objects.filter(finding=finding)

dojo/product/api/views.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import dojo.api_v2.mixins as dojo_mixins
99
from dojo.api_v2 import prefetch
1010
from dojo.api_v2 import serializers as api_v2_serializers
11-
from dojo.api_v2.views import PrefetchDojoModelViewSet, report_generate, schema_with_prefetch
11+
from dojo.api_v2.views import PrefetchDojoModelViewSet, report_generate_response, schema_with_prefetch
1212
from dojo.authorization import api_permissions as permissions
1313
from dojo.models import Endpoint, Product, Product_API_Scan_Configuration
1414
from dojo.product.api.filters import ApiProductFilter
@@ -120,11 +120,10 @@ def generate_report(self, request, pk=None):
120120
options[
121121
"include_table_of_contents"
122122
] = report_options.validated_data["include_table_of_contents"]
123+
options["report_type"] = report_options.validated_data["report_type"]
123124
else:
124125
return Response(
125126
report_options.errors, status=status.HTTP_400_BAD_REQUEST,
126127
)
127128

128-
data = report_generate(request, product, options)
129-
report = api_v2_serializers.ReportGenerateSerializer(data)
130-
return Response(report.data)
129+
return report_generate_response(request, product, options)

dojo/reports/ui/views.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ def report_url_resolver(request):
7171
try:
7272
url_resolver = request.META["HTTP_X_FORWARDED_PROTO"] + "://" + request.META["HTTP_X_FORWARDED_FOR"]
7373
except:
74+
if "HTTP_HOST" not in request.META:
75+
return request.build_absolute_uri("/").rstrip("/")
7476
hostname = request.META["HTTP_HOST"]
7577
port_index = hostname.find(":")
7678
if port_index != -1:
@@ -846,8 +848,11 @@ def get(self, request):
846848
findings = prefetch_related_findings_for_report(findings)
847849
self.findings = findings
848850
findings = self.add_findings_data()
851+
return self.build_response(findings)
852+
853+
def build_response(self, findings, filename="findings.csv"):
849854
response = HttpResponse(content_type="text/csv")
850-
response["Content-Disposition"] = "attachment; filename=findings.csv"
855+
response["Content-Disposition"] = f"attachment; filename={filename}"
851856
writer = csv.writer(response)
852857
allowed_attributes = get_attributes()
853858
excludes_list = get_excludes()
@@ -983,6 +988,9 @@ def get(self, request):
983988
findings = prefetch_related_findings_for_report(findings)
984989
self.findings = findings
985990
findings = self.add_findings_data()
991+
return self.build_response(findings)
992+
993+
def build_response(self, findings, filename="findings.xlsx"):
986994
workbook = Workbook()
987995
workbook.iso_dates = True
988996
worksheet = workbook.active
@@ -1140,5 +1148,5 @@ def get(self, request):
11401148
content=stream,
11411149
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
11421150
)
1143-
response["Content-Disposition"] = "attachment; filename=findings.xlsx"
1151+
response["Content-Disposition"] = f"attachment; filename={filename}"
11441152
return response

dojo/test/api/views.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from rest_framework.response import Response
99

1010
from dojo.api_v2 import serializers as api_v2_serializers
11-
from dojo.api_v2.views import PrefetchDojoModelViewSet, report_generate
11+
from dojo.api_v2.views import PrefetchDojoModelViewSet, report_generate_response
1212
from dojo.authorization import api_permissions as permissions
1313
from dojo.models import (
1414
FileUpload,
@@ -110,14 +110,13 @@ def generate_report(self, request, pk=None):
110110
options[
111111
"include_table_of_contents"
112112
] = report_options.validated_data["include_table_of_contents"]
113+
options["report_type"] = report_options.validated_data["report_type"]
113114
else:
114115
return Response(
115116
report_options.errors, status=status.HTTP_400_BAD_REQUEST,
116117
)
117118

118-
data = report_generate(request, test, options)
119-
report = api_v2_serializers.ReportGenerateSerializer(data)
120-
return Response(report.data)
119+
return report_generate_response(request, test, options)
121120

122121
@extend_schema(
123122
methods=["GET"],

0 commit comments

Comments
 (0)