diff --git a/dojo/test/api/serializer.py b/dojo/test/api/serializer.py index c5dda0409a8..7ce8f5613c6 100644 --- a/dojo/test/api/serializer.py +++ b/dojo/test/api/serializer.py @@ -13,6 +13,23 @@ 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. + # 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.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.") 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"])