Skip to content

Commit

Permalink
Add an option to bring the evidences with the duplicated applied control
Browse files Browse the repository at this point in the history
  • Loading branch information
monsieurswag committed Oct 16, 2024
1 parent 8d22523 commit af3e7c9
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 58 deletions.
11 changes: 11 additions & 0 deletions backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1248,6 +1248,17 @@ def get_size(self):
else:
return f"{size / 1024 / 1024:.1f} MB"

def duplicate_into_folder(self, folder: Folder) -> Self:
duplicated_evidence = Evidence(
folder=folder,
name=self.name,
description=self.description,
attachment=self.attachment,
link=self.link,
)
duplicated_evidence.save()
return duplicated_evidence


class AppliedControl(NameDescriptionMixin, FolderMixin, PublishInRootFolderMixin):
class Status(models.TextChoices):
Expand Down
18 changes: 12 additions & 6 deletions backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -927,11 +927,12 @@ def duplicate(self, request, pk):
if UUID(pk) in object_ids_view:
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=Folder.objects.get(id=data["folder"]),
folder=new_folder,
category=applied_control.category,
csf_function=applied_control.csf_function,
status=applied_control.status,
Expand All @@ -942,11 +943,16 @@ def duplicate(self, request, pk):
effort=applied_control.effort,
cost=applied_control.cost,
)
# Should we duplicate the owners and evidences ?
# duplicate_applied_control.owner.set(applied_control.owner.all())
# The evidences must be cloned before being linked to the applied_control if they are not in the same scope (an applied_control with a scope FOLDER1/PROJECT1 must have evidences into FOLDER1/PROJECT1)
# duplicate_applied_control.evidences.set(applied_control.evidences.all())
# duplicate_applied_control.save() # This line must be used if one of the ManyToManyField of the applied_control is modified during this function execution.
if request.data["duplicate_evidences"]:
for evidence in applied_control.evidences.all():
# Evidences will only be duplicated if necessary
if new_folder.can_access(evidence.folder):
duplicate_applied_control.evidences.add(evidence)
else:
new_evidence = evidence.duplicate_into_folder(new_folder)
duplicate_applied_control.evidences.add(new_evidence)

duplicate_applied_control.save()

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

