Skip to content

Commit fa83063

Browse files
committed
add API endpoints for AnalysisPreset and TriageRuleset
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent 49ff7cf commit fa83063

3 files changed

Lines changed: 300 additions & 0 deletions

File tree

dejacode/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
from reporting.api import ReportViewSet
5656
from vulnerabilities.api import VulnerabilityAnalysisViewSet
5757
from vulnerabilities.api import VulnerabilityViewSet
58+
from vulnerabilities.triage.api import AnalysisPresetViewSet
59+
from vulnerabilities.triage.api import TriageRulesetViewSet
5860
from workflow.api import RequestTemplateViewSet
5961
from workflow.api import RequestViewSet
6062

@@ -83,6 +85,8 @@
8385
api_router.register("usage_policies", UsagePolicyViewSet)
8486
api_router.register("vulnerabilities", VulnerabilityViewSet)
8587
api_router.register("vulnerability_analyses", VulnerabilityAnalysisViewSet)
88+
api_router.register("triage_rulesets", TriageRulesetViewSet)
89+
api_router.register("analysis_presets", AnalysisPresetViewSet)
8690

8791

8892
urlpatterns = [

vulnerabilities/triage/api.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# DejaCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: AGPL-3.0-only
5+
# See https://github.com/aboutcode-org/dejacode for support or download.
6+
# See https://aboutcode.org for more information about AboutCode FOSS projects.
7+
#
8+
9+
from rest_framework import mixins
10+
from rest_framework import serializers
11+
12+
from dje.api import CreateRetrieveUpdateListViewSet
13+
from dje.api import DataspacedSerializer
14+
from dje.api import ExtraPermissionsViewSetMixin
15+
from dje.api_custom import TabPermission
16+
from vulnerabilities.triage.models import AnalysisPreset
17+
from vulnerabilities.triage.models import TriageRuleset
18+
19+
20+
class AnalysisPresetSerializer(DataspacedSerializer):
21+
class Meta:
22+
model = AnalysisPreset
23+
fields = (
24+
"api_url",
25+
"uuid",
26+
"name",
27+
"description",
28+
"state",
29+
"justification",
30+
"responses",
31+
"detail",
32+
"is_reachable",
33+
)
34+
extra_kwargs = {
35+
"api_url": {
36+
"view_name": "api_v2:analysispreset-detail",
37+
"lookup_field": "uuid",
38+
},
39+
}
40+
41+
def validate(self, data):
42+
content_fields = ("state", "justification", "responses", "detail")
43+
44+
def get_value(field_name):
45+
if field_name in data:
46+
return data[field_name]
47+
if self.instance is not None:
48+
return getattr(self.instance, field_name)
49+
return None
50+
51+
if not any(get_value(field_name) for field_name in content_fields):
52+
raise serializers.ValidationError(
53+
"At least one of state, justification, responses or detail must be provided."
54+
)
55+
return data
56+
57+
58+
class AnalysisPresetViewSet(
59+
ExtraPermissionsViewSetMixin,
60+
mixins.DestroyModelMixin,
61+
CreateRetrieveUpdateListViewSet,
62+
):
63+
queryset = AnalysisPreset.objects.all()
64+
serializer_class = AnalysisPresetSerializer
65+
lookup_field = "uuid"
66+
extra_permissions = (TabPermission,)
67+
search_fields = (
68+
"name",
69+
"description",
70+
)
71+
ordering_fields = ("name",)
72+
allow_reference_access = True
73+
74+
75+
class TriageRulesetSerializer(DataspacedSerializer):
76+
class Meta:
77+
model = TriageRuleset
78+
fields = (
79+
"api_url",
80+
"uuid",
81+
"name",
82+
"description",
83+
"recommended_action",
84+
"precedence",
85+
"enabled",
86+
"rules_config",
87+
"analysis_preset",
88+
"request_template",
89+
)
90+
extra_kwargs = {
91+
"api_url": {
92+
"view_name": "api_v2:triageruleset-detail",
93+
"lookup_field": "uuid",
94+
},
95+
"analysis_preset": {
96+
"view_name": "api_v2:analysispreset-detail",
97+
"lookup_field": "uuid",
98+
"required": False,
99+
"allow_null": True,
100+
},
101+
"request_template": {
102+
"view_name": "api_v2:requesttemplate-detail",
103+
"lookup_field": "uuid",
104+
"required": False,
105+
"allow_null": True,
106+
},
107+
}
108+
109+
def validate_request_template(self, value):
110+
if value and not value.created_by_id:
111+
raise serializers.ValidationError(
112+
"This request template has no creator and cannot be used to open requests."
113+
)
114+
return value
115+
116+
117+
class TriageRulesetViewSet(
118+
ExtraPermissionsViewSetMixin,
119+
mixins.DestroyModelMixin,
120+
CreateRetrieveUpdateListViewSet,
121+
):
122+
queryset = TriageRuleset.objects.all()
123+
serializer_class = TriageRulesetSerializer
124+
lookup_field = "uuid"
125+
extra_permissions = (TabPermission,)
126+
search_fields = (
127+
"name",
128+
"description",
129+
)
130+
ordering_fields = (
131+
"name",
132+
"precedence",
133+
)
134+
allow_reference_access = True
135+
136+
def get_queryset(self):
137+
return super().get_queryset().select_related("analysis_preset", "request_template")
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# DejaCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: AGPL-3.0-only
5+
# See https://github.com/aboutcode-org/dejacode for support or download.
6+
# See https://aboutcode.org for more information about AboutCode FOSS projects.
7+
#
8+
9+
from django.contrib.contenttypes.models import ContentType
10+
from django.test import TestCase
11+
from django.urls import reverse
12+
13+
from rest_framework import status
14+
15+
from dje.models import Dataspace
16+
from dje.tests import create_superuser
17+
from product_portfolio.models import Product
18+
from vulnerabilities.triage.models import AnalysisPreset
19+
from vulnerabilities.triage.models import TriageAction
20+
from vulnerabilities.triage.models import TriageRuleset
21+
from vulnerabilities.triage.tests import make_analysis_preset
22+
from vulnerabilities.triage.tests import make_triage_ruleset
23+
from workflow.models import RequestTemplate
24+
25+
26+
class AnalysisPresetAPITestCase(TestCase):
27+
def setUp(self):
28+
self.dataspace = Dataspace.objects.create(name="nexB")
29+
self.alternate = Dataspace.objects.create(name="Alternate")
30+
self.super_user = create_superuser("super_user", self.dataspace)
31+
32+
self.list_url = reverse("api_v2:analysispreset-list")
33+
self.preset = make_analysis_preset(
34+
self.dataspace, name="Preset1", state=AnalysisPreset.State.NOT_AFFECTED
35+
)
36+
self.detail_url = reverse("api_v2:analysispreset-detail", args=[self.preset.uuid])
37+
make_analysis_preset(self.alternate, name="OtherPreset")
38+
39+
def test_api_analysispreset_list_endpoint_user_available_scope(self):
40+
self.client.login(username="super_user", password="secret")
41+
response = self.client.get(self.list_url)
42+
self.assertEqual(1, response.data["count"])
43+
self.assertContains(response, self.preset.name)
44+
45+
def test_api_analysispreset_detail_endpoint(self):
46+
self.client.login(username="super_user", password="secret")
47+
response = self.client.get(self.detail_url)
48+
self.assertEqual(self.preset.name, response.data["name"])
49+
self.assertEqual(AnalysisPreset.State.NOT_AFFECTED, response.data["state"])
50+
51+
def test_api_analysispreset_endpoint_create(self):
52+
self.client.login(username="super_user", password="secret")
53+
data = {"name": "New Preset", "detail": "Some detail"}
54+
response = self.client.post(self.list_url, data)
55+
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
56+
preset = AnalysisPreset.objects.get(name="New Preset")
57+
self.assertEqual("Some detail", preset.detail)
58+
59+
def test_api_analysispreset_endpoint_create_rejects_no_content_field(self):
60+
self.client.login(username="super_user", password="secret")
61+
data = {"name": "No content", "is_reachable": True}
62+
response = self.client.post(self.list_url, data)
63+
self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code)
64+
msg = "At least one of state, justification, responses or detail must be provided."
65+
self.assertIn(msg, response.data["non_field_errors"])
66+
67+
def test_api_analysispreset_endpoint_update(self):
68+
self.client.login(username="super_user", password="secret")
69+
data = {"detail": "Updated detail"}
70+
response = self.client.patch(self.detail_url, data=data, content_type="application/json")
71+
self.assertEqual(status.HTTP_200_OK, response.status_code)
72+
self.preset.refresh_from_db()
73+
self.assertEqual("Updated detail", self.preset.detail)
74+
75+
def test_api_analysispreset_endpoint_delete(self):
76+
self.client.login(username="super_user", password="secret")
77+
response = self.client.delete(self.detail_url)
78+
self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code)
79+
self.assertFalse(AnalysisPreset.objects.filter(pk=self.preset.pk).exists())
80+
81+
82+
class TriageRulesetAPITestCase(TestCase):
83+
def setUp(self):
84+
self.dataspace = Dataspace.objects.create(name="nexB")
85+
self.alternate = Dataspace.objects.create(name="Alternate")
86+
self.super_user = create_superuser("super_user", self.dataspace)
87+
88+
self.list_url = reverse("api_v2:triageruleset-list")
89+
self.ruleset = make_triage_ruleset(
90+
self.dataspace,
91+
name="Ruleset1",
92+
recommended_action=TriageAction.UPGRADE,
93+
precedence=100,
94+
)
95+
self.detail_url = reverse("api_v2:triageruleset-detail", args=[self.ruleset.uuid])
96+
make_triage_ruleset(self.alternate, name="OtherRuleset")
97+
98+
def test_api_triageruleset_list_endpoint_user_available_scope(self):
99+
self.client.login(username="super_user", password="secret")
100+
response = self.client.get(self.list_url)
101+
self.assertEqual(1, response.data["count"])
102+
self.assertContains(response, self.ruleset.name)
103+
104+
def test_api_triageruleset_detail_endpoint(self):
105+
self.client.login(username="super_user", password="secret")
106+
response = self.client.get(self.detail_url)
107+
self.assertEqual(self.ruleset.name, response.data["name"])
108+
self.assertEqual(TriageAction.UPGRADE, response.data["recommended_action"])
109+
self.assertEqual(100, response.data["precedence"])
110+
111+
def test_api_triageruleset_endpoint_create(self):
112+
self.client.login(username="super_user", password="secret")
113+
data = {
114+
"name": "New Ruleset",
115+
"precedence": 200,
116+
"recommended_action": TriageAction.NOTIFY,
117+
"rules_config": {"risk_score": {"is_active": True, "min_risk_score": 8.0}},
118+
}
119+
response = self.client.post(self.list_url, data=data, content_type="application/json")
120+
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
121+
ruleset = TriageRuleset.objects.get(name="New Ruleset")
122+
self.assertEqual(
123+
{"is_active": True, "min_risk_score": 8.0}, ruleset.rules_config["risk_score"]
124+
)
125+
126+
def test_api_triageruleset_endpoint_create_rejects_request_template_with_no_creator(self):
127+
self.client.login(username="super_user", password="secret")
128+
request_template = RequestTemplate.objects.create(
129+
name="Broken Template",
130+
description="Header",
131+
dataspace=self.dataspace,
132+
content_type=ContentType.objects.get_for_model(Product),
133+
)
134+
request_template_url = reverse(
135+
"api_v2:requesttemplate-detail", args=[request_template.uuid]
136+
)
137+
data = {
138+
"name": "New Ruleset",
139+
"precedence": 200,
140+
"request_template": request_template_url,
141+
}
142+
response = self.client.post(self.list_url, data=data, content_type="application/json")
143+
self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code)
144+
msg = "This request template has no creator and cannot be used to open requests."
145+
self.assertIn(msg, response.data["request_template"])
146+
147+
def test_api_triageruleset_endpoint_update(self):
148+
self.client.login(username="super_user", password="secret")
149+
data = {"enabled": False}
150+
response = self.client.patch(self.detail_url, data=data, content_type="application/json")
151+
self.assertEqual(status.HTTP_200_OK, response.status_code)
152+
self.ruleset.refresh_from_db()
153+
self.assertFalse(self.ruleset.enabled)
154+
155+
def test_api_triageruleset_endpoint_delete(self):
156+
self.client.login(username="super_user", password="secret")
157+
response = self.client.delete(self.detail_url)
158+
self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code)
159+
self.assertFalse(TriageRuleset.objects.filter(pk=self.ruleset.pk).exists())

0 commit comments

Comments
 (0)