Skip to content

Commit 1c10771

Browse files
committed
add triage_records action on product REST API
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent f13b087 commit 1c10771

2 files changed

Lines changed: 72 additions & 0 deletions

File tree

product_portfolio/api.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
from product_portfolio.models import ProductPolicyViolation
5252
from product_portfolio.models import ScanCodeProject
5353
from vulnerabilities.api import VulnerabilityAnalysisSerializer
54+
from vulnerabilities.triage.models import TriageRecord
5455

5556
base_extra_kwargs = {
5657
"licenses": {
@@ -363,6 +364,24 @@ class Meta:
363364
)
364365

365366

367+
class TriageRecordSerializer(serializers.ModelSerializer):
368+
advisory_id = serializers.ReadOnlyField(source="vulnerability.advisory_id")
369+
ruleset = serializers.ReadOnlyField(source="ruleset.name")
370+
request = serializers.StringRelatedField()
371+
372+
class Meta:
373+
model = TriageRecord
374+
fields = (
375+
"advisory_id",
376+
"ruleset",
377+
"recommended_action",
378+
"matched_rules",
379+
"request",
380+
"detected_date",
381+
"last_checked",
382+
)
383+
384+
366385
class ProductViewSet(
367386
ObjectPermissionsMixin,
368387
SendAboutFilesMixin,
@@ -441,6 +460,16 @@ def policy_violations(self, request, uuid):
441460
serializer = ProductPolicyViolationSerializer(violations, many=True)
442461
return Response(serializer.data)
443462

463+
@action(detail=True, url_path="triage_records")
464+
def triage_records(self, request, uuid):
465+
"""List active triage recommendations for this product, one per vulnerability."""
466+
product = self.get_object()
467+
records = product.triage_records.highest_precedence().select_related(
468+
"vulnerability", "ruleset", "request"
469+
)
470+
serializer = TriageRecordSerializer(records, many=True)
471+
return Response(serializer.data)
472+
444473
@action(detail=True, methods=["post"], serializer_class=LoadSBOMsFormSerializer)
445474
def load_sboms(self, request, *args, **kwargs):
446475
"""

product_portfolio/tests/test_api.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from component_catalog.models import Component
2525
from component_catalog.models import ComponentKeyword
2626
from component_catalog.models import Package
27+
from component_catalog.tests import make_package
2728
from dje.models import Dataspace
2829
from dje.models import History
2930
from dje.tests import MaxQueryMixin
@@ -47,8 +48,13 @@
4748
from product_portfolio.models import ProductRelationStatus
4849
from product_portfolio.models import ProductStatus
4950
from product_portfolio.models import ScanCodeProject
51+
from product_portfolio.tests import make_product_package
5052
from vulnerabilities.tests import make_vulnerability
5153
from vulnerabilities.tests import make_vulnerability_analysis
54+
from vulnerabilities.triage.engine import evaluate_ruleset
55+
from vulnerabilities.triage.models import TriageAction
56+
from vulnerabilities.triage.tests import make_product_triage_ruleset
57+
from vulnerabilities.triage.tests import make_triage_ruleset
5258

5359

5460
class ProductAPITestCase(MaxQueryMixin, TestCase):
@@ -758,6 +764,43 @@ def test_api_product_endpoint_policy_violations_action(self):
758764
self.assertEqual(status.HTTP_200_OK, response.status_code)
759765
self.assertEqual([], response.data)
760766

767+
def test_api_product_endpoint_triage_records_action(self):
768+
url = reverse("api_v2:product-triage-records", args=[self.product1.uuid])
769+
770+
self.client.login(username=self.base_user.username, password="secret")
771+
response = self.client.get(url)
772+
self.assertEqual(status.HTTP_404_NOT_FOUND, response.status_code)
773+
774+
add_perm(self.base_user, "add_product")
775+
assign_perm("view_product", self.base_user, self.product1)
776+
777+
response = self.client.get(url)
778+
self.assertEqual(status.HTTP_200_OK, response.status_code)
779+
self.assertEqual([], response.data)
780+
781+
package = make_package(self.dataspace)
782+
make_product_package(self.product1, package=package)
783+
vulnerability = make_vulnerability(self.dataspace, affecting=package, risk_score=9.0)
784+
ruleset = make_triage_ruleset(
785+
self.dataspace,
786+
recommended_action=TriageAction.UPGRADE,
787+
rules_config={"risk_score": {"is_active": True, "min_risk_score": 8.0}},
788+
)
789+
make_product_triage_ruleset(self.product1, ruleset=ruleset)
790+
evaluate_ruleset(ruleset, self.product1)
791+
792+
response = self.client.get(url)
793+
self.assertEqual(status.HTTP_200_OK, response.status_code)
794+
self.assertEqual(1, len(response.data))
795+
entry = response.data[0]
796+
self.assertEqual(vulnerability.advisory_id, entry["advisory_id"])
797+
self.assertEqual(ruleset.name, entry["ruleset"])
798+
self.assertEqual(TriageAction.UPGRADE, entry["recommended_action"])
799+
self.assertEqual(["risk_score"], entry["matched_rules"])
800+
self.assertIsNone(entry["request"])
801+
self.assertIn("detected_date", entry)
802+
self.assertIn("last_checked", entry)
803+
761804

762805
class ProductRelatedAPITestCase(TestCase):
763806
def setUp(self):

0 commit comments

Comments
 (0)