diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml index 750868c..792cbff 100644 --- a/.github/workflows/django.yml +++ b/.github/workflows/django.yml @@ -13,7 +13,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 0f0b311..f675f17 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} diff --git a/.gitignore b/.gitignore index e106314..9761704 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ __pycache__ db.sqlite3 media +attachments/ # Backup files # *.bak diff --git a/README.md b/README.md index c1fed4c..80e746e 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Oxomium is an opensource project build to help company to manage the cybersecurity compliance of organisations. -It provides help to CISO or other security people to follow conformity to a Policy. +It provides help to CISO or other security people to follow conformity to a Framework. More information on [Oxomium Website](https://www.oxomium.org). diff --git a/conformity/admin.py b/conformity/admin.py index 8196b68..329feee 100644 --- a/conformity/admin.py +++ b/conformity/admin.py @@ -5,7 +5,7 @@ from django.contrib import admin from import_export import resources from import_export.admin import ImportExportModelAdmin -from .models import Organization, Policy, Measure, Conformity, Audit, Finding, Action, Control, ControlPoint +from .models import Organization, Framework, Requirement, Conformity, Audit, Finding, Action, Control, ControlPoint, Attachment class OrganizationResources(resources.ModelResource): @@ -17,22 +17,22 @@ class OrganizationAdmin(ImportExportModelAdmin): ressource_class = Organization -class PolicyResources(resources.ModelResource): +class FrameworkResources(resources.ModelResource): class Meta: - model = Policy + model = Framework -class PolicyAdmin(ImportExportModelAdmin): - ressource_class = Policy +class FrameworkAdmin(ImportExportModelAdmin): + ressource_class = Framework -class MeasureResources(resources.ModelResource): +class RequirementResources(resources.ModelResource): class Meta: - model = Measure + model = Requirement -class MeasureAdmin(ImportExportModelAdmin): - ressource_class = Measure +class RequirementAdmin(ImportExportModelAdmin): + ressource_class = Requirement class ConformityResources(resources.ModelResource): @@ -55,8 +55,8 @@ class ActionAdmin(ImportExportModelAdmin): # Registration -admin.site.register(Policy, PolicyAdmin) -admin.site.register(Measure, MeasureAdmin) +admin.site.register(Framework, FrameworkAdmin) +admin.site.register(Requirement, RequirementAdmin) admin.site.register(Conformity, ConformityAdmin) admin.site.register(Action, ActionAdmin) admin.site.register(Audit) @@ -64,3 +64,4 @@ class ActionAdmin(ImportExportModelAdmin): admin.site.register(Organization, OrganizationAdmin) admin.site.register(Control) admin.site.register(ControlPoint) +admin.site.register(Attachment) diff --git a/conformity/filterset.py b/conformity/filterset.py index e7d6673..35834f7 100644 --- a/conformity/filterset.py +++ b/conformity/filterset.py @@ -1,5 +1,5 @@ from django_filters import FilterSet, CharFilter -from .models import Action, ControlPoint +from .models import Action, Control, ControlPoint, Attachment class ActionFilter(FilterSet): @@ -10,6 +10,13 @@ class Meta: class ControlFilter(FilterSet): + class Meta: + model = Control + fields = ['level', 'organization', 'conformity__id', 'frequency'] + + +class ControlPointFilter(FilterSet): class Meta: model = ControlPoint - fields = ['control__level', 'control__organization', 'control__conformity__id', 'control__control__id', 'control__frequency', 'control__level'] + fields = [ 'control__id', 'control__frequency', 'status' ] + diff --git a/conformity/forms.py b/conformity/forms.py index 837e5df..fff6681 100644 --- a/conformity/forms.py +++ b/conformity/forms.py @@ -2,7 +2,7 @@ Forms for front-end editing of Models instance """ -from django.forms import ModelForm +from django.forms import ModelForm, FileField, ClearableFileInput from django.contrib.auth.mixins import LoginRequiredMixin from django.http import request from django.utils import timezone @@ -22,15 +22,18 @@ def __init__(self, *args, **kwargs): class OrganizationForm(LoginRequiredMixin, ModelForm): + attachments = FileField(required=False, widget=ClearableFileInput()) class Meta: model = Organization - fields = '__all__' + fields = ['name', 'administrative_id', 'description', 'applicable_frameworks'] class AuditForm(LoginRequiredMixin, ModelForm): + attachments = FileField(required=False, widget=ClearableFileInput()) class Meta: model = Audit fields = '__all__' + exclude = ['attachment'] class FindingForm(LoginRequiredMixin, ModelForm): @@ -77,15 +80,29 @@ class Meta: class ControlPointForm(LoginRequiredMixin, ModelForm): + attachments = FileField(required=False, widget=ClearableFileInput()) class Meta: model = ControlPoint - fields = ['control_date', 'control_user', 'status', 'comment'] + fields = ['control_date', 'control_user', 'status', 'comment', 'attachments'] def __init__(self, *args, **kwargs): + self.user = kwargs.pop('user', None) super(ControlPointForm, self).__init__(*args, **kwargs) - self.initial['control_date'] = timezone.now() - self.fields['control_date'].disabled = True - if self.get_initial_for_field(self.fields['status'], 'status') != ControlPoint.Status.TOBEEVALUATED.value: + # Set some value for all situation + self.fields['control_date'].disabled = True + self.fields['control_user'].disabled = True + + # Set some value if the ControlPoint has to be evaluated + if self.get_initial_for_field(self.fields['status'], 'status') == ControlPoint.Status.TOBEEVALUATED.value: + self.initial['control_date'] = timezone.now() + self.initial['control_user'] = self.user + self.fields['status'].widget.choices = [ + (ControlPoint.Status.COMPLIANT, ControlPoint.Status.COMPLIANT.label), + (ControlPoint.Status.NONCOMPLIANT, ControlPoint.Status.NONCOMPLIANT.label), + ] + # Switch to display mode if ControlPoint is not to be evaluated + else: + del self.fields['attachments'] for field in self.fields: self.fields[field].disabled = True diff --git a/conformity/middleware.py b/conformity/middleware.py new file mode 100644 index 0000000..17fd755 --- /dev/null +++ b/conformity/middleware.py @@ -0,0 +1,51 @@ +from django.contrib.auth.signals import user_logged_in +from django.dispatch import receiver +from datetime import datetime +from dateutil.relativedelta import relativedelta +from .models import ControlPoint + + +class SanityCheckMiddleware: + last_checked = datetime.today() # Class variable to store the last check date + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + self.run_daily_checks() + return response + + def run_daily_checks(self): + """Performs the daily integrity checks.""" + today = datetime.today().date() + + if SanityCheckMiddleware.last_checked != today: + self.check_control_points(today) + SanityCheckMiddleware.last_checked = today + + @staticmethod + def check_control_points(today): + """Checks and updates the status of ControlPoint.""" + yesterday=today - relativedelta(days=1) + + """Update SCHD to TOBE when period start""" + s=today.replace(day=1) + e=today.replace(day=1) + relativedelta(months=1) + scheduled_controls = ControlPoint.objects.filter(period_start_date__lte=today, + period_end_date__gte=today, + status="SCHD") + scheduled_controls.update(status='TOBE') + + """Update expired TOBE to MISS """ + missed_controls = ControlPoint.objects.filter(period_start_date__lt=today, + period_end_date__lt=today, + status="TOBE") + missed_controls.update(status='MISS') + + +# Connect the user login signal +@receiver(user_logged_in) +def update_on_login(sender, user, request, **kwargs): + middleware = SanityCheckMiddleware(None) + middleware.run_daily_checks() \ No newline at end of file diff --git a/conformity/migrations/0001_squashed_0014_mesure_is_parent.py b/conformity/migrations/0001_squashed_0014_mesure_is_parent.py index f7c8759..ea1c41b 100644 --- a/conformity/migrations/0001_squashed_0014_mesure_is_parent.py +++ b/conformity/migrations/0001_squashed_0014_mesure_is_parent.py @@ -47,7 +47,7 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=256)), ('administrative_id', models.CharField(blank=True, max_length=256)), ('description', models.CharField(blank=True, max_length=256)), - ('applicable_policies', models.ManyToManyField(blank=True, to='conformity.policy')), + ('applicable_frameworks', models.ManyToManyField(blank=True, to='conformity.policy')), ], ), migrations.CreateModel( diff --git a/conformity/migrations/0001_squashed_0029_alter_action_control_comment_and_more.py b/conformity/migrations/0001_squashed_0029_alter_action_control_comment_and_more.py index 670c280..efe0eed 100644 --- a/conformity/migrations/0001_squashed_0029_alter_action_control_comment_and_more.py +++ b/conformity/migrations/0001_squashed_0029_alter_action_control_comment_and_more.py @@ -40,7 +40,7 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=256, unique=True)), ('administrative_id', models.CharField(blank=True, max_length=256)), ('description', models.TextField(blank=True, max_length=4096)), - ('applicable_policies', models.ManyToManyField(blank=True, to='conformity.policy')), + ('applicable_frameworks', models.ManyToManyField(blank=True, to='conformity.policy')), ], options={ 'ordering': ['name'], diff --git a/conformity/migrations/0041_rename_policy_framework_alter_framework_options_and_more.py b/conformity/migrations/0041_rename_policy_framework_alter_framework_options_and_more.py new file mode 100644 index 0000000..815c005 --- /dev/null +++ b/conformity/migrations/0041_rename_policy_framework_alter_framework_options_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 4.2.9 on 2024-08-25 02:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('conformity', '0040_alter_control_level'), + ] + + operations = [ + migrations.RenameModel( + old_name='Policy', + new_name='Framework', + ), + migrations.AlterModelOptions( + name='framework', + options={'ordering': ['name'], 'verbose_name': 'Framework', 'verbose_name_plural': 'Frameworks'}, + ), + migrations.RenameField( + model_name='measure', + old_name='policy', + new_name='framework', + ), + migrations.RenameField( + model_name='audit', + old_name='audited_policies', + new_name='audited_frameworks', + ), + ] diff --git a/conformity/migrations/0042_rename_measure_requirement_alter_conformity_options_and_more.py b/conformity/migrations/0042_rename_measure_requirement_alter_conformity_options_and_more.py new file mode 100644 index 0000000..072e587 --- /dev/null +++ b/conformity/migrations/0042_rename_measure_requirement_alter_conformity_options_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.9 on 2024-08-25 04:38 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('conformity', '0041_rename_policy_framework_alter_framework_options_and_more'), + ] + + operations = [ + migrations.RenameModel( + old_name='Measure', + new_name='Requirement', + ), + migrations.AlterModelOptions( + name='conformity', + options={'ordering': ['organization', 'requirement'], 'verbose_name': 'Conformity', 'verbose_name_plural': 'Conformities'}, + ), + migrations.RenameField( + model_name='conformity', + old_name='measure', + new_name='requirement', + ), + migrations.AlterUniqueTogether( + name='conformity', + unique_together={('organization', 'requirement')}, + ), + ] diff --git a/conformity/migrations/0043_attachment.py b/conformity/migrations/0043_attachment.py new file mode 100644 index 0000000..de2f86d --- /dev/null +++ b/conformity/migrations/0043_attachment.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.9 on 2024-10-12 02:57 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('conformity', '0042_rename_measure_requirement_alter_conformity_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Attachment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to='attachments/')), + ('comment', models.TextField(blank=True, max_length=4096)), + ('mime_type', models.CharField(blank=True, max_length=255)), + ('create_date', models.DateField(default=django.utils.timezone.now)), + ], + ), + ] diff --git a/conformity/migrations/0044_action_attachment_audit_attachment_and_more.py b/conformity/migrations/0044_action_attachment_audit_attachment_and_more.py new file mode 100644 index 0000000..7049b28 --- /dev/null +++ b/conformity/migrations/0044_action_attachment_audit_attachment_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.2.9 on 2024-10-12 03:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('conformity', '0043_attachment'), + ] + + operations = [ + migrations.AddField( + model_name='action', + name='attachment', + field=models.ManyToManyField(blank=True, related_name='actions', to='conformity.attachment'), + ), + migrations.AddField( + model_name='audit', + name='attachment', + field=models.ManyToManyField(blank=True, related_name='audits', to='conformity.attachment'), + ), + migrations.AddField( + model_name='controlpoint', + name='attachment', + field=models.ManyToManyField(blank=True, related_name='ControlPoint', to='conformity.attachment'), + ), + migrations.AddField( + model_name='finding', + name='attachment', + field=models.ManyToManyField(blank=True, related_name='findings', to='conformity.attachment'), + ), + migrations.AddField( + model_name='framework', + name='attachment', + field=models.ManyToManyField(blank=True, related_name='frameworks', to='conformity.attachment'), + ), + migrations.AddField( + model_name='organization', + name='attachment', + field=models.ManyToManyField(blank=True, related_name='organizations', to='conformity.attachment'), + ), + ] diff --git a/conformity/migrations/0045_alter_attachment_create_date_and_more.py b/conformity/migrations/0045_alter_attachment_create_date_and_more.py new file mode 100644 index 0000000..877532b --- /dev/null +++ b/conformity/migrations/0045_alter_attachment_create_date_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.9 on 2024-10-12 04:28 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('conformity', '0044_action_attachment_audit_attachment_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='attachment', + name='create_date', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='attachment', + name='mime_type', + field=models.CharField(max_length=255), + ), + ] diff --git a/conformity/migrations/0046_alter_attachment_mime_type.py b/conformity/migrations/0046_alter_attachment_mime_type.py new file mode 100644 index 0000000..85d534c --- /dev/null +++ b/conformity/migrations/0046_alter_attachment_mime_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.9 on 2024-10-12 04:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('conformity', '0045_alter_attachment_create_date_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='attachment', + name='mime_type', + field=models.CharField(blank=True, max_length=255), + ), + ] diff --git a/conformity/migrations/0047_alter_attachment_create_date.py b/conformity/migrations/0047_alter_attachment_create_date.py new file mode 100644 index 0000000..bc34acf --- /dev/null +++ b/conformity/migrations/0047_alter_attachment_create_date.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.9 on 2024-10-12 04:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('conformity', '0046_alter_attachment_mime_type'), + ] + + operations = [ + migrations.AlterField( + model_name='attachment', + name='create_date', + field=models.DateTimeField(auto_now_add=True), + ), + ] diff --git a/conformity/migrations/0048_remove_finding_attachment.py b/conformity/migrations/0048_remove_finding_attachment.py new file mode 100644 index 0000000..6b230f5 --- /dev/null +++ b/conformity/migrations/0048_remove_finding_attachment.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.9 on 2024-10-13 01:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('conformity', '0047_alter_attachment_create_date'), + ] + + operations = [ + migrations.RemoveField( + model_name='finding', + name='attachment', + ), + ] diff --git a/conformity/migrations/0049_remove_action_attachment.py b/conformity/migrations/0049_remove_action_attachment.py new file mode 100644 index 0000000..3f72411 --- /dev/null +++ b/conformity/migrations/0049_remove_action_attachment.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.9 on 2024-10-13 01:16 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('conformity', '0048_remove_finding_attachment'), + ] + + operations = [ + migrations.RemoveField( + model_name='action', + name='attachment', + ), + ] diff --git a/conformity/models.py b/conformity/models.py index 5db9825..8adca41 100644 --- a/conformity/models.py +++ b/conformity/models.py @@ -1,11 +1,12 @@ """ Conformity module manage all the manual declarative aspect of conformity management. -It's Organized around Organization, Policy, Measure and Conformity classes. +It's Organized around Organization, Framework, Requirement and Conformity classes. """ from calendar import monthrange from statistics import mean from datetime import date, timedelta from django.db import models +from django.db.models import Q from django.db.models.signals import m2m_changed, pre_save, post_save, post_init from django.core.validators import MaxValueValidator, MinValueValidator from django.dispatch import receiver @@ -15,23 +16,24 @@ from django.utils.translation import gettext_lazy as _ from django.urls import reverse from auditlog.context import set_actor +import magic User = get_user_model() -class PolicyManager(models.Manager): +class FrameworkManager(models.Manager): def get_by_natural_key(self, name): return self.get(name=name) -class Policy(models.Model): +class Framework(models.Model): """ - Policy class represent the conformity policy you will apply on Organization. - A Policy is simply a collections of Measure with publication parameter. + Framework class represent the conformity framework you will apply on Organization. + A Framework is simply a collections of Requirement with publication parameter. """ class Type(models.TextChoices): - """ List of the Type of policy """ + """ List of the Type of framework """ INTERNATIONAL = 'INT', _('International Standard') NATIONAL = 'NAT', _('National Standard') TECHNICAL = 'TECH', _('Technical Standard') @@ -39,7 +41,7 @@ class Type(models.TextChoices): POLICY = 'POL', _('Internal Policy') OTHER = 'OTHER', _('Other') - objects = PolicyManager() + objects = FrameworkManager() name = models.CharField(max_length=256, unique=True) version = models.IntegerField(default=0) publish_by = models.CharField(max_length=256) @@ -48,48 +50,50 @@ class Type(models.TextChoices): choices=Type.choices, default=Type.OTHER, ) + attachment = models.ManyToManyField('Attachment', blank=True, related_name='frameworks') class Meta: ordering = ['name'] - verbose_name = 'Policy' - verbose_name_plural = 'Policies' + verbose_name = 'Framework' + verbose_name_plural = 'Frameworks' def __str__(self): return str(self.name) def natural_key(self): - return self.name + return (self.name) def get_type(self): - """return the readable version of the Policy Type""" + """return the readable version of the Framework Type""" return self.Type(self.type).label - def get_measures(self): - """return all Measure related to the Policy""" - return Measure.objects.filter(policy=self.id) + def get_requirements(self): + """return all Requirement related to the Framework""" + return Requirement.objects.filter(framework=self.id) - def get_measures_number(self): - """return the number of leaf Measure related to the Policy""" - return Measure.objects.filter(policy=self.id).filter(measure__is_parent=False).count() + def get_requirements_number(self): + """return the number of leaf Requirement related to the Framework""" + return Requirement.objects.filter(framework=self.id).filter(requirement__is_parent=False).count() - def get_root_measure(self): - """return the root Measure of the Policy""" - return Measure.objects.filter(policy=self.id).filter(level=0).order_by('order') + def get_root_requirement(self): + """return the root Requirement of the Framework""" + return Requirement.objects.filter(framework=self.id).filter(level=0).order_by('order') - def get_first_measures(self): - """return the Measure of the first hierarchical level of the Policy""" - return Measure.objects.filter(policy=self.id).filter(level=1).order_by('order') + def get_first_requirements(self): + """return the Requirement of the first hierarchical level of the Framework""" + return Requirement.objects.filter(framework=self.id).filter(level=1).order_by('order') class Organization(models.Model): """ - Organization class is a representation of a company, a division of company, a administration... - The Organization may answer to one or several Policy. + Organization class is a representation of a company, a division of company, an administration... + The Organization may answer to one or several Framework. """ name = models.CharField(max_length=256, unique=True) administrative_id = models.CharField(max_length=256, blank=True) description = models.TextField(max_length=4096, blank=True) - applicable_policies = models.ManyToManyField(Policy, blank=True) + applicable_frameworks = models.ManyToManyField(Framework, blank=True) + attachment = models.ManyToManyField('Attachment', blank=True, related_name='organizations') class Meta: ordering = ['name'] @@ -98,50 +102,50 @@ def __str__(self): return str(self.name) def natural_key(self): - return self.name + return (self.name) @staticmethod def get_absolute_url(): """return the absolute URL for Forms, could probably do better""" return reverse('conformity:organization_index') - def get_policies(self): - """return all Policy applicable to the Organization""" - return self.applicable_policies.all() + def get_frameworks(self): + """return all Framework applicable to the Organization""" + return self.applicable_frameworks.all() def remove_conformity(self, pid): """Cascade deletion of conformity""" with set_actor('system'): - measure_set = Measure.objects.filter(policy=pid) - for measure in measure_set: - Conformity.objects.filter(measure=measure.id).filter(organization=self.id).delete() + requirement_set = Requirement.objects.filter(framework=pid) + for requirement in requirement_set: + Conformity.objects.filter(requirement=requirement.id).filter(organization=self.id).delete() def add_conformity(self, pid): """Automatic creation of conformity""" with set_actor('system'): - measure_set = Measure.objects.filter(policy=pid) - for measure in measure_set: - conformity = Conformity(organization=self, measure=measure) + requirement_set = Requirement.objects.filter(framework=pid) + for requirement in requirement_set: + conformity = Conformity(organization=self, requirement=requirement) conformity.save() -class MeasureManager(models.Manager): +class RequirementManager(models.Manager): def get_by_natural_key(self, name): return self.get(name=name) -class Measure(models.Model): +class Requirement(models.Model): """ - A Measure is a precise requirement. - Measure can be hierarchical in order to form a collection of Measure, aka Policy. - A Measure is not representing the conformity level, see Conformity class. + A Requirement is a precise requirement. + Requirement can be hierarchical in order to form a collection of Requirement, aka Framework. + A Requirement is not representing the conformity level, see Conformity class. """ - objects = MeasureManager() + objects = RequirementManager() code = models.CharField(max_length=5, blank=True) name = models.CharField(max_length=50, blank=True, unique=True) level = models.IntegerField(default=0) order = models.IntegerField(default=1) - policy = models.ForeignKey(Policy, on_delete=models.CASCADE) + framework = models.ForeignKey(Framework, on_delete=models.CASCADE) parent = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True) title = models.CharField(max_length=256, blank=True) description = models.TextField(blank=True) @@ -151,57 +155,57 @@ class Meta: ordering = ['name'] def __str__(self): - return str(self.name + ": " + self.title) + return str(self.name) + ": " + str(self.title) def natural_key(self): - return self.name + return (self.name) - natural_key.dependencies = ['conformity.policy'] + natural_key.dependencies = ['conformity.framework'] def get_children(self): - """Return all children of the measure""" - return Measure.objects.filter(parent=self.id).order_by('order') + """Return all children of the requirement""" + return Requirement.objects.filter(parent=self.id).order_by('order') class Conformity(models.Model): """ - Conformity represent the conformity of an Organization to a Measure. - Value are automatically update for parent measure conformity + Conformity represent the conformity of an Organization to a Requirement. + Value are automatically update for parent requirement conformity """ organization = models.ForeignKey(Organization, on_delete=models.CASCADE, null=True) - measure = models.ForeignKey(Measure, on_delete=models.CASCADE, null=True) + requirement = models.ForeignKey(Requirement, on_delete=models.CASCADE, null=True) applicable = models.BooleanField(default=True) status = models.IntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(100)], null=True, blank=True) responsible = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True) comment = models.TextField(max_length=4096, blank=True) class Meta: - ordering = ['organization', 'measure'] + ordering = ['organization', 'requirement'] verbose_name = 'Conformity' verbose_name_plural = 'Conformities' - unique_together = (('organization', 'measure'),) + unique_together = (('organization', 'requirement'),) def __str__(self): - return "[" + str(self.organization) + "] " + str(self.measure) + return "[" + str(self.organization) + "] " + str(self.requirement) def natural_key(self): - return self.organization, self.measure + return self.organization, self.requirement - natural_key.dependencies = ['conformity.policy', 'conformity.measure', 'conformity.organization'] + natural_key.dependencies = ['conformity.framework', 'conformity.requirement', 'conformity.organization'] def get_absolute_url(self): """Return the absolute URL of the class for Form, probably not the best way to do it""" - return reverse('conformity:conformity_orgpol_index', - kwargs={'org': self.organization.id, 'pol': self.measure.policy.id}) + return reverse('conformity:conformity_detail_index', + kwargs={'org': self.organization.id, 'pol': self.requirement.framework.id}) def get_children(self): - """Return all children Conformity based on Measure hierarchy""" + """Return all children Conformity based on Requirement hierarchy""" return Conformity.objects.filter(organization=self.organization) \ - .filter(measure__parent=self.measure.id).order_by('measure__order') + .filter(requirement__parent=self.requirement.id).order_by('requirement__order') def get_parent(self): - """Return the parent Conformity based on Measure hierarchy""" - p = Conformity.objects.filter(organization=self.organization).filter(measure=self.measure.parent) + """Return the parent Conformity based on Requirement hierarchy""" + p = Conformity.objects.filter(organization=self.organization).filter(requirement=self.requirement.parent) if len(p) == 1: return p[0] else: @@ -247,10 +251,9 @@ def update(self): # Callback functions - -@receiver(pre_save, sender=Measure) +@receiver(pre_save, sender=Requirement) def post_init_callback(instance, **kwargs): - """This function keep hierarchy of the Measure working on each Measure instantiation""" + """This function keep hierarchy of the Requirement working on each Requirement instantiation""" if instance.parent: instance.name = instance.parent.name + "-" + instance.code instance.level = instance.parent.level + 1 @@ -259,8 +262,8 @@ def post_init_callback(instance, **kwargs): instance.name = instance.code -@receiver(m2m_changed, sender=Organization.applicable_policies.through) -def change_policy(instance, action, pk_set, *args, **kwargs): +@receiver(m2m_changed, sender=Organization.applicable_frameworks.through) +def change_framework(instance, action, pk_set, *args, **kwargs): if action == "post_add": for pk in pk_set: instance.add_conformity(pk) @@ -288,7 +291,7 @@ class Type(models.TextChoices): description = models.TextField(max_length=4096, blank=True) conclusion = models.TextField(max_length=4096, blank=True) auditor = models.CharField(max_length=256) - audited_policies = models.ManyToManyField(Policy, blank=True) + audited_frameworks = models.ManyToManyField(Framework, blank=True) start_date = models.DateField(null=True, blank=True) end_date = models.DateField(null=True, blank=True) report_date = models.DateField(null=True, blank=True) @@ -297,30 +300,32 @@ class Type(models.TextChoices): choices=Type.choices, default=Type.OTHER, ) + attachment = models.ManyToManyField('Attachment', blank=True, related_name='audits') class Meta: ordering = ['report_date'] def __str__(self): + if self.report_date: - date = self.report_date.strftime('%b %Y') + display_date = self.report_date.strftime('%b %Y') elif self.start_date: - date = self.start_date.strftime('%b %Y') + display_date = self.start_date.strftime('%b %Y') elif self.end_date: - date = self.end_date.strftime('%b %Y') + display_date = self.end_date.strftime('%b %Y') else: - date = "xx-xxxx" + display_date = "xx-xxxx" - return "[" + str(self.organization) + "] " + str(self.auditor) + " (" + date + ")" + return "[" + str(self.organization) + "] " + str(self.auditor) + " (" + display_date + ")" @staticmethod def get_absolute_url(): """return the absolute URL for Forms, could probably do better""" return reverse('conformity:audit_index') - def get_policies(self): - """return all Policy within the Audit scope""" - return self.audited_policies.all() + def get_frameworks(self): + """return all Framework within the Audit scope""" + return self.audited_frameworks.all() def get_type(self): """return the readable version of the Audit Type""" @@ -391,7 +396,7 @@ def get_severity(self): return self.Severity(self.severity).label def get_absolute_url(self): - """"return somewhere else when a edit has work """ + """"return somewhere else when an edit has work """ return reverse('conformity:audit_detail', kwargs={'pk': self.audit_id}) def get_action(self): @@ -401,7 +406,7 @@ def get_action(self): class Control(models.Model): """ - Control class represent the periodic control needed to verify the security and the effectiveness of the security measure. + Control class represent the periodic control needed to verify the security and the effectiveness of the security requirement. """ class Frequency(models.IntegerChoices): @@ -432,7 +437,7 @@ class Level(models.IntegerChoices): ) def __str__(self): - return "[" + str(self.organization) + "] " + self.title + return "[" + str(self.organization) + "] " + str(self.title) @staticmethod def get_absolute_url(): @@ -441,6 +446,8 @@ def get_absolute_url(): @staticmethod def post_init_callback(instance, **kwargs): + ControlPoint.objects.filter(control=instance.id).filter(Q(status='SCHD') | Q(status='TOBE')).delete() + num_cp = instance.frequency today = date.today() start_date = date(today.year, 1, 1) @@ -449,15 +456,21 @@ def post_init_callback(instance, **kwargs): for _ in range(num_cp): period_start_date = date(start_date.year, start_date.month, 1) period_end_date = date(end_date.year, end_date.month, monthrange(end_date.year, end_date.month)[1]) - ControlPoint.objects.create( - control=instance, - period_start_date=period_start_date, - period_end_date=period_end_date, - ) + if not ControlPoint.objects.filter(control=instance.id).filter(period_start_date=period_start_date).filter(period_end_date=period_end_date) : + ControlPoint.objects.create( + control=instance, + period_start_date=period_start_date, + period_end_date=period_end_date, + ) start_date = period_end_date + timedelta(days=1) end_date = start_date + delta - timedelta(days=1) + def get_controlpoint(self): + """Return all control point based on this control""" + return ControlPoint.objects.filter(control=self.id).order_by('period_start_date') + + class ControlPoint(models.Model): """ A control point is a specific point of verification of a periodic Control. @@ -478,6 +491,7 @@ class Status(models.TextChoices): period_end_date = models.DateField() status = models.CharField(choices=Status.choices, max_length=4, default=Status.SCHEDULED) comment = models.TextField(max_length=4096, blank=True) + attachment = models.ManyToManyField('Attachment', blank=True, related_name='ControlPoint') @staticmethod def get_absolute_url(): @@ -486,13 +500,14 @@ def get_absolute_url(): @staticmethod def pre_save(sender, instance, *args, **kwargs): - today = date.today() - if instance.period_end_date < today: - instance.status = ControlPoint.Status.MISSED - elif instance.period_start_date <= today <= instance.period_end_date: - instance.status = ControlPoint.Status.TOBEEVALUATED - else: - instance.status = ControlPoint.Status.SCHEDULED + if instance.status != ControlPoint.Status.COMPLIANT and instance.status != ControlPoint.Status.NONCOMPLIANT: + today = date.today() + if instance.period_end_date < today: + instance.status = ControlPoint.Status.MISSED + elif instance.period_start_date <= today <= instance.period_end_date: + instance.status = ControlPoint.Status.TOBEEVALUATED + else: + instance.status = ControlPoint.Status.SCHEDULED def __str__(self): @@ -580,6 +595,25 @@ def save(self, *args, **kwargs): return super(Action, self).save(*args, **kwargs) +class Attachment(models.Model): + file = models.FileField(upload_to='attachments/') + comment = models.TextField(max_length=4096, blank=True) + mime_type = models.CharField(max_length=255, blank=True) + create_date = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.file.name.split("/")[1] + + @staticmethod + def pre_save(sender, instance, *args, **kwargs): + # Read file and set mime_type + file_content = instance.file.read() + instance.file.seek(0) + mime = magic.Magic(mime=True) + instance.mime_type = mime.from_buffer(file_content) + + # TODO filter on mime type + # # Signal # @@ -587,3 +621,4 @@ def save(self, *args, **kwargs): post_save.connect(Control.post_init_callback, sender=Control) pre_save.connect(ControlPoint.pre_save, sender=ControlPoint) +pre_save.connect(Attachment.pre_save, sender=Attachment) diff --git a/conformity/templates/conformity/action_form.html b/conformity/templates/conformity/action_form.html index b239670..4af7da1 100644 --- a/conformity/templates/conformity/action_form.html +++ b/conformity/templates/conformity/action_form.html @@ -2,7 +2,7 @@ {% load django_bootstrap5 %} {% block header %} -

