Skip to content

Commit 1efdfd9

Browse files
Fix #10268: Add context-aware filtering to Finding Group filter
- Implemented hierarchical context filtering (test > engagement > product > global) - Created get_finding_group_queryset_for_context() helper function to eliminate code duplication - Modified FindingFilter and FindingFilterWithoutObjectLookups to accept eid/tid parameters - Updated filter to show only Finding Groups from current test/engagement/product context - Added query optimization with .only("id", "name") for Finding Groups - Fixed user parameter passing to get_authorized_finding_groups_for_queryset() - Updated finding/views.py and test/views.py to pass context parameters to filters - Created comprehensive unit tests (8 test methods) covering all context levels This ensures users only see relevant Finding Groups in the filter dropdown based on their current page context, preventing confusion from seeing unrelated groups.
1 parent de0b39c commit 1efdfd9

4 files changed

Lines changed: 357 additions & 27 deletions

File tree

dojo/filters.py

Lines changed: 89 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2020,6 +2020,32 @@ def filter_mitigated_on(self, queryset, name, value):
20202020
return queryset.filter(mitigated=value)
20212021

20222022

2023+
def get_finding_group_queryset_for_context(pid=None, eid=None, tid=None):
2024+
"""
2025+
Helper function to build finding group queryset based on context hierarchy.
2026+
Context priority: test > engagement > product > global
2027+
2028+
Args:
2029+
pid: Product ID (least specific)
2030+
eid: Engagement ID
2031+
tid: Test ID (most specific)
2032+
2033+
Returns:
2034+
QuerySet of Finding_Group filtered by context
2035+
"""
2036+
if tid is not None:
2037+
# Most specific: filter by test
2038+
return Finding_Group.objects.filter(test_id=tid).only("id", "name")
2039+
if eid is not None:
2040+
# Filter by engagement's tests
2041+
return Finding_Group.objects.filter(test__engagement_id=eid).only("id", "name")
2042+
if pid is not None:
2043+
# Filter by product's tests
2044+
return Finding_Group.objects.filter(test__engagement__product_id=pid).only("id", "name")
2045+
# Global: return all (authorization will be applied separately)
2046+
return Finding_Group.objects.all().only("id", "name")
2047+
2048+
20232049
class FindingFilterWithoutObjectLookups(FindingFilterHelper, FindingTagStringFilter):
20242050
test__engagement__product__prod_type = NumberFilter(widget=HiddenInput())
20252051
test__engagement__product = NumberFilter(widget=HiddenInput())
@@ -2111,20 +2137,45 @@ class Meta:
21112137
def __init__(self, *args, **kwargs):
21122138
self.user = None
21132139
self.pid = None
2140+
self.eid = None
2141+
self.tid = None
21142142
if "user" in kwargs:
21152143
self.user = kwargs.pop("user")
21162144

21172145
if "pid" in kwargs:
21182146
self.pid = kwargs.pop("pid")
2147+
if "eid" in kwargs:
2148+
self.eid = kwargs.pop("eid")
2149+
if "tid" in kwargs:
2150+
self.tid = kwargs.pop("tid")
21192151
super().__init__(*args, **kwargs)
21202152
# Set some date fields
21212153
self.set_date_fields(*args, **kwargs)
2122-
# Don't show the product filter on the product finding view
2123-
if self.pid:
2124-
del self.form.fields["test__engagement__product__name"]
2125-
del self.form.fields["test__engagement__product__name_contains"]
2126-
del self.form.fields["test__engagement__product__prod_type__name"]
2127-
del self.form.fields["test__engagement__product__prod_type__name_contains"]
2154+
# Don't show the product/engagement/test filter fields when in specific context
2155+
if self.tid or self.eid or self.pid:
2156+
if "test__engagement__product__name" in self.form.fields:
2157+
del self.form.fields["test__engagement__product__name"]
2158+
if "test__engagement__product__name_contains" in self.form.fields:
2159+
del self.form.fields["test__engagement__product__name_contains"]
2160+
if "test__engagement__product__prod_type__name" in self.form.fields:
2161+
del self.form.fields["test__engagement__product__prod_type__name"]
2162+
if "test__engagement__product__prod_type__name_contains" in self.form.fields:
2163+
del self.form.fields["test__engagement__product__prod_type__name_contains"]
2164+
# Also hide engagement and test fields if in test or engagement context
2165+
if self.tid:
2166+
if "test__engagement__name" in self.form.fields:
2167+
del self.form.fields["test__engagement__name"]
2168+
if "test__engagement__name_contains" in self.form.fields:
2169+
del self.form.fields["test__engagement__name_contains"]
2170+
if "test__name" in self.form.fields:
2171+
del self.form.fields["test__name"]
2172+
if "test__name_contains" in self.form.fields:
2173+
del self.form.fields["test__name_contains"]
2174+
elif self.eid:
2175+
if "test__engagement__name" in self.form.fields:
2176+
del self.form.fields["test__engagement__name"]
2177+
if "test__engagement__name_contains" in self.form.fields:
2178+
del self.form.fields["test__engagement__name_contains"]
21282179

