Skip to content

Commit 23596c9

Browse files
author
maschaad
committed
Bulk update a component's statements across all systems
GovReady#1797
1 parent 31e8e63 commit 23596c9

15 files changed

Lines changed: 1231 additions & 953 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,31 @@
11
GovReady-Q Release Notes
22
========================
33

4-
v0.11.4 (December 17, 2022)
4+
v0.12.0-dev (February 4, 2022)
55
---------------------------
66

77
**Developer changes**
88

9-
* Dynamically set Internet schme (http or https) for swagger interface to support proper URL strings in swagger.
9+
* Add API endpoint and Element (component) model method to force update all Element consuming systems's control implementation statements with library Elements content.
10+
* Add parameter createOSCAL API endpoint to indicate update existing components.
11+
* Upgrade Python libraries.
12+
* Update NPM libraries.
1013

1114

12-
v0.11.3 (December 10, 2022)
15+
v0.11.4 (December 17, 2022)
1316
---------------------------
1417

1518
**Developer changes**
1619

17-
* Add processing for question actions targeted at system to handle `system/add_baseline/<value>` to add additional baseline set of controls to a system without deleting already assigned controls.A
18-
20+
* Dynamically set Internet schme (http or https) for swagger interface to support proper URL strings in swagger.
1921

2022

2123
v0.11.3 (December 10, 2022)
2224
---------------------------
2325

2426
**Developer changes**
2527

26-
* Add processing for question actions targeted at system to handle `system/add_baseline/<value>` to add additional baseline set of controls to a system without deleting already assigned controls.A
27-
28+
* Add processing for question actions targeted at system to handle `system/add_baseline/<value>` to add additional baseline set of controls to a system without deleting already assigned controls.
2829

2930

3031
v0.11.2 (December 10, 2022)
@@ -2868,4 +2869,4 @@ Development changes:
28682869
v0.7.0-rc2 (January 8, 2018)
28692870
----------------------------
28702871

2871-
First release.
2872+
First release.

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v0.11.4
1+
v0.12.0-dev

api/base/urls.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,3 @@ def get_swagger_urls():
2121
url(r'^docs/swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
2222
url(r'^docs/swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
2323
]
24-

api/controls/serializers/element.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,18 @@ class Meta:
3636

3737
class WriteElementOscalSerializer(WriteOnlySerializer):
3838
oscal = serializers.JSONField()
39+
update = serializers.NullBooleanField()
3940
class Meta:
4041
model = Element
41-
fields = ['oscal']
42+
fields = ['oscal', 'update']
43+
44+
class WriteSynchConsumingSystemsImplementationStatementsSerializer(WriteOnlySerializer):
45+
# oscal = serializers.JSONField()
46+
componentId = serializers.IntegerField(min_value=1, max_value=None)
47+
class Meta:
48+
model = Element
49+
fields = ['componentId']
50+
4251
class ReadElementOscalSerializer(ReadOnlySerializer):
4352
oscal = serializers.SerializerMethodField('get_oscal')
4453

@@ -276,4 +285,4 @@ class ElementCreateAndSetRequestSerializer(WriteOnlySerializer):
276285
status = serializers.CharField(min_length=None, max_length=None, allow_blank=True, trim_whitespace=True)
277286
class Meta:
278287
model = Element
279-
fields = ['proposalId', 'userId', 'systemId', 'criteria_comment', 'criteria_reject_comment', 'status']
288+
fields = ['proposalId', 'userId', 'systemId', 'criteria_comment', 'criteria_reject_comment', 'status']

api/controls/views/element.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
WriteElementTagsSerializer, ElementPermissionSerializer, UpdateElementPermissionSerializer, RemoveUserPermissionFromElementSerializer, \
2020
WriteElementAppointPartySerializer, ElementPartySerializer, DeletePartyAppointmentsFromElementSerializer, CreateMultipleAppointmentsFromRoleIds, \
2121
ElementRequestsSerializer, ElementSetRequestsSerializer, ElementCreateAndSetRequestSerializer, \
22-
WriteElementOscalSerializer, ReadElementOscalSerializer, SimpleGetElementByNameSerializer
22+
WriteElementOscalSerializer, ReadElementOscalSerializer, SimpleGetElementByNameSerializer, WriteSynchConsumingSystemsImplementationStatementsSerializer
2323
from controls.models import Element, System
2424
from siteapp.models import Appointment, Party, Proposal, Role, Request, User
2525
from controls.views import ComponentImporter, OSCALComponentSerializer
@@ -52,7 +52,9 @@ class ElementViewSet(ReadWriteViewSet):
5252
CreateAndSetRequest=ElementCreateAndSetRequestSerializer,
5353
createOSCAL=WriteElementOscalSerializer,
5454
getOSCAL=ReadElementOscalSerializer,
55-
downloadOSCAL=ReadElementOscalSerializer)
55+
downloadOSCAL=ReadElementOscalSerializer,
56+
synchConsumingSystemsImplementationStatements=WriteSynchConsumingSystemsImplementationStatementsSerializer
57+
)
5658

