Skip to content

Commit

Permalink
feat: add strategic scenario
Browse files Browse the repository at this point in the history
  • Loading branch information
Mohamed-Hacene committed Dec 17, 2024
1 parent 8c96b6b commit 2dd31a2
Show file tree
Hide file tree
Showing 16 changed files with 208 additions and 24 deletions.
4 changes: 4 additions & 0 deletions backend/core/startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,10 @@
"view_stakeholder",
"change_stakeholder",
"delete_stakeholder",
"add_strategicscenario",
"view_strategicscenario",
"change_strategicscenario",
"delete_strategicscenario",
"add_attackpath",
"view_attackpath",
"change_attackpath",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Generated by Django 5.1.4 on 2024-12-17 18:10

import django.db.models.deletion
import iam.models
import uuid
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('ebios_rm', '0006_alter_attackpath_stakeholders'),
('iam', '0010_user_preferences'),
]

operations = [
migrations.RemoveField(
model_name='attackpath',
name='ro_to_couple',
),
migrations.CreateModel(
name='StrategicScenario',
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')),
('ref_id', models.CharField(blank=True, max_length=100)),
('ebios_rm_study', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='strategic_scenarios', to='ebios_rm.ebiosrmstudy', verbose_name='EBIOS RM study')),
('folder', models.ForeignKey(default=iam.models.Folder.get_root_folder_id, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_folder', to='iam.folder')),
('ro_to_couple', models.ForeignKey(help_text='RO/TO couple from which the attach path is derived', on_delete=django.db.models.deletion.CASCADE, to='ebios_rm.roto', verbose_name='RO/TO couple')),
],
options={
'verbose_name': 'Strategic Scenario',
'verbose_name_plural': 'Strategic Scenarios',
'ordering': ['created_at'],
},
),
migrations.AddField(
model_name='attackpath',
name='strategic_scenario',
field=models.ForeignKey(default='', help_text='Strategic scenario from which the attack path is derived', on_delete=django.db.models.deletion.CASCADE, related_name='attack_paths', to='ebios_rm.strategicscenario', verbose_name='Strategic scenario'),
preserve_default=False,
),
]
33 changes: 32 additions & 1 deletion backend/ebios_rm/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,10 +374,11 @@ def residual_criticality(self):
)


class AttackPath(NameDescriptionMixin, FolderMixin):
class StrategicScenario(NameDescriptionMixin, FolderMixin):
ebios_rm_study = models.ForeignKey(
EbiosRMStudy,
verbose_name=_("EBIOS RM study"),
related_name="strategic_scenarios",
on_delete=models.CASCADE,
)
ro_to_couple = models.ForeignKey(
Expand All @@ -386,6 +387,31 @@ class AttackPath(NameDescriptionMixin, FolderMixin):
on_delete=models.CASCADE,
help_text=_("RO/TO couple from which the attach path is derived"),
)
ref_id = models.CharField(max_length=100, blank=True)

class Meta:
verbose_name = _("Strategic Scenario")
verbose_name_plural = _("Strategic Scenarios")
ordering = ["created_at"]

def save(self, *args, **kwargs):
self.folder = self.ebios_rm_study.folder
super().save(*args, **kwargs)


class AttackPath(NameDescriptionMixin, FolderMixin):
ebios_rm_study = models.ForeignKey(
EbiosRMStudy,
verbose_name=_("EBIOS RM study"),
on_delete=models.CASCADE,
)
strategic_scenario = models.ForeignKey(
StrategicScenario,
verbose_name=_("Strategic scenario"),
on_delete=models.CASCADE,
related_name="attack_paths",
help_text=_("Strategic scenario from which the attack path is derived"),
)
stakeholders = models.ManyToManyField(
Stakeholder,
verbose_name=_("Stakeholders"),
Expand All @@ -404,8 +430,13 @@ class Meta:
ordering = ["created_at"]

def save(self, *args, **kwargs):
self.ebios_rm_study = self.strategic_scenario.ebios_rm_study
self.folder = self.ebios_rm_study.folder
super().save(*args, **kwargs)

@property
def ro_to_couple(self):
return self.strategic_scenario.ro_to_couple

@property
def gravity(self):
Expand Down
19 changes: 18 additions & 1 deletion backend/ebios_rm/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
FearedEvent,
RoTo,
Stakeholder,
StrategicScenario,
AttackPath,
OperationalScenario,
)
Expand Down Expand Up @@ -128,10 +129,26 @@ class Meta:
fields = "__all__"


