Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create CRUD endpoints for EBIOS RM studies and feared events #1120

Merged
merged 16 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading