Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add applied control duplication #940

Merged
merged 30 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6c96748
Add applied control duplication
monsieurswag Oct 15, 2024
aefec25
fix: add missing import
Mohamed-Hacene Oct 15, 2024
fc7135b
Formatter
monsieurswag Oct 15, 2024
8d22523
Fix missing fetch for model foreign keys
monsieurswag Oct 16, 2024
af3e7c9
Add an option to bring the evidences with the duplicated applied control
monsieurswag Oct 16, 2024
526d867
Merge branch 'main' into CA-503-Duplicate-applied-controls
monsieurswag Oct 16, 2024
a3df53d
Merge branch 'main' into CA-503-Duplicate-applied-controls
monsieurswag Oct 17, 2024
da4e076
Fix conflicts
monsieurswag Oct 21, 2024
edecfa0
Fix missing edit button
monsieurswag Oct 21, 2024
60c091d
Codefactor fix 1
monsieurswag Oct 21, 2024
6a78979
Codefactor fix 2
monsieurswag Oct 21, 2024
dc80e5b
Formatter
monsieurswag Oct 21, 2024
b6cf0f9
Remove dead code
monsieurswag Oct 21, 2024
6ccdfbe
Remove useless comments
monsieurswag Oct 23, 2024
e9ce72e
Merge branch 'main' into CA-503-Duplicate-applied-controls
monsieurswag Nov 12, 2024
e391800
Attempt to fix startup tests
monsieurswag Nov 12, 2024
f765433
Fix conflicts
monsieurswag Nov 26, 2024
4cf0ec0
Fix conflicts
monsieurswag Nov 26, 2024
07d09e2
Light optimization
monsieurswag Nov 26, 2024
2cd22e5
Fix non-working duplicate modal and remove 2 useless values from the …
monsieurswag Nov 27, 2024
4904783
Set the default translations
monsieurswag Nov 27, 2024
3e72c9e
chore: update translations with Fink 🐦
monsieurswag Nov 27, 2024
0756134
Fix conflicts
monsieurswag Dec 3, 2024
2d6595d
Merge branch 'main' into CA-503-Duplicate-applied-controls
monsieurswag Dec 4, 2024
40cd65a
Also copy ref_id when duplicating an AppliedControl
monsieurswag Dec 4, 2024
b413b5f
Fix conflicts
monsieurswag Dec 4, 2024
841e781
Formatter
monsieurswag Dec 4, 2024
0acd167
Copy owner when duplicating AppliedControl
monsieurswag Dec 4, 2024
9a555ff
Fix wrong assignment to AppliedControl.owner ManyToManyField
monsieurswag Dec 4, 2024
a9fc211
Merge branch 'main' into CA-503-Duplicate-applied-controls
monsieurswag Dec 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1403,9 +1403,6 @@ def risk_assessments(self):
def projects(self):
return {risk_assessment.project for risk_assessment in self.risk_assessments}

def parent_project(self):
pass

def __str__(self):
return self.name

Expand Down
6 changes: 6 additions & 0 deletions backend/core/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,12 @@ class AppliedControlReadSerializer(AppliedControlWriteSerializer):
eta_missed = serializers.BooleanField()


class AppliedControlDuplicateSerializer(BaseModelSerializer):
class Meta:
model = AppliedControl
fields = ["name", "description", "folder"]


class PolicyWriteSerializer(AppliedControlWriteSerializer):
class Meta:
model = Policy
Expand Down
47 changes: 46 additions & 1 deletion backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@
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
from rest_framework.status import (
HTTP_400_BAD_REQUEST,
HTTP_403_FORBIDDEN,
HTTP_404_NOT_FOUND,
)
from rest_framework.utils.serializer_helpers import ReturnDict
from rest_framework.views import APIView

Expand Down Expand Up @@ -1039,6 +1043,47 @@ def get_timeline_info(self, request):
colorMap[domain.name] = next(color_cycle)
return Response({"entries": entries, "colorMap": colorMap})

@action(
detail=True,
name="Duplicate applied control",
methods=["post"],
serializer_class=AppliedControlDuplicateSerializer,
)
def duplicate(self, request, pk):
(object_ids_view, _, _) = RoleAssignment.get_accessible_object_ids(
Folder.get_root_folder(), request.user, AppliedControl
)
if UUID(pk) not in object_ids_view:
return Response(
{"results": "applied control duplicated"}, status=HTTP_404_NOT_FOUND
)

applied_control = self.get_object()
data = request.data
new_folder = Folder.objects.get(id=data["folder"])
duplicate_applied_control = AppliedControl.objects.create(
reference_control=applied_control.reference_control,
name=data["name"],
description=data["description"],
folder=new_folder,
category=applied_control.category,
csf_function=applied_control.csf_function,
status=applied_control.status,
start_date=applied_control.start_date,
eta=applied_control.eta,
expiry_date=applied_control.expiry_date,
link=applied_control.link,
effort=applied_control.effort,
cost=applied_control.cost,
)
if data["duplicate_evidences"]:
duplicate_related_objects(
applied_control, duplicate_applied_control, new_folder, "evidences"
)
duplicate_applied_control.save()

