Skip to content

Commit 27b3df0

Browse files
authored
feat(lib): enisa cyber resiliency maturity assessment framework and adjustments to make it more usable (#4567)
* feat(lib): enisa cyber resiliency maturity assessment framework and adjustments to make it more usable * fix excel typo based on the pdf
1 parent d67fddf commit 27b3df0

8 files changed

Lines changed: 913 additions & 5 deletions

File tree

backend/library/libraries/enisa-sme-cra-maturity.yaml

Lines changed: 793 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Framework `field_visibility` template: YAML import and CA seeding."""
2+
3+
import pytest
4+
from core.models import ComplianceAssessment, Framework, Perimeter, StoredLibrary
5+
from core.utils import build_initial_field_visibility, is_field_visible_to
6+
from iam.models import Folder
7+
8+
LIB_WITH_FIELD_VISIBILITY = """
9+
urn: urn:test:risk:library:fv-lib
10+
locale: en
11+
ref_id: FV-LIB
12+
name: Field visibility test library
13+
description: Framework carrying a field_visibility template
14+
version: 1
15+
publication_date: 2026-07-24
16+
copyright: test
17+
provider: test
18+
packager: test
19+
objects:
20+
framework:
21+
urn: urn:test:risk:framework:fv-lib
22+
ref_id: FV-LIB
23+
name: Field visibility test framework
24+
description: test
25+
min_score: 1
26+
max_score: 5
27+
field_visibility:
28+
score:
29+
auditor: edit
30+
respondent: hidden
31+
is_scored:
32+
auditor: edit
33+
respondent: hidden
34+
result:
35+
auditor: hidden
36+
respondent: hidden
37+
extended_result:
38+
auditor: hidden
39+
respondent: hidden
40+
requirement_nodes:
41+
- urn: urn:test:risk:req_node:fv-lib:1
42+
assessable: true
43+
depth: 1
44+
ref_id: '1'
45+
name: Requirement one
46+
""".lstrip().encode("utf-8")
47+
48+
49+
@pytest.mark.django_db
50+
def test_field_visibility_imports_and_seeds_new_assessments():
51+
stored, error = StoredLibrary.store_library_content(LIB_WITH_FIELD_VISIBILITY)
52+
assert error is None, error
53+
assert stored.load() is None
54+
55+
fw = Framework.objects.get(urn="urn:test:risk:framework:fv-lib")
56+
assert fw.field_visibility["score"] == {"auditor": "edit", "respondent": "hidden"}
57+
assert fw.field_visibility["result"] == {
58+
"auditor": "hidden",
59+
"respondent": "hidden",
60+
}
61+
62+
folder = Folder.get_root_folder()
63+
perimeter = Perimeter.objects.create(name="P", folder=folder)
64+
ca = ComplianceAssessment.objects.create(
65+
name="CA",
66+
framework=fw,
67+
perimeter=perimeter,
68+
folder=folder,
69+
field_visibility=build_initial_field_visibility(fw),
70+
)
71+
assert ca.scoring_enabled is True
72+
assert ca.extended_result_enabled is False
73+
assert ca.progress_status_enabled is True
74+
assert is_field_visible_to(ca, "result", "auditor") is False
75+
assert is_field_visible_to(ca, "score", "auditor") is True
76+
assert is_field_visible_to(ca, "answers", "respondent") is True
77+
78+
79+
@pytest.mark.django_db
80+
def test_framework_without_field_visibility_gets_empty_template():
81+
lib = LIB_WITH_FIELD_VISIBILITY.replace(b"fv-lib", b"fv-lib2")
82+
lib = b"\n".join(
83+
line
84+
for line in lib.split(b"\n")
85+
if b"field_visibility" not in line
86+
and b"auditor:" not in line
87+
and b"respondent:" not in line
88+
and not line.strip().startswith(b"score:")
89+
and not line.strip().startswith(b"is_scored:")
90+
and not line.strip().startswith(b"result:")
91+
and not line.strip().startswith(b"extended_result:")
92+
)
93+
stored, error = StoredLibrary.store_library_content(lib)
94+
assert error is None, error
95+
assert stored.load() is None
96+
97+
fw = Framework.objects.get(urn="urn:test:risk:framework:fv-lib2")
98+
assert fw.field_visibility == {}
99+
template = build_initial_field_visibility(fw)
100+
assert template["score"] == {"auditor": "hidden", "respondent": "hidden"}

backend/library/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,7 @@ def import_framework(self, library_object: LoadedLibrary):
427427
"implementation_groups_definition"
428428
),
429429
outcomes_definition=self.framework_data.get("outcomes_definition", []),
430+
field_visibility=self.framework_data.get("field_visibility") or {},
430431
provider=library_object.provider,
431432
locale=library_object.locale,
432433
default_locale=library_object.default_locale,

backend/scripts/convert_library_v2.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import sys
2323
import re
24+
import json
2425
import yaml
2526
import datetime
2627
import argparse
@@ -1040,6 +1041,18 @@ def _handle_framework(obj, library, object_blocks, prefix_to_urn, compat_mode, v
10401041
if "max_score" in meta:
10411042
framework["max_score"] = int(meta["max_score"])
10421043

1044+
if meta.get("field_visibility"):
1045+
try:
1046+
field_visibility = json.loads(meta["field_visibility"])
1047+
except json.JSONDecodeError as e:
1048+
raise ValueError(f"(framework) Invalid field_visibility JSON: {e}")
1049+
if not isinstance(field_visibility, dict):
1050+
raise ValueError(
1051+
"(framework) field_visibility must be a JSON object "
1052+
'({"field": {"role": "edit"|"read"|"hidden"}})'
1053+
)
1054+
framework["field_visibility"] = field_visibility
1055+
10431056
score_name = meta.get("scores_definition")
10441057
if score_name and score_name in object_blocks:
10451058
score_header, score_rows_data = parse_content_rows(

frontend/src/lib/utils/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export function getScoreHexColor(
9191
}
9292

9393
export function formatScoreValue(
94-
value: number,
94+
value: number | null,
9595
max_score: number,
9696
fullDonut = false,
9797
min_score = 0

frontend/src/routes/(app)/(third-party)/compliance-assessments/[id=uuid]/TreeViewItemContent.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@
398398
</div>
399399
{/if}
400400
{#if hasAssessableChildren}
401-
<div class="flex max-w-96 grow items-center space-x-2">
401+
<div class="flex max-w-96 grow items-center space-x-2 {showResult ? '' : 'justify-end'}">
402402
{#if showResult}
403403
<div
404404
class="flex max-w-96 grow bg-surface-200-800 rounded-full overflow-hidden h-4 shrink self-center"

frontend/src/routes/(app)/(third-party)/requirement-assessments/[id=uuid]/edit/+page.svelte

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
computeRequirementScoreAndResult,
4242
formatScoreValue,
4343
displayScoreColor,
44+
hasComputedScore,
4445
resultBadgeStyle,
4546
filterResultChoices
4647
} from '$lib/utils/helpers';
@@ -787,13 +788,13 @@
787788
</div>
788789
{/if}
789790
{#if page.data.compliance_assessment_score.scoring_enabled}
790-
{#if computedScore !== null}
791+
{#if computedScore !== null || hasComputedScore(page.data.requirementAssessment.requirement.questions)}
791792
{#if showScore}
792793
<div class="flex flex-row items-center space-x-4">
793794
<span class="font-medium">{m.score()}</span>
794795
<div class="shrink-0 relative">
795796
<Progress
796-
value={formatScoreValue(computedScore || 0, resolvedMax, false, resolvedMin)}
797+
value={formatScoreValue(computedScore, resolvedMax, false, resolvedMin)}
797798
min={0}
798799
max={100}
799800
>
@@ -804,7 +805,7 @@
804805
/>
805806
</Progress.Circle>
806807
<div class="absolute inset-0 flex items-center justify-center">
807-
<span class="text-xs font-bold">{computedScore}</span>
808+
<span class="text-xs font-bold">{computedScore ?? ''}</span>
808809
</div>
809810
</Progress>
810811
</div>
13.8 KB
Binary file not shown.

0 commit comments

Comments
 (0)