diff --git a/dojo/filters.py b/dojo/filters.py index bfc84c230f..65ec61816a 100644 --- a/dojo/filters.py +++ b/dojo/filters.py @@ -2016,6 +2016,41 @@ def set_related_object_fields(self, *args: list, **kwargs: dict): self.form.fields["reviewers"].queryset = self.form.fields["reporter"].queryset +class FindingGroupsFilter(FilterSet): + name = CharFilter(method="filter_name", label="Name") + severity = ChoiceFilter( + choices=[ + ("Low", "Low"), + ("Medium", "Medium"), + ("High", "High"), + ("Critical", "Critical"), + ], + method="filter_min_severity", + label="Min Severity", + ) + engagement = ModelMultipleChoiceFilter(queryset=Engagement.objects.none(), label="Engagement") + product = ModelMultipleChoiceFilter(queryset=Product.objects.none(), label="Product") + + class Meta: + model = Finding + fields = ["name", "severity", "engagement", "product"] + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop("user", None) + self.pid = kwargs.pop("pid", None) + super().__init__(*args, **kwargs) + self.set_related_object_fields() + + def set_related_object_fields(self): + if self.pid is not None: + self.form.fields["engagement"].queryset = Engagement.objects.filter(product_id=self.pid) + if "product" in self.form.fields: + del self.form.fields["product"] + else: + self.form.fields["product"].queryset = get_authorized_products(Permissions.Product_View) + self.form.fields["engagement"].queryset = get_authorized_engagements(Permissions.Engagement_View) + + class AcceptedFindingFilter(FindingFilter): risk_acceptance__created__date = DateRangeFilter(label="Acceptance Date") risk_acceptance__owner = ModelMultipleChoiceFilter( diff --git a/dojo/finding_group/urls.py b/dojo/finding_group/urls.py index 5abb46ece5..938d95ba9e 100644 --- a/dojo/finding_group/urls.py +++ b/dojo/finding_group/urls.py @@ -8,4 +8,9 @@ re_path(r"^finding_group/(?P\d+)/delete$", views.delete_finding_group, name="delete_finding_group"), re_path(r"^finding_group/(?P\d+)/jira/push$", views.push_to_jira, name="finding_group_push_to_jira"), re_path(r"^finding_group/(?P\d+)/jira/unlink$", views.unlink_jira, name="finding_group_unlink_jira"), + + # finding group list views + re_path(r"^finding_group/all$", views.ListFindingGroups.as_view(), name="all_finding_groups"), + re_path(r"^finding_group/open$", views.ListOpenFindingGroups.as_view(), name="open_finding_groups"), + re_path(r"^finding_group/closed$", views.ListClosedFindingGroups.as_view(), name="closed_finding_groups"), ] diff --git a/dojo/finding_group/views.py b/dojo/finding_group/views.py index 8eae4e9775..baa521e01e 100644 --- a/dojo/finding_group/views.py +++ b/dojo/finding_group/views.py @@ -2,20 +2,28 @@ from django.contrib import messages from django.contrib.admin.utils import NestedObjects +from django.core.paginator import Page, Paginator +from django.db.models import Count, Min, Q, QuerySet, Subquery from django.db.utils import DEFAULT_DB_ALIAS +from django.http import HttpRequest from django.http.response import HttpResponse, HttpResponseRedirect, JsonResponse from django.shortcuts import get_object_or_404, render from django.urls.base import reverse +from django.views import View from django.views.decorators.http import require_POST import dojo.jira_link.helper as jira_helper from dojo.authorization.authorization import user_has_permission_or_403 from dojo.authorization.authorization_decorators import user_is_authorized from dojo.authorization.roles_permissions import Permissions -from dojo.filters import FindingFilter, FindingFilterWithoutObjectLookups +from dojo.filters import ( + FindingFilter, + FindingFilterWithoutObjectLookups, + FindingGroupsFilter, +) from dojo.finding.queries import prefetch_for_findings from dojo.forms import DeleteFindingGroupForm, EditFindingGroupForm, FindingBulkUpdateForm -from dojo.models import Engagement, Finding, Finding_Group, GITHUB_PKey, Product +from dojo.models import Dojo_Group, Engagement, Finding, Finding_Group, GITHUB_PKey, Global_Role, Product from dojo.utils import Product_Tab, add_breadcrumb, get_page_items, get_setting, get_system_setting, get_words_for_field logger = logging.getLogger(__name__) @@ -204,3 +212,118 @@ def push_to_jira(request, fgid): "Error pushing to JIRA", extra_tags="alert-danger") return HttpResponse(status=500) + + +class ListFindingGroups(View): + filter_name: str = "All" + + SEVERITY_ORDER = { + "Critical": 4, + "High": 3, + "Medium": 2, + "Low": 1, + "Info": 0, + } + + def get_template(self) -> str: + return "dojo/finding_groups_list.html" + + def order_field(self, request: HttpRequest, group_findings_queryset: QuerySet[Finding_Group]) -> QuerySet[Finding_Group]: + order_field_param: str | None = request.GET.get("o") + if order_field_param: + reverse_order = order_field_param.startswith("-") + order_field_param = order_field_param[1:] if reverse_order else order_field_param + if order_field_param in {"name", "creator", "findings_count", "sla_deadline"}: + prefix = "-" if reverse_order else "" + group_findings_queryset = group_findings_queryset.order_by(f"{prefix}{order_field_param}") + return group_findings_queryset + + def filters(self, request: HttpRequest) -> tuple[str, str | None, list[str], list[str]]: + name_filter: str = request.GET.get("name", "").lower() + min_severity_filter: str | None = request.GET.get("severity") + engagement_filter: list[str] = request.GET.getlist("engagement") + product_filter: list[str] = request.GET.getlist("product") + return name_filter, min_severity_filter, engagement_filter, product_filter + + def filter_check(self, request: HttpRequest) -> Q: + name_filter, min_severity_filter, engagement_filter, product_filter = self.filters(request) + q_objects = Q() + if name_filter: + q_objects &= Q(name__icontains=name_filter) + if product_filter: + q_objects &= Q(findings__test__engagement__product__id__in=product_filter) + if engagement_filter: + q_objects &= Q(findings__test__engagement__id__in=engagement_filter) + if min_severity_filter: + min_severity_order_value = self.SEVERITY_ORDER.get(min_severity_filter, -1) + valid_severities_for_filter = [ + sev for sev, order in self.SEVERITY_ORDER.items() if order >= min_severity_order_value + ] + q_objects &= Q(findings__severity__in=valid_severities_for_filter) + return q_objects + + def get_findings(self, products: QuerySet[Product] | None) -> tuple[QuerySet[Finding], QuerySet[Finding]]: + filters: dict = {} + if products: + filters["test__engagement__product__in"] = products + user_findings_qs = Finding.objects.filter(**filters) + return user_findings_qs, user_findings_qs.filter(active=True) + + def get_finding_groups(self, request: HttpRequest, products: QuerySet[Product] | None = None) -> QuerySet[Finding_Group]: + finding_groups_queryset = Finding_Group.objects.all() + if products is not None: + user_findings, _ = self.get_findings(products) + finding_groups_queryset = finding_groups_queryset.filter(findings__id__in=Subquery(user_findings.values("id"))).distinct() + request_filters_q = self.filter_check(request) + finding_groups_queryset = finding_groups_queryset.filter(request_filters_q).distinct() + finding_groups_queryset = finding_groups_queryset.annotate( + findings_count=Count("findings", distinct=True), + sla_deadline=Min("findings__sla_expiration_date"), + ) + return self.order_field(request, finding_groups_queryset) + + def paginate_queryset(self, queryset: QuerySet[Finding_Group], request: HttpRequest) -> Page: + page_size = int(request.GET.get("page_size", 25)) + paginator = Paginator(queryset, page_size) + page_number = request.GET.get("page") + return paginator.get_page(page_number) + + def get(self, request: HttpRequest) -> HttpResponse: + global_role = Global_Role.objects.filter(user=request.user).first() + user_groups = Dojo_Group.objects.filter(users=request.user) + products = Product.objects.filter(Q(members=request.user) | Q(authorization_groups__in=user_groups)).distinct() + if request.user.is_superuser or (global_role and global_role.role): + finding_groups = self.get_finding_groups(request) + elif products.exists(): + finding_groups = self.get_finding_groups(request, products) + else: + finding_groups = Finding_Group.objects.none() + + paginated_finding_groups = self.paginate_queryset(finding_groups, request) + + context = { + "filter_name": self.filter_name, + "filtered": FindingGroupsFilter(request.GET), + "finding_groups": paginated_finding_groups, + } + + add_breadcrumb(title="Finding Group", top_level=not request.GET, request=request) + return render(request, self.get_template(), context) + + +class ListOpenFindingGroups(ListFindingGroups): + filter_name: str = "Open" + + def get_finding_groups(self, request: HttpRequest, products: QuerySet[Product] | None = None) -> QuerySet[Finding_Group]: + finding_groups_queryset = super().get_finding_groups(request, products) + _, active_findings = self.get_findings(products) + return finding_groups_queryset.filter(findings__id__in=Subquery(active_findings.values("id"))).distinct() + + +class ListClosedFindingGroups(ListFindingGroups): + filter_name: str = "Closed" + + def get_finding_groups(self, request: HttpRequest, products: QuerySet[Product] | None = None) -> QuerySet[Finding_Group]: + finding_groups_queryset = super().get_finding_groups(request, products) + _, active_findings = self.get_findings(products) + return finding_groups_queryset.exclude(findings__id__in=Subquery(active_findings.values("id"))).distinct() diff --git a/dojo/templates/base.html b/dojo/templates/base.html index fd8da1023f..007612ce74 100644 --- a/dojo/templates/base.html +++ b/dojo/templates/base.html @@ -338,6 +338,31 @@ +
  • + + + +