From 6f50527d824b97fc4fc002b9bfbed5361ab9d0a1 Mon Sep 17 00:00:00 2001
From: eric-intuitem <71850047+eric-intuitem@users.noreply.github.com>
Date: Sat, 24 Feb 2024 20:04:47 +0100
Subject: [PATCH 1/9] Rationalize all object classes
Make an inheritance patttern that is simpler and more logical
Put IAM object in logical order
---
backend/cal/migrations/0001_initial.py | 2 +-
backend/core/base_models.py | 33 +++---
backend/core/migrations/0001_initial.py | 32 +++---
backend/core/migrations/0002_initial.py | 2 +-
backend/core/models.py | 28 ++---
backend/iam/migrations/0001_initial.py | 35 ++++--
backend/iam/migrations/0002_role_builtin.py | 18 +++
backend/iam/models.py | 118 ++++++++++----------
8 files changed, 149 insertions(+), 119 deletions(-)
create mode 100644 backend/iam/migrations/0002_role_builtin.py
diff --git a/backend/cal/migrations/0001_initial.py b/backend/cal/migrations/0001_initial.py
index 3977804241..a852e947d5 100644
--- a/backend/cal/migrations/0001_initial.py
+++ b/backend/cal/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.0.2 on 2024-02-23 00:51
+# Generated by Django 5.0.2 on 2024-02-24 18:49
from django.db import migrations, models
diff --git a/backend/core/base_models.py b/backend/core/base_models.py
index c4df8022f6..024019966b 100644
--- a/backend/core/base_models.py
+++ b/backend/core/base_models.py
@@ -7,20 +7,6 @@
from ciso_assistant import settings
-class NameDescriptionMixin(models.Model):
- """
- Mixin for models that have a name and a description.
- """
-
- name = models.CharField(max_length=200, verbose_name=_("Name"), unique=False)
- description = models.TextField(null=True, blank=True, verbose_name=_("Description"))
-
- class Meta:
- abstract = True
-
- def __str__(self) -> str:
- return self.name
-
class AbstractBaseModel(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
@@ -28,7 +14,6 @@ class AbstractBaseModel(models.Model):
class Meta:
abstract = True
- ordering = ["name"]
def scoped_id(self, scope: models.QuerySet) -> int:
"""
@@ -121,6 +106,24 @@ def save(self, *args, **kwargs) -> None:
super().save(*args, **kwargs)
+
+class NameDescriptionMixin(AbstractBaseModel):
+ """
+ Mixin for models that have a name and a description.
+ """
+
+ name = models.CharField(max_length=200, verbose_name=_("Name"), unique=False)
+ description = models.TextField(null=True, blank=True, verbose_name=_("Description"))
+
+ class Meta:
+ abstract = True
+ ordering = ["name"]
+
+ def __str__(self) -> str:
+ return self.name
+
+
+
class ReferentialObjectMixin(AbstractBaseModel):
"""
Mixin for models that have a name and a description.
diff --git a/backend/core/migrations/0001_initial.py b/backend/core/migrations/0001_initial.py
index 6b3a191e6b..ae08bf983d 100644
--- a/backend/core/migrations/0001_initial.py
+++ b/backend/core/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.0.2 on 2024-02-23 00:51
+# Generated by Django 5.0.2 on 2024-02-24 18:49
import core.validators
import uuid
@@ -16,10 +16,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Asset',
fields=[
- ('name', models.CharField(max_length=200, verbose_name='Name')),
- ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('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')),
+ ('name', models.CharField(max_length=200, verbose_name='Name')),
+ ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('business_value', models.CharField(blank=True, max_length=200, verbose_name='business value')),
('type', models.CharField(choices=[('PR', 'Primary'), ('SP', 'Support')], default='SP', max_length=2, verbose_name='type')),
('is_published', models.BooleanField(default=True, verbose_name='published')),
@@ -32,10 +32,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='ComplianceAssessment',
fields=[
- ('name', models.CharField(max_length=200, verbose_name='Name')),
- ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('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')),
+ ('name', models.CharField(max_length=200, verbose_name='Name')),
+ ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('version', models.CharField(blank=True, default='1.0', help_text='Version of the compliance assessment (eg. 1.0, 2.0, etc.)', max_length=100, null=True, verbose_name='Version')),
('status', models.CharField(choices=[('planned', 'Planned'), ('in_progress', 'In progress'), ('in_review', 'In review'), ('done', 'Done'), ('deprecated', 'Deprecated')], default='planned', max_length=100, verbose_name='Status')),
('eta', models.DateField(blank=True, help_text='Estimated time of arrival', null=True, verbose_name='ETA')),
@@ -50,10 +50,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Evidence',
fields=[
- ('name', models.CharField(max_length=200, verbose_name='Name')),
- ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('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')),
+ ('name', models.CharField(max_length=200, verbose_name='Name')),
+ ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('attachment', models.FileField(blank=True, help_text='Attachment for evidence (eg. screenshot, log file, etc.)', null=True, upload_to='', validators=[core.validators.validate_file_size, core.validators.validate_file_name], verbose_name='Attachment')),
('link', models.URLField(blank=True, help_text='Link to the evidence (eg. Jira ticket, etc.)', null=True, verbose_name='Link')),
],
@@ -105,10 +105,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Project',
fields=[
- ('name', models.CharField(max_length=200, verbose_name='Name')),
- ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('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')),
+ ('name', models.CharField(max_length=200, verbose_name='Name')),
+ ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('internal_reference', models.CharField(blank=True, max_length=100, null=True, verbose_name='Internal reference')),
('lc_status', models.CharField(choices=[('undefined', '--'), ('in_design', 'Design'), ('in_dev', 'Development'), ('in_prod', 'Production'), ('eol', 'End Of Life'), ('dropped', 'Dropped')], default='in_design', max_length=20, verbose_name='Status')),
],
@@ -177,10 +177,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='RiskAcceptance',
fields=[
- ('name', models.CharField(max_length=200, verbose_name='Name')),
- ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('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')),
+ ('name', models.CharField(max_length=200, verbose_name='Name')),
+ ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('state', models.CharField(choices=[('created', 'Created'), ('submitted', 'Submitted'), ('accepted', 'Accepted'), ('rejected', 'Rejected'), ('revoked', 'Revoked')], default='created', max_length=20, verbose_name='State')),
('expiry_date', models.DateField(help_text='Specify when the risk acceptance will no longer apply', null=True, verbose_name='Expiry date')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
@@ -198,10 +198,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='RiskAssessment',
fields=[
- ('name', models.CharField(max_length=200, verbose_name='Name')),
- ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('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')),
+ ('name', models.CharField(max_length=200, verbose_name='Name')),
+ ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('version', models.CharField(blank=True, default='1.0', help_text='Version of the compliance assessment (eg. 1.0, 2.0, etc.)', max_length=100, null=True, verbose_name='Version')),
('status', models.CharField(choices=[('planned', 'Planned'), ('in_progress', 'In progress'), ('in_review', 'In review'), ('done', 'Done'), ('deprecated', 'Deprecated')], default='planned', max_length=100, verbose_name='Status')),
('eta', models.DateField(blank=True, help_text='Estimated time of arrival', null=True, verbose_name='ETA')),
@@ -236,10 +236,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='RiskScenario',
fields=[
- ('name', models.CharField(max_length=200, verbose_name='Name')),
- ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('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')),
+ ('name', models.CharField(max_length=200, verbose_name='Name')),
+ ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('existing_measures', models.TextField(blank=True, help_text='The existing security measures to manage this risk. Edit the risk scenario to add extra security measures.', max_length=2000, verbose_name='Existing measures')),
('current_proba', models.SmallIntegerField(default=-1, verbose_name='Current probability')),
('current_impact', models.SmallIntegerField(default=-1, verbose_name='Current impact')),
@@ -282,9 +282,9 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='SecurityMeasure',
fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=200, verbose_name='Name')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('category', models.CharField(blank=True, choices=[('policy', 'Policy'), ('process', 'Process'), ('technical', 'Technical'), ('physical', 'Physical')], max_length=20, null=True, verbose_name='Category')),
('status', models.CharField(blank=True, choices=[('planned', 'Planned'), ('active', 'Active'), ('inactive', 'Inactive')], max_length=20, null=True, verbose_name='Status')),
('eta', models.DateField(blank=True, help_text='Estimated Time of Arrival', null=True, verbose_name='ETA')),
diff --git a/backend/core/migrations/0002_initial.py b/backend/core/migrations/0002_initial.py
index cf044a7d60..cef9d4a13a 100644
--- a/backend/core/migrations/0002_initial.py
+++ b/backend/core/migrations/0002_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.0.2 on 2024-02-23 00:51
+# Generated by Django 5.0.2 on 2024-02-24 18:49
import django.db.models.deletion
import iam.models
diff --git a/backend/core/models.py b/backend/core/models.py
index 3999bc6422..e5e3bef744 100644
--- a/backend/core/models.py
+++ b/backend/core/models.py
@@ -21,7 +21,7 @@
User = get_user_model()
-class Library(ReferentialObjectMixin, AbstractBaseModel, FolderMixin):
+class Library(ReferentialObjectMixin, FolderMixin):
copyright = models.CharField(
max_length=4096, null=True, blank=True, verbose_name=_("Copyright")
)
@@ -81,7 +81,7 @@ def delete(self, *args, **kwargs):
super(Library, self).delete(*args, **kwargs)
-class Assessment(AbstractBaseModel, NameDescriptionMixin):
+class Assessment(NameDescriptionMixin):
class Status(models.TextChoices):
PLANNED = "planned", _("Planned")
IN_PROGRESS = "in_progress", _("In progress")
@@ -137,7 +137,7 @@ class Meta:
abstract = True
-class Project(AbstractBaseModel, NameDescriptionMixin, FolderMixin):
+class Project(NameDescriptionMixin, FolderMixin):
PRJ_LC_STATUS = [
("undefined", _("--")),
("in_design", _("Design")),
@@ -181,7 +181,7 @@ def __str__(self):
return self.name
-class Threat(ReferentialObjectMixin, AbstractBaseModel, RootFolderMixin):
+class Threat(ReferentialObjectMixin, RootFolderMixin):
library = models.ForeignKey(
Library, on_delete=models.CASCADE, null=True, blank=True, related_name="threats"
)
@@ -207,7 +207,7 @@ def __str__(self):
return self.name
-class Asset(AbstractBaseModel, NameDescriptionMixin, RootFolderMixin):
+class Asset(NameDescriptionMixin, RootFolderMixin):
class Type(models.TextChoices):
"""
The type of the asset.
@@ -259,7 +259,7 @@ def ancestors_plus_self(self) -> list[Self]:
return list(result)
-class SecurityFunction(ReferentialObjectMixin, AbstractBaseModel, RootFolderMixin):
+class SecurityFunction(ReferentialObjectMixin, RootFolderMixin):
CATEGORY = [
("policy", _("Policy")),
("process", _("Process")),
@@ -308,7 +308,7 @@ def __str__(self):
return self.name
-class RiskMatrix(ReferentialObjectMixin, AbstractBaseModel, FolderMixin):
+class RiskMatrix(ReferentialObjectMixin, FolderMixin):
library = models.ForeignKey(
Library,
on_delete=models.CASCADE,
@@ -624,7 +624,7 @@ def risk_scoring(probability, impact, risk_matrix: RiskMatrix) -> int:
return risk_index
-class Evidence(AbstractBaseModel, NameDescriptionMixin, FolderMixin):
+class Evidence(NameDescriptionMixin, FolderMixin):
# TODO: Manage file upload to S3/MiniO
attachment = models.FileField(
# upload_to=settings.LOCAL_STORAGE_DIRECTORY,
@@ -688,7 +688,7 @@ def preview(self):
return ""
-class SecurityMeasure(AbstractBaseModel, NameDescriptionMixin, FolderMixin):
+class SecurityMeasure(NameDescriptionMixin, FolderMixin):
class Status(models.TextChoices):
PLANNED = "planned", _("Planned")
ACTIVE = "active", _("Active")
@@ -847,7 +847,7 @@ def save(self, *args, **kwargs):
super(Policy, self).save(*args, **kwargs)
-class RiskScenario(AbstractBaseModel, NameDescriptionMixin):
+class RiskScenario(NameDescriptionMixin):
TREATMENT_OPTIONS = [
("open", _("Open")),
("mitigate", _("Mitigate")),
@@ -1040,7 +1040,7 @@ def save(self, *args, **kwargs):
super(RiskScenario, self).save(*args, **kwargs)
-class RiskAcceptance(AbstractBaseModel, NameDescriptionMixin, FolderMixin):
+class RiskAcceptance(NameDescriptionMixin, FolderMixin):
ACCEPTANCE_STATE = [
("created", _("Created")),
("submitted", _("Submitted")),
@@ -1128,7 +1128,7 @@ def set_state(self, state):
self.save()
-class Framework(ReferentialObjectMixin, AbstractBaseModel, FolderMixin):
+class Framework(ReferentialObjectMixin, FolderMixin):
library = models.ForeignKey(
Library,
on_delete=models.CASCADE,
@@ -1162,7 +1162,7 @@ def is_deletable(self) -> bool:
return True
-class RequirementLevel(ReferentialObjectMixin, AbstractBaseModel, FolderMixin):
+class RequirementLevel(ReferentialObjectMixin, FolderMixin):
framework = models.ForeignKey(
Framework,
on_delete=models.CASCADE,
@@ -1177,7 +1177,7 @@ class Meta:
verbose_name_plural = _("Requirements levels")
-class RequirementNode(ReferentialObjectMixin, AbstractBaseModel, FolderMixin):
+class RequirementNode(ReferentialObjectMixin, FolderMixin):
threats = models.ManyToManyField(
"Threat",
blank=True,
diff --git a/backend/iam/migrations/0001_initial.py b/backend/iam/migrations/0001_initial.py
index 55092e2df7..d5b22163bd 100644
--- a/backend/iam/migrations/0001_initial.py
+++ b/backend/iam/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.0.2 on 2024-02-23 00:51
+# Generated by Django 5.0.2 on 2024-02-24 18:49
import django.db.models.deletion
import django.utils.timezone
@@ -20,10 +20,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Folder',
fields=[
- ('name', models.CharField(max_length=200, verbose_name='Name')),
- ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('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')),
+ ('name', models.CharField(max_length=200, verbose_name='Name')),
+ ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('content_type', models.CharField(choices=[('GL', 'GLOBAL'), ('DO', 'DOMAIN')], default='DO', max_length=2)),
('builtin', models.BooleanField(default=False)),
('parent_folder', models.ForeignKey(default=iam.models._get_root_folder, null=True, on_delete=django.db.models.deletion.CASCADE, to='iam.folder', verbose_name='parent folder')),
@@ -46,7 +46,7 @@ class Migration(migrations.Migration):
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
- ('folder', models.ForeignKey(default=iam.models._get_root_folder, on_delete=django.db.models.deletion.CASCADE, to='iam.folder', verbose_name='Folder')),
+ ('folder', models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder')),
],
options={
'verbose_name': 'user',
@@ -60,19 +60,27 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Role',
fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=150, verbose_name='name')),
- ('builtin', models.BooleanField(default=False)),
+ ('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')),
+ ('name', models.CharField(max_length=200, verbose_name='Name')),
+ ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
+ ('folder', models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder')),
('permissions', models.ManyToManyField(blank=True, to='auth.permission', verbose_name='permissions')),
],
+ options={
+ 'ordering': ['name'],
+ 'abstract': False,
+ },
),
migrations.CreateModel(
name='UserGroup',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
- ('name', models.CharField(max_length=150, verbose_name='name')),
+ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
+ ('name', models.CharField(max_length=200, verbose_name='Name')),
+ ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('builtin', models.BooleanField(default=False)),
- ('folder', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='iam.folder', verbose_name='Domain')),
+ ('folder', models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder')),
],
options={
'verbose_name': 'user group',
@@ -82,15 +90,22 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='RoleAssignment',
fields=[
+ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
+ ('name', models.CharField(max_length=200, verbose_name='Name')),
+ ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('is_recursive', models.BooleanField(default=False, verbose_name='sub folders are visible')),
('builtin', models.BooleanField(default=False)),
- ('folder', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='iam.folder', verbose_name='Folder')),
+ ('folder', models.ForeignKey(default=iam.models._get_root_folder, on_delete=django.db.models.deletion.CASCADE, to='iam.folder', verbose_name='Folder')),
('perimeter_folders', models.ManyToManyField(related_name='perimeter_folders', to='iam.folder', verbose_name='Domain')),
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='iam.role', verbose_name='Role')),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('user_group', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='iam.usergroup')),
],
+ options={
+ 'ordering': ['name'],
+ 'abstract': False,
+ },
),
migrations.AddField(
model_name='user',
diff --git a/backend/iam/migrations/0002_role_builtin.py b/backend/iam/migrations/0002_role_builtin.py
new file mode 100644
index 0000000000..58493b3aff
--- /dev/null
+++ b/backend/iam/migrations/0002_role_builtin.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.2 on 2024-02-24 18:52
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('iam', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='role',
+ name='builtin',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/backend/iam/models.py b/backend/iam/models.py
index b913a2cd86..7da81c3329 100644
--- a/backend/iam/models.py
+++ b/backend/iam/models.py
@@ -45,58 +45,6 @@
logger = structlog.get_logger(__name__)
-class UserGroup(models.Model):
- """UserGroup objects contain users and can be used as principals in role assignments"""
-
- id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
- folder = models.ForeignKey(
- "Folder", verbose_name=_("Domain"), on_delete=models.CASCADE, default=None
- )
- name = models.CharField(_("name"), max_length=150, unique=False)
- builtin = models.BooleanField(default=False)
-
- class Meta:
- """for Model"""
-
- verbose_name = _("user group")
- verbose_name_plural = _("user groups")
-
- def __str__(self) -> str:
- if self.builtin:
- return f"{self.folder.name} - {BUILTIN_USERGROUP_CODENAMES.get(self.name)}"
- return self.name
-
- def get_name_display(self) -> str:
- return self.name
-
- @staticmethod
- def get_user_groups(user):
- # pragma pylint: disable=no-member
- """get the list of user groups containing the user given in parameter"""
- user_group_list = []
- for user_group in UserGroup.objects.all():
- if user in user_group.user_set.all():
- user_group_list.append(user_group)
- return user_group_list
-
-
-class Role(models.Model):
- """A role is a list of permissions"""
-
- permissions = models.ManyToManyField(
- Permission,
- verbose_name=_("permissions"),
- blank=True,
- )
- name = models.CharField(_("name"), max_length=150, unique=False)
- builtin = models.BooleanField(default=False)
-
- def __str__(self) -> str:
- if self.builtin:
- return f"{BUILTIN_ROLE_CODENAMES.get(self.name)}"
- return self.name
-
-
def _get_root_folder():
"""helper function outside of class to facilitate serialization
to be used only in Folder class"""
@@ -106,7 +54,7 @@ def _get_root_folder():
return None
-class Folder(AbstractBaseModel, NameDescriptionMixin):
+class Folder(NameDescriptionMixin):
"""A folder is a container for other folders or any object
Folders are organized in a tree structure, with a single root folder
Folders are the base perimeter for role assignments
@@ -216,6 +164,7 @@ def get_folder(obj: Any):
return None
+
class FolderMixin(models.Model):
"""
Add foreign key to Folder
@@ -231,6 +180,7 @@ class Meta:
abstract = True
+
class RootFolderMixin(FolderMixin):
"""
Add foreign key to Folder, defaults to root folder
@@ -247,6 +197,38 @@ class Meta:
abstract = True
+
+class UserGroup(NameDescriptionMixin, RootFolderMixin):
+ """UserGroup objects contain users and can be used as principals in role assignments"""
+
+ builtin = models.BooleanField(default=False)
+
+ class Meta:
+ """for Model"""
+
+ verbose_name = _("user group")
+ verbose_name_plural = _("user groups")
+
+ def __str__(self) -> str:
+ if self.builtin:
+ return f"{self.folder.name} - {BUILTIN_USERGROUP_CODENAMES.get(self.name)}"
+ return self.name
+
+ def get_name_display(self) -> str:
+ return self.name
+
+ @staticmethod
+ def get_user_groups(user):
+ # pragma pylint: disable=no-member
+ """get the list of user groups containing the user given in parameter"""
+ user_group_list = []
+ for user_group in UserGroup.objects.all():
+ if user in user_group.user_set.all():
+ user_group_list.append(user_group)
+ return user_group_list
+
+
+
class UserManager(BaseUserManager):
use_in_migrations = True
@@ -300,7 +282,7 @@ def create_superuser(self, email, password=None, **extra_fields):
return superuser
-class User(AbstractBaseUser):
+class User(AbstractBaseUser, RootFolderMixin):
"""a user is a principal corresponding to a human"""
try:
@@ -335,12 +317,6 @@ class User(AbstractBaseUser):
),
)
objects = UserManager()
- folder = models.ForeignKey(
- Folder,
- on_delete=models.CASCADE,
- verbose_name=_("Folder"),
- default=_get_root_folder
- )
except:
logger.debug("Exception kludge")
@@ -491,7 +467,25 @@ def set_username(self, username):
self.email = username
-class RoleAssignment(models.Model):
+
+class Role(NameDescriptionMixin, RootFolderMixin):
+ """A role is a list of permissions"""
+
+ permissions = models.ManyToManyField(
+ Permission,
+ verbose_name=_("permissions"),
+ blank=True,
+ )
+ builtin = models.BooleanField(default=False)
+
+ def __str__(self) -> str:
+ if self.builtin:
+ return f"{BUILTIN_ROLE_CODENAMES.get(self.name)}"
+ return self.name
+
+
+
+class RoleAssignment(NameDescriptionMixin, RootFolderMixin):
"""fundamental class for CISO Assistant RBAC model, similar to Azure IAM model"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
@@ -506,7 +500,7 @@ class RoleAssignment(models.Model):
is_recursive = models.BooleanField(_("sub folders are visible"), default=False)
builtin = models.BooleanField(default=False)
folder = models.ForeignKey(
- Folder, on_delete=models.CASCADE, verbose_name=_("Folder")
+ "Folder", verbose_name=_("Folder"), on_delete=models.CASCADE, default=_get_root_folder
)
def __str__(self) -> str:
From 34a33ffe79cbdd877cea4b133878434ec770e63a Mon Sep 17 00:00:00 2001
From: eric-intuitem <71850047+eric-intuitem@users.noreply.github.com>
Date: Sun, 25 Feb 2024 02:03:15 +0100
Subject: [PATCH 2/9] SImplify data model
Fusion RootFolderMixin and FolderMixin
Move RefentialObjectMixin in core (where it is only used)
Move updated_at in AbstracBaseModel
Remove redundant fields (UUID, folder)
---
backend/cal/migrations/0001_initial.py | 2 +-
backend/core/apps.py | 2 +-
backend/core/base_models.py | 55 +-----------
backend/core/migrations/0001_initial.py | 24 +++--
backend/core/migrations/0002_initial.py | 22 ++---
backend/core/models.py | 77 ++++++++++++----
backend/iam/migrations/0001_initial.py | 13 ++-
backend/iam/migrations/0002_role_builtin.py | 18 ----
backend/iam/models.py | 97 ++++++++-------------
9 files changed, 140 insertions(+), 170 deletions(-)
delete mode 100644 backend/iam/migrations/0002_role_builtin.py
diff --git a/backend/cal/migrations/0001_initial.py b/backend/cal/migrations/0001_initial.py
index a852e947d5..6a820714bc 100644
--- a/backend/cal/migrations/0001_initial.py
+++ b/backend/cal/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.0.2 on 2024-02-24 18:49
+# Generated by Django 5.0.2 on 2024-02-25 00:59
from django.db import migrations, models
diff --git a/backend/core/apps.py b/backend/core/apps.py
index 19bfdfe73a..418b5ec748 100644
--- a/backend/core/apps.py
+++ b/backend/core/apps.py
@@ -11,7 +11,7 @@ def startup(**kwargs):
"""
from django.contrib.auth.models import Permission
from iam.models import Folder, Role, RoleAssignment, User, UserGroup
- print("startup handler: initialize database", kwargs)
+ print("startup handler: initialize database")
auditor_permissions = Permission.objects.filter(
codename__in=[
diff --git a/backend/core/base_models.py b/backend/core/base_models.py
index 024019966b..13c77609ae 100644
--- a/backend/core/base_models.py
+++ b/backend/core/base_models.py
@@ -11,6 +11,7 @@
class AbstractBaseModel(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
+ updated_at = models.DateTimeField(auto_now=True, verbose_name=_("UpdatedÒ at"))
class Meta:
abstract = True
@@ -121,57 +122,3 @@ class Meta:
def __str__(self) -> str:
return self.name
-
-
-
-class ReferentialObjectMixin(AbstractBaseModel):
- """
- Mixin for models that have a name and a description.
- """
-
- urn = models.CharField(
- max_length=100, null=True, blank=True, unique=True, verbose_name=_("URN")
- )
- ref_id = models.CharField(
- max_length=100, blank=True, null=True, verbose_name=_("Reference ID")
- )
- locale = models.CharField(
- max_length=100, null=False, blank=False, default="en", verbose_name=_("Locale")
- )
- default_locale = models.BooleanField(default=True, verbose_name=_("Default locale"))
- provider = models.CharField(
- max_length=200, blank=True, null=True, verbose_name=_("Provider")
- )
- name = models.CharField(
- null=True, max_length=200, verbose_name=_("Name"), unique=False
- )
- description = models.TextField(null=True, blank=True, verbose_name=_("Description"))
- annotation = models.TextField(null=True, blank=True, verbose_name=_("Annotation"))
-
- class Meta:
- abstract = True
-
- def display_short(self) -> str:
- _name = (
- self.ref_id
- if not self.name
- else self.name
- if not self.ref_id
- else f"{self.ref_id} - {self.name}"
- )
- _name = "" if not _name else _name
- return _name
-
- def display_long(self) -> str:
- _name = self.display_short()
- _display = (
- _name
- if not self.description
- else self.description
- if _name == ""
- else f"{_name}: {self.description}"
- )
- return _display
-
- def __str__(self) -> str:
- return self.display_short()
diff --git a/backend/core/migrations/0001_initial.py b/backend/core/migrations/0001_initial.py
index ae08bf983d..f1823cc264 100644
--- a/backend/core/migrations/0001_initial.py
+++ b/backend/core/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.0.2 on 2024-02-24 18:49
+# Generated by Django 5.0.2 on 2024-02-25 00:59
import core.validators
import uuid
@@ -18,6 +18,7 @@ class Migration(migrations.Migration):
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')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('business_value', models.CharField(blank=True, max_length=200, verbose_name='business value')),
@@ -34,6 +35,7 @@ class Migration(migrations.Migration):
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')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('version', models.CharField(blank=True, default='1.0', help_text='Version of the compliance assessment (eg. 1.0, 2.0, etc.)', max_length=100, null=True, verbose_name='Version')),
@@ -52,6 +54,7 @@ class Migration(migrations.Migration):
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')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('attachment', models.FileField(blank=True, help_text='Attachment for evidence (eg. screenshot, log file, etc.)', null=True, upload_to='', validators=[core.validators.validate_file_size, core.validators.validate_file_name], verbose_name='Attachment')),
@@ -67,6 +70,7 @@ class Migration(migrations.Migration):
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')),
('urn', models.CharField(blank=True, max_length=100, null=True, unique=True, verbose_name='URN')),
('ref_id', models.CharField(blank=True, max_length=100, null=True, verbose_name='Reference ID')),
('locale', models.CharField(default='en', max_length=100, verbose_name='Locale')),
@@ -86,6 +90,7 @@ class Migration(migrations.Migration):
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')),
('urn', models.CharField(blank=True, max_length=100, null=True, unique=True, verbose_name='URN')),
('ref_id', models.CharField(blank=True, max_length=100, null=True, verbose_name='Reference ID')),
('locale', models.CharField(default='en', max_length=100, verbose_name='Locale')),
@@ -107,6 +112,7 @@ class Migration(migrations.Migration):
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')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('internal_reference', models.CharField(blank=True, max_length=100, null=True, verbose_name='Internal reference')),
@@ -122,6 +128,7 @@ class Migration(migrations.Migration):
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')),
('status', models.CharField(choices=[('to_do', 'To do'), ('in_progress', 'In progress'), ('non_compliant', 'Non compliant'), ('partially_compliant', 'Partially compliant'), ('compliant', 'Compliant'), ('not_applicable', 'Not applicable')], default='to_do', max_length=100, verbose_name='Status')),
('observation', models.TextField(blank=True, null=True, verbose_name='Observation')),
],
@@ -135,6 +142,7 @@ class Migration(migrations.Migration):
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')),
('urn', models.CharField(blank=True, max_length=100, null=True, unique=True, verbose_name='URN')),
('ref_id', models.CharField(blank=True, max_length=100, null=True, verbose_name='Reference ID')),
('locale', models.CharField(default='en', max_length=100, verbose_name='Locale')),
@@ -155,6 +163,7 @@ class Migration(migrations.Migration):
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')),
('urn', models.CharField(blank=True, max_length=100, null=True, unique=True, verbose_name='URN')),
('ref_id', models.CharField(blank=True, max_length=100, null=True, verbose_name='Reference ID')),
('locale', models.CharField(default='en', max_length=100, verbose_name='Locale')),
@@ -179,11 +188,11 @@ class Migration(migrations.Migration):
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')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('state', models.CharField(choices=[('created', 'Created'), ('submitted', 'Submitted'), ('accepted', 'Accepted'), ('rejected', 'Rejected'), ('revoked', 'Revoked')], default='created', max_length=20, verbose_name='State')),
('expiry_date', models.DateField(help_text='Specify when the risk acceptance will no longer apply', null=True, verbose_name='Expiry date')),
- ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('accepted_at', models.DateTimeField(blank=True, null=True, verbose_name='Acceptance date')),
('rejected_at', models.DateTimeField(blank=True, null=True, verbose_name='Rejection date')),
('revoked_at', models.DateTimeField(blank=True, null=True, verbose_name='Revocation date')),
@@ -200,13 +209,13 @@ class Migration(migrations.Migration):
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')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('version', models.CharField(blank=True, default='1.0', help_text='Version of the compliance assessment (eg. 1.0, 2.0, etc.)', max_length=100, null=True, verbose_name='Version')),
('status', models.CharField(choices=[('planned', 'Planned'), ('in_progress', 'In progress'), ('in_review', 'In review'), ('done', 'Done'), ('deprecated', 'Deprecated')], default='planned', max_length=100, verbose_name='Status')),
('eta', models.DateField(blank=True, help_text='Estimated time of arrival', null=True, verbose_name='ETA')),
('due_date', models.DateField(blank=True, help_text='Due date', null=True, verbose_name='Due date')),
- ('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Risk assessment',
@@ -218,6 +227,7 @@ class Migration(migrations.Migration):
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')),
('urn', models.CharField(blank=True, max_length=100, null=True, unique=True, verbose_name='URN')),
('ref_id', models.CharField(blank=True, max_length=100, null=True, verbose_name='Reference ID')),
('locale', models.CharField(default='en', max_length=100, verbose_name='Locale')),
@@ -238,6 +248,7 @@ class Migration(migrations.Migration):
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')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('existing_measures', models.TextField(blank=True, help_text='The existing security measures to manage this risk. Edit the risk scenario to add extra security measures.', max_length=2000, verbose_name='Existing measures')),
@@ -248,7 +259,6 @@ class Migration(migrations.Migration):
('residual_impact', models.SmallIntegerField(default=-1, verbose_name='Residual impact')),
('residual_level', models.SmallIntegerField(default=-1, help_text='The risk level when all the extra measures are done. Automatically updated on Save, based on the chosen risk matrix', verbose_name='Residual level')),
('treatment', models.CharField(choices=[('open', 'Open'), ('mitigate', 'Mitigate'), ('accept', 'Accept'), ('avoid', 'Avoid'), ('transfer', 'Transfer')], default='open', max_length=20, verbose_name='Treatment status')),
- ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('strength_of_knowledge', models.CharField(choices=[('--', '--'), ('0', 'Low'), ('1', 'Medium'), ('2', 'High')], default='--', max_length=20, verbose_name='Strength of Knowledge')),
('justification', models.CharField(blank=True, max_length=500, null=True, verbose_name='Justification')),
],
@@ -262,6 +272,7 @@ class Migration(migrations.Migration):
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')),
('urn', models.CharField(blank=True, max_length=100, null=True, unique=True, verbose_name='URN')),
('ref_id', models.CharField(blank=True, max_length=100, null=True, verbose_name='Reference ID')),
('locale', models.CharField(default='en', max_length=100, verbose_name='Locale')),
@@ -283,6 +294,8 @@ class Migration(migrations.Migration):
name='SecurityMeasure',
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')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('category', models.CharField(blank=True, choices=[('policy', 'Policy'), ('process', 'Process'), ('technical', 'Technical'), ('physical', 'Physical')], max_length=20, null=True, verbose_name='Category')),
@@ -291,8 +304,6 @@ class Migration(migrations.Migration):
('expiry_date', models.DateField(blank=True, help_text='Date after which the security measure is no longer valid', null=True, verbose_name='Expiry date')),
('link', models.CharField(blank=True, help_text='External url for action follow-up (eg. Jira ticket)', max_length=1000, null=True, verbose_name='Link')),
('effort', models.CharField(blank=True, choices=[('S', 'Small'), ('M', 'Medium'), ('L', 'Large'), ('XL', 'Extra-Large')], help_text='Relative effort of the measure (using T-Shirt sizing)', max_length=2, null=True, verbose_name='Effort')),
- ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
- ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
],
options={
'verbose_name': 'Security measure',
@@ -304,6 +315,7 @@ class Migration(migrations.Migration):
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')),
('urn', models.CharField(blank=True, max_length=100, null=True, unique=True, verbose_name='URN')),
('ref_id', models.CharField(blank=True, max_length=100, null=True, verbose_name='Reference ID')),
('locale', models.CharField(default='en', max_length=100, verbose_name='Locale')),
diff --git a/backend/core/migrations/0002_initial.py b/backend/core/migrations/0002_initial.py
index cef9d4a13a..60119ec1ab 100644
--- a/backend/core/migrations/0002_initial.py
+++ b/backend/core/migrations/0002_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.0.2 on 2024-02-24 18:49
+# Generated by Django 5.0.2 on 2024-02-25 00:59
import django.db.models.deletion
import iam.models
@@ -40,12 +40,12 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='evidence',
name='folder',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
+ field=models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
),
migrations.AddField(
model_name='framework',
name='folder',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
+ field=models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
),
migrations.AddField(
model_name='complianceassessment',
@@ -60,7 +60,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='library',
name='folder',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
+ field=models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
),
migrations.AddField(
model_name='framework',
@@ -70,7 +70,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='project',
name='folder',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
+ field=models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
),
migrations.AddField(
model_name='complianceassessment',
@@ -90,12 +90,12 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='requirementassessment',
name='folder',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
+ field=models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
),
migrations.AddField(
model_name='requirementlevel',
name='folder',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
+ field=models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
),
migrations.AddField(
model_name='requirementlevel',
@@ -105,7 +105,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='requirementnode',
name='folder',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
+ field=models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
),
migrations.AddField(
model_name='requirementnode',
@@ -125,7 +125,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='riskacceptance',
name='folder',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
+ field=models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
),
migrations.AddField(
model_name='riskassessment',
@@ -145,7 +145,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='riskmatrix',
name='folder',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
+ field=models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
),
migrations.AddField(
model_name='riskmatrix',
@@ -195,7 +195,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='securitymeasure',
name='folder',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
+ field=models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder'),
),
migrations.AddField(
model_name='securitymeasure',
diff --git a/backend/core/models.py b/backend/core/models.py
index e5e3bef744..113e0e20ae 100644
--- a/backend/core/models.py
+++ b/backend/core/models.py
@@ -7,7 +7,7 @@
from .base_models import *
from .validators import validate_file_size, validate_file_name
-from iam.models import FolderMixin, RootFolderMixin
+from iam.models import FolderMixin
from django.core import serializers
import os
@@ -21,7 +21,60 @@
User = get_user_model()
-class Library(ReferentialObjectMixin, FolderMixin):
+class ReferentialObjectMixin(NameDescriptionMixin, FolderMixin):
+ """
+ Mixin for referential objects.
+ """
+
+ urn = models.CharField(
+ max_length=100, null=True, blank=True, unique=True, verbose_name=_("URN")
+ )
+ ref_id = models.CharField(
+ max_length=100, blank=True, null=True, verbose_name=_("Reference ID")
+ )
+ locale = models.CharField(
+ max_length=100, null=False, blank=False, default="en", verbose_name=_("Locale")
+ )
+ default_locale = models.BooleanField(default=True, verbose_name=_("Default locale"))
+ provider = models.CharField(
+ max_length=200, blank=True, null=True, verbose_name=_("Provider")
+ )
+ name = models.CharField(
+ null=True, max_length=200, verbose_name=_("Name"), unique=False
+ )
+ description = models.TextField(null=True, blank=True, verbose_name=_("Description"))
+ annotation = models.TextField(null=True, blank=True, verbose_name=_("Annotation"))
+
+ class Meta:
+ abstract = True
+
+ def display_short(self) -> str:
+ _name = (
+ self.ref_id
+ if not self.name
+ else self.name
+ if not self.ref_id
+ else f"{self.ref_id} - {self.name}"
+ )
+ _name = "" if not _name else _name
+ return _name
+
+ def display_long(self) -> str:
+ _name = self.display_short()
+ _display = (
+ _name
+ if not self.description
+ else self.description
+ if _name == ""
+ else f"{_name}: {self.description}"
+ )
+ return _display
+
+ def __str__(self) -> str:
+ return self.display_short()
+
+
+class Library(ReferentialObjectMixin):
copyright = models.CharField(
max_length=4096, null=True, blank=True, verbose_name=_("Copyright")
)
@@ -181,7 +234,7 @@ def __str__(self):
return self.name
-class Threat(ReferentialObjectMixin, RootFolderMixin):
+class Threat(ReferentialObjectMixin):
library = models.ForeignKey(
Library, on_delete=models.CASCADE, null=True, blank=True, related_name="threats"
)
@@ -207,7 +260,7 @@ def __str__(self):
return self.name
-class Asset(NameDescriptionMixin, RootFolderMixin):
+class Asset(NameDescriptionMixin, FolderMixin):
class Type(models.TextChoices):
"""
The type of the asset.
@@ -259,7 +312,7 @@ def ancestors_plus_self(self) -> list[Self]:
return list(result)
-class SecurityFunction(ReferentialObjectMixin, RootFolderMixin):
+class SecurityFunction(ReferentialObjectMixin):
CATEGORY = [
("policy", _("Policy")),
("process", _("Process")),
@@ -308,7 +361,7 @@ def __str__(self):
return self.name
-class RiskMatrix(ReferentialObjectMixin, FolderMixin):
+class RiskMatrix(ReferentialObjectMixin):
library = models.ForeignKey(
Library,
on_delete=models.CASCADE,
@@ -376,7 +429,6 @@ class RiskAssessment(Assessment):
help_text=_("WARNING! After choosing it, you will not be able to change it"),
verbose_name=_("Risk matrix"),
)
- updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = _("Risk assessment")
@@ -760,9 +812,6 @@ class Status(models.TextChoices):
verbose_name=_("Effort"),
)
- created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
- updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated at"))
-
fields_to_check = ["name", "category"]
class Meta:
@@ -934,7 +983,6 @@ class RiskScenario(NameDescriptionMixin):
verbose_name=_("Treatment status"),
)
- updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated at"))
strength_of_knowledge = models.CharField(
max_length=20,
choices=SOK_OPTIONS,
@@ -1076,7 +1124,6 @@ class RiskAcceptance(NameDescriptionMixin, FolderMixin):
null=True,
verbose_name=_("Expiry date"),
)
- updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated at"))
accepted_at = models.DateTimeField(
blank=True, null=True, verbose_name=_("Acceptance date")
)
@@ -1128,7 +1175,7 @@ def set_state(self, state):
self.save()
-class Framework(ReferentialObjectMixin, FolderMixin):
+class Framework(ReferentialObjectMixin):
library = models.ForeignKey(
Library,
on_delete=models.CASCADE,
@@ -1162,7 +1209,7 @@ def is_deletable(self) -> bool:
return True
-class RequirementLevel(ReferentialObjectMixin, FolderMixin):
+class RequirementLevel(ReferentialObjectMixin):
framework = models.ForeignKey(
Framework,
on_delete=models.CASCADE,
@@ -1177,7 +1224,7 @@ class Meta:
verbose_name_plural = _("Requirements levels")
-class RequirementNode(ReferentialObjectMixin, FolderMixin):
+class RequirementNode(ReferentialObjectMixin):
threats = models.ManyToManyField(
"Threat",
blank=True,
diff --git a/backend/iam/migrations/0001_initial.py b/backend/iam/migrations/0001_initial.py
index d5b22163bd..1576eb0d7b 100644
--- a/backend/iam/migrations/0001_initial.py
+++ b/backend/iam/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.0.2 on 2024-02-24 18:49
+# Generated by Django 5.0.2 on 2024-02-25 00:59
import django.db.models.deletion
import django.utils.timezone
@@ -22,6 +22,7 @@ class Migration(migrations.Migration):
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')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('content_type', models.CharField(choices=[('GL', 'GLOBAL'), ('DO', 'DOMAIN')], default='DO', max_length=2)),
@@ -39,6 +40,8 @@ class Migration(migrations.Migration):
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('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')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('email', models.CharField(max_length=100, unique=True)),
@@ -62,8 +65,10 @@ class Migration(migrations.Migration):
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')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
+ ('builtin', models.BooleanField(default=False)),
('folder', models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder')),
('permissions', models.ManyToManyField(blank=True, to='auth.permission', verbose_name='permissions')),
],
@@ -77,6 +82,7 @@ class Migration(migrations.Migration):
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')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('builtin', models.BooleanField(default=False)),
@@ -90,13 +96,14 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='RoleAssignment',
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')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
- ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('is_recursive', models.BooleanField(default=False, verbose_name='sub folders are visible')),
('builtin', models.BooleanField(default=False)),
- ('folder', models.ForeignKey(default=iam.models._get_root_folder, on_delete=django.db.models.deletion.CASCADE, to='iam.folder', verbose_name='Folder')),
+ ('folder', models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder')),
('perimeter_folders', models.ManyToManyField(related_name='perimeter_folders', to='iam.folder', verbose_name='Domain')),
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='iam.role', verbose_name='Role')),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
diff --git a/backend/iam/migrations/0002_role_builtin.py b/backend/iam/migrations/0002_role_builtin.py
deleted file mode 100644
index 58493b3aff..0000000000
--- a/backend/iam/migrations/0002_role_builtin.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated by Django 5.0.2 on 2024-02-24 18:52
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('iam', '0001_initial'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='role',
- name='builtin',
- field=models.BooleanField(default=False),
- ),
- ]
diff --git a/backend/iam/models.py b/backend/iam/models.py
index 7da81c3329..6879dc2157 100644
--- a/backend/iam/models.py
+++ b/backend/iam/models.py
@@ -114,7 +114,7 @@ def get_parent_folders(self) -> List[Self]:
)
@staticmethod
- def navigate_structure(start, path):
+ def _navigate_structure(start, path):
"""
Navigate through a mixed structure of objects and dictionaries.
@@ -156,7 +156,7 @@ def get_folder(obj: Any):
# Attempt to traverse each path until a valid folder is found or all paths are exhausted.
for path in paths:
- folder = Folder.navigate_structure(obj, path)
+ folder = Folder._navigate_structure(obj, path)
if folder is not None:
return folder
@@ -166,22 +166,6 @@ def get_folder(obj: Any):
class FolderMixin(models.Model):
- """
- Add foreign key to Folder
- """
-
- folder = models.ForeignKey(
- Folder,
- on_delete=models.CASCADE,
- related_name="%(class)s_folder",
- )
-
- class Meta:
- abstract = True
-
-
-
-class RootFolderMixin(FolderMixin):
"""
Add foreign key to Folder, defaults to root folder
"""
@@ -198,7 +182,7 @@ class Meta:
-class UserGroup(NameDescriptionMixin, RootFolderMixin):
+class UserGroup(NameDescriptionMixin, FolderMixin):
"""UserGroup objects contain users and can be used as principals in role assignments"""
builtin = models.BooleanField(default=False)
@@ -282,44 +266,39 @@ def create_superuser(self, email, password=None, **extra_fields):
return superuser
-class User(AbstractBaseUser, RootFolderMixin):
+class User(AbstractBaseUser, AbstractBaseModel, FolderMixin):
"""a user is a principal corresponding to a human"""
- try:
- id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
- last_name = models.CharField(_("last name"), max_length=150, blank=True)
- first_name = models.CharField(_("first name"), max_length=150, blank=True)
- email = models.CharField(max_length=100, unique=True)
- first_login = models.BooleanField(default=True)
- is_active = models.BooleanField(
- _("active"),
- default=True,
- help_text=_(
- "Designates whether this user should be treated as active. "
- "Unselect this instead of deleting accounts."
- ),
- )
- date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
- is_superuser = models.BooleanField(
- _("superuser status"),
- default=False,
- help_text=_(
- "Designates that this user has all permissions without explicitly assigning them."
- ),
- )
- user_groups = models.ManyToManyField(
- UserGroup,
- verbose_name=_("user groups"),
- blank=True,
- help_text=_(
- "The user groups this user belongs to. A user will get all permissions "
- "granted to each of their user groups."
- ),
- )
- objects = UserManager()
-
- except:
- logger.debug("Exception kludge")
+ last_name = models.CharField(_("last name"), max_length=150, blank=True)
+ first_name = models.CharField(_("first name"), max_length=150, blank=True)
+ email = models.CharField(max_length=100, unique=True)
+ first_login = models.BooleanField(default=True)
+ is_active = models.BooleanField(
+ _("active"),
+ default=True,
+ help_text=_(
+ "Designates whether this user should be treated as active. "
+ "Unselect this instead of deleting accounts."
+ ),
+ )
+ date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
+ is_superuser = models.BooleanField(
+ _("superuser status"),
+ default=False,
+ help_text=_(
+ "Designates that this user has all permissions without explicitly assigning them."
+ ),
+ )
+ user_groups = models.ManyToManyField(
+ UserGroup,
+ verbose_name=_("user groups"),
+ blank=True,
+ help_text=_(
+ "The user groups this user belongs to. A user will get all permissions "
+ "granted to each of their user groups."
+ ),
+ )
+ objects = UserManager()
# USERNAME_FIELD is used as the unique identifier for the user
# and is required by Django to be set to a non-empty value.
@@ -468,7 +447,7 @@ def set_username(self, username):
-class Role(NameDescriptionMixin, RootFolderMixin):
+class Role(NameDescriptionMixin, FolderMixin):
"""A role is a list of permissions"""
permissions = models.ManyToManyField(
@@ -485,10 +464,9 @@ def __str__(self) -> str:
-class RoleAssignment(NameDescriptionMixin, RootFolderMixin):
+class RoleAssignment(NameDescriptionMixin, FolderMixin):
"""fundamental class for CISO Assistant RBAC model, similar to Azure IAM model"""
- id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
perimeter_folders = models.ManyToManyField(
"Folder", verbose_name=_("Domain"), related_name="perimeter_folders"
)
@@ -499,9 +477,6 @@ class RoleAssignment(NameDescriptionMixin, RootFolderMixin):
role = models.ForeignKey(Role, on_delete=models.CASCADE, verbose_name=_("Role"))
is_recursive = models.BooleanField(_("sub folders are visible"), default=False)
builtin = models.BooleanField(default=False)
- folder = models.ForeignKey(
- "Folder", verbose_name=_("Folder"), on_delete=models.CASCADE, default=_get_root_folder
- )
def __str__(self) -> str:
# pragma pylint: disable=no-member
From 12f98d48c07f6ed924e6ebb0634c9e473227dd74 Mon Sep 17 00:00:00 2001
From: eric-intuitem <71850047+eric-intuitem@users.noreply.github.com>
Date: Sun, 25 Feb 2024 02:29:26 +0100
Subject: [PATCH 3/9] Update models.py
Reorder classes in a logical way
---
backend/core/models.py | 1515 ++++++++++++++++++++--------------------
1 file changed, 763 insertions(+), 752 deletions(-)
diff --git a/backend/core/models.py b/backend/core/models.py
index 113e0e20ae..c46f296de8 100644
--- a/backend/core/models.py
+++ b/backend/core/models.py
@@ -20,6 +20,7 @@
User = get_user_model()
+########################### Referential objects #########################
class ReferentialObjectMixin(NameDescriptionMixin, FolderMixin):
"""
@@ -134,106 +135,6 @@ def delete(self, *args, **kwargs):
super(Library, self).delete(*args, **kwargs)
-class Assessment(NameDescriptionMixin):
- 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")
-
- project = models.ForeignKey(
- "Project", on_delete=models.CASCADE, verbose_name=_("Project")
- )
- version = models.CharField(
- max_length=100,
- blank=True,
- null=True,
- help_text=_("Version of the compliance assessment (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"),
- )
- authors = models.ManyToManyField(
- User,
- blank=True,
- verbose_name=_("Authors"),
- related_name="%(class)s_authors",
- )
- reviewers = models.ManyToManyField(
- User,
- blank=True,
- verbose_name=_("Reviewers"),
- related_name="%(class)s_reviewers",
- )
- eta = models.DateField(
- null=True,
- blank=True,
- help_text=_("Estimated time of arrival"),
- verbose_name=_("ETA"),
- )
- due_date = models.DateField(
- null=True,
- blank=True,
- help_text=_("Due date"),
- verbose_name=_("Due date"),
- )
-
- fields_to_check = ["name", "version"]
-
- class Meta:
- abstract = True
-
-
-class Project(NameDescriptionMixin, FolderMixin):
- PRJ_LC_STATUS = [
- ("undefined", _("--")),
- ("in_design", _("Design")),
- ("in_dev", _("Development")),
- ("in_prod", _("Production")),
- ("eol", _("End Of Life")),
- ("dropped", _("Dropped")),
- ]
- internal_reference = models.CharField(
- max_length=100, null=True, blank=True, verbose_name=_("Internal reference")
- )
- lc_status = models.CharField(
- max_length=20,
- default="in_design",
- choices=PRJ_LC_STATUS,
- verbose_name=_("Status"),
- )
-
- class Meta:
- verbose_name = _("Project")
- verbose_name_plural = _("Projects")
-
- def overall_compliance(self):
- compliance_assessments_list = [
- compliance_assessment
- for compliance_assessment in self.compliance_assessment_set.all()
- ]
- count = (
- RequirementAssessment.objects.filter(status="compliant")
- .filter(compliance_assessment__in=compliance_assessments_list)
- .count()
- )
- total = RequirementAssessment.objects.filter(
- compliance_assessment__in=compliance_assessments_list
- ).count()
- if total == 0:
- return 0
- return round(count * 100 / total)
-
- def __str__(self):
- return self.name
-
-
class Threat(ReferentialObjectMixin):
library = models.ForeignKey(
Library, on_delete=models.CASCADE, null=True, blank=True, related_name="threats"
@@ -260,58 +161,6 @@ def __str__(self):
return self.name
-class Asset(NameDescriptionMixin, FolderMixin):
- class Type(models.TextChoices):
- """
- The type of the asset.
-
- An asset can either be a primary or a support asset.
- A support asset can be linked to another "parent" asset of type primary or support.
- Cycles are not allowed
- """
-
- PRIMARY = "PR", _("Primary")
- SUPPORT = "SP", _("Support")
-
- business_value = models.CharField(
- max_length=200, blank=True, verbose_name=_("business value")
- )
- type = models.CharField(
- max_length=2, choices=Type.choices, default=Type.SUPPORT, verbose_name=_("type")
- )
- parent_assets = models.ManyToManyField(
- "self", blank=True, verbose_name=_("parent assets"), symmetrical=False
- )
- is_published = models.BooleanField(_("published"), default=True)
-
- fields_to_check = ["name"]
-
- class Meta:
- verbose_name_plural = _("Assets")
- verbose_name = _("Asset")
-
- def __str__(self) -> str:
- return str(self.name)
-
- def is_primary(self) -> bool:
- """
- Returns True if the asset is a primary asset.
- """
- return self.type == Asset.Type.PRIMARY
-
- def is_support(self) -> bool:
- """
- Returns True if the asset is a support asset.
- """
- return self.type == Asset.Type.SUPPORT
-
- def ancestors_plus_self(self) -> list[Self]:
- result = {self}
- for x in self.parent_assets.all():
- result.update(x.ancestors_plus_self())
- return list(result)
-
-
class SecurityFunction(ReferentialObjectMixin):
CATEGORY = [
("policy", _("Policy")),
@@ -422,82 +271,541 @@ def __str__(self) -> str:
return self.name
-class RiskAssessment(Assessment):
- risk_matrix = models.ForeignKey(
- RiskMatrix,
- on_delete=models.PROTECT,
- help_text=_("WARNING! After choosing it, you will not be able to change it"),
- verbose_name=_("Risk matrix"),
+class Framework(ReferentialObjectMixin):
+ library = models.ForeignKey(
+ Library,
+ on_delete=models.CASCADE,
+ null=True,
+ blank=True,
+ related_name="frameworks",
)
class Meta:
- verbose_name = _("Risk assessment")
- verbose_name_plural = _("Risk assessments")
+ verbose_name = _("Framework")
+ verbose_name_plural = _("Frameworks")
- def __str__(self) -> str:
- return f"{self.project.folder}/{self.project}/{self.name} - {self.version}"
+ def get_next_order_id(self, obj_type: models.Model, _parent_urn: str = None) -> int:
+ """
+ Returns the next order id for a given object type
+ """
+ if _parent_urn:
+ return (
+ obj_type.objects.filter(framework=self, parent_urn=_parent_urn).count()
+ + 1
+ )
+ else:
+ return obj_type.objects.filter(framework=self).count() + 1
- @property
- def path_display(self) -> str:
- return f"{self.project.folder}/{self.project}/{self.name} - {self.version}"
+ def is_deletable(self) -> bool:
+ """
+ Returns True if the framework can be deleted
+ """
+ if self.compliance_assessment_set.count() > 0:
+ return False
+ return True
- def get_scenario_count(self) -> int:
- count = RiskScenario.objects.filter(risk_assessment=self.id).count()
- scenario_count = count
- return scenario_count
- def quality_check(self) -> dict:
- errors_lst = list()
- warnings_lst = list()
- info_lst = list()
- # --- check on the risk risk_assessment:
- _object = serializers.serialize("json", [self])
- _object = json.loads(_object)
- if self.status == Assessment.Status.IN_PROGRESS:
- info_lst.append(
- {
- "msg": _("{}: Risk assessment is still in progress").format(
- str(self)
- ),
- "obj_type": "risk_assessment",
- "object": _object,
- }
- )
- if not self.authors:
- info_lst.append(
- {
- "msg": _(
- "{}: No author assigned to this risk risk assessment"
- ).format(str(self)),
- "obj_type": "risk_assessment",
- "object": _object,
- }
- )
- if not self.risk_scenarios.all():
- warnings_lst.append(
- {
- "msg": _(
- "{}: RiskAssessment is empty. No risk scenario declared yet"
- ).format(self),
- "obj_type": "risk_assessment",
- "object": _object,
- }
- )
- # ---
+class RequirementLevel(ReferentialObjectMixin):
+ framework = models.ForeignKey(
+ Framework,
+ on_delete=models.CASCADE,
+ null=True,
+ blank=True,
+ verbose_name=_("Framework"),
+ )
+ level = models.IntegerField(null=False, blank=False, verbose_name=_("Level"))
- # --- checks on the risk scenarios
- # TODO: Refactor this
- _scenarios = serializers.serialize(
- "json", self.risk_scenarios.all().order_by("created_at")
- )
- scenarios = [x["fields"] for x in json.loads(_scenarios)]
- for ri in scenarios:
- if ri["current_level"] < 0:
- warnings_lst.append(
- {
- "msg": _("{} current risk level has not been assessed").format(
- ri["name"]
- ),
+ class Meta:
+ verbose_name = _("Requirements level")
+ verbose_name_plural = _("Requirements levels")
+
+
+class RequirementNode(ReferentialObjectMixin):
+ threats = models.ManyToManyField(
+ "Threat",
+ blank=True,
+ verbose_name=_("Threats"),
+ related_name="requirements",
+ )
+ security_functions = models.ManyToManyField(
+ "SecurityFunction",
+ blank=True,
+ verbose_name=_("Security functions"),
+ related_name="requirements",
+ )
+ framework = models.ForeignKey(
+ Framework,
+ on_delete=models.CASCADE,
+ null=True,
+ blank=True,
+ verbose_name=_("Framework"),
+ )
+ parent_urn = models.CharField(
+ max_length=100, null=True, blank=True, verbose_name=_("Parent URN")
+ )
+ order_id = models.IntegerField(null=True, verbose_name=_("Order ID"))
+ level = models.IntegerField(null=True, verbose_name=_("Level"))
+ maturity = models.IntegerField(null=True, verbose_name=_("Maturity"))
+ assessable = models.BooleanField(null=False, verbose_name=_("Assessable"))
+
+ class Meta:
+ verbose_name = _("RequirementNode")
+ verbose_name_plural = _("RequirementNodes")
+
+
+########################### Domain objects #########################
+
+class Project(NameDescriptionMixin, FolderMixin):
+ PRJ_LC_STATUS = [
+ ("undefined", _("--")),
+ ("in_design", _("Design")),
+ ("in_dev", _("Development")),
+ ("in_prod", _("Production")),
+ ("eol", _("End Of Life")),
+ ("dropped", _("Dropped")),
+ ]
+ internal_reference = models.CharField(
+ max_length=100, null=True, blank=True, verbose_name=_("Internal reference")
+ )
+ lc_status = models.CharField(
+ max_length=20,
+ default="in_design",
+ choices=PRJ_LC_STATUS,
+ verbose_name=_("Status"),
+ )
+
+ class Meta:
+ verbose_name = _("Project")
+ verbose_name_plural = _("Projects")
+
+ def overall_compliance(self):
+ compliance_assessments_list = [
+ compliance_assessment
+ for compliance_assessment in self.compliance_assessment_set.all()
+ ]
+ count = (
+ RequirementAssessment.objects.filter(status="compliant")
+ .filter(compliance_assessment__in=compliance_assessments_list)
+ .count()
+ )
+ total = RequirementAssessment.objects.filter(
+ compliance_assessment__in=compliance_assessments_list
+ ).count()
+ if total == 0:
+ return 0
+ return round(count * 100 / total)
+
+ def __str__(self):
+ return self.name
+
+
+class Asset(NameDescriptionMixin, FolderMixin):
+ class Type(models.TextChoices):
+ """
+ The type of the asset.
+
+ An asset can either be a primary or a support asset.
+ A support asset can be linked to another "parent" asset of type primary or support.
+ Cycles are not allowed
+ """
+
+ PRIMARY = "PR", _("Primary")
+ SUPPORT = "SP", _("Support")
+
+ business_value = models.CharField(
+ max_length=200, blank=True, verbose_name=_("business value")
+ )
+ type = models.CharField(
+ max_length=2, choices=Type.choices, default=Type.SUPPORT, verbose_name=_("type")
+ )
+ parent_assets = models.ManyToManyField(
+ "self", blank=True, verbose_name=_("parent assets"), symmetrical=False
+ )
+ is_published = models.BooleanField(_("published"), default=True)
+
+ fields_to_check = ["name"]
+
+ class Meta:
+ verbose_name_plural = _("Assets")
+ verbose_name = _("Asset")
+
+ def __str__(self) -> str:
+ return str(self.name)
+
+ def is_primary(self) -> bool:
+ """
+ Returns True if the asset is a primary asset.
+ """
+ return self.type == Asset.Type.PRIMARY
+
+ def is_support(self) -> bool:
+ """
+ Returns True if the asset is a support asset.
+ """
+ return self.type == Asset.Type.SUPPORT
+
+ def ancestors_plus_self(self) -> list[Self]:
+ result = {self}
+ for x in self.parent_assets.all():
+ result.update(x.ancestors_plus_self())
+ return list(result)
+
+
+class Evidence(NameDescriptionMixin, FolderMixin):
+ # TODO: Manage file upload to S3/MiniO
+ attachment = models.FileField(
+ # upload_to=settings.LOCAL_STORAGE_DIRECTORY,
+ blank=True,
+ null=True,
+ help_text=_("Attachment for evidence (eg. screenshot, log file, etc.)"),
+ verbose_name=_("Attachment"),
+ validators=[validate_file_size, validate_file_name],
+ )
+ link = models.URLField(
+ blank=True,
+ null=True,
+ help_text=_("Link to the evidence (eg. Jira ticket, etc.)"),
+ verbose_name=_("Link"),
+ )
+
+ class Meta:
+ verbose_name = _("Evidence")
+ verbose_name_plural = _("Evidences")
+
+ def get_folder(self):
+ if self.security_measures:
+ return self.security_measures.first().folder
+ elif self.requirement_assessments:
+ return self.requirement_assessments.first().folder
+ else:
+ return None
+
+ def filename(self):
+ return os.path.basename(self.attachment.name)
+
+ def preview(self):
+ if self.attachment:
+ if self.filename().endswith((".png", ".jpg", ".jpeg")):
+ return (
+ "image",
+ mark_safe(''.format(self.attachment.url)),
+ )
+ if self.filename().endswith(".txt"):
+ with open(self.attachment.path, "r") as text:
+ return ("text", text.read())
+ if self.filename().endswith(".pdf"):
+ return (
+ "pdf",
+ mark_safe(
+ ''.format(
+ self.attachment.url
+ )
+ ),
+ )
+ if self.filename().endswith(".docx"):
+ return (
+ "icon",
+ mark_safe(''.format("/static/icons/word.png")),
+ )
+ if self.filename().endswith((".xls", ".xlsx", ".csv")):
+ return (
+ "icon",
+ mark_safe(''.format("/static/icons/excel.png")),
+ )
+ return ""
+
+
+class SecurityMeasure(NameDescriptionMixin, FolderMixin):
+ class Status(models.TextChoices):
+ PLANNED = "planned", _("Planned")
+ ACTIVE = "active", _("Active")
+ INACTIVE = "inactive", _("Inactive")
+
+ CATEGORY = SecurityFunction.CATEGORY
+
+ EFFORT = [
+ ("S", _("Small")),
+ ("M", _("Medium")),
+ ("L", _("Large")),
+ ("XL", _("Extra-Large")),
+ ]
+
+ MAP_EFFORT = {None: -1, "S": 1, "M": 2, "L": 4, "XL": 8}
+ # todo: think about a smarter model for ranking
+ security_function = models.ForeignKey(
+ SecurityFunction,
+ on_delete=models.CASCADE,
+ null=True,
+ blank=True,
+ verbose_name=_("Security Function"),
+ )
+ evidences = models.ManyToManyField(
+ Evidence,
+ blank=True,
+ verbose_name=_("Evidences"),
+ related_name="security_measures",
+ )
+ category = models.CharField(
+ max_length=20,
+ choices=CATEGORY,
+ null=True,
+ blank=True,
+ verbose_name=_("Category"),
+ )
+ status = models.CharField(
+ max_length=20,
+ choices=Status.choices,
+ null=True,
+ blank=True,
+ verbose_name=_("Status"),
+ )
+ eta = models.DateField(
+ blank=True,
+ null=True,
+ help_text=_("Estimated Time of Arrival"),
+ verbose_name=_("ETA"),
+ )
+ expiry_date = models.DateField(
+ blank=True,
+ null=True,
+ help_text=_("Date after which the security measure is no longer valid"),
+ verbose_name=_("Expiry date"),
+ )
+ link = models.CharField(
+ null=True,
+ blank=True,
+ max_length=1000,
+ help_text=_("External url for action follow-up (eg. Jira ticket)"),
+ verbose_name=_("Link"),
+ )
+ effort = models.CharField(
+ null=True,
+ blank=True,
+ max_length=2,
+ choices=EFFORT,
+ help_text=_("Relative effort of the measure (using T-Shirt sizing)"),
+ verbose_name=_("Effort"),
+ )
+
+ fields_to_check = ["name", "category"]
+
+ class Meta:
+ verbose_name = _("Security measure")
+ verbose_name_plural = _("Security measures")
+
+ def save(self, *args, **kwargs):
+ if self.security_function and self.category is None:
+ self.category = self.security_function.category
+ super(SecurityMeasure, self).save(*args, **kwargs)
+
+ @property
+ def risk_scenarios(self):
+ return self.risk_scenarios.all()
+
+ @property
+ def risk_assessments(self):
+ return {scenario.risk_assessment for scenario in self.risk_scenarios}
+
+ @property
+ def projects(self):
+ return {risk_assessment.project for risk_assessment in self.risk_assessments}
+
+ def parent_project(self):
+ pass
+
+ def __str__(self):
+ return self.name
+
+ @property
+ def mid(self):
+ return f"M.{self.scoped_id(scope=SecurityMeasure.objects.filter(folder=self.folder))}"
+
+ @property
+ def csv_value(self):
+ return f"[{self.status}] {self.name}" if self.status else self.name
+
+ def get_ranking_score(self):
+ if self.effort:
+ value = 0
+ for risk_scenario in self.risk_scenarios.all():
+ current = risk_scenario.current_level
+ residual = risk_scenario.residual_level
+ if current >= 0 and residual >= 0:
+ value += (1 + current - residual) * (current + 1)
+ return round(value / self.MAP_EFFORT[self.effort], 4)
+ else:
+ return 0
+
+ @property
+ def get_html_url(self):
+ url = reverse("securitymeasure-detail", args=(self.id,))
+ return format_html(
+ ' [MT-eta] {}: {} ',
+ url,
+ self.folder.name,
+ self.name,
+ )
+
+ def get_linked_requirements_count(self):
+ return RequirementNode.objects.filter(
+ requirementassessment__security_measures=self
+ ).count()
+
+
+class PolicyManager(models.Manager):
+ def create(self, *args, **kwargs):
+ kwargs["category"] = "policy" # Ensure category is always "policy"
+ return super().create(*args, **kwargs)
+
+
+class Policy(SecurityMeasure):
+ class Meta:
+ proxy = True
+ verbose_name = _("Policy")
+ verbose_name_plural = _("Policies")
+
+ objects = PolicyManager() # Use the custom manager
+
+ def save(self, *args, **kwargs):
+ self.category = "policy"
+ super(Policy, self).save(*args, **kwargs)
+
+
+########################### Secondary objects #########################
+
+
+class Assessment(NameDescriptionMixin):
+ 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")
+
+ project = models.ForeignKey(
+ "Project", on_delete=models.CASCADE, verbose_name=_("Project")
+ )
+ version = models.CharField(
+ max_length=100,
+ blank=True,
+ null=True,
+ help_text=_("Version of the compliance assessment (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"),
+ )
+ authors = models.ManyToManyField(
+ User,
+ blank=True,
+ verbose_name=_("Authors"),
+ related_name="%(class)s_authors",
+ )
+ reviewers = models.ManyToManyField(
+ User,
+ blank=True,
+ verbose_name=_("Reviewers"),
+ related_name="%(class)s_reviewers",
+ )
+ eta = models.DateField(
+ null=True,
+ blank=True,
+ help_text=_("Estimated time of arrival"),
+ verbose_name=_("ETA"),
+ )
+ due_date = models.DateField(
+ null=True,
+ blank=True,
+ help_text=_("Due date"),
+ verbose_name=_("Due date"),
+ )
+
+ fields_to_check = ["name", "version"]
+
+ class Meta:
+ abstract = True
+
+
+class RiskAssessment(Assessment):
+ risk_matrix = models.ForeignKey(
+ RiskMatrix,
+ on_delete=models.PROTECT,
+ help_text=_("WARNING! After choosing it, you will not be able to change it"),
+ verbose_name=_("Risk matrix"),
+ )
+
+ class Meta:
+ verbose_name = _("Risk assessment")
+ verbose_name_plural = _("Risk assessments")
+
+ def __str__(self) -> str:
+ return f"{self.project.folder}/{self.project}/{self.name} - {self.version}"
+
+ @property
+ def path_display(self) -> str:
+ return f"{self.project.folder}/{self.project}/{self.name} - {self.version}"
+
+ def get_scenario_count(self) -> int:
+ count = RiskScenario.objects.filter(risk_assessment=self.id).count()
+ scenario_count = count
+ return scenario_count
+
+ def quality_check(self) -> dict:
+ errors_lst = list()
+ warnings_lst = list()
+ info_lst = list()
+ # --- check on the risk risk_assessment:
+ _object = serializers.serialize("json", [self])
+ _object = json.loads(_object)
+ if self.status == Assessment.Status.IN_PROGRESS:
+ info_lst.append(
+ {
+ "msg": _("{}: Risk assessment is still in progress").format(
+ str(self)
+ ),
+ "obj_type": "risk_assessment",
+ "object": _object,
+ }
+ )
+ if not self.authors:
+ info_lst.append(
+ {
+ "msg": _(
+ "{}: No author assigned to this risk risk assessment"
+ ).format(str(self)),
+ "obj_type": "risk_assessment",
+ "object": _object,
+ }
+ )
+ if not self.risk_scenarios.all():
+ warnings_lst.append(
+ {
+ "msg": _(
+ "{}: RiskAssessment is empty. No risk scenario declared yet"
+ ).format(self),
+ "obj_type": "risk_assessment",
+ "object": _object,
+ }
+ )
+ # ---
+
+ # --- checks on the risk scenarios
+ # TODO: Refactor this
+ _scenarios = serializers.serialize(
+ "json", self.risk_scenarios.all().order_by("created_at")
+ )
+ scenarios = [x["fields"] for x in json.loads(_scenarios)]
+ for ri in scenarios:
+ if ri["current_level"] < 0:
+ warnings_lst.append(
+ {
+ "msg": _("{} current risk level has not been assessed").format(
+ ri["name"]
+ ),
"obj_type": "riskscenario",
"object": ri,
}
@@ -624,276 +932,56 @@ def quality_check(self) -> dict:
).format(mtg["name"]),
"obj_type": "securitymeasure",
"object": {"name": mtg["name"], "id": mtg["id"]},
- }
- )
-
- # --- checks on the risk acceptances
- _acceptances = serializers.serialize(
- "json",
- RiskAcceptance.objects.filter(risk_scenarios__risk_assessment=self)
- .distinct()
- .order_by("created_at"),
- )
- acceptances = [x["fields"] for x in json.loads(_acceptances)]
- for ra in acceptances:
- if not ra["expiry_date"]:
- warnings_lst.append(
- {
- "msg": _("{}: Acceptance has no expiry date").format(
- ra["name"]
- ),
- "obj_type": "securitymeasure",
- "object": ra,
- }
- )
- continue
- if date.today() > datetime.strptime(ra["expiry_date"], "%Y-%m-%d").date():
- errors_lst.append(
- {
- "msg": _(
- "{}: Acceptance has expired. Consider updating the status or the date"
- ).format(ra["name"]),
- "obj_type": "riskacceptance",
- "object": ra,
- }
- )
-
- findings = {
- "errors": errors_lst,
- "warnings": warnings_lst,
- "info": info_lst,
- "count": len(errors_lst + warnings_lst + info_lst),
- }
- return findings
-
- # NOTE: if your save() method throws an exception, you might want to override the clean() method to prevent
- # 500 errors when the form submitted. See https://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.clean
-
-
-def risk_scoring(probability, impact, risk_matrix: RiskMatrix) -> int:
- fields = json.loads(risk_matrix.json_definition)
- risk_index = fields["grid"][probability][impact]
- return risk_index
-
-
-class Evidence(NameDescriptionMixin, FolderMixin):
- # TODO: Manage file upload to S3/MiniO
- attachment = models.FileField(
- # upload_to=settings.LOCAL_STORAGE_DIRECTORY,
- blank=True,
- null=True,
- help_text=_("Attachment for evidence (eg. screenshot, log file, etc.)"),
- verbose_name=_("Attachment"),
- validators=[validate_file_size, validate_file_name],
- )
- link = models.URLField(
- blank=True,
- null=True,
- help_text=_("Link to the evidence (eg. Jira ticket, etc.)"),
- verbose_name=_("Link"),
- )
-
- class Meta:
- verbose_name = _("Evidence")
- verbose_name_plural = _("Evidences")
-
- def get_folder(self):
- if self.security_measures:
- return self.security_measures.first().folder
- elif self.requirement_assessments:
- return self.requirement_assessments.first().folder
- else:
- return None
-
- def filename(self):
- return os.path.basename(self.attachment.name)
-
- def preview(self):
- if self.attachment:
- if self.filename().endswith((".png", ".jpg", ".jpeg")):
- return (
- "image",
- mark_safe(''.format(self.attachment.url)),
- )
- if self.filename().endswith(".txt"):
- with open(self.attachment.path, "r") as text:
- return ("text", text.read())
- if self.filename().endswith(".pdf"):
- return (
- "pdf",
- mark_safe(
- ''.format(
- self.attachment.url
- )
- ),
- )
- if self.filename().endswith(".docx"):
- return (
- "icon",
- mark_safe(''.format("/static/icons/word.png")),
- )
- if self.filename().endswith((".xls", ".xlsx", ".csv")):
- return (
- "icon",
- mark_safe(''.format("/static/icons/excel.png")),
- )
- return ""
-
-
-class SecurityMeasure(NameDescriptionMixin, FolderMixin):
- class Status(models.TextChoices):
- PLANNED = "planned", _("Planned")
- ACTIVE = "active", _("Active")
- INACTIVE = "inactive", _("Inactive")
-
- CATEGORY = SecurityFunction.CATEGORY
-
- EFFORT = [
- ("S", _("Small")),
- ("M", _("Medium")),
- ("L", _("Large")),
- ("XL", _("Extra-Large")),
- ]
-
- MAP_EFFORT = {None: -1, "S": 1, "M": 2, "L": 4, "XL": 8}
- # todo: think about a smarter model for ranking
- security_function = models.ForeignKey(
- SecurityFunction,
- on_delete=models.CASCADE,
- null=True,
- blank=True,
- verbose_name=_("Security Function"),
- )
- evidences = models.ManyToManyField(
- Evidence,
- blank=True,
- verbose_name=_("Evidences"),
- related_name="security_measures",
- )
- category = models.CharField(
- max_length=20,
- choices=CATEGORY,
- null=True,
- blank=True,
- verbose_name=_("Category"),
- )
- status = models.CharField(
- max_length=20,
- choices=Status.choices,
- null=True,
- blank=True,
- verbose_name=_("Status"),
- )
- eta = models.DateField(
- blank=True,
- null=True,
- help_text=_("Estimated Time of Arrival"),
- verbose_name=_("ETA"),
- )
- expiry_date = models.DateField(
- blank=True,
- null=True,
- help_text=_("Date after which the security measure is no longer valid"),
- verbose_name=_("Expiry date"),
- )
- link = models.CharField(
- null=True,
- blank=True,
- max_length=1000,
- help_text=_("External url for action follow-up (eg. Jira ticket)"),
- verbose_name=_("Link"),
- )
- effort = models.CharField(
- null=True,
- blank=True,
- max_length=2,
- choices=EFFORT,
- help_text=_("Relative effort of the measure (using T-Shirt sizing)"),
- verbose_name=_("Effort"),
- )
-
- fields_to_check = ["name", "category"]
-
- class Meta:
- verbose_name = _("Security measure")
- verbose_name_plural = _("Security measures")
-
- def save(self, *args, **kwargs):
- if self.security_function and self.category is None:
- self.category = self.security_function.category
- super(SecurityMeasure, self).save(*args, **kwargs)
-
- @property
- def risk_scenarios(self):
- return self.risk_scenarios.all()
-
- @property
- def risk_assessments(self):
- return {scenario.risk_assessment for scenario in self.risk_scenarios}
-
- @property
- def projects(self):
- return {risk_assessment.project for risk_assessment in self.risk_assessments}
-
- def parent_project(self):
- pass
-
- def __str__(self):
- return self.name
-
- @property
- def mid(self):
- return f"M.{self.scoped_id(scope=SecurityMeasure.objects.filter(folder=self.folder))}"
-
- @property
- def csv_value(self):
- return f"[{self.status}] {self.name}" if self.status else self.name
-
- def get_ranking_score(self):
- if self.effort:
- value = 0
- for risk_scenario in self.risk_scenarios.all():
- current = risk_scenario.current_level
- residual = risk_scenario.residual_level
- if current >= 0 and residual >= 0:
- value += (1 + current - residual) * (current + 1)
- return round(value / self.MAP_EFFORT[self.effort], 4)
- else:
- return 0
-
- @property
- def get_html_url(self):
- url = reverse("securitymeasure-detail", args=(self.id,))
- return format_html(
- ' [MT-eta] {}: {} ',
- url,
- self.folder.name,
- self.name,
- )
-
- def get_linked_requirements_count(self):
- return RequirementNode.objects.filter(
- requirementassessment__security_measures=self
- ).count()
-
+ }
+ )
-class PolicyManager(models.Manager):
- def create(self, *args, **kwargs):
- kwargs["category"] = "policy" # Ensure category is always "policy"
- return super().create(*args, **kwargs)
+ # --- checks on the risk acceptances
+ _acceptances = serializers.serialize(
+ "json",
+ RiskAcceptance.objects.filter(risk_scenarios__risk_assessment=self)
+ .distinct()
+ .order_by("created_at"),
+ )
+ acceptances = [x["fields"] for x in json.loads(_acceptances)]
+ for ra in acceptances:
+ if not ra["expiry_date"]:
+ warnings_lst.append(
+ {
+ "msg": _("{}: Acceptance has no expiry date").format(
+ ra["name"]
+ ),
+ "obj_type": "securitymeasure",
+ "object": ra,
+ }
+ )
+ continue
+ if date.today() > datetime.strptime(ra["expiry_date"], "%Y-%m-%d").date():
+ errors_lst.append(
+ {
+ "msg": _(
+ "{}: Acceptance has expired. Consider updating the status or the date"
+ ).format(ra["name"]),
+ "obj_type": "riskacceptance",
+ "object": ra,
+ }
+ )
+ findings = {
+ "errors": errors_lst,
+ "warnings": warnings_lst,
+ "info": info_lst,
+ "count": len(errors_lst + warnings_lst + info_lst),
+ }
+ return findings
-class Policy(SecurityMeasure):
- class Meta:
- proxy = True
- verbose_name = _("Policy")
- verbose_name_plural = _("Policies")
+ # NOTE: if your save() method throws an exception, you might want to override the clean() method to prevent
+ # 500 errors when the form submitted. See https://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.clean
- objects = PolicyManager() # Use the custom manager
- def save(self, *args, **kwargs):
- self.category = "policy"
- super(Policy, self).save(*args, **kwargs)
+def risk_scoring(probability, impact, risk_matrix: RiskMatrix) -> int:
+ fields = json.loads(risk_matrix.json_definition)
+ risk_index = fields["grid"][probability][impact]
+ return risk_index
class RiskScenario(NameDescriptionMixin):
@@ -976,285 +1064,117 @@ class RiskScenario(NameDescriptionMixin):
),
)
- treatment = models.CharField(
- max_length=20,
- choices=TREATMENT_OPTIONS,
- default="open",
- verbose_name=_("Treatment status"),
- )
-
- strength_of_knowledge = models.CharField(
- max_length=20,
- choices=SOK_OPTIONS,
- default="--",
- verbose_name=_("Strength of Knowledge"),
- )
- justification = models.CharField(
- max_length=500, blank=True, null=True, verbose_name=_("Justification")
- )
-
- class Meta:
- verbose_name = _("Risk scenario")
- verbose_name_plural = _("Risk scenarios")
-
- # def get_rating_options(self, field: str) -> list[tuple]:
- # risk_matrix = self.risk_assessment.risk_matrix.parse_json()
- # return [(k, v) for k, v in risk_matrix.fields[field].items()]
-
- def parent_project(self):
- return self.risk_assessment.project
-
- parent_project.short_description = _("Project")
-
- def get_matrix(self):
- return self.risk_assessment.risk_matrix.parse_json()
-
- def get_current_risk(self):
- if self.current_level < 0:
- return {
- "abbreviation": "--",
- "name": "--",
- "description": "not rated",
- "hexcolor": "#A9A9A9",
- }
- risk_matrix = self.get_matrix()
- return risk_matrix["risk"][self.current_level]
-
- def get_current_impact(self):
- if self.current_impact < 0:
- return {"abbreviation": "--", "name": "--", "description": "not rated"}
- risk_matrix = self.get_matrix()
- return risk_matrix["impact"][self.current_impact]
-
- def get_current_proba(self):
- if self.current_proba < 0:
- return {"abbreviation": "--", "name": "--", "description": "not rated"}
- risk_matrix = self.get_matrix()
- return risk_matrix["probability"][self.current_proba]
-
- def get_residual_risk(self):
- if self.residual_level < 0:
- return {
- "abbreviation": "--",
- "name": "--",
- "description": "not rated",
- "hexcolor": "#A9A9A9",
- }
- risk_matrix = self.get_matrix()
- return risk_matrix["risk"][self.residual_level]
-
- def get_residual_impact(self):
- if self.residual_impact < 0:
- return {"abbreviation": "--", "name": "--", "description": "not rated"}
- risk_matrix = self.get_matrix()
- return risk_matrix["impact"][self.residual_impact]
-
- def get_residual_proba(self):
- if self.residual_proba < 0:
- return {"abbreviation": "--", "name": "--", "description": "not rated"}
- risk_matrix = self.get_matrix()
- return risk_matrix["probability"][self.residual_proba]
-
- def __str__(self):
- return (
- str(self.parent_project().folder)
- + _("/")
- + str(self.parent_project())
- + _(": ")
- + str(self.name)
- )
-
- @property
- def rid(self):
- return f"R.{self.scoped_id(scope=RiskScenario.objects.filter(risk_assessment=self.risk_assessment))}"
-
- def save(self, *args, **kwargs):
- if self.current_proba >= 0 and self.current_impact >= 0:
- self.current_level = risk_scoring(
- self.current_proba,
- self.current_impact,
- self.risk_assessment.risk_matrix,
- )
- else:
- self.current_level = -1
- if self.residual_proba >= 0 and self.residual_impact >= 0:
- self.residual_level = risk_scoring(
- self.residual_proba,
- self.residual_impact,
- self.risk_assessment.risk_matrix,
- )
- else:
- self.residual_level = -1
- super(RiskScenario, self).save(*args, **kwargs)
-
-
-class RiskAcceptance(NameDescriptionMixin, FolderMixin):
- ACCEPTANCE_STATE = [
- ("created", _("Created")),
- ("submitted", _("Submitted")),
- ("accepted", _("Accepted")),
- ("rejected", _("Rejected")),
- ("revoked", _("Revoked")),
- ]
-
- risk_scenarios = models.ManyToManyField(
- RiskScenario,
- verbose_name=_("Risk scenarios"),
- help_text=_(
- "Select the risk scenarios to be accepted, attention they must be part of the chosen domain"
- ),
- )
- approver = models.ForeignKey(
- User,
- max_length=200,
- help_text=_("Risk owner and approver identity"),
- verbose_name=_("Approver"),
- on_delete=models.SET_NULL,
- null=True,
- blank=True,
- )
- state = models.CharField(
- max_length=20,
- choices=ACCEPTANCE_STATE,
- default="created",
- verbose_name=_("State"),
- )
- expiry_date = models.DateField(
- help_text=_("Specify when the risk acceptance will no longer apply"),
- null=True,
- verbose_name=_("Expiry date"),
- )
- accepted_at = models.DateTimeField(
- blank=True, null=True, verbose_name=_("Acceptance date")
- )
- rejected_at = models.DateTimeField(
- blank=True, null=True, verbose_name=_("Rejection date")
- )
- revoked_at = models.DateTimeField(
- blank=True, null=True, verbose_name=_("Revocation date")
- )
- justification = models.CharField(
- max_length=500, blank=True, null=True, verbose_name=_("Justification")
- )
-
- fields_to_check = ["name"]
-
- class Meta:
- permissions = [
- ("approve_riskacceptance", "Can validate/rejected risk acceptances")
- ]
- verbose_name = _("Risk acceptance")
- verbose_name_plural = _("Risk acceptances")
-
- def __str__(self):
- if self.name:
- return self.name
- scenario_names: str = ", ".join(
- [str(scenario) for scenario in self.risk_scenarios.all()]
- )
- return f"{scenario_names}"
-
- @property
- def get_html_url(self):
- url = reverse("riskacceptance-detail", args=(self.id,))
- return format_html(
- ' [RA-exp] {}: {} ',
- url,
- self.folder.name,
- self.name,
- )
-
- def set_state(self, state):
- self.state = state
- if state == "accepted":
- self.accepted_at = datetime.now()
- if state == "rejected":
- self.rejected_at = datetime.now()
- elif state == "revoked":
- self.revoked_at = datetime.now()
- self.save()
-
+ treatment = models.CharField(
+ max_length=20,
+ choices=TREATMENT_OPTIONS,
+ default="open",
+ verbose_name=_("Treatment status"),
+ )
-class Framework(ReferentialObjectMixin):
- library = models.ForeignKey(
- Library,
- on_delete=models.CASCADE,
- null=True,
- blank=True,
- related_name="frameworks",
+ strength_of_knowledge = models.CharField(
+ max_length=20,
+ choices=SOK_OPTIONS,
+ default="--",
+ verbose_name=_("Strength of Knowledge"),
+ )
+ justification = models.CharField(
+ max_length=500, blank=True, null=True, verbose_name=_("Justification")
)
class Meta:
- verbose_name = _("Framework")
- verbose_name_plural = _("Frameworks")
+ verbose_name = _("Risk scenario")
+ verbose_name_plural = _("Risk scenarios")
- def get_next_order_id(self, obj_type: models.Model, _parent_urn: str = None) -> int:
- """
- Returns the next order id for a given object type
- """
- if _parent_urn:
- return (
- obj_type.objects.filter(framework=self, parent_urn=_parent_urn).count()
- + 1
- )
- else:
- return obj_type.objects.filter(framework=self).count() + 1
+ # def get_rating_options(self, field: str) -> list[tuple]:
+ # risk_matrix = self.risk_assessment.risk_matrix.parse_json()
+ # return [(k, v) for k, v in risk_matrix.fields[field].items()]
- def is_deletable(self) -> bool:
- """
- Returns True if the framework can be deleted
- """
- if self.compliance_assessment_set.count() > 0:
- return False
- return True
+ def parent_project(self):
+ return self.risk_assessment.project
+ parent_project.short_description = _("Project")
-class RequirementLevel(ReferentialObjectMixin):
- framework = models.ForeignKey(
- Framework,
- on_delete=models.CASCADE,
- null=True,
- blank=True,
- verbose_name=_("Framework"),
- )
- level = models.IntegerField(null=False, blank=False, verbose_name=_("Level"))
+ def get_matrix(self):
+ return self.risk_assessment.risk_matrix.parse_json()
- class Meta:
- verbose_name = _("Requirements level")
- verbose_name_plural = _("Requirements levels")
+ def get_current_risk(self):
+ if self.current_level < 0:
+ return {
+ "abbreviation": "--",
+ "name": "--",
+ "description": "not rated",
+ "hexcolor": "#A9A9A9",
+ }
+ risk_matrix = self.get_matrix()
+ return risk_matrix["risk"][self.current_level]
+ def get_current_impact(self):
+ if self.current_impact < 0:
+ return {"abbreviation": "--", "name": "--", "description": "not rated"}
+ risk_matrix = self.get_matrix()
+ return risk_matrix["impact"][self.current_impact]
-class RequirementNode(ReferentialObjectMixin):
- threats = models.ManyToManyField(
- "Threat",
- blank=True,
- verbose_name=_("Threats"),
- related_name="requirements",
- )
- security_functions = models.ManyToManyField(
- "SecurityFunction",
- blank=True,
- verbose_name=_("Security functions"),
- related_name="requirements",
- )
- framework = models.ForeignKey(
- Framework,
- on_delete=models.CASCADE,
- null=True,
- blank=True,
- verbose_name=_("Framework"),
- )
- parent_urn = models.CharField(
- max_length=100, null=True, blank=True, verbose_name=_("Parent URN")
- )
- order_id = models.IntegerField(null=True, verbose_name=_("Order ID"))
- level = models.IntegerField(null=True, verbose_name=_("Level"))
- maturity = models.IntegerField(null=True, verbose_name=_("Maturity"))
- assessable = models.BooleanField(null=False, verbose_name=_("Assessable"))
+ def get_current_proba(self):
+ if self.current_proba < 0:
+ return {"abbreviation": "--", "name": "--", "description": "not rated"}
+ risk_matrix = self.get_matrix()
+ return risk_matrix["probability"][self.current_proba]
- class Meta:
- verbose_name = _("RequirementNode")
- verbose_name_plural = _("RequirementNodes")
+ def get_residual_risk(self):
+ if self.residual_level < 0:
+ return {
+ "abbreviation": "--",
+ "name": "--",
+ "description": "not rated",
+ "hexcolor": "#A9A9A9",
+ }
+ risk_matrix = self.get_matrix()
+ return risk_matrix["risk"][self.residual_level]
+
+ def get_residual_impact(self):
+ if self.residual_impact < 0:
+ return {"abbreviation": "--", "name": "--", "description": "not rated"}
+ risk_matrix = self.get_matrix()
+ return risk_matrix["impact"][self.residual_impact]
+
+ def get_residual_proba(self):
+ if self.residual_proba < 0:
+ return {"abbreviation": "--", "name": "--", "description": "not rated"}
+ risk_matrix = self.get_matrix()
+ return risk_matrix["probability"][self.residual_proba]
+
+ def __str__(self):
+ return (
+ str(self.parent_project().folder)
+ + _("/")
+ + str(self.parent_project())
+ + _(": ")
+ + str(self.name)
+ )
+
+ @property
+ def rid(self):
+ """return associated risk assessment id"""
+ return f"R.{self.scoped_id(scope=RiskScenario.objects.filter(risk_assessment=self.risk_assessment))}"
+
+ def save(self, *args, **kwargs):
+ if self.current_proba >= 0 and self.current_impact >= 0:
+ self.current_level = risk_scoring(
+ self.current_proba,
+ self.current_impact,
+ self.risk_assessment.risk_matrix,
+ )
+ else:
+ self.current_level = -1
+ if self.residual_proba >= 0 and self.residual_impact >= 0:
+ self.residual_level = risk_scoring(
+ self.residual_proba,
+ self.residual_impact,
+ self.risk_assessment.risk_matrix,
+ )
+ else:
+ self.residual_level = -1
+ super(RiskScenario, self).save(*args, **kwargs)
class ComplianceAssessment(Assessment):
@@ -1485,3 +1405,94 @@ def __str__(self) -> str:
class Meta:
verbose_name = _("Requirement assessment")
verbose_name_plural = _("Requirement assessments")
+
+
+########################### RiskAcesptance is a domain object relying on secondary objects #########################
+
+
+class RiskAcceptance(NameDescriptionMixin, FolderMixin):
+ ACCEPTANCE_STATE = [
+ ("created", _("Created")),
+ ("submitted", _("Submitted")),
+ ("accepted", _("Accepted")),
+ ("rejected", _("Rejected")),
+ ("revoked", _("Revoked")),
+ ]
+
+ risk_scenarios = models.ManyToManyField(
+ RiskScenario,
+ verbose_name=_("Risk scenarios"),
+ help_text=_(
+ "Select the risk scenarios to be accepted, attention they must be part of the chosen domain"
+ ),
+ )
+ approver = models.ForeignKey(
+ User,
+ max_length=200,
+ help_text=_("Risk owner and approver identity"),
+ verbose_name=_("Approver"),
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ )
+ state = models.CharField(
+ max_length=20,
+ choices=ACCEPTANCE_STATE,
+ default="created",
+ verbose_name=_("State"),
+ )
+ expiry_date = models.DateField(
+ help_text=_("Specify when the risk acceptance will no longer apply"),
+ null=True,
+ verbose_name=_("Expiry date"),
+ )
+ accepted_at = models.DateTimeField(
+ blank=True, null=True, verbose_name=_("Acceptance date")
+ )
+ rejected_at = models.DateTimeField(
+ blank=True, null=True, verbose_name=_("Rejection date")
+ )
+ revoked_at = models.DateTimeField(
+ blank=True, null=True, verbose_name=_("Revocation date")
+ )
+ justification = models.CharField(
+ max_length=500, blank=True, null=True, verbose_name=_("Justification")
+ )
+
+ fields_to_check = ["name"]
+
+ class Meta:
+ permissions = [
+ ("approve_riskacceptance", "Can validate/rejected risk acceptances")
+ ]
+ verbose_name = _("Risk acceptance")
+ verbose_name_plural = _("Risk acceptances")
+
+ def __str__(self):
+ if self.name:
+ return self.name
+ scenario_names: str = ", ".join(
+ [str(scenario) for scenario in self.risk_scenarios.all()]
+ )
+ return f"{scenario_names}"
+
+ @property
+ def get_html_url(self):
+ url = reverse("riskacceptance-detail", args=(self.id,))
+ return format_html(
+ ' [RA-exp] {}: {} ',
+ url,
+ self.folder.name,
+ self.name,
+ )
+
+ def set_state(self, state):
+ self.state = state
+ if state == "accepted":
+ self.accepted_at = datetime.now()
+ if state == "rejected":
+ self.rejected_at = datetime.now()
+ elif state == "revoked":
+ self.revoked_at = datetime.now()
+ self.save()
+
From 345204e4e047a7ec237901cea06fda01a6189653 Mon Sep 17 00:00:00 2001
From: eric-intuitem <71850047+eric-intuitem@users.noreply.github.com>
Date: Sun, 25 Feb 2024 18:49:09 +0100
Subject: [PATCH 4/9] Update VERSION
---
backend/ciso_assistant/VERSION | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/ciso_assistant/VERSION b/backend/ciso_assistant/VERSION
index e3e180701e..7e310bae19 100644
--- a/backend/ciso_assistant/VERSION
+++ b/backend/ciso_assistant/VERSION
@@ -1 +1 @@
-0.9.8
+0.9.9
From 56696ff9e485eba17937cf1c667adaa61c9d1067 Mon Sep 17 00:00:00 2001
From: monsieurswag
Date: Wed, 21 Feb 2024 10:04:01 +0100
Subject: [PATCH 5/9] Make The website work offline by using font-awesome
locally
---
frontend/package-lock.json | 10 ++++++++++
frontend/package.json | 3 ++-
frontend/src/app.html | 8 +-------
3 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index c2d5fccdbd..8a0ef0351e 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -10,6 +10,7 @@
"hasInstallScript": true,
"dependencies": {
"@floating-ui/dom": "^1.5.1",
+ "@fortawesome/fontawesome-free": "^6.5.1",
"@inlang/paraglide-js-adapter-vite": "^1.2.14",
"dotenv": "^16.4.1",
"echarts": "^5.4.3",
@@ -803,6 +804,15 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@fortawesome/fontawesome-free": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.1.tgz",
+ "integrity": "sha512-CNy5vSwN3fsUStPRLX7fUYojyuzoEMSXPl7zSLJ8TgtRfjv24LOnOWKT2zYwaHZCJGkdyRnTmstR0P+Ah503Gw==",
+ "hasInstallScript": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 654f3a9bba..7879ba055a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -18,8 +18,8 @@
"postinstall": "paraglide-js compile --project ./project.inlang"
},
"devDependencies": {
- "@playwright/test": "^1.40.1",
"@inlang/paraglide-js": "1.2.5",
+ "@playwright/test": "^1.40.1",
"@skeletonlabs/skeleton": "^2.3.0",
"@skeletonlabs/tw-plugin": "^0.2.2",
"@sveltejs/adapter-auto": "^3.0.0",
@@ -61,6 +61,7 @@
"type": "module",
"dependencies": {
"@floating-ui/dom": "^1.5.1",
+ "@fortawesome/fontawesome-free": "^6.5.1",
"@inlang/paraglide-js-adapter-vite": "^1.2.14",
"dotenv": "^16.4.1",
"echarts": "^5.4.3",
diff --git a/frontend/src/app.html b/frontend/src/app.html
index ff6d832aaa..06cf9318ec 100644
--- a/frontend/src/app.html
+++ b/frontend/src/app.html
@@ -3,13 +3,7 @@
-
+
%sveltekit.head%