Skip to content

Commit

Permalink
Create CRUD endpoints for EBIOS RM studies and feared events (#1120)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mohamed-Hacene authored Dec 4, 2024
2 parents 427ceed + bc11fb0 commit 09d652e
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 13 deletions.
24 changes: 24 additions & 0 deletions backend/core/startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,30 @@
"view_filteringlabel",
"change_filteringlabel",
"delete_filteringlabel",
"add_ebiosrmstudy",
"view_ebiosrmstudy",
"change_ebiosrmstudy",
"delete_ebiosrmstudy",
"add_fearedevent",
"view_fearedevent",
"change_fearedevent",
"delete_fearedevent",
"add_roto",
"view_roto",
"change_roto",
"delete_roto",
"add_stakeholder",
"view_stakeholder",
"change_stakeholder",
"delete_stakeholder",
"add_attackpath",
"view_attackpath",
"change_attackpath",
"delete_attackpath",
"add_operationalscenario",
"view_operationalscenario",
"change_operationalscenario",
"delete_operationalscenario",
]

THIRD_PARTY_RESPONDENT_PERMISSIONS_LIST = [
Expand Down
1 change: 1 addition & 0 deletions backend/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
path("iam/", include("iam.urls")),
path("serdes/", include("serdes.urls")),
path("settings/", include("global_settings.urls")),
path("ebios-rm/", include("ebios_rm.urls")),
path("csrf/", get_csrf_token, name="get_csrf_token"),
path("build/", get_build, name="get_build"),
path("evidences/<uuid:pk>/upload/", UploadAttachmentView.as_view(), name="upload"),
Expand Down
5 changes: 4 additions & 1 deletion backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ def get_queryset(self):
return None
object_ids_view = None
if self.request.method == "GET":
if q := re.match("/api/[\w-]+/([0-9a-f-]+)", self.request.path):
if q := re.match(
"/api/[\w-]+/([\w-]+/)?([0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}(,[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12})+)",
self.request.path,
):
""""get_queryset is called by Django even for an individual object via get_object
https://stackoverflow.com/questions/74048193/why-does-a-retrieve-request-end-up-calling-get-queryset"""
id = UUID(q.group(1))
Expand Down
51 changes: 48 additions & 3 deletions backend/ebios_rm/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.1.1 on 2024-12-03 12:57
# Generated by Django 5.1.1 on 2024-12-04 13:02

import django.core.validators
import django.db.models.deletion
Expand Down Expand Up @@ -54,7 +54,7 @@ class Migration(migrations.Migration):
"due_date",
models.DateField(blank=True, null=True, verbose_name="Due date"),
),
("ref_id", models.CharField(max_length=100)),
("ref_id", models.CharField(blank=True, max_length=100)),
(
"version",
models.CharField(
Expand Down Expand Up @@ -205,6 +205,15 @@ class Migration(migrations.Migration):
"justification",
models.TextField(blank=True, verbose_name="Justification"),
),
(
"folder",
models.ForeignKey(
default=iam.models.Folder.get_root_folder_id,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_folder",
to="iam.folder",
),
),
(
"ebios_rm_study",
models.ForeignKey(
Expand Down Expand Up @@ -249,7 +258,7 @@ class Migration(migrations.Migration):
"description",
models.TextField(blank=True, null=True, verbose_name="Description"),
),
("ref_id", models.CharField(max_length=100)),
("ref_id", models.CharField(blank=True, max_length=100)),
(
"gravity",
models.SmallIntegerField(default=-1, verbose_name="Gravity"),
Expand Down Expand Up @@ -280,6 +289,15 @@ class Migration(migrations.Migration):
verbose_name="EBIOS RM study",
),
),
(
"folder",
models.ForeignKey(
default=iam.models.Folder.get_root_folder_id,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_folder",
to="iam.folder",
),
),
(
"qualifications",
models.ManyToManyField(
Expand Down Expand Up @@ -352,6 +370,15 @@ class Migration(migrations.Migration):
verbose_name="EBIOS RM study",
),
),
(
"folder",
models.ForeignKey(
default=iam.models.Folder.get_root_folder_id,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_folder",
to="iam.folder",
),
),
(
"threats",
models.ManyToManyField(
Expand Down Expand Up @@ -488,6 +515,15 @@ class Migration(migrations.Migration):
verbose_name="Feared events",
),
),
(
"folder",
models.ForeignKey(
default=iam.models.Folder.get_root_folder_id,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_folder",
to="iam.folder",
),
),
],
options={
"verbose_name": "RO/TO couple",
Expand Down Expand Up @@ -655,6 +691,15 @@ class Migration(migrations.Migration):
verbose_name="Entity",
),
),
(
"folder",
models.ForeignKey(
default=iam.models.Folder.get_root_folder_id,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_folder",
to="iam.folder",
),
),
],
options={
"verbose_name": "Stakeholder",
Expand Down
34 changes: 27 additions & 7 deletions backend/ebios_rm/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class Status(models.TextChoices):
default=Entity.get_main_entity,
)

