diff --git a/README.md b/README.md index f236cca77..883c96be4 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Read the [full article](https://intuitem.com/blog/we-are-going-open-source/) abo - CMMC v2 - PSPF -Checkout the [library](/library/libraries/) for the Domain Specific Language used and how you can define your own. +Checkout the [library](/library/libraries/) and [tools](/tools/) for the Domain Specific Language used and how you can define your own. ### Coming soon @@ -75,6 +75,8 @@ You can then reach CISO Assistant using your web brower at [https://localhost:84 For the following executions, use "docker-compose up" directly. +If you want to restart a fresh install, simply delete the db directory, where the database is stored. + ## Setting up CISO Assistant for development ### Requirements @@ -132,7 +134,7 @@ export EMAIL_HOST_USER_RESCUE= export EMAIL_HOST_PASSWORD_RESCUE= export EMAIL_USE_TLS_RESCUE=True -# You can define the email of the first superuser, useful for automation +# You can define the email of the first superuser, useful for automation. A mail is sent to the superuser for password initlization export CISO_SUPERUSER_EMAIL= # By default, Django secret key is generated randomly at each start of CISO Assistant. This is convenient for quick test, @@ -267,6 +269,25 @@ python manage.py migrate These migration files should be tracked by version control. +## Test harness + +To run API tests on the backend, simply type "pytest" in a shell in the backend folder. + +To run functional tests on the frontend, do the following actions: +- in the backend folder, launch the following commands: +```shell +DJANGO_SUPERUSER_EMAIL=admin@tests.com DJANGO_SUPERUSER_PASSWORD=1234 python manage.py createsuperuser --noinput +CISO_ASSISTANT_URL=http://localhost:4173 python manage.py runserver +``` +- in parallel, in the frontend folder, launch the following command: +```shell +PUBLIC_BACKEND_API_URL=http://localhost:8000/api npx playwright test +``` + +For tests requiring mail sending, it is necessary to launch mailhog in a separate terminal. + +The goal of the test harness is to prevent any regression, i.e. all the tests shall be successful. This is achieved for API tests, and will be soon achieved for functional tests. + ## Built With - [Django](https://www.djangoproject.com/) - Python Web Development Framework diff --git a/backend/core/apps.py b/backend/core/apps.py index 2403132d8..81ad0aefc 100644 --- a/backend/core/apps.py +++ b/backend/core/apps.py @@ -1,7 +1,7 @@ from django.apps import AppConfig from django.db.models.signals import post_migrate from ciso_assistant.settings import CISO_ASSISTANT_SUPERUSER_EMAIL - +import os def startup(**kwargs): """ @@ -9,10 +9,9 @@ def startup(**kwargs): This makes sure root folder and global groups are defined before any other object is created Create superuser if CISO_ASSISTANT_SUPERUSER_EMAIL defined """ - from django.contrib.auth.models import Permission from iam.models import Folder, Role, RoleAssignment, User, UserGroup - print("post-migrate handler: initialize database") + print("startup handler: initialize database", kwargs) auditor_permissions = Permission.objects.filter( codename__in=[ @@ -329,4 +328,6 @@ class CoreConfig(AppConfig): verbose_name = "Core" def ready(self): - post_migrate.connect(startup, sender=self) + # avoid post_migrate handler if we are in the main, as it interferes with restore + if not os.environ.get('RUN_MAIN'): + post_migrate.connect(startup, sender=self) diff --git a/backend/core/helpers.py b/backend/core/helpers.py index fe2b09699..1d74ac39e 100644 --- a/backend/core/helpers.py +++ b/backend/core/helpers.py @@ -516,16 +516,16 @@ def aggregate_risks_per_field( count = ( RiskScenario.objects.filter(id__in=object_ids_view) .filter(residual_level=i) - .filter(risk_assessment__risk_matrix__name=["name"]) + # .filter(risk_assessment__risk_matrix__name=["name"]) .count() - ) + ) # What the second filter does ? Is this usefull ? else: count = ( RiskScenario.objects.filter(id__in=object_ids_view) .filter(current_level=i) - .filter(risk_assessment__risk_matrix__name=["name"]) + # .filter(risk_assessment__risk_matrix__name=["name"]) .count() - ) + ) # What the second filter does ? Is this usefull ? if "count" not in values[m["risk"][i][field]]: values[m["risk"][i][field]]["count"] = count @@ -551,7 +551,6 @@ def risks_count_per_level(user: User, risk_assessments: list | None = None): residual_level.append( {"name": r[0], "value": r[1]["count"], "color": r[1]["color"], "localName": camel_case(r[0])} ) - return {"current": current_level, "residual": residual_level} @@ -709,7 +708,6 @@ def risk_status(user: User, risk_assessment_list): names.append(str(risk_assessment.project) + " " + str(risk_assessment.version)) y_max_rsk = max(max_tmp, default=0) + 1 - print("y_max_rsk: ", y_max_rsk) return { "names": names, diff --git a/backend/core/views.py b/backend/core/views.py index 48a7d62e2..8d7ee541b 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -682,7 +682,7 @@ def impact(self, request, pk): return Response(choices) @action(detail=False, name="Get risk count per level") - def count_per_level(self, request): # _per_level + def count_per_level(self, request): return Response({"results": risks_count_per_level(request.user)}) @@ -766,9 +766,10 @@ class UserFilter(df.FilterSet): is_approver = df.BooleanFilter(method="filter_approver", label="Approver") def filter_approver(self, queryset, name, value): + """ we don't know yet which folders will be used, so filter on any folder""" approvers_id = [] for candidate in User.objects.all(): - if RoleAssignment.has_permission(candidate, "approve_riskacceptance"): + if 'approve_riskacceptance' in candidate.permissions: approvers_id.append(candidate.id) if value: return queryset.filter(id__in=approvers_id) diff --git a/backend/iam/models.py b/backend/iam/models.py index 1397dca38..b913a2cd8 100644 --- a/backend/iam/models.py +++ b/backend/iam/models.py @@ -467,7 +467,11 @@ def get_user_groups(self): @property def has_backup_permission(self) -> bool: - return RoleAssignment.has_permission(self, "backup") + return RoleAssignment.is_access_allowed( + user=self, + perm=Permission.objects.get(codename="backup"), + folder=Folder.get_root_folder(), + ) @property def edit_url(self) -> str: diff --git a/backend/serdes/urls.py b/backend/serdes/urls.py index b37fe2a18..17acce4b0 100644 --- a/backend/serdes/urls.py +++ b/backend/serdes/urls.py @@ -5,5 +5,5 @@ urlpatterns = [ path("dump-db/", login_required(views.dump_db_view), name="dump-db"), - path("load-backup/", views.LoadBackupView.as_view(), name="load-backup"), + path("load-backup/", login_required(views.LoadBackupView.as_view()), name="load-backup"), ] diff --git a/backend/serdes/views.py b/backend/serdes/views.py index 0dc38f086..5ac8316cb 100644 --- a/backend/serdes/views.py +++ b/backend/serdes/views.py @@ -49,6 +49,8 @@ class LoadBackupView(APIView): serializer_class = LoadBackupSerializer def post(self, request, *args, **kwargs): + if not is_admin_check(request.user): + return Response(status=status.HTTP_403_FORBIDDEN) if request.data: sys.stdin = io.StringIO(json.dumps(request.data[1])) request.session.flush() diff --git a/frontend/messages/en.json b/frontend/messages/en.json index faecb3f4e..140a3381d 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -1,9 +1,31 @@ { "$schema": "https://inlang.com/schema/inlang-message-format", - "addButton": "Add {model}", - "associatedObject": "Associated {model}", "french": "French", "english": "English", + "addThreat": "New threat", + "addSecurityFunction": "New security function", + "addSecurityMeasure": "New security measure", + "addAsset": "New asset", + "addRiskAssessment": "New risk assessment", + "addRiskScenario": "New risk scenario", + "addRiskAcceptance": "New risk acceptance", + "addComplianceAssessment": "New compliance assessment", + "addEvidence": "New evidence", + "addDomain": "New domain", + "addProject": "New project", + "addUser": "New user", + "associatedThreats": "Associated threats", + "associatedSecurityFunctions": "Associated security functions", + "associatedSecurityMeasures": "Associated security measures", + "associatedAssets": "Associated assets", + "associatedRiskAssessments": "Associated risk assessments", + "associatedRiskScenarios": "Associated risk scenarios", + "associatedRiskAcceptances": "Associated risk acceptances", + "associatedComplianceAssessments": "Associated compliance assessments", + "associatedEvidences": "Associated evidences", + "associatedDomains": "Associated domains", + "associatedProjects": "Associated projects", + "associatedUsers": "Associated users", "home": "Home", "edit": "Edit", "overview": "Overview", @@ -125,7 +147,7 @@ "statistics": "Statistics", "myProjects": "My projects", "scenarios": "Scenarios", - "assignedObjects": "Assigned to {number} {object}", + "assignedProjects": "Assigned to {number} project{s}", "currentRiskLevelPerScenario": "Current risk level per risk scenario", "residualRiskLevelPerScenario": "Residual risk level per risk scenario", "securityMeasuresStatus": "Security measures status", @@ -147,7 +169,8 @@ "upcoming": "Upcoming", "today": "Today", "actionRequested": "Action requested", - "noObjectYet": "No {object} yet", + "noRiskAcceptanceYet": "No risk acceptance yet", + "noSecurityMeasureYet": "No security measure yet", "authors": "Authors", "reviewers": "Reviewers", "process": "Process", @@ -161,7 +184,7 @@ "pendingMeasures": "Your pending measures", "orderdByRankingScore": "Ordered by ranking score", "rankingScore": "Ranking score", - "noPendingObject": "No pending {object}", + "noPendingSecurityMeasure": "No pending security measure", "rankingScoreDefintion": "Ranking score is an adaptive metric that combines the information of effort and current risk level, and crosses it with the other data to assist you for the prioritization", "actions": "Actions", "projectsSummaryEmpty": "Projects summary is empty", @@ -172,6 +195,39 @@ "measureOpen": "Measure: open", "measureProgress": "Measure: in progress", "measureHold": "Measure: on hold", - "measureDone": "Measure: done" - + "measureDone": "Measure: done", + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday", + "sunday": "Sunday", + "january": "January", + "february": "February", + "march": "March", + "april": "April", + "may": "May", + "june": "June", + "july": "July", + "august": "August", + "september": "September", + "october": "October", + "november": "November", + "december": "December", + "errorsFound": "error{s} found", + "warningsFound": "warning{s} found", + "infosFound": "info{s} found", + "remediationPlan": "Remediation plan", + "treatmentPlan": "Treatment plan", + "asPDF": "as PDF", + "asCSV": "as CSV", + "draft": "Draft", + "riskMatrixView": "Risk matrix view", + "currentInMatrixView": "Current", + "residualInMatrixView": "Residual", + "probability": "Probability", + "riskLevels": "Risk levels", + "cancel": "Cancel", + "save": "Save" } diff --git a/frontend/messages/fr.json b/frontend/messages/fr.json index 124e7f29a..ec12603a8 100644 --- a/frontend/messages/fr.json +++ b/frontend/messages/fr.json @@ -1,9 +1,31 @@ { "$schema": "https://inlang.com/schema/inlang-message-format", - "addButton": "Ajouter {determinant} {model}", - "associatedObject": "{model} associé{e}s", "french": "Français", "english": "Anglais", + "addThreat": "Ajouter une menace", + "addSecurityFunction": "Ajouter une fonction de sécurité", + "addSecurityMeasure": "Ajouter une mesure de sécurité", + "addAsset": "Ajouter un bien sensible", + "addRiskAssessment": "Ajouter une évaluation de risque", + "addRiskScenario": "Ajouter un scénario de risque", + "addRiskAcceptance": "Ajouter une acceptation de risque", + "addComplianceAssessment": "Ajouter une évaluation de conformité", + "addEvidence": "Ajouter une preuve", + "addDomain": "Ajouter un domaine", + "addProject": "Ajouter un projet", + "addUser": "Ajouter un utilisateur", + "associatedThreats": "Menaces associées", + "associatedSecurityFunctions": "Fonctions de sécurité associées", + "associatedSecurityMeasures": "Mesures de sécurité associées", + "associatedAssets": "Biens sensibles associés", + "associatedRiskAssessments": "Évaluations de risque associées", + "associatedRiskScenarios": "Scénarios de risque associés", + "associatedRiskAcceptances": "Acceptations de risque associées", + "associatedComplianceAssessments": "Évaluations de conformité associées", + "associatedEvidences": "Preuves associées", + "associatedDomains": "Domaines associés", + "associatedProjects": "Projets associés", + "associatedUsers": "Utilisateurs associés", "home": "Accueil", "edit": "Modifier", "overview": "Vue d'ensemble", @@ -39,7 +61,7 @@ "userGroups": "Groupes d'utilisateurs", "roleAssignments": "Affectations de rôle", "xRays": "X-rays", - "scoringAssistant": "Assistat d'évaluation", + "scoringAssistant": "Assistant d'évaluation", "libraries": "Bibliothèques", "backupRestore": "Sauvegarde et restauration", "myProfile": "Mon profil", @@ -125,7 +147,7 @@ "statistics": "Statistiques", "myProjects": "Mes projets", "scenarios": "Scénarios", - "assignedObjects": "Assigné à {number} {object}", + "assignedProjects": "Assigné à {number} project{s}", "currentRiskLevelPerScenario": "Niveau de risque courant par scénario de risque", "residualRiskLevelPerScenario": "Niveau de risque résiduel par scénario de risque", "securityMeasuresStatus": "Statut des mesures de sécurité", @@ -147,7 +169,8 @@ "upcoming": "À venir", "today": "Aujourd'hui", "actionRequested": "Action requise", - "noObjectYet": "Aucun{e} {object} pour le moment", + "noRiskAcceptanceYet": "Aucune acceptation de risque pour le moment", + "noSecurityMeasureYet": "Aucune mesure de sécurité pour le moment", "authors": "Auteurs", "reviewers": "Relecteurs", "process": "Traiter", @@ -161,7 +184,7 @@ "pendingMeasures": "Vos mesures en attente", "orderdByRankingScore": "Classées par score", "rankingScore": "Score de classement", - "noPendingObject": "Aucun{e} {objet} en attente", + "noPendingSecurityMeasure": "Aucune mesure de sécurité en attente", "rankingScoreDefintion": "Le score de classement est une mesure adaptative qui combine les informations relatives à l'effort et au niveau de risque actuel, et les croise avec d'autres données pour vous aider à établir des priorités", "actions": "Actions", "projectsSummaryEmpty": "Le résumé des projets est vide", @@ -172,6 +195,39 @@ "measureOpen": "Mesure: ouverte", "measureProgress": "Mesure: en cours", "measureHold": "Mesure: en attente", - "measureDone": "Mesure: terminée" + "measureDone": "Mesure: terminée", + "monday": "Lundi", + "tuesday": "Mardi", + "wednesday": "Mercredi", + "thursday": "Jeudi", + "friday": "Vendredi", + "saturday": "Samedi", + "sunday": "Dimanche", + "january": "Janvier", + "february": "Février", + "march": "Mars", + "april": "Avril", + "may": "Mai", + "june": "Juin", + "july": "Juillet", + "august": "Août", + "september": "Septembre", + "october": "Octobre", + "november": "Novembre", + "december": "Décembre", + "errorsFound": "erreur{s} trouvée{s}", + "warningsFound": "avertissement{s} trouvé{s}", + "infosFound": "info{s} trouvée{s}", + "remediationPlan": "Plan de remédiation", + "treatmentPlan": "Plan de traitement", + "asPDF": "en PDF", + "asCSV": "en CSV", + "draft": "Brouillon", + "riskMatrixView": "Vue de la matrice de risque", + "currentInMatrixView": "Courante", + "residualInMatrixView": "Residuelle", + "probability": "Probabilité", + "riskLevels": "Niveaux de risque", + "cancel": "Annuler", + "save": "Enregistrer" } - diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index d858e03c1..85d1bf407 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -3,7 +3,9 @@ import { devices } from '@playwright/test'; const config: PlaywrightTestConfig = { webServer: { - command: process.env.COMPOSE_TEST ? 'echo "The docker compose frontend server didn\'t start correctly"' : 'npm run build && npm run preview', + command: process.env.COMPOSE_TEST + ? 'echo "The docker compose frontend server didn\'t start correctly"' + : 'npm run build && npm run preview', port: process.env.COMPOSE_TEST ? 3000 : 4173, reuseExistingServer: process.env.COMPOSE_TEST }, @@ -15,34 +17,37 @@ const config: PlaywrightTestConfig = { workers: process.env.CI ? 1 : 1, globalTimeout: 60 * 60 * 1000, timeout: 50 * 1000, - expect : { + expect: { timeout: 10 * 1000 }, reporter: [ [process.env.CI ? 'github' : 'list'], - ['html', { - open: process.env.CI ? 'never' : process.env.DOCKER ? 'always' : 'on-failure', - outputFolder: 'tests/reports', - host: process.env.DOCKER ? '0.0.0.0' : 'localhost' - }] + [ + 'html', + { + open: process.env.CI ? 'never' : process.env.DOCKER ? 'always' : 'on-failure', + outputFolder: 'tests/reports', + host: process.env.DOCKER ? '0.0.0.0' : 'localhost' + } + ] ], use: { screenshot: 'only-on-failure', video: process.env.CI ? 'retain-on-failure' : 'on', trace: process.env.CI ? 'retain-on-failure' : 'on', contextOptions: { - recordVideo: { dir: "tests/results/videos"}, - }, + recordVideo: { dir: 'tests/results/videos' } + } }, projects: [ { name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + use: { ...devices['Desktop Chrome'] } }, { name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, + use: { ...devices['Desktop Firefox'] } + } // { // name: 'webkit', // use: { ...devices['Desktop Safari'] }, diff --git a/frontend/src/lib/components/Breadcrumbs/Breadcrumbs.svelte b/frontend/src/lib/components/Breadcrumbs/Breadcrumbs.svelte index 6c72faf1d..fadc08ae0 100644 --- a/frontend/src/lib/components/Breadcrumbs/Breadcrumbs.svelte +++ b/frontend/src/lib/components/Breadcrumbs/Breadcrumbs.svelte @@ -9,41 +9,42 @@ let crumbs: Array<{ label: string; href: string; icon?: string }> = []; function capitalizeSecondWord(sentence: string) { - var words = sentence.split(' '); + var words = sentence.split(' '); - if (words.length >= 2) { - words[1] = words[1].charAt(0).toUpperCase() + words[1].substring(1); - return words.join(''); - } else { - return sentence; - } -} + if (words.length >= 2) { + words[1] = words[1].charAt(0).toUpperCase() + words[1].substring(1); + return words.join(''); + } else { + return sentence; + } + } $: { // Remove zero-length tokens. const tokens = $page.url.pathname.split('/').filter((t) => t !== ''); + let title = ''; // Create { label, href } pairs for each token. let tokenPath = ''; crumbs = tokens.map((t) => { tokenPath += '/' + t; if (t === $breadcrumbObject.id) { - if ($breadcrumbObject.name) t = $breadcrumbObject.name; - else t = $breadcrumbObject.email; + if ($breadcrumbObject.name) title = $breadcrumbObject.name; + else title = $breadcrumbObject.email; } else if (t === 'folders') { t = 'domains'; } t = t.replace(/-/g, ' '); t = capitalizeSecondWord(t); return { - label: $page.data.label || t, + label: $page.data.label || title || t, href: Object.keys(listViewFields).includes(tokens[0]) ? tokenPath : null }; }); crumbs.unshift({ label: m.home(), href: '/', icon: 'fa-regular fa-compass' }); if (crumbs[crumbs.length - 1].label != 'edit') pageTitle.set(crumbs[crumbs.length - 1].label); - else pageTitle.set(m.edit()+ ' ' + crumbs[crumbs.length - 2].label); + else pageTitle.set(m.edit() + ' ' + crumbs[crumbs.length - 2].label); } diff --git a/frontend/src/lib/components/Calendar/Calendar.svelte b/frontend/src/lib/components/Calendar/Calendar.svelte index a7af659ac..3750e2e61 100644 --- a/frontend/src/lib/components/Calendar/Calendar.svelte +++ b/frontend/src/lib/components/Calendar/Calendar.svelte @@ -4,6 +4,8 @@ import Notifications from './Notifications.svelte'; import { showNotification } from '$lib/utils/stores'; + import * as m from '$paraglide/messages'; + export let info: object[]; let showNotificationBool = JSON.parse($showNotification); @@ -14,20 +16,28 @@ let daysInMonth = new Date(year, month, 0).getDate(); let firstDay = new Date(year, month - 1, 1).getDay(); - const daysOfWeek = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; + const daysOfWeek = [ + m.monday(), + m.tuesday(), + m.wednesday(), + m.thursday(), + m.friday(), + m.saturday(), + m.sunday() + ]; const monthNames = [ - 'january', - 'february', - 'march', - 'april', - 'may', - 'june', - 'july', - 'august', - 'september', - 'october', - 'november', - 'december' + m.january(), + m.february(), + m.march(), + m.april(), + m.may(), + m.june(), + m.july(), + m.august(), + m.september(), + m.october(), + m.november(), + m.december() ]; function todayDay() { @@ -112,19 +122,19 @@ class="font-light text-lg border rounded-lg border-white p-2 hover:bg-white text-white hover:text-primary-500 transition duration-300" > - Today + {m.today()} - + --> - {#if showNotificationBool} + diff --git a/frontend/src/lib/components/Chart/DonutChart.svelte b/frontend/src/lib/components/Chart/DonutChart.svelte index 684edde32..13c203b1f 100644 --- a/frontend/src/lib/components/Chart/DonutChart.svelte +++ b/frontend/src/lib/components/Chart/DonutChart.svelte @@ -3,13 +3,14 @@ import { localItems } from '$lib/utils/locales'; import { languageTag } from '$paraglide/runtime'; + export let name: string; export let s_label: string; export let values: any[]; // Set the types for these variables later on export let colors: string[] = []; for (const item in values) { - values[item]['name'] = localItems(languageTag())[values[item]['localName']] + values[item]['name'] = localItems(languageTag())[values[item]['localName']]; } let chart_element: HTMLElement | null = null; @@ -75,4 +76,4 @@ }); -
+
\ No newline at end of file diff --git a/frontend/src/lib/components/Forms/ModelForm.svelte b/frontend/src/lib/components/Forms/ModelForm.svelte index ce1a165b7..a371870a8 100644 --- a/frontend/src/lib/components/Forms/ModelForm.svelte +++ b/frontend/src/lib/components/Forms/ModelForm.svelte @@ -126,7 +126,13 @@ field="reviewers" label={m.reviewers()} /> - + {:else if URLModel === 'security-measures' || URLModel === 'policies'} {#if schema.shape.category} - {/if} +