From f562825cd9c75f7b4a914b16c0cfc82920391617 Mon Sep 17 00:00:00 2001 From: Greg Anderson Date: Fri, 3 Jul 2026 17:19:27 -0600 Subject: [PATCH 1/2] Expose effective dedupe matching policy on the Test API Adds two read-only fields to TestSerializer, backed by the existing Test model properties that the importer and dedupe machinery already use on every import: - deduplication_algorithm: legacy, unique_id_from_tool, hash_code, or unique_id_from_tool_or_hash_code - hash_code_fields: the finding fields hashed for this test's scan type (null when the scan type has no per-scanner configuration and legacy default fields apply) "Which fields are compared exactly?" is a recurring support question: matching behavior differs per scanner and the answer currently lives in settings.dist.py where users cannot see it. No new logic - this only surfaces what the system already computes, making the effective policy visible via the API and available for UIs to render. Co-Authored-By: Claude Fable 5 --- dojo/test/api/serializer.py | 12 +++ unittests/test_apiv2_test_dedupe_policy.py | 85 ++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 unittests/test_apiv2_test_dedupe_policy.py diff --git a/dojo/test/api/serializer.py b/dojo/test/api/serializer.py index c5dda0409a8..d7b7101c079 100644 --- a/dojo/test/api/serializer.py +++ b/dojo/test/api/serializer.py @@ -13,6 +13,18 @@ class TestSerializer(serializers.ModelSerializer): test_type_name = serializers.ReadOnlyField() + # Effective finding-matching policy for this test, resolved from the + # per-scanner settings (DEDUPLICATION_ALGORITHM_PER_PARSER / + # HASHCODE_FIELDS_PER_SCANNER). Read-only: surfaced so users can see + # which algorithm and fields deduplication and reimport matching use, + # instead of having to ask support or read settings.dist.py. + deduplication_algorithm = serializers.ReadOnlyField( + help_text="Algorithm used to match findings for deduplication and reimport " + "(legacy, unique_id_from_tool, hash_code, or unique_id_from_tool_or_hash_code).") + hash_code_fields = serializers.ReadOnlyField( + help_text="Finding fields hashed to compute hash_code for this test's scan type. " + "Null when the scan type has no per-scanner configuration and legacy " + "default fields are used.") class Meta: model = Test diff --git a/unittests/test_apiv2_test_dedupe_policy.py b/unittests/test_apiv2_test_dedupe_policy.py new file mode 100644 index 00000000000..4e037847487 --- /dev/null +++ b/unittests/test_apiv2_test_dedupe_policy.py @@ -0,0 +1,85 @@ +import logging + +from django.conf import settings +from django.utils import timezone +from rest_framework.authtoken.models import Token + +from dojo.importers.default_importer import DefaultImporter +from dojo.models import Development_Environment, Engagement, Product, Product_Type, User + +from .dojo_test_case import DojoAPITestCase, get_unit_tests_scans_path + +logger = logging.getLogger(__name__) + + +class TestDedupePolicyOnTestAPI(DojoAPITestCase): + + """ + The Test API exposes the effective finding-matching policy + (deduplication_algorithm + hash_code_fields) so users can see which + algorithm and fields matching uses without reading settings.dist.py. + """ + + scan_type = "Acunetix Scan" + + def setUp(self): + user, _ = User.objects.get_or_create(username="admin", defaults={"is_superuser": True, "is_staff": True}) + token, _ = Token.objects.get_or_create(user=user) + self.client.credentials(HTTP_AUTHORIZATION=f"Token {token.key}") + + product_type, _ = Product_Type.objects.get_or_create(name="dedupe-policy-api") + environment, _ = Development_Environment.objects.get_or_create(name="Development") + product, _ = Product.objects.get_or_create( + name="TestDedupePolicyAPI", + description="Test", + prod_type=product_type, + ) + engagement, _ = Engagement.objects.get_or_create( + name="dedupe policy api", + product=product, + target_start=timezone.now(), + target_end=timezone.now(), + ) + import_options = { + "user": user, + "lead": user, + "scan_date": None, + "environment": environment, + "active": True, + "verified": False, + "engagement": engagement, + "scan_type": self.scan_type, + } + with (get_unit_tests_scans_path("acunetix") / "one_finding.xml").open(encoding="utf-8") as scan: + importer = DefaultImporter(close_old_findings=False, **import_options) + self.test, _, _, _, _, _, _ = importer.process_scan(scan, force_sync=True) + + def test_matching_policy_is_exposed_read_only(self): + response = self.client.get(f"/api/v2/tests/{self.test.id}/", format="json") + self.assertEqual(200, response.status_code, response.content) + data = response.json() + + # values mirror the per-scanner settings, so assert against them + # dynamically rather than hardcoding the current config + expected_algorithm = settings.DEDUPLICATION_ALGORITHM_PER_PARSER.get( + self.scan_type, settings.DEDUPE_ALGO_LEGACY) + expected_fields = settings.HASHCODE_FIELDS_PER_SCANNER.get(self.scan_type) + + self.assertIn("deduplication_algorithm", data) + self.assertIn("hash_code_fields", data) + self.assertEqual(expected_algorithm, data["deduplication_algorithm"]) + self.assertEqual(expected_fields, data["hash_code_fields"]) + + def test_matching_policy_ignored_on_write(self): + response = self.client.patch( + f"/api/v2/tests/{self.test.id}/", + {"deduplication_algorithm": "hash_code", "hash_code_fields": ["title"], "description": "updated"}, + format="json", + ) + self.assertEqual(200, response.status_code, response.content) + # read-only fields are silently ignored; effective policy still mirrors settings + data = self.client.get(f"/api/v2/tests/{self.test.id}/", format="json").json() + expected_algorithm = settings.DEDUPLICATION_ALGORITHM_PER_PARSER.get( + self.scan_type, settings.DEDUPE_ALGO_LEGACY) + self.assertEqual(expected_algorithm, data["deduplication_algorithm"]) + self.assertEqual("updated", data["description"]) From 4e71a280764ce581bd61a714496f42a4d0c983fa Mon Sep 17 00:00:00 2001 From: Greg Anderson Date: Fri, 3 Jul 2026 21:38:43 -0600 Subject: [PATCH 2/2] Use typed serializer fields so schema generation stays warning-free ReadOnlyField cannot be introspected by drf-spectacular (the CI schema check runs --fail-on-warn), so declare the matching-policy fields with their real types: CharField for deduplication_algorithm and a nullable ListField of strings for hash_code_fields. Co-Authored-By: Claude Fable 5 --- dojo/test/api/serializer.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dojo/test/api/serializer.py b/dojo/test/api/serializer.py index d7b7101c079..7ce8f5613c6 100644 --- a/dojo/test/api/serializer.py +++ b/dojo/test/api/serializer.py @@ -18,10 +18,15 @@ class TestSerializer(serializers.ModelSerializer): # HASHCODE_FIELDS_PER_SCANNER). Read-only: surfaced so users can see # which algorithm and fields deduplication and reimport matching use, # instead of having to ask support or read settings.dist.py. - deduplication_algorithm = serializers.ReadOnlyField( + # Typed fields (not ReadOnlyField) so drf-spectacular can emit an exact schema. + deduplication_algorithm = serializers.CharField( + read_only=True, help_text="Algorithm used to match findings for deduplication and reimport " "(legacy, unique_id_from_tool, hash_code, or unique_id_from_tool_or_hash_code).") - hash_code_fields = serializers.ReadOnlyField( + hash_code_fields = serializers.ListField( + child=serializers.CharField(), + read_only=True, + allow_null=True, help_text="Finding fields hashed to compute hash_code for this test's scan type. " "Null when the scan type has no per-scanner configuration and legacy " "default fields are used.")