class StrategicScenarioWriteSerializer(BaseModelSerializer):
class Meta:
model = StrategicScenario
exclude = ["created_at", "updated_at", "folder"]


class StrategicScenarioReadSerializer(BaseModelSerializer):
ebios_rm_study = FieldsRelatedField()
folder = FieldsRelatedField()
ro_to_couple = FieldsRelatedField()

class Meta:
model = StrategicScenario
fields = "__all__"


class AttackPathWriteSerializer(BaseModelSerializer):
class Meta:
model = AttackPath
exclude = ["created_at", "updated_at", "folder"]
exclude = ["created_at", "updated_at", "folder", "ebios_rm_study"]


class AttackPathReadSerializer(BaseModelSerializer):
Expand Down
2 changes: 2 additions & 0 deletions backend/ebios_rm/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
FearedEventViewSet,
RoToViewSet,
StakeholderViewSet,
StrategicScenarioViewSet,
AttackPathViewSet,
OperationalScenarioViewSet,
)
Expand All @@ -16,6 +17,7 @@
router.register(r"feared-events", FearedEventViewSet, basename="feared-events")
router.register(r"ro-to", RoToViewSet, basename="ro-to")
router.register(r"stakeholders", StakeholderViewSet, basename="stakeholders")
router.register(r"strategic-scenarios", StrategicScenarioViewSet, basename="strategic-scenarios")
router.register(r"attack-paths", AttackPathViewSet, basename="attack-paths")
router.register(
r"operational-scenarios",
Expand Down
9 changes: 9 additions & 0 deletions backend/ebios_rm/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
FearedEvent,
RoTo,
Stakeholder,
StrategicScenario,
AttackPath,
OperationalScenario,
)
Expand Down Expand Up @@ -124,6 +125,14 @@ def category(self, request):
return Response(dict(Stakeholder.Category.choices))


class StrategicScenarioViewSet(BaseModelViewSet):
model = StrategicScenario

filterset_fields = [
"ebios_rm_study",
]


class AttackPathFilter(df.FilterSet):
used = df.BooleanFilter(method="is_used", label="Used")