ref_id = models.CharField(max_length=100)
ref_id = models.CharField(max_length=100, blank=True)
version = models.CharField(
max_length=100,
blank=True,
Expand Down Expand Up @@ -101,7 +101,7 @@ class Meta:
ordering = ["created_at"]


class FearedEvent(NameDescriptionMixin):
class FearedEvent(NameDescriptionMixin, FolderMixin):
ebios_rm_study = models.ForeignKey(
EbiosRMStudy,
verbose_name=_("EBIOS RM study"),
Expand All @@ -122,7 +122,7 @@ class FearedEvent(NameDescriptionMixin):
help_text=_("Qualifications carried by the feared event"),
)

ref_id = models.CharField(max_length=100)
ref_id = models.CharField(max_length=100, blank=True)
gravity = models.SmallIntegerField(default=-1, verbose_name=_("Gravity"))
is_selected = models.BooleanField(verbose_name=_("Is selected"), default=False)
justification = models.TextField(verbose_name=_("Justification"), blank=True)
Expand All @@ -132,8 +132,12 @@ class Meta:
verbose_name_plural = _("Feared events")
ordering = ["created_at"]

def save(self, *args, **kwargs):
self.folder = self.ebios_rm_study.folder
super().save(*args, **kwargs)

class RoTo(AbstractBaseModel):

class RoTo(AbstractBaseModel, FolderMixin):
class RiskOrigin(models.TextChoices):
STATE = "state", _("State")
ORGANIZED_CRIME = "organized_crime", _("Organized crime")
Expand Down Expand Up @@ -206,8 +210,12 @@ class Meta:
verbose_name_plural = _("RO/TO couples")
ordering = ["created_at"]

def save(self, *args, **kwargs):
self.folder = self.ebios_rm_study.folder
super().save(*args, **kwargs)


class Stakeholder(AbstractBaseModel):
class Stakeholder(AbstractBaseModel, FolderMixin):
class Category(models.TextChoices):
CLIENT = "client", _("Client")
PARTNER = "partner", _("Partner")
Expand Down Expand Up @@ -289,6 +297,10 @@ class Meta:
verbose_name_plural = _("Stakeholders")
ordering = ["created_at"]

def save(self, *args, **kwargs):
self.folder = self.ebios_rm_study.folder
super().save(*args, **kwargs)

@staticmethod
def _compute_criticality(
dependency: int, penetration: int, maturity: int, trust: int
Expand Down Expand Up @@ -316,7 +328,7 @@ def residual_criticality(self):
)


class AttackPath(AbstractBaseModel):
class AttackPath(AbstractBaseModel, FolderMixin):
ebios_rm_study = models.ForeignKey(
EbiosRMStudy,
verbose_name=_("EBIOS RM study"),
Expand Down Expand Up @@ -344,8 +356,12 @@ class Meta:
verbose_name_plural = _("Attack paths")
ordering = ["created_at"]

def save(self, *args, **kwargs):
self.folder = self.ebios_rm_study.folder
super().save(*args, **kwargs)

class OperationalScenario(AbstractBaseModel):

class OperationalScenario(AbstractBaseModel, FolderMixin):
ebios_rm_study = models.ForeignKey(
EbiosRMStudy,
verbose_name=_("EBIOS RM study"),
Expand Down Expand Up @@ -375,3 +391,7 @@ class Meta:
verbose_name = _("Operational scenario")
verbose_name_plural = _("Operational scenarios")
ordering = ["created_at"]

def save(self, *args, **kwargs):
self.folder = self.ebios_rm_study.folder
super().save(*args, **kwargs)
74 changes: 74 additions & 0 deletions backend/ebios_rm/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from core.serializers import (
BaseModelSerializer,
FieldsRelatedField,
AssessmentReadSerializer,
)
from core.models import StoredLibrary, RiskMatrix
from .models import EbiosRMStudy, FearedEvent
from rest_framework import serializers
import logging


class EbiosRMStudyWriteSerializer(BaseModelSerializer):
risk_matrix = serializers.PrimaryKeyRelatedField(
queryset=RiskMatrix.objects.all(), required=False
)

def create(self, validated_data):
if not validated_data.get("risk_matrix"):
try:
ebios_matrix = RiskMatrix.objects.filter(
urn="urn:intuitem:risk:matrix:risk-matrix-4x4-ebios-rm"
).first()
if not ebios_matrix:
ebios_matrix_library = StoredLibrary.objects.get(
urn="urn:intuitem:risk:library:risk-matrix-4x4-ebios-rm"
)
ebios_matrix_library.load()
ebios_matrix = RiskMatrix.objects.get(
urn="urn:intuitem:risk:matrix:risk-matrix-4x4-ebios-rm"
)
validated_data["risk_matrix"] = ebios_matrix
except (StoredLibrary.DoesNotExist, RiskMatrix.DoesNotExist) as e:
logging.error(f"Error loading risk matrix: {str(e)}")
raise serializers.ValidationError(
"An error occurred while loading the risk matrix."
)
return super().create(validated_data)

class Meta:
model = EbiosRMStudy
exclude = ["created_at", "updated_at"]


class EbiosRMStudyReadSerializer(BaseModelSerializer):
str = serializers.CharField(source="__str__")
project = FieldsRelatedField(["id", "folder"])
folder = FieldsRelatedField()
risk_matrix = FieldsRelatedField()
reference_entity = FieldsRelatedField()
assets = FieldsRelatedField(many=True)
compliance_assessments = FieldsRelatedField(many=True)
risk_assessments = FieldsRelatedField(many=True)
authors = FieldsRelatedField(many=True)
reviewers = FieldsRelatedField(many=True)

class Meta:
model = EbiosRMStudy
fields = "__all__"


class FearedEventWriteSerializer(BaseModelSerializer):
class Meta:
model = FearedEvent
exclude = ["created_at", "updated_at", "folder"]


class FearedEventReadSerializer(BaseModelSerializer):
str = serializers.CharField(source="__str__")
ebios_rm_study = FieldsRelatedField()
folder = FieldsRelatedField()

class Meta:
model = FearedEvent
fields = "__all__"
13 changes: 13 additions & 0 deletions backend/ebios_rm/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.urls import include, path
from rest_framework import routers

from ebios_rm.views import EbiosRMStudyViewSet, FearedEventViewSet

router = routers.DefaultRouter()

router.register(r"studies", EbiosRMStudyViewSet, basename="studies")
router.register(r"feared-events", FearedEventViewSet, basename="feared-events")

urlpatterns = [
path("", include(router.urls)),
]
30 changes: 28 additions & 2 deletions backend/ebios_rm/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
from django.shortcuts import render
from core.views import BaseModelViewSet as AbstractBaseModelViewSet
from .models import EbiosRMStudy, FearedEvent
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from rest_framework.decorators import action
from rest_framework.response import Response

# Create your views here.
LONG_CACHE_TTL = 60 # mn


class BaseModelViewSet(AbstractBaseModelViewSet):
serializers_module = "ebios_rm.serializers"


class EbiosRMStudyViewSet(BaseModelViewSet):
"""
API endpoint that allows ebios rm studies to be viewed or edited.
"""

model = EbiosRMStudy

@method_decorator(cache_page(60 * LONG_CACHE_TTL))
@action(detail=False, name="Get status choices")
def status(self, request):
return Response(dict(EbiosRMStudy.Status.choices))


class FearedEventViewSet(BaseModelViewSet):
model = FearedEvent

0 comments on commit 09d652e

Please sign in to comment.