Skip to content

Commit 463ccd2

Browse files
committed
move evaluation in a signal
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent 034b5ad commit 463ccd2

5 files changed

Lines changed: 51 additions & 8 deletions

File tree

product_portfolio/tests/test_views.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4416,7 +4416,7 @@ def test_post_without_change_perm_returns_404(self):
44164416
response = self.client.post(url)
44174417
self.assertEqual(404, response.status_code)
44184418

4419-
@patch("product_portfolio.views.evaluate_ruleset")
4419+
@patch("vulnerabilities.triage.signals.evaluate_ruleset")
44204420
def test_post_assigns_the_submitted_rulesets(self, mock_evaluate):
44214421
self.client.login(username="nexb_user", password="secret")
44224422
url = self.product1.get_manage_triage_rulesets_url()
@@ -4429,11 +4429,12 @@ def test_post_assigns_the_submitted_rulesets(self, mock_evaluate):
44294429
)
44304430
mock_evaluate.assert_called_once_with(ruleset=self.ruleset, product=self.product1)
44314431

4432-
@patch("product_portfolio.views.evaluate_ruleset")
4432+
@patch("vulnerabilities.triage.signals.evaluate_ruleset")
44334433
def test_post_unassigns_the_deselected_rulesets(self, mock_evaluate):
44344434
ProductTriageRuleset.objects.create(
44354435
product=self.product1, ruleset=self.ruleset, dataspace=self.dataspace
44364436
)
4437+
mock_evaluate.reset_mock()
44374438
self.client.login(username="nexb_user", password="secret")
44384439
url = self.product1.get_manage_triage_rulesets_url()
44394440
response = self.client.post(url, {"ruleset_uuids": []})

product_portfolio/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,6 @@
150150
from vulnerabilities.models import Vulnerability
151151
from vulnerabilities.models import VulnerabilityAnalysis
152152
from vulnerabilities.models import get_risk_level
153-
from vulnerabilities.triage.engine import evaluate_ruleset
154153
from vulnerabilities.triage.models import AnalysisPreset
155154
from vulnerabilities.triage.models import ProductTriageRuleset
156155
from vulnerabilities.triage.models import TriageAction
@@ -2253,13 +2252,14 @@ def manage_triage_rulesets_view(request, dataspace, name, version=""):
22532252
for ruleset in available_rulesets:
22542253
ruleset_uuid = str(ruleset.uuid)
22552254
if ruleset_uuid in submitted_uuids and ruleset_uuid not in current_assignments:
2255+
# The evaluate_on_assign signal evaluates the ruleset against the product;
2256+
# wrapping in atomic() rolls the assignment back if that evaluation fails.
22562257
with transaction.atomic():
22572258
ProductTriageRuleset.objects.create(
22582259
product=product,
22592260
ruleset=ruleset,
22602261
dataspace=product.dataspace,
22612262
)
2262-
evaluate_ruleset(ruleset=ruleset, product=product)
22632263
for ruleset_uuid, assignment in current_assignments.items():
22642264
if ruleset_uuid not in submitted_uuids:
22652265
assignment.delete()

vulnerabilities/triage/rules.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515

1616
from policy.rules import BaseRule
1717

