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 @@