diff --git a/backend/ebios_rm/models.py b/backend/ebios_rm/models.py
index 044c6bc49..3473f896f 100644
--- a/backend/ebios_rm/models.py
+++ b/backend/ebios_rm/models.py
@@ -230,6 +230,9 @@ class Pertinence(models.IntegerChoices):
is_selected = models.BooleanField(verbose_name=_("Is selected"), default=False)
justification = models.TextField(verbose_name=_("Justification"), blank=True)
+ def __str__(self) -> str:
+ return f"{self.get_risk_origin_display()} - {self.target_objective}"
+
class Meta:
verbose_name = _("RO/TO couple")
verbose_name_plural = _("RO/TO couples")
@@ -322,6 +325,9 @@ class Meta:
verbose_name_plural = _("Stakeholders")
ordering = ["created_at"]
+ def __str__(self):
+ return f"{self.entity.name} - {self.get_category_display()}"
+
def save(self, *args, **kwargs):
self.folder = self.ebios_rm_study.folder
super().save(*args, **kwargs)
diff --git a/backend/ebios_rm/serializers.py b/backend/ebios_rm/serializers.py
index 4b5a0d9d2..4ce91514d 100644
--- a/backend/ebios_rm/serializers.py
+++ b/backend/ebios_rm/serializers.py
@@ -139,6 +139,8 @@ class AttackPathReadSerializer(BaseModelSerializer):
folder = FieldsRelatedField()
ro_to_couple = FieldsRelatedField()
stakeholders = FieldsRelatedField(many=True)
+ risk_origin = serializers.CharField(source="ro_to_couple.get_risk_origin_display")
+ target_objective = serializers.CharField(source="ro_to_couple.target_objective")
class Meta:
model = AttackPath
diff --git a/backend/ebios_rm/views.py b/backend/ebios_rm/views.py
index 8fcaf6ce9..833a576f9 100644
--- a/backend/ebios_rm/views.py
+++ b/backend/ebios_rm/views.py
@@ -101,6 +101,10 @@ def pertinence(self, request):
class StakeholderViewSet(BaseModelViewSet):
model = Stakeholder
+ filterset_fields = [
+ "ebios_rm_study",
+ ]
+
@action(detail=False, name="Get category choices")
def category(self, request):
return Response(dict(Stakeholder.Category.choices))
@@ -109,6 +113,10 @@ def category(self, request):
class AttackPathViewSet(BaseModelViewSet):
model = AttackPath
+ filterset_fields = [
+ "ebios_rm_study",
+ ]
+
class OperationalScenarioViewSet(BaseModelViewSet):
model = OperationalScenario
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index 0da5fd9e2..931583238 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -955,6 +955,7 @@
"fairlyRelevant": "Fairly relevant",
"highlyRelevant": "Highly relevant",
"roTo": "RO/TO",
+ "roToCouple": "RO/TO couple",
"addRoto": "Add RO/TO couple",
"organizedCrime": "Organized crime",
"terrorist": "Terrorist",
@@ -962,5 +963,19 @@
"professional": "Professional",
"amateur": "Amateur",
"avenger": "Avenger",
- "pathological": "Pathological"
+ "pathological": "Pathological",
+ "currentDependency": "Current dependency",
+ "currentPenetration": "Current penetration",
+ "currentMaturity": "Current maturity",
+ "currentTrust": "Current trust",
+ "residualDependency": "Residual dependency",
+ "residualPenetration": "Residual penetration",
+ "residualMaturity": "Residual maturity",
+ "residualTrust": "Residual trust",
+ "selected": "Selected",
+ "likelihood": "Likelihood",
+ "stakeholders": "Stakeholders",
+ "addAttackPath": "Add attack path",
+ "currentCriticality": "Current criticality",
+ "residualCriticality": "Residual criticality"
}
diff --git a/frontend/src/lib/components/Forms/ModelForm.svelte b/frontend/src/lib/components/Forms/ModelForm.svelte
index 3866c9d84..e0398bbe8 100644
--- a/frontend/src/lib/components/Forms/ModelForm.svelte
+++ b/frontend/src/lib/components/Forms/ModelForm.svelte
@@ -29,6 +29,8 @@
import EbiosRmForm from './ModelForm/EbiosRmForm.svelte';
import FearedEventForm from './ModelForm/FearedEventForm.svelte';
import RoToForm from './ModelForm/RoToForm.svelte';
+ import StakeholderForm from './ModelForm/StakeholderForm.svelte';
+ import AttackPathForm from './ModelForm/AttackPathForm.svelte';
import AutocompleteSelect from './AutocompleteSelect.svelte';
@@ -264,6 +266,10 @@
{:else if URLModel === 'ro-to'}
+ {:else if URLModel === 'stakeholders'}
+
+ {:else if URLModel === 'attack-paths'}
+
{/if}
{#if closeModal}
diff --git a/frontend/src/lib/components/Forms/ModelForm/AttackPathForm.svelte b/frontend/src/lib/components/Forms/ModelForm/AttackPathForm.svelte
new file mode 100644
index 000000000..321ce2e8c
--- /dev/null
+++ b/frontend/src/lib/components/Forms/ModelForm/AttackPathForm.svelte
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/lib/components/Forms/ModelForm/StakeholderForm.svelte b/frontend/src/lib/components/Forms/ModelForm/StakeholderForm.svelte
new file mode 100644
index 000000000..562fcff1e
--- /dev/null
+++ b/frontend/src/lib/components/Forms/ModelForm/StakeholderForm.svelte
@@ -0,0 +1,116 @@
+
+
+
+
+
+{#if context === 'edit'}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{/if}
diff --git a/frontend/src/lib/utils/crud.ts b/frontend/src/lib/utils/crud.ts
index b0e762ebe..e35c4c1ab 100644
--- a/frontend/src/lib/utils/crud.ts
+++ b/frontend/src/lib/utils/crud.ts
@@ -103,6 +103,7 @@ export const getOptions = ({
interface ForeignKeyField {
field: string;
urlModel: urlModel;
+ endpointUrl?: string;
urlParams?: string;
}
@@ -618,7 +619,7 @@ export const URL_MODEL_MAP: ModelMap = {
verboseName: 'Feared event',
verboseNamePlural: 'Feared events',
foreignKeyFields: [
- { field: 'ebios_rm_study', urlModel: 'ebios-rm' },
+ { field: 'ebios_rm_study', urlModel: 'ebios-rm', endpointUrl: 'ebios-rm/studies' },
{ field: 'assets', urlModel: 'assets' },
{ field: 'qualifications', urlModel: 'qualifications' }
],
@@ -632,8 +633,8 @@ export const URL_MODEL_MAP: ModelMap = {
verboseName: 'Ro to',
verboseNamePlural: 'Ro to',
foreignKeyFields: [
- { field: 'ebios_rm_study', urlModel: 'ebios-rm' },
- { field: 'feared_events', urlModel: 'feared-events' }
+ { field: 'ebios_rm_study', urlModel: 'ebios-rm', endpointUrl: 'ebios-rm/studies' },
+ { field: 'feared_events', urlModel: 'feared-events', endpointUrl: 'ebios-rm/feared-events' }
],
selectFields: [
{ field: 'risk-origin' },
@@ -641,6 +642,35 @@ export const URL_MODEL_MAP: ModelMap = {
{ field: 'resources', valueType: 'number' },
{ field: 'pertinence', valueType: 'number' }
]
+ },
+ stakeholders: {
+ endpointUrl: 'ebios-rm/stakeholders',
+ name: 'stakeholder',
+ localName: 'stakeholder',
+ localNamePlural: 'stakeholders',
+ verboseName: 'Stakeholder',
+ verboseNamePlural: 'Stakeholders',
+ foreignKeyFields: [
+ { field: 'entity', urlModel: 'entities' },
+ { field: 'applied_controls', urlModel: 'applied-controls' },
+ { field: 'ebios_rm_study', urlModel: 'ebios-rm', endpointUrl: 'ebios-rm/studies' },
+ { field: 'folder', urlModel: 'folders', urlParams: 'content_type=DO' }
+ ],
+ selectFields: [{ field: 'category' }]
+ },
+ 'attack-paths': {
+ endpointUrl: 'ebios-rm/attack-paths',
+ name: 'attackpath',
+ localName: 'attackPath',
+ localNamePlural: 'attackPaths',
+ verboseName: 'Attack path',
+ verboseNamePlural: 'Attack paths',
+ foreignKeyFields: [
+ { field: 'stakeholders', urlModel: 'stakeholders', endpointUrl: 'ebios-rm/stakeholders' },
+ { field: 'ro_to_couple', urlModel: 'ro-to', endpointUrl: 'ebios-rm/ro-to' },
+ { field: 'ebios_rm_study', urlModel: 'ebios-rm', endpointUrl: 'ebios-rm/studies' },
+ { field: 'folder', urlModel: 'folders', urlParams: 'content_type=DO' }
+ ]
}
};
diff --git a/frontend/src/lib/utils/schemas.ts b/frontend/src/lib/utils/schemas.ts
index ed65b698d..f8c18ec75 100644
--- a/frontend/src/lib/utils/schemas.ts
+++ b/frontend/src/lib/utils/schemas.ts
@@ -426,6 +426,32 @@ export const roToSchema = z.object({
justification: z.string().optional()
});
+export const StakeholderSchema = z.object({
+ ebios_rm_study: z.string(),
+ applied_controls: z.string().uuid().optional().array().optional(),
+ category: z.string().optional(),
+ entity: z.string().optional(),
+ current_dependency: z.number().min(0).max(4).optional(),
+ current_penetration: z.number().min(0).max(4).optional(),
+ current_maturity: z.number().min(1).max(4).optional(),
+ current_trust: z.number().min(1).max(4).optional(),
+ residual_dependency: z.number().min(0).max(4).optional(),
+ residual_penetration: z.number().min(0).max(4).optional(),
+ residual_maturity: z.number().min(1).max(4).optional(),
+ residual_trust: z.number().min(1).max(4).optional(),
+ is_selected: z.boolean().optional(),
+ justification: z.string().optional()
+});
+
+export const AttackPathSchema = z.object({
+ ebios_rm_study: z.string(),
+ ro_to_couple: z.string().uuid(),
+ stakeholders: z.string().uuid().array(),
+ description: z.string(),
+ is_selected: z.boolean().optional(),
+ justification: z.string().optional()
+});
+
const SCHEMA_MAP: Record
= {
folders: FolderSchema,
projects: ProjectSchema,
@@ -453,7 +479,9 @@ const SCHEMA_MAP: Record = {
'filtering-labels': FilteringLabelSchema,
'ebios-rm': ebiosRMSchema,
'feared-events': fearedEventsSchema,
- 'ro-to': roToSchema
+ 'ro-to': roToSchema,
+ stakeholders: StakeholderSchema,
+ 'attack-paths': AttackPathSchema
};
export const modelSchema = (model: string) => {
diff --git a/frontend/src/lib/utils/table.ts b/frontend/src/lib/utils/table.ts
index 07e8f9bc3..22d6ada9d 100644
--- a/frontend/src/lib/utils/table.ts
+++ b/frontend/src/lib/utils/table.ts
@@ -573,5 +573,13 @@ export const listViewFields: ListViewFieldsConfig = {
'feared_events',
'pertinence'
]
+ },
+ stakeholders: {
+ head: ['entity', 'category', 'current_criticality', 'applied_controls', 'residual_criticality'],
+ body: ['entity', 'category', 'current_criticality', 'applied_controls', 'residual_criticality']
+ },
+ 'attack-paths': {
+ head: ['risk_origin', 'target_objective', 'stakeholders', 'attackPath'],
+ body: ['risk_origin', 'target_objective', 'stakeholders', 'description']
}
};
diff --git a/frontend/src/lib/utils/types.ts b/frontend/src/lib/utils/types.ts
index 0e1dde450..2986b7790 100644
--- a/frontend/src/lib/utils/types.ts
+++ b/frontend/src/lib/utils/types.ts
@@ -53,7 +53,9 @@ export const URL_MODEL = [
'vulnerabilities',
'filtering-labels',
'feared-events',
- 'ro-to'
+ 'ro-to',
+ 'stakeholders',
+ 'attack-paths'
// 'ebios-rm',
] as const;
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 4929466a5..02e16a5f9 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
@@ -51,9 +51,21 @@
}
],
ws3: [
- { title: safeTranslate(m.ebiosWs3_1()), status: 'to_do', href: '#' },
- { title: safeTranslate(m.ebiosWs3_2()), status: 'to_do', href: '#' },
- { title: safeTranslate(m.ebiosWs3_3()), status: 'done', href: '#' }
+ {
+ title: safeTranslate(m.ebiosWs3_1()),
+ status: 'to_do',
+ href: `${$page.url.pathname}/workshop-three/ecosystem?next=${$page.url.pathname}`
+ },
+ {
+ title: safeTranslate(m.ebiosWs3_2()),
+ status: 'to_do',
+ href: `${$page.url.pathname}/workshop-three/strategic-scenarios?next=${$page.url.pathname}`
+ },
+ {
+ title: safeTranslate(m.ebiosWs3_3()),
+ status: 'done',
+ href: `${$page.url.pathname}/workshop-three/ecosystem?next=${$page.url.pathname}`
+ }
],
ws4: [
{ title: safeTranslate(m.ebiosWs4_1()), status: 'to_do', href: '#' },
diff --git a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-three/ecosystem/+page.server.ts b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-three/ecosystem/+page.server.ts
new file mode 100644
index 000000000..3449f6aa3
--- /dev/null
+++ b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-three/ecosystem/+page.server.ts
@@ -0,0 +1,104 @@
+import { defaultDeleteFormAction, defaultWriteFormAction } from '$lib/utils/actions';
+import { BASE_API_URL } from '$lib/utils/constants';
+import { getModelInfo, urlParamModelForeignKeyFields } from '$lib/utils/crud';
+import { modelSchema } from '$lib/utils/schemas';
+import type { ModelInfo, urlModel } from '$lib/utils/types';
+import { type Actions } from '@sveltejs/kit';
+import { superValidate } from 'sveltekit-superforms';
+import { zod } from 'sveltekit-superforms/adapters';
+import { z } from 'zod';
+import type { PageServerLoad } from './$types';
+import { listViewFields } from '$lib/utils/table';
+import { tableSourceMapper, type TableSource } from '@skeletonlabs/skeleton';
+
+export const load: PageServerLoad = async ({ params, fetch }) => {
+ const schema = z.object({ id: z.string().uuid() });
+ const deleteForm = await superValidate(zod(schema));
+ const URLModel = 'stakeholders';
+ const createSchema = modelSchema(URLModel);
+ const initialData = {
+ ebios_rm_study: params.id
+ };
+ const createForm = await superValidate(initialData, zod(createSchema), { errors: false });
+ const model: ModelInfo = getModelInfo(URLModel);
+ const foreignKeyFields = urlParamModelForeignKeyFields(URLModel);
+
+ const selectOptions: Record = {};
+ if (model.selectFields) {
+ await Promise.all(
+ model.selectFields.map(async (selectField) => {
+ const url = model.endpointUrl
+ ? `${BASE_API_URL}/${model.endpointUrl}/${selectField.field}`
+ : `${BASE_API_URL}/${model.urlModel}/${selectField.field}`;
+ const response = await fetch(url);
+ if (!response.ok) {
+ console.error(`Failed to fetch data from ${url}: ${response.statusText}`);
+ return null;
+ }
+ const data = await response.json();
+ if (data) {
+ selectOptions[selectField.field] = Object.entries(data).map(([key, value]) => ({
+ label: value,
+ value: key
+ }));
+ }
+ })
+ );
+ }
+ model.selectOptions = selectOptions;
+
+ const foreignKeys: Record = {};
+
+ for (const keyField of foreignKeyFields) {
+ const model = getModelInfo(keyField.urlModel);
+ const queryParams = keyField.urlParams ? `?${keyField.urlParams}` : '';
+ const url = model.endpointUrl
+ ? `${BASE_API_URL}/${model.endpointUrl}/${queryParams}`
+ : `${BASE_API_URL}/${model.urlModel}/${queryParams}`;
+ const response = await fetch(url);
+ if (response.ok) {
+ foreignKeys[keyField.field] = await response.json().then((data) => data.results);
+ } else {
+ console.error(`Failed to fetch data for ${keyField.field}: ${response.statusText}`);
+ }
+ }
+
+ model['foreignKeys'] = foreignKeys;
+
+ const endpoint = `${BASE_API_URL}/${model.endpointUrl}?ebios_rm_study=${params.id}`;
+ const res = await fetch(endpoint);
+ const data = await res.json().then((res) => res.results);
+
+ const bodyData = tableSourceMapper(data, listViewFields[URLModel as urlModel].body);
+
+ const headData: Record = listViewFields[URLModel as urlModel].body.reduce(
+ (obj, key, index) => {
+ obj[key] = listViewFields[URLModel as urlModel].head[index];
+ return obj;
+ },
+ {}
+ );
+
+ const table: TableSource = {
+ head: headData,
+ body: bodyData,
+ meta: data // metaData
+ };
+
+ return { createForm, deleteForm, model, URLModel, table };
+};
+
+export const actions: Actions = {
+ create: async (event) => {
+ // const redirectToWrittenObject = Boolean(event.params.model === 'entity-assessments');
+ return defaultWriteFormAction({
+ event,
+ urlModel: 'stakeholders',
+ action: 'create'
+ // redirectToWrittenObject: redirectToWrittenObject
+ });
+ },
+ delete: async (event) => {
+ return defaultDeleteFormAction({ event, urlModel: 'stakeholders' });
+ }
+};
diff --git a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-three/ecosystem/+page.svelte b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-three/ecosystem/+page.svelte
new file mode 100644
index 000000000..08ebedae8
--- /dev/null
+++ b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-three/ecosystem/+page.svelte
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-three/strategic-scenarios/+page.server.ts b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-three/strategic-scenarios/+page.server.ts
new file mode 100644
index 000000000..2b31addc0
--- /dev/null
+++ b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-three/strategic-scenarios/+page.server.ts
@@ -0,0 +1,104 @@
+import { defaultDeleteFormAction, defaultWriteFormAction } from '$lib/utils/actions';
+import { BASE_API_URL } from '$lib/utils/constants';
+import { getModelInfo, urlParamModelForeignKeyFields } from '$lib/utils/crud';
+import { modelSchema } from '$lib/utils/schemas';
+import type { ModelInfo, urlModel } from '$lib/utils/types';
+import { type Actions } from '@sveltejs/kit';
+import { superValidate } from 'sveltekit-superforms';
+import { zod } from 'sveltekit-superforms/adapters';
+import { z } from 'zod';
+import type { PageServerLoad } from './$types';
+import { listViewFields } from '$lib/utils/table';
+import { tableSourceMapper, type TableSource } from '@skeletonlabs/skeleton';
+
+export const load: PageServerLoad = async ({ params, fetch }) => {
+ const schema = z.object({ id: z.string().uuid() });
+ const deleteForm = await superValidate(zod(schema));
+ const URLModel = 'attack-paths';
+ const createSchema = modelSchema(URLModel);
+ const initialData = {
+ ebios_rm_study: params.id
+ };
+ const createForm = await superValidate(initialData, zod(createSchema), { errors: false });
+ const model: ModelInfo = getModelInfo(URLModel);
+ const foreignKeyFields = urlParamModelForeignKeyFields(URLModel);
+
+ const selectOptions: Record = {};
+ if (model.selectFields) {
+ await Promise.all(
+ model.selectFields.map(async (selectField) => {
+ const url = model.endpointUrl
+ ? `${BASE_API_URL}/${model.endpointUrl}/${selectField.field}`
+ : `${BASE_API_URL}/${model.urlModel}/${selectField.field}`;
+ const response = await fetch(url);
+ if (!response.ok) {
+ console.error(`Failed to fetch data from ${url}: ${response.statusText}`);
+ return null;
+ }
+ const data = await response.json();
+ if (data) {
+ selectOptions[selectField.field] = Object.entries(data).map(([key, value]) => ({
+ label: value,
+ value: key
+ }));
+ }
+ })
+ );
+ }
+ model.selectOptions = selectOptions;
+
+ const foreignKeys: Record = {};
+
+ for (const keyField of foreignKeyFields) {
+ const model = getModelInfo(keyField.urlModel);
+ const queryParams = keyField.urlParams ? `?${keyField.urlParams}` : '';
+ const url = model.endpointUrl
+ ? `${BASE_API_URL}/${model.endpointUrl}/${queryParams}`
+ : `${BASE_API_URL}/${model.urlModel}/${queryParams}`;
+ const response = await fetch(url);
+ if (response.ok) {
+ foreignKeys[keyField.field] = await response.json().then((data) => data.results);
+ } else {
+ console.error(`Failed to fetch data for ${keyField.field}: ${response.statusText}`);
+ }
+ }
+
+ model['foreignKeys'] = foreignKeys;
+
+ const endpoint = `${BASE_API_URL}/${model.endpointUrl}?ebios_rm_study=${params.id}`;
+ const res = await fetch(endpoint);
+ const data = await res.json().then((res) => res.results);
+
+ const bodyData = tableSourceMapper(data, listViewFields[URLModel as urlModel].body);
+
+ const headData: Record = listViewFields[URLModel as urlModel].body.reduce(
+ (obj, key, index) => {
+ obj[key] = listViewFields[URLModel as urlModel].head[index];
+ return obj;
+ },
+ {}
+ );
+
+ const table: TableSource = {
+ head: headData,
+ body: bodyData,
+ meta: data // metaData
+ };
+
+ return { createForm, deleteForm, model, URLModel, table };
+};
+
+export const actions: Actions = {
+ create: async (event) => {
+ // const redirectToWrittenObject = Boolean(event.params.model === 'entity-assessments');
+ return defaultWriteFormAction({
+ event,
+ urlModel: 'attack-paths',
+ action: 'create'
+ // redirectToWrittenObject: redirectToWrittenObject
+ });
+ },
+ delete: async (event) => {
+ return defaultDeleteFormAction({ event, urlModel: 'attack-paths' });
+ }
+};
diff --git a/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-three/strategic-scenarios/+page.svelte b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-three/strategic-scenarios/+page.svelte
new file mode 100644
index 000000000..08ebedae8
--- /dev/null
+++ b/frontend/src/routes/(app)/(internal)/ebios-rm/[id=uuid]/workshop-three/strategic-scenarios/+page.svelte
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+