diff --git a/backend/core/base_models.py b/backend/core/base_models.py index 264567870..5e1240797 100644 --- a/backend/core/base_models.py +++ b/backend/core/base_models.py @@ -46,13 +46,19 @@ def is_unique_in_scope(self, scope: models.QuerySet, fields_to_check: list) -> b # to avoid false positives as a result of the object being compared to itself if self.pk: scope = scope.exclude(pk=self.pk) - return not scope.filter( - **{ - f"{field}__iexact": getattr(self, field) - for field in fields_to_check - if hasattr(self, field) - } - ).exists() + filters = {} + for field in fields_to_check: + if hasattr(self, field): + field_value = getattr(self, field) + model_field = self._meta.get_field(field) + + # Use the appropriate lookup based on the field type + if isinstance(model_field, models.ForeignKey): + filters[f"{field}__exact"] = field_value + else: + filters[f"{field}__iexact"] = field_value + + return not scope.filter(**filters).exists() def display_path(self): pass diff --git a/backend/core/models.py b/backend/core/models.py index 4d5ba2c02..7407d4853 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -1494,6 +1494,17 @@ def ancestors_plus_self(self) -> set[Self]: result.update(x.ancestors_plus_self()) return set(result) + def get_children(self): + return Asset.objects.filter(parent_assets=self) + + def get_descendants(self) -> set[Self]: + children = self.get_children() + sub_children = set() + for child in children: + sub_children.append(child) + sub_children.update(child.get_descendants()) + return sub_children + def get_security_objectives(self) -> dict[str, dict[str, dict[str, int | bool]]]: """ Gets the security objectives of a given asset. diff --git a/backend/core/serializers.py b/backend/core/serializers.py index 43332b224..6595dd66a 100644 --- a/backend/core/serializers.py +++ b/backend/core/serializers.py @@ -119,6 +119,7 @@ class AssessmentReadSerializer(BaseModelSerializer): class RiskMatrixReadSerializer(ReferentialSerializer): folder = FieldsRelatedField() json_definition = serializers.JSONField(source="get_json_translated") + library = FieldsRelatedField(["name", "id"]) class Meta: model = RiskMatrix diff --git a/backend/core/startup.py b/backend/core/startup.py index 3196379e2..2ec1362d4 100644 --- a/backend/core/startup.py +++ b/backend/core/startup.py @@ -355,6 +355,10 @@ "view_stakeholder", "change_stakeholder", "delete_stakeholder", + "add_strategicscenario", + "view_strategicscenario", + "change_strategicscenario", + "delete_strategicscenario", "add_attackpath", "view_attackpath", "change_attackpath", diff --git a/backend/core/views.py b/backend/core/views.py index 6f4301571..915e0c054 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -69,10 +69,16 @@ AppliedControl, ComplianceAssessment, RequirementMappingSet, + RiskAssessment, ) from core.serializers import ComplianceAssessmentReadSerializer from core.utils import RoleCodename, UserGroupCodename +from ebios_rm.models import ( + EbiosRMStudy, + OperationalScenario, +) + from .models import * from .serializers import * @@ -578,6 +584,35 @@ class RiskAssessmentViewSet(BaseModelViewSet): "ebios_rm_study", ] + def perform_create(self, serializer): + instance: RiskAssessment = serializer.save() + if instance.ebios_rm_study: + instance.risk_matrix = instance.ebios_rm_study.risk_matrix + ebios_rm_study = EbiosRMStudy.objects.get(id=instance.ebios_rm_study.id) + for operational_scenario in [ + operational_scenario + for operational_scenario in ebios_rm_study.operational_scenarios.all() + if operational_scenario.is_selected + ]: + risk_scenario = RiskScenario.objects.create( + risk_assessment=instance, + name=operational_scenario.name, + ref_id=operational_scenario.ref_id + if operational_scenario.ref_id + else RiskScenario.get_default_ref_id(instance), + description=operational_scenario.operating_modes_description, + current_proba=operational_scenario.likelihood, + current_impact=operational_scenario.gravity, + ) + risk_scenario.assets.set(operational_scenario.get_assets()) + risk_scenario.threats.set(operational_scenario.threats.all()) + risk_scenario.existing_applied_controls.set( + operational_scenario.get_applied_controls() + ) + risk_scenario.save() + instance.save() + return super().perform_create(serializer) + @action(detail=False, name="Risk assessments per status") def per_status(self, request): data = assessment_per_status(request.user, RiskAssessment) diff --git a/backend/ebios_rm/migrations/0007_ebiosrmstudy_meta.py b/backend/ebios_rm/migrations/0007_ebiosrmstudy_meta.py new file mode 100644 index 000000000..5a22e4c5b --- /dev/null +++ b/backend/ebios_rm/migrations/0007_ebiosrmstudy_meta.py @@ -0,0 +1,66 @@ +# Generated by Django 5.1.4 on 2024-12-18 01:25 + +import core.validators +import ebios_rm.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("ebios_rm", "0006_alter_attackpath_stakeholders"), + ] + + operations = [ + migrations.AddField( + model_name="ebiosrmstudy", + name="meta", + field=models.JSONField( + default=ebios_rm.models.get_initial_meta, + validators=[ + core.validators.JSONSchemaInstanceValidator( + { + "$id": "https://ciso-assistant.com/schemas/ebiosrmstudy/meta.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Metadata of the EBIOS RM Study", + "properties": { + "workshops": { + "description": "A list of workshops, each containing steps", + "items": { + "additionalProperties": False, + "properties": { + "steps": { + "description": "The list of steps in the workshop", + "items": { + "additionalProperties": False, + "properties": { + "status": { + "description": "The current status of the step", + "enum": [ + "to_do", + "in_progress", + "done", + ], + "type": "string", + } + }, + "required": ["status"], + "type": "object", + }, + "type": "array", + } + }, + "required": ["steps"], + "type": "object", + }, + "type": "array", + } + }, + "title": "Metadata", + "type": "object", + } + ) + ], + verbose_name="Metadata", + ), + ), + ] diff --git a/backend/ebios_rm/migrations/0008_remove_attackpath_ro_to_couple_strategicscenario_and_more.py b/backend/ebios_rm/migrations/0008_remove_attackpath_ro_to_couple_strategicscenario_and_more.py new file mode 100644 index 000000000..3c98cffa8 --- /dev/null +++ b/backend/ebios_rm/migrations/0008_remove_attackpath_ro_to_couple_strategicscenario_and_more.py @@ -0,0 +1,97 @@ +# Generated by Django 5.1.4 on 2024-12-20 08:56 + +import django.db.models.deletion +import iam.models +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("ebios_rm", "0007_ebiosrmstudy_meta"), + ("iam", "0010_user_preferences"), + ] + + operations = [ + migrations.RemoveField( + model_name="attackpath", + name="ro_to_couple", + ), + migrations.CreateModel( + name="StrategicScenario", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created at"), + ), + ( + "updated_at", + models.DateTimeField(auto_now=True, verbose_name="Updated at"), + ), + ( + "is_published", + models.BooleanField(default=False, verbose_name="published"), + ), + ("name", models.CharField(max_length=200, verbose_name="Name")), + ( + "description", + models.TextField(blank=True, null=True, verbose_name="Description"), + ), + ("ref_id", models.CharField(blank=True, max_length=100)), + ( + "ebios_rm_study", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="strategic_scenarios", + to="ebios_rm.ebiosrmstudy", + 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", + ), + ), + ( + "ro_to_couple", + models.ForeignKey( + help_text="RO/TO couple from which the attach path is derived", + on_delete=django.db.models.deletion.CASCADE, + to="ebios_rm.roto", + verbose_name="RO/TO couple", + ), + ), + ], + options={ + "verbose_name": "Strategic Scenario", + "verbose_name_plural": "Strategic Scenarios", + "ordering": ["created_at"], + }, + ), + migrations.AddField( + model_name="attackpath", + name="strategic_scenario", + field=models.ForeignKey( + default="", + help_text="Strategic scenario from which the attack path is derived", + on_delete=django.db.models.deletion.CASCADE, + related_name="attack_paths", + to="ebios_rm.strategicscenario", + verbose_name="Strategic scenario", + ), + preserve_default=False, + ), + ] diff --git a/backend/ebios_rm/models.py b/backend/ebios_rm/models.py index 7eb44315e..ba5884409 100644 --- a/backend/ebios_rm/models.py +++ b/backend/ebios_rm/models.py @@ -1,7 +1,8 @@ from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.utils.translation import gettext_lazy as _ -from core.base_models import AbstractBaseModel, NameDescriptionMixin, ETADueDateMixin + +from core.base_models import AbstractBaseModel, ETADueDateMixin, NameDescriptionMixin from core.models import ( AppliedControl, Asset, @@ -10,9 +11,41 @@ RiskMatrix, Threat, ) +from core.validators import ( + JSONSchemaInstanceValidator, +) from iam.models import FolderMixin, User from tprm.models import Entity +INITIAL_META = { + "workshops": [ + { + "steps": [ + {"status": "to_do"}, + {"status": "to_do"}, + {"status": "to_do"}, + {"status": "to_do"}, + ] + }, + {"steps": [{"status": "to_do"}, {"status": "to_do"}, {"status": "to_do"}]}, + {"steps": [{"status": "to_do"}, {"status": "to_do"}, {"status": "to_do"}]}, + {"steps": [{"status": "to_do"}, {"status": "to_do"}]}, + { + "steps": [ + {"status": "to_do"}, + {"status": "to_do"}, + {"status": "to_do"}, + {"status": "to_do"}, + {"status": "to_do"}, + ] + }, + ] +} + + +def get_initial_meta(): + return INITIAL_META + class EbiosRMStudy(NameDescriptionMixin, ETADueDateMixin, FolderMixin): class Status(models.TextChoices): @@ -22,6 +55,43 @@ class Status(models.TextChoices): DONE = "done", _("Done") DEPRECATED = "deprecated", _("Deprecated") + META_JSONSCHEMA = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ciso-assistant.com/schemas/ebiosrmstudy/meta.schema.json", + "title": "Metadata", + "description": "Metadata of the EBIOS RM Study", + "type": "object", + "properties": { + "workshops": { + "type": "array", + "description": "A list of workshops, each containing steps", + "items": { + "type": "object", + "properties": { + "steps": { + "type": "array", + "description": "The list of steps in the workshop", + "items": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "The current status of the step", + "enum": ["to_do", "in_progress", "done"], + }, + }, + "required": ["status"], + "additionalProperties": False, + }, + }, + }, + "required": ["steps"], + "additionalProperties": False, + }, + } + }, + } + risk_matrix = models.ForeignKey( RiskMatrix, on_delete=models.PROTECT, @@ -87,6 +157,13 @@ class Status(models.TextChoices): related_name="reviewers", ) observation = models.TextField(null=True, blank=True, verbose_name=_("Observation")) + meta = models.JSONField( + default=get_initial_meta, + verbose_name=_("Metadata"), + validators=[JSONSchemaInstanceValidator(META_JSONSCHEMA)], + ) + + fields_to_check = ["name", "version"] class Meta: verbose_name = _("Ebios RM Study") @@ -97,6 +174,17 @@ class Meta: def parsed_matrix(self): return self.risk_matrix.parse_json_translated() + def update_workshop_step_status(self, workshop: int, step: int, new_status: str): + if workshop < 1 or workshop > 5: + raise ValueError("Workshop must be between 1 and 5") + if step < 1 or step > len(self.meta["workshops"][workshop - 1]["steps"]): + raise ValueError( + f"Worshop {workshop} has only {len(self.meta['workshops'][workshop - 1]['steps'])} steps" + ) + status = new_status + self.meta["workshops"][workshop - 1]["steps"][step - 1]["status"] = status + return self.save() + class FearedEvent(NameDescriptionMixin, FolderMixin): ebios_rm_study = models.ForeignKey( @@ -124,6 +212,8 @@ class FearedEvent(NameDescriptionMixin, FolderMixin): is_selected = models.BooleanField(verbose_name=_("Is selected"), default=False) justification = models.TextField(verbose_name=_("Justification"), blank=True) + fields_to_check = ["name", "ref_id"] + class Meta: verbose_name = _("Feared event") verbose_name_plural = _("Feared events") @@ -148,6 +238,7 @@ def get_gravity_display(self): "name": "--", "description": "not rated", "value": -1, + "hexcolor": "#f9fafb", } risk_matrix = self.parsed_matrix return { @@ -220,6 +311,8 @@ class Pertinence(models.IntegerChoices): is_selected = models.BooleanField(verbose_name=_("Is selected"), default=False) justification = models.TextField(verbose_name=_("Justification"), blank=True) + fields_to_check = ["target_objective", "risk_origin"] + def __str__(self) -> str: return f"{self.get_risk_origin_display()} - {self.target_objective}" @@ -246,6 +339,13 @@ def get_pertinence(self): PERTINENCE_MATRIX[self.motivation - 1][self.resources - 1] ).label + def get_gravity(self): + gravity = -1 + for feared_event in self.feared_events.all(): + if feared_event.gravity > gravity and feared_event.is_selected: + gravity = feared_event.gravity + return gravity + class Stakeholder(AbstractBaseModel, FolderMixin): class Category(models.TextChoices): @@ -324,11 +424,16 @@ class Category(models.TextChoices): is_selected = models.BooleanField(verbose_name=_("Is selected"), default=False) justification = models.TextField(verbose_name=_("Justification"), blank=True) + fields_to_check = ["entity", "category"] + class Meta: verbose_name = _("Stakeholder") verbose_name_plural = _("Stakeholders") ordering = ["created_at"] + def get_scope(self): + return self.__class__.objects.filter(ebios_rm_study=self.ebios_rm_study) + def __str__(self): return f"{self.entity.name} - {self.get_category_display()}" @@ -362,11 +467,26 @@ def residual_criticality(self): self.residual_trust, ) + def get_current_criticality_display(self) -> str: + return ( + f"{self.current_criticality:.2f}".rstrip("0").rstrip(".") + if "." in f"{self.current_criticality:.2f}" + else f"{self.current_criticality:.2f}" + ) -class AttackPath(NameDescriptionMixin, FolderMixin): + def get_residual_criticality_display(self) -> str: + return ( + f"{self.residual_criticality:.2f}".rstrip("0").rstrip(".") + if "." in f"{self.residual_criticality:.2f}" + else f"{self.residual_criticality:.2f}" + ) + + +class StrategicScenario(NameDescriptionMixin, FolderMixin): ebios_rm_study = models.ForeignKey( EbiosRMStudy, verbose_name=_("EBIOS RM study"), + related_name="strategic_scenarios", on_delete=models.CASCADE, ) ro_to_couple = models.ForeignKey( @@ -375,6 +495,51 @@ class AttackPath(NameDescriptionMixin, FolderMixin): on_delete=models.CASCADE, help_text=_("RO/TO couple from which the attach path is derived"), ) + ref_id = models.CharField(max_length=100, blank=True) + + fields_to_check = ["name", "ref_id"] + + class Meta: + verbose_name = _("Strategic Scenario") + verbose_name_plural = _("Strategic Scenarios") + ordering = ["created_at"] + + def get_scope(self): + return self.__class__.objects.filter(ebios_rm_study=self.ebios_rm_study) + + def save(self, *args, **kwargs): + self.folder = self.ebios_rm_study.folder + super().save(*args, **kwargs) + + def get_gravity_display(self): + if self.ro_to_couple.get_gravity() < 0: + return { + "abbreviation": "--", + "name": "--", + "description": "not rated", + "value": -1, + "hexcolor": "#f9fafb", + } + risk_matrix = self.ebios_rm_study.parsed_matrix + return { + **risk_matrix["impact"][self.ro_to_couple.get_gravity()], + "value": self.ro_to_couple.get_gravity(), + } + + +class AttackPath(NameDescriptionMixin, FolderMixin): + ebios_rm_study = models.ForeignKey( + EbiosRMStudy, + verbose_name=_("EBIOS RM study"), + on_delete=models.CASCADE, + ) + strategic_scenario = models.ForeignKey( + StrategicScenario, + verbose_name=_("Strategic scenario"), + on_delete=models.CASCADE, + related_name="attack_paths", + help_text=_("Strategic scenario from which the attack path is derived"), + ) stakeholders = models.ManyToManyField( Stakeholder, verbose_name=_("Stakeholders"), @@ -387,15 +552,29 @@ class AttackPath(NameDescriptionMixin, FolderMixin): is_selected = models.BooleanField(verbose_name=_("Is selected"), default=False) justification = models.TextField(verbose_name=_("Justification"), blank=True) + fields_to_check = ["name", "ref_id"] + class Meta: verbose_name = _("Attack path") verbose_name_plural = _("Attack paths") ordering = ["created_at"] + def get_scope(self): + return self.__class__.objects.filter(ebios_rm_study=self.ebios_rm_study) + def save(self, *args, **kwargs): + self.ebios_rm_study = self.strategic_scenario.ebios_rm_study self.folder = self.ebios_rm_study.folder super().save(*args, **kwargs) + @property + def ro_to_couple(self): + return self.strategic_scenario.ro_to_couple + + @property + def gravity(self): + return self.ro_to_couple.get_gravity() + class OperationalScenario(AbstractBaseModel, FolderMixin): ebios_rm_study = models.ForeignKey( @@ -444,6 +623,39 @@ def risk_matrix(self): def parsed_matrix(self): return self.risk_matrix.parse_json_translated() + @property + def ref_id(self): + return self.attack_path.ref_id + + @property + def name(self): + return self.attack_path.name + + @property + def gravity(self): + return self.attack_path.gravity + + @property + def stakeholders(self): + return self.attack_path.stakeholders.all() + + @property + def ro_to(self): + return self.attack_path.ro_to_couple + + def get_assets(self): + initial_assets = Asset.objects.filter( + feared_events__in=self.ro_to.feared_events.all(), is_selected=True + ) + assets = set() + for asset in initial_assets: + assets.add(asset) + assets.update(asset.get_descendants()) + return Asset.objects.filter(id__in=[asset.id for asset in assets]) + + def get_applied_controls(self): + return AppliedControl.objects.filter(stakeholders__in=self.stakeholders.all()) + def get_likelihood_display(self): if self.likelihood < 0: return { @@ -451,9 +663,40 @@ def get_likelihood_display(self): "name": "--", "description": "not rated", "value": -1, + "hexcolor": "#f9fafb", } risk_matrix = self.parsed_matrix return { **risk_matrix["probability"][self.likelihood], "value": self.likelihood, } + + def get_gravity_display(self): + if self.gravity < 0: + return { + "abbreviation": "--", + "name": "--", + "description": "not rated", + "value": -1, + "hexcolor": "#f9fafb", + } + risk_matrix = self.parsed_matrix + return { + **risk_matrix["impact"][self.gravity], + "value": self.gravity, + } + + def get_risk_level_display(self): + if self.likelihood < 0 or self.gravity < 0: + return { + "abbreviation": "--", + "name": "--", + "description": "not rated", + "value": -1, + } + risk_matrix = self.parsed_matrix + risk_index = risk_matrix["grid"][self.likelihood][self.gravity] + return { + **risk_matrix["risk"][risk_index], + "value": risk_index, + } diff --git a/backend/ebios_rm/serializers.py b/backend/ebios_rm/serializers.py index 7a9e597bd..b23c78876 100644 --- a/backend/ebios_rm/serializers.py +++ b/backend/ebios_rm/serializers.py @@ -8,6 +8,7 @@ FearedEvent, RoTo, Stakeholder, + StrategicScenario, AttackPath, OperationalScenario, ) @@ -120,18 +121,40 @@ class StakeholderReadSerializer(BaseModelSerializer): applied_controls = FieldsRelatedField(many=True) category = serializers.CharField(source="get_category_display") - current_criticality = serializers.IntegerField() - residual_criticality = serializers.IntegerField() + current_criticality = serializers.CharField( + source="get_current_criticality_display" + ) + residual_criticality = serializers.CharField( + source="get_residual_criticality_display" + ) class Meta: model = Stakeholder fields = "__all__" +class StrategicScenarioWriteSerializer(BaseModelSerializer): + class Meta: + model = StrategicScenario + exclude = ["created_at", "updated_at", "folder"] + + +class StrategicScenarioReadSerializer(BaseModelSerializer): + ebios_rm_study = FieldsRelatedField() + folder = FieldsRelatedField() + ro_to_couple = FieldsRelatedField() + gravity = serializers.JSONField(source="get_gravity_display") + attack_paths = FieldsRelatedField(many=True) + + class Meta: + model = StrategicScenario + fields = "__all__" + + class AttackPathWriteSerializer(BaseModelSerializer): class Meta: model = AttackPath - exclude = ["created_at", "updated_at", "folder"] + exclude = ["created_at", "updated_at", "folder", "ebios_rm_study"] class AttackPathReadSerializer(BaseModelSerializer): @@ -157,9 +180,14 @@ class OperationalScenarioReadSerializer(BaseModelSerializer): str = serializers.CharField(source="__str__") ebios_rm_study = FieldsRelatedField() folder = FieldsRelatedField() - attack_path = FieldsRelatedField() + attack_path = FieldsRelatedField(["id", "name", "description"]) + stakeholders = FieldsRelatedField(many=True) + ro_to = FieldsRelatedField(["risk_origin", "target_objective"]) threats = FieldsRelatedField(many=True) likelihood = serializers.JSONField(source="get_likelihood_display") + gravity = serializers.JSONField(source="get_gravity_display") + risk_level = serializers.JSONField(source="get_risk_level_display") + ref_id = serializers.CharField() class Meta: model = OperationalScenario diff --git a/backend/ebios_rm/urls.py b/backend/ebios_rm/urls.py index 76f7b3e2e..cf7bcaebf 100644 --- a/backend/ebios_rm/urls.py +++ b/backend/ebios_rm/urls.py @@ -6,6 +6,7 @@ FearedEventViewSet, RoToViewSet, StakeholderViewSet, + StrategicScenarioViewSet, AttackPathViewSet, OperationalScenarioViewSet, ) @@ -16,6 +17,9 @@ router.register(r"feared-events", FearedEventViewSet, basename="feared-events") router.register(r"ro-to", RoToViewSet, basename="ro-to") router.register(r"stakeholders", StakeholderViewSet, basename="stakeholders") +router.register( + r"strategic-scenarios", StrategicScenarioViewSet, basename="strategic-scenarios" +) router.register(r"attack-paths", AttackPathViewSet, basename="attack-paths") router.register( r"operational-scenarios", diff --git a/backend/ebios_rm/views.py b/backend/ebios_rm/views.py index 6d35d48a1..871119cd0 100644 --- a/backend/ebios_rm/views.py +++ b/backend/ebios_rm/views.py @@ -1,3 +1,4 @@ +import django_filters as df from core.serializers import RiskMatrixReadSerializer from core.views import BaseModelViewSet as AbstractBaseModelViewSet from core.serializers import RiskMatrixReadSerializer @@ -6,9 +7,11 @@ FearedEvent, RoTo, Stakeholder, + StrategicScenario, AttackPath, OperationalScenario, ) +from .serializers import EbiosRMStudyReadSerializer from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page from rest_framework.decorators import action @@ -65,6 +68,22 @@ def likelihood(self, request, pk): choices = undefined | _choices return Response(choices) + @action( + detail=True, + methods=["patch"], + name="Update workshop step status", + url_path="workshop/(?P[1-5])/step/(?P[1-5])", + ) + def update_workshop_step_status(self, request, pk, workshop, step): + ebios_rm_study: EbiosRMStudy = self.get_object() + workshop = int(workshop) + step = int(step) + # NOTE: For now, just set it as done. Will allow undoing this later. + ebios_rm_study.update_workshop_step_status( + workshop, step, new_status=request.data.get("status", "in_progress") + ) + return Response(EbiosRMStudyReadSerializer(ebios_rm_study).data) + class FearedEventViewSet(BaseModelViewSet): model = FearedEvent @@ -91,12 +110,23 @@ def gravity(self, request, pk): return Response(choices) +class RoToFilter(df.FilterSet): + used = df.BooleanFilter(method="is_used", label="Used") + + def is_used(self, queryset, name, value): + if value: + return queryset.filter(strategicscenario__isnull=False) + return queryset.filter(strategicscenario__isnull=True) + + class Meta: + model = RoTo + fields = ["ebios_rm_study", "is_selected", "used"] + + class RoToViewSet(BaseModelViewSet): model = RoTo - filterset_fields = [ - "ebios_rm_study", - ] + filterset_class = RoToFilter @action(detail=False, name="Get risk origin choices", url_path="risk-origin") def risk_origin(self, request): @@ -116,6 +146,7 @@ class StakeholderViewSet(BaseModelViewSet): filterset_fields = [ "ebios_rm_study", + "is_selected", ] @action(detail=False, name="Get category choices") @@ -123,14 +154,33 @@ def category(self, request): return Response(dict(Stakeholder.Category.choices)) -class AttackPathViewSet(BaseModelViewSet): - model = AttackPath +class StrategicScenarioViewSet(BaseModelViewSet): + model = StrategicScenario filterset_fields = [ "ebios_rm_study", ] +class AttackPathFilter(df.FilterSet): + used = df.BooleanFilter(method="is_used", label="Used") + + def is_used(self, queryset, name, value): + if value: + return queryset.filter(operational_scenario__isnull=False) + return queryset.filter(operational_scenario__isnull=True) + + class Meta: + model = AttackPath + fields = ["ebios_rm_study", "is_selected", "used"] + + +class AttackPathViewSet(BaseModelViewSet): + model = AttackPath + + filterset_class = AttackPathFilter + + class OperationalScenarioViewSet(BaseModelViewSet): model = OperationalScenario diff --git a/backend/library/libraries/risk-matrix-6x6-detailed.yaml b/backend/library/libraries/risk-matrix-6x6-detailed.yaml new file mode 100644 index 000000000..96a2de189 --- /dev/null +++ b/backend/library/libraries/risk-matrix-6x6-detailed.yaml @@ -0,0 +1,204 @@ +urn: urn:intuitem:risk:library:risk-matrix-6x6-detailed +locale: fr +ref_id: risk-matrix-6x6-detailed +name: 6x6 detailed +description: 6x6 detailed example +copyright: domaine public +version: 1 +provider: intuitem +packager: intuitem +objects: + risk_matrix: + - urn: urn:intuitem:risk:matrix:6x6-detailed + ref_id: risk-matrix-6x6-detailed + name: 6x6 detailed + description: 6x6 detailed example + probability: + - id: 0 + abbreviation: EX + name: Exceptionnel + description: Une fois tous les 5 ans ou 1 tous les 10.000 (< 1%) + translations: + en: + name: Exceptional + description: Once every 5 years or every 10,000 days (< 1%) + hexcolor: '#00B050' + - id: 1 + abbreviation: RA + name: Rare + description: Une fois par an ou 1 tous les 1.000 (< 5%) + translations: + en: + name: Rare + description: Once per year or every 1,000 days (< 5%) + hexcolor: '#FFFF00' + - id: 2 + abbreviation: UL + name: "Peu fr\xE9quent" + description: Une fois par trimestre ou 1 tous les 100 (< 10%) + translations: + en: + name: Uncommon + description: Once per quarter or every 100 days (< 10%) + hexcolor: '#FFC000' + - id: 3 + abbreviation: LI + name: "Fr\xE9quent" + description: Une fois par mois ou 1 tous les 50 (< 20%) + translations: + en: + name: Common + description: Once per month or every 50 days (< 20%) + hexcolor: '#FF0000' + - id: 4 + abbreviation: VF + name: "Tr\xE8s fr\xE9quent" + description: Une fois par semaine ou 1 tous les 10 (<90%) + translations: + en: + name: Very frequent + description: Once per week or every 10 days (< 90%) + hexcolor: '#FF0000' + - id: 5 + abbreviation: RE + name: "R\xE9current" + description: Une fois par jour 1 tous les 2 (> 90%) + translations: + en: + name: Recurrent + description: Once per day or every 2 days (> 90%) + hexcolor: '#FF0000' + impact: + - id: 0 + abbreviation: LO + name: 'Faible ' + description: "<10k\u20AC " + translations: + en: + name: 'Low ' + description: "<10k\u20AC " + hexcolor: '#00B050' + - id: 1 + abbreviation: MI + name: "Mod\xE9r\xE9 " + description: " entre 10 et 50k\u20AC " + translations: + en: + name: Moderate + description: " from 10 to 50k\u20AC " + hexcolor: '#FFFF00' + - id: 2 + abbreviation: SI + name: 'Significatif ' + description: " entre 50 et 100k\u20AC " + translations: + en: + name: 'Significant ' + description: " from 50 to 100k\u20AC " + hexcolor: '#FFC000' + - id: 3 + abbreviation: SE + name: "S\xE9rieux " + description: "entre 100 et 500 k\u20AC" + translations: + en: + name: Serious + description: "from 100 to 500 k\u20AC" + hexcolor: '#FF0000' + - id: 4 + abbreviation: CR + name: 'Critique ' + description: "entre 500 et 1 000 k\u20AC" + translations: + en: + name: 'Critical ' + description: "from 500 to 1 000 k\u20AC" + hexcolor: '#FF0000' + - id: 5 + abbreviation: CA + name: 'Catastrophique ' + description: "> 1 000 k\u20AC" + translations: + en: + name: 'Catastrophic ' + description: "> 1 000 k\u20AC" + hexcolor: '#FF0000' + risk: + - id: 0 + abbreviation: LO + name: Faible + description: "Risque n\xE9gligeable" + translations: + en: + name: Low + description: Negligible risk + hexcolor: '#00B050' + - id: 1 + abbreviation: MO + name: "Mod\xE9r\xE9" + description: "Risque relevant de l'activit\xE9 courante du m\xE9tier (dispositifs\ + \ de\nma\xEEtrise inscrits dans les proc\xE9dures et outils)" + translations: + en: + name: Moderate + description: Risk related to routine business activity (control measures + defined in procedures and tools) + hexcolor: '#FFFF00' + - id: 2 + abbreviation: SI + name: Significatif + description: "Risque demandant un niveau de ma\xEEtrise satisfaisant et un suivi\ + \ par le m\xE9tier" + translations: + en: + name: Significant + description: Risk requiring satisfactory control and monitoring by the business + hexcolor: '#FFC000' + - id: 3 + abbreviation: MA + name: Majeur + description: "Risque prioritaire dont le niveau de ma\xEEtrise doit \xEAtre\ + \ suivi par la Direction m\xE9tier en relation avec le RSSI" + translations: + en: + name: Major + description: Priority risk where control levels must be monitored by business + management in coordination with the CISO. + hexcolor: '#FF0000' + grid: + - - 0 + - 0 + - 0 + - 0 + - 1 + - 3 + - - 0 + - 0 + - 1 + - 1 + - 2 + - 3 + - - 0 + - 1 + - 1 + - 2 + - 2 + - 3 + - - 0 + - 1 + - 2 + - 2 + - 2 + - 3 + - - 0 + - 1 + - 2 + - 2 + - 3 + - 3 + - - 1 + - 2 + - 2 + - 3 + - 3 + - 3 diff --git a/documentation/architecture/data-model.md b/documentation/architecture/data-model.md index cdce6f2be..dce95dae0 100644 --- a/documentation/architecture/data-model.md +++ b/documentation/architecture/data-model.md @@ -1348,7 +1348,8 @@ erDiagram EBIOS_RM_STUDY }o--o{ COMPLIANCE_ASSESSMENT: leverages EBIOS_RM_STUDY }o--|| RISK_MATRIX : leverages EBIOS_RM_STUDY |o--o{ RISK_ASSESSMENT : generates - ATTACK_PATH }o--|| RO_TO : derives + STRATEGIC_SCENARIO }o--|| RO_TO : derives_from + ATTACK_PATH }o--|| STRATEGIC_SCENARIO : derives RO_TO }o--o{ FEARED_EVENT : corresponds_to OPERATIONAL_SCENARIO |o--|| ATTACK_PATH : derives OPERATIONAL_SCENARIO }o--o{ THREAT : leverages @@ -1405,6 +1406,12 @@ erDiagram string justification } + STRATEGIC_SCENARIO { + string ref_id + string name + string description + } + ATTACK_PATH { string ref_id string name diff --git a/enterprise/backend/enterprise_core/settings.py b/enterprise/backend/enterprise_core/settings.py index 88d783fe1..6e6ab658d 100644 --- a/enterprise/backend/enterprise_core/settings.py +++ b/enterprise/backend/enterprise_core/settings.py @@ -142,8 +142,8 @@ def set_ciso_assistant_url(_, __, event_dict): "iam", "global_settings", "tprm", - "core", "ebios_rm", + "core", "cal", "django_filters", "library", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 659e7ba99..3a82e3e6d 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -923,24 +923,24 @@ "ebiosWs5_3": "Define security measures", "ebiosWs5_4": "Assess and document residual risks", "ebiosWs5_5": "Establish risk monitoring framework", - "activity": "Activity", + "activity": "Step", "ebiosRmMatrixHelpText": "Risk matrix used as a reference for the study. Defaults to `urn:intuitem:risk:library:risk-matrix-4x4-ebios-rm`", - "activityOne": "Activity 1", - "activityTwo": "Activity 2", - "activityThree": "Activity 3", + "activityOne": "Step 1", + "activityTwo": "Step 2", + "activityThree": "Step 3", "ebiosRmStudy": "Ebios RM study", "qualifications": "Qualifications", "impacts": "Impacts", "ebiosRmStudies": "Ebios RM studies", "bringTheEvidences": "Bring the evidences", "bringTheEvidencesHelpText": "If disabled, the object will be duplicated without its evidences", - "gravity": "Gravity", + "gravity": "Severity", "existingControlsHelper": "What do you currently have to manage this risk", "extraControlsHelper": "What will you do to mitigate this risk", "existingContextHelper": "Description of the existing mitigations (this field will be deprecated soon)", "fearedEvent": "Feared event", "fearedEvents": "Feared events", - "isSelected": "Is selected", + "isSelected": "Selected", "ebiosRM": "Ebios RM", "riskOrigin": "Risk origin", "targetObjective": "Target objective", @@ -962,7 +962,7 @@ "organizedCrime": "Organized crime", "terrorist": "Terrorist", "activist": "Activist", - "professional": "Professional", + "professional": "Competitor", "amateur": "Amateur", "avenger": "Avenger", "pathological": "Pathological", @@ -983,9 +983,6 @@ "currentCriticality": "Current criticality", "residualCriticality": "Residual criticality", "notSelected": "Not selected", - "identifyRoTo": "Identify RO/TO", - "evaluateRoTo": "Evaluate RO/TO", - "selectRoTo": "Select RO/TO", "resetPasswordHere": "You can reset your password here.", "resetPassword": "Reset password", "ebiosRm": "Ebios RM", @@ -997,5 +994,24 @@ "noReviewer": "No reviewer assigned", "selectAudit": "Select audit", "errorAssetGraphMustNotContainCycles": "The asset graph must not contain cycles.", - "addStakeholder": "Add stakeholder" + "addStakeholder": "Add stakeholder", + "operationalScenario": "Operational scenario", + "operationalScenarioRefId": "Operational scenario {refId}", + "operationalScenarios": "Operational scenarios", + "addOperationalScenario": "Add operational scenario", + "workshopFour": "Workshop 4", + "noThreat": "No threat", + "likely": "Likely", + "unlikely": "Unlikely", + "veryLikely": "Very likely", + "certain": "Certain", + "minor": "Minor", + "operatingModesDescription": "Operating modes description", + "noStakeholders": "No stakeholders", + "strategicScenario": "Strategic scenario", + "strategicScenarios": "Strategic scenarios", + "goBackToEbiosRmStudy": "Go back to Ebios RM study", + "addStrategicScenario": "Add strategic scenario", + "markAsDone": "Mark as done", + "markAsInProgress": "Mark as in progress" } diff --git a/frontend/messages/fr.json b/frontend/messages/fr.json index 6211b45f9..a7e674606 100644 --- a/frontend/messages/fr.json +++ b/frontend/messages/fr.json @@ -797,6 +797,14 @@ "proof": "Preuve", "privacy": "Vie privée", "safety": "Sûreté", + "rto": "RTO", + "rtoHelpText": "Objectif de temps de récupération", + "rpo": "RPO", + "rpoHelpText": "Objectif du point de récupération", + "mtd": "MTD", + "mtdHelpText": "Temps d'arrêt maximal tolérable", + "securityObjectives": "Objectifs de sécurité", + "disasterRecoveryObjectives": "Objectifs de reprise d'activité", "noExpirationDateSet": "Aucune date d'expiration définie", "sumpageTotal": "Total", "sumpageActive": "Actif", @@ -858,6 +866,11 @@ "exploitable": "Exploitable", "mitigated": "Atténuée", "fixed": "Fixée", + "general": "Général", + "generalSettingsDescription": "Configurez vos paramètres ici.", + "securityObjectiveScale": "Échelle des objectifs de sécurité", + "generalSettingsUpdated": "Paramètres mis à jour", + "securityObjectiveScaleHelpText": "Choisissez votre échelle d'objectifs de sécurité (1 à 4 par défaut)", "labels": "Étiquettes", "addLabel": "Ajouter une étiquette", "labelsHelpText": "Les étiquettes sont utilisées pour catégoriser et filtrer les éléments.", @@ -871,12 +884,23 @@ "tags": "Étiquettes", "addTag": "Ajouter une étiquette", "tagsHelpText": "Les étiquettes sont utilisées pour classer et filtrer les éléments. Vous pouvez ajouter des étiquettes dans la section Extra", + "enabled": "Activé", + "hours": "Heures", + "minutes": "Minutes", + "seconds": "Secondes", + "days": "Jours", + "milliseconds": "Millisecondes", "existingMeasures": "Mesures existantes", + "youCanSetPasswordHere": "Vous pouvez définir votre mot de passe ici", "forgotPassword": "Mot de passe oublié", "ssoSettingsUpdated": "Paramètres SSO mis à jour", "scoreSemiColon": "Score:", "mappingInferenceHelpText": "Ces variables sont fixes et ne changeront pas en fonction de la source.", "priority": "Priorité", + "p1": "P1", + "p2": "P2", + "p3": "P3", + "p4": "P4", "ebiosWs1": "Atelier 1 : Cadrage et Socle de sécurité", "ebiosWs2": "Atelier 2 : Sources de risque", "ebiosWs3": "Atelier 3 : Scénarios stratégiques", @@ -900,11 +924,92 @@ "ebiosWs5_4": "Évaluer et documenter les risques résiduels", "ebiosWs5_5": "Mettre en place le cadre de suivi des risques", "activity": "Activité", + "ebiosRmMatrixHelpText": "Matrice de risque utilisée pour l'étude. La valeur par défaut est `urn:intuitem:risk:library:risk-matrix-4x4-ebios-rm`", + "activityOne": "Activité 1", + "activityTwo": "Activité 2", + "activityThree": "Activité 3", + "ebiosRmStudy": "Étude Ebios RM", + "qualifications": "Qualifications", + "impacts": "Impacts", + "ebiosRmStudies": "Études Ebios RM", "bringTheEvidences": "Apportez les preuves", "bringTheEvidencesHelpText": "Si désactivé, l'objet sera dupliqué sans ses preuves", + "gravity": "Gravité", "existingControlsHelper": "Que disposez-vous actuellement pour gérer ce risque", "extraControlsHelper": "Que ferez-vous pour atténuer ce risque", "existingContextHelper": "Description des mesures existantes (ce champ sera bientôt obsolète)", + "fearedEvent": "Événement redouté", + "fearedEvents": "Evénements redoutés", + "isSelected": "Retenu", + "ebiosRM": "Ebios RM", + "riskOrigin": "Source de risque", + "targetObjective": "Objectif visé", + "motivation": "Motivation", + "resources": "Ressources", + "pertinence": "Pertinence", + "limited": "Limité", + "significant": "Significatif", + "important": "Important", + "unlimited": "Illimité", + "strong": "Fort", + "irrelevant": "Non pertinent", + "partiallyRelevant": "Partiellement pertinent", + "fairlyRelevant": "Assez pertinent", + "highlyRelevant": "Très pertinent", + "roTo": "SR/OV", + "roToCouple": "Couple SR/OV", + "addRoto": "Ajouter un couple SR/OV", + "organizedCrime": "Crime organisé", + "terrorist": "Terroriste", + "activist": "Activiste", + "professional": "Concurrent", + "amateur": "Amateur", + "avenger": "Vengeur", + "pathological": "Pathologique", + "currentDependency": "Dépendance actuelle", + "currentPenetration": "Pénétration actuelle", + "currentMaturity": "Maturité actuelle", + "currentTrust": "Confiance actuelle", + "residualDependency": "Dépendance résiduelle", + "residualPenetration": "Pénétration résiduelle", + "residualMaturity": "Maturité résiduelle", + "residualTrust": "Confiance résiduelle", + "selected": "Retenu", + "likelihood": "Vraisemblance", + "stakeholders": "Parties prenantes", + "addAttackPath": "Ajouter un chemin d'attaque", + "attackPath": "Chemin d'attaque", + "attackPaths": "Chemins d'attaque", + "currentCriticality": "Criticité actuelle", + "residualCriticality": "Criticité résiduelle", + "notSelected": "Non retenu", "resetPasswordHere": "Vous pouvez réinitialiser votre mot de passe ici.", - "resetPassword": "Réinitialiser le mot de passe" + "resetPassword": "Réinitialiser le mot de passe", + "ebiosRm": "Ebios RM", + "workshopOne": "Atelier 1", + "refIdSemiColon": "ID :", + "addFearedEvent": "Ajouter un événement redouté", + "addEbiosRMstudy": "Ajouter une étude Ebios RM", + "noAuthor": "Aucun auteur attribué", + "noReviewer": "Aucun relecteur assigné", + "selectAudit": "Sélectionner un audit", + "errorAssetGraphMustNotContainCycles": "Le graphe des actifs ne doit pas contenir de boucles.", + "addStakeholder": "Ajouter une partie prenante", + "operationalScenario": "Scénario opérationnel", + "operationalScenarioRefId": "Scénario opérationnel {refId}", + "operationalScenarios": "Scénarios opérationnels", + "addOperationalScenario": "Ajouter un scénario opérationnel", + "workshopFour": "Atelier 4", + "noThreat": "Aucune menace", + "likely": "Probable", + "unlikely": "Peu probable", + "veryLikely": "Très probable", + "certain": "Certain", + "minor": "Mineure", + "operatingModesDescription": "Description des modes opératoires", + "noStakeholders": "Aucune partie prenante", + "strategicScenario": "Scénario stratégique", + "strategicScenarios": "Scénarios stratégiques", + "goBackToEbiosRmStudy": "Retour à l'étude", + "addStrategicScenario": "Ajouter un scénario stratégique" } diff --git a/frontend/src/lib/components/CommandPalette/paletteData.ts b/frontend/src/lib/components/CommandPalette/paletteData.ts index 8903454cc..491235c04 100644 --- a/frontend/src/lib/components/CommandPalette/paletteData.ts +++ b/frontend/src/lib/components/CommandPalette/paletteData.ts @@ -34,6 +34,10 @@ export const navigationLinks: NavigationLink[] = [ label: 'riskScenarios', href: '/risk-scenarios' }, + { + label: 'ebiosRM', + href: '/ebios-rm' + }, { label: 'actionPlan', href: '/applied-controls' diff --git a/frontend/src/lib/components/DetailView/DetailView.svelte b/frontend/src/lib/components/DetailView/DetailView.svelte index c9762a4c0..bcd978e68 100644 --- a/frontend/src/lib/components/DetailView/DetailView.svelte +++ b/frontend/src/lib/components/DetailView/DetailView.svelte @@ -204,6 +204,17 @@ !data.data.builtin ); }; + + export let orderRelatedModels = ['']; + if (data.urlModel === 'projects') { + orderRelatedModels = ['compliance-assessments', 'risk-assessments', 'entity-assessments']; + } + if (data.urlModel === 'entities') { + orderRelatedModels = ['entity-assessments', 'representatives', 'solutions']; + } + if (data.urlModel === 'folders') { + orderRelatedModels = ['projects', 'entities']; + }
@@ -313,7 +324,7 @@ {:else} -- {/if} - {:else if value.id} + {:else if value.id && !value.hexcolor} {@const itemHref = `/${ URL_MODEL_MAP[data.urlModel]['foreignKeyFields']?.find( (item) => item.field === key @@ -409,7 +420,9 @@ {#if Object.keys(data.relatedModels).length > 0}
- {#each Object.entries(data.relatedModels) as [urlmodel, model], index} + {#each Object.entries(data.relatedModels).sort((a, b) => { + return orderRelatedModels.indexOf(a[0]) - orderRelatedModels.indexOf(b[0]); + }) as [urlmodel, model], index} {safeTranslate(model.info.localNamePlural)} {#if model.table.body.length > 0} diff --git a/frontend/src/lib/components/Forms/ModelForm.svelte b/frontend/src/lib/components/Forms/ModelForm.svelte index 8ca71675b..97393bc4b 100644 --- a/frontend/src/lib/components/Forms/ModelForm.svelte +++ b/frontend/src/lib/components/Forms/ModelForm.svelte @@ -47,6 +47,7 @@ import { createModalCache } from '$lib/utils/stores'; import FilteringLabelForm from './ModelForm/FilteringLabelForm.svelte'; import OperationalScenarioForm from './ModelForm/OperationalScenarioForm.svelte'; + import StrategicScenarioForm from './ModelForm/StrategicScenarioForm.svelte'; export let form: SuperValidated; export let invalidateAll = true; // set to false to keep form data using muliple forms on a page @@ -270,10 +271,12 @@ {:else if URLModel === 'stakeholders'} + {:else if URLModel === 'strategic-scenarios'} + {:else if URLModel === 'attack-paths'} {:else if URLModel === 'operational-scenarios'} - + {/if}
{#if closeModal} diff --git a/frontend/src/lib/components/Forms/ModelForm/AttackPathForm.svelte b/frontend/src/lib/components/Forms/ModelForm/AttackPathForm.svelte index e6d7ab3a6..7619db182 100644 --- a/frontend/src/lib/components/Forms/ModelForm/AttackPathForm.svelte +++ b/frontend/src/lib/components/Forms/ModelForm/AttackPathForm.svelte @@ -12,17 +12,18 @@ export let cacheLocks: Record = {}; export let formDataCache: Record = {}; export let initialData: Record = {}; - export let context: 'create' | 'edit' = 'create';