Edit of an action record

+

Edit of an action record

{% endblock %} {% block content %} diff --git a/conformity/templates/conformity/attachment_list.html b/conformity/templates/conformity/attachment_list.html new file mode 100644 index 0000000..516f03f --- /dev/null +++ b/conformity/templates/conformity/attachment_list.html @@ -0,0 +1,48 @@ +{% extends "conformity/main.html" %} + +{% block header %} +

Attachment library

+{% endblock %} + +{% block content %} + + + + + + + + + + + {% for attachment in attachment_list %} + + + + + + {% empty %} + + {% endfor %} + +
Attachment library
AttachmentCreation dateReferences
+ {{ attachment | truncatechars:40 }} + + {{ attachment.create_date }} + + {% for org in attachment.organizations.all %} + {{ org }} + {% endfor %} + {% for framework in attachment.frameworks.all %} + {{ framework }} + {% endfor %} + {% for cp in attachment.ControlPoint.all %} + {{ cp }} + {% endfor %} + {% for audit in attachment.audits.all %} + {{ audit }} + {% endfor %} +
+{% endblock %} diff --git a/conformity/templates/conformity/audit_detail.html b/conformity/templates/conformity/audit_detail.html index 5d1bd8c..95b3faa 100644 --- a/conformity/templates/conformity/audit_detail.html +++ b/conformity/templates/conformity/audit_detail.html @@ -7,12 +7,12 @@

