Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ca 695 automatically generate a scenario in the risk analysis for each operational scenario created #1200

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,17 @@ def ancestors_plus_self(self) -> set[Self]:
result.update(x.ancestors_plus_self())
return set(result)

def get_children(self):
return Asset.objects.filter(parent_assets=self)

def get_descendants(self) -> set[Self]:
children = self.get_children()
sub_children = set()
for child in children:
sub_children.append(child)
sub_children.update(child.get_descendants())
return sub_children

def get_security_objectives(self) -> dict[str, dict[str, dict[str, int | bool]]]:
"""
Gets the security objectives of a given asset.
Expand Down
35 changes: 35 additions & 0 deletions backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,16 @@
AppliedControl,
ComplianceAssessment,
RequirementMappingSet,
RiskAssessment,
)
from core.serializers import ComplianceAssessmentReadSerializer
from core.utils import RoleCodename, UserGroupCodename

from ebios_rm.models import (
EbiosRMStudy,
OperationalScenario,
)

from .models import *
from .serializers import *

Expand Down Expand Up @@ -578,6 +584,35 @@ class RiskAssessmentViewSet(BaseModelViewSet):
"ebios_rm_study",
]

def perform_create(self, serializer):
instance: RiskAssessment = serializer.save()
if instance.ebios_rm_study:
instance.risk_matrix = instance.ebios_rm_study.risk_matrix
ebios_rm_study = EbiosRMStudy.objects.get(id=instance.ebios_rm_study.id)
for operational_scenario in [
operational_scenario
for operational_scenario in ebios_rm_study.operational_scenarios.all()
if operational_scenario.is_selected
]:
risk_scenario = RiskScenario.objects.create(
risk_assessment=instance,
name=operational_scenario.name,
ref_id=operational_scenario.ref_id
if operational_scenario.ref_id
else RiskScenario.get_default_ref_id(instance),
description=operational_scenario.operating_modes_description,
current_proba=operational_scenario.likelihood,
current_impact=operational_scenario.gravity,
)
risk_scenario.assets.set(operational_scenario.get_assets())
risk_scenario.threats.set(operational_scenario.threats.all())
risk_scenario.existing_applied_controls.set(
operational_scenario.get_applied_controls()
)
risk_scenario.save()
instance.save()
return super().perform_create(serializer)

@action(detail=False, name="Risk assessments per status")
def per_status(self, request):
data = assessment_per_status(request.user, RiskAssessment)
Expand Down
30 changes: 22 additions & 8 deletions backend/ebios_rm/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
)
from iam.models import FolderMixin, User
from tprm.models import Entity
import json

INITIAL_META = {
"workshops": [
Expand Down Expand Up @@ -158,13 +157,14 @@ class Status(models.TextChoices):
related_name="reviewers",
)
observation = models.TextField(null=True, blank=True, verbose_name=_("Observation"))

meta = models.JSONField(
default=get_initial_meta,
verbose_name=_("Metadata"),
validators=[JSONSchemaInstanceValidator(META_JSONSCHEMA)],
)

fields_to_check = ["name", "version"]

class Meta:
verbose_name = _("Ebios RM Study")
verbose_name_plural = _("Ebios RM Studies")
Expand Down Expand Up @@ -236,6 +236,7 @@ def get_gravity_display(self):
"name": "--",
"description": "not rated",
"value": -1,
"hexcolor": "#f9fafb",
}
risk_matrix = self.parsed_matrix
return {
Expand Down Expand Up @@ -559,12 +560,11 @@ def parsed_matrix(self):

@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
return self.attack_path.ref_id

@property
def name(self):
return self.attack_path.name

@property
def gravity(self):
Expand All @@ -578,6 +578,19 @@ def stakeholders(self):
def ro_to(self):
return self.attack_path.ro_to_couple

def get_assets(self):
initial_assets = Asset.objects.filter(
feared_events__in=self.ro_to.feared_events.all(), is_selected=True
)
assets = set()
for asset in initial_assets:
assets.add(asset)
assets.update(asset.get_descendants())
return Asset.objects.filter(id__in=[asset.id for asset in assets])

def get_applied_controls(self):
return AppliedControl.objects.filter(stakeholders__in=self.stakeholders.all())

def get_likelihood_display(self):
if self.likelihood < 0:
return {
Expand All @@ -600,6 +613,7 @@ def get_gravity_display(self):
"name": "--",
"description": "not rated",
"value": -1,
"hexcolor": "#f9fafb",
}
risk_matrix = self.parsed_matrix
return {
Expand Down
4 changes: 2 additions & 2 deletions frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,8 @@
"noAuthor": "No author assigned",
"noReviewer": "No reviewer assigned",
"selectAudit": "Select audit",
"errorAssetGraphMustNotContainCycles": "The asset graph must not contain cycles.",
"addStakeholder": "Add stakeholder",
"operationalScenario": "Operational scenario",
"operationalScenarioRefId": "Operational scenario {refId}",
"operationalScenarios": "Operational scenarios",
Expand All @@ -1006,8 +1008,6 @@
"minor": "Minor",
"operatingModesDescription": "Operating modes description",
"noStakeholders": "No stakeholders",
"errorAssetGraphMustNotContainCycles": "The asset graph must not contain cycles.",
"addStakeholder": "Add stakeholder",
"markAsDone": "Mark as done",
"markAsInProgress": "Mark as in progress"
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@
<AutocompleteSelect
{form}
options={getOptions({
objects: model.foreignKeys['attack_path'],
label: 'str'
objects: model.foreignKeys['attack_path']
})}
field="attack_path"
label={m.attackPath()}
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/lib/utils/crud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ interface ForeignKeyField {
endpointUrl?: string;
urlParams?: string;
detail?: boolean;
detailUrlParams?: string[]; // To prepare possible fetch for foreign keys with detail in generic views
}

interface Field {
Expand Down Expand Up @@ -625,7 +626,7 @@ export const URL_MODEL_MAP: ModelMap = {
verboseNamePlural: 'Feared events',
foreignKeyFields: [
{ field: 'ebios_rm_study', urlModel: 'ebios-rm', endpointUrl: 'ebios-rm/studies' },
{ field: 'assets', urlModel: 'assets', urlParams: 'ebios_rm_studies=', detail: true },
{ field: 'assets', urlModel: 'assets', urlParams: 'type=PR&ebios_rm_studies=', detail: true },
{ field: 'qualifications', urlModel: 'qualifications' }
],
selectFields: [{ field: 'gravity', valueType: 'number', detail: true }]
Expand Down Expand Up @@ -677,7 +678,13 @@ 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' },
{
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' }
]
Expand Down
24 changes: 20 additions & 4 deletions frontend/src/lib/utils/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,8 +571,8 @@ export const listViewFields: ListViewFieldsConfig = {
body: ['name', 'description']
},
'feared-events': {
head: ['selected', 'assets', 'fearedEvent', 'qualifications', 'gravity'],
body: ['is_selected', 'assets', 'description', 'qualifications', 'gravity']
head: ['selected', 'name', 'assets', 'fearedEvent', 'qualifications', 'gravity'],
body: ['is_selected', 'name', 'assets', 'description', 'qualifications', 'gravity']
},
'ro-to': {
head: [
Expand All @@ -597,8 +597,24 @@ export const listViewFields: ListViewFieldsConfig = {
body: ['entity', 'category', 'current_criticality', 'applied_controls', 'residual_criticality']
},
'attack-paths': {
head: ['ref_id', 'name', 'risk_origin', 'target_objective', 'stakeholders', 'attackPath'],
body: ['ref_id', 'name', 'risk_origin', 'target_objective', 'stakeholders', 'description']
head: [
'is_selected',
'ref_id',
'name',
'risk_origin',
'target_objective',
'stakeholders',
'attackPath'
],
body: [
'is_selected',
'ref_id',
'name',
'risk_origin',
'target_objective',
'stakeholders',
'description'
]
},
'operational-scenarios': {
head: ['operatingModesDescription', 'threats', 'likelihood'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,17 @@ export const load: LayoutServerLoad = async (event) => {

if (foreignKeyFields) {
for (const keyField of foreignKeyFields) {
const queryParams = keyField.urlParams ? `?${keyField.urlParams}` : '';
let queryParams = keyField.urlParams ? `?${keyField.urlParams}` : '';
if (keyField.detailUrlParams && Array.isArray(keyField.detailUrlParams)) {
keyField.detailUrlParams.forEach((detailParam) => {
const paramValue = object[detailParam]?.id;
if (paramValue) {
queryParams += queryParams
? `&${detailParam}=${paramValue}`
: `?${detailParam}=${paramValue}`;
}
});
} // To prepare possible fetch for foreign keys with detail in generic views
const keyModel = getModelInfo(keyField.urlModel);
let url = keyModel.endpointUrl
? `${BASE_API_URL}/${keyModel.endpointUrl}/${queryParams}`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { defaultWriteFormAction } from '$lib/utils/actions';
import { BASE_API_URL } from '$lib/utils/constants';
import { getModelInfo } from '$lib/utils/crud';
import {
getModelInfo,
urlParamModelForeignKeyFields,
urlParamModelSelectFields
} from '$lib/utils/crud';
import { modelSchema } from '$lib/utils/schemas';
import type { ModelInfo } from '$lib/utils/types';
import { type Actions } from '@sveltejs/kit';
Expand Down Expand Up @@ -28,8 +32,51 @@ export const load: PageServerLoad = async ({ params, fetch }) => {
const createRiskAnalysisForm = await superValidate(initialData, zod(createSchema), {
errors: false
});
const riskModel = getModelInfo('risk-assessments');
const foreignKeyFields = urlParamModelForeignKeyFields(riskModel.urlModel);
const selectFields = urlParamModelSelectFields(riskModel.urlModel);

return { createRiskAnalysisForm, model: getModelInfo('risk-assessments') };
const foreignKeys: Record<string, any> = {};

for (const keyField of foreignKeyFields) {
const queryParams = keyField.urlParams ? `?${keyField.urlParams}` : '';
const keyModel = getModelInfo(keyField.urlModel);
const url = keyModel.endpointUrl
? `${BASE_API_URL}/${keyModel.endpointUrl}/${queryParams}`
: `${BASE_API_URL}/${keyField.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}`);
}
}

riskModel['foreignKeys'] = foreignKeys;

const selectOptions: Record<string, any> = {};

for (const selectField of selectFields) {
if (selectField.detail) continue;
const url = riskModel.endpointUrl
? `${BASE_API_URL}/${riskModel.endpointUrl}/${selectField.field}/`
: `${BASE_API_URL}/${riskModel.urlModel}/${selectField.field}/`;
const response = await fetch(url);
if (response.ok) {
selectOptions[selectField.field] = await response.json().then((data) =>
Object.entries(data).map(([key, value]) => ({
label: value,
value: selectField.valueType === 'number' ? parseInt(key) : key
}))
);
} else {
console.error(`Failed to fetch data for ${selectField.field}: ${response.statusText}`);
}
}

riskModel['selectOptions'] = selectOptions;

return { createRiskAnalysisForm, riskModel };
};

export const actions: Actions = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,17 +123,18 @@
ref: CreateModal,
props: {
form: data.createRiskAnalysisForm,
model: data.model
model: data.riskModel
}
};
let modal: ModalSettings = {
type: 'component',
component: modalComponent,
// Data
title: safeTranslate('add-' + data.model.localName)
title: safeTranslate('add-' + data.riskModel.localName)
};
if (
checkConstraints(data.createRiskAnalysisForm.constraints, data.model.foreignKeys).length > 0
checkConstraints(data.createRiskAnalysisForm.constraints, data.riskModel.foreignKeys).length >
0
) {
modalComponent = {
ref: MissingConstraintsModal
Expand All @@ -142,8 +143,8 @@
type: 'component',
component: modalComponent,
title: m.warning(),
body: safeTranslate('add-' + data.model.localName).toLowerCase(),
value: checkConstraints(data.createRiskAnalysisForm.constraints, data.model.foreignKeys)
body: safeTranslate('add-' + data.riskModel.localName).toLowerCase(),
value: checkConstraints(data.createRiskAnalysisForm.constraints, data.riskModel.foreignKeys)
};
}
modalStore.trigger(modal);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ export const load: PageServerLoad = async ({ params, fetch }) => {

for (const keyField of foreignKeyFields) {
const model = getModelInfo(keyField.urlModel);
const queryParams = keyField.urlParams ? `?${keyField.urlParams}` : '';
const queryParams = keyField.urlParams
? `?${keyField.urlParams}${keyField.detail ? params.id : ''}`
: '';
const url = model.endpointUrl
? `${BASE_API_URL}/${model.endpointUrl}/${queryParams}`
: `${BASE_API_URL}/${model.urlModel}/${queryParams}`;
Expand Down
Loading