示例#1
0
def test_bad_unicode_in_file(tmp_path: pathlib.Path) -> None:
    """Test error on read of bad unicode in control markdown."""
    bad_file = tmp_path / 'bad_unicode.md'
    with open(bad_file, 'wb') as f:
        f.write(b'\x81')
    with pytest.raises(TrestleError):
        ControlIOReader._load_control_lines_and_header(bad_file)
示例#2
0
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'
示例#3
0
def test_control_with_components() -> None:
    """Test loading and parsing of implementated reqs with components."""
    control_path = pathlib.Path(
        'tests/data/author/controls/control_with_components.md').resolve()
    comp_prose_dict, _ = ControlIOReader.read_all_implementation_prose_and_header(
        control_path)
    assert len(comp_prose_dict.keys()) == 3
    assert len(comp_prose_dict['This System'].keys()) == 3
    assert len(comp_prose_dict['Trestle Component'].keys()) == 1
    assert len(comp_prose_dict['Fancy Thing'].keys()) == 2
    assert comp_prose_dict['Fancy Thing']['a.'] == [
        'Text for fancy thing component'
    ]

    # need to build the needed components so they can be referenced
    comp_dict = {}
    for comp_name in comp_prose_dict.keys():
        comp = gens.generate_sample_model(ossp.SystemComponent)
        comp.title = comp_name
        comp_dict[comp_name] = comp

    # confirm that the header content was inserted into the props of the imp_req
    imp_req = ControlIOReader.read_implemented_requirement(
        control_path, comp_dict)
    assert len(imp_req.props) == 12
    assert len(imp_req.statements) == 3
    assert len(imp_req.statements[0].by_components) == 3
示例#4
0
def test_write_control_header_params(overwrite_header_values,
                                     tmp_path: pathlib.Path) -> None:
    """Test write/read of control header params."""
    # orig file just has one param ac-1_prm_3
    src_control_path = pathlib.Path(
        'tests/data/author/controls/control_with_components_and_params.md')
    # header has two params - 3 and 4
    header = {
        const.SET_PARAMS_TAG: {
            'ac-1_prm_3': {
                'values': 'new prm_3 val from input header'
            },
            'ac-1_prm_4': {
                'values': 'new prm_4 val from input header'
            }
        },
        'foo': 'new bar',
        'new-reviewer': 'James',
        'special': 'new value to ignore',
        'none-thing': 'none value to ignore'
    }
    control_path = tmp_path / 'ac-1.md'
    shutil.copyfile(src_control_path, control_path)
    markdown_processor = MarkdownProcessor()
    # header_1 should have one param: 3
    header_1, _ = markdown_processor.read_markdown_wo_processing(control_path)
    assert len(header_1.keys()) == 8
    orig_control_read, group_title = ControlIOReader.read_control(
        control_path, True)
    assert group_title == 'Access Control'
    control_writer = ControlIOWriter()
    # write the control back out with the test header
    control_writer.write_control(tmp_path, orig_control_read, group_title,
                                 header, None, False, False, None,
                                 overwrite_header_values, None, None)
    # header_2 should have 2 params: 3 and 4
    header_2, _ = markdown_processor.read_markdown_wo_processing(control_path)
    assert len(header_2.keys()) == 9
    assert header_2['new-reviewer'] == 'James'
    assert len(header_2[const.SET_PARAMS_TAG]) == 2
    assert 'new' in header_2[const.SET_PARAMS_TAG]['ac-1_prm_4']['values']
    if not overwrite_header_values:
        assert 'orig' in header_2[const.SET_PARAMS_TAG]['ac-1_prm_3']['values']
        assert header_2['foo'] == 'bar'
        assert header_2['special'] == ''
        assert header_2['none-thing'] is None
    else:
        assert 'new' in header_2[const.SET_PARAMS_TAG]['ac-1_prm_3']['values']
        assert header_2['foo'] == 'new bar'
        assert header_2['special'] == 'new value to ignore'
        assert header_2['none-thing'] == 'none value to ignore'
        assert 'orig' in orig_control_read.params[0].values[0].__root__
    new_control_read, _ = ControlIOReader.read_control(control_path, True)
    # insert the new param in the orig control so we can compare the two controls
    orig_control_read.params.append(new_control_read.params[1])
    if overwrite_header_values:
        orig_control_read.params[0] = new_control_read.params[0]
    assert test_utils.controls_equivalent(orig_control_read, new_control_read)