5759
@action(detail=False, url_path="createOSCAL", methods=["POST"])
5860
def createOSCAL(self, request, **kwargs):
@@ -61,11 +63,17 @@ def createOSCAL(self, request, **kwargs):
6163
if "metadata" in request.data["oscal"]["component-definition"]:
6264
title = request.data["oscal"]["component-definition"]["metadata"]["title"]
6365
date_string = datetime.now().strftime("%Y-%m-%d-%H-%M")
66+
67+
# check if update value set to True
68+
if "update" in request.data and request.data["update"]:
69+
update = True
70+
else:
71+
update = False
6472

6573
import_record_name = title + "_api-import_" + date_string
6674
oscal_component_json = json.dumps(request.data["oscal"])
6775

68-
import_record_result = ComponentImporter().import_components_as_json(import_record_name, oscal_component_json, request)
76+
import_record_result = ComponentImporter().import_components_as_json(import_record_name, oscal_component_json, request, update=update)
6977
element = Element.objects.filter(import_record=import_record_result).first()
7078

7179
serializer_class = self.get_serializer_class('retrieve')
@@ -84,6 +92,24 @@ def createOSCAL(self, request, **kwargs):
8492
# serializer = self.get_serializer(serializer_class, element)
8593
# return Response(serializer.data)
8694

95+
@action(detail=False, url_path="synchConsumingSystemsImplementationStatements", methods=["POST"])
96+
def synchConsumingSystemsImplementationStatements(self, request, **kwargs):
97+
"""
98+
Force update all element consuming system control impl smts with content of protoype component control impl smt
99+
"""
100+
if "componentId" in request.data:
101+
component_id = request.data['componentId']
102+
element = Element.objects.filter(id=component_id).first()
103+
if element is not None:
104+
# TODO: check user permisson
105+
system_smts_updated = element.synch_consuming_systems_implementation_statements()
106+
result = {"system_smts_updated": system_smts_updated}
107+
return Response(result)
108+
else:
109+
# element not found
110+
result = {"system_smts_updated": 0}
111+
return Response(result2)
112+
87113
@action(detail=True, url_path="getOSCAL", methods=["GET"])
88114
def getOSCAL(self, request, **kwargs):
89115
element, validated_data = self.validate_serializer_and_get_object(request)

controls/models.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
get_perms_for_model, get_user_perms,
1212
get_users_with_perms, remove_perm)
1313
from simple_history.models import HistoricalRecords
14+
from simple_history.utils import bulk_update_with_history
1415
from jsonfield import JSONField
1516
from natsort import natsorted
1617

@@ -394,6 +395,7 @@ def assign_user_permissions(self, user, permissions):
394395
user={"id": user.id, "username": user.username}
395396
)
396397
return False
398+
397399
def remove_all_permissions_from_user(self, user):
398400
try:
399401
current_permissions = get_user_perms(user, self)
@@ -417,6 +419,7 @@ def remove_all_permissions_from_user(self, user):
417419
user={"id": user.id, "username": user.username}
418420
)
419421
return False
422+
420423
def get_permissible_users(self):
421424
return get_users_with_perms(self, attach_perms=True)
422425

@@ -597,6 +600,70 @@ def copy(self, name=None):
597600
smt_copy.save()
598601
return e_copy
599602

