diff --git a/dojo/finding/helper.py b/dojo/finding/helper.py index 95021e9575c..a1c7993d30f 100644 --- a/dojo/finding/helper.py +++ b/dojo/finding/helper.py @@ -1,5 +1,6 @@ import logging from contextlib import suppress +from datetime import datetime from time import strftime from django.conf import settings @@ -9,6 +10,7 @@ from django.dispatch.dispatcher import receiver from django.urls import reverse from django.utils import timezone +from django.utils.timezone import is_naive, make_aware, now from fieldsignals import pre_save_changed import dojo.jira_link.helper as jira_helper @@ -740,6 +742,17 @@ def save_vulnerability_ids_template(finding_template, vulnerability_ids): finding_template.cve = None +def normalize_datetime(value): + """Ensure value is timezone-aware datetime.""" + if value: + if not isinstance(value, datetime): + value = datetime.combine(value, datetime.min.time()) + # Make timezone-aware if naive + if is_naive(value): + value = make_aware(value) + return value + + def close_finding( *, finding, @@ -761,15 +774,16 @@ def close_finding( """ # Core status updates finding.is_mitigated = is_mitigated - now = timezone.now() - finding.mitigated = mitigated or now + current_time = now() + mitigated_date = normalize_datetime(mitigated) or current_time + finding.mitigated = mitigated_date finding.mitigated_by = mitigated_by or user finding.active = False finding.false_p = bool(false_p) finding.out_of_scope = bool(out_of_scope) finding.duplicate = bool(duplicate) finding.under_review = False - finding.last_reviewed = finding.mitigated + finding.last_reviewed = mitigated_date finding.last_reviewed_by = user # Create note if provided @@ -779,16 +793,16 @@ def close_finding( entry=note_entry, author=user, note_type=note_type, - date=finding.mitigated, + date=mitigated_date, ) finding.notes.add(new_note) # Endpoint statuses for status in finding.status_finding.all(): status.mitigated_by = finding.mitigated_by - status.mitigated_time = finding.mitigated + status.mitigated_time = mitigated_date status.mitigated = True - status.last_modified = timezone.now() + status.last_modified = current_time status.save() # Risk acceptance diff --git a/unittests/test_finding_model.py b/unittests/test_finding_model.py index bc65ffd1096..6de9e4847fc 100644 --- a/unittests/test_finding_model.py +++ b/unittests/test_finding_model.py @@ -1,8 +1,23 @@ -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta from crum import impersonate - -from dojo.models import DojoMeta, Engagement, Finding, Test, User +from django.utils.timezone import is_naive, now + +from dojo.finding.helper import close_finding +from dojo.models import ( + DojoMeta, + Endpoint, + Endpoint_Status, + Engagement, + Finding, + Note_Type, + Notes, + Product, + Product_Type, + Test, + Test_Type, + User, +) from .dojo_test_case import DojoTestCase @@ -10,6 +25,79 @@ class TestFindingModel(DojoTestCase): fixtures = ["dojo_testdata.json"] + def setUp(self): + self.user = User.objects.first() # Use a user from fixtures + self.product_type = Product_Type.objects.create(name="Test Product Type") + self.product = Product.objects.create(name="Test Product", prod_type=self.product_type) + self.engagement = Engagement.objects.create( + name="Test Engagement", + product=self.product, + target_start=now(), + target_end=now(), + ) + self.test_type = Test_Type.objects.create(name="Unit Test Type") + self.test = Test.objects.create( + engagement=self.engagement, + test_type=self.test_type, + title="Test for Finding", + target_start=now(), + target_end=now(), + ) + self.finding = Finding.objects.create(title="Close Finding Test", active=True, test=self.test) + self.endpoint = Endpoint.objects.create(host="test.local") + self.endpoint_status = Endpoint_Status.objects.create(finding=self.finding, endpoint=self.endpoint) + self.finding.status_finding.add(self.endpoint_status) + + def test_close_finding_with_naive_date(self): + note_type_obj = Note_Type.objects.create(name="General") + naive_date = date.today() # No timezone + close_finding( + finding=self.finding, + user=self.user, + is_mitigated=True, + mitigated=naive_date, + mitigated_by=None, + false_p=False, + out_of_scope=False, + duplicate=False, + note_entry="Mitigation note", + note_type=note_type_obj, + ) + self.assertFalse(is_naive(self.finding.mitigated)) + note = Notes.objects.filter(finding=self.finding).first() + self.assertIsNotNone(note) + self.assertFalse(is_naive(note.date)) + status = Endpoint_Status.objects.filter(finding=self.finding).first() + self.assertTrue(status.mitigated) + self.assertFalse(is_naive(status.mitigated_time)) + + def test_close_finding_with_naive_datetime(self): + naive_datetime = datetime(2025, 11, 12, 0, 0, 0) + close_finding( + finding=self.finding, + user=self.user, + is_mitigated=True, + mitigated=naive_datetime, + mitigated_by=None, + false_p=False, + out_of_scope=False, + duplicate=False, + ) + self.assertFalse(is_naive(self.finding.mitigated)) + + def test_close_finding_with_none_mitigated(self): + close_finding( + finding=self.finding, + user=self.user, + is_mitigated=True, + mitigated=None, + mitigated_by=None, + false_p=False, + out_of_scope=False, + duplicate=False, + ) + self.assertFalse(is_naive(self.finding.mitigated)) + def test_get_sast_source_file_path_with_link_no_file_path(self): finding = Finding() self.assertEqual(None, finding.get_sast_source_file_path_with_link())