示例#5
0
def test_read_control_no_label(testdata_dir: pathlib.Path) -> None:
    """Test reading a control that doesn't have a part label in statement."""
    md_file = testdata_dir / 'author/controls/control_no_labels.md'
    control, group_title = ControlIOReader.read_control(md_file, True)
    assert group_title == 'My Group Title'
    assert control.parts[0].parts[2].props[0].value == 'c'
    assert control.parts[0].parts[2].parts[0].props[0].value == '1'
    md_file = testdata_dir / 'author/controls/control_some_labels.md'
    control, group_title = ControlIOReader.read_control(md_file, True)
    assert group_title == 'My Group Title'
    assert control.parts[0].parts[2].props[0].value == 'aa'
    assert control.parts[0].parts[2].parts[1].props[0].value == 'abc13'
    assert control.parts[0].parts[3].props[0].value == 'ab'
示例#6
0
 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)
示例#7
0
def confirm_control_contains(trestle_dir: pathlib.Path, control_id: str,
                             part_label: str, seek_str: str) -> bool:
    """Confirm the text is present in the control markdown in the correct part."""
    control_dir = trestle_dir / ssp_name / control_id.split('-')[0]
    md_file = control_dir / f'{control_id}.md'

    comp_dict, _ = ControlIOReader.read_all_implementation_prose_and_header(
        md_file)
    for label_dict in comp_dict.values():
        if part_label in label_dict:
            prose = '\n'.join(label_dict[part_label])
            if seek_str in prose:
                return True
    return False
示例#8
0
def test_get_profile_param_dict(tmp_trestle_dir: pathlib.Path) -> None:
    """Test get profile param dict for control."""
    test_utils.setup_for_multi_profile(tmp_trestle_dir, False, True)
    profile, profile_path = ModelUtils.load_top_level_model(
        tmp_trestle_dir, 'test_profile_a', prof.Profile, FileContentType.JSON)
    profile_resolver = ProfileResolver()
    catalog = profile_resolver.get_resolved_profile_catalog(
        tmp_trestle_dir, profile_path)
    catalog_interface = CatalogInterface(catalog)
    control = catalog_interface.get_control('ac-1')

    full_param_dict = CatalogInterface._get_full_profile_param_dict(profile)
    control_param_dict = CatalogInterface._get_profile_param_dict(
        control, full_param_dict, False)
    assert ControlIOReader.param_to_str(
        control_param_dict['ac-1_prm_1'],
        ParameterRep.VALUE_OR_LABEL_OR_CHOICES) == 'all alert personnel'
    assert ControlIOReader.param_to_str(
        control_param_dict['ac-1_prm_6'],
        ParameterRep.VALUE_OR_LABEL_OR_CHOICES) == 'monthly'
    # param 7 has no value so its label will be used
    assert ControlIOReader.param_to_str(control_param_dict['ac-1_prm_7'],
                                        ParameterRep.VALUE_OR_LABEL_OR_CHOICES
                                        ) == 'organization-defined events'
示例#9
0
    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
示例#10
0
def test_control_failures(tmp_path: pathlib.Path) -> None:
    """Test various failure modes."""
    part = common.Part(name='foo')
    assert ControlIOWriter.get_label(part) == ''

    assert ControlIOReader._strip_to_make_ncname('1a@foo') == 'afoo'
    with pytest.raises(TrestleError):
        ControlIOReader._strip_to_make_ncname('1@')

    with pytest.raises(TrestleError):
        ControlIOReader._indent('')

    with pytest.raises(TrestleError):
        ControlIOReader._indent('  foo')
