-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
base: dev
Are you sure you want to change the base?
Conversation
🔴 Risk threshold exceeded.This pull request contains a potential Denial of Service vulnerability in the
🔴 Configured Codepaths Edit in
|
Vulnerability | Configured Codepaths Edit |
---|---|
Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml . |
🔴 Configured Codepaths Edit in dojo/finding_group/views.py
Vulnerability | Configured Codepaths Edit |
---|---|
Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml . |
🔴 Configured Codepaths Edit in dojo/filters.py
Vulnerability | Configured Codepaths Edit |
---|---|
Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml . |
🔴 Configured Codepaths Edit in dojo/finding_group/urls.py
Vulnerability | Configured Codepaths Edit |
---|---|
Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml . |
🔴 Configured Codepaths Edit in dojo/finding_group/views.py
Vulnerability | Configured Codepaths Edit |
---|---|
Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml . |
🔴 Configured Codepaths Edit in dojo/templates/base.html
Vulnerability | Configured Codepaths Edit |
---|---|
Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml . |
🔴 Configured Codepaths Edit in dojo/templates/dojo/finding_groups_list.html
Vulnerability | Configured Codepaths Edit |
---|---|
Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml . |
🔴 Configured Codepaths Edit in dojo/templates/dojo/finding_groups_list_snippet.html
Vulnerability | Configured Codepaths Edit |
---|---|
Description | Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml . |
Potential Denial of Service (DoS) in dojo/finding_group/views.py
Vulnerability | Potential Denial of Service (DoS) |
---|---|
Description | The ListFindingGroups view is vulnerable to a potential Denial of Service (DoS) attack due to several factors. User-controlled GET parameters for engagement and product are processed as lists using request.GET.getlist() , allowing an attacker to supply an arbitrary number of IDs. These lists are then used in __in lookups within database queries, which can become extremely slow and resource-intensive with a large number of values. Additionally, the page_size parameter, also user-controlled, lacks an upper limit, enabling an attacker to request an excessively large number of items per page, further exacerbating the load on the database. The combination of filter , distinct , and annotate with Count(distinct=True) and Min operations on potentially large datasets, especially when combined with broad name__icontains searches, can lead to disproportionately slow query execution, consuming significant database resources and potentially causing a denial of service. |
django-DefectDojo/dojo/finding_group/views.py
Lines 212 to 329 in a86218e
"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() |
We've notified @mtesauro.
All finding details can be found in the DryRun Security Dashboard.
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
Conflicts have been resolved. A maintainer will review the pull request shortly. |
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): |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
@@ -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") |
There was a problem hiding this comment.
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")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've placed some comments. Let me also check if the rest of the team also thinks this is a useful addition.
Okay, but I think it's better to just leave the title "Finding Group" page. I made it similar to the "Finding" page with the All/Open/Closed options. |
One thing I noticed while implementing the code, which I think is worth mentioning: when we perform two scan imports, each with one finding that would be grouped into the same Finding_Group, DefectDojo is creating two groups with the same name instead of just including the finding from the second scan in the first group created, doubling the space occupied in the database. Is it supposed to be like this? |
I developed a new tab so that people can better see the information of the Finding_Group that is created during the scan import. This is acording to #12684.
As a next step we plan to allow include an option to dynamically create and visualize groups by properties of Findings.
Bellow the image of Findings tab:


And the image of the reduced result with Finding_Group tab: