From c2ea717b6494dedc642eac4eac00aa7d17a43509 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Thu, 29 Feb 2024 15:28:55 +0100 Subject: [PATCH 1/9] Stream tree promise --- .../(app)/libraries/[id=urn]/+layout.server.ts | 9 ++++----- .../src/routes/(app)/libraries/[id=urn]/+page.svelte | 12 ++++++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/frontend/src/routes/(app)/libraries/[id=urn]/+layout.server.ts b/frontend/src/routes/(app)/libraries/[id=urn]/+layout.server.ts index 2dc014d4b..86d467871 100644 --- a/frontend/src/routes/(app)/libraries/[id=urn]/+layout.server.ts +++ b/frontend/src/routes/(app)/libraries/[id=urn]/+layout.server.ts @@ -5,9 +5,8 @@ import type { LayoutServerLoad } from './$types'; export const load: LayoutServerLoad = async ({ fetch, params }) => { const endpoint = `${BASE_API_URL}/libraries/${params.id}/`; - const library = await fetch(endpoint).then((res) => res.json()); - const tree = - (await fetch(`${BASE_API_URL}/libraries/${params.id}/tree`).then((res) => res.json())) ?? {}; - - return { library, tree }; + return { + tree: fetch(`${BASE_API_URL}/libraries/${params.id}/tree`).then((res) => res.json()) ?? {}, + library: await fetch(endpoint).then((res) => res.json()) + }; }; diff --git a/frontend/src/routes/(app)/libraries/[id=urn]/+page.svelte b/frontend/src/routes/(app)/libraries/[id=urn]/+page.svelte index b39359b68..15d4238d5 100644 --- a/frontend/src/routes/(app)/libraries/[id=urn]/+page.svelte +++ b/frontend/src/routes/(app)/libraries/[id=urn]/+page.svelte @@ -18,8 +18,6 @@ const threats = libraryObjects['threats'] ?? []; const framework = libraryObjects['framework']; - const tree = data.tree; - function transformToTreeView(nodes) { return nodes.map(([id, node]) => { return { @@ -30,7 +28,6 @@ }; }); } - let treeViewNodes: TreeViewNode[] = transformToTreeView(Object.entries(tree)); import { ProgressRadial, tableSourceMapper, type TreeViewNode } from '@skeletonlabs/skeleton'; import RecursiveTreeView from '$lib/components/TreeView/RecursiveTreeView.svelte'; @@ -157,6 +154,13 @@ {#if framework}

{m.framework()}

- + {#await data.tree} + loading... + {:then tree} + + {/await} {/if} From a662a543eeb32766c15caffc8d3e7cfca49607f5 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Fri, 1 Mar 2024 17:20:41 +0100 Subject: [PATCH 2/9] Stream library load function --- .../routes/(app)/libraries/[id=urn]/+page.ts | 10 ++++++++ .../(app)/libraries/[id=urn]/+server.ts | 23 +++++++++++++++++++ .../(app)/libraries/[id=urn]/tree/+server.ts | 23 +++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 frontend/src/routes/(app)/libraries/[id=urn]/+page.ts create mode 100644 frontend/src/routes/(app)/libraries/[id=urn]/+server.ts create mode 100644 frontend/src/routes/(app)/libraries/[id=urn]/tree/+server.ts diff --git a/frontend/src/routes/(app)/libraries/[id=urn]/+page.ts b/frontend/src/routes/(app)/libraries/[id=urn]/+page.ts new file mode 100644 index 000000000..b1756f740 --- /dev/null +++ b/frontend/src/routes/(app)/libraries/[id=urn]/+page.ts @@ -0,0 +1,10 @@ +import type { PageLoad } from './$types'; + +export const load: PageLoad = async ({ fetch, params }) => { + const endpoint = `/libraries/${params.id}`; + + return { + tree: fetch(`/libraries/${params.id}/tree`).then((res) => res.json()) ?? {}, + library: await fetch(endpoint).then((res) => res.json()) + }; +}; diff --git a/frontend/src/routes/(app)/libraries/[id=urn]/+server.ts b/frontend/src/routes/(app)/libraries/[id=urn]/+server.ts new file mode 100644 index 000000000..4411bbf10 --- /dev/null +++ b/frontend/src/routes/(app)/libraries/[id=urn]/+server.ts @@ -0,0 +1,23 @@ +import { BASE_API_URL } from '$lib/utils/constants'; + +import { error, type NumericRange } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ fetch, url }) => { + console.log(url); + const endpoint = `${BASE_API_URL}${url.pathname}/${ + url.searchParams ? '?' + url.searchParams.toString() : '' + }`; + + const res = await fetch(endpoint); + if (!res.ok) { + error(res.status as NumericRange<400, 599>, await res.json()); + } + const data = await res.json(); + + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json' + } + }); +}; diff --git a/frontend/src/routes/(app)/libraries/[id=urn]/tree/+server.ts b/frontend/src/routes/(app)/libraries/[id=urn]/tree/+server.ts new file mode 100644 index 000000000..4411bbf10 --- /dev/null +++ b/frontend/src/routes/(app)/libraries/[id=urn]/tree/+server.ts @@ -0,0 +1,23 @@ +import { BASE_API_URL } from '$lib/utils/constants'; + +import { error, type NumericRange } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ fetch, url }) => { + console.log(url); + const endpoint = `${BASE_API_URL}${url.pathname}/${ + url.searchParams ? '?' + url.searchParams.toString() : '' + }`; + + const res = await fetch(endpoint); + if (!res.ok) { + error(res.status as NumericRange<400, 599>, await res.json()); + } + const data = await res.json(); + + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json' + } + }); +}; From d6764748e058789a18dc8bcb1bd28603d7e288ca Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Fri, 1 Mar 2024 21:11:58 +0100 Subject: [PATCH 3/9] Refactor requirement tree build --- backend/core/helpers.py | 103 +++++++++++++++------------------------- 1 file changed, 39 insertions(+), 64 deletions(-) diff --git a/backend/core/helpers.py b/backend/core/helpers.py index 99e5a1d74..ce3042311 100644 --- a/backend/core/helpers.py +++ b/backend/core/helpers.py @@ -188,98 +188,73 @@ def _get_all_requirement_nodes_id_in_requirement_node( def get_sorted_requirement_nodes( - requirement_nodes: list, - requirements_assessed: list | None, + requirement_nodes: list, requirements_assessed: list | None ) -> dict: """ - Recursive function to build framework groups tree - requirement_nodes: the list of all requirement_nodes - requirements_assessed: the list of all requirements_assessed - Returns a dictionary containing key=name and value={"description": description, "style": "leaf|node"}} + Optimized function to build a framework groups tree. """ - requirement_assessment_from_requirement_id = ( + # Preprocess requirements_assessed for O(1) lookup. + ra_dict = ( {str(ra.requirement.id): ra for ra in requirements_assessed} if requirements_assessed else {} ) - def get_sorted_requirement_nodes_rec( - requirement_nodes: list, - requirements_assessed: list, - start: list, - ) -> dict: + # Preprocess requirement_nodes into a dict by parent_urn for O(1) child lookup. + children_dict = {} + for node in requirement_nodes: + if node.parent_urn not in children_dict: + children_dict[node.parent_urn] = [] + children_dict[node.parent_urn].append(node) + + def get_sorted_requirement_nodes_rec(parent_urn): """ - Recursive function to build framework groups tree, within get_sorted_requirements_and_groups - start: the initial list + Recursive helper function to build the framework groups tree. """ result = {} - for node in start: - children = [ - requirement_node - for requirement_node in requirement_nodes - if requirement_node.parent_urn == node.urn - ] - result[str(node.id)] = { + for node in children_dict.get(parent_urn, []): + node_info = { "urn": node.urn, "parent_urn": node.parent_urn, "name": node.display_short(), "node_content": node.display_long(), - "style": "node", + "style": "node" if node.urn in children_dict else "leaf", "assessable": node.assessable, "description": node.description, - "children": get_sorted_requirement_nodes_rec( - requirement_nodes, requirements_assessed, children - ), + "children": get_sorted_requirement_nodes_rec(node.urn), } - for req in [ - requirement_node - for requirement_node in requirement_nodes - if requirement_node.parent_urn == node.urn - ]: - if requirements_assessed: - req_as = requirement_assessment_from_requirement_id[str(req.id)] - result[str(node.id)]["children"][str(req.id)].update( - { - "urn": req.urn, - "name": req.display_short(), - "description": req.description, - "ra_id": str(req_as.id), - "leaf_content": req.display_long(), - "status": req_as.status, - "status_display": req_as.get_status_display(), - "status_i18n": camel_case(req_as.status), - "style": "leaf", - "threats": ThreatReadSerializer( - req.threats.all(), many=True - ).data, - "security_functions": SecurityFunctionReadSerializer( - req.security_functions.all(), many=True - ).data, - } - ) - else: - result[str(node.id)]["children"][str(req.id)].update( + + # Add requirement assessment info if available + if ra_dict: + ra = ra_dict.get(str(node.id)) + if ra: + node_info.update( { - "urn": req.urn, - "name": req.display_short(), - "leaf_content": req.display_long(), - "description": req.description, - "style": "leaf", + "ra_id": str(ra.id), + "leaf_content": node_info.get("node_content", ""), + "status": ra.status, + "status_display": ra.get_status_display(), + "status_i18n": camel_case(ra.status), "threats": ThreatReadSerializer( - req.threats.all(), many=True + ra.requirement.threats.all(), many=True ).data, "security_functions": SecurityFunctionReadSerializer( - req.security_functions.all(), many=True + ra.requirement.security_functions.all(), many=True ).data, } ) + node_info[ + "style" + ] = "leaf" # Update style to leaf if it has an assessment + + result[str(node.id)] = node_info + return result + # Initialize the recursive building from root nodes (those without a parent_urn). tree = get_sorted_requirement_nodes_rec( - requirement_nodes, - requirements_assessed, - [rg for rg in requirement_nodes if not rg.parent_urn], - ) + None + ) # Assuming root nodes have `parent_urn` as None or similar. return tree From 95073e528aa671366cb9d3f1ffd993465fd03c62 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Sat, 2 Mar 2024 00:22:14 +0100 Subject: [PATCH 4/9] Optimize library lookup and retrieval --- backend/core/models.py | 63 +++++++++++++++++++ ..._anssi_rules.yaml => anssi-nis-rules.yaml} | 0 .../libraries/{cmmc-v2.yaml => cmmc-2.0.yaml} | 0 ...3x3.yaml => critical_risk_matrix_3x3.yaml} | 0 ...5x5.yaml => critical_risk_matrix_5x5.yaml} | 0 .../{iso27001.yaml => iso27001-2022.yaml} | 0 ...itre-attack.yaml => mitre-attack-v14.yaml} | 0 ...nist-csf-1.1-en.yaml => nist-csf-1.1.yaml} | 0 ...nist_csf-2.0-en.yaml => nist-csf-2.0.yaml} | 0 ... top 10 web.yaml => owasp-top-10-web.yaml} | 0 .../{pcidss.yaml => pcidss-4_0.yaml} | 0 .../libraries/{soc2.yaml => soc2-2017.yaml} | 0 backend/library/serializers.py | 28 ++++++--- backend/library/utils.py | 52 +++++++++++---- 14 files changed, 123 insertions(+), 20 deletions(-) rename backend/library/libraries/{nis_anssi_rules.yaml => anssi-nis-rules.yaml} (100%) rename backend/library/libraries/{cmmc-v2.yaml => cmmc-2.0.yaml} (100%) rename backend/library/libraries/{critical_matrix_3x3.yaml => critical_risk_matrix_3x3.yaml} (100%) rename backend/library/libraries/{critical_matrix_5x5.yaml => critical_risk_matrix_5x5.yaml} (100%) rename backend/library/libraries/{iso27001.yaml => iso27001-2022.yaml} (100%) rename backend/library/libraries/{mitre-attack.yaml => mitre-attack-v14.yaml} (100%) rename backend/library/libraries/{nist-csf-1.1-en.yaml => nist-csf-1.1.yaml} (100%) rename backend/library/libraries/{nist_csf-2.0-en.yaml => nist-csf-2.0.yaml} (100%) rename backend/library/libraries/{OWASP top 10 web.yaml => owasp-top-10-web.yaml} (100%) rename backend/library/libraries/{pcidss.yaml => pcidss-4_0.yaml} (100%) rename backend/library/libraries/{soc2.yaml => soc2-2017.yaml} (100%) diff --git a/backend/core/models.py b/backend/core/models.py index 12be40c1b..7c00b22f5 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -1,4 +1,5 @@ from django.apps import apps +from django.forms.models import model_to_dict from django.contrib.auth import get_user_model from django.db import models from django.utils.translation import gettext_lazy as _ @@ -100,6 +101,23 @@ class Library(ReferentialObjectMixin): "self", blank=True, verbose_name=_("Dependencies"), symmetrical=False ) + @property + def _objects(self): + res = {} + if self.frameworks.count() > 0: + res["framework"] = model_to_dict(self.frameworks.first()) + res["framework"].update(self.frameworks.first().library_entry) + if self.threats.count() > 0: + res["threats"] = [model_to_dict(threat) for threat in self.threats.all()] + if self.security_functions.count() > 0: + res["security_functions"] = [ + model_to_dict(security_function) + for security_function in self.security_functions.all() + ] + if self.risk_matrices.count() > 0: + res["risk_matrix"] = model_to_dict(self.risk_matrices.first()) + return res + @property def reference_count(self) -> int: """ @@ -304,6 +322,49 @@ def is_deletable(self) -> bool: return False return True + @property + def library_entry(self): + res = {} + requirement_nodes = self.get_requirement_nodes() + if requirement_nodes: + res["requirement_nodes"] = requirement_nodes + + requirement_levels = self.get_requirement_levels() + if requirement_levels: + res["requirement_levels"] = requirement_levels + + return res + + def get_requirement_nodes(self): + # Prefetch related objects if they exist to reduce database queries. + # Adjust prefetch_related paths according to your model relationships. + nodes_queryset = self.requirement_nodes.prefetch_related( + "threats", "security_functions" + ) + if nodes_queryset.exists(): + return [self.process_node(node) for node in nodes_queryset] + return [] + + def process_node(self, node): + # Convert the node to dict and process threats and security functions. + node_dict = model_to_dict(node) + if node.threats.exists(): + node_dict["threats"] = [ + model_to_dict(threat) for threat in node.threats.all() + ] + if node.security_functions.exists(): + node_dict["security_functions"] = [ + model_to_dict(security_function) + for security_function in node.security_functions.all() + ] + return node_dict + + def get_requirement_levels(self): + levels_queryset = self.requirement_levels.all() + if levels_queryset.exists(): + return [model_to_dict(level) for level in levels_queryset] + return [] + class RequirementLevel(ReferentialObjectMixin): framework = models.ForeignKey( @@ -312,6 +373,7 @@ class RequirementLevel(ReferentialObjectMixin): null=True, blank=True, verbose_name=_("Framework"), + related_name="requirement_levels", ) level = models.IntegerField(null=False, blank=False, verbose_name=_("Level")) @@ -339,6 +401,7 @@ class RequirementNode(ReferentialObjectMixin): null=True, blank=True, verbose_name=_("Framework"), + related_name="requirement_nodes", ) parent_urn = models.CharField( max_length=100, null=True, blank=True, verbose_name=_("Parent URN") diff --git a/backend/library/libraries/nis_anssi_rules.yaml b/backend/library/libraries/anssi-nis-rules.yaml similarity index 100% rename from backend/library/libraries/nis_anssi_rules.yaml rename to backend/library/libraries/anssi-nis-rules.yaml diff --git a/backend/library/libraries/cmmc-v2.yaml b/backend/library/libraries/cmmc-2.0.yaml similarity index 100% rename from backend/library/libraries/cmmc-v2.yaml rename to backend/library/libraries/cmmc-2.0.yaml diff --git a/backend/library/libraries/critical_matrix_3x3.yaml b/backend/library/libraries/critical_risk_matrix_3x3.yaml similarity index 100% rename from backend/library/libraries/critical_matrix_3x3.yaml rename to backend/library/libraries/critical_risk_matrix_3x3.yaml diff --git a/backend/library/libraries/critical_matrix_5x5.yaml b/backend/library/libraries/critical_risk_matrix_5x5.yaml similarity index 100% rename from backend/library/libraries/critical_matrix_5x5.yaml rename to backend/library/libraries/critical_risk_matrix_5x5.yaml diff --git a/backend/library/libraries/iso27001.yaml b/backend/library/libraries/iso27001-2022.yaml similarity index 100% rename from backend/library/libraries/iso27001.yaml rename to backend/library/libraries/iso27001-2022.yaml diff --git a/backend/library/libraries/mitre-attack.yaml b/backend/library/libraries/mitre-attack-v14.yaml similarity index 100% rename from backend/library/libraries/mitre-attack.yaml rename to backend/library/libraries/mitre-attack-v14.yaml diff --git a/backend/library/libraries/nist-csf-1.1-en.yaml b/backend/library/libraries/nist-csf-1.1.yaml similarity index 100% rename from backend/library/libraries/nist-csf-1.1-en.yaml rename to backend/library/libraries/nist-csf-1.1.yaml diff --git a/backend/library/libraries/nist_csf-2.0-en.yaml b/backend/library/libraries/nist-csf-2.0.yaml similarity index 100% rename from backend/library/libraries/nist_csf-2.0-en.yaml rename to backend/library/libraries/nist-csf-2.0.yaml diff --git a/backend/library/libraries/OWASP top 10 web.yaml b/backend/library/libraries/owasp-top-10-web.yaml similarity index 100% rename from backend/library/libraries/OWASP top 10 web.yaml rename to backend/library/libraries/owasp-top-10-web.yaml diff --git a/backend/library/libraries/pcidss.yaml b/backend/library/libraries/pcidss-4_0.yaml similarity index 100% rename from backend/library/libraries/pcidss.yaml rename to backend/library/libraries/pcidss-4_0.yaml diff --git a/backend/library/libraries/soc2.yaml b/backend/library/libraries/soc2-2017.yaml similarity index 100% rename from backend/library/libraries/soc2.yaml rename to backend/library/libraries/soc2-2017.yaml diff --git a/backend/library/serializers.py b/backend/library/serializers.py index 10dca64fc..ff18cb774 100644 --- a/backend/library/serializers.py +++ b/backend/library/serializers.py @@ -1,16 +1,20 @@ from rest_framework import serializers +from core.models import Library + +from core.serializers import ( + BaseModelSerializer, + FrameworkReadSerializer, + RiskMatrixReadSerializer, + SecurityFunctionReadSerializer, + ThreatReadSerializer, +) class LibraryObjectSerializer(serializers.Serializer): - type = serializers.ChoiceField( - choices=[ - "risk_matrix", - "security_function", - "threat", - "framework", - ] - ) - fields = serializers.DictField(child=serializers.CharField()) + framework = FrameworkReadSerializer() + risk_matrix = RiskMatrixReadSerializer() + threats = ThreatReadSerializer(many=True) + security_functions = SecurityFunctionReadSerializer(many=True) class LibrarySerializer(serializers.Serializer): @@ -22,6 +26,12 @@ class LibrarySerializer(serializers.Serializer): copyright = serializers.CharField() +class LibraryModelSerializer(BaseModelSerializer): + class Meta: + model = Library + fields = "__all__" + + class LibraryUploadSerializer(serializers.Serializer): file = serializers.FileField(required=True) diff --git a/backend/library/utils.py b/backend/library/utils.py index 0c7327a9e..4af9b9133 100644 --- a/backend/library/utils.py +++ b/backend/library/utils.py @@ -15,6 +15,10 @@ from django.db import transaction from iam.models import Folder +import structlog + +logger = structlog.get_logger(__name__) + def get_available_library_files(): """ @@ -71,18 +75,44 @@ def get_library_names(libraries): def get_library(urn: str) -> dict | None: """ - Returns a library by urn + Returns a library by urn, trying to minimize file I/O by directly accessing the specific YAML file. Args: - urn: urn of the library to return + urn: URN of the library to return. Returns: - library: library with the given urn + The library with the given urn, or None if not found. """ - libraries = get_available_libraries() - for lib in libraries: - if lib["urn"] == urn: - return lib + # First, try to fetch the library from the database. + lib = Library.objects.filter(urn=urn).first() + if lib is not None: + return { + "id": lib.id, + "urn": lib.urn, + "name": lib.name, + "description": lib.description, + "provider": lib.provider, + "packager": lib.packager, + "copyright": lib.copyright, + "reference_count": lib.reference_count, + "objects": lib._objects, + } + + # Construct the path to the YAML file from the urn. + filename = urn.split(":")[-1] + path = settings.BASE_DIR / "library/libraries" / f"{filename}.yaml" + logger.info(f"Attempting to load library from file {path}") + + # Attempt to directly load the library from its specific YAML file. + try: + with open(path, "r", encoding="utf-8") as file: + library_data = yaml.safe_load(file) + if library_data and library_data.get("urn") == urn: + return library_data + except FileNotFoundError: + pass + + # Return None if the library is neither in the database nor found in the specific YAML file. return None @@ -128,7 +158,7 @@ def import_requirement_node(self, framework_object: Framework): maturity=self.requirement_data.get("maturity"), locale=framework_object.locale, default_locale=framework_object.default_locale, - is_published=True + is_published=True, ) for threat in self.requirement_data.get("threats", []): @@ -218,7 +248,7 @@ def import_framework(self, library_object: Library): provider=library_object.provider, locale=library_object.locale, default_locale=library_object.default_locale, # Change this in the future ? - is_published=True + is_published=True, ) for requirement_node in self._requirement_nodes: @@ -325,7 +355,7 @@ def import_risk_matrix(self, library_object: Library): is_enabled=self.risk_matrix_data.get("is_enabled", True), locale=library_object.locale, default_locale=library_object.default_locale, # Change this in the future ? - is_published=True + is_published=True, ) @@ -488,7 +518,7 @@ def create_or_update_library(self): "provider": self._library_data.get("provider", None), "packager": self._library_data.get("packager", None), "folder": Folder.get_root_folder(), # TODO: make this configurable - "is_published": True + "is_published": True, }, urn=_urn, locale=_locale, From 40618139d47ab7c6920561bfbd78bb8387879ef4 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Mon, 4 Mar 2024 15:22:57 +0100 Subject: [PATCH 5/9] Prevent directory traversal The original issue flagged by CodeQL was probably not exploitable --- backend/library/utils.py | 71 ++++++++++++++++++++++++++++++---------- backend/library/views.py | 1 + 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/backend/library/utils.py b/backend/library/utils.py index 4af9b9133..b51e18a07 100644 --- a/backend/library/utils.py +++ b/backend/library/utils.py @@ -1,6 +1,10 @@ import json import os +from pathlib import Path +import re from typing import List, Union +from django.core.exceptions import SuspiciousFileOperation +from django.http import Http404 import yaml from ciso_assistant import settings @@ -19,6 +23,16 @@ logger = structlog.get_logger(__name__) +URN_REGEX = r"^urn:([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)(?::([a-zA-Z0-9_-]+))?:(.+)$" + + +def match_urn(urn_string): + match = re.match(URN_REGEX, urn_string) + if match: + return match.groups() # Returns all captured groups from the regex match + else: + return None + def get_available_library_files(): """ @@ -83,9 +97,13 @@ def get_library(urn: str) -> dict | None: Returns: The library with the given urn, or None if not found. """ - # First, try to fetch the library from the database. - lib = Library.objects.filter(urn=urn).first() - if lib is not None: + if match_urn(urn) is None: + logger.error("Invalid URN", urn=urn) + raise ValueError("Invalid URN") + + # First, try to fetch the library from the database.dd + try: + lib = Library.objects.get(urn=urn) return { "id": lib.id, "urn": lib.urn, @@ -97,23 +115,40 @@ def get_library(urn: str) -> dict | None: "reference_count": lib.reference_count, "objects": lib._objects, } + except Library.DoesNotExist: + # Construct the path to the YAML file from the urn. + filename = urn.split(":")[-1] + libraries_path = settings.BASE_DIR / "library/libraries" + _path = libraries_path / f"{filename}.yaml" + + # Ensure that the path is within the libraries directory to prevent directory traversal attacks. + path = Path(os.path.abspath(_path)) + + logger.info( + "Attempting to load library", + filename=filename, + path=path, + ) + if not path.parent.samefile(libraries_path): + logger.error( + "Attempted to access file outside of libraries directory", + path=path, + libraries_path=libraries_path, + ) + raise SuspiciousFileOperation( + "Attempted to access file outside of libraries directory" + ) - # Construct the path to the YAML file from the urn. - filename = urn.split(":")[-1] - path = settings.BASE_DIR / "library/libraries" / f"{filename}.yaml" - logger.info(f"Attempting to load library from file {path}") + # Attempt to directly load the library from its specific YAML file. + if os.path.isfile(path): + with open(path, "r", encoding="utf-8") as file: + library_data = yaml.safe_load(file) + if library_data and library_data.get("urn") == urn: + return library_data + logger.error("File not found", path=path) - # Attempt to directly load the library from its specific YAML file. - try: - with open(path, "r", encoding="utf-8") as file: - library_data = yaml.safe_load(file) - if library_data and library_data.get("urn") == urn: - return library_data - except FileNotFoundError: - pass - - # Return None if the library is neither in the database nor found in the specific YAML file. - return None + logger.error("Library not found", urn=urn) + raise Http404("Library not found") def get_library_items(library, type: str) -> list[dict]: diff --git a/backend/library/views.py b/backend/library/views.py index 0884869d5..dbde1a552 100644 --- a/backend/library/views.py +++ b/backend/library/views.py @@ -141,6 +141,7 @@ def post(self, request): try: attachment = request.FILES["file"] validate_file_extension(attachment) + # Use safe_load to prevent arbitrary code execution. library = yaml.safe_load(attachment) # This code doesn't handle the library "dependencies" field yet as decribed in the architecture. From c86b1f0f2672a0d5b7b6960c8f6d54d1e2bd3955 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Tue, 5 Mar 2024 12:13:50 +0100 Subject: [PATCH 6/9] chore: run formatter --- backend/app_tests/api/test_api_evidences.py | 12 +- .../api/test_api_requirement_assessments.py | 4 +- backend/cal/migrations/0001_initial.py | 24 +- backend/core/forms.py | 4 +- backend/core/helpers.py | 21 +- backend/core/management/commands/status.py | 18 +- ...lter_riskscenario_strength_of_knowledge.py | 13 +- ...plianceassessment_is_published_and_more.py | 99 +++-- ..._lc_status_alter_securitymeasure_effort.py | 39 +- ...uritymeasure_security_function_and_more.py | 150 ++++--- backend/core/models.py | 1 - backend/core/urls.py | 4 +- backend/iam/migrations/0001_initial.py | 371 ++++++++++++++---- backend/iam/views.py | 2 +- backend/library/helpers.py | 4 +- backend/library/serializers.py | 7 +- backend/library/utils.py | 4 +- 17 files changed, 538 insertions(+), 239 deletions(-) diff --git a/backend/app_tests/api/test_api_evidences.py b/backend/app_tests/api/test_api_evidences.py index 53ee93857..279cd2104 100644 --- a/backend/app_tests/api/test_api_evidences.py +++ b/backend/app_tests/api/test_api_evidences.py @@ -104,9 +104,7 @@ class TestEvidencesAuthenticated: def test_get_evidences(self, test): """test to get evidences from the API with authentication""" - applied_control = AppliedControl.objects.create( - name="test", folder=test.folder - ) + applied_control = AppliedControl.objects.create(name="test", folder=test.folder) EndpointTestsQueries.Auth.get_object( test.client, @@ -134,9 +132,7 @@ def test_get_evidences(self, test): def test_create_evidences(self, test): """test to create evidences with the API with authentication""" - applied_control = AppliedControl.objects.create( - name="test", folder=test.folder - ) + applied_control = AppliedControl.objects.create(name="test", folder=test.folder) with open( path.join(path.dirname(path.dirname(__file__)), EVIDENCE_ATTACHMENT), "rb" @@ -171,9 +167,7 @@ def test_update_evidences(self, test): """test to update evidences with the API with authentication""" folder = Folder.objects.create(name="test2") - applied_control = AppliedControl.objects.create( - name="test", folder=test.folder - ) + applied_control = AppliedControl.objects.create(name="test", folder=test.folder) applied_control2 = AppliedControl.objects.create(name="test2", folder=folder) with open( diff --git a/backend/app_tests/api/test_api_requirement_assessments.py b/backend/app_tests/api/test_api_requirement_assessments.py index 0a1ed18ba..41d701ca8 100644 --- a/backend/app_tests/api/test_api_requirement_assessments.py +++ b/backend/app_tests/api/test_api_requirement_assessments.py @@ -144,9 +144,7 @@ def test_create_requirement_assessments(self, test): project=Project.objects.create(name="test", folder=test.folder), framework=Framework.objects.all()[0], ) - applied_control = AppliedControl.objects.create( - name="test", folder=test.folder - ) + applied_control = AppliedControl.objects.create(name="test", folder=test.folder) EndpointTestsQueries.Auth.create_object( test.client, diff --git a/backend/cal/migrations/0001_initial.py b/backend/cal/migrations/0001_initial.py index 658332309..2706e865e 100644 --- a/backend/cal/migrations/0001_initial.py +++ b/backend/cal/migrations/0001_initial.py @@ -4,21 +4,27 @@ class Migration(migrations.Migration): - initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Event', + name="Event", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=200)), - ('description', models.TextField()), - ('start_time', models.DateTimeField()), - ('end_time', models.DateTimeField()), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=200)), + ("description", models.TextField()), + ("start_time", models.DateTimeField()), + ("end_time", models.DateTimeField()), ], ), ] diff --git a/backend/core/forms.py b/backend/core/forms.py index e5d42d5d8..220cf4daf 100644 --- a/backend/core/forms.py +++ b/backend/core/forms.py @@ -63,9 +63,7 @@ def __init__(self, recommended_applied_controls=None, *args, **kwargs) -> None: def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) if self.recommended_applied_controls: - context[ - "recommended_applied_controls" - ] = self.recommended_applied_controls + context["recommended_applied_controls"] = self.recommended_applied_controls return context diff --git a/backend/core/helpers.py b/backend/core/helpers.py index 7c4a17f50..6f818394a 100644 --- a/backend/core/helpers.py +++ b/backend/core/helpers.py @@ -210,7 +210,6 @@ def get_sorted_requirement_nodes( else {} ) - def get_sorted_requirement_nodes_rec( requirement_nodes: list, requirements_assessed: list, @@ -232,12 +231,14 @@ def get_sorted_requirement_nodes_rec( "description": node.description, "children": get_sorted_requirement_nodes_rec(node.urn), } - for req in sorted([ - requirement_node - for requirement_node in requirement_nodes - if requirement_node.parent_urn == node.urn - ], key=lambda x: x.order_id): - + for req in sorted( + [ + requirement_node + for requirement_node in requirement_nodes + if requirement_node.parent_urn == node.urn + ], + key=lambda x: x.order_id, + ): if requirements_assessed: req_as = requirement_assessment_from_requirement_id[str(req.id)] result[str(node.id)]["children"][str(req.id)].update( @@ -283,8 +284,10 @@ def get_sorted_requirement_nodes_rec( tree = get_sorted_requirement_nodes_rec( requirement_nodes, requirements_assessed, - sorted([rn for rn in requirement_nodes if not rn.parent_urn], - key=lambda x: x.order_id), + sorted( + [rn for rn in requirement_nodes if not rn.parent_urn], + key=lambda x: x.order_id, + ), ) return tree diff --git a/backend/core/management/commands/status.py b/backend/core/management/commands/status.py index 2728ce531..36f5fd2c1 100644 --- a/backend/core/management/commands/status.py +++ b/backend/core/management/commands/status.py @@ -1,14 +1,16 @@ from django.core.management.base import BaseCommand from core.models import * from iam.models import User, Folder + + class Command(BaseCommand): - help = 'Displays instance status' + help = "Displays instance status" def handle(self, *args, **kwargs): nb_users = User.objects.all().count() nb_first_login = User.objects.filter(first_login=True).count() nb_libraries = Library.objects.all().count() - nb_domains = Folder.objects.filter(content_type='DO').count() + nb_domains = Folder.objects.filter(content_type="DO").count() nb_projects = Project.objects.all().count() nb_assets = Asset.objects.all().count() nb_threats = Threat.objects.all().count() @@ -19,8 +21,10 @@ def handle(self, *args, **kwargs): nb_risk_assessments = RiskAssessment.objects.all().count() nb_risk_scenarios = RiskScenario.objects.all().count() nb_risk_acceptances = RiskAcceptance.objects.all().count() - self.stdout.write(f"users={nb_users} first_logins={nb_first_login} libraries={nb_libraries} " + - f"domains={nb_domains} projects={nb_projects} assets={nb_assets} " + - f"threats={nb_threats} functions={nb_functions} measures={nb_measures} " + - f"evidences={nb_evidences} compliance={nb_compliance_assessments} risk={nb_risk_assessments} " + - f"scenarios={nb_risk_scenarios} acceptances={nb_risk_acceptances}") \ No newline at end of file + self.stdout.write( + f"users={nb_users} first_logins={nb_first_login} libraries={nb_libraries} " + + f"domains={nb_domains} projects={nb_projects} assets={nb_assets} " + + f"threats={nb_threats} functions={nb_functions} measures={nb_measures} " + + f"evidences={nb_evidences} compliance={nb_compliance_assessments} risk={nb_risk_assessments} " + + f"scenarios={nb_risk_scenarios} acceptances={nb_risk_acceptances}" + ) diff --git a/backend/core/migrations/0003_alter_riskscenario_strength_of_knowledge.py b/backend/core/migrations/0003_alter_riskscenario_strength_of_knowledge.py index 011b043b6..5d7aa3560 100644 --- a/backend/core/migrations/0003_alter_riskscenario_strength_of_knowledge.py +++ b/backend/core/migrations/0003_alter_riskscenario_strength_of_knowledge.py @@ -4,15 +4,18 @@ class Migration(migrations.Migration): - dependencies = [ - ('core', '0002_initial'), + ("core", "0002_initial"), ] operations = [ migrations.AlterField( - model_name='riskscenario', - name='strength_of_knowledge', - field=models.IntegerField(default=-1, help_text='The strength of the knowledge supporting the assessment', verbose_name='Strength of Knowledge'), + model_name="riskscenario", + name="strength_of_knowledge", + field=models.IntegerField( + default=-1, + help_text="The strength of the knowledge supporting the assessment", + verbose_name="Strength of Knowledge", + ), ), ] diff --git a/backend/core/migrations/0004_complianceassessment_is_published_and_more.py b/backend/core/migrations/0004_complianceassessment_is_published_and_more.py index 269d7b198..9f142344a 100644 --- a/backend/core/migrations/0004_complianceassessment_is_published_and_more.py +++ b/backend/core/migrations/0004_complianceassessment_is_published_and_more.py @@ -4,90 +4,89 @@ class Migration(migrations.Migration): - dependencies = [ - ('core', '0003_alter_riskscenario_strength_of_knowledge'), + ("core", "0003_alter_riskscenario_strength_of_knowledge"), ] operations = [ migrations.AddField( - model_name='complianceassessment', - name='is_published', - field=models.BooleanField(default=False, verbose_name='published'), + model_name="complianceassessment", + name="is_published", + field=models.BooleanField(default=False, verbose_name="published"), ), migrations.AddField( - model_name='evidence', - name='is_published', - field=models.BooleanField(default=False, verbose_name='published'), + model_name="evidence", + name="is_published", + field=models.BooleanField(default=False, verbose_name="published"), ), migrations.AddField( - model_name='framework', - name='is_published', - field=models.BooleanField(default=False, verbose_name='published'), + model_name="framework", + name="is_published", + field=models.BooleanField(default=False, verbose_name="published"), ), migrations.AddField( - model_name='library', - name='is_published', - field=models.BooleanField(default=False, verbose_name='published'), + model_name="library", + name="is_published", + field=models.BooleanField(default=False, verbose_name="published"), ), migrations.AddField( - model_name='project', - name='is_published', - field=models.BooleanField(default=False, verbose_name='published'), + model_name="project", + name="is_published", + field=models.BooleanField(default=False, verbose_name="published"), ), migrations.AddField( - model_name='requirementassessment', - name='is_published', - field=models.BooleanField(default=False, verbose_name='published'), + model_name="requirementassessment", + name="is_published", + field=models.BooleanField(default=False, verbose_name="published"), ), migrations.AddField( - model_name='requirementlevel', - name='is_published', - field=models.BooleanField(default=False, verbose_name='published'), + model_name="requirementlevel", + name="is_published", + field=models.BooleanField(default=False, verbose_name="published"), ), migrations.AddField( - model_name='requirementnode', - name='is_published', - field=models.BooleanField(default=False, verbose_name='published'), + model_name="requirementnode", + name="is_published", + field=models.BooleanField(default=False, verbose_name="published"), ), migrations.AddField( - model_name='riskacceptance', - name='is_published', - field=models.BooleanField(default=False, verbose_name='published'), + model_name="riskacceptance", + name="is_published", + field=models.BooleanField(default=False, verbose_name="published"), ), migrations.AddField( - model_name='riskassessment', - name='is_published', - field=models.BooleanField(default=False, verbose_name='published'), + model_name="riskassessment", + name="is_published", + field=models.BooleanField(default=False, verbose_name="published"), ), migrations.AddField( - model_name='riskmatrix', - name='is_published', - field=models.BooleanField(default=False, verbose_name='published'), + model_name="riskmatrix", + name="is_published", + field=models.BooleanField(default=False, verbose_name="published"), ), migrations.AddField( - model_name='riskscenario', - name='is_published', - field=models.BooleanField(default=False, verbose_name='published'), + model_name="riskscenario", + name="is_published", + field=models.BooleanField(default=False, verbose_name="published"), ), migrations.AddField( - model_name='securitymeasure', - name='is_published', - field=models.BooleanField(default=False, verbose_name='published'), + model_name="securitymeasure", + name="is_published", + field=models.BooleanField(default=False, verbose_name="published"), ), migrations.AlterField( - model_name='asset', - name='is_published', - field=models.BooleanField(default=False, verbose_name='published'), + model_name="asset", + name="is_published", + field=models.BooleanField(default=False, verbose_name="published"), ), migrations.AlterField( - model_name='securityfunction', - name='is_published', - field=models.BooleanField(default=False, verbose_name='published'), + model_name="securityfunction", + name="is_published", + field=models.BooleanField(default=False, verbose_name="published"), ), migrations.AlterField( - model_name='threat', - name='is_published', - field=models.BooleanField(default=False, verbose_name='published'), + model_name="threat", + name="is_published", + field=models.BooleanField(default=False, verbose_name="published"), ), ] diff --git a/backend/core/migrations/0005_alter_project_lc_status_alter_securitymeasure_effort.py b/backend/core/migrations/0005_alter_project_lc_status_alter_securitymeasure_effort.py index 57e8da58f..81fa3bc5e 100644 --- a/backend/core/migrations/0005_alter_project_lc_status_alter_securitymeasure_effort.py +++ b/backend/core/migrations/0005_alter_project_lc_status_alter_securitymeasure_effort.py @@ -4,20 +4,43 @@ class Migration(migrations.Migration): - dependencies = [ - ('core', '0004_complianceassessment_is_published_and_more'), + ("core", "0004_complianceassessment_is_published_and_more"), ] operations = [ migrations.AlterField( - model_name='project', - name='lc_status', - field=models.CharField(choices=[('undefined', 'Undefined'), ('in_design', 'Design'), ('in_dev', 'Development'), ('in_prod', 'Production'), ('eol', 'EndOfLife'), ('dropped', 'Dropped')], default='in_design', max_length=20, verbose_name='Status'), + model_name="project", + name="lc_status", + field=models.CharField( + choices=[ + ("undefined", "Undefined"), + ("in_design", "Design"), + ("in_dev", "Development"), + ("in_prod", "Production"), + ("eol", "EndOfLife"), + ("dropped", "Dropped"), + ], + default="in_design", + max_length=20, + verbose_name="Status", + ), ), migrations.AlterField( - model_name='securitymeasure', - name='effort', - field=models.CharField(blank=True, choices=[('S', 'Small'), ('M', 'Medium'), ('L', 'Large'), ('XL', 'Extra Large')], help_text='Relative effort of the measure (using T-Shirt sizing)', max_length=2, null=True, verbose_name='Effort'), + model_name="securitymeasure", + name="effort", + field=models.CharField( + blank=True, + choices=[ + ("S", "Small"), + ("M", "Medium"), + ("L", "Large"), + ("XL", "Extra Large"), + ], + help_text="Relative effort of the measure (using T-Shirt sizing)", + max_length=2, + null=True, + verbose_name="Effort", + ), ), ] diff --git a/backend/core/migrations/0006_remove_securitymeasure_security_function_and_more.py b/backend/core/migrations/0006_remove_securitymeasure_security_function_and_more.py index 264cfe69a..af0ffc188 100644 --- a/backend/core/migrations/0006_remove_securitymeasure_security_function_and_more.py +++ b/backend/core/migrations/0006_remove_securitymeasure_security_function_and_more.py @@ -7,90 +7,146 @@ class Migration(migrations.Migration): - dependencies = [ - ('core', '0005_alter_project_lc_status_alter_securitymeasure_effort'), - ('iam', '0001_initial'), + ("core", "0005_alter_project_lc_status_alter_securitymeasure_effort"), + ("iam", "0001_initial"), ] operations = [ - migrations.RenameModel('SecurityFunction', 'ReferenceControl'), + migrations.RenameModel("SecurityFunction", "ReferenceControl"), migrations.DeleteModel("Policy"), - migrations.RenameModel('SecurityMeasure', 'AppliedControl'), + migrations.RenameModel("SecurityMeasure", "AppliedControl"), migrations.AlterModelOptions( - name='appliedcontrol', - options={'verbose_name': 'Applied control', 'verbose_name_plural': 'Applied controls'}, + name="appliedcontrol", + options={ + "verbose_name": "Applied control", + "verbose_name_plural": "Applied controls", + }, ), migrations.AlterModelOptions( - name='referencecontrol', - options={'verbose_name': 'Reference control', 'verbose_name_plural': 'Reference controls'}, + name="referencecontrol", + options={ + "verbose_name": "Reference control", + "verbose_name_plural": "Reference controls", + }, ), migrations.RenameField( - model_name='appliedcontrol', old_name='security_function', new_name='reference_control', + model_name="appliedcontrol", + old_name="security_function", + new_name="reference_control", ), migrations.RenameField( - model_name='requirementassessment', old_name='security_measures', new_name='applied_controls', + model_name="requirementassessment", + old_name="security_measures", + new_name="applied_controls", ), migrations.RenameField( - model_name='requirementnode', old_name='security_functions', new_name='reference_controls', + model_name="requirementnode", + old_name="security_functions", + new_name="reference_controls", ), migrations.RenameField( - model_name='riskscenario', old_name='security_measures', new_name='applied_controls', + model_name="riskscenario", + old_name="security_measures", + new_name="applied_controls", ), migrations.AlterField( - model_name='appliedcontrol', - name='evidences', - field=models.ManyToManyField(blank=True, related_name='applied_controls', to='core.evidence', verbose_name='Evidences'), + model_name="appliedcontrol", + name="evidences", + field=models.ManyToManyField( + blank=True, + related_name="applied_controls", + to="core.evidence", + verbose_name="Evidences", + ), ), migrations.AlterField( - model_name='appliedcontrol', - name='expiry_date', - field=models.DateField(blank=True, help_text='Date after which the applied control is no longer valid', null=True, verbose_name='Expiry date'), + model_name="appliedcontrol", + name="expiry_date", + field=models.DateField( + blank=True, + help_text="Date after which the applied control is no longer valid", + null=True, + verbose_name="Expiry date", + ), ), migrations.AlterField( - model_name='referencecontrol', - name='library', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reference_controls', to='core.library'), + model_name="referencecontrol", + name="library", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="reference_controls", + to="core.library", + ), ), migrations.RenameField( - model_name='riskscenario', old_name='existing_measures', new_name='existing_controls', + model_name="riskscenario", + old_name="existing_measures", + new_name="existing_controls", ), migrations.AlterField( - model_name='riskscenario', - name='existing_controls', - field=models.TextField(blank=True, help_text='The existing controls to manage this risk. Edit the risk scenario to add extra applied controls.', max_length=2000, verbose_name='Existing controls'), + model_name="riskscenario", + name="existing_controls", + field=models.TextField( + blank=True, + help_text="The existing controls to manage this risk. Edit the risk scenario to add extra applied controls.", + max_length=2000, + verbose_name="Existing controls", + ), ), migrations.CreateModel( - name='Policy', - fields=[ - ], + name="Policy", + fields=[], options={ - 'verbose_name': 'Policy', - 'verbose_name_plural': 'Policies', - 'proxy': True, - 'indexes': [], - 'constraints': [], + "verbose_name": "Policy", + "verbose_name_plural": "Policies", + "proxy": True, + "indexes": [], + "constraints": [], }, - bases=('core.appliedcontrol',), + bases=("core.appliedcontrol",), ), migrations.AlterField( - model_name='appliedcontrol', - name='reference_control', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.referencecontrol', verbose_name='Reference Control'), + model_name="appliedcontrol", + name="reference_control", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.referencecontrol", + verbose_name="Reference Control", + ), ), migrations.AlterField( - model_name='requirementassessment', - name='applied_controls', - field=models.ManyToManyField(blank=True, related_name='requirement_assessments', to='core.appliedcontrol', verbose_name='Applied controls'), + model_name="requirementassessment", + name="applied_controls", + field=models.ManyToManyField( + blank=True, + related_name="requirement_assessments", + to="core.appliedcontrol", + verbose_name="Applied controls", + ), ), migrations.AlterField( - model_name='requirementnode', - name='reference_controls', - field=models.ManyToManyField(blank=True, related_name='requirements', to='core.referencecontrol', verbose_name='Reference controls'), + model_name="requirementnode", + name="reference_controls", + field=models.ManyToManyField( + blank=True, + related_name="requirements", + to="core.referencecontrol", + verbose_name="Reference controls", + ), ), migrations.AlterField( - model_name='riskscenario', - name='applied_controls', - field=models.ManyToManyField(blank=True, related_name='risk_scenarios', to='core.appliedcontrol', verbose_name='Applied controls'), + model_name="riskscenario", + name="applied_controls", + field=models.ManyToManyField( + blank=True, + related_name="risk_scenarios", + to="core.appliedcontrol", + verbose_name="Applied controls", + ), ), ] diff --git a/backend/core/models.py b/backend/core/models.py index 9ee810d84..1d8b5c40b 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -302,7 +302,6 @@ class Meta: verbose_name = _("Framework") verbose_name_plural = _("Frameworks") - def is_deletable(self) -> bool: """ Returns True if the framework can be deleted diff --git a/backend/core/urls.py b/backend/core/urls.py index a0aba0c5c..5be718e5c 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -15,9 +15,7 @@ router.register(r"risk-assessments", RiskAssessmentViewSet, basename="risk-assessments") router.register(r"threats", ThreatViewSet, basename="threats") router.register(r"risk-scenarios", RiskScenarioViewSet, basename="risk-scenarios") -router.register( - r"applied-controls", AppliedControlViewSet, basename="applied-controls" -) +router.register(r"applied-controls", AppliedControlViewSet, basename="applied-controls") router.register(r"policies", PolicyViewSet, basename="policies") router.register(r"risk-acceptances", RiskAcceptanceViewSet, basename="risk-acceptances") router.register( diff --git a/backend/iam/migrations/0001_initial.py b/backend/iam/migrations/0001_initial.py index bb963f776..2dfb3056d 100644 --- a/backend/iam/migrations/0001_initial.py +++ b/backend/iam/migrations/0001_initial.py @@ -9,119 +9,340 @@ class Migration(migrations.Migration): - initial = True dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), + ("auth", "0012_alter_user_first_name_max_length"), ] operations = [ migrations.CreateModel( - name='Folder', + name="Folder", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='UpdatedÒ at')), - ('is_published', models.BooleanField(default=False, verbose_name='published')), - ('name', models.CharField(max_length=200, verbose_name='Name')), - ('description', models.TextField(blank=True, null=True, verbose_name='Description')), - ('content_type', models.CharField(choices=[('GL', 'GLOBAL'), ('DO', 'DOMAIN')], default='DO', max_length=2)), - ('builtin', models.BooleanField(default=False)), - ('parent_folder', models.ForeignKey(default=iam.models._get_root_folder, null=True, on_delete=django.db.models.deletion.CASCADE, to='iam.folder', verbose_name='parent folder')), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created at"), + ), + ( + "updated_at", + models.DateTimeField(auto_now=True, verbose_name="UpdatedÒ at"), + ), + ( + "is_published", + models.BooleanField(default=False, verbose_name="published"), + ), + ("name", models.CharField(max_length=200, verbose_name="Name")), + ( + "description", + models.TextField(blank=True, null=True, verbose_name="Description"), + ), + ( + "content_type", + models.CharField( + choices=[("GL", "GLOBAL"), ("DO", "DOMAIN")], + default="DO", + max_length=2, + ), + ), + ("builtin", models.BooleanField(default=False)), + ( + "parent_folder", + models.ForeignKey( + default=iam.models._get_root_folder, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="iam.folder", + verbose_name="parent folder", + ), + ), ], options={ - 'verbose_name': 'Folder', - 'verbose_name_plural': 'Folders', + "verbose_name": "Folder", + "verbose_name_plural": "Folders", }, ), migrations.CreateModel( - name='User', + name="User", fields=[ - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='UpdatedÒ at')), - ('is_published', models.BooleanField(default=False, verbose_name='published')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('email', models.CharField(max_length=100, unique=True)), - ('first_login', models.BooleanField(default=True)), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('folder', models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder')), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created at"), + ), + ( + "updated_at", + models.DateTimeField(auto_now=True, verbose_name="UpdatedÒ at"), + ), + ( + "is_published", + models.BooleanField(default=False, verbose_name="published"), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ("email", models.CharField(max_length=100, unique=True)), + ("first_login", models.BooleanField(default=True)), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "folder", + models.ForeignKey( + default=iam.models.Folder.get_root_folder, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_folder", + to="iam.folder", + ), + ), ], options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'permissions': (('backup', 'backup'), ('restore', 'restore')), + "verbose_name": "user", + "verbose_name_plural": "users", + "permissions": (("backup", "backup"), ("restore", "restore")), }, managers=[ - ('objects', iam.models.UserManager()), + ("objects", iam.models.UserManager()), ], ), migrations.CreateModel( - name='Role', + name="Role", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='UpdatedÒ at')), - ('is_published', models.BooleanField(default=False, verbose_name='published')), - ('name', models.CharField(max_length=200, verbose_name='Name')), - ('description', models.TextField(blank=True, null=True, verbose_name='Description')), - ('builtin', models.BooleanField(default=False)), - ('folder', models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder')), - ('permissions', models.ManyToManyField(blank=True, to='auth.permission', verbose_name='permissions')), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created at"), + ), + ( + "updated_at", + models.DateTimeField(auto_now=True, verbose_name="UpdatedÒ at"), + ), + ( + "is_published", + models.BooleanField(default=False, verbose_name="published"), + ), + ("name", models.CharField(max_length=200, verbose_name="Name")), + ( + "description", + models.TextField(blank=True, null=True, verbose_name="Description"), + ), + ("builtin", models.BooleanField(default=False)), + ( + "folder", + models.ForeignKey( + default=iam.models.Folder.get_root_folder, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_folder", + to="iam.folder", + ), + ), + ( + "permissions", + models.ManyToManyField( + blank=True, to="auth.permission", verbose_name="permissions" + ), + ), ], options={ - 'ordering': ['name'], - 'abstract': False, + "ordering": ["name"], + "abstract": False, }, ), migrations.CreateModel( - name='UserGroup', + name="UserGroup", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='UpdatedÒ at')), - ('is_published', models.BooleanField(default=False, verbose_name='published')), - ('name', models.CharField(max_length=200, verbose_name='Name')), - ('description', models.TextField(blank=True, null=True, verbose_name='Description')), - ('builtin', models.BooleanField(default=False)), - ('folder', models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder')), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created at"), + ), + ( + "updated_at", + models.DateTimeField(auto_now=True, verbose_name="UpdatedÒ at"), + ), + ( + "is_published", + models.BooleanField(default=False, verbose_name="published"), + ), + ("name", models.CharField(max_length=200, verbose_name="Name")), + ( + "description", + models.TextField(blank=True, null=True, verbose_name="Description"), + ), + ("builtin", models.BooleanField(default=False)), + ( + "folder", + models.ForeignKey( + default=iam.models.Folder.get_root_folder, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_folder", + to="iam.folder", + ), + ), ], options={ - 'verbose_name': 'user group', - 'verbose_name_plural': 'user groups', + "verbose_name": "user group", + "verbose_name_plural": "user groups", }, ), migrations.CreateModel( - name='RoleAssignment', + name="RoleAssignment", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='UpdatedÒ at')), - ('is_published', models.BooleanField(default=False, verbose_name='published')), - ('name', models.CharField(max_length=200, verbose_name='Name')), - ('description', models.TextField(blank=True, null=True, verbose_name='Description')), - ('is_recursive', models.BooleanField(default=False, verbose_name='sub folders are visible')), - ('builtin', models.BooleanField(default=False)), - ('folder', models.ForeignKey(default=iam.models.Folder.get_root_folder, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder')), - ('perimeter_folders', models.ManyToManyField(related_name='perimeter_folders', to='iam.folder', verbose_name='Domain')), - ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='iam.role', verbose_name='Role')), - ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('user_group', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='iam.usergroup')), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created at"), + ), + ( + "updated_at", + models.DateTimeField(auto_now=True, verbose_name="UpdatedÒ at"), + ), + ( + "is_published", + models.BooleanField(default=False, verbose_name="published"), + ), + ("name", models.CharField(max_length=200, verbose_name="Name")), + ( + "description", + models.TextField(blank=True, null=True, verbose_name="Description"), + ), + ( + "is_recursive", + models.BooleanField( + default=False, verbose_name="sub folders are visible" + ), + ), + ("builtin", models.BooleanField(default=False)), + ( + "folder", + models.ForeignKey( + default=iam.models.Folder.get_root_folder, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_folder", + to="iam.folder", + ), + ), + ( + "perimeter_folders", + models.ManyToManyField( + related_name="perimeter_folders", + to="iam.folder", + verbose_name="Domain", + ), + ), + ( + "role", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="iam.role", + verbose_name="Role", + ), + ), + ( + "user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user_group", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="iam.usergroup", + ), + ), ], options={ - 'ordering': ['name'], - 'abstract': False, + "ordering": ["name"], + "abstract": False, }, ), migrations.AddField( - model_name='user', - name='user_groups', - field=models.ManyToManyField(blank=True, help_text='The user groups this user belongs to. A user will get all permissions granted to each of their user groups.', to='iam.usergroup', verbose_name='user groups'), + model_name="user", + name="user_groups", + field=models.ManyToManyField( + blank=True, + help_text="The user groups this user belongs to. A user will get all permissions granted to each of their user groups.", + to="iam.usergroup", + verbose_name="user groups", + ), ), ] diff --git a/backend/iam/views.py b/backend/iam/views.py index 46290d717..a3e91e4fc 100644 --- a/backend/iam/views.py +++ b/backend/iam/views.py @@ -59,7 +59,7 @@ def post(self, request) -> Response: status=HTTP_401_UNAUTHORIZED, ) - user.first_login=False + user.first_login = False user.save() return Response(None, status=HTTP_202_ACCEPTED) diff --git a/backend/library/helpers.py b/backend/library/helpers.py index 48dabb196..79659e8fd 100644 --- a/backend/library/helpers.py +++ b/backend/library/helpers.py @@ -10,9 +10,9 @@ def preview_library(library) -> dict[str, list]: preview = {} requirement_nodes_list = [] if library["objects"]["framework"].get("requirement_nodes"): - index=0 + index = 0 for requirement_node in library["objects"]["framework"]["requirement_nodes"]: - index+=1 + index += 1 requirement_nodes_list.append( RequirementNode( description=requirement_node.get("description"), diff --git a/backend/library/serializers.py b/backend/library/serializers.py index 94c2b938b..b75407092 100644 --- a/backend/library/serializers.py +++ b/backend/library/serializers.py @@ -1,13 +1,8 @@ -from rest_framework import serializers from core.models import Library - from core.serializers import ( BaseModelSerializer, - FrameworkReadSerializer, - RiskMatrixReadSerializer, - SecurityFunctionReadSerializer, - ThreatReadSerializer, ) +from rest_framework import serializers class LibraryObjectSerializer(serializers.Serializer): diff --git a/backend/library/utils.py b/backend/library/utils.py index 31e57e8d9..673635e17 100644 --- a/backend/library/utils.py +++ b/backend/library/utils.py @@ -220,7 +220,9 @@ def init_requirement_nodes(self, requirement_nodes: List[dict]) -> Union[str, No requirement_node_importers = [] import_errors = [] for index, requirement_node_data in enumerate(requirement_nodes): - requirement_node_importer = RequirementNodeImporter(requirement_node_data, index) + requirement_node_importer = RequirementNodeImporter( + requirement_node_data, index + ) requirement_node_importers.append(requirement_node_importer) if ( requirement_node_error := requirement_node_importer.is_valid() From e9366471581d4af5e2f4926ec318865b8bf8fbe8 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Tue, 5 Mar 2024 12:16:14 +0100 Subject: [PATCH 7/9] fix regression in requirements tree building --- backend/core/helpers.py | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/backend/core/helpers.py b/backend/core/helpers.py index 6f818394a..6633654ed 100644 --- a/backend/core/helpers.py +++ b/backend/core/helpers.py @@ -188,7 +188,8 @@ def _get_all_requirement_nodes_id_in_requirement_node( def get_sorted_requirement_nodes( - requirement_nodes: list, requirements_assessed: list | None + requirement_nodes: list, + requirements_assessed: list | None, ) -> dict: """ Recursive function to build framework groups tree @@ -220,16 +221,23 @@ def get_sorted_requirement_nodes_rec( start: the initial list """ result = {} - for node in children_dict.get(parent_urn, []): - node_info = { + for node in start: + children = [ + requirement_node + for requirement_node in requirement_nodes + if requirement_node.parent_urn == node.urn + ] + result[str(node.id)] = { "urn": node.urn, "parent_urn": node.parent_urn, "name": node.display_short(), "node_content": node.display_long(), - "style": "node" if node.urn in children_dict else "leaf", + "style": "node", "assessable": node.assessable, "description": node.description, - "children": get_sorted_requirement_nodes_rec(node.urn), + "children": get_sorted_requirement_nodes_rec( + requirement_nodes, requirements_assessed, children + ), } for req in sorted( [ @@ -243,13 +251,17 @@ def get_sorted_requirement_nodes_rec( req_as = requirement_assessment_from_requirement_id[str(req.id)] result[str(node.id)]["children"][str(req.id)].update( { - "ra_id": str(ra.id), - "leaf_content": node_info.get("node_content", ""), - "status": ra.status, - "status_display": ra.get_status_display(), - "status_i18n": camel_case(ra.status), + "urn": req.urn, + "name": req.display_short(), + "description": req.description, + "ra_id": str(req_as.id), + "leaf_content": req.display_long(), + "status": req_as.status, + "status_display": req_as.get_status_display(), + "status_i18n": camel_case(req_as.status), + "style": "leaf", "threats": ThreatReadSerializer( - ra.requirement.threats.all(), many=True + req.threats.all(), many=True ).data, "reference_controls": ReferenceControlReadSerializer( req.reference_controls.all(), many=True @@ -272,15 +284,8 @@ def get_sorted_requirement_nodes_rec( ).data, } ) - node_info[ - "style" - ] = "leaf" # Update style to leaf if it has an assessment - - result[str(node.id)] = node_info - return result - # Initialize the recursive building from root nodes (those without a parent_urn). tree = get_sorted_requirement_nodes_rec( requirement_nodes, requirements_assessed, From 57d538849c38b938ef4e2dd1d65e5d2bfef94583 Mon Sep 17 00:00:00 2001 From: Nassim Tabchiche Date: Tue, 5 Mar 2024 13:47:28 +0100 Subject: [PATCH 8/9] Fix risk matrices in imported libraries --- backend/core/models.py | 50 +++++++++++++++---- .../(app)/libraries/[id=urn]/+page.svelte | 2 +- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/backend/core/models.py b/backend/core/models.py index 1d8b5c40b..568a46e30 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -109,13 +109,20 @@ def _objects(self): res["framework"].update(self.frameworks.first().library_entry) if self.threats.count() > 0: res["threats"] = [model_to_dict(threat) for threat in self.threats.all()] - if self.security_functions.count() > 0: - res["security_functions"] = [ - model_to_dict(security_function) - for security_function in self.security_functions.all() + if self.reference_controls.count() > 0: + res["reference_controls"] = [ + model_to_dict(reference_control) + for reference_control in self.reference_controls.all() ] if self.risk_matrices.count() > 0: - res["risk_matrix"] = model_to_dict(self.risk_matrices.first()) + matrix = self.risk_matrices.first() + res["risk_matrix"] = model_to_dict(matrix) + res["risk_matrix"]["probability"] = matrix.probability + res["risk_matrix"]["impact"] = matrix.impact + res["risk_matrix"]["risk"] = matrix.risk + res["risk_matrix"]["grid"] = matrix.grid + res["strength_of_knowledge"] = matrix.strength_of_knowledge + res["risk_matrix"] = [res["risk_matrix"]] return res @property @@ -271,13 +278,34 @@ def projects(self) -> list: def parse_json(self) -> dict: return json.loads(self.json_definition) - def get_detailed_grid(self) -> list: + @property + def grid(self) -> list: risk_matrix = self.parse_json() grid = [] for row in risk_matrix["grid"]: grid.append([item for item in row]) return grid + @property + def probability(self) -> list: + risk_matrix = self.parse_json() + return risk_matrix["probability"] + + @property + def impact(self) -> list: + risk_matrix = self.parse_json() + return risk_matrix["impact"] + + @property + def risk(self) -> list: + risk_matrix = self.parse_json() + return risk_matrix["risk"] + + @property + def strength_of_knowledge(self): + risk_matrix = self.parse_json() + return risk_matrix.get("strength_of_knowledge") + def render_grid_as_colors(self): risk_matrix = self.parse_json() grid = risk_matrix["grid"] @@ -327,7 +355,7 @@ def get_requirement_nodes(self): # Prefetch related objects if they exist to reduce database queries. # Adjust prefetch_related paths according to your model relationships. nodes_queryset = self.requirement_nodes.prefetch_related( - "threats", "security_functions" + "threats", "reference_controls" ) if nodes_queryset.exists(): return [self.process_node(node) for node in nodes_queryset] @@ -340,10 +368,10 @@ def process_node(self, node): node_dict["threats"] = [ model_to_dict(threat) for threat in node.threats.all() ] - if node.security_functions.exists(): - node_dict["security_functions"] = [ - model_to_dict(security_function) - for security_function in node.security_functions.all() + if node.reference_controls.exists(): + node_dict["reference_controls"] = [ + model_to_dict(reference_control) + for reference_control in node.reference_controls.all() ] return node_dict diff --git a/frontend/src/routes/(app)/libraries/[id=urn]/+page.svelte b/frontend/src/routes/(app)/libraries/[id=urn]/+page.svelte index 963fda15b..69a829c39 100644 --- a/frontend/src/routes/(app)/libraries/[id=urn]/+page.svelte +++ b/frontend/src/routes/(app)/libraries/[id=urn]/+page.svelte @@ -35,7 +35,7 @@ import { enhance } from '$app/forms'; const riskMatricesTable: TableSource = { - head: ['name', 'description'], + head: { name: 'name', description: 'description' }, body: tableSourceMapper(riskMatrices, ['name', 'description']) }; From b928dab167404ecf6b5a5ae9acf416bd36f00dda Mon Sep 17 00:00:00 2001 From: Mohamed-Hacene Date: Tue, 5 Mar 2024 15:59:47 +0100 Subject: [PATCH 9/9] feat: add loading translation --- frontend/src/routes/(app)/libraries/[id=urn]/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/routes/(app)/libraries/[id=urn]/+page.svelte b/frontend/src/routes/(app)/libraries/[id=urn]/+page.svelte index 69a829c39..ba1d5761f 100644 --- a/frontend/src/routes/(app)/libraries/[id=urn]/+page.svelte +++ b/frontend/src/routes/(app)/libraries/[id=urn]/+page.svelte @@ -155,7 +155,7 @@ {#if framework}

{m.framework()}

{#await data.tree} - loading... + {m.loading()}... {:then tree}