示例#11
0
def test_control_objective(tmp_path: pathlib.Path) -> None:
    """Test read and write of control with objective."""
    # write the control directly as raw markdown text
    md_path = tmp_path / 'xy-9.md'
    with open(md_path, 'w') as f:
        f.write(control_text)
    # read it in as markdown to an OSCAL control in memory
    control, group_title = ControlIOReader.read_control(md_path, True)
    assert group_title == 'My Group Title'
    sub_dir = tmp_path / 'sub_dir'
    sub_dir.mkdir(exist_ok=True)
    # write it out as markdown in a separate directory to avoid name clash
    control_writer = ControlIOWriter()
    control_writer.write_control(sub_dir, control, 'My Group Title', None,
                                 None, False, False, None, False, None, None)
    # confirm the newly written markdown text is identical to what was read originally
    assert test_utils.text_files_equal(md_path, sub_dir / 'xy-9.md')
示例#12
0
 def read_additional_content(md_path: pathlib.Path,
                             required_sections_list: List[str]) -> Tuple[List[prof.Alter], Dict[str, Any]]:
     """Read all markdown controls and return list of alters plus control param dict."""
     new_alters: List[prof.Alter] = []
     final_param_dict: Dict[str, Any] = {}
     for group_path in CatalogInterface._get_group_ids_and_dirs(md_path).values():
         for control_file in CatalogInterface._get_sorted_control_paths(group_path):
             control_alters, control_param_dict = ControlIOReader.read_new_alters_and_params(
                 control_file,
                 required_sections_list
             )
             new_alters.extend(control_alters)
             for param_id, param_dict in control_param_dict.items():
                 # if profile_values are present, overwrite values with them
                 if const.PROFILE_VALUES in param_dict:
                     param_dict[const.VALUES] = param_dict.pop(const.PROFILE_VALUES)
                     final_param_dict[param_id] = param_dict
     return new_alters, final_param_dict
示例#13
0
    def _get_profile_param_dict(
        control: cat.Control, profile_param_dict: Dict[str, common.Parameter], values_only: bool
    ) -> Dict[str, common.Parameter]:
        """
        Get the dict of params for this control including possible overrides made by the profile modifications.

        Args:
            control: The control being queried
            profile_param_dict: The full dict of params and modified values made by the profile

        Returns:
            mapping of param ids to their final parameter states after possible modify by the profile setparameters
        """
        # get the mapping of param_id's to params for this control, excluding those with no value set
        param_dict = ControlIOReader.get_control_param_dict(control, values_only)
        for key in param_dict.keys():
            if key in profile_param_dict:
                param_dict[key] = profile_param_dict[key]
        return param_dict
示例#14
0
    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
示例#15
0
    def read_catalog_imp_reqs(md_path: pathlib.Path,
                              avail_comps: Dict[str, ossp.SystemComponent]) -> List[ossp.ImplementedRequirement]:
        """Read the full set of control implemented requirements from markdown.

        Args:
            md_path: Path to the markdown control files, with directories for each group
            avail_comps: Dict mapping component names to known components

        Returns:
            List of implemented requirements gathered from each control

        Notes:
            As the controls are read into the catalog the needed components are added if not already available.
            avail_comps provides the mapping of component name to the actual component.
        """
        imp_reqs: List[ossp.ImplementedRequirement] = []
        for group_path in CatalogInterface._get_group_ids_and_dirs(md_path).values():
            for control_file in CatalogInterface._get_sorted_control_paths(group_path):
                imp_reqs.append(ControlIOReader.read_implemented_requirement(control_file, avail_comps))
        return imp_reqs
