Skip to content

Commit

Permalink
Merge pull request #675 from intuitem/improve/add_function
Browse files Browse the repository at this point in the history
CSF functions
  • Loading branch information
ab-smith authored Jul 26, 2024
2 parents 9d9d86d + e4956e5 commit a6c20a0
Show file tree
Hide file tree
Showing 35 changed files with 258 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Generated by Django 5.0.6 on 2024-07-20 22:27

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("core", "0017_requirementassessment_mapping_inference_and_more"),
]

operations = [
migrations.AddField(
model_name="appliedcontrol",
name="csf_function",
field=models.CharField(
blank=True,
choices=[
("govern", "Govern"),
("identify", "Identify"),
("protect", "Protect"),
("detect", "Detect"),
("respond", "Respond"),
("recover", "Recover"),
],
max_length=20,
null=True,
verbose_name="CSF Function",
),
),
migrations.AddField(
model_name="referencecontrol",
name="csf_function",
field=models.CharField(
blank=True,
choices=[
("govern", "Govern"),
("identify", "Identify"),
("protect", "Protect"),
("detect", "Detect"),
("respond", "Respond"),
("recover", "Recover"),
],
max_length=20,
null=True,
verbose_name="CSF Function",
),
),
]
44 changes: 41 additions & 3 deletions backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import os
import json
import yaml
import re

from django.core.exceptions import ValidationError

Expand All @@ -32,6 +33,18 @@

User = get_user_model()


URN_REGEX = r"^urn:([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)(?::([a-zA-Z0-9_-]+))?:([0-9A-Za-z\[\]\(\)\-\._:]+)$"


def match_urn(urn_string):
match = re.match(URN_REGEX, urn_string)
if match:
return match.groups() # Returns all captured groups from the regex match
else:
return None


########################### Referential objects #########################


Expand Down Expand Up @@ -168,8 +181,6 @@ def __init_class__(cls):
def store_library_content(
cls, library_content: bytes, builtin: bool = False
) -> "StoredLibrary | None":
from library.utils import match_urn

hash_checksum = sha256(library_content)
if hash_checksum in StoredLibrary.HASH_CHECKSUM_SET:
return None # We do not store the library if its hash checksum is in the database.
Expand All @@ -182,7 +193,6 @@ def store_library_content(
except yaml.YAMLError as e:
logger.error("Error while loading library content", error=e)
raise e

missing_fields = StoredLibrary.REQUIRED_FIELDS - set(library_data.keys())

if missing_fields:
Expand Down Expand Up @@ -247,6 +257,7 @@ def store_library_file(
) -> "StoredLibrary | None":
with open(fname, "rb") as f:
library_content = f.read()

return StoredLibrary.store_library_content(library_content, builtin)

def load(self) -> Union[str, None]:
Expand Down Expand Up @@ -657,6 +668,15 @@ class ReferenceControl(ReferentialObjectMixin, I18nObjectMixin):
("physical", _("Physical")),
]

CSF_FUNCTION = [
("govern", _("Govern")),
("identify", _("Identify")),
("protect", _("Protect")),
("detect", _("Detect")),
("respond", _("Respond")),
("recover", _("Recover")),
]

library = models.ForeignKey(
LoadedLibrary,
on_delete=models.CASCADE,
Expand All @@ -673,6 +693,14 @@ class ReferenceControl(ReferentialObjectMixin, I18nObjectMixin):
verbose_name=_("Category"),
)

csf_function = models.CharField(
max_length=20,
null=True,
blank=True,
choices=CSF_FUNCTION,
verbose_name=_("CSF Function"),
)

typical_evidence = models.JSONField(
verbose_name=_("Typical evidence"), null=True, blank=True
)
Expand Down Expand Up @@ -1149,6 +1177,7 @@ class Status(models.TextChoices):
INACTIVE = "inactive", _("Inactive")

CATEGORY = ReferenceControl.CATEGORY
CSF_FUNCTION = ReferenceControl.CSF_FUNCTION

