Skip to content

Commit 56d455a

Browse files
devGregAclaude
andauthored
Expose effective dedupe matching policy on the Test API (2/3) (#15151)
* 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent e702f12 commit 56d455a

2 files changed

Lines changed: 102 additions & 0 deletions

File tree

dojo/test/api/serializer.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,23 @@
1313

1414
class TestSerializer(serializers.ModelSerializer):
1515
test_type_name = serializers.ReadOnlyField()
16+
# Effective finding-matching policy for this test, resolved from the
17+
# per-scanner settings (DEDUPLICATION_ALGORITHM_PER_PARSER /
18+
# HASHCODE_FIELDS_PER_SCANNER). Read-only: surfaced so users can see
19+
# which algorithm and fields deduplication and reimport matching use,
20+
# instead of having to ask support or read settings.dist.py.
21+
# Typed fields (not ReadOnlyField) so drf-spectacular can emit an exact schema.
22+
deduplication_algorithm = serializers.CharField(
23+
read_only=True,
24+
help_text="Algorithm used to match findings for deduplication and reimport "
25+
"(legacy, unique_id_from_tool, hash_code, or unique_id_from_tool_or_hash_code).")
26+
hash_code_fields = serializers.ListField(
27+
child=serializers.CharField(),
28+
read_only=True,
29+
allow_null=True,
30+
help_text="Finding fields hashed to compute hash_code for this test's scan type. "
31+
"Null when the scan type has no per-scanner configuration and legacy "
32+
"default fields are used.")
1633

1734
class Meta:
1835
model = Test
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import logging
2+
3+
from django.conf import settings
4+
from django.utils import timezone
5+
from rest_framework.authtoken.models import Token
6+
7+
from dojo.importers.default_importer import DefaultImporter
8+
from dojo.models import Development_Environment, Engagement, Product, Product_Type, User
9+
10+
from .dojo_test_case import DojoAPITestCase, get_unit_tests_scans_path
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
class TestDedupePolicyOnTestAPI(DojoAPITestCase):
16+
17+
"""
18+
The Test API exposes the effective finding-matching policy
19+
(deduplication_algorithm + hash_code_fields) so users can see which
20+
algorithm and fields matching uses without reading settings.dist.py.
21+
"""
22+
23+
scan_type = "Acunetix Scan"
24+
25+
def setUp(self):
26+
user, _ = User.objects.get_or_create(username="admin", defaults={"is_superuser": True, "is_staff": True})
27+
token, _ = Token.objects.get_or_create(user=user)
28+
self.client.credentials(HTTP_AUTHORIZATION=f"Token {token.key}")
29+
30+
product_type, _ = Product_Type.objects.get_or_create(name="dedupe-policy-api")
31+
environment, _ = Development_Environment.objects.get_or_create(name="Development")
32+
product, _ = Product.objects.get_or_create(
33+
name="TestDedupePolicyAPI",
34+
description="Test",
35+
prod_type=product_type,
36+
)
37+
engagement, _ = Engagement.objects.get_or_create(
38+
name="dedupe policy api",
39+
product=product,
40+
target_start=timezone.now(),
41+
target_end=timezone.now(),
42+
)
43+
import_options = {
44+
"user": user,
45+
"lead": user,
46+
"scan_date": None,
47+
"environment": environment,
48+
"active": True,
49+
"verified": False,
50+
"engagement": engagement,
51+
"scan_type": self.scan_type,
52+
}
53+
with (get_unit_tests_scans_path("acunetix") / "one_finding.xml").open(encoding="utf-8") as scan:
54+
importer = DefaultImporter(close_old_findings=False, **import_options)
55+
self.test, _, _, _, _, _, _ = importer.process_scan(scan, force_sync=True)
56+
57+
def test_matching_policy_is_exposed_read_only(self):
58+
response = self.client.get(f"/api/v2/tests/{self.test.id}/", format="json")
59+
self.assertEqual(200, response.status_code, response.content)
60+
data = response.json()
61+
62+
# values mirror the per-scanner settings, so assert against them
63+
# dynamically rather than hardcoding the current config
64+
expected_algorithm = settings.DEDUPLICATION_ALGORITHM_PER_PARSER.get(
65+
self.scan_type, settings.DEDUPE_ALGO_LEGACY)
66+
expected_fields = settings.HASHCODE_FIELDS_PER_SCANNER.get(self.scan_type)
67+
68+
self.assertIn("deduplication_algorithm", data)
69+
self.assertIn("hash_code_fields", data)
70+
self.assertEqual(expected_algorithm, data["deduplication_algorithm"])
71+
self.assertEqual(expected_fields, data["hash_code_fields"])
72+
73+
def test_matching_policy_ignored_on_write(self):
74+
response = self.client.patch(
75+
f"/api/v2/tests/{self.test.id}/",
76+
{"deduplication_algorithm": "hash_code", "hash_code_fields": ["title"], "description": "updated"},
77+
format="json",
78+
)
79+
self.assertEqual(200, response.status_code, response.content)
80+
# read-only fields are silently ignored; effective policy still mirrors settings
81+
data = self.client.get(f"/api/v2/tests/{self.test.id}/", format="json").json()
82+
expected_algorithm = settings.DEDUPLICATION_ALGORITHM_PER_PARSER.get(
83+
self.scan_type, settings.DEDUPE_ALGO_LEGACY)
84+
self.assertEqual(expected_algorithm, data["deduplication_algorithm"])
85+
self.assertEqual("updated", data["description"])

0 commit comments

Comments
 (0)