Skip to content

Commit a1f409f

Browse files
authored
Global Finding Groups page (#12814)
* static group view * refactoring to filters and finding_group view
1 parent 723621c commit a1f409f

File tree

6 files changed

+440
-2
lines changed

6 files changed

+440
-2
lines changed

dojo/filters.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2016,6 +2016,40 @@ def set_related_object_fields(self, *args: list, **kwargs: dict):
20162016
self.form.fields["reviewers"].queryset = self.form.fields["reporter"].queryset
20172017

20182018

2019+
class FindingGroupsFilter(FilterSet):
2020+
name = CharFilter(lookup_expr="icontains", label="Name")
2021+
severity = ChoiceFilter(
2022+
choices=[
2023+
("Low", "Low"),
2024+
("Medium", "Medium"),
2025+
("High", "High"),
2026+
("Critical", "Critical"),
2027+
],
2028+
label="Min Severity",
2029+
)
2030+
engagement = ModelMultipleChoiceFilter(queryset=Engagement.objects.none(), label="Engagement")
2031+
product = ModelMultipleChoiceFilter(queryset=Product.objects.none(), label="Product")
2032+
2033+
class Meta:
2034+
model = Finding
2035+
fields = ["name", "severity", "engagement", "product"]
2036+
2037+
def __init__(self, *args, **kwargs):
2038+
self.user = kwargs.pop("user", None)
2039+
self.pid = kwargs.pop("pid", None)
2040+
super().__init__(*args, **kwargs)
2041+
self.set_related_object_fields()
2042+
2043+
def set_related_object_fields(self):
2044+
if self.pid is not None:
2045+
self.form.fields["engagement"].queryset = Engagement.objects.filter(product_id=self.pid)
2046+
if "product" in self.form.fields:
2047+
del self.form.fields["product"]
2048+
else:
2049+
self.form.fields["product"].queryset = get_authorized_products(Permissions.Product_View)
2050+
self.form.fields["engagement"].queryset = get_authorized_engagements(Permissions.Engagement_View)
2051+
2052+
20192053
class AcceptedFindingFilter(FindingFilter):
20202054
risk_acceptance__created__date = DateRangeFilter(label="Acceptance Date")
20212055
risk_acceptance__owner = ModelMultipleChoiceFilter(

dojo/finding_group/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,9 @@
88
re_path(r"^finding_group/(?P<fgid>\d+)/delete$", views.delete_finding_group, name="delete_finding_group"),
99
re_path(r"^finding_group/(?P<fgid>\d+)/jira/push$", views.push_to_jira, name="finding_group_push_to_jira"),
1010
re_path(r"^finding_group/(?P<fgid>\d+)/jira/unlink$", views.unlink_jira, name="finding_group_unlink_jira"),
11+
12+
# finding group list views
13+
re_path(r"^finding_group/all$", views.ListFindingGroups.as_view(), name="all_finding_groups"),
14+
re_path(r"^finding_group/open$", views.ListOpenFindingGroups.as_view(), name="open_finding_groups"),
15+
re_path(r"^finding_group/closed$", views.ListClosedFindingGroups.as_view(), name="closed_finding_groups"),
1116
]

dojo/finding_group/views.py

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,29 @@
22

33
from django.contrib import messages
44
from django.contrib.admin.utils import NestedObjects
5+
from django.core.paginator import Page, Paginator
6+
from django.db.models import Count, Min, Q, QuerySet, Subquery
57
from django.db.utils import DEFAULT_DB_ALIAS
8+
from django.http import HttpRequest
69
from django.http.response import HttpResponse, HttpResponseRedirect, JsonResponse
710
from django.shortcuts import get_object_or_404, render
811
from django.urls.base import reverse
12+
from django.views import View
913
from django.views.decorators.http import require_POST
1014

1115
import dojo.jira_link.helper as jira_helper
1216
from dojo.authorization.authorization import user_has_permission_or_403
1317
from dojo.authorization.authorization_decorators import user_is_authorized
1418
from dojo.authorization.roles_permissions import Permissions
15-
from dojo.filters import FindingFilter, FindingFilterWithoutObjectLookups
19+
from dojo.filters import (
20+
FindingFilter,
21+
FindingFilterWithoutObjectLookups,
22+
FindingGroupsFilter,
23+
)
1624
from dojo.finding.queries import prefetch_for_findings
1725
from dojo.forms import DeleteFindingGroupForm, EditFindingGroupForm, FindingBulkUpdateForm
18-
from dojo.models import Engagement, Finding, Finding_Group, GITHUB_PKey, Product
26+
from dojo.models import Engagement, Finding, Finding_Group, GITHUB_PKey, Global_Role, Product
27+
from dojo.product.queries import get_authorized_products
1928
from dojo.utils import Product_Tab, add_breadcrumb, get_page_items, get_setting, get_system_setting, get_words_for_field
2029

2130
logger = logging.getLogger(__name__)
@@ -204,3 +213,117 @@ def push_to_jira(request, fgid):
204213
"Error pushing to JIRA",
205214
extra_tags="alert-danger")
206215
return HttpResponse(status=500)
216+
217+
218+
class ListFindingGroups(View):
219+
filter_name: str = "All"
220+
221+
SEVERITY_ORDER = {
222+
"Critical": 4,
223+
"High": 3,
224+
"Medium": 2,
225+
"Low": 1,
226+
"Info": 0,
227+
}
228+
229+
def get_template(self) -> str:
230+
return "dojo/finding_groups_list.html"
231+
232+
def order_field(self, request: HttpRequest, group_findings_queryset: QuerySet[Finding_Group]) -> QuerySet[Finding_Group]:
233+
order_field_param: str | None = request.GET.get("o")
234+
if order_field_param:
235+
reverse_order = order_field_param.startswith("-")
236+
order_field_param = order_field_param[1:] if reverse_order else order_field_param
237+
if order_field_param in {"name", "creator", "findings_count", "sla_deadline"}:
238+
prefix = "-" if reverse_order else ""
239+
group_findings_queryset = group_findings_queryset.order_by(f"{prefix}{order_field_param}")
240+
return group_findings_queryset
241+
242+
def filters(self, request: HttpRequest) -> tuple[str, str | None, list[str], list[str]]:
243+
name_filter: str = request.GET.get("name", "").lower()
244+
min_severity_filter: str | None = request.GET.get("severity")
245+
engagement_filter: list[str] = request.GET.getlist("engagement")
246+
product_filter: list[str] = request.GET.getlist("product")
247+
return name_filter, min_severity_filter, engagement_filter, product_filter
248+
249+
def filter_check(self, request: HttpRequest) -> Q:
250+
name_filter, min_severity_filter, engagement_filter, product_filter = self.filters(request)
251+
q_objects = Q()
252+
if name_filter:
253+
q_objects &= Q(name__icontains=name_filter)
254+
if product_filter:
255+
q_objects &= Q(findings__test__engagement__product__id__in=product_filter)
256+
if engagement_filter:
257+
q_objects &= Q(findings__test__engagement__id__in=engagement_filter)
258+
if min_severity_filter:
259+
min_severity_order_value = self.SEVERITY_ORDER.get(min_severity_filter, -1)
260+
valid_severities_for_filter = [
261+
sev for sev, order in self.SEVERITY_ORDER.items() if order >= min_severity_order_value
262+
]
263+
q_objects &= Q(findings__severity__in=valid_severities_for_filter)
264+
return q_objects
265+
266+
def get_findings(self, products: QuerySet[Product] | None) -> tuple[QuerySet[Finding], QuerySet[Finding]]:
267+
filters: dict = {}
268+
if products:
269+
filters["test__engagement__product__in"] = products
270+
user_findings_qs = Finding.objects.filter(**filters)
271+
return user_findings_qs, user_findings_qs.filter(active=True)
272+
273+
def get_finding_groups(self, request: HttpRequest, products: QuerySet[Product] | None = None) -> QuerySet[Finding_Group]:
274+
finding_groups_queryset = Finding_Group.objects.all()
275+
if products is not None:
276+
user_findings, _ = self.get_findings(products)
277+
finding_groups_queryset = finding_groups_queryset.filter(findings__id__in=Subquery(user_findings.values("id"))).distinct()
278+
request_filters_q = self.filter_check(request)
279+
finding_groups_queryset = finding_groups_queryset.filter(request_filters_q).distinct()
280+
finding_groups_queryset = finding_groups_queryset.annotate(
281+
findings_count=Count("findings", distinct=True),
282+
sla_deadline=Min("findings__sla_expiration_date"),
283+
)
284+
return self.order_field(request, finding_groups_queryset)
285+
286+
def paginate_queryset(self, queryset: QuerySet[Finding_Group], request: HttpRequest) -> Page:
287+
page_size = int(request.GET.get("page_size", 25))
288+
paginator = Paginator(queryset, page_size)
289+
page_number = request.GET.get("page")
290+
return paginator.get_page(page_number)
291+
292+
def get(self, request: HttpRequest) -> HttpResponse:
293+
global_role = Global_Role.objects.filter(user=request.user).first()
294+
products = get_authorized_products(Permissions.Product_View)
295+
if request.user.is_superuser or (global_role and global_role.role):
296+
finding_groups = self.get_finding_groups(request)
297+
elif products.exists():
298+
finding_groups = self.get_finding_groups(request, products)
299+
else:
300+
finding_groups = Finding_Group.objects.none()
301+
302+
paginated_finding_groups = self.paginate_queryset(finding_groups, request)
303+
304+
context = {
305+
"filter_name": self.filter_name,
306+
"filtered": FindingGroupsFilter(request.GET),
307+
"finding_groups": paginated_finding_groups,
308+
}
309+
310+
add_breadcrumb(title="Finding Group", top_level=not request.GET, request=request)
311+
return render(request, self.get_template(), context)
312+
313+
314+
class ListOpenFindingGroups(ListFindingGroups):
315+
filter_name: str = "Open"
316+
317+
def get_finding_groups(self, request: HttpRequest, products: QuerySet[Product] | None = None) -> QuerySet[Finding_Group]:
318+
finding_groups_queryset = super().get_finding_groups(request, products)
319+
_, active_findings = self.get_findings(products)
320+
return finding_groups_queryset.filter(findings__id__in=Subquery(active_findings.values("id"))).distinct()
321+
322+
323+
class ListClosedFindingGroups(ListFindingGroups):
324+
filter_name: str = "Closed"
325+
326+
def get_finding_groups(self, request: HttpRequest, products: QuerySet[Product] | None = None) -> QuerySet[Finding_Group]:
327+
finding_groups_queryset = super().get_finding_groups(request, products)
328+
_, active_findings = self.get_findings(products)
329+
return finding_groups_queryset.exclude(findings__id__in=Subquery(active_findings.values("id"))).distinct()