Expand Down
4 changes: 3 additions & 1 deletion frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1007,5 +1007,7 @@
"certain": "Certain",
"minor": "Minor",
"operatingModesDescription": "Operating modes description",
"noStakeholders": "No stakeholders"
"noStakeholders": "No stakeholders",
"strategicScenario": "Strategic scenario",
"strategicScenarios": "Strategic scenarios"
}
3 changes: 3 additions & 0 deletions frontend/src/lib/components/Forms/ModelForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import { createModalCache } from '$lib/utils/stores';
import FilteringLabelForm from './ModelForm/FilteringLabelForm.svelte';
import OperationalScenarioForm from './ModelForm/OperationalScenarioForm.svelte';
import StrategicScenarioForm from './ModelForm/StrategicScenarioForm.svelte';
export let form: SuperValidated<AnyZodObject>;
export let invalidateAll = true; // set to false to keep form data using muliple forms on a page
Expand Down Expand Up @@ -270,6 +271,8 @@
<RoToForm {form} {model} {cacheLocks} {formDataCache} {initialData} {context} />
{:else if URLModel === 'stakeholders'}
<StakeholderForm {form} {model} {cacheLocks} {formDataCache} {context} />
{:else if URLModel === 'strategic-scenarios'}
<StrategicScenarioForm {form} {model} {cacheLocks} {formDataCache} {initialData} {context} />
{:else if URLModel === 'attack-paths'}
<AttackPathForm {form} {model} {cacheLocks} {formDataCache} {initialData} />
{:else if URLModel === 'operational-scenarios'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,18 @@
export let cacheLocks: Record<string, CacheLock> = {};
export let formDataCache: Record<string, any> = {};
export let initialData: Record<string, any> = {};
export let context: 'create' | 'edit' = 'create';
</script>

<AutocompleteSelect
{form}
options={getOptions({ objects: model.foreignKeys['ro_to_couple'], label: 'str' })}
field="ro_to_couple"
cacheLock={cacheLocks['ro_to_couple']}
bind:cachedValue={formDataCache['ro_to_couple']}
label={m.roToCouple()}
hidden={initialData.ro_to_couple}
options={getOptions({
objects: model.foreignKeys['strategic_scenario']
})}
field="strategic_scenario"
cacheLock={cacheLocks['strategic_scenario']}
bind:cachedValue={formDataCache['strategic_scenario']}
label={m.strategicScenario()}
hidden={initialData['strategic_scenario']}
/>
<AutocompleteSelect
{form}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script lang="ts">
import AutocompleteSelect from '$lib/components/Forms/AutocompleteSelect.svelte';
import { getOptions } from '$lib/utils/crud';
import type { CacheLock, ModelInfo } from '$lib/utils/types';
import * as m from '$paraglide/messages.js';
import type { SuperValidated } from 'sveltekit-superforms';
import TextField from '../TextArea.svelte';
export let form: SuperValidated<any>;
export let model: ModelInfo;
export let cacheLocks: Record<string, CacheLock> = {};
export let formDataCache: Record<string, any> = {};
export let initialData: Record<string, any> = {};
export let context: string;
</script>

{#if context !== 'edit'}
<AutocompleteSelect
{form}
options={getOptions({ objects: model.foreignKeys['ro_to_couple'], label: 'str' })}
field="ro_to_couple"
cacheLock={cacheLocks['ro_to_couple']}
bind:cachedValue={formDataCache['ro_to_couple']}
label={m.roToCouple()}
hidden={initialData.ro_to_couple}
/>
{/if}
<TextField
{form}
field="ref_id"
label={m.refId()}
cacheLock={cacheLocks['ref_id']}
bind:cachedValue={formDataCache['ref_id']}
/>
24 changes: 16 additions & 8 deletions frontend/src/lib/utils/crud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,20 @@ export const URL_MODEL_MAP: ModelMap = {
],
selectFields: [{ field: 'category' }]
},
'strategic-scenarios': {
endpointUrl: 'ebios-rm/strategic-scenarios',
name: 'strategicscenario',
localName: 'strategicScenario',
localNamePlural: 'strategicScenarios',
verboseName: 'Strategic scenario',
verboseNamePlural: 'Strategic scenarios',
foreignKeyFields: [
{ field: 'ebios_rm_study', urlModel: 'ebios-rm', endpointUrl: 'ebios-rm/studies' },
{ field: 'ro_to_couple', urlModel: 'ro-to', endpointUrl: 'ebios-rm/ro-to', urlParams: 'ebios_rm_study=', detail: true },
{ field: 'folder', urlModel: 'folders', urlParams: 'content_type=DO' }
],
reverseForeignKeyFields: [{ field: 'strategic_scenario', urlModel: 'attack-paths', endpointUrl: 'ebios-rm/attack-paths' }],
},
'attack-paths': {
endpointUrl: 'ebios-rm/attack-paths',
name: 'attackpath',
Expand All @@ -678,15 +692,9 @@ export const URL_MODEL_MAP: ModelMap = {
verboseNamePlural: 'Attack paths',
foreignKeyFields: [
{ field: 'stakeholders', urlModel: 'stakeholders', endpointUrl: 'ebios-rm/stakeholders' },
{
field: 'ro_to_couple',
urlModel: 'ro-to',
endpointUrl: 'ebios-rm/ro-to',
urlParams: 'ebios_rm_study=',
detail: true
},
{ field: 'ebios_rm_study', urlModel: 'ebios-rm', endpointUrl: 'ebios-rm/studies' },
{ field: 'folder', urlModel: 'folders', urlParams: 'content_type=DO' }
{ field: 'folder', urlModel: 'folders', urlParams: 'content_type=DO' },
{ field: 'strategic_scenario', urlModel: 'strategic-scenarios', endpointUrl: 'ebios-rm/strategic-scenarios', urlParams: 'ebios_rm_study=', detail: true }
]
},
'operational-scenarios': {
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/lib/utils/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,10 +445,16 @@ export const StakeholderSchema = z.object({
justification: z.string().optional()
});

export const StrategicScenarioSchema = z.object({
...NameDescriptionMixin,
ro_to_couple: z.string().uuid(),
ref_id: z.string().optional(),
})

export const AttackPathSchema = z.object({
...NameDescriptionMixin,
ebios_rm_study: z.string(),
ro_to_couple: z.string().uuid(),
strategic_scenario: z.string().uuid(),
stakeholders: z.string().uuid().optional().array().optional(),
is_selected: z.boolean().optional(),
justification: z.string().optional()
Expand Down Expand Up @@ -493,6 +499,7 @@ const SCHEMA_MAP: Record<string, AnyZodObject> = {
'feared-events': fearedEventsSchema,
'ro-to': roToSchema,
stakeholders: StakeholderSchema,
'strategic-scenarios': StrategicScenarioSchema,
'attack-paths': AttackPathSchema,
'operational-scenarios': operationalScenarioSchema
};
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/lib/utils/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,9 +596,13 @@ export const listViewFields: ListViewFieldsConfig = {
head: ['entity', 'category', 'current_criticality', 'applied_controls', 'residual_criticality'],
body: ['entity', 'category', 'current_criticality', 'applied_controls', 'residual_criticality']
},
'strategic-scenarios': {
head: ['ref_id', 'name', 'description', 'ro_to_couple', 'attackPaths'],
body: ['ref_id', 'name', 'description', 'ro_to_couple', 'attack_paths']
},
'attack-paths': {
head: ['is_selected', 'name', 'risk_origin', 'target_objective', 'stakeholders', 'attackPath'],
body: ['is_selected', 'name', 'risk_origin', 'target_objective', 'stakeholders', 'description']
head: ['is_selected', 'name', 'stakeholders', 'description'],
body: ['is_selected', 'name', 'stakeholders', 'description']
},
'operational-scenarios': {
head: ['operatingModesDescription', 'threats', 'likelihood'],
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const URL_MODEL = [
'feared-events',
'ro-to',
'stakeholders',
'strategic-scenarios',
'attack-paths',
'operational-scenarios'
// 'ebios-rm',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,20 @@ export const load: PageServerLoad = async ({ params, fetch }) => {
}
}

const likelihoodChoicesEndpoint = `${BASE_API_URL}/ebios-rm/studies/${params.id}/likelihood/`;
const likelihoodChoicesResponse = await fetch(likelihoodChoicesEndpoint);

if (likelihoodChoicesResponse.ok) {
selectOptions['likelihood'] = await likelihoodChoicesResponse.json().then((data) =>
Object.entries(data).map(([key, value]) => ({
label: value,
value: parseInt(key)
}))
);
} else {
console.error(`Failed to fetch data for likelihood: ${likelihoodChoicesResponse.statusText}`);
}

model['selectOptions'] = selectOptions;

const bodyData = tableSourceMapper(data, listViewFields[URLModel as urlModel].body);
Expand Down
Loading

0 comments on commit 2dd31a2

Please sign in to comment.