18-
# Not shared with policy.rules.TERMINAL_VULNERABILITY_STATES: the two lists are
19-
# intentionally scoped to their own engine and are not guaranteed to stay identical.
2018
TRIAGE_TERMINAL_VULNERABILITY_STATES = [
2119
"resolved",
2220
"resolved_with_pedigree",

vulnerabilities/triage/signals.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ def reevaluate_or_delete_on_ruleset_save(sender, instance, created, **kwargs):
4242
evaluate_ruleset(ruleset=instance, product=assignment.product)
4343

4444

45+
@receiver(post_save, sender="vulnerabilities_triage.ProductTriageRuleset")
46+
def evaluate_on_assign(sender, instance, created, **kwargs):
47+
"""Evaluate the ruleset against the product as soon as it is assigned."""
48+
if not created or not instance.ruleset.enabled:
49+
return
50+
51+
evaluate_ruleset(ruleset=instance.ruleset, product=instance.product)
52+
53+
4554
@receiver(post_delete, sender="vulnerabilities_triage.ProductTriageRuleset")
4655
def delete_triage_records_on_unassign(sender, instance, **kwargs):
4756
"""Delete triage records and associated preset analyses when a ruleset is de-assigned."""
@@ -66,8 +75,9 @@ def reevaluate_on_analysis_change(sender, instance, **kwargs):
6675
"""Re-evaluate triage when an analysis state or reachability is updated."""
6776
signal = kwargs.get("signal")
6877
if signal == post_save and instance.applied_by_preset_id:
69-
return # Written by the triage engine itself -- re-evaluating would loop
70-
# When a human explicitly deletes their analysis, skip preset application to avoid
78+
return # Written by the triage engine itself, re-evaluating would loop
79+
80+
# When an user explicitly deletes their analysis, skip preset application to avoid
7181
# having the engine immediately recreate it.
7282
is_human_delete = signal == post_delete and not instance.applied_by_preset_id
7383
reevaluate_product_rulesets(

vulnerabilities/triage/tests/test_signals.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def setUp(self):
4242
def test_evaluates_every_enabled_ruleset_assigned_to_the_product(self, mock_evaluate):
4343
ruleset = make_triage_ruleset(self.dataspace, enabled=True)
4444
make_product_triage_ruleset(self.product, ruleset=ruleset)
45+
mock_evaluate.reset_mock() # Called once already by the evaluate_on_assign signal.
4546

4647
reevaluate_product_rulesets(self.product)
4748

@@ -62,6 +63,7 @@ def test_skips_disabled_ruleset_assignments(self, mock_evaluate):
6263
def test_apply_preset_flag_is_forwarded(self, mock_evaluate):
6364
ruleset = make_triage_ruleset(self.dataspace, enabled=True)
6465
make_product_triage_ruleset(self.product, ruleset=ruleset)
66+
mock_evaluate.reset_mock() # Called once already by the evaluate_on_assign signal.
6567

6668
reevaluate_product_rulesets(self.product, apply_preset=False)
6769

@@ -196,6 +198,38 @@ def test_disabling_then_reenabling_a_ruleset_reuses_the_existing_request(self):
196198
self.assertEqual(original_request, TriageRecord.objects.get().request)
197199

198200

201+
class EvaluateOnAssignSignalTestCase(TestCase):
202+
def setUp(self):
203+
self.dataspace = Dataspace.objects.create(name="nexB")
204+
self.product = make_product(self.dataspace)
205+
206+
@patch("vulnerabilities.triage.signals.evaluate_ruleset")
207+
def test_assigning_an_enabled_ruleset_evaluates_it(self, mock_evaluate):
208+
ruleset = make_triage_ruleset(self.dataspace, enabled=True)
209+
210+
make_product_triage_ruleset(self.product, ruleset=ruleset)
211+
212+
mock_evaluate.assert_called_once_with(ruleset=ruleset, product=self.product)
213+
214+
@patch("vulnerabilities.triage.signals.evaluate_ruleset")
215+
def test_assigning_a_disabled_ruleset_does_not_evaluate(self, mock_evaluate):
216+
ruleset = make_triage_ruleset(self.dataspace, enabled=False)
217+
218+
make_product_triage_ruleset(self.product, ruleset=ruleset)
219+
220+
mock_evaluate.assert_not_called()
221+
222+
@patch("vulnerabilities.triage.signals.evaluate_ruleset")
223+
def test_resaving_an_existing_assignment_does_not_reevaluate(self, mock_evaluate):
224+
ruleset = make_triage_ruleset(self.dataspace, enabled=True)
225+
assignment = make_product_triage_ruleset(self.product, ruleset=ruleset)
226+
mock_evaluate.reset_mock()
227+
228+
assignment.save()
229+
230+
mock_evaluate.assert_not_called()
231+
232+
199233
class DeleteTriageRecordsOnUnassignSignalTestCase(TestCase):
200234
def setUp(self):
201235
self.dataspace = Dataspace.objects.create(name="nexB")

0 commit comments

Comments
 (0)