Skip to content

Commit 178b098

Browse files
Maffoochclaude
andcommitted
test(dedupe): add Fortify unique_id repro fixtures + order-independence test
Two .fpr fixtures contain findings that share a unique_id_from_tool but differ in content, in opposite document order. Under unique_id_from_tool dedup the surviving original must be the same finding regardless of the scanner's export order (asserted by test_fortify_same_uid_different_content_original_is_order_independent). Fails without the ordering fix (surviving finding flips with file order), passes with it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ae8c325 commit 178b098

3 files changed

Lines changed: 69 additions & 0 deletions

File tree

23.3 KB
Binary file not shown.
23.3 KB
Binary file not shown.

unittests/test_deduplication_logic.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from crum import impersonate
88
from django.conf import settings
99
from django.core import serializers
10+
from django.test import override_settings
1011
from django.utils import timezone
1112

1213
from dojo.finding.deduplication import (
@@ -24,10 +25,12 @@
2425
Engagement,
2526
Finding,
2627
Product,
28+
Product_Type,
2729
System_Settings,
2830
Test,
2931
Test_Import,
3032
Test_Import_Finding_Action,
33+
Test_Type,
3134
User,
3235
copy_model_util,
3336
)
@@ -617,6 +620,72 @@ def test_deduplication_ordering_key_is_order_independent(self):
617620
backward = sorted([f_b, f_a], key=deduplication_ordering_key)
618621
self.assertEqual([f.id for f in forward], [f.id for f in backward])
619622

623+
def _dedupe_fortify_repro_scan(self, fpr_filename, engagement_name):
624+
"""
625+
Import one corrected Fortify repro .fpr into its own (isolated) engagement
626+
under a shared product, using unique_id_from_tool deduplication, and return
627+
the single surviving non-duplicate finding.
628+
629+
The two fixtures contain the same pair of findings - which share a
630+
unique_id_from_tool but have different content (different rule/title/line) -
631+
in opposite document order.
632+
"""
633+
from dojo.tools.fortify.parser import FortifyParser # noqa: PLC0415
634+
635+
product_type, _ = Product_Type.objects.get_or_create(name="Fortify UID Repro PT")
636+
product, _ = Product.objects.get_or_create(
637+
name="Fortify UID Repro Product",
638+
defaults={"description": "Fortify UID dedupe repro", "prod_type": product_type},
639+
)
640+
test_type, _ = Test_Type.objects.get_or_create(name="Fortify Scan")
641+
engagement = Engagement.objects.create(
642+
name=engagement_name,
643+
product=product,
644+
target_start=timezone.now().date(),
645+
target_end=timezone.now().date(),
646+
deduplication_on_engagement=True, # isolate each scan's dedupe scope
647+
)
648+
test = Test.objects.create(
649+
engagement=engagement,
650+
test_type=test_type,
651+
scan_type="Fortify Scan",
652+
target_start=timezone.now(),
653+
target_end=timezone.now(),
654+
)
655+
with (get_unit_tests_scans_path("fortify") / fpr_filename).open(encoding="utf-8") as testfile:
656+
parsed_findings = FortifyParser().get_findings(testfile, test)
657+
658+
admin = User.objects.get(username="admin")
659+
saved = []
660+
for parsed in parsed_findings:
661+
parsed.test = test
662+
parsed.reporter = admin
663+
parsed.save(dedupe_option=False) # defer dedupe to the batch call below
664+
saved.append(parsed)
665+
666+
dedupe_batch_of_findings(get_finding_models_for_deduplication([f.id for f in saved]))
667+
668+
refreshed = [Finding.objects.get(id=f.id) for f in saved]
669+
non_duplicates = [f for f in refreshed if not f.duplicate]
670+
# unique_id_from_tool dedupe collapses the shared-uid pair to one original
671+
self.assertEqual(len(non_duplicates), 1, f"expected exactly one original, got {[(f.id, f.line) for f in refreshed]}")
672+
return non_duplicates[0]
673+
674+
@override_settings(DEDUPLICATION_ALGORITHM_PER_PARSER={"Fortify Scan": settings.DEDUPE_ALGO_UNIQUE_ID_FROM_TOOL})
675+
def test_fortify_same_uid_different_content_original_is_order_independent(self):
676+
# Corrected Fortify import-order repro. Two findings share unique_id_from_tool
677+
# but differ in content, provided in opposite order across two .fpr files.
678+
# Under unique_id_from_tool dedupe the surviving "original" must be the same
679+
# (content-canonical) finding regardless of the scanner's export order.
680+
# Pre-fix this flips (lowest-id / first-in-file wins); with the fix it is stable.
681+
original_scan1 = self._dedupe_fortify_repro_scan("fortify_dedupe_uid_repro_scan1.fpr", "fortify-uid-repro-scan1")
682+
original_scan2 = self._dedupe_fortify_repro_scan("fortify_dedupe_uid_repro_scan2.fpr", "fortify-uid-repro-scan2")
683+
684+
self.assertEqual(original_scan1.unique_id_from_tool, original_scan2.unique_id_from_tool)
685+
# The same content survives regardless of file order (line/title stable).
686+
self.assertEqual(original_scan1.line, original_scan2.line)
687+
self.assertEqual(original_scan1.title, original_scan2.title)
688+
620689
def test_identical_except_title_hash_code(self):
621690
# 4 is already a duplicate of 2, let's see what happens if we create an identical finding with different title (and reset status)
622691
# expect: NOT marked as duplicate as title is part of hash_code calculation

0 commit comments

Comments
 (0)