From f613b38ede4dfe5ac7c28d85bd493e647f92a58f Mon Sep 17 00:00:00 2001 From: Greg Elin Date: Sun, 30 May 2021 09:31:01 -0500 Subject: [PATCH] Batch update cntl impl smts when component_statement changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented a faster way to update status of system controls. When user sets a system component state to "operational" all statements associated with that component for the system get their status set to "Implemented". Similarly, setting component’s state to "planned" batch sets all component statements for that system to "Planned", and "under-development" sets component statements to "Partially Implemented". Display system component component_state and component_type when component is listed for a system. --- CHANGELOG.md | 2 + controls/models.py | 6 +++ controls/tests.py | 50 +++++++++++++++++++--- controls/views.py | 11 +++++ siteapp/static/css/govready-q.css | 2 + templates/controls/editor.html | 5 +++ templates/systems/components_selected.html | 11 +++-- 7 files changed, 76 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8df079fce..9674fb383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ v999 (May XX, 2021) * Can now edit a system componet's state and type in the detail page for a selected component. * Improve project pages appearance: decrease action button width and left align text; widen from 9 to 10 columns main content. * Remove "Refresh Documents" button on task finished page because caches are now automatically cleared and document content refreshed. +* Display system component component_state and component_type when component is listed for a system. **Developer changes** @@ -25,6 +26,7 @@ v999 (May XX, 2021) * Task caches are now automatically cleared and document content refreshed when document downloaded. * Add test for system control page. * Refactor creating system control statements from component library prototype statements when adding a component from the library to a system and reduce by an order a magnitude the time it takes to add a component to system. +* Create System method to batch update an element's control implementation statements based on the component's state. **Bug fix** diff --git a/controls/models.py b/controls/models.py index 6d4cdd960..5a1464bc8 100644 --- a/controls/models.py +++ b/controls/models.py @@ -723,6 +723,12 @@ def get_producer_elements(self): producer_elements = cached_property(get_producer_elements) + def set_component_control_status(self, element, status): + """Batch update status of system control implementation statements for a specific element.""" + + self.root_element.statements_consumed.filter(producer_element=element, statement_type=StatementTypeEnum.CONTROL_IMPLEMENTATION.value).update(status=status) + return True + class CommonControlProvider(models.Model): name = models.CharField(max_length=150, help_text="Name of the CommonControlProvider", unique=False) description = models.CharField(max_length=255, help_text="Brief description of the CommonControlProvider", unique=False) diff --git a/controls/tests.py b/controls/tests.py index 5f11d4183..f87ec6bf0 100644 --- a/controls/tests.py +++ b/controls/tests.py @@ -673,14 +673,14 @@ def test_element_role(self): class SystemUnitTests(TestCase): def test_system_create(self): - e = Element.objects.create(name="New Element", full_name="New Element Full Name", element_type="system") - self.assertTrue(e.id is not None) - self.assertTrue(e.name == "New Element") - self.assertTrue(e.full_name == "New Element Full Name") - self.assertTrue(e.element_type == "system") - s = System(root_element=e) + sre = Element.objects.create(name="New Element", full_name="New Element Full Name", element_type="system") + self.assertTrue(sre.id is not None) + self.assertTrue(sre.name == "New Element") + self.assertTrue(sre.full_name == "New Element Full Name") + self.assertTrue(sre.element_type == "system") + s = System(root_element=sre) s.save() - self.assertEqual(s.root_element.name,e.name) + self.assertEqual(s.root_element.name,sre.name) u2 = User.objects.create(username="Jane2", email="jane@example.com") # Test no permissions for user @@ -696,6 +696,42 @@ def test_system_create(self): self.assertIn('delete_system', perms) self.assertIn('view_system', perms) + # Create an element with control implementation statements and assign to system + e = Element.objects.create(name="OAuth", full_name="OAuth Service", element_type="system_element", component_state="operational") + self.assertTrue(e.id is not None) + self.assertTrue(e.name == "OAuth") + e.save() + smt_1 = Statement.objects.create( + sid = "au-3", + sid_class = "NIST_SP-800-53_rev4", + body = "This is the first test statement.", + statement_type=StatementTypeEnum.CONTROL_IMPLEMENTATION.value, + status = "Implemented", + producer_element = e, + consumer_element = s.root_element + ) + smt_1.save() + smt_2 = Statement.objects.create( + sid = "au-4", + sid_class = "NIST_SP-800-53_rev4", + body = "This is the first test statement.", + statement_type=StatementTypeEnum.CONTROL_IMPLEMENTATION.value, + status = "Implemented", + producer_element = e, + consumer_element = s.root_element + ) + smt_2.save() + + # Batch update system statements status by changing system component state + element = e + control_status = "planned" + s.set_component_control_status(element, control_status) + # Test the system's component's statements status were changed + smt_1_updated = Statement.objects.get(pk=smt_1.id) + self.assertTrue(smt_1_updated.status, control_status) + smt_2_updated = Statement.objects.get(pk=smt_2.id) + self.assertTrue(smt_2_updated.status, control_status) + class SystemUITests(OrganizationSiteFunctionalTests): def test_deployments_page_exists(self): diff --git a/controls/views.py b/controls/views.py index ad100a400..0fc5c186b 100644 --- a/controls/views.py +++ b/controls/views.py @@ -895,9 +895,20 @@ def edit_component_state(request, system_id, element_id): # Retrieve related selected controls if user has permission on system if request.user.has_perm('change_system', system): # Retrieve element + # TODO: Make atomic transaction element = Element.objects.get(id=element_id) element.component_state = request.POST['state_change'] element.save() + logger.info(event=f"change_system update_component_state {element} {element.component_state}", + object={"object": "system", "id": system.id}, + user={"id": request.user.id, "username": request.user.username}) + # Batch update status of control implementation statements provided by the element to the system + state_status = {"operational": "Implemented", "under-development": "Partially Implemented", "planned": "Planned"} + control_status = state_status.get(request.POST['state_change']) or "Not Implemented" + system.set_component_control_status(element, control_status) + logger.info(event=f"change_system batch_update_component_control_status {element} {control_status}", + object={"object": "system", "id": system.id}, + user={"id": request.user.id, "username": request.user.username}) return redirect(reverse('system_element', args=[system_id, element_id])) def edit_component_type(request, system_id, element_id): diff --git a/siteapp/static/css/govready-q.css b/siteapp/static/css/govready-q.css index f17aecd65..5ad6200e5 100644 --- a/siteapp/static/css/govready-q.css +++ b/siteapp/static/css/govready-q.css @@ -295,6 +295,8 @@ form #id_json_content { width: 100%; overflow: auto !important; } /* The !import .component-tag { font-size: 0.7em; color: #999; padding: 0px 6px 0px 6px; border: 1px solid #bbb; border-radius: 24px; } .component-tag a { color: #999; } +.component-state { font-size: 0.8em; color: #444; padding: 0px 6px 0px 6px; border: 1px solid #bbb; border-radius: 24px; } +.component-type { font-size: 0.8em; color: #444; padding: 0px 6px 0px 6px; border: 1px solid #bbb; border-radius: 24px; } .component-form { float: left; diff --git a/templates/controls/editor.html b/templates/controls/editor.html index 8964b8f86..6d2dc2a81 100644 --- a/templates/controls/editor.html +++ b/templates/controls/editor.html @@ -174,7 +174,12 @@

{{ smt.producer_element.name }} +
+ {{ smt.producer_element.component_type }} + {{ smt.producer_element.component_state }} +
+
{% if smt.pid is not None and smt.pid != "" %}
{{ smt.pid }}.
{% endif %}{{ smt.body }}
{% spaceless %} diff --git a/templates/systems/components_selected.html b/templates/systems/components_selected.html index cd95d4544..3d95ab555 100644 --- a/templates/systems/components_selected.html +++ b/templates/systems/components_selected.html @@ -45,10 +45,14 @@ {% for component in system_elements %} {# Each "component" is a Element model object. #}
-
+ -
+
+ {{ component.component_type }} + {{ component.component_state }} +
+
{% if component.description %}{{ component.description }}{% else %}No description provided.{% endif %}
{% for tag in component.tags.all %}{{ tag.label }} {% endfor %}
@@ -57,13 +61,12 @@ {% if ctl_count %}{{ ctl_count }} control{{ ctl_count|pluralize }}{% else %}None{% endif %}
{% endwith %} - {% get_obj_perms request.user for system as "system_perms" %} {% if "change_system" in system_perms %}