dojo/templates/base.html

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,31 @@
338338
</ul>
339339
<!-- /.nav-second-level -->
340340
</li>
341+
<li>
342+
<a href="{% url 'all_finding_groups' %}" aria-expanded="false" aria-label="Problems">
343+
<i class="fa-solid fa-triangle-exclamation fa-fw"></i>
344+
<span>{% trans "Dashboard" %}</span>
345+
<span class="glyphicon arrow"></span>
346+
</a>
347+
<ul class="nav nav-second-level">
348+
<li>
349+
<a href="{% url 'open_finding_groups' %}">
350+
{% trans "Open Findings Groups" %}
351+
</a>
352+
</li>
353+
<li>
354+
<a href="{% url 'all_finding_groups' %}">
355+
{% trans "All Findings Groups" %}
356+
</a>
357+
</li>
358+
<li>
359+
<a href="{% url 'closed_finding_groups' %}">
360+
{% trans "Closed Findings Groups" %}
361+
</a>
362+
</li>
363+
</ul>
364+
<!-- /.nav-second-level -->
365+
</li>
341366
<li>
342367
<a href="{% url 'components' %}" id="product_component_view" aria-expanded="false" aria-label="Components">
343368
<i class="fa-solid fa-table-cells-large fa-fw"></i>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{% extends "base.html" %}
2+
{% load navigation_tags %}
3+
{% load display_tags %}
4+
{% load static %}
5+
{% block content %}
6+
{% comment %} All/Open/Closed Finding Groups {% endcomment %}
7+
{% include "dojo/finding_groups_list_snippet.html" %}
8+
{% endblock %}

0 commit comments

Comments
 (0)