diff --git a/backend/core/startup.py b/backend/core/startup.py index da29f4f911..833cff1779 100644 --- a/backend/core/startup.py +++ b/backend/core/startup.py @@ -343,6 +343,26 @@ "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 = [ diff --git a/backend/core/urls.py b/backend/core/urls.py index 189ae345f8..b04b4cb2ea 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -85,6 +85,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//upload/", UploadAttachmentView.as_view(), name="upload"), diff --git a/backend/core/views.py b/backend/core/views.py index 0ff3dbdb8d..8802cda6b5 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -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)) diff --git a/backend/ebios_rm/migrations/0001_initial.py b/backend/ebios_rm/migrations/0001_initial.py index d5c610f5ec..8980b23f0f 100644 --- a/backend/ebios_rm/migrations/0001_initial.py +++ b/backend/ebios_rm/migrations/0001_initial.py @@ -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 @@ -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( @@ -206,6 +206,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( @@ -250,7 +259,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"), @@ -281,6 +290,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( @@ -353,6 +371,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( @@ -489,6 +516,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", @@ -656,6 +692,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", diff --git a/backend/ebios_rm/models.py b/backend/ebios_rm/models.py index fea80f6b52..f51d4fc8bb 100644 --- a/backend/ebios_rm/models.py +++ b/backend/ebios_rm/models.py @@ -65,7 +65,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, @@ -102,7 +102,7 @@ class Meta: ordering = ["created_at"] -class FearedEvent(NameDescriptionMixin): +class FearedEvent(NameDescriptionMixin, FolderMixin): ebios_rm_study = models.ForeignKey( EbiosRMStudy, verbose_name=_("EBIOS RM study"), @@ -123,7 +123,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) @@ -133,8 +133,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") @@ -207,8 +211,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") @@ -290,6 +298,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 @@ -317,7 +329,7 @@ def residual_criticality(self): ) -class AttackPath(AbstractBaseModel): +class AttackPath(AbstractBaseModel, FolderMixin): ebios_rm_study = models.ForeignKey( EbiosRMStudy, verbose_name=_("EBIOS RM study"), @@ -345,8 +357,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"), @@ -376,3 +392,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) diff --git a/backend/ebios_rm/serializers.py b/backend/ebios_rm/serializers.py index 2c95f71eb0..3507bf69fc 100644 --- a/backend/ebios_rm/serializers.py +++ b/backend/ebios_rm/serializers.py @@ -4,23 +4,31 @@ AssessmentReadSerializer, ) from core.models import StoredLibrary, RiskMatrix -from .models import EbiosRMStudy +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_library = StoredLibrary.objects.get( - urn="urn:intuitem:risk:library:risk-matrix-4x4-ebios-rm" - ) - ebios_matrix_library.load() - - validated_data["risk_matrix"] = RiskMatrix.objects.get( + 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( @@ -33,15 +41,34 @@ class Meta: exclude = ["created_at", "updated_at"] -class EbiosRMStudyReadSerializer(AssessmentReadSerializer): +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__" diff --git a/backend/ebios_rm/urls.py b/backend/ebios_rm/urls.py new file mode 100644 index 0000000000..47ab37bcfd --- /dev/null +++ b/backend/ebios_rm/urls.py @@ -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)), +] diff --git a/backend/ebios_rm/views.py b/backend/ebios_rm/views.py index 50a2f2d108..69676bf77d 100644 --- a/backend/ebios_rm/views.py +++ b/backend/ebios_rm/views.py @@ -1,5 +1,5 @@ from core.views import BaseModelViewSet as AbstractBaseModelViewSet -from .models import EbiosRMStudy +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 @@ -23,3 +23,7 @@ class EbiosRMStudyViewSet(BaseModelViewSet): @action(detail=False, name="Get status choices") def status(self, request): return Response(dict(EbiosRMStudy.Status.choices)) + + +class FearedEventViewSet(BaseModelViewSet): + model = FearedEvent diff --git a/features.png b/features.png index 036cf36356..fac5e5142a 100644 Binary files a/features.png and b/features.png differ