return Response({"results": "applied control duplicated"})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's return the actual controls as JSON



class PolicyViewSet(AppliedControlViewSet):
model = Policy
Expand Down
34 changes: 16 additions & 18 deletions backend/iam/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Inspired from Azure IAM model"""

from collections import defaultdict
from typing import Any, List, Self, Tuple
from typing import Any, List, Self, Tuple, Generator
import uuid
from allauth.account.models import EmailAddress
from django.utils import timezone
Expand Down Expand Up @@ -100,24 +100,22 @@ class Meta:
def __str__(self) -> str:
return self.name.__str__()

def sub_folders(self) -> List[Self]:
def get_sub_folders(self) -> Generator[Self, None, None]:
"""Return the list of subfolders"""

def sub_folders_in(f, sub_folder_list):
for sub_folder in f.folder_set.all():
sub_folder_list.append(sub_folder)
sub_folders_in(sub_folder, sub_folder_list)
return sub_folder_list
def sub_folders_in(folder):
for sub_folder in folder.folder_set.all():
yield sub_folder
yield from sub_folders_in(sub_folder)

return sub_folders_in(self, [])
yield from sub_folders_in(self)

def get_parent_folders(self) -> List[Self]:
# Should we update data-model.md now that this method is a generator ?
def get_parent_folders(self) -> Generator[Self, None, None]:
"""Return the list of parent folders"""
return (
[self.parent_folder] + Folder.get_parent_folders(self.parent_folder)
if self.parent_folder
else []
)
current_folder = self
while (current_folder := current_folder.parent_folder) is not None:
yield current_folder

@staticmethod
def _navigate_structure(start, path):
Expand Down Expand Up @@ -651,11 +649,11 @@ def get_accessible_folders(
]:
for f in ra.perimeter_folders.all():
folders_set.add(f)
folders_set.update(f.sub_folders())
folders_set.update(f.get_sub_folders())
# calculate perimeter
perimeter = set()
perimeter.add(folder)
perimeter.update(folder.sub_folders())
perimeter.update(folder.get_sub_folders())
# return filtered result
return [
x.id
Expand Down Expand Up @@ -692,7 +690,7 @@ def get_accessible_object_ids(
folder_for_object = {x: Folder.get_folder(x) for x in all_objects}
perimeter = set()
perimeter.add(folder)
perimeter.update(folder.sub_folders())
perimeter.update(folder.get_sub_folders())
for ra in [
x
for x in RoleAssignment.get_role_assignments(user)
Expand All @@ -701,7 +699,7 @@ def get_accessible_object_ids(
ra_permissions = ra.role.permissions.all()
for my_folder in perimeter & set(ra.perimeter_folders.all()):
target_folders = (
[my_folder] + my_folder.sub_folders()
[my_folder, *my_folder.get_sub_folders()]
if ra.is_recursive
else [my_folder]
)
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,7 @@
"ssoSettingsDescription": "قم بتكوين إعدادات تسجيل الدخول الموحد هنا.",
"sso": "SSO",
"isSso": "هل هو SSO",
"duplicateAppliedControl": "تكرار عنصر التحكم المطبق",
"suggestion": "اقتراح",
"suggestionColon": "اقتراح:",
"annotationColon": "ملاحظة:",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@
"back": "Zurückkehren",
"duplicate": "Duplikat",
"duplicateRiskAssessment": "Duplizieren Sie die Risikobewertung",
"duplicateAppliedControl": "Duplizieren Sie das angewendete kontrolle",
"size": "Größe",
"favicon": "Favicon",
"logo": "Logo",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@
"back": "Back",
"duplicate": "Duplicate",
"duplicateRiskAssessment": "Duplicate the risk assessment",
"duplicateAppliedControl": "Duplicate the applied control",
"size": "Size",
"favicon": "Favicon",
"logo": "Logo",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@
"back": "Devolver",
"duplicate": "Duplicar",
"duplicateRiskAssessment": "Duplicar la evaluación de riesgo",
"duplicateAppliedControl": "Duplicar el control aplicado",
"size": "Tamaño",
"favicon": "Favicon",
"logo": "Logo",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@
"back": "Retour",
"duplicate": "Dupliquer",
"duplicateRiskAssessment": "Dupliquer l’évaluation de risque",
"duplicateAppliedControl": "Dupliquer la mesure appliquée",
"size": "Taille",
"favicon": "Icône de favori",
"logo": "Logo",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@
"back": "वापस",
"duplicate": "प्रतिलिपि",
"duplicateRiskAssessment": "जोखिम आकलन की प्रतिलिपि बनाएँ",
"duplicateAppliedControl": "लागू नियंत्रण की प्रतिलिपि बनाएँ",
"size": "आकार",
"favicon": "फ़ेविकॉन",
"logo": "प्रतीक चिन्ह",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@
"back": "Ripetere",
"duplicate": "Duplicare",
"duplicateRiskAssessment": "Duplicare la valutazione del rischio",
"duplicateAppliedControl": "Duplica il controllo applicato",
"size": "Dimensione",
"favicon": "Icona preferita",
"logo": "Logo",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@
"back": "Opbrengst",
"duplicate": "Duplicaat",
"duplicateRiskAssessment": "Dupliceer de risicobeoordeling",
"duplicateAppliedControl": "Dupliceer de toegepaste controle",
"size": "Grootte",
"favicon": "Favorieten",
"logo": "Logo",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@
"back": "Powrót",
"duplicate": "Duplikować",
"duplicateRiskAssessment": "Powielić ocenę ryzyka",
"duplicateAppliedControl": "Duplikuj zastosowaną kontrolę",
"size": "Rozmiar",
"favicon": "Favicon",
"logo": "Logo",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@
"back": "Retornar",
"duplicate": "Duplicado",
"duplicateRiskAssessment": "Duplicar a avaliação de risco",
"duplicateAppliedControl": "Duplicar o controle aplicado",
"size": "Tamanho",
"favicon": "Favicon",
"logo": "Logotipo",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/ro.json
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@
"back": "Înapoi",
"duplicate": "Dublică",
"duplicateRiskAssessment": "Dublarea evaluării riscului",
"duplicateAppliedControl": "Duplicați control aplicat",
"size": "Mărime",
"favicon": "Favicon",
"logo": "Logo",
Expand Down
1 change: 1 addition & 0 deletions frontend/messages/ur.json
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@
"back": "واپس",
"duplicate": "نقل کریں",
"duplicateRiskAssessment": "خطرے کی تشخیص کو نقل کریں",
"duplicateAppliedControl": "لاگو کنٹرول کی نقل تیار کریں۔",
"size": "سائز",
"favicon": "فیویکان",
"logo": "لوگو",
Expand Down
43 changes: 38 additions & 5 deletions frontend/src/lib/components/DetailView/DetailView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,26 @@
modalStore.trigger(modal);
}

function modalAppliedControlDuplicateForm(): void {
const modalComponent: ModalComponent = {
ref: CreateModal,
props: {
form: data.duplicateForm,
model: data.model,
debug: false,
duplicate: true,
formAction: '?/duplicate'
}
};

const modal: ModalSettings = {
type: 'component',
component: modalComponent,
title: m.duplicateAppliedControl()
};
modalStore.trigger(modal);
}

function modalMailConfirm(id: string, name: string, action: string): void {
const modalComponent: ModalComponent = {
ref: ConfirmModal,
Expand Down Expand Up @@ -315,11 +335,24 @@
</button>
{/if}
{#if displayEditButton()}
<a
href={`${$page.url.pathname}/edit?next=${$page.url.pathname}`}
class="btn variant-filled-primary h-fit"
><i class="fa-solid fa-pen-to-square mr-2" data-testid="edit-button" />{m.edit()}</a
>
<div class="flex flex-col space-y-2 ml-4">
<a
href={`${$page.url.pathname}/edit?next=${$page.url.pathname}`}
class="btn variant-filled-primary h-fit"
><i class="fa-solid fa-pen-to-square mr-2" data-testid="edit-button" />{m.edit()}</a
>
{#if data.urlModel === 'applied-controls'}
<span class="pt-4 font-light text-sm">Power-ups:</span>
<button
class="btn text-gray-100 bg-gradient-to-l from-sky-500 to-green-600"
on:click={(_) => modalAppliedControlDuplicateForm()}
data-testid="duplicate-button"
>
<i class="fa-solid fa-copy mr-2"></i>
{m.duplicate()}</button
>
{/if}
</div>
{/if}
</div>
</div>
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/lib/components/Forms/ModelForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
export let parent: any;
export let suggestions: { [key: string]: any } = {};
export let cancelButton = true;
export let riskAssessmentDuplication = false;
export let duplicate = false;

const URLModel = model.urlModel as urlModel;
export let schema = modelSchema(URLModel);
Expand Down Expand Up @@ -121,7 +121,7 @@
<input type="hidden" name="urlmodel" value={model.urlModel} />
<!--NOTE: Not the cleanest pattern, will refactor-->
<!--TODO: Refactor-->
{#if shape.reference_control}
{#if shape.reference_control && !duplicate}
<AutocompleteSelect
{form}
options={getOptions({
Expand Down Expand Up @@ -180,11 +180,11 @@
<ProjectForm {form} {model} {cacheLocks} {formDataCache} {initialData} />
{:else if URLModel === 'folders'}
<FolderForm {form} {model} {cacheLocks} {formDataCache} {initialData} />
{:else if URLModel === 'risk-assessments' || URLModel === 'risk-assessment-duplicate'}
{:else if URLModel === 'risk-assessments'}
<RiskAssessmentForm
{form}
{model}
{riskAssessmentDuplication}
{duplicate}
{cacheLocks}
{formDataCache}
{initialData}
Expand All @@ -200,6 +200,7 @@
<AppliedControlsPoliciesForm
{form}
{model}
{duplicate}
{cacheLocks}
{formDataCache}
{schema}
Expand Down
Loading
Loading