Skip to content

Commit 4e1762c

Browse files
committed
add REST API action for product policy violations
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent 4285ca7 commit 4e1762c

2 files changed

Lines changed: 71 additions & 0 deletions

File tree

product_portfolio/api.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from product_portfolio.models import ProductComponent
4949
from product_portfolio.models import ProductDependency
5050
from product_portfolio.models import ProductPackage
51+
from product_portfolio.models import ProductPolicyViolation
5152
from product_portfolio.models import ScanCodeProject
5253
from vulnerabilities.api import VulnerabilityAnalysisSerializer
5354

@@ -343,6 +344,25 @@ class Meta:
343344
)
344345

345346

347+
class ProductPolicyViolationSerializer(serializers.ModelSerializer):
348+
rule_label = serializers.ReadOnlyField()
349+
rule_description = serializers.ReadOnlyField()
350+
rule_severity = serializers.ReadOnlyField()
351+
352+
class Meta:
353+
model = ProductPolicyViolation
354+
fields = (
355+
"rule_type",
356+
"rule_label",
357+
"rule_description",
358+
"rule_severity",
359+
"violation_count",
360+
"detected_date",
361+
"resolved",
362+
"resolved_date",
363+
)
364+
365+
346366
class ProductViewSet(
347367
ObjectPermissionsMixin,
348368
SendAboutFilesMixin,
@@ -413,6 +433,14 @@ def imports(self, request, uuid):
413433
projects_data = ScanCodeProjectSerializer(scancode_projects, many=True).data
414434
return Response(projects_data)
415435

436+
@action(detail=True, url_path="policy_violations")
437+
def policy_violations(self, request, uuid):
438+
"""List active policy violations for this product, with rule details and counts."""
439+
product = self.get_object()
440+
violations = product.policy_violations.filter(resolved=False)
441+
serializer = ProductPolicyViolationSerializer(violations, many=True)
442+
return Response(serializer.data)
443+
416444
@action(detail=True, methods=["post"], serializer_class=LoadSBOMsFormSerializer)
417445
def load_sboms(self, request, *args, **kwargs):
418446
"""

product_portfolio/tests/test_api.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from product_portfolio.models import ProductComponent
4444
from product_portfolio.models import ProductItemPurpose
4545
from product_portfolio.models import ProductPackage
46+
from product_portfolio.models import ProductPolicyViolation
4647
from product_portfolio.models import ProductRelationStatus
4748
from product_portfolio.models import ProductStatus
4849
from product_portfolio.models import ScanCodeProject
@@ -717,6 +718,48 @@ def test_api_product_endpoint_manage_permissions_action(self):
717718
self.assertIn("errors", response.data)
718719

719720

721+
def test_api_product_endpoint_policy_violations_action(self):
722+
url = reverse("api_v2:product-policy-violations", args=[self.product1.uuid])
723+
724+
self.client.login(username=self.base_user.username, password="secret")
725+
response = self.client.get(url)
726+
self.assertEqual(status.HTTP_404_NOT_FOUND, response.status_code)
727+
728+
add_perm(self.base_user, "add_product")
729+
assign_perm("view_product", self.base_user, self.product1)
730+
731+
response = self.client.get(url)
732+
self.assertEqual(status.HTTP_200_OK, response.status_code)
733+
self.assertEqual([], response.data)
734+
735+
violation = ProductPolicyViolation.objects.create(
736+
product=self.product1,
737+
dataspace=self.dataspace,
738+
rule_type="usage_policy_error",
739+
violation_count=5,
740+
)
741+
742+
response = self.client.get(url)
743+
self.assertEqual(status.HTTP_200_OK, response.status_code)
744+
self.assertEqual(1, len(response.data))
745+
entry = response.data[0]
746+
self.assertEqual("usage_policy_error", entry["rule_type"])
747+
self.assertEqual(5, entry["violation_count"])
748+
self.assertFalse(entry["resolved"])
749+
self.assertIsNone(entry["resolved_date"])
750+
self.assertIn("rule_label", entry)
751+
self.assertIn("rule_description", entry)
752+
self.assertIn("rule_severity", entry)
753+
self.assertIn("detected_date", entry)
754+
755+
violation.resolved = True
756+
violation.save()
757+
758+
response = self.client.get(url)
759+
self.assertEqual(status.HTTP_200_OK, response.status_code)
760+
self.assertEqual([], response.data)
761+
762+
720763
class ProductRelatedAPITestCase(TestCase):
721764
def setUp(self):
722765
self.dataspace = Dataspace.objects.create(name="nexB")

0 commit comments

Comments
 (0)