diff --git a/controls/oscal.py b/controls/oscal.py index 271543b16..2bf44d213 100644 --- a/controls/oscal.py +++ b/controls/oscal.py @@ -96,6 +96,13 @@ def check_and_extend(values, external_values, extendtype, splitter): values.extend(files) return values +def de_oscalize_control(control_id): + """ + Returns the regular control formatting from an oscalized version of the control number. + de_oscalize_control("ac-2.3") --> AC-2 (3) + """ + return re.sub(r'^([A-Za-z][A-Za-z]-)([0-9]*)\.([0-9]*)$', r'\1\2 (\3)', control_id).upper() + class Catalog(object): """Represent a catalog""" @@ -431,7 +438,7 @@ def get_flattened_control_as_dict(self, control): description_print = description.replace("\n", "
") cl_dict = { "id": control['id'], - "id_display": re.sub(r'^([A-Za-z][A-Za-z]-)([0-9]*)\.([0-9]*)$', r'\1\2 (\3)', control['id']), + "id_display": de_oscalize_control(control['id']), "title": control['title'], "family_id": family_id, "family_title": self.get_group_title_by_id(family_id), diff --git a/controls/tests.py b/controls/tests.py index ae5cf3b9d..0aff5a12a 100644 --- a/controls/tests.py +++ b/controls/tests.py @@ -29,7 +29,7 @@ from system_settings.models import SystemSettings from controls.models import * from controls.enums.statements import StatementTypeEnum -from controls.oscal import Catalogs, Catalog, EXTERNAL_CATALOG_PATH +from controls.oscal import Catalogs, Catalog, EXTERNAL_CATALOG_PATH, de_oscalize_control from siteapp.models import User, Project, Portfolio from system_settings.models import SystemSettings @@ -1225,3 +1225,12 @@ def test_export_oscal_system_security_plan(self): response.get('Content-Disposition') ) + def test_deoscalization_control_id(self): + """ + Tests de_oscalize_control function on expected formats from sid (oscal) format to regular. + """ + controls = ["ac-2.4", "ac-2.5", "ac-2.11","ac-2.13", "ac-3", "ac-4", "si-3.2", "si-4.2", "si-4.5"] + regular_sid_controls = [de_oscalize_control(control) for control in controls] + self.assertEqual(['AC-2 (4)', 'AC-2 (5)', 'AC-2 (11)', 'AC-2 (13)', 'AC-3', 'AC-4', 'SI-3 (2)', 'SI-4 (2)', 'SI-4 (5)'], regular_sid_controls) + + diff --git a/guidedmodules/forms.py b/guidedmodules/forms.py index 5c4a13138..186b6c63b 100644 --- a/guidedmodules/forms.py +++ b/guidedmodules/forms.py @@ -1,10 +1,13 @@ from django import forms - class ExportCSVTemplateSSPForm(forms.Form): info_system = forms.CharField(label='"Project Name" exported to column named: ', widget=forms.TextInput(attrs={'class': 'form-control', 'style':'resize:none;width:500px;', 'placeholder': 'Information System'})) control_id = forms.CharField(label='"Control ID" exported to column named:', widget=forms.TextInput(attrs={'class': 'form-control', 'style':'resize:none;width:500px;', 'placeholder': 'Control ID'})) catalog = forms.CharField(label='"Control Catalog" exported to column named:', widget=forms.TextInput(attrs={'class': 'form-control', 'style':'resize:none;width:500px;', 'placeholder': 'Control Set Version Number'})) shared_imps = forms.CharField(label='"Implementation Statement (Library)" exported to column named:', widget=forms.TextInput(attrs={'class': 'form-control', 'style':'resize:none;width:500px;', 'placeholder': 'Shared Implementation Details'})) - private_imps = forms.CharField(label='"Implementation Statement (Local)" exported to column named:', widget=forms.TextInput(attrs={'class': 'form-control', 'style':'resize:none;width:500px;', 'placeholder': 'Private Implementation Details'})) \ No newline at end of file + private_imps = forms.CharField(label='"Implementation Statement (Local)" exported to column named:', widget=forms.TextInput(attrs={'class': 'form-control', 'style':'resize:none;width:500px;', 'placeholder': 'Private Implementation Details'})) + oscal_format = forms.BooleanField( + label='Would you like to have the Control ID data in OSCAL format?', + required=False + ) \ No newline at end of file diff --git a/guidedmodules/views.py b/guidedmodules/views.py index e82b117b5..6c80134b0 100644 --- a/guidedmodules/views.py +++ b/guidedmodules/views.py @@ -3,9 +3,9 @@ from datetime import datetime from zipfile import ZipFile from zipfile import BadZipFile -from django.shortcuts import render, redirect, get_object_or_404 +from django.shortcuts import render, get_object_or_404 from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseForbidden, JsonResponse, HttpResponseNotAllowed -from django.urls import reverse +from controls.oscal import de_oscalize_control from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required from django.conf import settings @@ -2021,14 +2021,14 @@ def start_a_discussion(request): # Get the TaskAnswer for this task. It may not exist yet. tq, isnew = TaskAnswer.objects.get_or_create(**tq_filter) - + # Filter for discussion and return the first entry (if it doesn't exist it returns None) discussion = Discussion.get_for(task.project.organization, tq) if not discussion: # Validate user can create discussion. if not task.has_read_priv(request.user): return JsonResponse({ "status": "error", "message": "You do not have permission!" }) - # Get the Discussion. + # Create a Discussion. discussion = Discussion.get_for(task.project.organization, tq, create=True) return JsonResponse(discussion.render_context_dict(request.user)) @@ -2131,7 +2131,7 @@ def compute_table(opt): ] }) -def export_ssp_csv(export_csv_data, system): +def export_ssp_csv(form_data, system): """ Export an SSP's control implementations with the submitted headers """ @@ -2140,10 +2140,22 @@ def export_ssp_csv(export_csv_data, system): statement_type=StatementTypeEnum.CONTROL_IMPLEMENTATION.name).order_by('pid') selected_controls = list(smts.values_list('sid', flat=True)) - catalog_keys = list(smts.values_list('sid_class', flat=True)) + # If the user selected to format the control id in OSCAL this will be skipped + if not form_data.get('oscal_format'): + # De-oscalize every control id (sid) + selected_controls = [de_oscalize_control(control) for control in selected_controls] + db_catalog_keys = list(smts.values_list('sid_class', flat=True)) + catalog_keys = [] + # XYZ_3_0 --> XYZ 3.0 + for catalog in db_catalog_keys: + if catalog.count("_") == 3: + catalog_keys.append(" ".join(catalog.split("_")[:2]) + " " + ".".join(catalog.split("_")[-2:])) + else: + catalog_keys.append(catalog) imps = list(smts.values_list('body', flat=True)) - headers = [export_csv_data.get('info_system'), export_csv_data.get('control_id'), export_csv_data.get('catalog'), export_csv_data.get('shared_imps'), export_csv_data.get('private_imps')] + headers = [form_data.get('info_system'), form_data.get('control_id'), form_data.get('catalog'), form_data.get('shared_imps'), form_data.get('private_imps')] system_name = system.root_element.name # TODO: Should this come from questionnaire answer or project name as we have it? + data = [ [system_name] * len(selected_controls), selected_controls, diff --git a/templates/controls/detail.html b/templates/controls/detail.html index ddb1db9e0..e5377cebf 100644 --- a/templates/controls/detail.html +++ b/templates/controls/detail.html @@ -35,7 +35,7 @@
{% if control.title is not None %}

- {{ control.id_display|upper }} {{ control.title }} + {{ control.id_display }} {{ control.title }}

{% else %} Control was not found in the catalog.