diff --git a/backend/ciso_assistant/settings.py b/backend/ciso_assistant/settings.py index 32b1a25d3..708220052 100644 --- a/backend/ciso_assistant/settings.py +++ b/backend/ciso_assistant/settings.py @@ -140,6 +140,7 @@ def set_ciso_assistant_url(_, __, event_dict): "global_settings", "tprm", "core", + "ebios_rm", "cal", "django_filters", "library", diff --git a/backend/core/migrations/0044_qualification.py b/backend/core/migrations/0044_qualification.py new file mode 100644 index 000000000..39592ce97 --- /dev/null +++ b/backend/core/migrations/0044_qualification.py @@ -0,0 +1,130 @@ +# Generated by Django 5.1.1 on 2024-12-02 17:01 + +import django.db.models.deletion +import iam.models +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0043_historicalmetric"), + ("iam", "0009_create_allauth_emailaddress_objects"), + ] + + operations = [ + migrations.CreateModel( + name="Qualification", + 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"), + ), + ( + "urn", + models.CharField( + blank=True, + max_length=255, + null=True, + unique=True, + verbose_name="URN", + ), + ), + ( + "ref_id", + models.CharField( + blank=True, + max_length=100, + null=True, + verbose_name="Reference ID", + ), + ), + ( + "provider", + models.CharField( + blank=True, max_length=200, null=True, verbose_name="Provider" + ), + ), + ( + "name", + models.CharField(max_length=200, null=True, verbose_name="Name"), + ), + ( + "description", + models.TextField(blank=True, null=True, verbose_name="Description"), + ), + ( + "annotation", + models.TextField(blank=True, null=True, verbose_name="Annotation"), + ), + ( + "translations", + models.JSONField( + blank=True, null=True, verbose_name="Translations" + ), + ), + ( + "locale", + models.CharField( + default="en", max_length=100, verbose_name="Locale" + ), + ), + ( + "default_locale", + models.BooleanField(default=True, verbose_name="Default locale"), + ), + ( + "abbreviation", + models.CharField( + blank=True, + max_length=20, + null=True, + verbose_name="Abbreviation", + ), + ), + ( + "qualification_ordering", + models.PositiveSmallIntegerField( + default=0, verbose_name="Ordering" + ), + ), + ( + "security_objective_ordering", + models.PositiveSmallIntegerField( + default=0, verbose_name="Security objective ordering" + ), + ), + ( + "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": "Qualification", + "verbose_name_plural": "Qualifications", + "ordering": ["qualification_ordering"], + }, + ), + ] diff --git a/backend/core/models.py b/backend/core/models.py index fa0d57fb9..1a72b87f2 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -1191,6 +1191,108 @@ def coverage(self) -> str: return RequirementMapping.Coverage.PARTIAL +class Qualification(ReferentialObjectMixin, I18nObjectMixin): + DEFAULT_QUALIFICATIONS = [ + { + "abbreviation": "C", + "qualification_ordering": 1, + "security_objective_ordering": 1, + "name": "Confidentiality", + "urn": "urn:intuitem:risk:qualification:confidentiality", + }, + { + "abbreviation": "I", + "qualification_ordering": 2, + "security_objective_ordering": 2, + "name": "Integrity", + "urn": "urn:intuitem:risk:qualification:integrity", + }, + { + "abbreviation": "A", + "qualification_ordering": 3, + "security_objective_ordering": 3, + "name": "Availability", + "urn": "urn:intuitem:risk:qualification:availability", + }, + { + "abbreviation": "P", + "qualification_ordering": 4, + "security_objective_ordering": 4, + "name": "Proof", + "urn": "urn:intuitem:risk:qualification:proof", + }, + { + "abbreviation": "Aut", + "qualification_ordering": 5, + "security_objective_ordering": 5, + "name": "Authenticity", + "urn": "urn:intuitem:risk:qualification:authenticity", + }, + { + "abbreviation": "Priv", + "qualification_ordering": 6, + "security_objective_ordering": 6, + "name": "Privacy", + "urn": "urn:intuitem:risk:qualification:privacy", + }, + { + "abbreviation": "Safe", + "qualification_ordering": 7, + "security_objective_ordering": 7, + "name": "Safety", + "urn": "urn:intuitem:risk:qualification:safety", + }, + { + "abbreviation": "Rep", + "qualification_ordering": 8, + "name": "Reputation", + "urn": "urn:intuitem:risk:qualification:reputation", + }, + { + "abbreviation": "Ope", + "qualification_ordering": 9, + "name": "Operational", + "urn": "urn:intuitem:risk:qualification:operational", + }, + { + "abbreviation": "Leg", + "qualification_ordering": 10, + "name": "Legal", + "urn": "urn:intuitem:risk:qualification:legal", + }, + { + "abbreviation": "Fin", + "qualification_ordering": 11, + "name": "Financial", + "urn": "urn:intuitem:risk:qualification:financial", + }, + ] + + abbreviation = models.CharField( + max_length=20, null=True, blank=True, verbose_name=_("Abbreviation") + ) + qualification_ordering = models.PositiveSmallIntegerField( + verbose_name=_("Ordering"), default=0 + ) + security_objective_ordering = models.PositiveSmallIntegerField( + verbose_name=_("Security objective ordering"), default=0 + ) + + class Meta: + verbose_name = _("Qualification") + verbose_name_plural = _("Qualifications") + ordering = ["qualification_ordering"] + + @classmethod + def create_default_qualifications(cls): + for qualification in cls.DEFAULT_QUALIFICATIONS: + Qualification.objects.update_or_create( + urn=qualification["urn"], + defaults=qualification, + create_defaults=qualification, + ) + + ########################### Domain objects ######################### diff --git a/backend/core/startup.py b/backend/core/startup.py index f3faad47d..d74419221 100644 --- a/backend/core/startup.py +++ b/backend/core/startup.py @@ -361,6 +361,7 @@ def startup(sender: AppConfig, **kwargs): """ from django.contrib.auth.models import Permission + from core.models import Qualification from iam.models import Folder, Role, RoleAssignment, User, UserGroup from tprm.models import Entity @@ -490,7 +491,13 @@ def startup(sender: AppConfig, **kwargs): email=CISO_ASSISTANT_SUPERUSER_EMAIL, is_superuser=True ) except Exception as e: - print(e) # NOTE: Add this exception in the logger + logger.error("Error creating superuser", exc_info=e) + + # Create default Qualifications + try: + Qualification.create_default_qualifications() + except Exception as e: + logger.error("Error creating default qualifications", exc_info=e) call_command("storelibraries") diff --git a/backend/ebios_rm/__init__.py b/backend/ebios_rm/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/ebios_rm/apps.py b/backend/ebios_rm/apps.py new file mode 100644 index 000000000..2c46fb400 --- /dev/null +++ b/backend/ebios_rm/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class EbiosRmConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "ebios_rm" diff --git a/backend/ebios_rm/migrations/0001_initial.py b/backend/ebios_rm/migrations/0001_initial.py new file mode 100644 index 000000000..b0fb9dc58 --- /dev/null +++ b/backend/ebios_rm/migrations/0001_initial.py @@ -0,0 +1,675 @@ +# Generated by Django 5.1.1 on 2024-12-03 12:57 + +import django.core.validators +import django.db.models.deletion +import iam.models +import tprm.models +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("core", "0044_qualification"), + ("iam", "0009_create_allauth_emailaddress_objects"), + ("tprm", "0003_entityassessment_representatives"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="EbiosRMStudy", + 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"), + ), + ("eta", models.DateField(blank=True, null=True, verbose_name="ETA")), + ( + "due_date", + models.DateField(blank=True, null=True, verbose_name="Due date"), + ), + ("ref_id", models.CharField(max_length=100)), + ( + "version", + models.CharField( + blank=True, + default="1.0", + help_text="Version of the Ebios RM study (eg. 1.0, 2.0, etc.)", + max_length=100, + null=True, + verbose_name="Version", + ), + ), + ( + "status", + models.CharField( + blank=True, + choices=[ + ("planned", "Planned"), + ("in_progress", "In progress"), + ("in_review", "In review"), + ("done", "Done"), + ("deprecated", "Deprecated"), + ], + default="planned", + max_length=100, + null=True, + verbose_name="Status", + ), + ), + ( + "observation", + models.TextField(blank=True, null=True, verbose_name="Observation"), + ), + ( + "assets", + models.ManyToManyField( + blank=True, + help_text="Assets that are pertinent to the study", + related_name="ebios_rm_studies", + to="core.asset", + verbose_name="Assets", + ), + ), + ( + "authors", + models.ManyToManyField( + blank=True, + related_name="authors", + to=settings.AUTH_USER_MODEL, + verbose_name="Authors", + ), + ), + ( + "compliance_assessments", + models.ManyToManyField( + blank=True, + help_text="Compliance assessments established as security baseline during workshop 1.4", + related_name="ebios_rm_studies", + to="core.complianceassessment", + verbose_name="Compliance assessments", + ), + ), + ( + "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", + ), + ), + ( + "reference_entity", + models.ForeignKey( + default=tprm.models.Entity.get_main_entity, + help_text="Entity that is the focus of the study", + on_delete=django.db.models.deletion.PROTECT, + related_name="ebios_rm_studies", + to="tprm.entity", + verbose_name="Reference entity", + ), + ), + ( + "reviewers", + models.ManyToManyField( + blank=True, + related_name="reviewers", + to=settings.AUTH_USER_MODEL, + verbose_name="Reviewers", + ), + ), + ( + "risk_assessments", + models.ManyToManyField( + blank=True, + help_text="Risk assessments generated at the end of workshop 4", + related_name="ebios_rm_studies", + to="core.riskassessment", + verbose_name="Risk assessments", + ), + ), + ( + "risk_matrix", + models.ForeignKey( + help_text="Risk matrix used as a reference for the study. Defaults to `urn:intuitem:risk:library:risk-matrix-4x4-ebios-rm`", + on_delete=django.db.models.deletion.PROTECT, + related_name="ebios_rm_studies", + to="core.riskmatrix", + verbose_name="Risk matrix", + ), + ), + ], + options={ + "verbose_name": "Ebios RM Study", + "verbose_name_plural": "Ebios RM Studies", + "ordering": ["created_at"], + }, + ), + migrations.CreateModel( + name="AttackPath", + 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"), + ), + ("description", models.TextField(verbose_name="Description")), + ( + "is_selected", + models.BooleanField(default=False, verbose_name="Is selected"), + ), + ( + "justification", + models.TextField(blank=True, verbose_name="Justification"), + ), + ( + "ebios_rm_study", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="ebios_rm.ebiosrmstudy", + verbose_name="EBIOS RM study", + ), + ), + ], + options={ + "verbose_name": "Attack path", + "verbose_name_plural": "Attack paths", + "ordering": ["created_at"], + }, + ), + migrations.CreateModel( + name="FearedEvent", + 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(max_length=100)), + ( + "gravity", + models.SmallIntegerField(default=-1, verbose_name="Gravity"), + ), + ( + "is_selected", + models.BooleanField(default=False, verbose_name="Is selected"), + ), + ( + "justification", + models.TextField(blank=True, verbose_name="Justification"), + ), + ( + "assets", + models.ManyToManyField( + blank=True, + help_text="Assets that are affected by the feared event", + related_name="feared_events", + to="core.asset", + verbose_name="Assets", + ), + ), + ( + "ebios_rm_study", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="ebios_rm.ebiosrmstudy", + verbose_name="EBIOS RM study", + ), + ), + ( + "qualifications", + models.ManyToManyField( + blank=True, + help_text="Qualifications carried by the feared event", + related_name="feared_events", + to="core.qualification", + verbose_name="Qualifications", + ), + ), + ], + options={ + "verbose_name": "Feared event", + "verbose_name_plural": "Feared events", + "ordering": ["created_at"], + }, + ), + migrations.CreateModel( + name="OperationalScenario", + 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"), + ), + ("description", models.TextField(verbose_name="Description")), + ( + "likelihood", + models.SmallIntegerField(default=-1, verbose_name="Likelihood"), + ), + ( + "is_selected", + models.BooleanField(default=False, verbose_name="Is selected"), + ), + ( + "justification", + models.TextField(blank=True, verbose_name="Justification"), + ), + ( + "attack_paths", + models.ManyToManyField( + help_text="Attack paths that are pertinent to the operational scenario", + related_name="operational_scenarios", + to="ebios_rm.attackpath", + verbose_name="Attack paths", + ), + ), + ( + "ebios_rm_study", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="operational_scenarios", + to="ebios_rm.ebiosrmstudy", + verbose_name="EBIOS RM study", + ), + ), + ( + "threats", + models.ManyToManyField( + blank=True, + help_text="Threats leveraged by the operational scenario", + related_name="operational_scenarios", + to="core.threat", + verbose_name="Threats", + ), + ), + ], + options={ + "verbose_name": "Operational scenario", + "verbose_name_plural": "Operational scenarios", + "ordering": ["created_at"], + }, + ), + migrations.CreateModel( + name="RoTo", + 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"), + ), + ( + "risk_origin", + models.CharField( + choices=[ + ("state", "State"), + ("organized_crime", "Organized crime"), + ("terrorist", "Terrorist"), + ("activist", "Activist"), + ("professional", "Professional"), + ("amateur", "Amateur"), + ("avenger", "Avenger"), + ("pathological", "Pathological"), + ], + max_length=32, + verbose_name="Risk origin", + ), + ), + ( + "target_objective", + models.CharField(max_length=200, verbose_name="Target objective"), + ), + ( + "motivation", + models.PositiveSmallIntegerField( + choices=[ + (0, "undefined"), + (1, "very_low"), + (2, "low"), + (3, "significant"), + (4, "strong"), + ], + default=0, + verbose_name="Motivation", + ), + ), + ( + "resources", + models.PositiveSmallIntegerField( + choices=[ + (0, "undefined"), + (1, "limited"), + (2, "significant"), + (3, "important"), + (4, "unlimited"), + ], + default=0, + verbose_name="Resources", + ), + ), + ( + "pertinence", + models.PositiveSmallIntegerField( + choices=[ + (0, "undefined"), + (1, "irrelevant"), + (2, "partially_relevant"), + (3, "fairly_relevant"), + (4, "highly_relevant"), + ], + default=0, + verbose_name="Pertinence", + ), + ), + ( + "activity", + models.PositiveSmallIntegerField( + default=0, + validators=[django.core.validators.MaxValueValidator(4)], + verbose_name="Activity", + ), + ), + ( + "is_selected", + models.BooleanField(default=False, verbose_name="Is selected"), + ), + ( + "justification", + models.TextField(blank=True, verbose_name="Justification"), + ), + ( + "ebios_rm_study", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="ebios_rm.ebiosrmstudy", + verbose_name="EBIOS RM study", + ), + ), + ( + "feared_events", + models.ManyToManyField( + related_name="ro_to_couples", + to="ebios_rm.fearedevent", + verbose_name="Feared events", + ), + ), + ], + options={ + "verbose_name": "RO/TO couple", + "verbose_name_plural": "RO/TO couples", + "ordering": ["created_at"], + }, + ), + migrations.AddField( + model_name="attackpath", + name="ro_to_couple", + field=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", + ), + ), + migrations.CreateModel( + name="Stakeholder", + 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"), + ), + ( + "category", + models.CharField( + choices=[ + ("client", "Client"), + ("partner", "Partner"), + ("supplier", "Supplier"), + ], + max_length=32, + verbose_name="Category", + ), + ), + ( + "current_dependency", + models.PositiveSmallIntegerField( + default=0, + validators=[django.core.validators.MaxValueValidator(4)], + verbose_name="Current dependency", + ), + ), + ( + "current_penetration", + models.PositiveSmallIntegerField( + default=0, + validators=[django.core.validators.MaxValueValidator(4)], + verbose_name="Current penetration", + ), + ), + ( + "current_maturity", + models.PositiveSmallIntegerField( + default=1, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(4), + ], + verbose_name="Current maturity", + ), + ), + ( + "current_trust", + models.PositiveSmallIntegerField( + default=1, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(4), + ], + verbose_name="Current trust", + ), + ), + ( + "residual_dependency", + models.PositiveSmallIntegerField( + default=0, + validators=[django.core.validators.MaxValueValidator(4)], + verbose_name="Residual dependency", + ), + ), + ( + "residual_penetration", + models.PositiveSmallIntegerField( + default=0, + validators=[django.core.validators.MaxValueValidator(4)], + verbose_name="Residual penetration", + ), + ), + ( + "residual_maturity", + models.PositiveSmallIntegerField( + default=1, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(4), + ], + verbose_name="Residual maturity", + ), + ), + ( + "residual_trust", + models.PositiveSmallIntegerField( + default=1, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(4), + ], + verbose_name="Residual trust", + ), + ), + ( + "is_selected", + models.BooleanField(default=False, verbose_name="Is selected"), + ), + ( + "justification", + models.TextField(blank=True, verbose_name="Justification"), + ), + ( + "applied_controls", + models.ManyToManyField( + blank=True, + help_text="Controls applied to lower stakeholder criticality", + related_name="stakeholders", + to="core.appliedcontrol", + verbose_name="Applied controls", + ), + ), + ( + "ebios_rm_study", + models.ForeignKey( + help_text="EBIOS RM study that the stakeholder is part of", + on_delete=django.db.models.deletion.CASCADE, + related_name="stakeholders", + to="ebios_rm.ebiosrmstudy", + verbose_name="EBIOS RM study", + ), + ), + ( + "entity", + models.ForeignKey( + help_text="Entity qualified by the stakeholder", + on_delete=django.db.models.deletion.CASCADE, + related_name="stakeholders", + to="tprm.entity", + verbose_name="Entity", + ), + ), + ], + options={ + "verbose_name": "Stakeholder", + "verbose_name_plural": "Stakeholders", + "ordering": ["created_at"], + }, + ), + migrations.AddField( + model_name="attackpath", + name="stakeholders", + field=models.ManyToManyField( + help_text="Stakeholders leveraged by the attack path", + related_name="attack_paths", + to="ebios_rm.stakeholder", + verbose_name="Stakeholders", + ), + ), + ] diff --git a/backend/ebios_rm/migrations/__init__.py b/backend/ebios_rm/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/ebios_rm/models.py b/backend/ebios_rm/models.py new file mode 100644 index 000000000..c800aa705 --- /dev/null +++ b/backend/ebios_rm/models.py @@ -0,0 +1,377 @@ +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.models import ( + AppliedControl, + Asset, + ComplianceAssessment, + Qualification, + RiskAssessment, + RiskMatrix, + Threat, +) +from iam.models import FolderMixin, User +from tprm.models import Entity + + +class EbiosRMStudy(NameDescriptionMixin, ETADueDateMixin, FolderMixin): + class Status(models.TextChoices): + PLANNED = "planned", _("Planned") + IN_PROGRESS = "in_progress", _("In progress") + IN_REVIEW = "in_review", _("In review") + DONE = "done", _("Done") + DEPRECATED = "deprecated", _("Deprecated") + + risk_matrix = models.ForeignKey( + RiskMatrix, + on_delete=models.PROTECT, + verbose_name=_("Risk matrix"), + related_name="ebios_rm_studies", + help_text=_( + "Risk matrix used as a reference for the study. Defaults to `urn:intuitem:risk:library:risk-matrix-4x4-ebios-rm`" + ), + ) + assets = models.ManyToManyField( + Asset, + verbose_name=_("Assets"), + related_name="ebios_rm_studies", + help_text=_("Assets that are pertinent to the study"), + blank=True, + ) + compliance_assessments = models.ManyToManyField( + ComplianceAssessment, + blank=True, + verbose_name=_("Compliance assessments"), + related_name="ebios_rm_studies", + help_text=_( + "Compliance assessments established as security baseline during workshop 1.4" + ), + ) + risk_assessments = models.ManyToManyField( + RiskAssessment, + blank=True, + verbose_name=_("Risk assessments"), + related_name="ebios_rm_studies", + help_text=_("Risk assessments generated at the end of workshop 4"), + ) + reference_entity = models.ForeignKey( + Entity, + on_delete=models.PROTECT, + verbose_name=_("Reference entity"), + related_name="ebios_rm_studies", + help_text=_("Entity that is the focus of the study"), + default=Entity.get_main_entity, + ) + + ref_id = models.CharField(max_length=100) + version = models.CharField( + max_length=100, + blank=True, + null=True, + help_text=_("Version of the Ebios RM study (eg. 1.0, 2.0, etc.)"), + verbose_name=_("Version"), + default="1.0", + ) + status = models.CharField( + max_length=100, + choices=Status.choices, + default=Status.PLANNED, + verbose_name=_("Status"), + blank=True, + null=True, + ) + authors = models.ManyToManyField( + User, + blank=True, + verbose_name=_("Authors"), + related_name="authors", + ) + reviewers = models.ManyToManyField( + User, + blank=True, + verbose_name=_("Reviewers"), + related_name="reviewers", + ) + observation = models.TextField(null=True, blank=True, verbose_name=_("Observation")) + + class Meta: + verbose_name = _("Ebios RM Study") + verbose_name_plural = _("Ebios RM Studies") + ordering = ["created_at"] + + +class FearedEvent(NameDescriptionMixin): + ebios_rm_study = models.ForeignKey( + EbiosRMStudy, + verbose_name=_("EBIOS RM study"), + on_delete=models.CASCADE, + ) + assets = models.ManyToManyField( + Asset, + blank=True, + verbose_name=_("Assets"), + related_name="feared_events", + help_text=_("Assets that are affected by the feared event"), + ) + qualifications = models.ManyToManyField( + Qualification, + blank=True, + verbose_name=_("Qualifications"), + related_name="feared_events", + help_text=_("Qualifications carried by the feared event"), + ) + + ref_id = models.CharField(max_length=100) + 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) + + class Meta: + verbose_name = _("Feared event") + verbose_name_plural = _("Feared events") + ordering = ["created_at"] + + +class RoTo(AbstractBaseModel): + class RiskOrigin(models.TextChoices): + STATE = "state", _("State") + ORGANIZED_CRIME = "organized_crime", _("Organized crime") + TERRORIST = "terrorist", _("Terrorist") + ACTIVIST = "activist", _("Activist") + PROFESSIONAL = "professional", _("Professional") + AMATEUR = "amateur", _("Amateur") + AVENGER = "avenger", _("Avenger") + PATHOLOGICAL = "pathological", _("Pathological") + + class Motivation(models.IntegerChoices): + UNDEFINED = 0, "undefined" + VERY_LOW = 1, "very_low" + LOW = 2, "low" + SIGNIFICANT = 3, "significant" + STRONG = 4, "strong" + + class Resources(models.IntegerChoices): + UNDEFINED = 0, "undefined" + LIMITED = 1, "limited" + SIGNIFICANT = 2, "significant" + IMPORTANT = 3, "important" + UNLIMITED = 4, "unlimited" + + class Pertinence(models.IntegerChoices): + UNDEFINED = 0, "undefined" + IRRELAVANT = 1, "irrelevant" + PARTIALLY_RELEVANT = 2, "partially_relevant" + FAIRLY_RELEVANT = 3, "fairly_relevant" + HIGHLY_RELEVANT = 4, "highly_relevant" + + ebios_rm_study = models.ForeignKey( + EbiosRMStudy, + verbose_name=_("EBIOS RM study"), + on_delete=models.CASCADE, + ) + feared_events = models.ManyToManyField( + FearedEvent, verbose_name=_("Feared events"), related_name="ro_to_couples" + ) + + risk_origin = models.CharField( + max_length=32, verbose_name=_("Risk origin"), choices=RiskOrigin.choices + ) + target_objective = models.CharField( + max_length=200, verbose_name=_("Target objective") + ) + motivation = models.PositiveSmallIntegerField( + verbose_name=_("Motivation"), + choices=Motivation.choices, + default=Motivation.UNDEFINED, + ) + resources = models.PositiveSmallIntegerField( + verbose_name=_("Resources"), + choices=Resources.choices, + default=Resources.UNDEFINED, + ) + pertinence = models.PositiveSmallIntegerField( + verbose_name=_("Pertinence"), + choices=Pertinence.choices, + default=Pertinence.UNDEFINED, + ) + activity = models.PositiveSmallIntegerField( + verbose_name=_("Activity"), default=0, validators=[MaxValueValidator(4)] + ) + is_selected = models.BooleanField(verbose_name=_("Is selected"), default=False) + justification = models.TextField(verbose_name=_("Justification"), blank=True) + + class Meta: + verbose_name = _("RO/TO couple") + verbose_name_plural = _("RO/TO couples") + ordering = ["created_at"] + + +class Stakeholder(AbstractBaseModel): + class Category(models.TextChoices): + CLIENT = "client", _("Client") + PARTNER = "partner", _("Partner") + SUPPLIER = "supplier", _("Supplier") + + ebios_rm_study = models.ForeignKey( + EbiosRMStudy, + verbose_name=_("EBIOS RM study"), + help_text=_("EBIOS RM study that the stakeholder is part of"), + related_name="stakeholders", + on_delete=models.CASCADE, + ) + entity = models.ForeignKey( + Entity, + on_delete=models.CASCADE, + verbose_name=_("Entity"), + related_name="stakeholders", + help_text=_("Entity qualified by the stakeholder"), + ) + applied_controls = models.ManyToManyField( + AppliedControl, + verbose_name=_("Applied controls"), + blank=True, + related_name="stakeholders", + help_text=_("Controls applied to lower stakeholder criticality"), + ) + + category = models.CharField( + max_length=32, verbose_name=_("Category"), choices=Category.choices + ) + + current_dependency = models.PositiveSmallIntegerField( + verbose_name=_("Current dependency"), + default=0, + validators=[MaxValueValidator(4)], + ) + current_penetration = models.PositiveSmallIntegerField( + verbose_name=_("Current penetration"), + default=0, + validators=[MaxValueValidator(4)], + ) + current_maturity = models.PositiveSmallIntegerField( + verbose_name=_("Current maturity"), + default=1, + validators=[MinValueValidator(1), MaxValueValidator(4)], + ) + current_trust = models.PositiveSmallIntegerField( + verbose_name=_("Current trust"), + default=1, + validators=[MinValueValidator(1), MaxValueValidator(4)], + ) + + residual_dependency = models.PositiveSmallIntegerField( + verbose_name=_("Residual dependency"), + default=0, + validators=[MaxValueValidator(4)], + ) + residual_penetration = models.PositiveSmallIntegerField( + verbose_name=_("Residual penetration"), + default=0, + validators=[MaxValueValidator(4)], + ) + residual_maturity = models.PositiveSmallIntegerField( + verbose_name=_("Residual maturity"), + default=1, + validators=[MinValueValidator(1), MaxValueValidator(4)], + ) + residual_trust = models.PositiveSmallIntegerField( + verbose_name=_("Residual trust"), + default=1, + validators=[MinValueValidator(1), MaxValueValidator(4)], + ) + + is_selected = models.BooleanField(verbose_name=_("Is selected"), default=False) + justification = models.TextField(verbose_name=_("Justification"), blank=True) + + class Meta: + verbose_name = _("Stakeholder") + verbose_name_plural = _("Stakeholders") + ordering = ["created_at"] + + @staticmethod + def _compute_criticality( + dependency: int, penetration: int, maturity: int, trust: int + ): + if (maturity * trust) == 0: + return 0 + return (dependency * penetration) / (maturity * trust) + + @property + def current_criticality(self): + return self._compute_criticality( + self.current_dependency, + self.current_penetration, + self.current_maturity, + self.current_trust, + ) + + @property + def residual_criticality(self): + return self._compute_criticality( + self.residual_dependency, + self.residual_penetration, + self.residual_maturity, + self.residual_trust, + ) + + +class AttackPath(AbstractBaseModel): + ebios_rm_study = models.ForeignKey( + EbiosRMStudy, + verbose_name=_("EBIOS RM study"), + on_delete=models.CASCADE, + ) + ro_to_couple = models.ForeignKey( + RoTo, + verbose_name=_("RO/TO couple"), + on_delete=models.CASCADE, + help_text=_("RO/TO couple from which the attach path is derived"), + ) + stakeholders = models.ManyToManyField( + Stakeholder, + verbose_name=_("Stakeholders"), + related_name="attack_paths", + help_text=_("Stakeholders leveraged by the attack path"), + ) + + description = models.TextField(verbose_name=_("Description")) + is_selected = models.BooleanField(verbose_name=_("Is selected"), default=False) + justification = models.TextField(verbose_name=_("Justification"), blank=True) + + class Meta: + verbose_name = _("Attack path") + verbose_name_plural = _("Attack paths") + ordering = ["created_at"] + + +class OperationalScenario(AbstractBaseModel): + ebios_rm_study = models.ForeignKey( + EbiosRMStudy, + verbose_name=_("EBIOS RM study"), + related_name="operational_scenarios", + on_delete=models.CASCADE, + ) + attack_paths = models.ManyToManyField( + AttackPath, + verbose_name=_("Attack paths"), + related_name="operational_scenarios", + help_text=_("Attack paths that are pertinent to the operational scenario"), + ) + threats = models.ManyToManyField( + Threat, + verbose_name=_("Threats"), + blank=True, + related_name="operational_scenarios", + help_text=_("Threats leveraged by the operational scenario"), + ) + + description = models.TextField(verbose_name=_("Description")) + likelihood = models.SmallIntegerField(default=-1, verbose_name=_("Likelihood")) + is_selected = models.BooleanField(verbose_name=_("Is selected"), default=False) + justification = models.TextField(verbose_name=_("Justification"), blank=True) + + class Meta: + verbose_name = _("Operational scenario") + verbose_name_plural = _("Operational scenarios") + ordering = ["created_at"] diff --git a/backend/ebios_rm/tests/fixtures.py b/backend/ebios_rm/tests/fixtures.py new file mode 100644 index 000000000..a25d19dde --- /dev/null +++ b/backend/ebios_rm/tests/fixtures.py @@ -0,0 +1,60 @@ +import pytest + +from core.models import RiskMatrix, StoredLibrary, Asset +from ebios_rm.models import RoTo, EbiosRMStudy, FearedEvent + + +@pytest.fixture +def ebios_rm_matrix_fixture(): + library = StoredLibrary.objects.filter( + urn="urn:intuitem:risk:library:risk-matrix-4x4-ebios-rm" + ).last() + assert library is not None + library.load() + return RiskMatrix.objects.get( + urn="urn:intuitem:risk:matrix:risk-matrix-4x4-ebios-rm" + ) + + +@pytest.fixture +def basic_assets_tree_fixture(): + primary_asset_1 = Asset.objects.create(name="Primary Asset 1") + primary_asset_2 = Asset.objects.create(name="Primary Asset 2") + supporting_asset = Asset.objects.create( + name="Supporting Asset 1", type=Asset.Type.SUPPORT + ) + supporting_asset.parent_assets.add(primary_asset_1, primary_asset_2) + return primary_asset_1, primary_asset_2, supporting_asset + + +@pytest.fixture +def basic_ebios_rm_study_fixture(ebios_rm_matrix_fixture, basic_assets_tree_fixture): + study = EbiosRMStudy.objects.create( + name="test study", + description="test study description", + risk_matrix=ebios_rm_matrix_fixture, + ) + study.assets.set(basic_assets_tree_fixture) + return study + + +@pytest.fixture +def basic_feared_event_fixture(basic_ebios_rm_study_fixture): + feared_event = FearedEvent.objects.create( + name="test feared event", + description="test feared event description", + ebios_rm_study=basic_ebios_rm_study_fixture, + ) + asset = Asset.objects.get(name="Primary Asset 1") + feared_event.assets.add(asset) + + +@pytest.fixture +def basic_roto_fixture(basic_ebios_rm_study_fixture, basic_feared_event_fixture): + roto = RoTo.objects.create( + risk_origin=RoTo.RiskOrigin.STATE, + target_objective="test target objectives", + ebios_rm_study=basic_ebios_rm_study_fixture, + ) + roto.feared_events.set(FearedEvent.objects.filter(name="test feared event")) + return roto diff --git a/backend/ebios_rm/tests/test_ebios_rm_study.py b/backend/ebios_rm/tests/test_ebios_rm_study.py new file mode 100644 index 000000000..13fbf0d3a --- /dev/null +++ b/backend/ebios_rm/tests/test_ebios_rm_study.py @@ -0,0 +1,45 @@ +import pytest +from core.models import Asset, RiskMatrix +from ebios_rm.models import EbiosRMStudy + +from ebios_rm.tests.fixtures import * +from tprm.models import Entity + + +@pytest.mark.django_db +class TestEbiosRMStudy: + @pytest.mark.usefixtures("ebios_rm_matrix_fixture") + def test_create_ebios_rm_study_basic(self): + study = EbiosRMStudy.objects.create( + name="test study", + description="test study description", + risk_matrix=RiskMatrix.objects.get( + urn="urn:intuitem:risk:matrix:risk-matrix-4x4-ebios-rm" + ), + ) + assert study.name == "test study" + assert study.description == "test study description" + assert study.risk_matrix == RiskMatrix.objects.get( + urn="urn:intuitem:risk:matrix:risk-matrix-4x4-ebios-rm" + ) + assert study.assets.count() == 0 + assert study.reference_entity == Entity.get_main_entity() + + @pytest.mark.usefixtures("ebios_rm_matrix_fixture", "basic_assets_tree_fixture") + def test_create_ebios_rm_study_with_assets(self): + study = EbiosRMStudy.objects.create( + name="test study", + description="test study description", + risk_matrix=RiskMatrix.objects.get( + urn="urn:intuitem:risk:matrix:risk-matrix-4x4-ebios-rm" + ), + ) + study.assets.set(Asset.objects.filter(name="Primary Asset 1")) + assert study.name == "test study" + assert study.description == "test study description" + assert study.risk_matrix == RiskMatrix.objects.get( + urn="urn:intuitem:risk:matrix:risk-matrix-4x4-ebios-rm" + ) + + assert study.assets.count() == 1 + assert study.assets.filter(name="Primary Asset 1").exists() diff --git a/backend/ebios_rm/tests/test_feared_event.py b/backend/ebios_rm/tests/test_feared_event.py new file mode 100644 index 000000000..55b8d87c7 --- /dev/null +++ b/backend/ebios_rm/tests/test_feared_event.py @@ -0,0 +1,20 @@ +import pytest +from ebios_rm.models import EbiosRMStudy, FearedEvent + +from ebios_rm.tests.fixtures import * + + +@pytest.mark.django_db +class TestFearedEvent: + @pytest.mark.usefixtures("basic_ebios_rm_study_fixture") + def test_create_feared_event_basic(self): + feared_event = FearedEvent.objects.create( + name="test feared event", + description="test feared event description", + ebios_rm_study=EbiosRMStudy.objects.get(name="test study"), + ) + assert feared_event.name == "test feared event" + assert feared_event.description == "test feared event description" + assert feared_event.ebios_rm_study == EbiosRMStudy.objects.get( + name="test study" + ) diff --git a/backend/ebios_rm/tests/test_ro_to.py b/backend/ebios_rm/tests/test_ro_to.py new file mode 100644 index 000000000..9a0f23695 --- /dev/null +++ b/backend/ebios_rm/tests/test_ro_to.py @@ -0,0 +1,28 @@ +import pytest +from ebios_rm.models import EbiosRMStudy, FearedEvent, RoTo + +from ebios_rm.tests.fixtures import * + + +@pytest.mark.django_db +class TestRoTo: + @pytest.mark.usefixtures( + "basic_ebios_rm_study_fixture", "basic_feared_event_fixture" + ) + def test_create_roto_basic(self): + roto = RoTo.objects.create( + risk_origin=RoTo.RiskOrigin.STATE, + target_objective="test target objectives", + ebios_rm_study=EbiosRMStudy.objects.get(name="test study"), + ) + roto.feared_events.set(FearedEvent.objects.filter(name="test feared event")) + + assert roto.risk_origin == "state" + assert roto.target_objective == "test target objectives" + + assert roto.feared_events.count() == 1 + assert roto.feared_events.filter(name="test feared event").exists() + assert ( + roto.ebios_rm_study + == FearedEvent.objects.get(name="test feared event").ebios_rm_study + ) diff --git a/backend/ebios_rm/tests/test_stakeholder.py b/backend/ebios_rm/tests/test_stakeholder.py new file mode 100644 index 000000000..cc034af2d --- /dev/null +++ b/backend/ebios_rm/tests/test_stakeholder.py @@ -0,0 +1,28 @@ +import pytest +from ebios_rm.models import EbiosRMStudy, FearedEvent, RoTo, Stakeholder + +from tprm.models import Entity + +from ebios_rm.tests.fixtures import * + + +@pytest.mark.django_db +class TestStakeholder: + @pytest.mark.usefixtures( + "basic_ebios_rm_study_fixture", + ) + def test_create_stakeholder_basic(self): + study = EbiosRMStudy.objects.get(name="test study") + entity = Entity.objects.create(name="Entity") + stakeholder = Stakeholder.objects.create( + entity=entity, + category=Stakeholder.Category.SUPPLIER, + ebios_rm_study=study, + ) + + assert stakeholder in study.stakeholders.all() + assert stakeholder.entity == entity + assert stakeholder.category == Stakeholder.Category.SUPPLIER + + assert stakeholder.current_criticality == 0 + assert stakeholder.residual_criticality == 0 diff --git a/backend/ebios_rm/views.py b/backend/ebios_rm/views.py new file mode 100644 index 000000000..91ea44a21 --- /dev/null +++ b/backend/ebios_rm/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/enterprise/backend/enterprise_core/settings.py b/enterprise/backend/enterprise_core/settings.py index 32ef1e7c3..88d783fe1 100644 --- a/enterprise/backend/enterprise_core/settings.py +++ b/enterprise/backend/enterprise_core/settings.py @@ -143,6 +143,7 @@ def set_ciso_assistant_url(_, __, event_dict): "global_settings", "tprm", "core", + "ebios_rm", "cal", "django_filters", "library",