From b95b533a467bf13c028367ddd26fdaa9b73195b0 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Wed, 25 Sep 2024 14:36:34 +0200 Subject: [PATCH 01/37] Write first implementation of RequirementAssessment.create_applied_controls_from_suggestions --- backend/core/models.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/backend/core/models.py b/backend/core/models.py index b2ca68536..fc67d69d2 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -2581,6 +2581,33 @@ def infer_result( return (RequirementAssessment.Result.NON_COMPLIANT, None) return (None, None) + def create_applied_controls_from_suggestions(self) -> list[AppliedControl]: + applied_controls: list[AppliedControl] = [] + for reference_control in self.requirement.reference_controls.all(): + try: + # TODO: handle name unicity in scope + _name = reference_control.name or reference_control.ref_id + applied_control = AppliedControl.objects.create( + name=_name, + folder=self.folder, + reference_control=reference_control, + category=reference_control.category, + description=reference_control.description, + ) + logger.info( + "Successfully created applied control from reference_control", + applied_control=applied_control, + reference_control=reference_control, + ) + applied_controls.append(applied_control) + except Exception as e: + logger.error( + "An error occurred while creating applied control from reference control", + reference_control=reference_control, + exc_info=e, + ) + return applied_controls + class Meta: verbose_name = _("Requirement assessment") verbose_name_plural = _("Requirement assessments") From e6329e72d623155f7fba934d27eb9ae133336b22 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Wed, 25 Sep 2024 14:52:17 +0200 Subject: [PATCH 02/37] Skip applied control creation if it already exists --- backend/core/models.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/backend/core/models.py b/backend/core/models.py index fc67d69d2..916ea557b 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -2587,18 +2587,25 @@ def create_applied_controls_from_suggestions(self) -> list[AppliedControl]: try: # TODO: handle name unicity in scope _name = reference_control.name or reference_control.ref_id - applied_control = AppliedControl.objects.create( + applied_control, created = AppliedControl.objects.get_or_create( name=_name, folder=self.folder, reference_control=reference_control, category=reference_control.category, description=reference_control.description, ) - logger.info( - "Successfully created applied control from reference_control", - applied_control=applied_control, - reference_control=reference_control, - ) + if created: + logger.info( + "Successfully created applied control from reference_control", + applied_control=applied_control, + reference_control=reference_control, + ) + else: + logger.info( + "Applied control already exists, skipping creation and using existing one", + applied_control=applied_control, + reference_control=reference_control, + ) applied_controls.append(applied_control) except Exception as e: logger.error( From e74034a188a81c39d8eb69b2c2bc4204b707e5b4 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Wed, 25 Sep 2024 14:52:52 +0200 Subject: [PATCH 03/37] Serialize create_applied_controls_from_suggestions boolean field --- backend/core/serializers.py | 3 +++ backend/core/views.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/backend/core/serializers.py b/backend/core/serializers.py index d23ded61c..1842aa2d8 100644 --- a/backend/core/serializers.py +++ b/backend/core/serializers.py @@ -559,6 +559,9 @@ class ComplianceAssessmentWriteSerializer(BaseModelSerializer): required=False, allow_null=True, ) + create_applied_controls_from_suggestions = serializers.BooleanField( + write_only=True, required=False, default=False + ) def create(self, validated_data: Any): return super().create(validated_data) diff --git a/backend/core/views.py b/backend/core/views.py index 501f584dd..a8a0ed6ae 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -1661,6 +1661,9 @@ def perform_create(self, serializer): Create RequirementAssessment objects for the newly created ComplianceAssessment """ baseline = serializer.validated_data.pop("baseline", None) + create_applied_controls = serializer.validated_data.pop( + "create_applied_controls_from_suggestions", False + ) instance: ComplianceAssessment = serializer.save() instance.create_requirement_assessments(baseline) if baseline and baseline.framework != instance.framework: @@ -1688,6 +1691,9 @@ def perform_create(self, serializer): ] ) requirement_assessment.save() + if create_applied_controls: + for requirement_assessment in instance.requirement_assessments.all(): + requirement_assessment.create_applied_controls_from_suggestions() @action(detail=False, name="Compliance assessments per status") def per_status(self, request): From 8f68b0dd22b57932289dac7771a06fc4031310b3 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Wed, 25 Sep 2024 15:29:06 +0200 Subject: [PATCH 04/37] Set requirement assessment applied controls after creating or getting them --- backend/core/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/core/models.py b/backend/core/models.py index 916ea557b..f3ade3331 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -2613,6 +2613,8 @@ def create_applied_controls_from_suggestions(self) -> list[AppliedControl]: reference_control=reference_control, exc_info=e, ) + continue + self.applied_controls.set(applied_controls) return applied_controls class Meta: From 27f2aac5d1c3e51a077738a5c8313e9cb1205504 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Wed, 25 Sep 2024 15:29:22 +0200 Subject: [PATCH 05/37] Implement create_applied_controls_from_suggestions field in UI --- frontend/messages/en.json | 4 +++- .../Forms/ModelForm/ComplianceAssessmentForm.svelte | 9 +++++++++ frontend/src/lib/utils/schemas.ts | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 1e469a5ef..cf879d260 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -739,5 +739,7 @@ "nameDuplicate": "Name already exists", "noAnswer": "No answer", "successfullyUpdatedClientSettings": "Client settings successfully updated, please refresh the page.", - "xRaysEmptyMessage": "You have to create at least one project to use X-rays." + "xRaysEmptyMessage": "You have to create at least one project to use X-rays.", + "createAppliedControlsFromSuggestions": "Create applied controls from suggestions", + "createAppliedControlsFromSuggestionsHelpText": "Create applied controls from the framework's requirements' suggested reference controls" } diff --git a/frontend/src/lib/components/Forms/ModelForm/ComplianceAssessmentForm.svelte b/frontend/src/lib/components/Forms/ModelForm/ComplianceAssessmentForm.svelte index 5d3fd22ae..70b0fa891 100644 --- a/frontend/src/lib/components/Forms/ModelForm/ComplianceAssessmentForm.svelte +++ b/frontend/src/lib/components/Forms/ModelForm/ComplianceAssessmentForm.svelte @@ -7,6 +7,7 @@ import type { SuperValidated } from 'sveltekit-superforms'; import type { ModelInfo, CacheLock } from '$lib/utils/types'; import * as m from '$paraglide/messages.js'; + import Checkbox from '../Checkbox.svelte'; export let form: SuperValidated; export let model: ModelInfo; @@ -130,3 +131,11 @@ cacheLock={cacheLocks['observation']} bind:cachedValue={formDataCache['observation']} /> + diff --git a/frontend/src/lib/utils/schemas.ts b/frontend/src/lib/utils/schemas.ts index 176d1e429..9ae54d643 100644 --- a/frontend/src/lib/utils/schemas.ts +++ b/frontend/src/lib/utils/schemas.ts @@ -222,6 +222,7 @@ export const ComplianceAssessmentSchema = baseNamedObject({ authors: z.array(z.string().optional()).optional(), reviewers: z.array(z.string().optional()).optional(), baseline: z.string().optional().nullable(), + create_applied_controls_from_suggestions: z.boolean().optional().default(false), observation: z.string().optional().nullable() }); From 3d73a4ccf4ada854f6bdc091069667bc1451860f Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Wed, 25 Sep 2024 16:22:06 +0200 Subject: [PATCH 06/37] Only display create_applied_controls_from_suggestions on create form --- .../ModelForm/ComplianceAssessmentForm.svelte | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/src/lib/components/Forms/ModelForm/ComplianceAssessmentForm.svelte b/frontend/src/lib/components/Forms/ModelForm/ComplianceAssessmentForm.svelte index 70b0fa891..41d42cb05 100644 --- a/frontend/src/lib/components/Forms/ModelForm/ComplianceAssessmentForm.svelte +++ b/frontend/src/lib/components/Forms/ModelForm/ComplianceAssessmentForm.svelte @@ -131,11 +131,13 @@ cacheLock={cacheLocks['observation']} bind:cachedValue={formDataCache['observation']} /> - +{#if context === 'create'} + +{/if} From 8943e3e936cdcff79e681155d2063a9a680f614e Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Wed, 25 Sep 2024 16:23:22 +0200 Subject: [PATCH 07/37] Display a loading placeholder while RecursiveTreeView component is being mounted This greatly reduces the First Contentful Paint and Blocking Time of the compliance assessment detail page. --- .../components/TreeView/RecursiveTreeView.svelte | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/components/TreeView/RecursiveTreeView.svelte b/frontend/src/lib/components/TreeView/RecursiveTreeView.svelte index f5c7a1381..b99031fbd 100644 --- a/frontend/src/lib/components/TreeView/RecursiveTreeView.svelte +++ b/frontend/src/lib/components/TreeView/RecursiveTreeView.svelte @@ -1,9 +1,9 @@
- {#if nodes && nodes.length > 0} + {#if mounted && nodes && nodes.length > 0} + {:else} +
{/if}
From 34c6987caa07a278d329ca09b278f0f21bad7c35 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Wed, 25 Sep 2024 19:01:10 +0200 Subject: [PATCH 08/37] Write create_suffested_applied_controls action --- backend/core/views.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/backend/core/views.py b/backend/core/views.py index a8a0ed6ae..cdfd27355 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -1825,6 +1825,18 @@ def donut_data(self, request, pk): compliance_assessment = ComplianceAssessment.objects.get(id=pk) return Response(compliance_assessment.donut_render()) + @action( + detail=True, + methods=["post"], + url_path="suggestions/applied-controls", + ) + def create_suggested_applied_controls(self, request, pk): + compliance_assessment = self.get_object() + requirement_assessments = compliance_assessment.requirement_assessments.all() + for requirement_assessment in requirement_assessments: + requirement_assessment.create_applied_controls_from_suggestions() + return Response(status=status.HTTP_200_OK) + class RequirementAssessmentViewSet(BaseModelViewSet): """ From d216d47c75e2ec4ec124cc4fff5c37636f51cd4e Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Wed, 25 Sep 2024 19:03:21 +0200 Subject: [PATCH 09/37] Allow creating suggested controls from an existing audit --- frontend/messages/en.json | 4 ++- .../[id=uuid]/+page.svelte | 27 ++++++++++++++++++ .../suggestions/applied-controls/+server.ts | 28 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 frontend/src/routes/(app)/(third-party)/compliance-assessments/[id=uuid]/suggestions/applied-controls/+server.ts diff --git a/frontend/messages/en.json b/frontend/messages/en.json index cf879d260..f315df2c8 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -741,5 +741,7 @@ "successfullyUpdatedClientSettings": "Client settings successfully updated, please refresh the page.", "xRaysEmptyMessage": "You have to create at least one project to use X-rays.", "createAppliedControlsFromSuggestions": "Create applied controls from suggestions", - "createAppliedControlsFromSuggestionsHelpText": "Create applied controls from the framework's requirements' suggested reference controls" + "createAppliedControlsFromSuggestionsHelpText": "Create applied controls from the framework's requirements' suggested reference controls", + "createAppliedControlsFromSuggestionsSuccess": "Applied controls successfully created from the suggested reference controls", + "createAppliedControlsFromSuggestionsError": "An error occured while creating applied controls from the suggested reference controls" } diff --git a/frontend/src/routes/(app)/(third-party)/compliance-assessments/[id=uuid]/+page.svelte b/frontend/src/routes/(app)/(third-party)/compliance-assessments/[id=uuid]/+page.svelte index aad68ae6e..7dc9f4d7c 100644 --- a/frontend/src/routes/(app)/(third-party)/compliance-assessments/[id=uuid]/+page.svelte +++ b/frontend/src/routes/(app)/(third-party)/compliance-assessments/[id=uuid]/+page.svelte @@ -174,6 +174,25 @@ }; modalStore.trigger(modal); } + + async function createAppliedControlsFromSuggestions() { + const response = await fetch( + `/compliance-assessments/${data.compliance_assessment.id}/suggestions/applied-controls`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + } + ); + toastStore.trigger({ + message: response.ok + ? m.createAppliedControlsFromSuggestionsSuccess() + : m.createAppliedControlsFromSuggestionsError(), + background: response.ok ? 'variant-filled-success' : 'variant-filled-error' + }); + return; + }
@@ -343,6 +362,14 @@ > {m.mapping()} {/if} + {#if Object.hasOwn($page.data.user.permissions, 'add_appliedcontrol')} + + {/if}
{#if !$page.data.user.is_third_party} diff --git a/frontend/src/routes/(app)/(third-party)/compliance-assessments/[id=uuid]/suggestions/applied-controls/+server.ts b/frontend/src/routes/(app)/(third-party)/compliance-assessments/[id=uuid]/suggestions/applied-controls/+server.ts new file mode 100644 index 000000000..26851da6a --- /dev/null +++ b/frontend/src/routes/(app)/(third-party)/compliance-assessments/[id=uuid]/suggestions/applied-controls/+server.ts @@ -0,0 +1,28 @@ +import { BASE_API_URL } from '$lib/utils/constants'; +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async (event) => { + const requestInitOptions: RequestInit = { + method: 'POST' + }; + + const endpoint = `${BASE_API_URL}/compliance-assessments/${event.params.id}/suggestions/applied-controls/`; + const res = await event.fetch(endpoint, requestInitOptions); + + if (!res.ok) { + const response = await res.json(); + console.error(response); + return new Response(JSON.stringify(response), { + status: res.status, + headers: { + 'Content-Type': 'application/json' + } + }); + } + + return new Response(null, { + headers: { + 'Content-Type': 'application/json' + } + }); +}; From 01ea960b3e3ba5d9da5f27f1ddcb08d6da05c69a Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Wed, 25 Sep 2024 23:34:46 +0200 Subject: [PATCH 10/37] PoC: Create suggested controls from requirement assessment edit --- backend/core/urls.py | 4 ++ backend/core/views.py | 41 +++++++++++-------- .../[id=uuid]/edit/+page.svelte | 33 ++++++++++++++- .../suggestions/applied-controls/+server.ts | 28 +++++++++++++ 4 files changed, 87 insertions(+), 19 deletions(-) create mode 100644 frontend/src/routes/(app)/(internal)/requirement-assessments/[id=uuid]/suggestions/applied-controls/+server.ts diff --git a/backend/core/urls.py b/backend/core/urls.py index eb8b57965..d4e198b0a 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -90,6 +90,10 @@ ), # NOTE: This has to be placed before the allauth urls, otherwise our ACS implementation will not be used path("accounts/", include("allauth.urls")), path("_allauth/", include("allauth.headless.urls")), + path( + "requirement-assessments//suggestions/applied-controls/", + RequirementAssessmentViewSet.create_suggested_applied_controls, + ), ] # Additional modules take precedence over the default modules diff --git a/backend/core/views.py b/backend/core/views.py index cdfd27355..c4c419766 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -1,44 +1,37 @@ import csv -import importlib import mimetypes import re import tempfile import uuid import zipfile -from datetime import datetime +from datetime import date, datetime, timedelta from typing import Any, Tuple from uuid import UUID -from datetime import date, timedelta import django_filters as df -from ciso_assistant.settings import ( - BUILD, - VERSION, -) - -from django.utils.decorators import method_decorator -from django.views.decorators.cache import cache_page -from django.views.decorators.vary import vary_on_cookie, vary_on_headers -from django.core.cache import cache - +from django.conf import settings from django.contrib.auth.models import Permission +from django.core.cache import cache from django.core.files.storage import default_storage from django.db import models from django.forms import ValidationError from django.http import FileResponse, HttpResponse from django.middleware import csrf from django.template.loader import render_to_string +from django.utils.decorators import method_decorator from django.utils.functional import Promise -from django.utils import translation +from django.views.decorators.cache import cache_page +from django.views.decorators.vary import vary_on_cookie from django_filters.rest_framework import DjangoFilterBackend -from iam.models import Folder, RoleAssignment, User, UserGroup from rest_framework import filters, permissions, status, viewsets from rest_framework.decorators import ( action, api_view, permission_classes, + renderer_classes, ) from rest_framework.parsers import FileUploadParser +from rest_framework.renderers import JSONRenderer from rest_framework.request import Request from rest_framework.response import Response from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN @@ -46,21 +39,23 @@ from rest_framework.views import APIView from weasyprint import HTML +from ciso_assistant.settings import ( + BUILD, + VERSION, +) from core.helpers import * from core.models import ( AppliedControl, ComplianceAssessment, RequirementMappingSet, - ReferentialObjectMixin, ) from core.serializers import ComplianceAssessmentReadSerializer from core.utils import RoleCodename, UserGroupCodename +from iam.models import Folder, RoleAssignment, User, UserGroup from .models import * from .serializers import * -from django.conf import settings - User = get_user_model() SHORT_CACHE_TTL = 2 # mn @@ -1927,6 +1922,16 @@ def status(self, request): def result(self, request): return Response(dict(RequirementAssessment.Result.choices)) + @staticmethod + @api_view(["POST"]) + @renderer_classes([JSONRenderer]) + def create_suggested_applied_controls(request, pk): + if request.method == "POST": + requirement_assessment = RequirementAssessment.objects.get(id=pk) + requirement_assessment.create_applied_controls_from_suggestions() + return Response(status=status.HTTP_200_OK) + return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) + class RequirementMappingSetViewSet(BaseModelViewSet): model = RequirementMappingSet diff --git a/frontend/src/routes/(app)/(internal)/requirement-assessments/[id=uuid]/edit/+page.svelte b/frontend/src/routes/(app)/(internal)/requirement-assessments/[id=uuid]/edit/+page.svelte index 926160ef4..2f44a573f 100644 --- a/frontend/src/routes/(app)/(internal)/requirement-assessments/[id=uuid]/edit/+page.svelte +++ b/frontend/src/routes/(app)/(internal)/requirement-assessments/[id=uuid]/edit/+page.svelte @@ -43,6 +43,7 @@ import { getRequirementTitle } from '$lib/utils/helpers'; import { zod } from 'sveltekit-superforms/adapters'; import Question from '$lib/components/Forms/Question.svelte'; + import { invalidateAll } from '$app/navigation'; function cancel(): void { var currentUrl = window.location.href; @@ -126,6 +127,25 @@ } } + async function createAppliedControlsFromSuggestions() { + const response = await fetch( + `/requirement-assessments/${data.requirementAssessment.id}/suggestions/applied-controls`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + } + ); + toastStore.trigger({ + message: response.ok + ? m.createAppliedControlsFromSuggestionsSuccess() + : m.createAppliedControlsFromSuggestionsError(), + background: response.ok ? 'variant-filled-success' : 'variant-filled-error' + }); + invalidateAll(); + } + let { form: measureCreateForm, message: measureCreateMessage } = { form: {}, message: {} @@ -339,7 +359,18 @@
- + + {#if Object.hasOwn($page.data.user.permissions, 'add_appliedcontrol')} + + {/if} {/if} diff --git a/frontend/src/routes/(app)/(third-party)/compliance-assessments/[id=uuid]/+page.svelte b/frontend/src/routes/(app)/(third-party)/compliance-assessments/[id=uuid]/+page.svelte index cae5e9f99..ff719f9a8 100644 --- a/frontend/src/routes/(app)/(third-party)/compliance-assessments/[id=uuid]/+page.svelte +++ b/frontend/src/routes/(app)/(third-party)/compliance-assessments/[id=uuid]/+page.svelte @@ -175,7 +175,10 @@ modalStore.trigger(modal); } + $: createAppliedControlsLoading = false; + async function createAppliedControlsFromSuggestions() { + createAppliedControlsLoading = true; const response = await fetch( `/compliance-assessments/${data.compliance_assessment.id}/suggestions/applied-controls`, { @@ -185,6 +188,7 @@ } } ); + createAppliedControlsLoading = false; toastStore.trigger({ message: response.ok ? m.createAppliedControlsFromSuggestionsSuccess() @@ -366,7 +370,14 @@ {/if} From ce93394ce290d558c7b99aefe6dfbb813f7c1658 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Thu, 26 Sep 2024 17:41:47 +0200 Subject: [PATCH 21/37] chore: Run ruff format --- backend/core/tests/test_requirement_assessment.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/core/tests/test_requirement_assessment.py b/backend/core/tests/test_requirement_assessment.py index 36591f82a..ceeb844ad 100644 --- a/backend/core/tests/test_requirement_assessment.py +++ b/backend/core/tests/test_requirement_assessment.py @@ -37,9 +37,9 @@ def test_create_applied_controls_from_suggestions(self): project=Project.objects.first(), ) - requirement_assessments: list[ - RequirementAssessment - ] = compliance_assessment.create_requirement_assessments() + requirement_assessments: list[RequirementAssessment] = ( + compliance_assessment.create_requirement_assessments() + ) assert requirement_assessments is not None assert len(requirement_assessments) > 0 From 7218136d5cba2d3307426af6ba4e87c467036ae0 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Thu, 26 Sep 2024 17:43:12 +0200 Subject: [PATCH 22/37] Remove done TODO --- backend/core/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/core/models.py b/backend/core/models.py index 79ea78385..7ec691f45 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -2587,7 +2587,6 @@ def create_applied_controls_from_suggestions(self) -> list[AppliedControl]: applied_controls: list[AppliedControl] = [] for reference_control in self.requirement.reference_controls.all(): try: - # TODO: handle name unicity in scope _name = reference_control.name or reference_control.ref_id applied_control, created = AppliedControl.objects.get_or_create( name=_name, From 8426c55b8749c09403120836190f90007b0fda66 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Fri, 27 Sep 2024 12:15:36 +0200 Subject: [PATCH 23/37] Import fixtures in test_models --- backend/core/tests/test_models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/core/tests/test_models.py b/backend/core/tests/test_models.py index 1478c7cf9..215fdbb3b 100644 --- a/backend/core/tests/test_models.py +++ b/backend/core/tests/test_models.py @@ -27,6 +27,8 @@ from django.core.files.uploadedfile import SimpleUploadedFile from iam.models import Folder +from .fixtures import * + User = get_user_model() From b71d536947519fd3f9007b9b05edfd7d6d1043f1 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Fri, 27 Sep 2024 13:14:58 +0200 Subject: [PATCH 24/37] Re implement suggested applied controls creation using form action --- .../lib/components/Modals/ConfirmModal.svelte | 15 ++- .../[id=uuid]/edit/+page.server.ts | 39 ++++++++ .../[id=uuid]/edit/+page.svelte | 65 +++++++----- .../[id=uuid]/+page.server.ts | 50 +++++++++- .../[id=uuid]/+page.svelte | 99 ++++++++----------- 5 files changed, 173 insertions(+), 95 deletions(-) diff --git a/frontend/src/lib/components/Modals/ConfirmModal.svelte b/frontend/src/lib/components/Modals/ConfirmModal.svelte index 788a68b33..6e225e710 100644 --- a/frontend/src/lib/components/Modals/ConfirmModal.svelte +++ b/frontend/src/lib/components/Modals/ConfirmModal.svelte @@ -19,7 +19,12 @@ export let formAction: string; import { superForm } from 'sveltekit-superforms'; - const { form /*, message*/, enhance } = superForm(_form); + import SuperForm from '$lib/components/Forms/Form.svelte'; + + const { form, enhance } = superForm(_form, { + dataType: 'json', + id: 'confirm-modal-form' + }); // Base Classes const cBase = 'card p-4 w-modal shadow-xl space-y-4'; @@ -35,15 +40,15 @@
{$modalStore[0].title ?? '(title missing)'}
{$modalStore[0].body ?? '(body missing)'}
- {#if debug === true} {/if} diff --git a/frontend/src/routes/(app)/(internal)/requirement-assessments/[id=uuid]/edit/+page.server.ts b/frontend/src/routes/(app)/(internal)/requirement-assessments/[id=uuid]/edit/+page.server.ts index b8aef03fc..a6f9d3e3f 100644 --- a/frontend/src/routes/(app)/(internal)/requirement-assessments/[id=uuid]/edit/+page.server.ts +++ b/frontend/src/routes/(app)/(internal)/requirement-assessments/[id=uuid]/edit/+page.server.ts @@ -13,6 +13,7 @@ import { setFlash } from 'sveltekit-flash-message/server'; import { setError, superValidate } from 'sveltekit-superforms'; import { zod } from 'sveltekit-superforms/adapters'; import type { PageServerLoad } from './$types'; +import { z } from 'zod'; export const load = (async ({ fetch, params }) => { const URLModel = 'requirement-assessments'; @@ -295,5 +296,43 @@ export const actions: Actions = { }, createEvidence: async (event) => { return nestedWriteFormAction({ event, action: 'create' }); + }, + createSuggestedControls: async (event) => { + const formData = await event.request.formData(); + + if (!formData) { + return fail(400, { form: null }); + } + + const schema = z.object({ id: z.string().uuid() }); + const form = await superValidate(formData, zod(schema)); + + const response = await event.fetch( + `/requirement-assessments/${event.params.id}/suggestions/applied-controls`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + } + ); + if (response.ok) { + setFlash( + { + type: 'success', + message: m.createAppliedControlsFromSuggestionsSuccess() + }, + event + ); + } else { + setFlash( + { + type: 'error', + message: m.createAppliedControlsFromSuggestionsError() + }, + event + ); + } + return { form }; } }; diff --git a/frontend/src/routes/(app)/(internal)/requirement-assessments/[id=uuid]/edit/+page.svelte b/frontend/src/routes/(app)/(internal)/requirement-assessments/[id=uuid]/edit/+page.svelte index acd0f88c9..c9406bf22 100644 --- a/frontend/src/routes/(app)/(internal)/requirement-assessments/[id=uuid]/edit/+page.svelte +++ b/frontend/src/routes/(app)/(internal)/requirement-assessments/[id=uuid]/edit/+page.svelte @@ -1,9 +1,11 @@
@@ -362,14 +335,20 @@ {#if !$page.data.user.is_third_party} {/if} {#if Object.hasOwn($page.data.user.permissions, 'add_appliedcontrol')} {/if} {/if}
From 3f063c236eaf1710ec40ef4b77aa50699f9be6af Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Fri, 27 Sep 2024 20:52:04 +0200 Subject: [PATCH 27/37] Allow passing a component as prop to ConfirmModal component --- frontend/src/lib/components/Modals/ConfirmModal.svelte | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/src/lib/components/Modals/ConfirmModal.svelte b/frontend/src/lib/components/Modals/ConfirmModal.svelte index 7653f26af..3ba8f0f38 100644 --- a/frontend/src/lib/components/Modals/ConfirmModal.svelte +++ b/frontend/src/lib/components/Modals/ConfirmModal.svelte @@ -17,6 +17,9 @@ export let URLModel: urlModel; export let id: string; export let formAction: string; + export let bodyComponent: ComponentType | undefined; + export let bodyProps: Record = {}; + import { superForm } from 'sveltekit-superforms'; import SuperForm from '$lib/components/Forms/Form.svelte'; @@ -32,6 +35,7 @@ const cForm = 'p-4 space-y-4 rounded-container-token'; import SuperDebug from 'sveltekit-superforms'; + import type { ComponentType } from 'svelte'; export let debug = false; @@ -39,6 +43,11 @@ {#if !$page.data.user.is_third_party}
-

- {m.associatedRequirements()} +
+ {m.associatedRequirements()} {assessableNodesCount(treeViewNodes)} -

- -
- {#if $displayOnlyAssessableNodes} -

{m.ShowAllNodesMessage()}

- {:else} -

{m.ShowAllNodesMessage()}

- {/if} - ($displayOnlyAssessableNodes = !$displayOnlyAssessableNodes)} - > +
{#if $displayOnlyAssessableNodes} -

{m.ShowOnlyAssessable()}

+

{m.ShowAllNodesMessage()}

{:else} -

{m.ShowOnlyAssessable()}

+

{m.ShowAllNodesMessage()}

{/if} - + ($displayOnlyAssessableNodes = !$displayOnlyAssessableNodes)} + > + {#if $displayOnlyAssessableNodes} +

{m.ShowOnlyAssessable()}

+ {:else} +

{m.ShowOnlyAssessable()}

+ {/if} +
+
+

{m.mappingInferenceTip()}