21292180

21302181
class FindingFilter(FindingFilterHelper, FindingTagFilter):
@@ -2163,29 +2214,57 @@ class Meta:
21632214
def __init__(self, *args, **kwargs):
21642215
self.user = None
21652216
self.pid = None
2217+
self.eid = None
2218+
self.tid = None
21662219
if "user" in kwargs:
21672220
self.user = kwargs.pop("user")
21682221

21692222
if "pid" in kwargs:
21702223
self.pid = kwargs.pop("pid")
2224+
if "eid" in kwargs:
2225+
self.eid = kwargs.pop("eid")
2226+
if "tid" in kwargs:
2227+
self.tid = kwargs.pop("tid")
21712228
super().__init__(*args, **kwargs)
21722229
# Set some date fields
21732230
self.set_date_fields(*args, **kwargs)
21742231
# Don't show the product filter on the product finding view
21752232
self.set_related_object_fields(*args, **kwargs)
21762233

21772234
def set_related_object_fields(self, *args: list, **kwargs: dict):
2178-
finding_group_query = Finding_Group.objects.all()
2179-
if self.pid is not None:
2235+
# Use helper to get contextual finding group queryset
2236+
finding_group_query = get_finding_group_queryset_for_context(
2237+
pid=self.pid,
2238+
eid=self.eid,
2239+
tid=self.tid,
2240+
)
2241+
2242+
# Filter by most specific context: test > engagement > product
2243+
if self.tid is not None:
2244+
# Test context: filter finding groups by test
2245+
del self.form.fields["test__engagement__product"]
2246+
del self.form.fields["test__engagement__product__prod_type"]
2247+
del self.form.fields["test__engagement"]
2248+
del self.form.fields["test"]
2249+
elif self.eid is not None:
2250+
# Engagement context: filter finding groups by engagement
2251+
del self.form.fields["test__engagement__product"]
2252+
del self.form.fields["test__engagement__product__prod_type"]
2253+
del self.form.fields["test__engagement"]
2254+
# Filter tests by engagement - get_authorized_tests doesn't support engagement param
2255+
engagement = Engagement.objects.get(id=self.eid)
2256+
self.form.fields["test"].queryset = get_authorized_tests(Permissions.Test_View, product=engagement.product).filter(engagement_id=self.eid).prefetch_related("test_type")
2257+
elif self.pid is not None:
2258+
# Product context: filter finding groups by product
21802259
del self.form.fields["test__engagement__product"]
21812260
del self.form.fields["test__engagement__product__prod_type"]
21822261
# TODO: add authorized check to be sure
21832262
self.form.fields["test__engagement"].queryset = Engagement.objects.filter(
21842263
product_id=self.pid,
21852264
).all()
21862265
self.form.fields["test"].queryset = get_authorized_tests(Permissions.Test_View, product=self.pid).prefetch_related("test_type")
2187-
finding_group_query = Finding_Group.objects.filter(test__engagement__product_id=self.pid)
21882266
else:
2267+
# Global context: show all authorized finding groups
21892268
self.form.fields[
21902269
"test__engagement__product__prod_type"].queryset = get_authorized_product_types(Permissions.Product_Type_View)
21912270
self.form.fields["test__engagement"].queryset = get_authorized_engagements(Permissions.Engagement_View)
@@ -2194,7 +2273,7 @@ def set_related_object_fields(self, *args: list, **kwargs: dict):
21942273
if self.form.fields.get("test__engagement__product"):
21952274
self.form.fields["test__engagement__product"].queryset = get_authorized_products(Permissions.Product_View)
21962275
if self.form.fields.get("finding_group", None):
2197-
self.form.fields["finding_group"].queryset = get_authorized_finding_groups_for_queryset(Permissions.Finding_Group_View, finding_group_query)
2276+
self.form.fields["finding_group"].queryset = get_authorized_finding_groups_for_queryset(Permissions.Finding_Group_View, finding_group_query, user=self.user)
21982277
self.form.fields["reporter"].queryset = get_authorized_users(Permissions.Finding_View)
21992278
self.form.fields["reviewers"].queryset = self.form.fields["reporter"].queryset
22002279

