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

add ref id to risk scenario #1078

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions backend/app_tests/api/test_api_risk_scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
RISK_SCENARIO_DESCRIPTION = "Test Description"
RISK_SCENARIO_existing_controls = "Test Existing Controls"
RISK_SCENARIO_existing_controls2 = "Test New Existing Controls"
RISK_SCENARIO_REF_ID = "Test Ref ID"
RISK_SCENARIO_CURRENT_PROBABILITIES = {
"value": 2,
"abbreviation": "H",
Expand Down Expand Up @@ -223,6 +224,7 @@ def test_get_risk_scenarios(self, test):
{
"name": RISK_SCENARIO_NAME,
"description": RISK_SCENARIO_DESCRIPTION,
"ref_id": RISK_SCENARIO_REF_ID,
"existing_controls": RISK_SCENARIO_existing_controls[0],
"current_proba": RISK_SCENARIO_CURRENT_PROBABILITIES["value"],
"current_impact": RISK_SCENARIO_CURRENT_IMPACT["value"],
Expand Down Expand Up @@ -280,6 +282,7 @@ def test_create_risk_scenarios(self, test):
{
"name": RISK_SCENARIO_NAME,
"description": RISK_SCENARIO_DESCRIPTION,
"ref_id": RISK_SCENARIO_REF_ID,
"existing_controls": RISK_SCENARIO_existing_controls[0],
"current_proba": RISK_SCENARIO_CURRENT_PROBABILITIES["value"],
"current_impact": RISK_SCENARIO_CURRENT_IMPACT["value"],
Expand Down Expand Up @@ -349,6 +352,7 @@ def test_update_risk_scenarios(self, test):
{
"name": RISK_SCENARIO_NAME,
"description": RISK_SCENARIO_DESCRIPTION,
"ref_id": RISK_SCENARIO_REF_ID,
"existing_controls": RISK_SCENARIO_existing_controls[0],
"current_proba": RISK_SCENARIO_CURRENT_PROBABILITIES["value"],
"current_impact": RISK_SCENARIO_CURRENT_IMPACT["value"],
Expand All @@ -364,6 +368,7 @@ def test_update_risk_scenarios(self, test):
{
"name": "new " + RISK_SCENARIO_NAME,
"description": "new " + RISK_SCENARIO_DESCRIPTION,
"ref_id": "new" + RISK_SCENARIO_REF_ID,
"existing_controls": RISK_SCENARIO_existing_controls2[0],
"current_proba": RISK_SCENARIO_CURRENT_PROBABILITIES2["value"],
"current_impact": RISK_SCENARIO_CURRENT_IMPACT2["value"],
Expand Down
39 changes: 39 additions & 0 deletions backend/core/migrations/0039_riskscenario_ref_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Generated by Django 5.1.1 on 2024-11-21 17:22

from django.db import migrations, models


def populate_default_ref_ids(apps, schema_editor):
RiskAssessment = apps.get_model("core", "RiskAssessment")

def get_default_ref_id(risk_assessment):
"""Return a unique reference ID for a given risk assessment."""
scenarios_ref_ids = [
x.ref_id for x in risk_assessment.risk_scenarios.all() if x.ref_id
]
nb_scenarios = len(scenarios_ref_ids) + 1
candidates = [f"R.{i+1}" for i in range(nb_scenarios)]
return next(x for x in candidates if x not in scenarios_ref_ids)

for risk_assessment in RiskAssessment.objects.all():
for scenario in risk_assessment.risk_scenarios.filter(ref_id=""):
scenario.ref_id = get_default_ref_id(risk_assessment)
scenario.save()


class Migration(migrations.Migration):
dependencies = [
("core", "0038_asset_disaster_recovery_objectives_and_more"),
]

operations = [
migrations.AddField(
model_name="riskscenario",
name="ref_id",
field=models.CharField(
default="", max_length=100, verbose_name="Reference ID"
),
preserve_default=False,
),
migrations.RunPython(populate_default_ref_ids),
]
17 changes: 12 additions & 5 deletions backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2237,6 +2237,18 @@ class RiskScenario(NameDescriptionMixin):
verbose_name=_("Treatment status"),
)

@classmethod
def get_default_ref_id(cls, risk_assessment: RiskAssessment):
"""return associated risk assessment id"""
scenarios_ref_ids = [x.ref_id for x in risk_assessment.risk_scenarios.all()]
nb_scenarios = len(scenarios_ref_ids) + 1
candidates = [f"R.{i}" for i in range(1, nb_scenarios + 1)]
return next(x for x in candidates if x not in scenarios_ref_ids)

ref_id = models.CharField(
max_length=100, blank=False, verbose_name=_("Reference ID")
)

qualifications = models.JSONField(default=list, verbose_name=_("Qualifications"))

strength_of_knowledge = models.IntegerField(
Expand Down Expand Up @@ -2364,11 +2376,6 @@ def get_strength_of_knowledge(self):
def __str__(self):
return str(self.parent_project()) + _(": ") + str(self.name)

@property
def rid(self):
"""return associated risk assessment id"""
return f"R.{self.scoped_id(scope=RiskScenario.objects.filter(risk_assessment=self.risk_assessment))}"

def save(self, *args, **kwargs):
if self.current_proba >= 0 and self.current_impact >= 0:
self.current_level = risk_scoring(
Expand Down
1 change: 0 additions & 1 deletion backend/core/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,6 @@ class RiskScenarioReadSerializer(RiskScenarioWriteSerializer):

applied_controls = FieldsRelatedField(many=True)
existing_applied_controls = FieldsRelatedField(many=True)
rid = serializers.CharField()

owner = FieldsRelatedField(many=True)

Expand Down
9 changes: 6 additions & 3 deletions backend/core/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,22 +585,25 @@ def test_risk_scenario_rid_is_deterministic(self):
scenario = RiskScenario.objects.create(
name="test scenario",
description="test scenario description",
ref_id="R.1",
risk_assessment=risk_assessment,
)
scenario2 = RiskScenario.objects.create(
name="test scenario 2",
description="test scenario description",
ref_id="R.2",
risk_assessment=risk_assessment,
)
scenario3 = RiskScenario.objects.create(
name="test scenario 3",
description="test scenario description",
ref_id="R.3",
risk_assessment=risk_assessment,
)

assert scenario.rid == "R.1"
assert scenario2.rid == "R.2"
assert scenario3.rid == "R.3"
assert scenario.ref_id == "R.1"
assert scenario2.ref_id == "R.2"
assert scenario3.ref_id == "R.3"


@pytest.mark.django_db
Expand Down
29 changes: 26 additions & 3 deletions backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN
from rest_framework.utils.serializer_helpers import ReturnDict
from rest_framework.views import APIView
from rest_framework.permissions import AllowAny


from weasyprint import HTML

Expand Down Expand Up @@ -611,7 +613,7 @@ def treatment_plan_csv(self, request, pk):
):
risk_scenarios = ",".join(
[
f"{scenario.rid}: {scenario.name}"
f"{scenario.ref_id}: {scenario.name}"
for scenario in mtg.risk_scenarios.all()
]
)
Expand Down Expand Up @@ -648,7 +650,7 @@ def risk_assessment_csv(self, request, pk):

writer = csv.writer(response, delimiter=";")
columns = [
"rid",
"ref_id",
"threats",
"name",
"description",
Expand All @@ -666,7 +668,7 @@ def risk_assessment_csv(self, request, pk):
)
threats = ",".join([t.name for t in scenario.threats.all()])
row = [
scenario.rid,
scenario.ref_id,
threats,
scenario.name,
scenario.description,
Expand Down Expand Up @@ -1172,6 +1174,27 @@ def count_per_level(self, request):
def per_status(self, request):
return Response({"results": risk_per_status(request.user)})

@action(
detail=False, methods=["get"], permission_classes=[permissions.IsAuthenticated]
)
def default_ref_id(self, request):
risk_assessment_id = request.query_params.get("risk_assessment")
if not risk_assessment_id:
return Response(
{"error": "Missing 'risk_assessment' parameter."}, status=400
)
try:
risk_assessment = RiskAssessment.objects.get(pk=risk_assessment_id)

# Use the class method to compute the default ref_id
default_ref_id = RiskScenario.get_default_ref_id(risk_assessment)
return Response({"results": default_ref_id})
except Exception as e:
logger.error("Error in default_ref_id: %s", str(e))
return Response(
{"error": "Error in default_ref_id has occurred."}, status=400
)


class RiskAcceptanceViewSet(BaseModelViewSet):
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
<script lang="ts">
import AutocompleteSelect from '../AutocompleteSelect.svelte';
import { getOptions } from '$lib/utils/crud';
import TextField from '$lib/components/Forms/TextField.svelte';
import type { SuperValidated } from 'sveltekit-superforms';
import type { ModelInfo, CacheLock } from '$lib/utils/types';
import * as m from '$paraglide/messages.js';
import { BASE_API_URL } from '$lib/utils/constants';

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 updated_fields: Set<string> = new Set();

async function fetchDefaultRefId(riskAssessmentId: string) {
try {
const response = await fetch(
`/risk-scenarios/default-ref-id/?risk_assessment=${riskAssessmentId}`
);
const result = await response.json();
console.log(result);
if (response.ok && result.results) {
form.form.update((currentData) => {
updated_fields.add('ref_id');
return { ...currentData, ref_id: result.results };
});
} else {
console.error(result.error || 'Failed to fetch default ref_id');
}
} catch (error) {
console.error('Error fetching default ref_id:', error);
}
}
</script>

<AutocompleteSelect
Expand All @@ -24,7 +48,21 @@
bind:cachedValue={formDataCache['risk_assessment']}
label={m.riskAssessment()}
hidden={initialData.risk_assessment}
on:change={async (e) => {
if (e.detail) {
await fetchDefaultRefId(e.detail);
}
}}
/>

<TextField
{form}
field="ref_id"
label={m.refId()}
cacheLock={cacheLocks['ref_id']}
bind:cachedValue={formDataCache['ref_id']}
/>

<AutocompleteSelect
{form}
multiple
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type Story = StoryObj<typeof meta>;
const scenario = {
id: '7059ef1b-7d4f-46bc-b735-ed41b531bb22',
name: 'RS1',
rid: 'R.1',
ref_id: 'R.1',
strength_of_knowledge: {
name: 'Very High',
description: 'The strength of the knowledge supporting the assessment is very high',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
{#if data.strength_of_knowledge && data.strength_of_knowledge.symbol !== undefined}
<sup class="font-mono text-lg">{data.strength_of_knowledge.symbol}</sup>
{/if}
<span>{data.rid}</span>
<span>{data.ref_id}</span>
</p>
3 changes: 2 additions & 1 deletion frontend/src/lib/utils/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ export const RiskScenarioSchema = baseNamedObject({
threats: z.string().uuid().optional().array().optional(),
assets: z.string().uuid().optional().array().optional(),
vulnerabilities: z.string().uuid().optional().array().optional(),
owner: z.string().uuid().optional().array().optional()
owner: z.string().uuid().optional().array().optional(),
ref_id: z.string()
});

export const AppliedControlSchema = baseNamedObject({
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lib/utils/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ export const listViewFields: ListViewFieldsConfig = {
},
'risk-scenarios': {
head: [
'ref_id',
'name',
'threats',
'riskAssessment',
Expand All @@ -331,6 +332,7 @@ export const listViewFields: ListViewFieldsConfig = {
'residualLevel'
],
body: [
'ref_id',
'name',
'threats',
'risk_assessment',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { BASE_API_URL } from '$lib/utils/constants';

import { error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ fetch, url }) => {
console.log(url);
const riskAssessmentId = url.searchParams.get('risk_assessment');
const endpoint = `${BASE_API_URL}/risk-scenarios/default_ref_id/?risk_assessment=${riskAssessmentId}`;

const res = await fetch(endpoint);
if (!res.ok) {
error(400, 'Failed to fetch default ref_id');
}
const logo = await res.json();

return new Response(JSON.stringify(logo), {
headers: {
'Content-Type': 'application/json'
}
});
};
Loading