Skip to content

Commit 02a0b2e

Browse files
closed finding metrics: use mitigated_date instead of created_date
1 parent 26fba7f commit 02a0b2e

4 files changed

Lines changed: 126 additions & 11 deletions

File tree

dojo/fixtures/unit_metrics_additional_data.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@
175175
"description": "TEST finding",
176176
"mitigated_by": null,
177177
"reporter": 2,
178-
"mitigated": null,
178+
"mitigated": "2018-01-02T00:00:00Z",
179179
"active": false,
180180
"line": 100,
181181
"under_review": false,
@@ -416,7 +416,7 @@
416416
"description": "test finding",
417417
"mitigated_by": null,
418418
"reporter": 1,
419-
"mitigated": null,
419+
"mitigated": "2017-12-28T00:00:00Z",
420420
"active": true,
421421
"line": 123,
422422
"under_review": false,

dojo/metrics/utils.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,13 @@ def finding_queries(
7676

7777
# Filter by the date ranges supplied
7878
all_findings_within_date_range = all_authorized_findings.filter(date__range=[start_date, end_date])
79-
# Get the list of closed and risk accepted findings
80-
closed_filtered_findings = all_findings_within_date_range.filter(CLOSED_FINDINGS_QUERY)
79+
# Get the list of closed findings filtered by mitigated date (not discovery date)
80+
# This ensures findings closed within the date range are included even if discovered outside it
81+
closed_filtered_findings = all_authorized_findings.filter(
82+
CLOSED_FINDINGS_QUERY,
83+
mitigated__range=[start_date, end_date],
84+
mitigated__isnull=False,
85+
)
8186
accepted_filtered_findings = all_findings_within_date_range.filter(ACCEPTED_FINDINGS_QUERY)
8287
active_filtered_findings = all_findings_within_date_range.filter(OPEN_FINDINGS_QUERY)
8388

dojo/product/views.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,8 @@ def finding_queries(request, prod):
438438
filters["new_verified"] = findings_qs.filter(finding_helper.VERIFIED_FINDINGS_QUERY).filter(date__range=[start_date, end_date]).order_by("date")
439439
filters["open"] = findings_qs.filter(finding_helper.OPEN_FINDINGS_QUERY).filter(date__range=[start_date, end_date]).order_by("date")
440440
filters["inactive"] = findings_qs.filter(finding_helper.INACTIVE_FINDINGS_QUERY).filter(date__range=[start_date, end_date]).order_by("date")
441-
filters["closed"] = findings_qs.filter(finding_helper.CLOSED_FINDINGS_QUERY).filter(date__range=[start_date, end_date]).order_by("date")
441+
# Filter closed findings by mitigated date (not discovery date) to show findings closed within the date range
442+
filters["closed"] = findings_qs.filter(finding_helper.CLOSED_FINDINGS_QUERY).filter(mitigated__range=[start_date, end_date], mitigated__isnull=False).order_by("mitigated")
442443
filters["false_positive"] = findings_qs.filter(finding_helper.FALSE_POSITIVE_FINDINGS_QUERY).filter(date__range=[start_date, end_date]).order_by("date")
443444
filters["out_of_scope"] = findings_qs.filter(finding_helper.OUT_OF_SCOPE_FINDINGS_QUERY).filter(date__range=[start_date, end_date]).order_by("date")
444445
filters["all"] = findings_qs.order_by("date")
@@ -610,7 +611,8 @@ def view_product_metrics(request, pid):
610611

611612
all_findings = list(filters.get("all", []).values("id", "date", "severity"))
612613
open_findings = list(filters.get("open", []).values("id", "date", "mitigated", "severity"))
613-
closed_findings = list(filters.get("closed", []).values("id", "date", "severity"))
614+
# Include mitigated date for closed findings to group by when they were closed, not discovered
615+
closed_findings = list(filters.get("closed", []).values("id", "date", "mitigated", "severity"))
614616
accepted_findings = list(filters.get("accepted", []).values("id", "date", "severity"))
615617

616618
"""
@@ -681,11 +683,29 @@ def view_product_metrics(request, pid):
681683
if open_objs_by_severity.get(finding.get("severity")) is not None:
682684
open_objs_by_severity[finding.get("severity")] += 1
683685

684-
# Close findings
686+
# Close findings - group by mitigated date, not discovery date
685687
elif closed_findings_dict.get(finding.get("id", None)):
686-
if unix_timestamp in open_close_weekly:
688+
# Find the closed finding to get its mitigated date
689+
closed_finding = next((f for f in closed_findings if f.get("id") == finding.get("id")), None)
690+
if closed_finding and closed_finding.get("mitigated"):
691+
# Use mitigated date for grouping closed findings
692+
mitigated_date = closed_finding.get("mitigated")
693+
mitigated_date_only = mitigated_date.date() if isinstance(mitigated_date, datetime) else mitigated_date
694+
iso_cal = mitigated_date_only.isocalendar()
695+
mitigated_week_start = iso_to_gregorian(iso_cal[0], iso_cal[1], 1)
696+
mitigated_html_date = mitigated_week_start.strftime("<span class='small'>%m/%d<br/>%Y</span>")
697+
mitigated_unix_timestamp = (tcalendar.timegm(mitigated_week_start.timetuple()) * 1000)
698+
699+
if mitigated_unix_timestamp in open_close_weekly:
700+
open_close_weekly[mitigated_unix_timestamp]["closed"] += 1
701+
else:
702+
open_close_weekly[mitigated_unix_timestamp] = {"closed": 1, "open": 0, "accepted": 0}
703+
open_close_weekly[mitigated_unix_timestamp]["week"] = mitigated_html_date
704+
elif unix_timestamp in open_close_weekly:
705+
# Fallback to discovery date if mitigated date is not available
687706
open_close_weekly[unix_timestamp]["closed"] += 1
688707
else:
708+
# Fallback to discovery date if mitigated date is not available
689709
open_close_weekly[unix_timestamp] = {"closed": 1, "open": 0, "accepted": 0}
690710
open_close_weekly[unix_timestamp]["week"] = html_date
691711
# Optimization: count severity level on server side

unittests/test_metrics_queries.py

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from django.urls import reverse
99

1010
from dojo.metrics import utils
11-
from dojo.models import Product_Type, User
11+
from dojo.models import Engagement, Finding, Product, Product_Type, Test, User
1212

1313
from .dojo_test_case import DojoTestCase
1414

@@ -31,12 +31,12 @@ def add(*args, **kwargs):
3131
FINDING_8 = {"id": 240, "date": date(2018, 1, 1), "severity": "High", "active": True, "verified": False, "false_p": False, "duplicate": True, "duplicate_finding_id": 2, "out_of_scope": False, "risk_accepted": False, "under_review": False, "is_mitigated": False, "mitigated": None, "mitigated_by_id": None, "reporter_id": 1, "numerical_severity": "S0"}
3232
FINDING_9 = {"id": 241, "date": date(2018, 1, 1), "severity": "High", "active": False, "verified": False, "false_p": False, "duplicate": True, "duplicate_finding_id": 2, "out_of_scope": False, "risk_accepted": True, "under_review": False, "is_mitigated": False, "mitigated": None, "mitigated_by_id": None, "reporter_id": 1, "numerical_severity": "S0"}
3333
FINDING_10 = {"id": 242, "date": date(2018, 1, 1), "severity": "High", "active": False, "verified": False, "false_p": False, "duplicate": True, "duplicate_finding_id": 2, "out_of_scope": False, "risk_accepted": True, "under_review": False, "is_mitigated": False, "mitigated": None, "mitigated_by_id": None, "reporter_id": 1, "numerical_severity": "S0"}
34-
FINDING_11 = {"id": 243, "date": date(2017, 12, 31), "severity": "High", "active": False, "verified": False, "false_p": False, "duplicate": False, "duplicate_finding_id": None, "out_of_scope": False, "risk_accepted": True, "under_review": False, "is_mitigated": True, "mitigated": None, "mitigated_by_id": None, "reporter_id": 2, "numerical_severity": "S0"}
34+
FINDING_11 = {"id": 243, "date": date(2017, 12, 31), "severity": "High", "active": False, "verified": False, "false_p": False, "duplicate": False, "duplicate_finding_id": None, "out_of_scope": False, "risk_accepted": True, "under_review": False, "is_mitigated": True, "mitigated": datetime(2018, 1, 2, tzinfo=zoneinfo.ZoneInfo("UTC")), "mitigated_by_id": None, "reporter_id": 2, "numerical_severity": "S0"}
3535
FINDING_12 = {"id": 244, "date": date(2017, 12, 29), "severity": "Low", "active": True, "verified": True, "false_p": False, "duplicate": False, "duplicate_finding_id": None, "out_of_scope": False, "risk_accepted": False, "under_review": False, "is_mitigated": False, "mitigated": None, "mitigated_by_id": None, "reporter_id": 1, "numerical_severity": "S0"}
3636
FINDING_13 = {"id": 245, "date": date(2017, 12, 27), "severity": "Low", "active": False, "verified": False, "false_p": False, "duplicate": True, "duplicate_finding_id": 22, "out_of_scope": False, "risk_accepted": False, "under_review": False, "is_mitigated": False, "mitigated": None, "mitigated_by_id": None, "reporter_id": 1, "numerical_severity": "S0"}
3737
FINDING_14 = {"id": 246, "date": date(2018, 1, 2), "severity": "Low", "active": False, "verified": False, "false_p": False, "duplicate": True, "duplicate_finding_id": 22, "out_of_scope": False, "risk_accepted": False, "under_review": False, "is_mitigated": False, "mitigated": None, "mitigated_by_id": None, "reporter_id": 1, "numerical_severity": "S0"}
3838
FINDING_15 = {"id": 247, "date": date(2018, 1, 3), "severity": "Low", "active": False, "verified": False, "false_p": False, "duplicate": True, "duplicate_finding_id": None, "out_of_scope": False, "risk_accepted": False, "under_review": False, "is_mitigated": False, "mitigated": None, "mitigated_by_id": None, "reporter_id": 1, "numerical_severity": "S0"}
39-
FINDING_16 = {"id": 248, "date": date(2017, 12, 27), "severity": "Low", "active": True, "verified": True, "false_p": False, "duplicate": False, "duplicate_finding_id": None, "out_of_scope": False, "risk_accepted": False, "under_review": False, "is_mitigated": True, "mitigated": None, "mitigated_by_id": None, "reporter_id": 1, "numerical_severity": "S0"}
39+
FINDING_16 = {"id": 248, "date": date(2017, 12, 27), "severity": "Low", "active": True, "verified": True, "false_p": False, "duplicate": False, "duplicate_finding_id": None, "out_of_scope": False, "risk_accepted": False, "under_review": False, "is_mitigated": True, "mitigated": datetime(2017, 12, 28, tzinfo=zoneinfo.ZoneInfo("UTC")), "mitigated_by_id": None, "reporter_id": 1, "numerical_severity": "S0"}
4040
FINDING_17 = {"id": 249, "date": date(2018, 1, 4), "severity": "Low", "active": False, "verified": False, "false_p": False, "duplicate": True, "duplicate_finding_id": 224, "out_of_scope": False, "risk_accepted": False, "under_review": False, "is_mitigated": False, "mitigated": None, "mitigated_by_id": None, "reporter_id": 1, "numerical_severity": "S0"}
4141

4242

@@ -163,6 +163,96 @@ def test_finding_queries(self, mock_timezone):
163163
self.assertIsInstance(finding_queries["start_date"], datetime)
164164
self.assertIsInstance(finding_queries["end_date"], datetime)
165165

166+
@patch("django.utils.timezone.now")
167+
def test_closed_findings_filtered_by_mitigated_date(self, mock_timezone):
168+
"""
169+
Test that closed findings are filtered by mitigated date, not discovery date.
170+
171+
This test verifies the fix for issue #9735: findings discovered outside the date
172+
range but closed within it should appear in closed metrics.
173+
"""
174+
mock_datetime = datetime(2020, 12, 9, tzinfo=zoneinfo.ZoneInfo("UTC"))
175+
mock_timezone.return_value = mock_datetime
176+
177+
# Get a test product and engagement
178+
product = Product.objects.first()
179+
if not product:
180+
self.skipTest("No product available in test data")
181+
engagement = Engagement.objects.filter(product=product).first()
182+
if not engagement:
183+
self.skipTest("No engagement available in test data")
184+
test = Test.objects.filter(engagement=engagement).first()
185+
if not test:
186+
self.skipTest("No test available in test data")
187+
188+
# Create a finding discovered BEFORE the date range but closed WITHIN it
189+
# Date range: 2017-12-26 to 2018-01-05
190+
finding_discovered_before = Finding.objects.create(
191+
title="Finding discovered before range, closed within range",
192+
description="Test finding",
193+
severity="High",
194+
date=date(2017, 10, 1), # Discovered before range
195+
test=test,
196+
reporter=self.request.user,
197+
active=False,
198+
is_mitigated=True,
199+
mitigated=datetime(2018, 1, 2, tzinfo=zoneinfo.ZoneInfo("UTC")), # Closed within range
200+
)
201+
202+
# Create a finding discovered WITHIN the date range but closed AFTER it
203+
finding_closed_after = Finding.objects.create(
204+
title="Finding discovered within range, closed after range",
205+
description="Test finding",
206+
severity="Medium",
207+
date=date(2017, 12, 30), # Discovered within range
208+
test=test,
209+
reporter=self.request.user,
210+
active=False,
211+
is_mitigated=True,
212+
mitigated=datetime(2018, 2, 1, tzinfo=zoneinfo.ZoneInfo("UTC")), # Closed after range
213+
)
214+
215+
# Create a finding discovered and closed WITHIN the date range
216+
finding_both_within = Finding.objects.create(
217+
title="Finding discovered and closed within range",
218+
description="Test finding",
219+
severity="Low",
220+
date=date(2017, 12, 30), # Discovered within range
221+
test=test,
222+
reporter=self.request.user,
223+
active=False,
224+
is_mitigated=True,
225+
mitigated=datetime(2018, 1, 3, tzinfo=zoneinfo.ZoneInfo("UTC")), # Closed within range
226+
)
227+
228+
try:
229+
product_types = []
230+
finding_queries = utils.finding_queries(
231+
product_types,
232+
self.request,
233+
)
234+
235+
closed_findings = finding_queries["closed"]
236+
closed_ids = list(closed_findings.values_list("id", flat=True))
237+
238+
# The finding discovered before but closed within should appear
239+
self.assertIn(finding_discovered_before.id, closed_ids,
240+
"Finding discovered before range but closed within range should appear in closed metrics")
241+
242+
# The finding discovered within but closed after should NOT appear
243+
self.assertNotIn(finding_closed_after.id, closed_ids,
244+
"Finding discovered within range but closed after range should NOT appear in closed metrics")
245+
246+
# The finding discovered and closed within should appear
247+
self.assertIn(finding_both_within.id, closed_ids,
248+
"Finding discovered and closed within range should appear in closed metrics")
249+
250+
finally:
251+
# Clean up test findings
252+
finding_discovered_before.delete()
253+
finding_closed_after.delete()
254+
finding_both_within.delete()
255+
166256

167257
class EndpointQueriesTest(DojoTestCase):
168258
fixtures = ["dojo_testdata.json"]

0 commit comments

Comments
 (0)