def test_get_control_param_dict(tmp_trestle_dir: pathlib.Path) -> None: """Test getting the param dict of a control.""" test_utils.setup_for_multi_profile(tmp_trestle_dir, False, True) prof_a_path = ModelUtils.path_for_top_level_model(tmp_trestle_dir, 'test_profile_a', prof.Profile, FileContentType.JSON) catalog = ProfileResolver.get_resolved_profile_catalog( tmp_trestle_dir, prof_a_path) catalog_interface = CatalogInterface(catalog) control = catalog_interface.get_control('ac-1') param_dict = ControlIOReader.get_control_param_dict(control, False) # confirm profile value is used assert ControlIOReader.param_values_as_str( param_dict['ac-1_prm_1']) == 'all alert personnel' # confirm original param label is used since no value was assigned assert ControlIOReader.param_to_str(param_dict['ac-1_prm_7'], ParameterRep.VALUE_OR_LABEL_OR_CHOICES ) == 'organization-defined events' param = control.params[0] param.values = None param.select = common.ParameterSelection( how_many=common.HowMany.one_or_more, choice=['choice 1', 'choice 2']) param_dict = ControlIOReader.get_control_param_dict(control, False) assert ControlIOReader.param_to_str( param_dict['ac-1_prm_1'], ParameterRep.VALUE_OR_LABEL_OR_CHOICES) == 'choice 1, choice 2'
def test_profile_resolver(tmp_trestle_dir: pathlib.Path) -> None: """Test the resolver.""" test_utils.setup_for_multi_profile(tmp_trestle_dir, False, True) prof_a_path = ModelUtils.path_for_top_level_model( tmp_trestle_dir, 'test_profile_a', prof.Profile, FileContentType.JSON ) cat = ProfileResolver.get_resolved_profile_catalog(tmp_trestle_dir, prof_a_path) interface = CatalogInterface(cat) # added part ac-1_expevid from prof a list1 = find_string_in_all_controls_prose(interface, 'Detailed evidence logs') # modify param ac-3.3_prm_2 in prof b list2 = find_string_in_all_controls_prose(interface, 'full and complete compliance') assert len(list1) == 1 assert len(list2) == 1 assert interface.get_count_of_controls_in_catalog(False) == 6 assert interface.get_count_of_controls_in_catalog(True) == 7 assert len(cat.controls) == 4 assert interface.get_dependent_control_ids('ac-3') == ['ac-3.3'] control = interface.get_control('a-1') assert control.parts[0].parts[0].id == 'a-1_deep' assert control.parts[0].parts[0].prose == 'Extra added part in subpart'
def test_profile_resolver_merge(sample_catalog_rich_controls: cat.Catalog) -> None: """Test profile resolver merge.""" profile = gens.generate_sample_model(prof.Profile) method = prof.Method.merge combine = prof.Combine(method=method) profile.merge = prof.Merge(combine=combine) merge = Merge(profile) # merge into empty catalog merged = gens.generate_sample_model(cat.Catalog) new_merged = merge._merge_catalog(merged, sample_catalog_rich_controls) catalog_interface = CatalogInterface(new_merged) assert catalog_interface.get_count_of_controls_in_catalog(True) == 5 # add part to first control and merge, then make sure it is there part = com.Part(name='foo', title='added part') control_id = sample_catalog_rich_controls.controls[0].id cat_with_added_part = copy.deepcopy(sample_catalog_rich_controls) cat_with_added_part.controls[0].parts.append(part) final_merged = merge._merge_catalog(sample_catalog_rich_controls, cat_with_added_part) catalog_interface = CatalogInterface(final_merged) assert catalog_interface.get_count_of_controls_in_catalog(True) == 5 assert catalog_interface.get_control(control_id).parts[-1].name == 'foo' # add part to first control and merge but with use-first. The part should not be there at end. method = prof.Method.use_first combine = prof.Combine(method=method) profile.merge = prof.Merge(combine=combine) merge = Merge(profile) final_merged = merge._merge_catalog(sample_catalog_rich_controls, cat_with_added_part) catalog_interface = CatalogInterface(final_merged) assert catalog_interface.get_count_of_controls_in_catalog(True) == 5 assert len(catalog_interface.get_control(control_id).parts) == 1 # now force a merge with keep profile.merge = None merge_keep = Merge(profile) merged_keep = merge_keep._merge_catalog(new_merged, sample_catalog_rich_controls) assert CatalogInterface(merged_keep).get_count_of_controls_in_catalog(True) == 10
def test_add_props(tmp_trestle_dir: pathlib.Path) -> None: """Test all types of property additions.""" test_utils.setup_for_multi_profile(tmp_trestle_dir, False, True) prof_f_path = ModelUtils.path_for_top_level_model( tmp_trestle_dir, 'test_profile_f', prof.Profile, FileContentType.JSON ) cat = ProfileResolver.get_resolved_profile_catalog(tmp_trestle_dir, prof_f_path) interface = CatalogInterface(cat) ac_3 = interface.get_control('ac-3') assert len(ac_3.props) == 6 assert ac_3.props[-1].value == 'four' for part in ac_3.parts: if part.id == 'ac-3_stmt': assert len(part.props) == 4 ac_5 = interface.get_control('ac-5') for part in ac_5.parts: if part.id == 'ac-5_stmt': for sub_part in part.parts: if sub_part.id == 'ac-5_smt.a': assert len(sub_part.props) == 4
def catalog_interface_equivalent(cat_int_a: CatalogInterface, cat_b: cat.Catalog, strong=True) -> bool: """Test equivalence of catalog dict contents in various ways.""" cat_int_b = CatalogInterface(cat_b) if cat_int_b.get_count_of_controls_in_dict() != cat_int_a.get_count_of_controls_in_dict(): logger.error('count of controls is different') return False for a in cat_int_a.get_all_controls_from_dict(): try: b = cat_int_b.get_control(a.id) except Exception as e: logger.error(f'error finding control {a.id} {e}') if not controls_equivalent(a, b, strong): logger.error(f'controls differ: {a.id}') return False return True
def test_parameter_resolution(tmp_trestle_dir: pathlib.Path) -> None: """Test whether expected order of operations is preserved for parameter substution.""" test_utils.setup_for_multi_profile(tmp_trestle_dir, False, True) prof_e_path = ModelUtils.path_for_top_level_model( tmp_trestle_dir, 'test_profile_e', prof.Profile, FileContentType.JSON ) profile_e_parameter_string = '## Override value ##' profile_a_value = 'all alert personnel' # based on 800-53 rev 5 cat = ProfileResolver.get_resolved_profile_catalog(tmp_trestle_dir, prof_e_path) interface = CatalogInterface(cat) control = interface.get_control('ac-1') locations = interface.find_string_in_control(control, profile_e_parameter_string) locations_a = interface.find_string_in_control(control, profile_a_value) assert len(locations) == 1 assert len(locations_a) == 0 assert len(control.params[1].constraints) == 1
def test_all_positions_for_alter_can_be_resolved(tmp_trestle_dir: pathlib.Path) -> None: """Test that all alter adds positions can be resolved.""" cat_path = test_utils.JSON_TEST_DATA_PATH / test_utils.SIMPLIFIED_NIST_CATALOG_NAME repo = Repository(tmp_trestle_dir) repo.load_and_import_model(cat_path, 'nist_cat') prof_d_path = test_utils.JSON_TEST_DATA_PATH / 'test_profile_d.json' repo.load_and_import_model(prof_d_path, 'test_profile_d') cat = ProfileResolver.get_resolved_profile_catalog(tmp_trestle_dir, prof_d_path) interface = CatalogInterface(cat) control_a1 = interface.get_control('ac-1') control_a2 = interface.get_control('ac-2') assert control_a1.parts[0].id == 'ac-1_first_lev1' assert control_a1.parts[1].parts[3].id == 'ac-1_last_lev2' assert control_a1.parts[2].id == 'ac-1_after1_ac-1_smt_lev1' assert control_a1.parts[3].id == 'ac-1_after2_ac-1_smt_lev1' assert control_a1.parts[1].parts[0].parts[1].id == 'ac-1_smt_before1_a.2_lev3' assert control_a1.parts[1].parts[0].parts[2].id == 'ac-1_smt_before2_a.2_lev3' assert control_a1.parts[1].parts[0].parts[3].parts[0].id == 'ac-1_smt_inside1_at_the_end_a.2_lev4' assert control_a1.parts[1].parts[0].parts[3].parts[1].id == 'ac-1_smt_inside2_at_the_end_a.2_lev4' assert control_a2.parts[0].id == 'ac-2_implgdn_lev1'
def filter_ssp(self, trestle_root: pathlib.Path, ssp_name: str, profile_name: str, out_name: str, regenerate: bool, version: Optional[str]) -> int: """ Filter the ssp based on controls included by the profile and output new ssp. Args: trestle_root: root directory of the trestle project ssp_name: name of the ssp model profile_name: name of the profile model used for filtering out_name: name of the output ssp model with filtered controls regenerate: whether to regenerate the uuid's in the ssp version: new version for the model Returns: 0 on success, 1 otherwise """ ssp: ossp.SystemSecurityPlan ssp, _ = ModelUtils.load_top_level_model(trestle_root, ssp_name, ossp.SystemSecurityPlan, FileContentType.JSON) profile_path = ModelUtils.path_for_top_level_model( trestle_root, profile_name, prof.Profile, FileContentType.JSON) prof_resolver = ProfileResolver() catalog = prof_resolver.get_resolved_profile_catalog( trestle_root, profile_path) catalog_interface = CatalogInterface(catalog) # The input ssp should reference a superset of the controls referenced by the profile # Need to cull references in the ssp to controls not in the profile # Also make sure the output ssp contains imp reqs for all controls in the profile control_imp = ssp.control_implementation ssp_control_ids: Set[str] = set() new_set_params: List[ossp.SetParameter] = [] for set_param in as_list(control_imp.set_parameters): control = catalog_interface.get_control_by_param_id( set_param.param_id) if control is not None: new_set_params.append(set_param) ssp_control_ids.add(control.id) control_imp.set_parameters = new_set_params if new_set_params else None new_imp_requirements: List[ossp.ImplementedRequirement] = [] for imp_requirement in as_list(control_imp.implemented_requirements): control = catalog_interface.get_control(imp_requirement.control_id) if control is not None: new_imp_requirements.append(imp_requirement) ssp_control_ids.add(control.id) control_imp.implemented_requirements = new_imp_requirements # make sure all controls in the profile have implemented reqs in the final ssp if not ssp_control_ids.issuperset(catalog_interface.get_control_ids()): raise TrestleError( 'Unable to filter the ssp because the profile references controls not in it.' ) ssp.control_implementation = control_imp if regenerate: ssp, _, _ = ModelUtils.regenerate_uuids(ssp) if version: ssp.metadata.version = com.Version(__root__=version) ModelUtils.update_last_modified(ssp) ModelUtils.save_top_level_model(ssp, trestle_root, out_name, FileContentType.JSON) return CmdReturnCodes.SUCCESS.value
class Modify(Pipeline.Filter): """Modify the controls based on the profile.""" def __init__( self, profile: prof.Profile, change_prose: bool = False, block_adds: bool = False, block_params: bool = False, params_format: str = None, param_rep: ParameterRep = ParameterRep.VALUE_OR_LABEL_OR_CHOICES ) -> None: """Initialize the filter.""" self._profile = profile self._catalog_interface: Optional[CatalogInterface] = None self._block_adds = block_adds self._block_params = block_params self._change_prose = change_prose self._params_format = params_format self._param_rep = param_rep logger.debug( f'modify initialize filter with profile {profile.metadata.title}') @staticmethod def _replace_ids_with_text(prose: str, param_rep: ParameterRep, param_dict: Dict[str, common.Parameter]) -> str: """Find all instances of param_ids in prose and replace each with corresponding parameter representation. Need to check all values in dict for a match Reject matches where the string has an adjacent alphanumeric char: param_1 and param_10 or aparam_1 """ for param in param_dict.values(): if param.id not in prose: continue # create the replacement text for the param_id param_str = ControlIOReader.param_to_str(param, param_rep) # non-capturing groups are odd in re.sub so capture all 3 groups and replace the middle one pattern = r'(^|[^a-zA-Z0-9_])' + param.id + r'($|[^a-zA-Z0-9_])' prose = re.sub(pattern, r'\1' + param_str + r'\2', prose) return prose @staticmethod def _replace_params( text: str, param_dict: Dict[str, common.Parameter], params_format: Optional[str] = None, param_rep: ParameterRep = ParameterRep.VALUE_OR_LABEL_OR_CHOICES ) -> str: """ Replace params found in moustaches with values from the param_dict. A single line of prose may contain multiple moustaches. """ # first check if there are any moustache patterns in the text if param_rep == ParameterRep.LEAVE_MOUSTACHE: return text staches: List[str] = re.findall(r'{{.*?}}', text) if not staches: return text # now have list of all staches including braces, e.g. ['{{foo}}', '{{bar}}'] # clean the staches so they just have the param ids param_ids = [] for stache in staches: # remove braces so these are just param_ids but may have extra chars stache_contents = stache[2:(-2)] param_id = stache_contents.replace('insert: param,', '').strip() param_ids.append(param_id) # now replace original stache text with param values for i, _ in enumerate(staches): # A moustache may refer to a param_id not listed in the control's params if param_ids[i] not in param_dict: logger.warning( f'Control prose references param {param_ids[i]} not found in the control.' ) elif param_dict[param_ids[i]] is not None: param = param_dict[param_ids[i]] param_str = ControlIOReader.param_to_str( param, param_rep, False, False, params_format) text = text.replace(staches[i], param_str, 1) else: logger.warning( f'Control prose references param {param_ids[i]} with no specified value.' ) return text @staticmethod def _replace_part_prose( control: cat.Control, part: common.Part, param_dict: Dict[str, common.Parameter], params_format: Optional[str] = None, param_rep: ParameterRep = ParameterRep.VALUE_OR_LABEL_OR_CHOICES ) -> None: """Replace the part prose according to set_param.""" if part.prose is not None: fixed_prose = Modify._replace_params(part.prose, param_dict, params_format, param_rep) # change the prose in the control itself part.prose = fixed_prose for prt in as_list(part.parts): Modify._replace_part_prose(control, prt, param_dict, params_format, param_rep) for sub_control in as_list(control.controls): for prt in as_list(sub_control.parts): Modify._replace_part_prose(sub_control, prt, param_dict, params_format, param_rep) @staticmethod def _replace_control_prose( control: cat.Control, param_dict: Dict[str, common.Parameter], params_format: Optional[str] = None, param_rep: ParameterRep = ParameterRep.VALUE_OR_LABEL_OR_CHOICES ) -> None: """Replace the control prose according to set_param.""" for part in as_list(control.parts): if part.prose is not None: fixed_prose = Modify._replace_params(part.prose, param_dict, params_format, param_rep) # change the prose in the control itself part.prose = fixed_prose for prt in as_list(part.parts): Modify._replace_part_prose(control, prt, param_dict, params_format, param_rep) for sub_control in as_list(control.controls): prts: List[common.Part] = as_list(sub_control.parts) for prt in prts: Modify._replace_part_prose(sub_control, prt, param_dict, params_format, param_rep) @staticmethod def _add_contents_as_list(add: prof.Add) -> List[OBT]: add_list = [] add_list.extend(as_list(add.props)) add_list.extend(as_list(add.parts)) add_list.extend(as_list(add.links)) return add_list @staticmethod def _add_adds_to_part(part: common.Part, add: prof.Add) -> None: for attr in ['params', 'props', 'parts', 'links']: add_list = getattr(add, attr, None) if add_list: Modify._add_attr_to_part(part, add_list, attr, add.position) @staticmethod def _add_to_list(input_list: List[OBT], add: prof.Add) -> bool: """Add the contents of the add according to its by_id and position. Return True on success or False if id needed and not found. This is only called when by_id is not None. The add will be inserted if the id is found, or return False if not. This allows a separate recursive routine to search sub-lists for the id. """ add_list = Modify._add_contents_as_list(add) # Test here for matched by_id attribute. try: for index in range(len(input_list)): if input_list[index].id == add.by_id: if add.position == prof.Position.after: for offset, new_item in enumerate(add_list): input_list.insert(index + 1 + offset, new_item) return True elif add.position == prof.Position.before: for offset, new_item in enumerate(add_list): input_list.insert(index + offset, new_item) return True # if starting or ending, the adds go directly into this part according to type Modify._add_adds_to_part(input_list[index], add) return True except AttributeError: raise TrestleError( 'Cannot use "after" or "before" modifications for a list where elements' + ' do not contain the referenced by_id attribute.') return False @staticmethod def _add_to_parts(parts: List[common.Part], add: prof.Add) -> bool: """ Add the add to the parts. This is only called if add.by_id is not None. """ if Modify._add_to_list(parts, add): return True for part in parts: if part.parts is not None and Modify._add_to_parts( part.parts, add): return True return False @staticmethod def _add_attr_to_part(part: common.Part, items: List[OBT], attr: str, position: prof.Position) -> None: attr_list = as_list(getattr(part, attr, None)) if position in [prof.Position.starting, prof.Position.before]: items.extend(attr_list) attr_list = items else: attr_list.extend(items) setattr(part, attr, attr_list) @staticmethod def _add_attr_to_control(control: cat.Control, items: List[OBT], attr: str, position: prof.Position) -> None: attr_list = as_list(getattr(control, attr, None)) if position in [prof.Position.starting, prof.Position.before]: items.extend(attr_list) attr_list = items else: attr_list.extend(items) setattr(control, attr, attr_list) @staticmethod def _add_to_control(control: cat.Control, add: prof.Add) -> None: """First step in applying Add to control.""" control.parts = as_list(control.parts) if add.by_id is None or add.by_id == control.id: # add contents will be added to the control directly and with no recursion for attr in ['params', 'props', 'parts', 'links']: add_list = getattr(add, attr, None) if add_list: Modify._add_attr_to_control(control, add_list, attr, add.position) return else: # this is only called if by_id is not None if not Modify._add_to_parts(control.parts, add): logger.warning( f'Could not find id for add in control {control.id}: {add.by_id}' ) def _set_parameter_in_control(self, set_param: prof.SetParameter) -> None: """ Find the control with the param_id in it and set the parameter value. This does not recurse because expectation is that only top level params will be set. """ control = self._catalog_interface.get_control_by_param_id( set_param.param_id) if control is None: raise TrestleError( f'Set parameter object in profile does not have a corresponding param-id: "{set_param.param_id}"' ) control.params = as_list(control.params) param_ids = [param.id for param in control.params] if set_param.param_id not in param_ids: raise TrestleNotFoundError( f'Param id {set_param.param_id} not found in control {control.id}' ) index = param_ids.index(set_param.param_id) param = control.params[index] param.values = set_param.values param.constraints = set_param.constraints param.guidelines = set_param.guidelines param.links = set_param.links param.props = set_param.props param.select = set_param.select param.usage = set_param.usage control.params[index] = param self._catalog_interface.replace_control(control) def _change_prose_with_param_values(self): """Go through all controls and change prose based on param values.""" param_dict: Dict[str, common.Paramter] = {} # build the full mapping of params to values for control in self._catalog_interface.get_all_controls_from_dict(): param_dict.update( ControlIOReader.get_control_param_dict(control, False)) # insert param values into prose of all controls for control in self._catalog_interface.get_all_controls_from_dict(): self._replace_control_prose(control, param_dict, self._params_format, self._param_rep) def _modify_controls(self, catalog: cat.Catalog) -> cat.Catalog: """Modify the controls based on the profile.""" logger.debug( f'modify specify catalog {catalog.metadata.title} for profile {self._profile.metadata.title}' ) self._catalog_interface = CatalogInterface(catalog) alters: Optional[List[prof.Alter]] = None # find the modify and alters if self._profile.modify: # change all parameter values if self._profile.modify.set_parameters and not self._block_params: set_param_list = self._profile.modify.set_parameters for set_param in set_param_list: self._set_parameter_in_control(set_param) alters = self._profile.modify.alters if alters is not None: title = self._profile.metadata.title for alter in alters: if alter.control_id is None: logger.warning( f'Alter must have control id specified in profile {title}.' ) continue id_ = alter.control_id if alter.removes is not None: logger.warning( f'Alter not supported for removes in profile {title} control {id_}' ) continue # we want a warning about adds even if adds are blocked, as in profile generate if alter.adds is None: logger.warning( f'Alter has no adds in profile {title} control {id_}') continue if not self._block_adds: for add in alter.adds: if add.position is None and add.parts is not None: msg = f'Alter/Add position is not specified in profile {title} control {id_}' msg += ' when adding part, so defaulting to ending.' logger.warning(msg) add.position = prof.Position.ending control = self._catalog_interface.get_control(id_) if control is None: logger.warning( f'Alter/Add refers to control {id_} but it is not found in the import ' + f'for profile {self._profile.metadata.title}') else: self._add_to_control(control, add) self._catalog_interface.replace_control(control) if self._change_prose: # go through all controls and fix the prose based on param values self._change_prose_with_param_values() catalog = self._catalog_interface.get_catalog() # update the original profile metadata with new contents # roles and responsible-parties will be pulled in with new uuid's new_metadata = self._profile.metadata new_metadata.title = f'{catalog.metadata.title}: Resolved by profile {self._profile.metadata.title}' links: List[common.Link] = [] for import_ in self._profile.imports: links.append( common.Link(**{ 'href': import_.href, 'rel': 'resolution-source' })) new_metadata.links = links # move catalog controls from dummy group '' into the catalog for group in as_list(catalog.groups): if not group.id: catalog.controls = group.controls catalog.groups.remove(group) break catalog.metadata = new_metadata return catalog def process(self, catalog_iter: Iterator[cat.Catalog]) -> Iterator[cat.Catalog]: """Make the modifications to the controls based on the profile.""" catalog = next(catalog_iter) logger.debug( f'modify process with catalog {catalog.metadata.title} using profile {self._profile.metadata.title}' ) yield self._modify_controls(catalog)