Skip to content

Commit 75a1390

Browse files
feat(finding): pluggable false-positive-history candidate filter
Add FINDING_FALSE_POSITIVE_HISTORY_CANDIDATE_FILTER_METHOD hook to do_false_positive_history_batch. When configured, the per-finding candidate list resolved by the deduplication algorithm is passed through the plugin method before the FP decision, letting a plugin (Pro) narrow candidates by fields excluded from the hash string but compared per pair (set-match tokens on vulnerability_ids / CWEs). Resolved once via get_custom_method; a no-op when unset, so default behavior is unchanged. Tests: hook can suppress matches (drops candidates -> no FP replication) and passthrough matches the default path.
1 parent d1673f3 commit 75a1390

2 files changed

Lines changed: 69 additions & 0 deletions

File tree

dojo/finding/deduplication.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,6 +1098,13 @@ def do_false_positive_history_batch(findings):
10981098
# Fetch all candidate existing findings with one DB query
10991099
candidates = _fetch_fp_candidates_for_batch(findings, product, dedup_alg)
11001100

1101+
# Optional plugin hook: refine the per-finding candidate list after it is resolved by
1102+
# deduplication_algorithm. Lets a plugin (e.g. Pro) narrow candidates by fields that are
1103+
# excluded from the hash string but compared per pair (set-match tokens on
1104+
# vulnerability_ids / CWEs). Resolved once; a no-op when unset. See get_custom_method.
1105+
from dojo.utils import get_custom_method # noqa: PLC0415 -- circular import
1106+
fp_candidate_filter = get_custom_method("FINDING_FALSE_POSITIVE_HISTORY_CANDIDATE_FILTER_METHOD")
1107+
11011108
to_mark_as_fp_ids: set = set()
11021109

11031110
for finding in findings:
@@ -1121,6 +1128,9 @@ def do_false_positive_history_batch(findings):
11211128
else:
11221129
existing = []
11231130

1131+
if fp_candidate_filter:
1132+
existing = fp_candidate_filter(finding, existing)
1133+
11241134
existing_fps = [ef for ef in existing if ef.false_p]
11251135

11261136
if existing_fps:

unittests/test_false_positive_history_logic.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from crum import impersonate
66
from django.conf import settings
7+
from django.test import override_settings
78

89
from dojo.finding.deduplication import do_false_positive_history_batch
910
from dojo.finding.ui.views import EditFinding
@@ -125,6 +126,24 @@
125126
# product 3: Security Podcast
126127

127128

129+
# Module-level candidate-filter hooks for FINDING_FALSE_POSITIVE_HISTORY_CANDIDATE_FILTER_METHOD.
130+
# get_custom_method resolves the setting to a dotted path and imports it, so these must be
131+
# importable at module scope.
132+
_fp_candidate_filter_calls = []
133+
134+
135+
def _drop_all_fp_candidates(finding, candidates):
136+
"""Hook that discards every candidate — simulates a plugin rejecting all matches."""
137+
_fp_candidate_filter_calls.append((finding, list(candidates)))
138+
return []
139+
140+
141+
def _passthrough_fp_candidates(finding, candidates):
142+
"""Hook that keeps every candidate — FP history must behave exactly as with no hook."""
143+
_fp_candidate_filter_calls.append((finding, list(candidates)))
144+
return candidates
145+
146+
128147
@versioned_fixtures
129148
class TestFalsePositiveHistoryLogic(DojoTestCase):
130149
fixtures = ["dojo_testdata.json"]
@@ -175,6 +194,46 @@ def test_fp_history_equal_hash_code_same_test(self):
175194
self.assert_finding(find_created_before_mark, false_p=True, not_pk=2, test_id=3, hash_code=find_2.hash_code)
176195
self.assert_finding(find_created_after_mark, false_p=True, not_pk=2, test_id=3, hash_code=find_2.hash_code)
177196

197+
# Candidate-filter hook (FINDING_FALSE_POSITIVE_HISTORY_CANDIDATE_FILTER_METHOD) #
198+
199+
@override_settings(
200+
FINDING_FALSE_POSITIVE_HISTORY_CANDIDATE_FILTER_METHOD="unittests.test_false_positive_history_logic._drop_all_fp_candidates",
201+
)
202+
def test_fp_history_hook_can_suppress_matches(self):
203+
# A plugin hook that drops all candidates must prevent FP replication even when
204+
# findings share a hash_code with an existing false-positive finding.
205+
_fp_candidate_filter_calls.clear()
206+
find_created_before_mark, find_2 = self.copy_and_reset_finding(find_id=2)
207+
find_created_before_mark.save()
208+
find_2 = Finding.objects.get(id=2)
209+
find_2.false_p = True
210+
find_2.save()
211+
find_created_after_mark, find_2 = self.copy_and_reset_finding(find_id=2)
212+
find_created_after_mark.save()
213+
# Hook discarded every candidate, so neither copy is marked despite the shared hash_code.
214+
self.assert_finding(find_created_before_mark, false_p=False, not_pk=2, test_id=3, hash_code=find_2.hash_code)
215+
self.assert_finding(find_created_after_mark, false_p=False, not_pk=2, test_id=3, hash_code=find_2.hash_code)
216+
# And the hook was actually invoked.
217+
self.assertTrue(_fp_candidate_filter_calls)
218+
219+
@override_settings(
220+
FINDING_FALSE_POSITIVE_HISTORY_CANDIDATE_FILTER_METHOD="unittests.test_false_positive_history_logic._passthrough_fp_candidates",
221+
)
222+
def test_fp_history_hook_passthrough_matches_default(self):
223+
# A passthrough hook must leave default FP-history behavior unchanged.
224+
_fp_candidate_filter_calls.clear()
225+
find_created_before_mark, find_2 = self.copy_and_reset_finding(find_id=2)
226+
find_created_before_mark.save()
227+
find_2 = Finding.objects.get(id=2)
228+
find_2.false_p = True
229+
find_2.save()
230+
find_created_after_mark, find_2 = self.copy_and_reset_finding(find_id=2)
231+
find_created_after_mark.save()
232+
# Identical outcome to test_fp_history_equal_hash_code_same_test — both copies marked.
233+
self.assert_finding(find_created_before_mark, false_p=True, not_pk=2, test_id=3, hash_code=find_2.hash_code)
234+
self.assert_finding(find_created_after_mark, false_p=True, not_pk=2, test_id=3, hash_code=find_2.hash_code)
235+
self.assertTrue(_fp_candidate_filter_calls)
236+
178237
# Finding 2 in Product 2, Engagement 1, Test 3
179238
def test_fp_history_equal_hash_code_same_test_non_retroactive(self):
180239
# Disable retroactive FP history

0 commit comments

Comments
 (0)