From 019b2589b16b38ff82c19e6c80af08e9a370dc1f Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Fri, 6 Sep 2024 18:36:00 +0200 Subject: [PATCH] Allow audit creation when creating entity assessment --- backend/core/helpers.py | 35 --------- backend/core/models.py | 73 ++++++++++++++++++- backend/core/views.py | 30 +------- backend/tprm/serializers.py | 43 ++++++++++- backend/tprm/views.py | 7 ++ .../src/lib/components/Forms/ModelForm.svelte | 36 +++++++++ .../lib/components/Modals/CreateModal.svelte | 2 +- frontend/src/lib/utils/crud.ts | 3 +- frontend/src/lib/utils/schemas.ts | 7 +- 9 files changed, 164 insertions(+), 72 deletions(-) diff --git a/backend/core/helpers.py b/backend/core/helpers.py index a06b86f8a..099bf32b6 100644 --- a/backend/core/helpers.py +++ b/backend/core/helpers.py @@ -1154,39 +1154,4 @@ def handle(exc, context): return drf_exception_handler(exc, context) -def transform_question_to_answer(json_data): - """ - Used during Requirement Assessment creation to create a questionnaire base on - the Requirement Node question JSON field - - Args: - json_data (json): JSON describing a questionnaire from a Requirement Node - - Returns: - json: JSON formatted for the frontend to display a form - """ - question_type = json_data.get("question_type", "") - question_choices = json_data.get("question_choices", []) - questions = json_data.get("questions", []) - - form_fields = [] - - for question in questions: - field = {} - field["urn"] = question.get("urn", "") - field["text"] = question.get("text", "") - - if question_type == "unique_choice": - field["type"] = "unique_choice" - field["options"] = question_choices - elif question_type == "date": - field["type"] = "date" - else: - field["type"] = "text" - - field["answer"] = "" - - form_fields.append(field) - form_json = {"questions": form_fields} - return form_json diff --git a/backend/core/models.py b/backend/core/models.py index f54ede8fc..a7a7cd6eb 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -31,7 +31,7 @@ from django.utils.html import format_html from django.utils.translation import get_language from django.utils.translation import gettext_lazy as _ -from iam.models import FolderMixin, PublishInRootFolderMixin +from iam.models import Folder, FolderMixin, PublishInRootFolderMixin from library.helpers import update_translations, update_translations_in_object from structlog import get_logger @@ -937,6 +937,45 @@ class Meta: verbose_name = _("RequirementNode") verbose_name_plural = _("RequirementNodes") + def format_answer(self) -> dict: + """ + Used during Requirement Assessment creation to create a questionnaire based on + the Requirement Node question JSON field + + Args: + json_data (json): JSON describing a questionnaire from a Requirement Node + + Returns: + json: JSON formatted for the frontend to display a form + """ + if not self.question: + return {} + json_data = self.question + _type = json_data.get("question_type", "") + choices = json_data.get("question_choices", []) + questions = json_data.get("questions", []) + + form_fields = [] + + for question in questions: + field = {} + field["urn"] = question.get("urn", "") + field["text"] = question.get("text", "") + + if _type == "unique_choice": + field["type"] = "unique_choice" + field["options"] = choices + elif _type == "date": + field["type"] = "date" + else: + field["type"] = "text" + + field["answer"] = "" + + form_fields.append(field) + + return {"questions": form_fields} + class RequirementMappingSet(ReferentialObjectMixin): library = models.ForeignKey( @@ -2001,6 +2040,38 @@ def save(self, *args, **kwargs) -> None: self.scores_definition = self.framework.scores_definition super().save(*args, **kwargs) + def create_requirement_assessments(self, baseline: Self | None = None): + requirements = RequirementNode.objects.filter(framework=self.framework) + requirement_assessments = [] + for requirement in requirements: + requirement_assessment = RequirementAssessment.objects.create( + compliance_assessment=self, + requirement=requirement, + folder=Folder.objects.get(id=self.project.folder.id), + answer=transform_question_to_answer(requirement.question) + if requirement.question + else {}, + ) + if baseline and baseline.framework == self.framework: + baseline_requirement_assessment = RequirementAssessment.objects.get( + compliance_assessment=baseline, requirement=requirement + ) + requirement_assessment.result = baseline_requirement_assessment.result + requirement_assessment.status = baseline_requirement_assessment.status + requirement_assessment.score = baseline_requirement_assessment.score + requirement_assessment.is_scored = ( + baseline_requirement_assessment.is_scored + ) + requirement_assessment.evidences.set( + baseline_requirement_assessment.evidences.all() + ) + requirement_assessment.applied_controls.set( + baseline_requirement_assessment.applied_controls.all() + ) + requirement_assessment.save() + requirement_assessments.append(requirement_assessment) + return requirement_assessments + def get_global_score(self): requirement_assessments_scored = ( RequirementAssessment.objects.filter(compliance_assessment=self) diff --git a/backend/core/views.py b/backend/core/views.py index 445432e47..0d01fa3cf 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -1482,34 +1482,8 @@ def perform_create(self, serializer): Create RequirementAssessment objects for the newly created ComplianceAssessment """ baseline = serializer.validated_data.pop("baseline", None) - instance = serializer.save() - requirements = RequirementNode.objects.filter(framework=instance.framework) - for requirement in requirements: - requirement_assessment = RequirementAssessment.objects.create( - compliance_assessment=instance, - requirement=requirement, - folder=Folder.objects.get(id=instance.project.folder.id), - answer=transform_question_to_answer(requirement.question) - if requirement.question - else {}, - ) - if baseline and baseline.framework == instance.framework: - baseline_requirement_assessment = RequirementAssessment.objects.get( - compliance_assessment=baseline, requirement=requirement - ) - requirement_assessment.result = baseline_requirement_assessment.result - requirement_assessment.status = baseline_requirement_assessment.status - requirement_assessment.score = baseline_requirement_assessment.score - requirement_assessment.is_scored = ( - baseline_requirement_assessment.is_scored - ) - requirement_assessment.evidences.set( - baseline_requirement_assessment.evidences.all() - ) - requirement_assessment.applied_controls.set( - baseline_requirement_assessment.applied_controls.all() - ) - requirement_assessment.save() + instance: ComplianceAssessment = serializer.save() + instance.create_requirement_assessments(baseline) if baseline and baseline.framework != instance.framework: mapping_set = RequirementMappingSet.objects.get( target_framework=serializer.validated_data["framework"], diff --git a/backend/tprm/serializers.py b/backend/tprm/serializers.py index 6bf98f9d4..dafe1cf55 100644 --- a/backend/tprm/serializers.py +++ b/backend/tprm/serializers.py @@ -1,9 +1,9 @@ +from rest_framework import serializers +from core.models import ComplianceAssessment, Framework + from core.serializer_fields import FieldsRelatedField from core.serializers import BaseModelSerializer -from iam.models import Folder -from tprm.models import Entity, Representative, Solution, EntityAssessment -from rest_framework import serializers -from django.utils.translation import gettext_lazy as _ +from tprm.models import Entity, EntityAssessment, Representative, Solution class EntityReadSerializer(BaseModelSerializer): @@ -38,6 +38,41 @@ class Meta: exclude = [] +class EntityAssessmentCreateSerializer(BaseModelSerializer): + create_audit = serializers.BooleanField(default=True) + framework = serializers.PrimaryKeyRelatedField( + queryset=Framework.objects.all(), required=False + ) + selected_implementation_groups = serializers.ListField( + child=serializers.CharField(), required=False + ) + + def create(self, validated_data): + create_audit = validated_data.pop("create_audit") + _framework = validated_data.pop("framework", None) + _selected_implementation_groups = validated_data.pop( + "selected_implementation_groups", None + ) + instance = super().create(validated_data) + if create_audit: + if not _framework: + raise serializers.ValidationError("frameworkRequiredToCreateAudit") + audit = ComplianceAssessment.objects.create( + name=validated_data["name"], + framework=_framework, + project=validated_data["project"], + selected_implementation_groups=_selected_implementation_groups, + ) + audit.create_requirement_assessments() + instance.compliance_assessment = audit + instance.save() + return instance + + class Meta: + model = EntityAssessment + exclude = [] + + class RepresentativeReadSerializer(BaseModelSerializer): entity = FieldsRelatedField() diff --git a/backend/tprm/views.py b/backend/tprm/views.py index 9ff226450..123c75274 100644 --- a/backend/tprm/views.py +++ b/backend/tprm/views.py @@ -3,6 +3,8 @@ from tprm.models import Entity, Representative, Solution, EntityAssessment from rest_framework.decorators import action +from tprm.serializers import EntityAssessmentCreateSerializer + class BaseModelViewSet(AbstractBaseModelViewSet): serializers_module = "tprm.serializers" @@ -25,6 +27,11 @@ class EntityAssessmentViewSet(BaseModelViewSet): model = EntityAssessment filterset_fields = ["status", "project", "project__folder", "authors", "entity"] + def get_serializer_class(self, **kwargs): + if self.action == "create": + return EntityAssessmentCreateSerializer + return super().get_serializer_class(**kwargs) + @action(detail=False, name="Get status choices") def status(self, request): return Response(dict(EntityAssessment.Status.choices)) diff --git a/frontend/src/lib/components/Forms/ModelForm.svelte b/frontend/src/lib/components/Forms/ModelForm.svelte index 74adc7151..07814566b 100644 --- a/frontend/src/lib/components/Forms/ModelForm.svelte +++ b/frontend/src/lib/components/Forms/ModelForm.svelte @@ -735,6 +735,42 @@ hide={initialData.folder} /> {:else if URLModel === 'entity-assessments'} + {#if context === 'create'} + + { + if (e.detail) { + await fetch(`/frameworks/${e.detail}`) + .then((r) => r.json()) + .then((r) => { + const implementation_groups = r['implementation_groups_definition'] || []; + model.selectOptions['selected_implementation_groups'] = implementation_groups.map( + (group) => ({ label: group.name, value: group.ref_id }) + ); + }); + } + }} + /> + {#if model.selectOptions['selected_implementation_groups'] && model.selectOptions['selected_implementation_groups'].length} + + {/if} + {/if} = { users: UserCreateSchema, 'sso-settings': SSOSettingsSchema, entities: EntitiesSchema, - 'entity-assessments': entityAssessmentsSchema, + 'entity-assessments': EntityAssessmentSchema, representatives: representativeSchema, solutions: solutionSchema };