Audit of {{ audit.organization }} by {{ aud {% block content %}

Audit information

{{ audit.get_type }} realized by {{ audit.auditor }} from {{ audit.start_date }} to {{ audit.end_date }}.

-

The following policies were within the audit scope :

+

The following frameworks were within the audit scope :

@@ -118,6 +118,19 @@

Audit findings


+
+

Attachments

+ +
+ +
+ diff --git a/conformity/templates/conformity/audit_form.html b/conformity/templates/conformity/audit_form.html index 0db4ead..a6d82fd 100644 --- a/conformity/templates/conformity/audit_form.html +++ b/conformity/templates/conformity/audit_form.html @@ -2,11 +2,11 @@ {% load django_bootstrap5 %} {% block header %} -

Edit of an audit record

+

Edit of an audit record

{% endblock %} {% block content %} -
+ {% csrf_token %}
diff --git a/conformity/templates/conformity/conformity_orgpol_list.html b/conformity/templates/conformity/conformity_detail_list.html similarity index 94% rename from conformity/templates/conformity/conformity_orgpol_list.html rename to conformity/templates/conformity/conformity_detail_list.html index 9fbb378..c8e7e76 100644 --- a/conformity/templates/conformity/conformity_orgpol_list.html +++ b/conformity/templates/conformity/conformity_detail_list.html @@ -2,21 +2,21 @@ {% block header %}

- {{ conformity_list.0.organization }} conformity to {{ conformity_list.0.measure.policy }}: + {{ conformity_list.0.organization }} conformity to {{ conformity_list.0.requirement.framework }}: Conformity: {{ conformity_list.0.status }} %

{% endblock %} {% block content %} - + - + - - + + @@ -24,7 +24,7 @@

{% for con in conformity_list.0.get_children %} - + {% endfor %} diff --git a/conformity/templates/conformity/conformity_form.html b/conformity/templates/conformity/conformity_form.html index 8eea14f..9dc2ba0 100644 --- a/conformity/templates/conformity/conformity_form.html +++ b/conformity/templates/conformity/conformity_form.html @@ -2,7 +2,7 @@ {% load django_bootstrap5 %} {% block header %} -

Edit of a conformity item

+

Edit of a conformity item

{% endblock %} {% block content %} @@ -13,11 +13,9 @@

Edit of
- {% if conformity.applicable %} - - {% else %} - - {% endif %} +
diff --git a/conformity/templates/conformity/conformity_list.html b/conformity/templates/conformity/conformity_list.html index e6c8962..ad07372 100644 --- a/conformity/templates/conformity/conformity_list.html +++ b/conformity/templates/conformity/conformity_list.html @@ -6,11 +6,11 @@

Conformities

{% block content %}
List and status if the security controlsList and status of framework requirements for the organisation.
MeasureRequirement Status OwnerControlsActionsControlAction Comment Edit
{{ con.measure.title }}{{ con.requirement.title }}
{% if not con.applicable %} @@ -67,7 +67,7 @@

{% for con in con.get_children %}

- {{ con.measure.title }} + {{ con.requirement.title }}
@@ -89,7 +89,7 @@

{% if con.get_control %} - + {% endif %} @@ -113,7 +113,7 @@

{% for con in con.get_children %}

- {{ con.measure.title }} + {{ con.requirement.title }}
@@ -160,7 +160,7 @@

{% endfor %} {% empty %}

No data to display
- + - + @@ -21,10 +21,10 @@

Conformities

{{ con.organization }} - + + @@ -26,15 +27,18 @@

Organizations

{{ org.administrative_id }}

+ @@ -44,7 +48,7 @@

Organizations

No organization defined. {% endfor %} -  +
Overview of the organizations conformity to there policiesOverview of the organizations conformity to there Frameworks
OrganizationPolicyFramework Status
- {{ con.measure.policy.name }} + {{ con.requirement.framework.name }} - +
{{ con.status }}%
diff --git a/conformity/templates/conformity/control_detail_list.html b/conformity/templates/conformity/control_detail_list.html new file mode 100644 index 0000000..f73d2cf --- /dev/null +++ b/conformity/templates/conformity/control_detail_list.html @@ -0,0 +1,92 @@ +{% extends "conformity/main.html" %} +{% load render_table from django_tables2 %} + +{% block header %} +

+ Control detail +

+

+ {% if control.level == 1 %} + {% elif control.level == 2 %} + {%endif %} {{ control.get_level_display }} + {{ control.get_frequency_display }} + {{ control.organization }} +

+{% endblock %} + +{% block content %} +

Titre : {{ control.title }}

+

Description : {{ control.description }}

+

Control history :

+ +
+
+
+ + + + + + + + + + + + + {% for cp in control.controlpoint %} + + + + + + + + {% empty %} + + + {% endfor %} + +
List of all controls
Start DateEnd DateOwnerStatusEdit
+ {{ cp.period_start_date | date:'d-M-Y'}} + + {{ cp.period_end_date | date:'d-M-Y'}} + + {{ cp.control_user| default_if_none:"" }} + + {% if cp.status == "SCHD" %} + + {% endif %} + {% if cp.status == "TOBE" %} + + {% endif %} + {% if cp.status == "NOK" %} + + {% endif %} + {% if cp.status == "OK" %} + + {% endif %} + {% if cp.status == "MISS" %} + + {% endif %} + {{ cp.get_status_display }} + + + {% if cp.status == "TOBE" %} + + {% else %} + + {% endif %} + +
No data to display
+
+ + + +
+
+
+
+{% endblock %} diff --git a/conformity/templates/conformity/control_form.html b/conformity/templates/conformity/control_form.html index a9793d5..47785c9 100644 --- a/conformity/templates/conformity/control_form.html +++ b/conformity/templates/conformity/control_form.html @@ -2,7 +2,7 @@ {% load django_bootstrap5 %} {% block header %} -

Edit of an control

+

Edit of a control

{% endblock %} {% block content %} diff --git a/conformity/templates/conformity/control_list.html b/conformity/templates/conformity/control_list.html new file mode 100644 index 0000000..bedf619 --- /dev/null +++ b/conformity/templates/conformity/control_list.html @@ -0,0 +1,171 @@ +{% extends "conformity/main.html" %} +{% load render_table from django_tables2 %} + +{% block header %} +

Controls

+{% endblock %} + +{% block content %} +
+ +
+ + + + + + + + + + + + + + + {% for control in control_list %} + + + + + + + + + {% empty %} + + + {% endfor %} + +
List of all controls
TitreOrganizationLevelFrequencyControl PointsEdit
+ {{ control.title }} + + {{ control.organization }} + + + {% if control.level == 1 %} + + {% endif %} + {% if control.level == 2 %} + + {%endif %} + {{ control.get_level_display }} + + + + + {{ control.get_frequency_display }} + + + + + + + +
No data to display
+
+ + + +
+{% endblock %} diff --git a/conformity/templates/conformity/controlpoint_form.html b/conformity/templates/conformity/controlpoint_form.html index 84fb5a1..b8b3cef 100644 --- a/conformity/templates/conformity/controlpoint_form.html +++ b/conformity/templates/conformity/controlpoint_form.html @@ -2,11 +2,11 @@ {% load django_bootstrap5 %} {% block header %} -

Edit of a control point

+

Edit of a control point

{% endblock %} {% block content %} - + {% csrf_token %}
@@ -17,8 +17,19 @@

{{ controlpoint.control.title }}

  • Control frequency: {{ controlpoint.control.get_frequency_display }}
  • Control period: From {{controlpoint.period_start_date}} to {{controlpoint.period_end_date}}
  • +
    + Attachments +
      + {% for attachment in controlpoint.attachment.all %} +
    • {{ attachment }}
    • + {% empty %} + No attachment + {% endfor %} +
    +

    {% bootstrap_form form %} +

    Associated actions

    {% for action in controlpoint.get_action %} @@ -28,14 +39,13 @@

    Associated actions

    {% empty %}

    No action associated

    {% endfor %} - + Register a corrective action


    {% bootstrap_button button_type="submit" content="Save" %} -
    diff --git a/conformity/templates/conformity/controlpoint_list.html b/conformity/templates/conformity/controlpoint_list.html index f5d5bcf..86e629a 100644 --- a/conformity/templates/conformity/controlpoint_list.html +++ b/conformity/templates/conformity/controlpoint_list.html @@ -2,22 +2,33 @@ {% load render_table from django_tables2 %} {% block header %} -

    Control

    +

    + Control Point +

    + {% if '?control__id=' in request.build_absolute_uri %} +

    + {% if controlpoint_list.0.control.level == 1 %} + {% elif controlpoint_list.0.control.level == 2 %} + {%endif %} {{ controlpoint_list.0.control.get_level_display }} + {{ controlpoint_list.0.control.get_frequency_display }} + {{ controlpoint_list.0.control.organization }} +

    + {% endif %} {% endblock %} {% block content %} -{% render_table object_list %} - -
    +{% if '?control__id=' in request.build_absolute_uri %} +

    Titre : {{ controlpoint_list.0.control.title }}

    +

    Description : {{ controlpoint_list.0.control.description }}

    +{% endif %} +
    +
    +
    - - - - @@ -28,27 +39,6 @@

    Control

    {% for cp in controlpoint_list %} - - - - @@ -56,7 +46,7 @@

    Control

    {{ cp.period_end_date | date:'d-M-Y'}} - {% empty %} - + {% endfor %} -
    List of all controls
    TitreOrganizationLevelFrequency Start Date End Date Owner
    - {{ cp.control.title }} - - {{ cp.control.organization }} - - - {% if cp.control.level == 1 %} - - {% endif %} - {% if cp.control.level == 2 %} - - {%endif %} - {{ cp.control.get_level_display }} - - - - {{ cp.control.get_frequency_display }} - {{ cp.period_start_date | date:'d-M-Y'}} - {{ cp.control.owner | default_if_none:"" }} + {{ cp.control_user| default_if_none:"" }} {% if cp.status == "SCHD" %} @@ -76,27 +66,30 @@

    Control

    {% endif %} {{ cp.get_status_display }}
    - + {% if cp.status == "TOBE" %} - class="bi bi-pencil-square" + {% else %} - class="bi bi-eye" + {% endif %} - > +
    No data to display
    No data to display
    +

    - - + +
    +
    +
    +
    {% endblock %} diff --git a/conformity/templates/conformity/finding_detail.html b/conformity/templates/conformity/finding_detail.html index 7fbe109..bba9f48 100644 --- a/conformity/templates/conformity/finding_detail.html +++ b/conformity/templates/conformity/finding_detail.html @@ -27,11 +27,11 @@

    {% block content %}

    Audit information

    {{ finding.audit.get_type }} realized by {{ finding.audit.auditor }} on {{ finding.audit.organization }} from {{ finding.audit.start_date }} to {{ finding.audit.end_date }}.

    - {% if finding.audit.get_policies %} + {% if finding.audit.get_frameworks %}

    The following policies were within the audit scope :

      - {% for policy in finding.audit.get_policies %} -
    • {{ policy }}
    • + {% for framework in finding.audit.get_frameworks %} +
    • {{ framework }}
    • {% endfor %}
    {% endif %} @@ -48,7 +48,7 @@

    Associated actions

    {% empty %}

    No action associated

    {% endfor %} - + Register a corrective action diff --git a/conformity/templates/conformity/finding_form.html b/conformity/templates/conformity/finding_form.html index 96d4fd9..a792307 100644 --- a/conformity/templates/conformity/finding_form.html +++ b/conformity/templates/conformity/finding_form.html @@ -2,7 +2,7 @@ {% load django_bootstrap5 %} {% block header %} -

    Edit of a finding record

    +

    Edit of a finding record

    {% endblock %} {% block content %} diff --git a/conformity/templates/conformity/policy_detail.html b/conformity/templates/conformity/framework_detail.html similarity index 77% rename from conformity/templates/conformity/policy_detail.html rename to conformity/templates/conformity/framework_detail.html index cea69c7..4dfcdd6 100644 --- a/conformity/templates/conformity/policy_detail.html +++ b/conformity/templates/conformity/framework_detail.html @@ -1,12 +1,12 @@ {% extends "conformity/main.html" %} {% block header %} -

    Policy: {{ policy.name }} (v{{policy.version}})

    +

    Framework: {{ framework.name }} {% if framework.version %} {{ framework.version }} {% endif %}

    {% endblock %} {% block content %} - {% if policy.get_root_measure %} - {% for m in policy.get_first_measures %} + {% if framework.get_root_requirement %} + {% for m in framework.get_first_requirements %}

    {{ m.title }}

    @@ -48,7 +48,22 @@
    {% endfor %} {% else %} {% endif %} + +
    + +
    +

    Attachments

    +
      + {% for attachment in framework.attachment.all %} +
    • {{ attachment }}
    • + {% empty %} + No attachment + {% endfor %} +
    +
    + + {% endblock %} diff --git a/conformity/templates/conformity/framework_list.html b/conformity/templates/conformity/framework_list.html new file mode 100644 index 0000000..6444f8c --- /dev/null +++ b/conformity/templates/conformity/framework_list.html @@ -0,0 +1,39 @@ +{% extends "conformity/main.html" %} + +{% block header %} +

    Frameworks

    +{% endblock %} + +{% block content %} + {% if framework_list %} + + + + + + + + + + + + {% for framework in framework_list %} + + + + + + + {% endfor %} + +
    List of frameworks
    FrameworkPublished byTypeRequirements
    {{ framework.name }} {% if framework.version %} {{ framework.version }} {% endif %}{{ framework.publish_by }}{{ framework.get_type }} + + + +
    + {% else %} + + {% endif %} +{% endblock %} diff --git a/conformity/templates/conformity/main.html b/conformity/templates/conformity/main.html index 9fb71ef..9876ef6 100644 --- a/conformity/templates/conformity/main.html +++ b/conformity/templates/conformity/main.html @@ -1,8 +1,10 @@ {% load django_bootstrap5 %} {% load static %} +{% load l10n %} {% load render_table from django_tables2 %} + @@ -67,9 +69,9 @@ @@ -90,12 +92,6 @@ @@ -114,6 +116,17 @@ diff --git a/conformity/templates/conformity/organization_detail.html b/conformity/templates/conformity/organization_detail.html new file mode 100644 index 0000000..d552bc4 --- /dev/null +++ b/conformity/templates/conformity/organization_detail.html @@ -0,0 +1,34 @@ +{% extends "conformity/main.html" %} + +{% block header %} +

    Organizations {{ organization }}

    + {{ organization.administrative_id|default:'ø' }} +{% endblock %} + +{% block content %} +

    {{ organization.description }}

    + +

    Applicable policy :

    + {% for item in organization.get_frameworks %} + +
    + {{ item }} +
    +
    +
    + {% endfor %} + +
    + +
    +

    Attachments

    +
      + {% for attachment in organization.attachment.all %} +
    • {{ attachment }}
    • + {% empty %} + No attachment + {% endfor %} +
    +
    + +{% endblock %} diff --git a/conformity/templates/conformity/organization_form.html b/conformity/templates/conformity/organization_form.html index ef7bb12..b14df3c 100644 --- a/conformity/templates/conformity/organization_form.html +++ b/conformity/templates/conformity/organization_form.html @@ -6,7 +6,7 @@

    Organization update

    {% endblock %} {% block content %} -
    + {% csrf_token %}
    diff --git a/conformity/templates/conformity/organization_list.html b/conformity/templates/conformity/organization_list.html index 5e5cc38..a520dea 100644 --- a/conformity/templates/conformity/organization_list.html +++ b/conformity/templates/conformity/organization_list.html @@ -11,7 +11,8 @@

    Organizations

    Organization DescriptionApplicable policiesApplicable frameworksView Edit
    - {% for item in org.get_policies %} - - + +
    +

    diff --git a/conformity/templates/conformity/policy_list.html b/conformity/templates/conformity/policy_list.html deleted file mode 100644 index ab72121..0000000 --- a/conformity/templates/conformity/policy_list.html +++ /dev/null @@ -1,39 +0,0 @@ -{% extends "conformity/main.html" %} - -{% block header %} -

    Policies

    -{% endblock %} - -{% block content %} - {% if policy_list %} - - - - - - - - - - - - {% for pol in policy_list %} - - - - - - - {% endfor %} - -
    List of the policies
    PolicyPublished byPolicy typeMeasures
    {{ pol.name }} (v{{ pol.version }}){{ pol.publish_by }}{{ pol.get_type }} - - - -
    - {% else %} - - {% endif %} -{% endblock %} diff --git a/conformity/templates/help.html b/conformity/templates/help.html new file mode 100644 index 0000000..9f426c6 --- /dev/null +++ b/conformity/templates/help.html @@ -0,0 +1,98 @@ +{% extends "conformity/main.html" %} +{% load django_bootstrap5 %} + +{% block header %} +

    Help Page

    +{% endblock %} + +{% block content %} +

    Welcome to the Oxomium!

    + +

    This help guide is designed to ensure we have a common understanding of key concepts we use and a clear view of each section and how to use the features effectively.

    + +

    Key Concepts

    +
    +
    Frameworks:
    +
    Structured sets of requirements and guidelines designed to help organizations secure their information systems. Examples include ISO 27001, NIST, and CIS.
    + +
    Requirements:
    +
    Specific criteria or conditions defined by a framework that an organization must meet to achieve compliance. Requirements outline what needs to be accomplished through the implementation of controls.
    + +
    Conformity Assessments:
    +
    Evaluations conducted to determine how well an organization meets the requirements of a selected security framework. These assessments help identify areas of strength and potential gaps.
    + +
    Controls:
    +
    Specific actions or safeguards implemented to manage risks and achieve compliance with security frameworks. Controls can be technical, administrative, or physical measures.
    + +
    Control Points:
    +
    The execution instances of a control at a specific time. Control Points are where compliance and effectiveness of the controls are evaluated.
    + +
    Audits:
    +
    Systematic evaluations of the effectiveness of controls and the overall security posture of the organization. Audits are used to ensure that the controls are operating as intended and to identify any areas for improvement.
    + +
    Actions:
    +
    Corrective steps taken to address non-conformities or issues identified during audits or control assessments. Actions are tracked to ensure that problems are resolved promptly and effectively.
    +
    + +

    Conformity Management

    + +

    Frameworks

    +

    Description:

    +

    This section lists all the security frameworks available within the software. Frameworks are structured sets of guidelines and requirements designed to help organizations secure their information systems. Examples include ISO 27001, NIST, CIS, and any custom or internal frameworks.

    +

    Key Features:

    +
      +
    • Browse Frameworks: View a list of all available frameworks.
    • +
    • Framework Requirements: Select a framework to view its associated requirements. These are the specific conditions or controls that must be met to achieve compliance with the framework.
    • +
    + +

    Organizations

    +

    Description:

    +

    In this section, you can manage the different organizations that are being monitored within the software. Each organization can have its own set of policies, controls, and compliance statuses.

    +

    Key Features:

    +
      +
    • List of Organizations: View and manage all the organizations tracked by the software.
    • +
    • Organization Details: Select an organization to see detailed information about its security posture, including the frameworks it is aligned with and its current conformity status.
    • +
    + +

    Conformities

    +

    Description:

    +

    The Conformities section allows you to assess and manage the compliance of your organizations with specific frameworks. Here, you can conduct conformity assessments to evaluate how well an organization meets the requirements of a chosen framework.

    +

    Key Features:

    +
      +
    • Assessment Process: Select an organization and a framework to begin a conformity assessment. The software will guide you through the process of evaluating each requirement.
    • +
    • Compliance Status: View the compliance status of each organization against selected frameworks, including areas of strength and potential gaps.
    • +
    + +

    Chapter: Continuous Improvements

    + +

    Controls

    +

    Description:

    +

    Controls are specific actions or safeguards implemented to manage risks and ensure compliance with security frameworks. This section allows you to create, manage, and track these controls.

    +

    Key Features:

    +
      +
    • Control Plans: Organize related controls into structured plans tailored for specific objectives or frameworks.
    • +
    • Control Execution: Manage the scheduling and tracking of control executions, known as Control Points, where each control is applied and monitored.
    • +
    • Outcome Tracking: Record the results of control executions, including compliance with defined thresholds and any observations or issues identified during the process.
    • +
    + +

    Audits

    +

    Description:

    +

    The Audits section is where you can plan, execute, and review audits of your organization’s security practices. Audits are systematic evaluations of the effectiveness of your controls and overall security posture.

    +

    Key Features:

    +
      +
    • Audit Planning: Schedule and organize audits, including the scope, objectives, and responsible personnel.
    • +
    • Audit Execution: Conduct audits and record findings, including any non-conformities or areas for improvement.
    • +
    • Audit Reports: Generate and review audit reports, which summarize the audit findings and provide recommendations for corrective actions.
    • +
    + +

    Actions

    +

    Description:

    +

    Actions are the specific steps taken to address issues identified during audits or through continuous monitoring of controls. This section helps you manage and track these corrective actions.

    +

    Key Features:

    +
      +
    • Action Management: Create, assign, and track actions based on audit findings or control assessments.
    • +
    • Progress Tracking: Monitor the progress of each action, including deadlines, responsible users, and completion status.
    • +
    • Reporting: Generate reports on the status of actions to ensure that issues are addressed promptly and effectively.
    • +
    + +{% endblock %} \ No newline at end of file diff --git a/conformity/templates/home.html b/conformity/templates/home.html index 97f6193..da4d587 100644 --- a/conformity/templates/home.html +++ b/conformity/templates/home.html @@ -7,7 +7,22 @@

    Welcome {{request.user.first_name}} !

    {% block content %}
    -
    +
    + + +
    + +
    My Conformities
    My actives actions
    -
    Policy
    +
    Frameworks
    {{policy_list|length}} policies + href="{%url 'conformity:framework_index'%}">{{framework_list|length}} frameworks
      - {% for policy in policy_list %} + {% for framework in framework_list %} {{policy}} + href="{%url 'conformity:framework_detail' framework.id %}"> {{framework}} {% endfor %}
    diff --git a/conformity/templates/registration/login.html b/conformity/templates/registration/login.html index ccb55c1..e950cf4 100644 --- a/conformity/templates/registration/login.html +++ b/conformity/templates/registration/login.html @@ -2,6 +2,7 @@ {% load static %} + diff --git a/conformity/tests.py b/conformity/tests.py index bca136e..fabc426 100644 --- a/conformity/tests.py +++ b/conformity/tests.py @@ -1,8 +1,10 @@ import glob import importlib from calendar import monthrange -from datetime import date, timedelta +from datetime import date, timedelta, datetime +from dateutil.utils import today +from dateutil.relativedelta import relativedelta from django.conf import settings from django.db import IntegrityError from django.test import TestCase, Client @@ -11,94 +13,96 @@ from django.utils import timezone, inspect from django.views import View -from .models import Policy, Organization, Audit, Finding, Measure, Conformity, Action, User, Control, ControlPoint +from .models import Framework, Organization, Audit, Finding, Requirement, Conformity, Action, User, Control, ControlPoint from .views import * +from .middleware import SanityCheckMiddleware + import random, inspect from statistics import mean +# pylint: disable=no-member - -class PolicyModelTest(TestCase): +class FrameworkModelTest(TestCase): def setUp(self): - self.policy = Policy.objects.create(name='Test Policy', version=1, publish_by='Test Publisher', - type=Policy.Type.INTERNATIONAL) + self.framework = Framework.objects.create(name='Test Framework', version=1, publish_by='Test Publisher', + type=Framework.Type.INTERNATIONAL) def test_str_representation(self): - """Test the string representation of the Policy model""" - self.assertEqual(str(self.policy), 'Test Policy') + """Test the string representation of the Framework model""" + self.assertEqual(str(self.framework), 'Test Framework') def test_natural_key(self): - """Test the natural key of the Policy model""" - self.assertEqual(self.policy.natural_key(), 'Test Policy') + """Test the natural key of the Framework model""" + self.assertEqual(self.framework.natural_key(), 'Test Framework') def test_get_by_natural_key_does_not_exist(self): - with self.assertRaises(Policy.DoesNotExist): - Policy.objects.get_by_natural_key("Non Existing Policy") + with self.assertRaises(Framework.DoesNotExist): + Framework.objects.get_by_natural_key("Non Existing Framework") def test_get_type(self): - """Test the get_type method of the Policy model""" - self.assertEqual(self.policy.get_type(), 'International Standard') + """Test the get_type method of the Framework model""" + self.assertEqual(self.framework.get_type(), 'International Standard') - def test_get_measures(self): - """Test the get_measures method of the Policy model""" - measures = self.policy.get_measures() - self.assertQuerysetEqual(measures, []) + def test_get_requirement(self): + """Test the get_requirements method of the Framework model""" + requirements = self.framework.get_requirements() + self.assertQuerysetEqual(requirements, []) - def test_get_measures_number(self): - """Test the get_measures_number method of the Policy model""" - measures_number = self.policy.get_measures_number() - self.assertEqual(measures_number, 0) + def test_get_requirements_number(self): + """Test the get_requirements_number method of the Framework model""" + requirements_number = self.framework.get_requirements_number() + self.assertEqual(requirements_number, 0) - def test_get_root_measure(self): - """Test the get_root_measure method of the Policy model""" - root_measure = self.policy.get_root_measure() - self.assertQuerysetEqual(root_measure, []) + def test_get_root_requirement(self): + """Test the get_root_requirement method of the Framework model""" + root_requirement = self.framework.get_root_requirement() + self.assertQuerysetEqual(root_requirement, []) - def test_get_first_measures(self): - """Test the get_first_measures method of the Policy model""" - first_measures = self.policy.get_first_measures() - self.assertQuerysetEqual(first_measures, []) + def test_get_first_requirements(self): + """Test the get_first_requirements method of the Framework model""" + first_requirements = self.framework.get_first_requirements() + self.assertQuerysetEqual(first_requirements, []) def test_unique_name(self): """Test if the name field is unique""" - policy = Policy(name='Test Policy', version=1, publish_by='Test Publisher', type=Policy.Type.INTERNATIONAL) + framework = Framework(name='Test Framework', version=1, publish_by='Test Publisher', type=Framework.Type.INTERNATIONAL) with self.assertRaises(IntegrityError): - policy.save() + framework.save() def test_default_version(self): """Test if the version field has a default value of 0""" - policy = Policy.objects.create( - name='Test Policy 2', + framework = Framework.objects.create( + name='Test Framework 2', publish_by='Test Publisher 2', - type=Policy.Type.NATIONAL + type=Framework.Type.NATIONAL ) - self.assertEqual(policy.version, 0) + self.assertEqual(framework.version, 0) def test_default_type(self): """Test if the type field has a default value of 'OTHER'""" - policy = Policy.objects.create( - name='Test Policy 3', + framework = Framework.objects.create( + name='Test Framework 3', version=1, publish_by='Test Publisher 3', ) - self.assertEqual(policy.type, Policy.Type.OTHER) + self.assertEqual(framework.type, Framework.Type.OTHER) class OrganizationModelTest(TestCase): def setUp(self): - self.policy1 = Policy.objects.create(name="Policy 1", version=1, publish_by="Publisher 1") - self.policy2 = Policy.objects.create(name="Policy 2", version=2, publish_by="Publisher 2") - self.measure1 = Measure.objects.create(code='1', name='Test Measure 1', policy=self.policy1) - self.measure2 = Measure.objects.create(code='2000', name='Test Measure 2', policy=self.policy2) - self.measure3 = Measure.objects.create(code='2100', name='Test Measure 2.1', - policy=self.policy2, parent=self.measure2) - self.measure4 = Measure.objects.create(code='2110', name='Test Measure 2.1.1', - policy=self.policy2, parent=self.measure3) - self.measure5 = Measure.objects.create(code='2120', name='Test Measure 2.1.2', - policy=self.policy2, parent=self.measure4) - self.measure6 = Measure.objects.create(code='2200', name='Test Measure 2.2', - policy=self.policy2, parent=self.measure2) + self.framework1 = Framework.objects.create(name="Framework 1", version=1, publish_by="Publisher 1") + self.framework2 = Framework.objects.create(name="Framework 2", version=2, publish_by="Publisher 2") + self.requirement1 = Requirement.objects.create(code='1', name='Test Requirement 1', framework=self.framework1) + self.requirement2 = Requirement.objects.create(code='2000', name='Test Requirement 2', framework=self.framework2) + self.requirement3 = Requirement.objects.create(code='2100', name='Test Requirement 2.1', + framework=self.framework2, parent=self.requirement2) + self.requirement4 = Requirement.objects.create(code='2110', name='Test Requirement 2.1.1', + framework=self.framework2, parent=self.requirement3) + self.requirement5 = Requirement.objects.create(code='2120', name='Test Requirement 2.1.2', + framework=self.framework2, parent=self.requirement4) + self.requirement6 = Requirement.objects.create(code='2200', name='Test Requirement 2.2', + framework=self.framework2, parent=self.requirement2) self.organization = Organization.objects.create(name="Organization 1", administrative_id="Admin ID 1", description="Organization 1 description") @@ -112,35 +116,35 @@ def test_get_absolute_url(self): self.assertEqual(self.organization.get_absolute_url(), '/organization/') def test_add_remove_conformity(self): - # Add the policies to the organization - self.organization.add_conformity(self.policy1) - self.organization.add_conformity(self.policy2) + # Add the frameworks to the organization + self.organization.add_conformity(self.framework1) + self.organization.add_conformity(self.framework2) - # Check if the conformity is created for the policies + # Check if the conformity is created for the frameworks conformities = Conformity.objects.filter(organization=self.organization) self.assertEqual(conformities.count(), 6) - # Remove conformity for policy1 - self.organization.remove_conformity(self.policy2) + # Remove conformity for framework1 + self.organization.remove_conformity(self.framework2) conformities = Conformity.objects.filter(organization=self.organization) self.assertEqual(conformities.count(), 1) - def test_get_policies(self): - self.organization.applicable_policies.add(self.policy1) - self.organization.applicable_policies.add(self.policy2) - policies = self.organization.get_policies() - self.assertIn(self.policy1, policies) - self.assertIn(self.policy2, policies) + def test_get_framworks(self): + self.organization.applicable_frameworks.add(self.framework1) + self.organization.applicable_frameworks.add(self.framework2) + frameworks = self.organization.get_frameworks() + self.assertIn(self.framework1, frameworks) + self.assertIn(self.framework2, frameworks) class AuditModelTests(TestCase): def setUp(self): organization = Organization.objects.create(name='Organization A') - policy = Policy.objects.create(name='Policy A', organization=organization) + framework = Framework.objects.create(name='Framework A', organization=organization) audit = Audit.objects.create(organization=organization, description='Test Audit', conclusion='Test Conclusion', auditor='Test Auditor', start_date=timezone.now(), end_date=timezone.now(), report_date=timezone.now()) - audit.audited_policies.add(policy) + audit.audited_frameworks.add(framework) finding = Finding.objects.create(short_description='Test Finding', description='Test Description', reference='Test Reference', audit=audit, severity=Finding.Severity.CRITICAL) @@ -152,9 +156,9 @@ def test_absolute_url(self): audit = Audit.objects.get(id=1) self.assertEqual(audit.get_absolute_url(), '/audit/') - def test_policies(self): + def test_frameworks(self): audit = Audit.objects.get(id=1) - self.assertEqual(audit.get_policies().count(), 1) + self.assertEqual(audit.get_frameworks().count(), 1) def test_type(self): audit = Audit.objects.get(id=1) @@ -220,32 +224,32 @@ def test_str(self): self.assertEqual(str(finding), 'Test Short Description') -class MeasureModelTest(TestCase): +class RequirementModelTest(TestCase): def setUp(self): - policy = Policy.objects.create(name='test policy') - self.measure1 = Measure.objects.create(code="m1", name='Measure 1', policy=policy, title='Measure 1 Title', - description='Measure 1 Description') - self.measure2 = Measure.objects.create(code="m2", name='Measure 2', policy=policy, title='Measure 2 Title', - description='Measure 2 Description', parent=self.measure1) - self.measure3 = Measure.objects.create(code="m3", name='Measure 3', policy=policy, title='Measure 3 Title', - description='Measure 3 Description', parent=self.measure1) + framework = Framework.objects.create(name='test framework') + self.requirement1 = Requirement.objects.create(code="m1", name='Requirement 1', framework=framework, title='Requirement 1 Title', + description='Requirement 1 Description') + self.requirement2 = Requirement.objects.create(code="m2", name='Requirement 2', framework=framework, title='Requirement 2 Title', + description='Requirement 2 Description', parent=self.requirement1) + self.requirement3 = Requirement.objects.create(code="m3", name='Requirement 3', framework=framework, title='Requirement 3 Title', + description='Requirement 3 Description', parent=self.requirement1) def test_str(self): - self.assertEqual(str(self.measure1), 'm1: Measure 1 Title') + self.assertEqual(str(self.requirement1), 'm1: Requirement 1 Title') def test_get_by_natural_key(self): - self.assertEqual(self.measure1.natural_key(), 'm1') + self.assertEqual(self.requirement1.natural_key(), 'm1') def test_get_children(self): - children = self.measure1.get_children() + children = self.requirement1.get_children() self.assertEqual(len(children), 2) - self.assertIn(self.measure2, children) - self.assertIn(self.measure3, children) + self.assertIn(self.requirement2, children) + self.assertIn(self.requirement3, children) - def test_measure_without_policy(self): - measure = Measure(name="Measure without policy", title="Test measure without policy") + def test_requirement_without_framework(self): + requirement = Requirement(name="Requirement without framework", title="Test requirement without framework") with self.assertRaises(Exception): - measure.save() + requirement.save() class ConformityModelTest(TestCase): @@ -253,18 +257,18 @@ class ConformityModelTest(TestCase): def setUp(self): # create a user self.organization = Organization.objects.create(name='Test Organization') - self.policy = Policy.objects.create(name='test policy') - self.measure0 = Measure.objects.create(policy=self.policy, code='TEST-00', name='Test Measure Root') - self.measure1 = Measure.objects.create(policy=self.policy, code='TEST-01', - name='Test Measure 01', parent=self.measure0) - self.measure2 = Measure.objects.create(policy=self.policy, code='TEST-02', - name='Test Measure 02', parent=self.measure0) - self.measure3 = Measure.objects.create(policy=self.policy, code='TEST-03', - name='Test Measure 03', parent=self.measure2) - self.measure4 = Measure.objects.create(policy=self.policy, code='TEST-04', - name='Test Measure 04', parent=self.measure2) - - self.organization.add_conformity(self.policy) + self.framework = Framework.objects.create(name='test framework') + self.requirement0 = Requirement.objects.create(framework=self.framework, code='TEST-00', name='Test Requirement Root') + self.requirement1 = Requirement.objects.create(framework=self.framework, code='TEST-01', + name='Test Requirement 01', parent=self.requirement0) + self.requirement2 = Requirement.objects.create(framework=self.framework, code='TEST-02', + name='Test Requirement 02', parent=self.requirement0) + self.requirement3 = Requirement.objects.create(framework=self.framework, code='TEST-03', + name='Test Requirement 03', parent=self.requirement2) + self.requirement4 = Requirement.objects.create(framework=self.framework, code='TEST-04', + name='Test Requirement 04', parent=self.requirement2) + + self.organization.add_conformity(self.framework) def test_str(self): conformity = Conformity.objects.get(id=1) @@ -272,7 +276,7 @@ def test_str(self): def test_get_absolute_url(self): conformity = Conformity.objects.get(id=1) - self.assertEqual(conformity.get_absolute_url(), '/conformity/org/1/pol/1/') + self.assertEqual(conformity.get_absolute_url(), '/conformity/organization/1/framework/1/') def test_get_children(self): conformity = Conformity.objects.get(id=1) @@ -348,17 +352,17 @@ def setUp(self): OrganizationDetailView, OrganizationUpdateView, OrganizationCreateView, - PolicyIndexView, - PolicyDetailView, + FrameworkIndexView, + FrameworkDetailView, ConformityIndexView, - ConformityOrgPolIndexView, + ConformityDetailIndexView, ConformityUpdateView, ActionCreateView, ActionIndexView, ActionUpdateView, AuditLogDetailView, # Form - #ConformityForm, #TODO issue with the references at the Form instanciation. Exclude from test. + #ConformityForm, #TODO issue with the references at the Form instantiation. Exclude from test. OrganizationForm, AuditForm, FindingForm, @@ -466,3 +470,145 @@ def test_control_point_status(self): def test_get_absolute_url(self): self.assertEqual(ControlPoint.objects.first().get_absolute_url(), '/control/') + + +class SanityCheckMiddlewareTest(TestCase): + @classmethod + def setUpTestData(cls): + """Set up test data for all tests.""" + today = datetime.today().date() + + # Create a user and a control instance as they are foreign keys in ControlPoint + cls.user = User.objects.create_user(username='testuser', password='password') + cls.control = Control.objects.create(title="Test Control") + + # Control points that should be marked as 'MISS' + cls.missed_control_1 = ControlPoint.objects.create( + control=cls.control, + control_user=cls.user, + period_start_date=today - relativedelta(days=10), + period_end_date=today - relativedelta(days=1), # missed yesterday + status="TOBE" + ) + cls.missed_control_3 = ControlPoint.objects.create( + control=cls.control, + control_user=cls.user, + period_start_date=today - relativedelta(days=10), + period_end_date=today - relativedelta(days=5), # missed 5 days ago + status="TOBE" + ) + + # Control points that should not be updated (not in TOBE status) + cls.not_missed_control = ControlPoint.objects.create( + control=cls.control, + control_user=cls.user, + period_start_date=today - relativedelta(days=10), + period_end_date=today - relativedelta(days=5), # missed but not in TOBE + status="MISS" + ) + cls.not_missed_control_2 = ControlPoint.objects.create( + control=cls.control, + control_user=cls.user, + period_start_date=today - relativedelta(days=10), + period_end_date=today, # today is not miss + status="TOBE" + ) + + # Control points that should be marked as 'TOBE' + cls.scheduled_control_1 = ControlPoint.objects.create( + control=cls.control, + control_user=cls.user, + period_start_date=today.replace(day=1), + period_end_date=today.replace(day=1) + relativedelta(months=1) - relativedelta(days=1), + status="SCHD" + ) + cls.scheduled_control_2 = ControlPoint.objects.create( + control=cls.control, + control_user=cls.user, + period_start_date=today.replace(day=1) - relativedelta(months=3), + period_end_date=today.replace(day=1) + relativedelta(months=2), + status="SCHD" + ) + + # Control point that should stay in SCHED + cls.scheduled_control_3 = ControlPoint.objects.create( + control=cls.control, + control_user=cls.user, + period_start_date=today + relativedelta(days=1), + period_end_date=today + relativedelta(days=30), + status="SCHD" + ) + + # Control point that should not be updated (not in SCHD status) + cls.not_scheduled_control = ControlPoint.objects.create( + control=cls.control, + control_user=cls.user, + period_start_date=today.replace(day=1), + period_end_date=today.replace(day=1) + relativedelta(months=1) - relativedelta(days=1), + status="TOBE" + ) + + # Control point that should not be updated (OK ou NOK) + cls.ok_control = ControlPoint.objects.create( + control=cls.control, + control_user=cls.user, + period_start_date=today.replace(day=1), + period_end_date=today.replace(day=1) + relativedelta(months=1) - relativedelta(days=1), + status="OK" + ) + cls.nok_control = ControlPoint.objects.create( + control=cls.control, + control_user=cls.user, + period_start_date=today.replace(day=1) - relativedelta(months=3), + period_end_date=today.replace(day=1) - relativedelta(days=1), + status="NOK" + ) + + def test_missed_controls_update(self): + """Test that missed controls are updated to 'MISS'.""" + today = datetime.today().date() + SanityCheckMiddleware.check_control_points(today) + + self.missed_control_1.refresh_from_db() + self.not_missed_control_2.refresh_from_db() + self.missed_control_3.refresh_from_db() + + self.assertEqual(self.missed_control_1.status, 'MISS') + self.assertEqual(self.not_missed_control_2.status, 'TOBE') + self.assertEqual(self.missed_control_3.status, 'MISS') + + def test_scheduled_controls_update(self): + """Test that scheduled controls are updated to 'TOBE'.""" + today = datetime.today().date() + SanityCheckMiddleware.check_control_points(today) + + self.scheduled_control_1.refresh_from_db() + self.scheduled_control_2.refresh_from_db() + self.scheduled_control_3.refresh_from_db() + + self.assertEqual(self.scheduled_control_1.status, 'TOBE') + self.assertEqual(self.scheduled_control_2.status, 'TOBE') + self.assertEqual(self.scheduled_control_3.status, 'SCHD') + + def test_no_update(self): + """Test some control that should not be updated""" + today = datetime.today().date() + SanityCheckMiddleware.check_control_points(today) + + """Test a SCHD control that should not switch to TOBE""" + self.scheduled_control_3.refresh_from_db() + self.assertEqual(self.scheduled_control_3.status, 'SCHD') + + """Test that controls not in 'TOBE' or 'SCHD' are not updated.""" + self.not_missed_control.refresh_from_db() + self.not_scheduled_control.refresh_from_db() + + self.assertEqual(self.not_missed_control.status, 'MISS') + self.assertEqual(self.not_scheduled_control.status, 'TOBE') + + """Test that controls not in 'OK' or 'NOK' are not updated.""" + self.ok_control.refresh_from_db() + self.nok_control.refresh_from_db() + + self.assertEqual(self.ok_control.status, 'OK') + self.assertEqual(self.nok_control.status, 'NOK') \ No newline at end of file diff --git a/conformity/urls.py b/conformity/urls.py index 6db9f9c..56c6bd0 100644 --- a/conformity/urls.py +++ b/conformity/urls.py @@ -2,6 +2,7 @@ Conformity module URL router """ from django.urls import path +from django.views.generic import TemplateView from . import views @@ -15,8 +16,8 @@ path('audit/update/', views.AuditUpdateView.as_view(), name='audit_form'), path('conformity/', views.ConformityIndexView.as_view(), name='conformity_index'), - path('conformity/org//pol//', views.ConformityOrgPolIndexView.as_view(), - name='conformity_orgpol_index'), + path('conformity/organization//framework//', views.ConformityDetailIndexView.as_view(), + name='conformity_detail_index'), path('conformity/update/', views.ConformityUpdateView.as_view(), name='conformity_form'), path('finding/create', views.FindingCreateView.as_view(), name='finding_create'), @@ -28,8 +29,8 @@ path('organization/create', views.OrganizationCreateView.as_view(), name='organization_create'), path('organization/update/', views.OrganizationUpdateView.as_view(), name='organization_form'), - path('policy/', views.PolicyIndexView.as_view(), name='policy_index'), - path('policy//', views.PolicyDetailView.as_view(), name='policy_detail'), + path('framework/', views.FrameworkIndexView.as_view(), name='framework_index'), + path('framework//', views.FrameworkDetailView.as_view(), name='framework_detail'), path('action/', views.ActionIndexView.as_view(), name='action_index'), path('action/create', views.ActionCreateView.as_view(), name='action_create'), @@ -38,7 +39,14 @@ path('control/', views.ControlIndexView.as_view(), name='control_index'), path('control/create', views.ControlCreateView.as_view(), name='control_create'), path('control/update/', views.ControlUpdateView.as_view(), name='control_form'), + path('control/', views.ControlDetailView.as_view(), name='control_detail'), - path('auditlog/', views.AuditLogDetailView.as_view(), name='auditlog_index'), + path('controlpoint/', views.ControlPointIndexView.as_view(), name='controlpoint_index'), + path('controlpoint/update/', views.ControlPointUpdateView.as_view(), name='controlpoint_form'), + + path('attachment/', views.AttachmentIndexView.as_view(), name='attachment_index'), + path('attachment//', views.AttachmentDownloadView.as_view(), name='attachment_download'), + path('help/', TemplateView.as_view(template_name='help.html'), name='help'), + path('auditlog/', views.AuditLogDetailView.as_view(), name='auditlog_index'), ] diff --git a/conformity/views.py b/conformity/views.py index d7c7123..2a1fb36 100644 --- a/conformity/views.py +++ b/conformity/views.py @@ -9,10 +9,14 @@ from django_filters.views import FilterView from auditlog.models import LogEntry -from .filterset import ActionFilter, ControlFilter +from .filterset import ActionFilter, ControlFilter, ControlPointFilter from .forms import ConformityForm, AuditForm, FindingForm, ActionForm, OrganizationForm, ControlForm, ControlPointForm -from .models import Organization, Policy, Conformity, Audit, Action, Finding, Control, ControlPoint +from .models import Organization, Framework, Conformity, Audit, Action, Finding, Control, ControlPoint, Attachment +from django.views import View +from django.http import HttpResponse +from django.shortcuts import get_object_or_404 +import os # # Home @@ -26,12 +30,13 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) user = self.request.user context['organization_list'] = Organization.objects.all() - context['policy_list'] = Policy.objects.all() - context['conformity_list'] = Conformity.objects.filter(measure__level=0) + context['framework_list'] = Framework.objects.all() + context['conformity_list'] = Conformity.objects.filter(requirement__level=0) context['audit_list'] = Audit.objects.all() context['action_list'] = Action.objects.all() context['my_action'] = Action.objects.filter(owner=user).filter(active=True).order_by('status')[:50] context['my_conformity'] = Conformity.objects.filter(responsible=user).order_by('status')[:50] + context['cp_list'] = ControlPoint.objects.filter(status='TOBE').order_by('period_end_date')[:50] return context @@ -52,11 +57,28 @@ class AuditUpdateView(LoginRequiredMixin, UpdateView): model = Audit form_class = AuditForm + def form_valid(self, form): + response = super().form_valid(form) + attachments = self.request.FILES.getlist('attachments') + for file in attachments: + attachment = Attachment.objects.create(file=file) + self.object.attachment.add(attachment) + return response + + class AuditCreateView(LoginRequiredMixin, CreateView): model = Audit form_class = AuditForm + def form_valid(self, form): + response = super().form_valid(form) + attachments = self.request.FILES.getlist('attachments') + for file in attachments: + attachment = Attachment.objects.create(file=file) + self.object.attachment.add(attachment) + return response + # # Findings @@ -87,34 +109,46 @@ class OrganizationIndexView(LoginRequiredMixin, ListView): ordering = ['name'] -class OrganizationDetailView(LoginRequiredMixin, ListView): - model = Conformity - template = "conformity/template/conformity/conformity_list.html" - - def get_queryset(self, **kwargs): - return Conformity.objects.filter(organization__id=self.kwargs['pk']) +class OrganizationDetailView(LoginRequiredMixin, DetailView): + model = Organization class OrganizationUpdateView(LoginRequiredMixin, UpdateView): model = Organization form_class = OrganizationForm + def form_valid(self, form): + response = super().form_valid(form) + attachments = self.request.FILES.getlist('attachments') + for file in attachments: + attachment = Attachment.objects.create(file=file) + self.object.attachment.add(attachment) + return response + class OrganizationCreateView(LoginRequiredMixin, CreateView): model = Organization form_class = OrganizationForm + def form_valid(self, form): + response = super().form_valid(form) + attachments = self.request.FILES.getlist('attachments') + for file in attachments: + attachment = Attachment.objects.create(file=file) + self.object.attachment.add(attachment) + return response + # -# Policy +# Framework # -class PolicyIndexView(LoginRequiredMixin, ListView): - model = Policy +class FrameworkIndexView(LoginRequiredMixin, ListView): + model = Framework -class PolicyDetailView(LoginRequiredMixin, DetailView): - model = Policy +class FrameworkDetailView(LoginRequiredMixin, DetailView): + model = Framework # @@ -124,18 +158,18 @@ class ConformityIndexView(LoginRequiredMixin, ListView): model = Conformity def get_queryset(self, **kwargs): - return Conformity.objects.filter(measure__level=0) + return Conformity.objects.filter(requirement__level=0) -class ConformityOrgPolIndexView(LoginRequiredMixin, ListView): +class ConformityDetailIndexView(LoginRequiredMixin, ListView): model = Conformity - template_name = 'conformity/conformity_orgpol_list.html' + template_name = 'conformity/conformity_detail_list.html' def get_queryset(self, **kwargs): return Conformity.objects.filter(organization__id=self.kwargs['org']) \ - .filter(measure__policy__id=self.kwargs['pol']) \ - .filter(measure__level=0) \ - .order_by('measure__order') + .filter(requirement__framework__id=self.kwargs['pol']) \ + .filter(requirement__level=0) \ + .order_by('requirement__order') class ConformityUpdateView(LoginRequiredMixin, UpdateView): @@ -180,14 +214,82 @@ class ControlCreateView(LoginRequiredMixin, CreateView): class ControlIndexView(LoginRequiredMixin, FilterView): - model = ControlPoint + model = Control filterset_class = ControlFilter - template_name = 'conformity/controlpoint_list.html' + template_name = 'conformity/control_list.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + user = self.request.user + context['controlpoint_list'] = ControlPoint.objects.all() + context['c1st'] = Control.objects.filter(level="1").count() + context['c2nd'] = Control.objects.filter(level="2").count() + context['cp0x'] = ControlPoint.objects.filter(status="TOBE").count() + context['cp1x'] = ControlPoint.objects.filter(control__frequency="1").filter(status="TOBE").count() + context['cp2x'] = ControlPoint.objects.filter(control__frequency="2").filter(status="TOBE").count() + context['cp4x'] = ControlPoint.objects.filter(control__frequency="4").filter(status="TOBE").count() + context['cp6x'] = ControlPoint.objects.filter(control__frequency="6").filter(status="TOBE").count() + context['cp12x'] = ControlPoint.objects.filter(control__frequency="12").filter(status="TOBE").count() + + return context class ControlUpdateView(LoginRequiredMixin, UpdateView): + model = Control + form_class = ControlForm + + +class ControlDetailView(LoginRequiredMixin, DetailView): + model = Control + template_name = 'conformity/control_detail_list.html' + + + +class ControlPointIndexView(LoginRequiredMixin, FilterView): + model = ControlPoint + filterset_class = ControlPointFilter + template_name = 'conformity/controlpoint_list.html' + + +class ControlPointUpdateView(LoginRequiredMixin, UpdateView): model = ControlPoint form_class = ControlPointForm + #success_url = reverse_lazy('conformity:controlpoint_list') # Adjust the success URL as needed + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['user'] = self.request.user + return kwargs + + def form_valid(self, form): + response = super().form_valid(form) + attachments = self.request.FILES.getlist('attachments') + for file in attachments: + attachment = Attachment.objects.create(file=file) + self.object.attachment.add(attachment) + return response + + +# +# Attachment +# + + +class AttachmentIndexView(LoginRequiredMixin, ListView): + model = Attachment + ordering = ['-create_date', 'file'] + + +class AttachmentDownloadView(View): + def get(self, request, pk): + attachment = get_object_or_404(Attachment, id=pk) + + file_path = attachment.file.path + file_name = os.path.basename(file_path) + + response = HttpResponse(open(file_path, 'rb'), content_type='application/octet-stream') + response['Content-Disposition'] = f'attachment; filename="{file_name}"' + return response # diff --git a/misc/oxomium.service b/misc/oxomium.service index f00173d..02657ef 100644 --- a/misc/oxomium.service +++ b/misc/oxomium.service @@ -11,7 +11,7 @@ User=www-data Group=www-data DynamicUser=yes -PrivateDevies=yes +PrivateDevices=yes PrivateUsers=yes PrivateDevices=yes PrivateTmp=yes diff --git a/oxomium/settings.py b/oxomium/settings.py index cb0962a..189bdfe 100644 --- a/oxomium/settings.py +++ b/oxomium/settings.py @@ -65,6 +65,7 @@ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'auditlog.middleware.AuditlogMiddleware', + 'conformity.middleware.SanityCheckMiddleware', ] ROOT_URLCONF = 'oxomium.urls' @@ -145,9 +146,9 @@ # AuditLog configuration AUDITLOG_INCLUDE_ALL_MODELS = True AUDITLOG_INCLUDE_TRACKING_MODELS = ( - "conformity.Organization", {"model": "conformity.Organization", "m2m_fields": ["applicable_policies"], }, + "conformity.Organization", {"model": "conformity.Organization", "m2m_fields": ["applicable_frameworks"], }, "conformity.Action", {"model": "conformity.Action", "m2m_fields": ["associated_conformity", "associated_findings"], }, - "conformity.Audit", {"model": "conformity.Audit", "m2m_fields": ["audited_policies"], } + "conformity.Audit", {"model": "conformity.Audit", "m2m_fields": ["audited_frameworks"], } ) AUDITLOG_EXCLUDE_TRACKING_FIELDS = ( "created", diff --git a/q b/q new file mode 100644 index 0000000..9b96974 --- /dev/null +++ b/q @@ -0,0 +1,421 @@ +diff --git a/conformity/admin.py b/conformity/admin.py +index 8196b68..27dad88 100644 +--- a/conformity/admin.py ++++ b/conformity/admin.py +@@ -5,7 +5,7 @@ Customize Django Admin Site to manage my Models instances + from django.contrib import admin + from import_export import resources + from import_export.admin import ImportExportModelAdmin +-from .models import Organization, Policy, Measure, Conformity, Audit, Finding, Action, Control, ControlPoint ++from .models import Organization, Framework, Measure, Conformity, Audit, Finding, Action, Control, ControlPoint +  +  + class OrganizationResources(resources.ModelResource): +@@ -17,13 +17,13 @@ class OrganizationAdmin(ImportExportModelAdmin): + ressource_class = Organization +  +  +-class PolicyResources(resources.ModelResource): ++class FrameworkResources(resources.ModelResource): + class Meta: +- model = Policy ++ model = Framework +  +  +-class PolicyAdmin(ImportExportModelAdmin): +- ressource_class = Policy ++class FrameworkAdmin(ImportExportModelAdmin): ++ ressource_class = Framework +  +  + class MeasureResources(resources.ModelResource): +@@ -55,7 +55,7 @@ class ActionAdmin(ImportExportModelAdmin): +  +  + # Registration +-admin.site.register(Policy, PolicyAdmin) ++admin.site.register(Framework, FrameworkAdmin) + admin.site.register(Measure, MeasureAdmin) + admin.site.register(Conformity, ConformityAdmin) + admin.site.register(Action, ActionAdmin) +diff --git a/conformity/migrations/0001_squashed_0014_mesure_is_parent.py b/conformity/migrations/0001_squashed_0014_mesure_is_parent.py +index f7c8759..ea1c41b 100644 +--- a/conformity/migrations/0001_squashed_0014_mesure_is_parent.py ++++ b/conformity/migrations/0001_squashed_0014_mesure_is_parent.py +@@ -47,7 +47,7 @@ class Migration(migrations.Migration): + ('name', models.CharField(max_length=256)), + ('administrative_id', models.CharField(blank=True, max_length=256)), + ('description', models.CharField(blank=True, max_length=256)), +- ('applicable_policies', models.ManyToManyField(blank=True, to='conformity.policy')), ++ ('applicable_frameworks', models.ManyToManyField(blank=True, to='conformity.policy')), + ], + ), + migrations.CreateModel( +diff --git a/conformity/migrations/0001_squashed_0029_alter_action_control_comment_and_more.py b/conformity/migrations/0001_squashed_0029_alter_action_control_comment_and_more.py +index 670c280..efe0eed 100644 +--- a/conformity/migrations/0001_squashed_0029_alter_action_control_comment_and_more.py ++++ b/conformity/migrations/0001_squashed_0029_alter_action_control_comment_and_more.py +@@ -40,7 +40,7 @@ class Migration(migrations.Migration): + ('name', models.CharField(max_length=256, unique=True)), + ('administrative_id', models.CharField(blank=True, max_length=256)), + ('description', models.TextField(blank=True, max_length=4096)), +- ('applicable_policies', models.ManyToManyField(blank=True, to='conformity.policy')), ++ ('applicable_frameworks', models.ManyToManyField(blank=True, to='conformity.policy')), + ], + options={ + 'ordering': ['name'], +diff --git a/conformity/migrations/0041_rename_policy_framework_alter_framework_options_and_more.py b/conformity/migrations/0041_rename_policy_framework_alter_framework_options_and_more.py +index 03d4015..067c37d 100644 +--- a/conformity/migrations/0041_rename_policy_framework_alter_framework_options_and_more.py ++++ b/conformity/migrations/0041_rename_policy_framework_alter_framework_options_and_more.py +@@ -23,4 +23,9 @@ class Migration(migrations.Migration): + old_name='policy', + new_name='framework', + ), ++ migrations.RenameField( ++ model_name='organization', ++ old_name='applicable_policies', ++ new_name='applicable_frameworks', ++ ), + ] +diff --git a/conformity/models.py b/conformity/models.py +index e255c84..7de3cb4 100644 +--- a/conformity/models.py ++++ b/conformity/models.py +@@ -1,6 +1,6 @@ + """ + Conformity module manage all the manual declarative aspect of conformity management. +-It's Organized around Organization, Policy, Measure and Conformity classes. ++It's Organized around Organization, Framework, Measure and Conformity classes. + """ + from calendar import monthrange + from statistics import mean +@@ -20,19 +20,19 @@ from auditlog.context import set_actor + User = get_user_model() +  +  +-class PolicyManager(models.Manager): ++class FrameworkManager(models.Manager): + def get_by_natural_key(self, name): + return self.get(name=name) +  +  +-class Policy(models.Model): ++class Framework(models.Model): + """ +- Policy class represent the conformity policy you will apply on Organization. +- A Policy is simply a collections of Measure with publication parameter. ++ Framework class represent the conformity framework you will apply on Organization. ++ A Framework is simply a collections of Measure with publication parameter. + """ +  + class Type(models.TextChoices): +- """ List of the Type of policy """ ++ """ List of the Type of framework """ + INTERNATIONAL = 'INT', _('International Standard') + NATIONAL = 'NAT', _('National Standard') + TECHNICAL = 'TECH', _('Technical Standard') +@@ -40,7 +40,7 @@ class Policy(models.Model): + POLICY = 'POL', _('Internal Policy') + OTHER = 'OTHER', _('Other') +  +- objects = PolicyManager() ++ objects = FrameworkManager() + name = models.CharField(max_length=256, unique=True) + version = models.IntegerField(default=0) + publish_by = models.CharField(max_length=256) +@@ -62,35 +62,35 @@ class Policy(models.Model): + return (self.name) +  + def get_type(self): +- """return the readable version of the Policy Type""" ++ """return the readable version of the Framework Type""" + return self.Type(self.type).label +  + def get_measures(self): +- """return all Measure related to the Policy""" +- return Measure.objects.filter(policy=self.id) ++ """return all Measure related to the Framework""" ++ return Measure.objects.filter(framework=self.id) +  + def get_measures_number(self): +- """return the number of leaf Measure related to the Policy""" +- return Measure.objects.filter(policy=self.id).filter(measure__is_parent=False).count() ++ """return the number of leaf Measure related to the Framework""" ++ return Measure.objects.filter(framework=self.id).filter(measure__is_parent=False).count() +  + def get_root_measure(self): +- """return the root Measure of the Policy""" +- return Measure.objects.filter(policy=self.id).filter(level=0).order_by('order') ++ """return the root Measure of the Framework""" ++ return Measure.objects.filter(framework=self.id).filter(level=0).order_by('order') +  + def get_first_measures(self): +- """return the Measure of the first hierarchical level of the Policy""" +- return Measure.objects.filter(policy=self.id).filter(level=1).order_by('order') ++ """return the Measure of the first hierarchical level of the Framework""" ++ return Measure.objects.filter(framework=self.id).filter(level=1).order_by('order') +  +  + class Organization(models.Model): + """ + Organization class is a representation of a company, a division of company, an administration... +- The Organization may answer to one or several Policy. ++ The Organization may answer to one or several Framework. + """ + name = models.CharField(max_length=256, unique=True) + administrative_id = models.CharField(max_length=256, blank=True) + description = models.TextField(max_length=4096, blank=True) +- applicable_policies = models.ManyToManyField(Policy, blank=True) ++ applicable_frameworks = models.ManyToManyField(Framework, blank=True) +  + class Meta: + ordering = ['name'] +@@ -106,21 +106,21 @@ class Organization(models.Model): + """return the absolute URL for Forms, could probably do better""" + return reverse('conformity:organization_index') +  +- def get_policies(self): +- """return all Policy applicable to the Organization""" +- return self.applicable_policies.all() ++ def get_frameworks(self): ++ """return all Framework applicable to the Organization""" ++ return self.applicable_frameworks.all() +  + def remove_conformity(self, pid): + """Cascade deletion of conformity""" + with set_actor('system'): +- measure_set = Measure.objects.filter(policy=pid) ++ measure_set = Measure.objects.filter(framework=pid) + for measure in measure_set: + Conformity.objects.filter(measure=measure.id).filter(organization=self.id).delete() +  + def add_conformity(self, pid): + """Automatic creation of conformity""" + with set_actor('system'): +- measure_set = Measure.objects.filter(policy=pid) ++ measure_set = Measure.objects.filter(framework=pid) + for measure in measure_set: + conformity = Conformity(organization=self, measure=measure) + conformity.save() +@@ -134,7 +134,7 @@ class MeasureManager(models.Manager): + class Measure(models.Model): + """ + A Measure is a precise requirement. +- Measure can be hierarchical in order to form a collection of Measure, aka Policy. ++ Measure can be hierarchical in order to form a collection of Measure, aka Framework. + A Measure is not representing the conformity level, see Conformity class. + """ + objects = MeasureManager() +@@ -142,7 +142,7 @@ class Measure(models.Model): + name = models.CharField(max_length=50, blank=True, unique=True) + level = models.IntegerField(default=0) + order = models.IntegerField(default=1) +- policy = models.ForeignKey(Policy, on_delete=models.CASCADE) ++ framework = models.ForeignKey(Framework, on_delete=models.CASCADE) + parent = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True) + title = models.CharField(max_length=256, blank=True) + description = models.TextField(blank=True) +@@ -157,7 +157,7 @@ class Measure(models.Model): + def natural_key(self): + return (self.name) +  +- natural_key.dependencies = ['conformity.policy'] ++ natural_key.dependencies = ['conformity.framework'] +  + def get_children(self): + """Return all children of the measure""" +@@ -188,12 +188,12 @@ class Conformity(models.Model): + def natural_key(self): + return self.organization, self.measure +  +- natural_key.dependencies = ['conformity.policy', 'conformity.measure', 'conformity.organization'] ++ natural_key.dependencies = ['conformity.framework', 'conformity.measure', 'conformity.organization'] +  + def get_absolute_url(self): + """Return the absolute URL of the class for Form, probably not the best way to do it""" + return reverse('conformity:conformity_orgpol_index', +- kwargs={'org': self.organization.id, 'pol': self.measure.policy.id}) ++ kwargs={'org': self.organization.id, 'pol': self.measure.framework.id}) +  + def get_children(self): + """Return all children Conformity based on Measure hierarchy""" +@@ -259,8 +259,8 @@ def post_init_callback(instance, **kwargs): + instance.name = instance.code +  +  +-@receiver(m2m_changed, sender=Organization.applicable_policies.through) +-def change_policy(instance, action, pk_set, *args, **kwargs): ++@receiver(m2m_changed, sender=Organization.applicable_frameworks.through) ++def change_framework(instance, action, pk_set, *args, **kwargs): + if action == "post_add": + for pk in pk_set: + instance.add_conformity(pk) +@@ -288,7 +288,7 @@ class Audit(models.Model): + description = models.TextField(max_length=4096, blank=True) + conclusion = models.TextField(max_length=4096, blank=True) + auditor = models.CharField(max_length=256) +- audited_policies = models.ManyToManyField(Policy, blank=True) ++ audited_policies = models.ManyToManyField(Framework, blank=True) + start_date = models.DateField(null=True, blank=True) + end_date = models.DateField(null=True, blank=True) + report_date = models.DateField(null=True, blank=True) +@@ -320,7 +320,7 @@ class Audit(models.Model): + return reverse('conformity:audit_index') +  + def get_policies(self): +- """return all Policy within the Audit scope""" ++ """return all Framework within the Audit scope""" + return self.audited_policies.all() +  + def get_type(self): +diff --git a/conformity/templates/conformity/audit_detail.html b/conformity/templates/conformity/audit_detail.html +index 5d1bd8c..ba97035 100644 +--- a/conformity/templates/conformity/audit_detail.html ++++ b/conformity/templates/conformity/audit_detail.html +@@ -9,7 +9,7 @@ +

    {{ audit.get_type }} realized by {{ audit.auditor }} from {{ audit.start_date }} to {{ audit.end_date }}.

     +

    The following policies were within the audit scope :

     +
       +- {% for policy in audit.get_policies %} ++ {% for policy in audit.get_frameworks %} +
    • {{ policy }}
    •  + {% empty %} +
    • No policy within the audit scope.
    •  +diff --git a/conformity/templates/conformity/finding_detail.html b/conformity/templates/conformity/finding_detail.html +index c3e46ff..a75f81e 100644 +--- a/conformity/templates/conformity/finding_detail.html ++++ b/conformity/templates/conformity/finding_detail.html +@@ -27,10 +27,10 @@ + {% block content %} +

      Audit information

       +

      {{ finding.audit.get_type }} realized by {{ finding.audit.auditor }} on {{ finding.audit.organization }} from {{ finding.audit.start_date }} to {{ finding.audit.end_date }}.

       +- {% if finding.audit.get_policies %} ++ {% if finding.audit.get_frameworks %} +

      The following policies were within the audit scope :

       +
         +- {% for policy in finding.audit.get_policies %} ++ {% for policy in finding.audit.get_frameworks %} +
      • {{ policy }}
      •  + {% endfor %} +
       +diff --git a/conformity/templates/conformity/organization_list.html b/conformity/templates/conformity/organization_list.html +index b9c1588..85eec52 100644 +--- a/conformity/templates/conformity/organization_list.html ++++ b/conformity/templates/conformity/organization_list.html +@@ -26,7 +26,7 @@ +

      {{ org.administrative_id }}

       +
     +- {% for item in org.get_policies %} ++ {% for item in org.get_frameworks %} +  +
     + {{ item }} +diff --git a/conformity/tests.py b/conformity/tests.py +index bca136e..460b8d2 100644 +--- a/conformity/tests.py ++++ b/conformity/tests.py +@@ -126,9 +126,9 @@ class OrganizationModelTest(TestCase): + self.assertEqual(conformities.count(), 1) +  + def test_get_policies(self): +- self.organization.applicable_policies.add(self.policy1) +- self.organization.applicable_policies.add(self.policy2) +- policies = self.organization.get_policies() ++ self.organization.applicable_frameworks.add(self.policy1) ++ self.organization.applicable_frameworks.add(self.policy2) ++ policies = self.organization.get_frameworks() + self.assertIn(self.policy1, policies) + self.assertIn(self.policy2, policies) +  +@@ -154,7 +154,7 @@ class AuditModelTests(TestCase): +  + def test_policies(self): + audit = Audit.objects.get(id=1) +- self.assertEqual(audit.get_policies().count(), 1) ++ self.assertEqual(audit.get_frameworks().count(), 1) +  + def test_type(self): + audit = Audit.objects.get(id=1) +diff --git a/conformity/urls.py b/conformity/urls.py +index e68154c..8bd82b7 100644 +--- a/conformity/urls.py ++++ b/conformity/urls.py +@@ -29,8 +29,8 @@ urlpatterns = [ + path('organization/create', views.OrganizationCreateView.as_view(), name='organization_create'), + path('organization/update/', views.OrganizationUpdateView.as_view(), name='organization_form'), +  +- path('framework/', views.PolicyIndexView.as_view(), name='policy_index'), +- path('framework//', views.PolicyDetailView.as_view(), name='policy_detail'), ++ path('framework/', views.FrameworkIndexView.as_view(), name='framework_index'), ++ path('framework//', views.FrameworkDetailView.as_view(), name='framework_detail'), +  + path('action/', views.ActionIndexView.as_view(), name='action_index'), + path('action/create', views.ActionCreateView.as_view(), name='action_create'), +diff --git a/conformity/views.py b/conformity/views.py +index b54d9bd..273e5bb 100644 +--- a/conformity/views.py ++++ b/conformity/views.py +@@ -11,7 +11,7 @@ from auditlog.models import LogEntry +  + from .filterset import ActionFilter, ControlFilter, ControlPointFilter + from .forms import ConformityForm, AuditForm, FindingForm, ActionForm, OrganizationForm, ControlForm, ControlPointForm +-from .models import Organization, Policy, Conformity, Audit, Action, Finding, Control, ControlPoint ++from .models import Organization, Framework, Conformity, Audit, Action, Finding, Control, ControlPoint +  +  + # +@@ -26,7 +26,7 @@ class HomeView(LoginRequiredMixin, TemplateView): + context = super().get_context_data(**kwargs) + user = self.request.user + context['organization_list'] = Organization.objects.all() +- context['policy_list'] = Policy.objects.all() ++ context['framework_list'] = Framework.objects.all() + context['conformity_list'] = Conformity.objects.filter(measure__level=0) + context['audit_list'] = Audit.objects.all() + context['action_list'] = Action.objects.all() +@@ -105,16 +105,16 @@ class OrganizationCreateView(LoginRequiredMixin, CreateView): + form_class = OrganizationForm +  + # +-# Policy ++# Framework + # +  +  +-class PolicyIndexView(LoginRequiredMixin, ListView): +- model = Policy ++class FrameworkIndexView(LoginRequiredMixin, ListView): ++ model = Framework +  +  +-class PolicyDetailView(LoginRequiredMixin, DetailView): +- model = Policy ++class FrameworkDetailView(LoginRequiredMixin, DetailView): ++ model = Framework +  +  + # +@@ -133,7 +133,7 @@ class ConformityOrgPolIndexView(LoginRequiredMixin, ListView): +  + def get_queryset(self, **kwargs): + return Conformity.objects.filter(organization__id=self.kwargs['org']) \ +- .filter(measure__policy__id=self.kwargs['pol']) \ ++ .filter(measure__framework__id=self.kwargs['pol']) \ + .filter(measure__level=0) \ + .order_by('measure__order') +  +diff --git a/oxomium/settings.py b/oxomium/settings.py +index cb0962a..688c10c 100644 +--- a/oxomium/settings.py ++++ b/oxomium/settings.py +@@ -145,7 +145,7 @@ LOGOUT_REDIRECT_URL = "/" + # AuditLog configuration + AUDITLOG_INCLUDE_ALL_MODELS = True + AUDITLOG_INCLUDE_TRACKING_MODELS = ( +- "conformity.Organization", {"model": "conformity.Organization", "m2m_fields": ["applicable_policies"], }, ++ "conformity.Organization", {"model": "conformity.Organization", "m2m_fields": ["applicable_frameworks"], }, + "conformity.Action", {"model": "conformity.Action", "m2m_fields": ["associated_conformity", "associated_findings"], }, + "conformity.Audit", {"model": "conformity.Audit", "m2m_fields": ["audited_policies"], } + ) diff --git a/requirements.txt b/requirements.txt index 79e3a00..95c4938 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ -Django +Django == 4.2.* django-bootstrap5 django-import-export django-auditlog django-tinymce django-filter -django-tables2 \ No newline at end of file +django-tables2 +python-magic \ No newline at end of file diff --git a/sonar-project.properties b/sonar-project.properties index f24aee0..d035b44 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,4 +1,4 @@ sonar.projectKey=pep-un_Oxomium sonar.organization=pep-un -sonar.python.version = 3.8, 3.9, 3.10, 3.11 +sonar.python.version = 3.9, 3.10, 3.11, 3.12 sonar.python.coverage.reportPaths=coverage.xml