Skip to content

Commit 136f54f

Browse files
Prevent reimport from reactivating duplicate findings as active/verified (#14935)
* 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. * Initialise reimporter accumulators in duplicate reactivation tests process_matched_mitigated_finding appends to self.reactivated_items, which is normally created in process_findings(). The tests drive the method directly, so set the accumulator lists explicitly.
1 parent bae53d1 commit 136f54f

2 files changed

Lines changed: 127 additions & 5 deletions

File tree

dojo/importers/default_reimporter.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -801,9 +801,16 @@ def process_matched_mitigated_finding(
801801
existing_finding.mitigated = None
802802
existing_finding.is_mitigated = False
803803
existing_finding.mitigated_by = None
804-
existing_finding.active = True
805-
if self.verified is not None:
806-
existing_finding.verified = self.verified
804+
# A duplicate finding must stay inactive/unverified (see set_duplicate and the
805+
# "Duplicate findings cannot be verified or active" form validation). Un-mitigate it
806+
# but do not reactivate it, otherwise we create an invalid active/verified duplicate state.
807+
if existing_finding.duplicate:
808+
existing_finding.active = False
809+
existing_finding.verified = False
810+
else:
811+
existing_finding.active = True
812+
if self.verified is not None:
813+
existing_finding.verified = self.verified
807814

808815
component_name = getattr(unsaved_finding, "component_name", None)
809816
component_version = getattr(unsaved_finding, "component_version", None)
@@ -819,7 +826,11 @@ def process_matched_mitigated_finding(
819826
# don't dedupe before endpoints/locations are added, postprocessing will be done on next save (in calling method)
820827
existing_finding.save_no_options()
821828

822-
note = Notes(entry=f"Re-activated by {self.scan_type} re-upload.", author=self.user)
829+
if existing_finding.duplicate:
830+
note_entry = f"Un-mitigated by {self.scan_type} re-upload but kept inactive because the finding is a duplicate."
831+
else:
832+
note_entry = f"Re-activated by {self.scan_type} re-upload."
833+
note = Notes(entry=note_entry, author=self.user)
823834
note.save()
824835
self.location_handler.record_reactivations_for_finding(existing_finding)
825836
existing_finding.notes.add(note)

unittests/test_importers_importer.py

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)