Skip to content

Commit 5e4110b

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 98005cf commit 5e4110b

File tree

4 files changed

+357
-27
lines changed

4 files changed

+357
-27
lines changed

dojo/filters.py

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

20182018

2019+
def get_finding_group_queryset_for_context(pid=None, eid=None, tid=None):
2020+
"""
2021+
Helper function to build finding group queryset based on context hierarchy.
2022+
Context priority: test > engagement > product > global
2023+
2024+
Args:
2025+
pid: Product ID (least specific)
2026+
eid: Engagement ID
2027+
tid: Test ID (most specific)
2028+
2029+
Returns:
2030+
QuerySet of Finding_Group filtered by context
2031+
"""
2032+
if tid is not None:
2033+
# Most specific: filter by test
2034+
return Finding_Group.objects.filter(test_id=tid).only("id", "name")
2035+
if eid is not None:
2036+
# Filter by engagement's tests
2037+
return Finding_Group.objects.filter(test__engagement_id=eid).only("id", "name")
2038+
if pid is not None:
2039+
# Filter by product's tests
2040+
return Finding_Group.objects.filter(test__engagement__product_id=pid).only("id", "name")
2041+
# Global: return all (authorization will be applied separately)
2042+
return Finding_Group.objects.all().only("id", "name")
2043+
2044+
20192045
class FindingFilterWithoutObjectLookups(FindingFilterHelper, FindingTagStringFilter):
20202046
test__engagement__product__prod_type = NumberFilter(widget=HiddenInput())
20212047
test__engagement__product = NumberFilter(widget=HiddenInput())
@@ -2107,20 +2133,45 @@ class Meta:
21072133
def __init__(self, *args, **kwargs):
21082134
self.user = None
21092135
self.pid = None
2136+
self.eid = None
2137+
self.tid = None
21102138
if "user" in kwargs:
21112139
self.user = kwargs.pop("user")
21122140

21132141
if "pid" in kwargs:
21142142
self.pid = kwargs.pop("pid")
2143+
if "eid" in kwargs:
2144+
self.eid = kwargs.pop("eid")
2145+
if "tid" in kwargs:
2146+
self.tid = kwargs.pop("tid")
21152147
super().__init__(*args, **kwargs)
21162148
# Set some date fields
21172149
self.set_date_fields(*args, **kwargs)
2118-
# Don't show the product filter on the product finding view
2119-
if self.pid:
2120-
del self.form.fields["test__engagement__product__name"]
2121-
del self.form.fields["test__engagement__product__name_contains"]
2122-
del self.form.fields["test__engagement__product__prod_type__name"]
2123-
del self.form.fields["test__engagement__product__prod_type__name_contains"]
2150+
# Don't show the product/engagement/test filter fields when in specific context
2151+
if self.tid or self.eid or self.pid:
2152+
if "test__engagement__product__name" in self.form.fields:
2153+
del self.form.fields["test__engagement__product__name"]
2154+
if "test__engagement__product__name_contains" in self.form.fields:
2155+
del self.form.fields["test__engagement__product__name_contains"]
2156+
if "test__engagement__product__prod_type__name" in self.form.fields:
2157+
del self.form.fields["test__engagement__product__prod_type__name"]
2158+
if "test__engagement__product__prod_type__name_contains" in self.form.fields:
2159+
del self.form.fields["test__engagement__product__prod_type__name_contains"]
2160+
# Also hide engagement and test fields if in test or engagement context
2161+
if self.tid:
2162+
if "test__engagement__name" in self.form.fields:
2163+
del self.form.fields["test__engagement__name"]
2164+
if "test__engagement__name_contains" in self.form.fields:
2165+
del self.form.fields["test__engagement__name_contains"]
2166+
if "test__name" in self.form.fields:
2167+
del self.form.fields["test__name"]
2168+
if "test__name_contains" in self.form.fields:
2169+
del self.form.fields["test__name_contains"]
2170+
elif self.eid:
2171+
if "test__engagement__name" in self.form.fields:
2172+
del self.form.fields["test__engagement__name"]
2173+
if "test__engagement__name_contains" in self.form.fields:
2174+
del self.form.fields["test__engagement__name_contains"]
21242175

21252176

21262177
class FindingFilter(FindingFilterHelper, FindingTagFilter):
@@ -2159,29 +2210,57 @@ class Meta:
21592210
def __init__(self, *args, **kwargs):
21602211
self.user = None
21612212
self.pid = None
2213+
self.eid = None
2214+
self.tid = None
21622215
if "user" in kwargs:
21632216
self.user = kwargs.pop("user")
21642217

21652218
if "pid" in kwargs:
21662219
self.pid = kwargs.pop("pid")
2220+
if "eid" in kwargs:
2221+
self.eid = kwargs.pop("eid")
2222+
if "tid" in kwargs:
2223+
self.tid = kwargs.pop("tid")
21672224
super().__init__(*args, **kwargs)
21682225
# Set some date fields
21692226
self.set_date_fields(*args, **kwargs)
21702227
# Don't show the product filter on the product finding view
21712228
self.set_related_object_fields(*args, **kwargs)
21722229