示例#16
0
    def read_catalog_from_markdown(self, md_path: pathlib.Path, set_parameters: bool) -> cat.Catalog:
        """
        Read the groups and catalog controls from the given directory.

        This will overwrite the existing groups and controls in the catalog.
        """
        if not self._catalog:
            self._catalog = gens.generate_sample_model(cat.Catalog)
        id_map = CatalogInterface._get_group_ids_and_dirs(md_path)
        groups: List[cat.Group] = []
        # read each group dir
        for group_id, group_dir in id_map.items():
            control_list = []
            group_title = ''
            # Need to get group title from at least one control in this directory
            # All controls in dir should have same group title
            # Set group title to the first one found and warn if different non-empty title appears
            # Controls with empty group titles are tolerated but at least one title must be present or warning given
            # The special group with no name that has the catalog as parent is just a list and has no title
            for control_path in CatalogInterface._get_sorted_control_paths(group_dir):
                control, control_group_title = ControlIOReader.read_control(control_path, set_parameters)
                if control_group_title:
                    if group_title:
                        if control_group_title != group_title:
                            logger.warning(
                                f'Control {control.id} group title {control_group_title} differs from {group_title}'
                            )
                    else:
                        group_title = control_group_title
                control_list.append(control)
            if group_id:
                if not group_title:
                    logger.warning(f'No group title found in controls for group {group_id}')
                new_group = cat.Group(id=group_id, title=group_title)
                new_group.controls = control_list
                groups.append(new_group)
            else:
                # if the list of controls has no group id it also has no title and is just the controls of the catalog
                self._catalog.controls = control_list
        self._catalog.groups = groups if groups else None
        return self._catalog
示例#17
0
    def write_catalog_as_markdown(
        self,
        md_path: pathlib.Path,
        yaml_header: dict,
        sections_dict: Optional[Dict[str, str]],
        prompt_responses: bool,
        additional_content: bool = False,
        profile: Optional[prof.Profile] = None,
        overwrite_header_values: bool = False,
        set_parameters: bool = False,
        required_sections: Optional[str] = None,
        allowed_sections: Optional[str] = None
    ) -> None:
        """
        Write out the catalog controls from dict as markdown files to the specified directory.

        Args:
            md_path: Path to directory in which to write the markdown
            yaml_header: Dictionary to write into the yaml header of the controls
            sections_dict: Optional dict mapping section short names to long
            prompt_responses: Whether to prompt for responses in the control markdown
            additional_content: Should the additional content be printed corresponding to profile adds
            profile: Optional profile containing the adds making up additional content
            overwrite_header_values: Overwrite existing values in markdown header content but add new content
            set_parameters: Set header values based on params in the control and in the profile
            required_sections: Optional string containing list of sections that should be prompted for prose
            allowed_sections: Optional string containing list of sections that should be included in markdown

        Returns:
            None
        """
        writer = ControlIOWriter()
        required_section_list = required_sections.split(',') if required_sections else []
        allowed_section_list = allowed_sections.split(',') if allowed_sections else []

        # create the directory in which to write the control markdown files
        md_path.mkdir(exist_ok=True, parents=True)
        catalog_interface = CatalogInterface(self._catalog)
        # get the list of SetParams for this profile
        full_profile_param_dict = CatalogInterface._get_full_profile_param_dict(profile) if profile else {}
        # write out the controls
        for control in catalog_interface.get_all_controls_from_catalog(True):
            # make copy of incoming yaml header
            new_header = copy.deepcopy(yaml_header)
            # here we do special handling of how set-parameters merge with the yaml header
            if set_parameters:
                # get all params for this control
                control_param_dict = ControlIOReader.get_control_param_dict(control, False)
                set_param_dict: Dict[str, str] = {}
                for param_id, param_dict in control_param_dict.items():
                    # if the param is in the profile set_params, load its contents first and mark as profile-values
                    if param_id in full_profile_param_dict:
                        # get the param from the profile set_param
                        param = full_profile_param_dict[param_id]
                        # assign its contents to the dict
                        new_dict = ModelUtils.parameter_to_dict(param, True)
                        profile_values = new_dict.get(const.VALUES, None)
                        if profile_values:
                            new_dict[const.PROFILE_VALUES] = profile_values
                            new_dict.pop(const.VALUES)
                        # then insert the original, incoming values as values
                        if param_id in control_param_dict:
                            orig_param = control_param_dict[param_id]
                            orig_dict = ModelUtils.parameter_to_dict(orig_param, True)
                            new_dict[const.VALUES] = orig_dict.get(const.VALUES, None)
                            # merge contents from the two sources with priority to the profile-param
                            for item in ['select', 'label']:
                                if item in orig_dict and item not in new_dict:
                                    new_dict[item] = orig_dict[item]
                    else:
                        new_dict = ModelUtils.parameter_to_dict(param_dict, True)
                    new_dict.pop('id')
                    set_param_dict[param_id] = new_dict
                if set_param_dict:
                    if const.SET_PARAMS_TAG not in new_header:
                        new_header[const.SET_PARAMS_TAG] = {}
                    if overwrite_header_values:
                        # update the control params with new values
                        for key, value in new_header[const.SET_PARAMS_TAG].items():
                            if key in control_param_dict:
                                set_param_dict[key] = value
                    else:
                        # update the control params with any values in yaml header not set in control
                        # need to maintain order in the set_param_dict
                        for key, value in new_header[const.SET_PARAMS_TAG].items():
                            if key in control_param_dict and key not in set_param_dict:
                                set_param_dict[key] = value
                    new_header[const.SET_PARAMS_TAG] = set_param_dict
                elif const.SET_PARAMS_TAG in new_header:
                    # need to cull any params that are not in control
                    pop_list: List[str] = []
                    for key in new_header[const.SET_PARAMS_TAG].keys():
                        if key not in control_param_dict:
                            pop_list.append(key)
                    for pop in pop_list:
                        new_header[const.SET_PARAMS_TAG].pop(pop)
            _, group_title, _ = catalog_interface.get_group_info_by_control(control.id)
            # control could be in sub-group of group so build path to it
            group_dir = md_path
            control_path = catalog_interface._get_control_path(control.id)
            for sub_dir in control_path:
                group_dir = group_dir / sub_dir
                if not group_dir.exists():
                    group_dir.mkdir(parents=True, exist_ok=True)
            writer.write_control(
                group_dir,
                control,
                group_title,
                new_header,
                sections_dict,
                additional_content,
                prompt_responses,
                profile,
                overwrite_header_values,
                required_section_list,
                allowed_section_list
            )
