From 2dd31a2db502becda17ece13873de31ae9d2a650 Mon Sep 17 00:00:00 2001 From: Mohamed-Hacene Date: Tue, 17 Dec 2024 20:00:45 +0100 Subject: [PATCH] feat: add strategic scenario --- backend/core/startup.py | 4 ++ ...ro_to_couple_strategicscenario_and_more.py | 47 +++++++++++++++++++ backend/ebios_rm/models.py | 33 ++++++++++++- backend/ebios_rm/serializers.py | 19 +++++++- backend/ebios_rm/urls.py | 2 + backend/ebios_rm/views.py | 9 ++++ frontend/messages/en.json | 4 +- .../src/lib/components/Forms/ModelForm.svelte | 3 ++ .../Forms/ModelForm/AttackPathForm.svelte | 15 +++--- .../ModelForm/StrategicScenarioForm.svelte | 34 ++++++++++++++ frontend/src/lib/utils/crud.ts | 24 ++++++---- frontend/src/lib/utils/schemas.ts | 9 +++- frontend/src/lib/utils/table.ts | 8 +++- frontend/src/lib/utils/types.ts | 1 + .../operational-scenario/+page.server.ts | 14 ++++++ .../strategic-scenarios/+page.server.ts | 6 +-- 16 files changed, 208 insertions(+), 24 deletions(-) create mode 100644 backend/ebios_rm/migrations/0007_remove_attackpath_ro_to_couple_strategicscenario_and_more.py create mode 100644 frontend/src/lib/components/Forms/ModelForm/StrategicScenarioForm.svelte 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/ebios_rm/migrations/0007_remove_attackpath_ro_to_couple_strategicscenario_and_more.py b/backend/ebios_rm/migrations/0007_remove_attackpath_ro_to_couple_strategicscenario_and_more.py new file mode 100644 index 000000000..8184e449c --- /dev/null +++ b/backend/ebios_rm/migrations/0007_remove_attackpath_ro_to_couple_strategicscenario_and_more.py @@ -0,0 +1,47 @@ +# Generated by Django 5.1.4 on 2024-12-17 18:10 + +import django.db.models.deletion +import iam.models +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ebios_rm', '0006_alter_attackpath_stakeholders'), + ('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 71d6a9db9..295cb7769 100644 --- a/backend/ebios_rm/models.py +++ b/backend/ebios_rm/models.py @@ -374,10 +374,11 @@ def residual_criticality(self): ) -class AttackPath(NameDescriptionMixin, FolderMixin): +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( @@ -386,6 +387,31 @@ 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) + + class Meta: + verbose_name = _("Strategic Scenario") + verbose_name_plural = _("Strategic Scenarios") + ordering = ["created_at"] + + def save(self, *args, **kwargs): + self.folder = self.ebios_rm_study.folder + super().save(*args, **kwargs) + + +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"), @@ -404,8 +430,13 @@ class Meta: ordering = ["created_at"] 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): diff --git a/backend/ebios_rm/serializers.py b/backend/ebios_rm/serializers.py index 9e362dae2..9a56bd5d2 100644 --- a/backend/ebios_rm/serializers.py +++ b/backend/ebios_rm/serializers.py @@ -8,6 +8,7 @@ FearedEvent, RoTo, Stakeholder, + StrategicScenario, AttackPath, OperationalScenario, ) @@ -128,10 +129,26 @@ class Meta: 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() + + 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): diff --git a/backend/ebios_rm/urls.py b/backend/ebios_rm/urls.py index 76f7b3e2e..4af4f7096 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,7 @@ 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 5c9515d04..1c04b7f39 100644 --- a/backend/ebios_rm/views.py +++ b/backend/ebios_rm/views.py @@ -7,6 +7,7 @@ FearedEvent, RoTo, Stakeholder, + StrategicScenario, AttackPath, OperationalScenario, ) @@ -124,6 +125,14 @@ def category(self, request): return Response(dict(Stakeholder.Category.choices)) +class StrategicScenarioViewSet(BaseModelViewSet): + model = StrategicScenario + + filterset_fields = [ + "ebios_rm_study", + ] + + class AttackPathFilter(df.FilterSet): used = df.BooleanFilter(method="is_used", label="Used") diff --git a/frontend/messages/en.json b/frontend/messages/en.json index f1854fc09..49abb549f 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -1007,5 +1007,7 @@ "certain": "Certain", "minor": "Minor", "operatingModesDescription": "Operating modes description", - "noStakeholders": "No stakeholders" + "noStakeholders": "No stakeholders", + "strategicScenario": "Strategic scenario", + "strategicScenarios": "Strategic scenarios" } diff --git a/frontend/src/lib/components/Forms/ModelForm.svelte b/frontend/src/lib/components/Forms/ModelForm.svelte index 939955bce..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,6 +271,8 @@ {:else if URLModel === 'stakeholders'} + {:else if URLModel === 'strategic-scenarios'} + {:else if URLModel === 'attack-paths'} {:else if URLModel === 'operational-scenarios'} 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';