@@ -932,4 +932,115 @@ 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+ # These accumulators are normally initialised inside process_findings(); set them
998+ # here because the test drives process_matched_mitigated_finding() directly.
999+ reimporter .new_items = []
1000+ reimporter .reactivated_items = []
1001+ reimporter .unchanged_items = []
1002+
1003+ result_finding , _ = reimporter .process_matched_mitigated_finding (unsaved_finding , existing_duplicate )
1004+
1005+ result_finding .refresh_from_db ()
1006+ # The mitigation is cleared (the finding reappeared in the scan)...
1007+ self .assertFalse (result_finding .is_mitigated )
1008+ self .assertIsNone (result_finding .mitigated )
1009+ # ...but a duplicate must never become active or verified (issue #14910)
1010+ self .assertTrue (result_finding .duplicate )
1011+ self .assertFalse (result_finding .active )
1012+ self .assertFalse (result_finding .verified )
1013+
1014+ def test_reactivation_of_non_duplicate_still_activates (self ):
1015+ # A regular mitigated finding (not a duplicate) must still reactivate as before
1016+ existing = self ._make_finding (
1017+ "regular finding" ,
1018+ active = False ,
1019+ verified = False ,
1020+ is_mitigated = True ,
1021+ mitigated = timezone .now (),
1022+ mitigated_by = self .user ,
1023+ )
1024+ unsaved_finding = self ._make_finding ("regular finding incoming" , active = True , verified = True )
1025+
1026+ reimporter = DefaultReImporter (
1027+ test = self .test ,
1028+ user = self .user ,
1029+ scan_type = NPM_AUDIT_SCAN_TYPE ,
1030+ active = True ,
1031+ verified = True ,
1032+ do_not_reactivate = False ,
1033+ )
1034+ # These accumulators are normally initialised inside process_findings(); set them
1035+ # here because the test drives process_matched_mitigated_finding() directly.
1036+ reimporter .new_items = []
1037+ reimporter .reactivated_items = []
1038+ reimporter .unchanged_items = []
1039+
1040+ result_finding , _ = reimporter .process_matched_mitigated_finding (unsaved_finding , existing )
1041+
1042+ result_finding .refresh_from_db ()
1043+ self .assertFalse (result_finding .is_mitigated )
1044+ self .assertIsNone (result_finding .mitigated )
1045+ self .assertTrue (result_finding .active )
1046+ self .assertTrue (result_finding .verified )
0 commit comments