Skip to content

Commit

Permalink
Add strategic scenario model (#1206)
Browse files Browse the repository at this point in the history
  • Loading branch information
nas-tabchiche authored Dec 20, 2024
2 parents e5b1b38 + d521e7c commit 182615d
Show file tree
Hide file tree
Showing 27 changed files with 643 additions and 89 deletions.
20 changes: 13 additions & 7 deletions backend/core/base_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions backend/core/startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,10 @@
"view_stakeholder",
"change_stakeholder",
"delete_stakeholder",
"add_strategicscenario",
"view_strategicscenario",
"change_strategicscenario",
"delete_strategicscenario",
"add_attackpath",
"view_attackpath",
"change_attackpath",
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
),
]
69 changes: 67 additions & 2 deletions backend/ebios_rm/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,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")
Expand Down Expand Up @@ -309,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}"

Expand Down Expand Up @@ -338,7 +342,7 @@ def get_pertinence(self):
def get_gravity(self):
gravity = -1
for feared_event in self.feared_events.all():
if feared_event.gravity > gravity:
if feared_event.gravity > gravity and feared_event.is_selected:
gravity = feared_event.gravity
return gravity

Expand Down Expand Up @@ -420,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()}"

Expand Down Expand Up @@ -473,10 +482,11 @@ def get_residual_criticality_display(self) -> str:
)


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(
Expand All @@ -485,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"),
Expand All @@ -497,15 +552,25 @@ 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()
Expand Down
29 changes: 22 additions & 7 deletions backend/ebios_rm/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
FearedEvent,
RoTo,
Stakeholder,
StrategicScenario,
AttackPath,
OperationalScenario,
)
Expand Down Expand Up @@ -104,12 +105,8 @@ class Meta:


class StakeholderWriteSerializer(BaseModelSerializer):
current_criticality = serializers.CharField(
source="get_current_criticality_display"
)
residual_criticality = serializers.CharField(
source="get_residual_criticality_display"
)
current_criticality = serializers.IntegerField(read_only=True)
residual_criticality = serializers.IntegerField(read_only=True)

class Meta:
model = Stakeholder
Expand All @@ -136,10 +133,28 @@ 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()
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):
Expand Down
4 changes: 4 additions & 0 deletions backend/ebios_rm/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
FearedEventViewSet,
RoToViewSet,
StakeholderViewSet,
StrategicScenarioViewSet,
AttackPathViewSet,
OperationalScenarioViewSet,
)
Expand All @@ -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",
Expand Down
27 changes: 24 additions & 3 deletions backend/ebios_rm/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
FearedEvent,
RoTo,
Stakeholder,
StrategicScenario,
AttackPath,
OperationalScenario,
)
Expand Down Expand Up @@ -109,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):
Expand All @@ -134,13 +146,22 @@ class StakeholderViewSet(BaseModelViewSet):

filterset_fields = [
"ebios_rm_study",
"is_selected",
]

@action(detail=False, name="Get category choices")
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")

Expand Down
Loading

0 comments on commit 182615d

Please sign in to comment.