Skip to content

Add "All Finding Groups" page #12814

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions dojo/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Copy link
Member

@valentijnscholten valentijnscholten Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be some case insensitive like name = CharFilter(lookup_expr="icontains", 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(
Expand Down
5 changes: 5 additions & 0 deletions dojo/finding_group/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@
re_path(r"^finding_group/(?P<fgid>\d+)/delete$", views.delete_finding_group, name="delete_finding_group"),
re_path(r"^finding_group/(?P<fgid>\d+)/jira/push$", views.push_to_jira, name="finding_group_push_to_jira"),
re_path(r"^finding_group/(?P<fgid>\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"),
]
127 changes: 125 additions & 2 deletions dojo/finding_group/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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):
Comment on lines +292 to +295
Copy link
Member

@valentijnscholten valentijnscholten Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect some calls to get_authorized_products or something similar here (i.e. not building a queryfilter yourself)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or maybe there should be an get_authorized_finding_groups method added somewhere to abstract his logic away.

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()
25 changes: 25 additions & 0 deletions dojo/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,31 @@
</ul>
<!-- /.nav-second-level -->
</li>
<li>
<a href="{% url 'all_finding_groups' %}" aria-expanded="false" aria-label="Problems">
<i class="fa-solid fa-triangle-exclamation fa-fw"></i>
<span>{% trans "Dashboard" %}</span>
<span class="glyphicon arrow"></span>
</a>
<ul class="nav nav-second-level">
<li>
<a href="{% url 'open_finding_groups' %}">
{% trans "Open Findings Groups" %}
</a>
</li>
<li>
<a href="{% url 'all_finding_groups' %}">
{% trans "All Findings Groups" %}
</a>
</li>
<li>
<a href="{% url 'closed_finding_groups' %}">
{% trans "Closed Findings Groups" %}
</a>
</li>
</ul>
<!-- /.nav-second-level -->
</li>
<li>
<a href="{% url 'components' %}" id="product_component_view" aria-expanded="false" aria-label="Components">
<i class="fa-solid fa-table-cells-large fa-fw"></i>
Expand Down
8 changes: 8 additions & 0 deletions dojo/templates/dojo/finding_groups_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{% extends "base.html" %}
{% load navigation_tags %}
{% load display_tags %}
{% load static %}
{% block content %}
{% comment %} All/Open/Closed Finding Groups {% endcomment %}
{% include "dojo/finding_groups_list_snippet.html" %}
{% endblock %}
Loading