Skip to content

Commit 2b1cb44

Browse files
Fix #10268: Make Finding Group filter context-aware (Test/Engagement/Product) (#14271)
* 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. * Switch to DojoTestCase for better test compatibility Use DojoTestCase instead of plain TestCase to align with DefectDojo testing conventions and ensure proper test setup/teardown.
1 parent 0dca4f0 commit 2b1cb44

4 files changed

Lines changed: 382 additions & 34 deletions

File tree

dojo/filters.py

Lines changed: 110 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2020,6 +2020,33 @@ 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+
"""
2037+
if tid is not None:
2038+
# Most specific: filter by test
2039+
return Finding_Group.objects.filter(test_id=tid).only("id", "name")
2040+
if eid is not None:
2041+
# Filter by engagement's tests
2042+
return Finding_Group.objects.filter(test__engagement_id=eid).only("id", "name")
2043+
if pid is not None:
2044+
# Filter by product's tests
2045+
return Finding_Group.objects.filter(test__engagement__product_id=pid).only("id", "name")
2046+
# Global: return all (authorization will be applied separately)
2047+
return Finding_Group.objects.all().only("id", "name")
2048+
2049+
20232050
class FindingFilterWithoutObjectLookups(FindingFilterHelper, FindingTagStringFilter):
20242051
test__engagement__product__prod_type = NumberFilter(widget=HiddenInput())
20252052
test__engagement__product = NumberFilter(widget=HiddenInput())
@@ -2111,20 +2138,45 @@ class Meta:
21112138
def __init__(self, *args, **kwargs):
21122139
self.user = None
21132140
self.pid = None
2141+
self.eid = None
2142+
self.tid = None
21142143
if "user" in kwargs:
21152144
self.user = kwargs.pop("user")
21162145

21172146
if "pid" in kwargs:
21182147
self.pid = kwargs.pop("pid")
2148+
if "eid" in kwargs:
2149+
self.eid = kwargs.pop("eid")
2150+
if "tid" in kwargs:
2151+
self.tid = kwargs.pop("tid")
21192152
super().__init__(*args, **kwargs)
21202153
# Set some date fields
21212154
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"]
2155+
# Don't show the product/engagement/test filter fields when in specific context
2156+
if self.tid or self.eid or self.pid:
2157+
if "test__engagement__product__name" in self.form.fields:
2158+
del self.form.fields["test__engagement__product__name"]
2159+
if "test__engagement__product__name_contains" in self.form.fields:
2160+
del self.form.fields["test__engagement__product__name_contains"]
2161+
if "test__engagement__product__prod_type__name" in self.form.fields:
2162+
del self.form.fields["test__engagement__product__prod_type__name"]
2163+
if "test__engagement__product__prod_type__name_contains" in self.form.fields:
2164+
del self.form.fields["test__engagement__product__prod_type__name_contains"]
2165+
# Also hide engagement and test fields if in test or engagement context
2166+
if self.tid:
2167+
if "test__engagement__name" in self.form.fields:
2168+
del self.form.fields["test__engagement__name"]
2169+
if "test__engagement__name_contains" in self.form.fields:
2170+
del self.form.fields["test__engagement__name_contains"]
2171+
if "test__name" in self.form.fields:
2172+
del self.form.fields["test__name"]
2173+
if "test__name_contains" in self.form.fields:
2174+
del self.form.fields["test__name_contains"]
2175+
elif self.eid:
2176+
if "test__engagement__name" in self.form.fields:
2177+
del self.form.fields["test__engagement__name"]
2178+
if "test__engagement__name_contains" in self.form.fields:
2179+
del self.form.fields["test__engagement__name_contains"]
21282180

21292181

21302182
class FindingFilter(FindingFilterHelper, FindingTagFilter):
@@ -2163,38 +2215,79 @@ class Meta:
21632215
def __init__(self, *args, **kwargs):
21642216
self.user = None
21652217
self.pid = None
2218+
self.eid = None
2219+
self.tid = None
21662220
if "user" in kwargs:
21672221
self.user = kwargs.pop("user")
21682222

21692223
if "pid" in kwargs:
21702224
self.pid = kwargs.pop("pid")
2225+
if "eid" in kwargs:
2226+
self.eid = kwargs.pop("eid")
2227+
if "tid" in kwargs:
2228+
self.tid = kwargs.pop("tid")
21712229
super().__init__(*args, **kwargs)
21722230
# Set some date fields
21732231
self.set_date_fields(*args, **kwargs)
21742232
# Don't show the product filter on the product finding view
21752233
self.set_related_object_fields(*args, **kwargs)
21762234

21772235
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:
2180-
del self.form.fields["test__engagement__product"]
2181-
del self.form.fields["test__engagement__product__prod_type"]
2236+
# Use helper to get contextual finding group queryset
2237+
finding_group_query = get_finding_group_queryset_for_context(
2238+
pid=self.pid,
2239+
eid=self.eid,
2240+
tid=self.tid,
2241+
)
2242+
2243+
# Filter by most specific context: test > engagement > product
2244+
if self.tid is not None:
2245+
# Test context: filter finding groups by test
2246+
if "test__engagement__product" in self.form.fields:
2247+
del self.form.fields["test__engagement__product"]
2248+
if "test__engagement__product__prod_type" in self.form.fields:
2249+
del self.form.fields["test__engagement__product__prod_type"]
2250+
if "test__engagement" in self.form.fields:
2251+
del self.form.fields["test__engagement"]
2252+
if "test" in self.form.fields:
2253+
del self.form.fields["test"]
2254+
elif self.eid is not None:
2255+
# Engagement context: filter finding groups by engagement
2256+
if "test__engagement__product" in self.form.fields:
2257+
del self.form.fields["test__engagement__product"]
2258+
if "test__engagement__product__prod_type" in self.form.fields:
2259+
del self.form.fields["test__engagement__product__prod_type"]
2260+
if "test__engagement" in self.form.fields:
2261+
del self.form.fields["test__engagement"]
2262+
# Filter tests by engagement - get_authorized_tests doesn't support engagement param
2263+
engagement = Engagement.objects.filter(id=self.eid).select_related("product").first()
2264+
if engagement:
2265+
self.form.fields["test"].queryset = get_authorized_tests(Permissions.Test_View, product=engagement.product).filter(engagement_id=self.eid).prefetch_related("test_type")
2266+
elif self.pid is not None:
2267+
# Product context: filter finding groups by product
2268+
if "test__engagement__product" in self.form.fields:
2269+
del self.form.fields["test__engagement__product"]
2270+
if "test__engagement__product__prod_type" in self.form.fields:
2271+
del self.form.fields["test__engagement__product__prod_type"]
21822272
# TODO: add authorized check to be sure
2183-
self.form.fields["test__engagement"].queryset = Engagement.objects.filter(
2184-
product_id=self.pid,
2185-
).all()
2186-
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)
2273+
if "test__engagement" in self.form.fields:
2274+
self.form.fields["test__engagement"].queryset = Engagement.objects.filter(
2275+
product_id=self.pid,
2276+
).all()
2277+
if "test" in self.form.fields:
2278+
self.form.fields["test"].queryset = get_authorized_tests(Permissions.Test_View, product=self.pid).prefetch_related("test_type")
21882279
else:
2280+
# Global context: show all authorized finding groups
21892281
self.form.fields[
21902282
"test__engagement__product__prod_type"].queryset = get_authorized_product_types(Permissions.Product_Type_View)
21912283
self.form.fields["test__engagement"].queryset = get_authorized_engagements(Permissions.Engagement_View)
2192-
del self.form.fields["test"]
2284+
if "test" in self.form.fields:
2285+
del self.form.fields["test"]
21932286

21942287
if self.form.fields.get("test__engagement__product"):
21952288
self.form.fields["test__engagement__product"].queryset = get_authorized_products(Permissions.Product_View)
21962289
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)
2290+
self.form.fields["finding_group"].queryset = get_authorized_finding_groups_for_queryset(Permissions.Finding_Group_View, finding_group_query, user=self.user)
21982291
self.form.fields["reporter"].queryset = get_authorized_users(Permissions.Finding_View)
21992292
self.form.fields["reviewers"].queryset = self.form.fields["reporter"].queryset
22002293

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)