21732230
def set_related_object_fields(self, *args: list, **kwargs: dict):
2174-
finding_group_query = Finding_Group.objects.all()
2175-
if self.pid is not None:
2231+
# Use helper to get contextual finding group queryset
2232+
finding_group_query = get_finding_group_queryset_for_context(
2233+
pid=self.pid,
2234+
eid=self.eid,
2235+
tid=self.tid,
2236+
)
2237+
2238+
# Filter by most specific context: test > engagement > product
2239+
if self.tid is not None:
2240+
# Test context: filter finding groups by test
2241+
del self.form.fields["test__engagement__product"]
2242+
del self.form.fields["test__engagement__product__prod_type"]
2243+
del self.form.fields["test__engagement"]
2244+
del self.form.fields["test"]
2245+
elif self.eid is not None:
2246+
# Engagement context: filter finding groups by engagement
2247+
del self.form.fields["test__engagement__product"]
2248+
del self.form.fields["test__engagement__product__prod_type"]
2249+
del self.form.fields["test__engagement"]
2250+
# Filter tests by engagement - get_authorized_tests doesn't support engagement param
2251+
engagement = Engagement.objects.get(id=self.eid)
2252+
self.form.fields["test"].queryset = get_authorized_tests(Permissions.Test_View, product=engagement.product).filter(engagement_id=self.eid).prefetch_related("test_type")
2253+
elif self.pid is not None:
2254+
# Product context: filter finding groups by product
21762255
del self.form.fields["test__engagement__product"]
21772256
del self.form.fields["test__engagement__product__prod_type"]
21782257
# TODO: add authorized check to be sure
21792258
self.form.fields["test__engagement"].queryset = Engagement.objects.filter(
21802259
product_id=self.pid,
21812260
).all()
21822261
self.form.fields["test"].queryset = get_authorized_tests(Permissions.Test_View, product=self.pid).prefetch_related("test_type")
2183-
finding_group_query = Finding_Group.objects.filter(test__engagement__product_id=self.pid)
21842262
else:
2263+
# Global context: show all authorized finding groups
21852264
self.form.fields[
21862265
"test__engagement__product__prod_type"].queryset = get_authorized_product_types(Permissions.Product_Type_View)
21872266
self.form.fields["test__engagement"].queryset = get_authorized_engagements(Permissions.Engagement_View)
@@ -2190,7 +2269,7 @@ def set_related_object_fields(self, *args: list, **kwargs: dict):
21902269
if self.form.fields.get("test__engagement__product"):
21912270
self.form.fields["test__engagement__product"].queryset = get_authorized_products(Permissions.Product_View)
21922271
if self.form.fields.get("finding_group", None):
2193-
self.form.fields["finding_group"].queryset = get_authorized_finding_groups_for_queryset(Permissions.Finding_Group_View, finding_group_query)
2272+
self.form.fields["finding_group"].queryset = get_authorized_finding_groups_for_queryset(Permissions.Finding_Group_View, finding_group_query, user=self.user)
21942273
self.form.fields["reporter"].queryset = get_authorized_users(Permissions.Finding_View)
21952274
self.form.fields["reviewers"].queryset = self.form.fields["reporter"].queryset
21962275

dojo/finding/views.py

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

271273
filter_string_matching = get_system_setting("filter_string_matching", False)
@@ -359,10 +361,11 @@ def add_breadcrumbs(self, request: HttpRequest, context: dict):
359361

360362
return request, context
361363

362-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
363-
# Store the product and engagement ids
364+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
365+
# Store the product, engagement, and test ids
364366
self.product_id = product_id
365367
self.engagement_id = engagement_id
368+
self.test_id = test_id
366369
# Get the initial context
367370
request, context = self.get_initial_context(request)
368371
# Get the filtered findings
@@ -385,46 +388,46 @@ def get(self, request: HttpRequest, product_id: int | None = None, engagement_id
385388

386389

387390
class ListOpenFindings(ListFindings):
388-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
391+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
389392
self.filter_name = "Open"
390-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
393+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
391394

392395

393396
class ListVerifiedFindings(ListFindings):
394-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
397+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
395398
self.filter_name = "Verified"
396-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
399+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
397400

398401

399402
class ListOutOfScopeFindings(ListFindings):
400-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
403+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
401404
self.filter_name = "Out of Scope"
402-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
405+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
403406

404407

405408
class ListFalsePositiveFindings(ListFindings):
406-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
409+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
407410
self.filter_name = "False Positive"
408-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
411+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
409412

410413

411414
class ListInactiveFindings(ListFindings):
412-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
415+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
413416
self.filter_name = "Inactive"
414-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
417+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
415418

416419

417420
class ListAcceptedFindings(ListFindings):
418-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
421+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
419422
self.filter_name = "Accepted"
420-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
423+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
421424

422425

423426
class ListClosedFindings(ListFindings):
424-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
427+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
425428
self.filter_name = "Closed"
426429
self.order_by = "-mitigated"
427-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
430+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
428431

429432

430433
class ViewFinding(View):

dojo/test/views.py

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

0 commit comments

Comments
 (0)