示例#18
0
        part_b.parts = [part_b1, part_b2]
        parts = [part_a, part_b]
    else:
        parts = None

    statement_part.parts = parts
    control.parts = [statement_part]
    if sections:
        control.parts.extend([sec_1, sec_2])

    writer = ControlIOWriter()
    writer.write_control(tmp_path, control, 'My Group Title', None, None,
                         additional_content, False, None, False, None, None)

    md_path = tmp_path / f'{control.id}.md'
    reader = ControlIOReader()
    new_control, group_title = reader.read_control(md_path, False)
    new_control.title = dummy_title
    assert group_title == 'My Group Title'
    assert len(new_control.parts) == len(control.parts)
    assert control.parts[0].prose == new_control.parts[0].prose
    assert control.parts[0].parts == new_control.parts[0].parts
    assert control == new_control


def test_control_objective(tmp_path: pathlib.Path) -> None:
    """Test read and write of control with objective."""
    # write the control directly as raw markdown text
    md_path = tmp_path / 'xy-9.md'
    with open(md_path, 'w') as f:
        f.write(control_text)
示例#19
0
def test_control_bad_components(md_file: str) -> None:
    """Test loading of imp reqs for control with bad components."""
    control_path = pathlib.Path('tests/data/author/controls/') / md_file
    with pytest.raises(TrestleError):
        ControlIOReader.read_all_implementation_prose_and_header(control_path)
示例#20
0
def test_broken_yaml_header(testdata_dir: pathlib.Path) -> None:
    """Test for a bad markdown header."""
    bad_file = testdata_dir / 'author' / 'bad_md_header.md'
    with pytest.raises(TrestleError):
        ControlIOReader._load_control_lines_and_header(bad_file)
示例#21
0
def test_create_next_label(prev_label, next_label, indent) -> None:
    """Test bumping of label strings."""
    assert ControlIOReader._create_next_label(prev_label, indent) == next_label
示例#22
0
def test_bump_label(prev_label, bumped_label) -> None:
    """Test bumping of label strings."""
    assert ControlIOReader._bump_label(prev_label) == bumped_label