603+
@transaction.atomic
604+
def synch_consuming_systems_implementation_statements(self):
605+
"""
606+
Force update all Element's consuming systems' control implementation statements to be the same
607+
as the Element's control implementation prototype statements
608+
"""
609+
610+
# get Element's consuming_systems
611+
consuming_systems = self.consuming_systems()
612+
# get Element's control_implementation_prototype statements
613+
element_prototype_smts = self.statements(StatementTypeEnum.CONTROL_IMPLEMENTATION_PROTOTYPE.name)
614+
# track system control implementation statements touched via synchronization (whether changed or not)
615+
total_system_smts_updated = 0
616+
consuming_systems_updated = []
617+
# loop through Element's control_implementation_prototype statements
618+
for prototype_smt in element_prototype_smts:
619+
# find the consuming systems' control implementation statements to be updated with current control_implementation_prototype
620+
system_smts_to_update = Statement.objects.filter(statement_type=StatementTypeEnum.CONTROL_IMPLEMENTATION.name, prototype_id=prototype_smt.id)
621+
# track updated smts for bulk update
622+
system_smts_updated = []
623+
# determine list of all consuming systems to be updated
624+
consuming_systems_to_update = Statement.objects.filter(statement_type=StatementTypeEnum.CONTROL_IMPLEMENTATION.name, prototype_id=prototype_smt.id).values('consumer_element')
625+
626+
# consuming systems that have a statement that has been removed from the producing Element
627+
628+
# update the related control_implementation statements
629+
for smt in system_smts_to_update:
630+
631+
# update the system if not already synced with prototype
632+
# TODO: improve Statement.protype_synched() to check pid, status, etc
633+
if smt.prototype_synched == STATEMENT_NOT_SYNCHED:
634+
smt.body = prototype_smt.body
635+
smt.pid = prototype_smt.pid
636+
# smt.status = prototype_smt.status
637+
# TODO: add changelog
638+
# TODO: log change
639+
# record a reason for the change in simple_history
640+
smt._change_reason = 'Forced synchronization with library component statement'
641+
system_smts_updated.append(smt)
642+
# bulk save the changes and update simple_history records to reduce database calls
643+
bulk_update_with_history(system_smts_updated, Statement, ['body'], batch_size=500)
644+
total_system_smts_updated += len(system_smts_updated)
645+
646+
# add this prototype smt to any consuming system not currently having a child smt
647+
# determine which consuming systems are missing the prototype smt
648+
consuming_systems_missing_smt = [cs for cs in consuming_systems if cs not in consuming_systems_to_update]
649+
for cs in consuming_systems_missing_smt:
650+
# add statement to consuming system's root element
651+
prototype_smt.create_system_control_smt_from_component_prototype_smt(cs.root_element.id)
652+
total_system_smts_updated =+ 1
653+
654+
# remove any statements deleted from element in consuming systems
655+
# by searching through consuming systems's to delete orphaned statements
656+
# associated with the this element
657+
for consuming_system in consuming_systems:
658+
consumed_smts = consuming_system.root_element.statements_consumed.filter(statement_type=StatementTypeEnum.CONTROL_IMPLEMENTATION.name, producer_element=self)
659+
for consumed_smt in consumed_smts:
660+
if consumed_smt.prototype_synched == STATEMENT_ORPHANED:
661+
# delete statement
662+
consumed_smt.delete()
663+
total_system_smts_updated =+ 1
664+
# TODO: add count for deleted smt
665+
return total_system_smts_updated
666+
600667
@property
601668
def selected_controls_oscal_ctl_ids(self):
602669
"""Return array of selected controls oscal ids"""

controls/tests.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,19 @@ def test_component_type_state(self):
581581
self.assertTrue(e2.component_type == "hardware")
582582
self.assertTrue(e2.component_state == "disposition")
583583

584+
def test_element_update_control_implementation_with_prototype(self):
585+
e = Element.objects.create(name="New component", element_type="system")
586+
self.assertTrue(e.id is not None)
587+
self.assertTrue(e.component_type == "software")
588+
# add two statements
589+
# create two systems
590+
# assign element to two systems
591+
# check statements
592+
# modify element statements
593+
# execute element_update_control_implementation_with_prototype
594+
# assert system statements changed
595+
596+
584597
class ElementUITests(OrganizationSiteFunctionalTests):
585598

586599
def test_element_create_form(self):
@@ -1252,4 +1265,3 @@ def create_simple_import_record(self):
12521265
statement.save()
12531266

12541267
return import_record
1255-

controls/utilities.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def oscalize_control_id(cl_id):
8282
cl_id = re.sub(r'^([A-Za-z][A-Za-z]-)([0-9]*)([ ]*)\(([0-9]*)\)$', r'\1\2.\4', cl_id)
8383
# Remove trailing space
8484
cl_id = cl_id.strip(" ")
85-
# makes ure lowercase
85+
# makes sure lowercase
8686
cl_id = cl_id.lower()
8787

8888
return cl_id

0 commit comments

Comments
 (0)