dojo/finding/views.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,8 @@ def filter_findings_by_form(self, request: HttpRequest, findings: QuerySet[Findi
267267
kwargs = {
268268
"user": request.user,
269269
"pid": self.get_product_id(),
270+
"eid": self.get_engagement_id(),
271+
"tid": self.get_test_id(),
270272
}
271273

272274
filter_string_matching = get_system_setting("filter_string_matching", False)
@@ -360,10 +362,11 @@ def add_breadcrumbs(self, request: HttpRequest, context: dict):
360362

361363
return request, context
362364

363-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
364-
# Store the product and engagement ids
365+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
366+
# Store the product, engagement, and test ids
365367
self.product_id = product_id
366368
self.engagement_id = engagement_id
369+
self.test_id = test_id
367370
# Get the initial context
368371
request, context = self.get_initial_context(request)
369372
# Get the filtered findings
@@ -386,46 +389,46 @@ def get(self, request: HttpRequest, product_id: int | None = None, engagement_id
386389

387390

388391
class ListOpenFindings(ListFindings):
389-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
392+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
390393
self.filter_name = "Open"
391-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
394+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
392395

393396

394397
class ListVerifiedFindings(ListFindings):
395-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
398+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
396399
self.filter_name = "Verified"
397-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
400+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
398401

399402

400403
class ListOutOfScopeFindings(ListFindings):
401-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
404+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
402405
self.filter_name = "Out of Scope"
403-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
406+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
404407

405408

406409
class ListFalsePositiveFindings(ListFindings):
407-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
410+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
408411
self.filter_name = "False Positive"
409-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
412+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
410413

411414

412415
class ListInactiveFindings(ListFindings):
413-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
416+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
414417
self.filter_name = "Inactive"
415-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
418+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
416419

417420

418421
class ListAcceptedFindings(ListFindings):
419-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
422+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
420423
self.filter_name = "Accepted"
421-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
424+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
422425

423426

424427
class ListClosedFindings(ListFindings):
425-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
428+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
426429
self.filter_name = "Closed"
427430
self.order_by = "-mitigated"
428-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
431+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
429432

430433

431434
class ViewFinding(View):

dojo/test/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def get_findings(self, request: HttpRequest, test: Test):
122122
findings = Finding.objects.filter(test=test).order_by("numerical_severity")
123123
filter_string_matching = get_system_setting("filter_string_matching", False)
124124
finding_filter_class = FindingFilterWithoutObjectLookups if filter_string_matching else FindingFilter
125-
findings = finding_filter_class(request.GET, pid=test.engagement.product.id, queryset=findings)
125+
findings = finding_filter_class(request.GET, pid=test.engagement.product.id, eid=test.engagement.id, tid=test.id, queryset=findings)
126126
paged_findings = get_page_items_and_count(request, prefetch_for_findings(findings.qs), 25, prefix="findings")
127127
fix_available_count = findings.qs.filter(fix_available=True).count()
128128

0 commit comments

Comments
 (0)