EFFORT = [
("S", _("Small")),
Expand Down Expand Up @@ -1179,6 +1208,13 @@ class Status(models.TextChoices):
blank=True,
verbose_name=_("Category"),
)
csf_function = models.CharField(
max_length=20,
choices=CSF_FUNCTION,
null=True,
blank=True,
verbose_name=_("CSF Function"),
)
status = models.CharField(
max_length=20,
choices=Status.choices,
Expand Down Expand Up @@ -1223,6 +1259,8 @@ class Meta:
def save(self, *args, **kwargs):
if self.reference_control and self.category is None:
self.category = self.reference_control.category
if self.reference_control and self.csf_function is None:
self.csf_function = self.reference_control.csf_function
super(AppliedControl, self).save(*args, **kwargs)

@property
Expand Down
3 changes: 3 additions & 0 deletions backend/core/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,9 @@ class AppliedControlReadSerializer(AppliedControlWriteSerializer):
category = serializers.CharField(
source="get_category_display"
) # type : get_type_display
csf_function = serializers.CharField(
source="get_csf_function_display"
) # type : get_type_display
status = serializers.CharField(source="get_status_display")
evidences = FieldsRelatedField(many=True)
effort = serializers.CharField(source="get_effort_display")
Expand Down
2 changes: 2 additions & 0 deletions backend/core/templates/core/action_plan_pdf.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ <h1 class="flex justify-center">{% trans "Action plan" %}</h1>
<th class="text-md p-2 text-center">{% trans "Name" %}</th>
<th class="text-md p-2 text-center">{% trans "Description" %}</th>
<th class="text-md p-2 text-center">{% trans "Category" %}</th>
<th class="text-md p-2 text-center">{% trans "CSF function" %}</th>
<th class="text-md p-2 text-center">{% trans "ETA" %}</th>
<th class="text-md p-2 text-center">{% trans "Expiry date" %}</th>
<th class="text-md p-2 text-center">{% trans "Effort" %}</th>
Expand All @@ -54,6 +55,7 @@ <h1 class="flex justify-center">{% trans "Action plan" %}</h1>
<td class="text-md p-2 text-center">{{ applied_control.name }}</td>
<td class="text-md p-2 text-center">{{ applied_control.description|default:"--" }}</td>
<td class="text-md p-2 text-center">{{ applied_control.get_category_display|default:"--" }}</td>
<td class="text-md p-2 text-center">{{ applied_control.get_csf_function_display|default:"--" }}</td>
<td class="text-md p-2 text-center">{{ applied_control.eta|default:"--" }}</td>
<td class="text-md p-2 text-center">{{ applied_control.expiry_date|default:"--" }}</td>
<td class="text-md p-2 text-center">{{ applied_control.get_effort_display|default:"--" }}</td>
Expand Down
16 changes: 10 additions & 6 deletions backend/core/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,16 +666,20 @@ def test_measure_creation_same_name_different_folder(self):
assert measure1.folder == root_folder
assert measure2.folder == folder

def test_measure_category_inherited_from_function(self):
def test_applied_control_inherited_from_reference_control(self):
root_folder = Folder.objects.get(content_type=Folder.ContentType.ROOT)
folder = Folder.objects.create(name="Parent", folder=root_folder)
function = ReferenceControl.objects.create(
name="Function", folder=root_folder, category="technical"
reference_control = ReferenceControl.objects.create(
name="Function",
folder=root_folder,
category="technical",
csf_function="identify",
)
measure = AppliedControl.objects.create(
name="Measure", folder=folder, reference_control=function
applied_control = AppliedControl.objects.create(
name="Measure", folder=folder, reference_control=reference_control
)
assert measure.category == "technical"
assert applied_control.category == "technical"
assert applied_control.csf_function == "identify"


@pytest.mark.django_db
Expand Down
20 changes: 19 additions & 1 deletion backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,13 +273,17 @@ class ReferenceControlViewSet(BaseModelViewSet):
"""

model = ReferenceControl
filterset_fields = ["folder", "category"]
filterset_fields = ["folder", "category", "csf_function"]
search_fields = ["name", "description", "provider"]

@action(detail=False, name="Get category choices")
def category(self, request):
return Response(dict(ReferenceControl.CATEGORY))

@action(detail=False, name="Get function choices")
def csf_function(self, request):
return Response(dict(ReferenceControl.CSF_FUNCTION))


class RiskMatrixViewSet(BaseModelViewSet):
"""
Expand Down Expand Up @@ -423,6 +427,7 @@ def treatment_plan_csv(self, request, pk):
"measure_name",
"measure_desc",
"category",
"csf_function",
"reference_control",
"eta",
"effort",
Expand All @@ -448,6 +453,7 @@ def treatment_plan_csv(self, request, pk):
mtg.name,
mtg.description,
mtg.get_category_display(),
mtg.get_csf_function_display(),
mtg.reference_control,
mtg.eta,
mtg.effort,
Expand Down Expand Up @@ -604,6 +610,7 @@ class AppliedControlViewSet(BaseModelViewSet):
filterset_fields = [
"folder",
"category",
"csf_function",
"status",
"reference_control",
"effort",
Expand All @@ -621,6 +628,10 @@ def status(self, request):
def category(self, request):
return Response(dict(AppliedControl.CATEGORY))

@action(detail=False, name="Get csf_function choices")
def csf_function(self, request):
return Response(dict(AppliedControl.CSF_FUNCTION))

@action(detail=False, name="Get effort choices")
def effort(self, request):
return Response(dict(AppliedControl.EFFORT))
Expand Down Expand Up @@ -699,6 +710,7 @@ class PolicyViewSet(AppliedControlViewSet):
model = Policy
filterset_fields = [
"folder",
"csf_function",
"status",
"reference_control",
"effort",
Expand All @@ -708,6 +720,10 @@ class PolicyViewSet(AppliedControlViewSet):
]
search_fields = ["name", "description", "risk_scenarios", "requirement_assessments"]

@action(detail=False, name="Get csf_function choices")
def csf_function(self, request):
return Response(dict(AppliedControl.CSF_FUNCTION))


class RiskScenarioViewSet(BaseModelViewSet):
"""
Expand Down Expand Up @@ -1787,6 +1803,7 @@ def export_mp_csv(request):
"measure_name",
"measure_desc",
"category",
"csf_function",
"reference_control",
"eta",
"effort",
Expand All @@ -1807,6 +1824,7 @@ def export_mp_csv(request):
mtg.name,
mtg.description,
mtg.category,
mtg.csf_function,
mtg.reference_control,
mtg.eta,
mtg.effort,
Expand Down
22 changes: 12 additions & 10 deletions backend/library/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,6 @@

logger = structlog.get_logger(__name__)

URN_REGEX = r"^urn:([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)(?::([a-zA-Z0-9_-]+))?:([0-9A-Za-z\[\]\(\)\-\._:]+)$"


def match_urn(urn_string):
match = re.match(URN_REGEX, urn_string)
if match:
return match.groups() # Returns all captured groups from the regex match
else:
return None


def preview_library(framework: dict) -> dict[str, list]:
"""
Expand Down Expand Up @@ -348,6 +338,9 @@ def import_threat(self, library_object: LoadedLibrary):
class ReferenceControlImporter:
REQUIRED_FIELDS = {"ref_id", "urn"}
CATEGORIES = set(_category[0] for _category in ReferenceControl.CATEGORY)
CSF_FUNCTIONS = set(
_csf_function[0] for _csf_function in ReferenceControl.CSF_FUNCTION
)

def __init__(self, reference_control_data: dict):
self.reference_control_data = reference_control_data
Expand All @@ -364,6 +357,14 @@ def is_valid(self) -> Union[str, None]:
category, ", ".join(ReferenceControlImporter.CATEGORIES)
)

if (
csf_function := self.reference_control_data.get("csf_function")
) is not None:
if csf_function not in ReferenceControlImporter.CSF_FUNCTIONS:
return "Invalid CSF function '{}', the function must be among the following list : {}".format(
csf_function, ", ".join(ReferenceControlImporter.CSF_FUNCTIONS)
)

def import_reference_control(self, library_object: LoadedLibrary):
ReferenceControl.objects.create(
library=library_object,
Expand All @@ -374,6 +375,7 @@ def import_reference_control(self, library_object: LoadedLibrary):
provider=library_object.provider,
typical_evidence=self.reference_control_data.get("typical_evidence"),
category=self.reference_control_data.get("category"),
csf_function=self.reference_control_data.get("csf_function"),
is_published=True,
locale=library_object.locale,
translations=self.reference_control_data.get("translations", {}),
Expand Down
Loading

0 comments on commit a6c20a0

Please sign in to comment.