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 a2df46e8d..5ab669cfd 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/documentation/architecture/data-model.md b/documentation/architecture/data-model.md index b94205a92..20766a703 100644 --- a/documentation/architecture/data-model.md +++ b/documentation/architecture/data-model.md @@ -100,19 +100,20 @@ erDiagram COMPLIANCE_ASSESSMENT }o--|| FRAMEWORK : is_based_on PROJECT |o--o{ COMPLIANCE_ASSESSMENT : contains COMPLIANCE_ASSESSMENT ||--o{ REQUIREMENT_ASSESSMENT: contains + APPLIED_CONTROL }o--o{ EVIDENCE : is_proved_by + FRAMEWORK ||--o{ REQUIREMENT_NODE : contains REQUIREMENT_ASSESSMENT }o--|| REQUIREMENT_NODE : implements REQUIREMENT_ASSESSMENT }o--o{ APPLIED_CONTROL : is_answered_by REQUIREMENT_ASSESSMENT }o--o{ EVIDENCE : is_proved_by APPLIED_CONTROL }o--o| REFERENCE_CONTROL : implements REQUIREMENT_NODE }o--o{ THREAT : addresses - FRAMEWORK ||--o{ REQUIREMENT_NODE : contains - APPLIED_CONTROL }o--o{ EVIDENCE : is_proved_by RISK_ASSESSMENT }o--|| RISK_MATRIX : applies PROJECT |o--o{ RISK_ASSESSMENT : contains RISK_ASSESSMENT ||--o{ RISK_SCENARIO : contains RISK_SCENARIO }o--o{ APPLIED_CONTROL : is_mitigated_by RISK_SCENARIO }o--o{ THREAT : derives_from RISK_SCENARIO }o--o{ ASSET : threatens + RISK_SCENARIO }o--o{ QUALIFICATION : bears RISK_ACCEPTANCE }o--o{ RISK_SCENARIO : covers RISK_ASSESSMENT_REVIEW }o--|| RISK_ASSESSMENT : reviews RISK_SCENARIO }o--o{ VULNERABILITY : exploits @@ -120,6 +121,8 @@ erDiagram USER }o--o{ RISK_SCENARIO : owns USER }o--o{ APPLIED_CONTROL : owns USER }o--o{ ASSET : owns + ASSET ||--o{ SECURITY_OBJECTIVE : has + SECURITY_OBJECTIVE }o--|| QUALIFICATION : implements PROJECT { string ref_id @@ -289,8 +292,9 @@ erDiagram string type asset parent_asset url reference_link - json security_objectives - json disaster_recovery_objectives + int rto + int rpo + int mtd } RISK_SCENARIO { @@ -307,7 +311,6 @@ erDiagram json target_risk_vector string strength_of_knowledge string justification - json qualifications string threat_actor } @@ -337,6 +340,19 @@ erDiagram string reviewer } + QUALIFICATION { + string ref_id + string name + string description + json translations + int order + bool is_objective + } + + SECURITY_OBJECTIVE { + int value + } + ``` ### Requirement mappings @@ -407,37 +423,47 @@ Projects have the following fields: - Description - Status: --/Design/Development/Production/End of life/Dropped -## Assets, security and disaster recovery objectives +## Qualifications -Assets are context objects defined by the entity using CISO Assistant. They are optional, assessments can be done without using them. +Qualifications are qualities/objectives that can be used to qualify risk scenarios or to set security objectives to primary assets. Some of them are hardcoded, but in the PRO version the administrator can define additional values and rename existing ones. -Assets are of type primary or supporting. A primary asset has no parent, a supporting asset can have parent assets (primary or supporting), but not itself. +The following values are preloaded: + +abbreviation | q_order | so_order | name | description | translations | urn +-------------|---------|----------|------------------|-------------|--------------|------------------------------------------------ +C | 1 | 1 | confidentiality | | ... | urn:intuitem:risk:qualification:confidentiality +I | 2 | 2 | integrity | | ... | urn:intuitem:risk:qualification:integrity +A | 3 | 3 | availability | | ... | urn:intuitem:risk:qualification:availability +P | 4 | 4 | proof | | ... | urn:intuitem:risk:qualification:proof +Aut | 5 | 5 | authenticity | | ... | urn:intuitem:risk:qualification:authenticity +Priv | 6 | 6 | privacy | | ... | urn:intuitem:risk:qualification:privacy +Safe | 7 | 7 | safety | | ... | urn:intuitem:risk:qualification:safety +Rep | 8 | | reputation | | ... | urn:intuitem:risk:qualification:safety +Ope | 9 | | operational | | ... | urn:intuitem:risk:qualification:operational +Leg | 10 | | legal | | ... | urn:intuitem:risk:qualification:legal +Fin | 11 | | financial | | ... | urn:intuitem:risk:qualification:financial -Primary assets have security objectives that are evolutive, so they are catched in a json field. +Qualifications that have so_order defined can be used to set security objectives to primary assets. -Security objectives are specific goals or requirements that an organization, system, or process aims to achieve in order to ensure its security and protect its primary assets. +The role of urn is to enable updates with a library, and to facilitate export/import between instances (not in MVP). -There is a global parameter that defines a list of security objectives with a corresponding scale and a corresponding boolean allowing to select or hide a security objective. The following security objectives are pre-defined: +Note: the order can be changed in a translation. This makes easy to transform CIAP (English) in DICP (French) (not in MVP). - ref_id | Name | Description | default scale | default select value ---------|----------------------------|-------------|---------------|--------------------- - C | Confidentiality | ... | 1-4 | True - I | Integrity | ... | 1-4 | True - A | Availability | ... | 1-4 | True - P | Proof | ... | 1-4 | True - Auth | Authenticity | ... | 1-4 | False - Priv | Privacy | ... | 1-4 | False - Safe | Safety | ... | 1-4 | False +## Assets, security and disaster recovery objectives + +Assets are context objects defined by the entity using CISO Assistant. They are optional, assessments can be done without using them. + +Assets are of type primary or supporting. A primary asset has no parent, a supporting asset can have parent assets (primary or supporting), but not itself. -The following disaster recovery objectives (measured in seconds) are pre-defined: +The following disaster recovery objectives (measured in seconds) can be defined on assets: - ref_id | Name | Description ---------|----------------------------|------------ - RTO | Recovery Time Objective | ... - RPO | Recovery Point Objetive | ... - MTD | Maximum Tolerable Downtime | ... + Abbreviation | Name | Description +--------------|----------------------------|------------ + RTO | Recovery Time Objective | ... + RPO | Recovery Point Objetive | ... + MTD | Maximum Tolerable Downtime | ... -In a future version, users will be able to define custom security objectives. +Assets have security objectives. Security objectives are specific goals or requirements that an organization, system, or process aims to achieve in order to ensure its security and protect its primary assets. They are a subset of qualifications. Security objectives are measured using a specifc scale. For now, the following scales are defined: - 0-3: coded as 0-3 @@ -461,9 +487,7 @@ FIPS-199 | 1 | moderate FIPS-199 | 2 | moderate FIPS-199 | 3 | high -Security objectives can be evaluated for each asset. The default value is Null. The corresponding json field is composed of a list of tuples {security_objective_ref_id, value}. - -When a security objective is hidden in the global parameters, it is simply not proposed for new edition. However, a security objective that is already used in an asset is kept and editable even if it is hidden globally. Thus, when selecting or hiding a security objective, no value is changed in asset. +THe scale to use is a global parameter. It has no impact on the encoding in the database, which always uses the internal value. ## Frameworks @@ -623,7 +647,6 @@ The following inference rules are used: A risk assessment is based on scenarios, covered by Applied controls. Gathering the risk scenarios constitutes the "risk identification" phase. - The risk matrix cannot be changed once the risk assessment is created. A risk assessment has an _risk_assessment_method_ field that can take the following values: 0 (risk matrix)/1 (Open FAIR). This cannot be changed once the risk assessment is created. Similarly, the risk matrix cannot be changed once the risk assessment is created. @@ -634,9 +657,7 @@ A risk scenario contains a treatment option with the values --/open/mitigate/acc A risk scenario also contains a "strength of knowledge", within the values --/0 (Low)/1 (Medium)/2 (High). This can be used to represent a third dimension of risk, as recommended by the Society for Risk Analysis. The field "justification" can be used to expose the knowledge. -A risk scenario also contains a "qualification" field, containing an array with the following possible values: Confidentiality, Integrity, Availability, Proof, Authenticity, Privacy, Safety, Reputation, Operational, Legal, Financial. The qualification can cover none, one or several of the values. - -Note: the list of qualifications is a superset of security objectives. +A risk scenario also contains qualifications. The risk evaluation is automatically done based on the selected risk matrix. @@ -1141,16 +1162,16 @@ Each of these objects will have its specific datamodel. Factoring will be done a EBIOS-RM (english) | EBIOS-RM (french) | CISO Assistant ----------------------|-------------------------|---------------- -Study | Etude | Bundle -Studied object | Objet de l'étude | Description of the bundle -Mission | Mission | Mission of the reference entity added to the bundle +Study | Etude | Study +Studied object | Objet de l'étude | Description of the Study +Mission | Mission | Mission of the reference entity added to the Study Business asset | Valeurs métier | Primary asset Supporting asset | Bien support | Supporting asset Feared event | Evénement redouté | Risk analysis at asset level Impact | Impact | Impact in a risk analysis Security baseline | Socle de sécurité | Compliance frameworks and audits -Risk origins | Sources de risque | TBD -Target objectives | Objectifs visés | TBD +Risk origins | Sources de risque | RoTo +Target objectives | Objectifs visés | RoTo Ecosystem | Ecosystème | Third Party Risk Management Strategic scenarios | Scénarios stratégiques | Risk analysis at strategic level (focus on impact) Security controls | Mesures de sécurité | Reference/applied controls @@ -1169,7 +1190,7 @@ The type EBIOS-RM study is a sort of assessment. It contains the following speci - a list of audits for the security baseline (workshop 1) - a list of feared events (workshop 1) - a list of risk_origin_target_objective (workshop 2) -- a list of ecosystem entities (workshop 3) +- a list of stakeholders (workshop 3) - a list of strategic scenarios/attack paths (workshop 3) - a list of opeating scenarios (workshop 4) - a resulting risk assessment (workshop 5) @@ -1207,7 +1228,7 @@ The object ecosystem entity (workshop 3) links to a TPRM entity, and contains th The object strategic attack path (workshop 3) contains the following fields: - risk_origin_target_objective - description -- affected ecosystem entities +- affected stakeholders - intial threat level - Controls - residual threat level @@ -1233,12 +1254,12 @@ The frontend for risk study shall propose the following steps: - workshop 2: risk origin/target objectives (sources de risque) - define risk_origin_target_objective objects - workshop 3: - - list of ecosystem entities + - list of stakeholders - list of strategic scenarios/attack paths - workshop 4: operational scenarios - list of operational scenarios + - The risk assessment is generated automatically, thanks to a dedicated button. When the risk assessment is generated again, automatic versioning is applied, and mitigations can be copied on demand (based on ref_id of operational scenarios). - workshop 5: risk treatment - - The risk assessment is generated from workshop 4, thanks to a dedicated button. When the risk assessment is generated again, automatic versioning is applied, and mitigations can be copied on demand (based on ref_id of operational scenarios). - After generation, a risk assessment is fully editable, to allow customisation, and the risk assessment can be managed normally as any other risk assessment. - risk treatment is based on the risk assessment. @@ -1246,7 +1267,7 @@ The frontend for risk study shall propose the following steps: ```mermaid erDiagram DOMAIN ||--o{ EBIOS_RM_STUDY : contains - DOMAIN ||--o{ ECOSYSTEM_ENTITY : contains + DOMAIN ||--o{ STAKEHOLDER : contains DOMAIN ||--o{ OPERATIONAL_SCENARIO : contains DOMAIN ||--o{ FEARED_EVENT : contains DOMAIN ||--o{ RO_TO : contains @@ -1257,22 +1278,25 @@ erDiagram ```mermaid erDiagram - ATTACK_PATH }o--|| RO_TO : derives - RO_TO }o--|{ FEARED_EVENT : corresponds_to - EBIOS_RM_STUDY }o--o{ RO_TO : contains - EBIOS_RM_STUDY }o--o{ ECOSYSTEM_ENTITY : contains - EBIOS_RM_STUDY }o--o{ OPERATIONAL_SCENARIO : contains - EBIOS_RM_STUDY }o--o{ FEARED_EVENT : contains - EBIOS_RM_STUDY }o--o{ ATTACK_PATH : contains + FEARED_EVENT }o--o{ ASSET : affects + STAKEHOLDER }o--|| ENTITY : qualifies + EBIOS_RM_STUDY ||--o{ RO_TO : contains + EBIOS_RM_STUDY ||--o{ STAKEHOLDER : contains + EBIOS_RM_STUDY ||--o{ OPERATIONAL_SCENARIO : contains + EBIOS_RM_STUDY ||--o{ FEARED_EVENT : contains + EBIOS_RM_STUDY ||--o{ ATTACK_PATH : contains + EBIOS_RM_STUDY }o--o{ ASSET : contains EBIOS_RM_STUDY }o--o| ENTITY : studies EBIOS_RM_STUDY }o--o{ COMPLIANCE_ASSESSMENT: leverages EBIOS_RM_STUDY }o--|| RISK_MATRIX : leverages EBIOS_RM_STUDY }o--o{ RISK_ASSESSMENT : generates - OPERATIONAL_SCENARIO }o--|| ATTACK_PATH : derives + ATTACK_PATH }o--|| RO_TO : derives + RO_TO }o--o{ FEARED_EVENT : corresponds_to + OPERATIONAL_SCENARIO }o--|{ ATTACK_PATH : derives OPERATIONAL_SCENARIO }o--o{ THREAT : leverages - ATTACK_PATH }o--o{ ECOSYSTEM_ENTITY : uses - ATTACK_PATH }o--o{ APPLIED_CONTROL : mitigated_by - ECOSYSTEM_ENTITY }o--|| ENTITY : qualifies + ATTACK_PATH }o--o{ STAKEHOLDER : leverages + STAKEHOLDER }o--o{ APPLIED_CONTROL : reinforces + FEARED_EVENT }o--o{ QUALIFICATION : bears EBIOS_RM_STUDY { string ref_id @@ -1292,10 +1316,9 @@ erDiagram string ref_id string name string description - json qualifications int gravity bool selected - bool justification + string justification } RO_TO { @@ -1306,25 +1329,27 @@ erDiagram int pertinence int activity bool selected - bool justification + string justification } - ECOSYSTEM_ENTITY { + STAKEHOLDER { string category - int dependence - int penetration - int maturity - int trust + int current_dependence + int current_penetration + int current_maturity + int current_trust + int residual_dependence + int residual_penetration + int residual_maturity + int residual_trust bool selected - bool justification + string justification } ATTACK_PATH { string description - int intial_threat_level - int residual_threat_level bool selected - bool justification + string justification } OPERATIONAL_SCENARIO { @@ -1338,5 +1363,7 @@ erDiagram ### Implementation -EBIOS-RM objects are defined within a dedicated Django "application" ebios_rm. - +- EBIOS-RM objects are defined within a dedicated Django "application" ebios_rm. +- There is no object for "strategic scenarios", as they result directly from attack paths and corresponding feared event (which is the title of the strategic scenario). +- the current and residual "criticity" are calculated on stakeholders, so they are not seen as fields. +- \ No newline at end of file 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", diff --git a/features.png b/features.png index 036cf3635..fac5e5142 100644 Binary files a/features.png and b/features.png differ