diff --git a/backend/ebios_rm/models.py b/backend/ebios_rm/models.py
index 9fd9c0cd3..b7c8a7081 100644
--- a/backend/ebios_rm/models.py
+++ b/backend/ebios_rm/models.py
@@ -12,6 +12,7 @@
)
from iam.models import FolderMixin, User
from tprm.models import Entity
+import json
class EbiosRMStudy(NameDescriptionMixin, ETADueDateMixin, FolderMixin):
@@ -246,6 +247,13 @@ def get_pertinence(self):
PERTINENCE_MATRIX[self.motivation - 1][self.resources - 1]
).label
+ def get_gravity(self):
+ gravity = -1
+ for feared_event in self.feared_events.all():
+ if feared_event.gravity > gravity:
+ gravity = feared_event.gravity
+ return gravity
+
class Stakeholder(AbstractBaseModel, FolderMixin):
class Category(models.TextChoices):
@@ -410,6 +418,10 @@ def save(self, *args, **kwargs):
self.folder = self.ebios_rm_study.folder
super().save(*args, **kwargs)
+ @property
+ def gravity(self):
+ return self.ro_to_couple.get_gravity()
+
class OperationalScenario(AbstractBaseModel, FolderMixin):
ebios_rm_study = models.ForeignKey(
@@ -458,6 +470,27 @@ def risk_matrix(self):
def parsed_matrix(self):
return self.risk_matrix.parse_json_translated()
+ @property
+ def ref_id(self):
+ sorted_operational_scenarios = list(
+ OperationalScenario.objects.filter(
+ ebios_rm_study=self.ebios_rm_study
+ ).order_by("created_at")
+ )
+ return sorted_operational_scenarios.index(self) + 1
+
+ @property
+ def gravity(self):
+ return self.attack_path.gravity
+
+ @property
+ def stakeholders(self):
+ return self.attack_path.stakeholders.all()
+
+ @property
+ def ro_to(self):
+ return self.attack_path.ro_to_couple
+
def get_likelihood_display(self):
if self.likelihood < 0:
return {
@@ -465,9 +498,39 @@ def get_likelihood_display(self):
"name": "--",
"description": "not rated",
"value": -1,
+ "hexcolor": "#f9fafb",
}
risk_matrix = self.parsed_matrix
return {
**risk_matrix["probability"][self.likelihood],
"value": self.likelihood,
}
+
+ def get_gravity_display(self):
+ if self.gravity < 0:
+ return {
+ "abbreviation": "--",
+ "name": "--",
+ "description": "not rated",
+ "value": -1,
+ }
+ risk_matrix = self.parsed_matrix
+ return {
+ **risk_matrix["impact"][self.gravity],
+ "value": self.gravity,
+ }
+
+ def get_risk_level_display(self):
+ if self.likelihood < 0 or self.gravity < 0:
+ return {
+ "abbreviation": "--",
+ "name": "--",
+ "description": "not rated",
+ "value": -1,
+ }
+ risk_matrix = self.parsed_matrix
+ risk_index = risk_matrix["grid"][self.likelihood][self.gravity]
+ return {
+ **risk_matrix["risk"][risk_index],
+ "value": risk_index,
+ }
diff --git a/backend/ebios_rm/serializers.py b/backend/ebios_rm/serializers.py
index 041b5904f..0b0ec8a72 100644
--- a/backend/ebios_rm/serializers.py
+++ b/backend/ebios_rm/serializers.py
@@ -165,9 +165,14 @@ class OperationalScenarioReadSerializer(BaseModelSerializer):
str = serializers.CharField(source="__str__")
ebios_rm_study = FieldsRelatedField()
folder = FieldsRelatedField()
- attack_path = FieldsRelatedField()
+ attack_path = FieldsRelatedField(["id", "name", "description"])
+ stakeholders = FieldsRelatedField(many=True)
+ ro_to = FieldsRelatedField(["risk_origin", "target_objective"])
threats = FieldsRelatedField(many=True)
likelihood = serializers.JSONField(source="get_likelihood_display")
+ gravity = serializers.JSONField(source="get_gravity_display")
+ risk_level = serializers.JSONField(source="get_risk_level_display")
+ ref_id = serializers.CharField()
class Meta:
model = OperationalScenario
diff --git a/backend/ebios_rm/views.py b/backend/ebios_rm/views.py
index 6d35d48a1..5c9515d04 100644
--- a/backend/ebios_rm/views.py
+++ b/backend/ebios_rm/views.py
@@ -1,3 +1,4 @@
+import django_filters as df
from core.serializers import RiskMatrixReadSerializer
from core.views import BaseModelViewSet as AbstractBaseModelViewSet
from core.serializers import RiskMatrixReadSerializer
@@ -123,12 +124,23 @@ def category(self, request):
return Response(dict(Stakeholder.Category.choices))
+class AttackPathFilter(df.FilterSet):
+ used = df.BooleanFilter(method="is_used", label="Used")
+
+ def is_used(self, queryset, name, value):
+ if value:
+ return queryset.filter(operational_scenario__isnull=False)
+ return queryset.filter(operational_scenario__isnull=True)
+
+ class Meta:
+ model = AttackPath
+ fields = ["ebios_rm_study", "is_selected", "used"]
+
+
class AttackPathViewSet(BaseModelViewSet):
model = AttackPath
- filterset_fields = [
- "ebios_rm_study",
- ]
+ filterset_class = AttackPathFilter
class OperationalScenarioViewSet(BaseModelViewSet):
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index 659e7ba99..30c486f51 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -983,9 +983,6 @@
"currentCriticality": "Current criticality",
"residualCriticality": "Residual criticality",
"notSelected": "Not selected",
- "identifyRoTo": "Identify RO/TO",
- "evaluateRoTo": "Evaluate RO/TO",
- "selectRoTo": "Select RO/TO",
"resetPasswordHere": "You can reset your password here.",
"resetPassword": "Reset password",
"ebiosRm": "Ebios RM",
@@ -996,6 +993,19 @@
"noAuthor": "No author assigned",
"noReviewer": "No reviewer assigned",
"selectAudit": "Select audit",
+ "operationalScenario": "Operational scenario",
+ "operationalScenarioRefId": "Operational scenario {refId}",
+ "operationalScenarios": "Operational scenarios",
+ "addOperationalScenario": "Add operational scenario",
+ "workshopFour": "Workshop 4",
+ "noThreat": "No threat",
+ "likely": "Likely",
+ "unlikely": "Unlikely",
+ "veryLikely": "Very likely",
+ "certain": "Certain",
+ "minor": "Minor",
+ "operatingModesDescription": "Operating modes description",
+ "noStakeholders": "No stakeholders",
"errorAssetGraphMustNotContainCycles": "The asset graph must not contain cycles.",
"addStakeholder": "Add stakeholder"
}
diff --git a/frontend/src/lib/components/Forms/ModelForm.svelte b/frontend/src/lib/components/Forms/ModelForm.svelte
index 8ca71675b..939955bce 100644
--- a/frontend/src/lib/components/Forms/ModelForm.svelte
+++ b/frontend/src/lib/components/Forms/ModelForm.svelte
@@ -273,7 +273,7 @@
{:else if URLModel === 'attack-paths'}
{:else if URLModel === 'operational-scenarios'}
-
+
{/if}
{#if closeModal}
diff --git a/frontend/src/lib/components/Forms/ModelForm/OperationalScenarioForm.svelte b/frontend/src/lib/components/Forms/ModelForm/OperationalScenarioForm.svelte
index f1eaffd9a..116a1205c 100644
--- a/frontend/src/lib/components/Forms/ModelForm/OperationalScenarioForm.svelte
+++ b/frontend/src/lib/components/Forms/ModelForm/OperationalScenarioForm.svelte
@@ -7,12 +7,27 @@
import { getOptions } from '$lib/utils/crud';
import TextArea from '../TextArea.svelte';
import Select from '../Select.svelte';
+ import { page } from '$app/stores';
export let form: SuperValidated
;
export let model: ModelInfo;
export let cacheLocks: Record = {};
export let formDataCache: Record = {};
export let initialData: Record = {};
+ export let context: string;
+
+ const activityBackground = context === 'edit' ? 'bg-white' : 'bg-surface-100-800-token';
+
+ let activeActivity: string | null = null;
+ $page.url.searchParams.forEach((value, key) => {
+ if (key === 'activity' && value === 'one') {
+ activeActivity = 'one';
+ } else if (key === 'activity' && value === 'two') {
+ activeActivity = 'two';
+ } else if (key === 'activity' && value === 'three') {
+ activeActivity = 'three';
+ }
+ });
-
-
-
-
-
+
+
+ {m.activityOne()}
+
+
+
+ {#if context !== 'edit'}
+
+ {/if}
+
+
+
+ {m.activityTwo()}
+
+
+
+
+
diff --git a/frontend/src/lib/utils/crud.ts b/frontend/src/lib/utils/crud.ts
index e6b4c3f30..a076556f0 100644
--- a/frontend/src/lib/utils/crud.ts
+++ b/frontend/src/lib/utils/crud.ts
@@ -682,14 +682,6 @@ export const URL_MODEL_MAP: ModelMap = {
{ field: 'folder', urlModel: 'folders', urlParams: 'content_type=DO' }
]
},
- attack_paths: {
- endpointUrl: 'ebios-rm/attack-paths',
- name: 'attackpath',
- localName: 'attackPath',
- localNamePlural: 'attackPaths',
- verboseName: 'Attack path',
- verboseNamePlural: 'Attack paths'
- },
'operational-scenarios': {
endpointUrl: 'ebios-rm/operational-scenarios',
name: 'operationalscenario',
@@ -700,9 +692,15 @@ export const URL_MODEL_MAP: ModelMap = {
foreignKeyFields: [
{ field: 'ebios_rm_study', urlModel: 'ebios-rm' },
{ field: 'threats', urlModel: 'threats' },
- { field: 'attack_path', urlModel: 'attack-paths' }
+ {
+ field: 'attack_path',
+ urlModel: 'attack-paths',
+ endpointUrl: 'ebios-rm/attack-paths',
+ urlParams: 'is_selected=true&used=false&ebios_rm_study=',
+ detail: true
+ }
],
- selectFields: [{ field: 'likelihood', valueType: 'number' }]
+ selectFields: [{ field: 'likelihood', valueType: 'number', detail: true }]
}
};
diff --git a/frontend/src/lib/utils/schemas.ts b/frontend/src/lib/utils/schemas.ts
index ffaf67a4e..73715c548 100644
--- a/frontend/src/lib/utils/schemas.ts
+++ b/frontend/src/lib/utils/schemas.ts
@@ -458,7 +458,7 @@ export const operationalScenarioSchema = z.object({
ebios_rm_study: z.string(),
attack_path: z.string().uuid(),
threats: z.string().uuid().optional().array().optional(),
- description: z.string(),
+ operating_modes_description: z.string(),
likelihood: z.number().optional().default(-1),
is_selected: z.boolean().optional().default(false),
justification: z.string().optional()
diff --git a/frontend/src/lib/utils/table.ts b/frontend/src/lib/utils/table.ts
index e1f76b0ae..5ba4451c7 100644
--- a/frontend/src/lib/utils/table.ts
+++ b/frontend/src/lib/utils/table.ts
@@ -601,7 +601,7 @@ export const listViewFields: ListViewFieldsConfig = {
body: ['ref_id', 'name', 'risk_origin', 'target_objective', 'stakeholders', 'description']
},
'operational-scenarios': {
- head: ['description', 'threats', 'likelihood'],
- body: ['description', 'threats', 'likelihood']
+ head: ['operatingModesDescription', 'threats', 'likelihood'],
+ body: ['operating_modes_description', 'threats', 'likelihood']
}
};
diff --git a/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/edit/+layout.server.ts b/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/edit/+layout.server.ts
index 10341dc6a..dadfb90bb 100644
--- a/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/edit/+layout.server.ts
+++ b/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/edit/+layout.server.ts
@@ -52,8 +52,11 @@ export const load: LayoutServerLoad = async (event) => {
let url = keyModel.endpointUrl
? `${BASE_API_URL}/${keyModel.endpointUrl}/${queryParams}`
: `${BASE_API_URL}/${keyModel.urlModel}/${queryParams}`;
- if (keyModel.urlModel === 'assets' && event.params.model === 'feared-events') {
- url = `${BASE_API_URL}/${keyModel.urlModel}/${queryParams}${object.ebios_rm_study}`;
+ if (
+ ['assets', 'attack-paths'].includes(keyModel.urlModel) &&
+ ['feared-events', 'operational-scenarios'].includes(event.params.model)
+ ) {
+ url = `${BASE_API_URL}/${keyModel.endpointUrl || keyModel.urlModel}/${queryParams}${object.ebios_rm_study}`;
}
const response = await event.fetch(url);
if (response.ok) {
diff --git a/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/edit/+page.svelte b/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/edit/+page.svelte
index 8658525a0..5551caac7 100644
--- a/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/edit/+page.svelte
+++ b/frontend/src/routes/(app)/(internal)/[model=urlmodel]/[id=uuid]/edit/+page.svelte
@@ -5,6 +5,8 @@
export let data: PageData;
+ const customNameDescription = ['operational-scenarios'].includes(data.model.urlModel);
+
breadcrumbObject.set(data.object);
@@ -14,5 +16,6 @@
selectOptions={data.selectOptions}
foreignKeys={data.foreignKeys}
model={data.model}
+ {customNameDescription}
context="edit"
/>
diff --git a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/+page.server.ts b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/+page.server.ts
index 06293b581..ad26bedfc 100644
--- a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/+page.server.ts
+++ b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/+page.server.ts
@@ -33,12 +33,11 @@ export const load: PageServerLoad = async ({ params, fetch }) => {
export const actions: Actions = {
create: async (event) => {
- // const redirectToWrittenObject = Boolean(event.params.model === 'entity-assessments');
return defaultWriteFormAction({
event,
urlModel: 'risk-assessments',
- action: 'create'
- // redirectToWrittenObject: redirectToWrittenObject
+ action: 'create',
+ redirectToWrittenObject: true
});
}
};
diff --git a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/+page.svelte b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/+page.svelte
index 35c997cad..6525cac63 100644
--- a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/+page.svelte
+++ b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/+page.svelte
@@ -3,16 +3,20 @@
import { safeTranslate } from '$lib/utils/i18n';
import Tile from './Tile.svelte';
import { page } from '$app/stores';
- import type { PageData } from './$types';
+ import type { PageData, ActionData } from './$types';
import { breadcrumbObject } from '$lib/utils/stores';
import type { ModalComponent, ModalSettings, ModalStore } from '@skeletonlabs/skeleton';
import { getModalStore } from '@skeletonlabs/skeleton';
import CreateModal from '$lib/components/Modals/CreateModal.svelte';
import MissingConstraintsModal from '$lib/components/Modals/MissingConstraintsModal.svelte';
import { checkConstraints } from '$lib/utils/crud';
+ import { goto } from '$app/navigation';
+ import { getSecureRedirect } from '$lib/utils/helpers';
+
const modalStore: ModalStore = getModalStore();
export let data: PageData;
+ export let form: ActionData;
$: breadcrumbObject.set(data.data);
@@ -79,12 +83,12 @@
{
title: safeTranslate(m.ebiosWs4_1()),
status: 'to_do',
- href: `${$page.url.pathname}/workshop-four/operational-scenario?next=${$page.url.pathname}`
+ href: `${$page.url.pathname}/workshop-four/operational-scenario?activity=one&next=${$page.url.pathname}`
},
{
title: safeTranslate(m.ebiosWs4_2()),
status: 'to_do',
- href: `${$page.url.pathname}/workshop-four/operational-scenario?next=${$page.url.pathname}`
+ href: `${$page.url.pathname}/workshop-four/operational-scenario?activity=two&next=${$page.url.pathname}`
}
],
ws5: [
@@ -146,6 +150,10 @@
}
modalStore.trigger(modal);
}
+
+ $: if (form && form.redirect) {
+ goto(getSecureRedirect(form.redirect));
+ }
diff --git a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-four/operational-scenario/+page.server.ts b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-four/operational-scenario/+page.server.ts
index 62a12af03..dc296d006 100644
--- a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-four/operational-scenario/+page.server.ts
+++ b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-four/operational-scenario/+page.server.ts
@@ -36,7 +36,9 @@ export const load: PageServerLoad = async ({ params, fetch }) => {
for (const keyField of foreignKeyFields) {
const keyModel = getModelInfo(keyField.urlModel);
- const queryParams = keyField.urlParams ? `?${keyField.urlParams}` : '';
+ const queryParams = keyField.urlParams
+ ? `?${keyField.urlParams}${keyField.detail ? params.id : ''}`
+ : '';
const url = keyModel.endpointUrl
? `${BASE_API_URL}/${keyModel.endpointUrl}/${queryParams}`
: `${BASE_API_URL}/${keyModel.urlModel}/${queryParams}`;
diff --git a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-four/operational-scenario/+page.svelte b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-four/operational-scenario/+page.svelte
index 08ebedae8..feeb89c3f 100644
--- a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-four/operational-scenario/+page.svelte
+++ b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-four/operational-scenario/+page.svelte
@@ -8,6 +8,7 @@
import MissingConstraintsModal from '$lib/components/Modals/MissingConstraintsModal.svelte';
import { checkConstraints } from '$lib/utils/crud';
import * as m from '$paraglide/messages.js';
+ import { page } from '$app/stores';
const modalStore: ModalStore = getModalStore();
@@ -20,7 +21,8 @@
ref: CreateModal,
props: {
form: data.createForm,
- model: data.model
+ model: data.model,
+ customNameDescription: true
}
};
let modal: ModalSettings = {
@@ -50,9 +52,25 @@
}
modalStore.trigger(modal);
}
+
+ let activeActivity: string | null = null;
+ $page.url.searchParams.forEach((value, key) => {
+ if (key === 'activity' && value === 'one') {
+ activeActivity = 'one';
+ } else if (key === 'activity' && value === 'two') {
+ activeActivity = 'two';
+ } else if (key === 'activity' && value === 'three') {
+ activeActivity = 'three';
+ }
+ });
-
+