Expand Down
65 changes: 37 additions & 28 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 django.utils import timezone
from django.db import models
Expand Down Expand Up @@ -99,23 +99,31 @@ 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"""
current_folder = self
while (current_folder := current_folder.parent_folder) is not None:
yield current_folder

# Is this function usefull ?
def can_access(self, folder: Self) -> bool:
return (
[self.parent_folder] + Folder.get_parent_folders(self.parent_folder)
if self.parent_folder
else []
self == folder
or any(
folder == parent_folder for parent_folder in self.get_parent_folders()
)
or any(folder == sub_folder for sub_folder in self.get_sub_folders())
)

@staticmethod
Expand Down Expand Up @@ -635,11 +643,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 @@ -676,7 +684,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 @@ -685,7 +693,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 All @@ -705,18 +713,19 @@ def get_accessible_object_ids(

if hasattr(object_type, "is_published"):
for my_folder in folders_with_local_view:
target_folders = []
my_folder2 = my_folder
while my_folder2:
if my_folder2 != my_folder:
target_folders.append(my_folder2)
my_folder2 = my_folder2.parent_folder
for object in [
x
for x in all_objects
if folder_for_object[x] in target_folders and x.is_published
]:
permissions_per_object_id[object.id].add(permissions[0])
if my_folder.content_type != Folder.ContentType.ENCLAVE:
target_folders = []
my_folder2 = my_folder
while my_folder2:
if my_folder2 != my_folder:
target_folders.append(my_folder2)
my_folder2 = my_folder2.parent_folder
for object in [
x
for x in all_objects
if folder_for_object[x] in target_folders and x.is_published
]:
permissions_per_object_id[object.id].add(permissions[0])

return (
[
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import AutocompleteSelect from '../AutocompleteSelect.svelte';
import Select from '../Select.svelte';
import Checkbox from '$lib/components/Forms/Checkbox.svelte';
import TextField from '$lib/components/Forms/TextField.svelte';
import NumberField from '$lib/components/Forms/NumberField.svelte';
import { getOptions } from '$lib/utils/crud';
Expand Down Expand Up @@ -118,6 +119,19 @@
bind:cachedValue={formDataCache['cost']}
/>
{/if}

{#if duplicate}
<!-- We must set the right translation for this checkbox -->
<!-- Duplicate the evidences -->
<!-- If disabled, the applied control will be duplicated without its evidences -->
<Checkbox
{form}
field="duplicate_evidences"
label={m.showImagesUnauthenticated()}
helpText={m.showImagesUnauthenticatedHelpText()}
/>
{/if}

<AutocompleteSelect
{form}
options={getOptions({ objects: model.foreignKeys['folder'] })}
Expand Down
66 changes: 43 additions & 23 deletions frontend/src/lib/utils/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,25 @@ const nameSchema = z

const descriptionSchema = z.string().optional().nullable();

const baseNamedObject = (additionalFields: any) =>
z.object({
name: nameSchema,
description: descriptionSchema,
...additionalFields
});

export const FolderSchema = baseNamedObject({
const NameDescriptionMixin = {
name: nameSchema,
description: descriptionSchema
};

export const FolderSchema = z.object({
...NameDescriptionMixin,
parent_folder: z.string().optional()
});

export const ProjectSchema = baseNamedObject({
export const ProjectSchema = z.object({
...NameDescriptionMixin,
folder: z.string(),
internal_reference: z.string().optional().nullable(),
lc_status: z.string().optional().default('in_design')
});

export const RiskMatrixSchema = baseNamedObject({
export const RiskMatrixSchema = z.object({
...NameDescriptionMixin,
folder: z.string(),
json_definition: z.string(),
is_enabled: z.boolean()
Expand All @@ -83,7 +84,8 @@ export const LibraryUploadSchema = z.object({
file: z.instanceof(File).optional()
});

export const RiskAssessmentSchema = baseNamedObject({
export const RiskAssessmentSchema = z.object({
...NameDescriptionMixin,
version: z.string().optional().default('0.1'),
project: z.string(),
status: z.string().optional().nullable(),
Expand All @@ -95,14 +97,16 @@ export const RiskAssessmentSchema = baseNamedObject({
observation: z.string().optional().nullable()
});

export const ThreatSchema = baseNamedObject({
export const ThreatSchema = z.object({
...NameDescriptionMixin,
folder: z.string(),
provider: z.string().optional().nullable(),
ref_id: z.string().optional().nullable(),
annotation: z.string().optional().nullable()
});

export const RiskScenarioSchema = baseNamedObject({
export const RiskScenarioSchema = z.object({
...NameDescriptionMixin,
existing_controls: z.string().optional(),
applied_controls: z.string().uuid().optional().array().optional(),
current_proba: z.number().optional(),
Expand All @@ -119,7 +123,8 @@ export const RiskScenarioSchema = baseNamedObject({
owner: z.string().uuid().optional().array().optional()
});

export const AppliedControlSchema = baseNamedObject({
export const AppliedControlSchema = z.object({
...NameDescriptionMixin,
category: z.string().optional().nullable(),
csf_function: z.string().optional().nullable(),
status: z.string().optional().default('--'),
Expand All @@ -135,7 +140,13 @@ export const AppliedControlSchema = baseNamedObject({
owner: z.string().uuid().optional().array().optional()
});

export const PolicySchema = baseNamedObject({
export const AppliedControlDuplicateSchema = z.object({
...AppliedControlSchema.shape,
duplicate_evidences: z.boolean()
});

export const PolicySchema = z.object({
...NameDescriptionMixin,
csf_function: z.string().optional().nullable(),
status: z.string().optional().default('--'),
evidences: z.string().optional().array().optional(),
Expand All @@ -147,15 +158,17 @@ export const PolicySchema = baseNamedObject({
reference_control: z.string().optional().nullable()
});

export const RiskAcceptanceSchema = baseNamedObject({
export const RiskAcceptanceSchema = z.object({
...NameDescriptionMixin,
folder: z.string(),
expiry_date: z.string().optional().nullable(),
justification: z.string().optional().nullable(),
approver: z.string(),
risk_scenarios: z.array(z.string())
});

export const ReferenceControlSchema = baseNamedObject({
export const ReferenceControlSchema = z.object({
...NameDescriptionMixin,
provider: z.string().optional().nullable(),
category: z.string().optional().nullable(),
csf_function: z.string().optional().nullable(),
Expand All @@ -164,7 +177,8 @@ export const ReferenceControlSchema = baseNamedObject({
annotation: z.string().optional().nullable()
});

export const AssetSchema = baseNamedObject({
export const AssetSchema = z.object({
...NameDescriptionMixin,
business_value: z.string().optional(),
type: z.string().default('PR'),
folder: z.string(),
Expand Down Expand Up @@ -212,7 +226,8 @@ export const SetPasswordSchema = z.object({
confirm_new_password: z.string()
});

export const ComplianceAssessmentSchema = baseNamedObject({
export const ComplianceAssessmentSchema = z.object({
...NameDescriptionMixin,
version: z.string().optional().default('0.1'),
project: z.string(),
status: z.string().optional().nullable(),
Expand All @@ -227,7 +242,8 @@ export const ComplianceAssessmentSchema = baseNamedObject({
observation: z.string().optional().nullable()
});

export const EvidenceSchema = baseNamedObject({
export const EvidenceSchema = z.object({
...NameDescriptionMixin,
attachment: z.any().optional().nullable(),
folder: z.string(),
applied_controls: z.preprocess(toArrayPreprocessor, z.array(z.string().optional())).optional(),
Expand Down Expand Up @@ -279,13 +295,15 @@ export const SSOSettingsSchema = z.object({
want_name_id_encrypted: z.boolean().optional().nullable()
});

export const EntitiesSchema = baseNamedObject({
export const EntitiesSchema = z.object({
...NameDescriptionMixin,
folder: z.string(),
mission: z.string().optional(),
reference_link: z.string().optional()
});

export const EntityAssessmentSchema = baseNamedObject({
export const EntityAssessmentSchema = z.object({
...NameDescriptionMixin,
create_audit: z.boolean().optional().default(false),
framework: z.string().optional(),
selected_implementation_groups: z.array(z.string().optional()).optional(),
Expand All @@ -310,7 +328,8 @@ export const EntityAssessmentSchema = baseNamedObject({
observation: z.string().optional().nullable()
});

export const solutionSchema = baseNamedObject({
export const solutionSchema = z.object({
...NameDescriptionMixin,
provider_entity: z.string(),
ref_id: z.string().optional(),
criticality: z.number().optional()
Expand All @@ -335,6 +354,7 @@ const SCHEMA_MAP: Record<string, AnyZodObject> = {
threats: ThreatSchema,
'risk-scenarios': RiskScenarioSchema,
'applied-controls': AppliedControlSchema,
'applied-controls_duplicate': AppliedControlDuplicateSchema,
policies: PolicySchema,
'risk-acceptances': RiskAcceptanceSchema,
'reference-controls': ReferenceControlSchema,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const actions: Actions = {

if (!formData) return;

const schema = modelSchema(event.params.model as string);
const schema = modelSchema((event.params.model + '_duplicate') as string);

const form = await superValidate(formData, zod(schema));

Expand Down

0 comments on commit af3e7c9

Please sign in to comment.