Skip to content

Commit 6138824

Browse files
Prevent reimport from reactivating duplicate findings as active/verified
Fixes #14910. process_matched_mitigated_finding reactivated a matched mitigated finding without checking whether it is a duplicate, producing an invalid active/verified duplicate state that the finding edit form rejects. Keep duplicates inactive/unverified on reactivation (un-mitigate only), matching the set_duplicate invariant.
1 parent ae0ea54 commit 6138824

2 files changed

Lines changed: 117 additions & 5 deletions

File tree

dojo/importers/default_reimporter.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -773,9 +773,16 @@ def process_matched_mitigated_finding(
773773
existing_finding.mitigated = None
774774
existing_finding.is_mitigated = False
775775
existing_finding.mitigated_by = None
776-
existing_finding.active = True
777-
if self.verified is not None:
778-
existing_finding.verified = self.verified
776+
# A duplicate finding must stay inactive/unverified (see set_duplicate and the
777+
# "Duplicate findings cannot be verified or active" form validation). Un-mitigate it
778+
# but do not reactivate it, otherwise we create an invalid active/verified duplicate state.
779+
if existing_finding.duplicate:
780+
existing_finding.active = False
781+
existing_finding.verified = False
782+
else:
783+
existing_finding.active = True
784+
if self.verified is not None:
785+
existing_finding.verified = self.verified
779786

780787
component_name = getattr(unsaved_finding, "component_name", None)
781788
component_version = getattr(unsaved_finding, "component_version", None)
@@ -791,7 +798,11 @@ def process_matched_mitigated_finding(
791798
# don't dedupe before endpoints/locations are added, postprocessing will be done on next save (in calling method)
792799
existing_finding.save_no_options()
793800

794-
note = Notes(entry=f"Re-activated by {self.scan_type} re-upload.", author=self.user)
801+
if existing_finding.duplicate:
802+
note_entry = f"Un-mitigated by {self.scan_type} re-upload but kept inactive because the finding is a duplicate."
803+
else:
804+
note_entry = f"Re-activated by {self.scan_type} re-upload."
805+
note = Notes(entry=note_entry, author=self.user)
795806
note.save()
796807
self.location_handler.record_reactivations_for_finding(existing_finding)
797808
existing_finding.notes.add(note)

unittests/test_importers_importer.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -932,4 +932,105 @@ def test_change_vulnerability_ids_on_reimport(self):
932932
# Verify only new Vulnerability_Id objects exist
933933
vuln_ids = list(Vulnerability_Id.objects.filter(finding=finding).values_list("vulnerability_id", flat=True))
934934
self.assertEqual(set(new_vulnerability_ids), set(vuln_ids))
935-
finding.delete()
935+
936+
937+
class ReimportDuplicateReactivationTest(DojoTestCase):
938+
939+
"""
940+
Regression test for https://github.com/DefectDojo/django-DefectDojo/issues/14910
941+
942+
Reimport reactivation of a mitigated finding must not produce an invalid
943+
active/verified duplicate finding state.
944+
"""
945+
946+
def setUp(self):
947+
self.user, _ = User.objects.get_or_create(username="admin", is_superuser=True)
948+
Development_Environment.objects.get_or_create(name="Development")
949+
self.product_type, _ = Product_Type.objects.get_or_create(name="dup_reactivation_pt")
950+
self.product, _ = Product.objects.get_or_create(
951+
name="DupReactivationProduct",
952+
description="test product",
953+
prod_type=self.product_type,
954+
)
955+
self.engagement = Engagement.objects.create(
956+
name="Dup Reactivation Engagement",
957+
product=self.product,
958+
target_start=timezone.now(),
959+
target_end=timezone.now(),
960+
)
961+
self.test = self.create_test(engagement=self.engagement, scan_type=NPM_AUDIT_SCAN_TYPE, title="dup reactivation test")
962+
963+
def _make_finding(self, title, **kwargs):
964+
return Finding.objects.create(
965+
title=title,
966+
test=self.test,
967+
severity="High",
968+
reporter=self.user,
969+
**kwargs,
970+
)
971+
972+
def test_reactivation_keeps_duplicate_inactive_and_unverified(self):
973+
# Original active finding
974+
original = self._make_finding("original finding", active=True, verified=True)
975+
# Mitigated finding that is marked as a duplicate of the original
976+
existing_duplicate = self._make_finding(
977+
"duplicate finding",
978+
active=False,
979+
verified=False,
980+
duplicate=True,
981+
duplicate_finding=original,
982+
is_mitigated=True,
983+
mitigated=timezone.now(),
984+
mitigated_by=self.user,
985+
)
986+
# The reimported (unsaved) finding that re-matches the duplicate, and is active/not mitigated
987+
unsaved_finding = self._make_finding("duplicate finding incoming", active=True, verified=True)
988+
989+
reimporter = DefaultReImporter(
990+
test=self.test,
991+
user=self.user,
992+
scan_type=NPM_AUDIT_SCAN_TYPE,
993+
active=True,
994+
verified=True,
995+
do_not_reactivate=False,
996+
)
997+
998+
result_finding, _ = reimporter.process_matched_mitigated_finding(unsaved_finding, existing_duplicate)
999+
1000+
result_finding.refresh_from_db()
1001+
# The mitigation is cleared (the finding reappeared in the scan)...
1002+
self.assertFalse(result_finding.is_mitigated)
1003+
self.assertIsNone(result_finding.mitigated)
1004+
# ...but a duplicate must never become active or verified (issue #14910)
1005+
self.assertTrue(result_finding.duplicate)
1006+
self.assertFalse(result_finding.active)
1007+
self.assertFalse(result_finding.verified)
1008+
1009+
def test_reactivation_of_non_duplicate_still_activates(self):
1010+
# A regular mitigated finding (not a duplicate) must still reactivate as before
1011+
existing = self._make_finding(
1012+
"regular finding",
1013+
active=False,
1014+
verified=False,
1015+
is_mitigated=True,
1016+
mitigated=timezone.now(),
1017+
mitigated_by=self.user,
1018+
)
1019+
unsaved_finding = self._make_finding("regular finding incoming", active=True, verified=True)
1020+
1021+
reimporter = DefaultReImporter(
1022+
test=self.test,
1023+
user=self.user,
1024+
scan_type=NPM_AUDIT_SCAN_TYPE,
1025+
active=True,
1026+
verified=True,
1027+
do_not_reactivate=False,
1028+
)
1029+
1030+
result_finding, _ = reimporter.process_matched_mitigated_finding(unsaved_finding, existing)
1031+
1032+
result_finding.refresh_from_db()
1033+
self.assertFalse(result_finding.is_mitigated)
1034+
self.assertIsNone(result_finding.mitigated)
1035+
self.assertTrue(result_finding.active)
1036+
self.assertTrue(result_finding.verified)

0 commit comments

Comments
 (0)