Exemplo n.º 1
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)
Exemplo n.º 2
0
    def _parameter_table(self, control_id: str, level: int) -> str:
        """Print Param_id | Default (aka label) | Value or set to 'none'."""
        if not self._ssp:
            raise TrestleError('Cannot get parameter table, set SSP first.')

        writer = ControlIOWriter()
        control = self._catalog_interface.get_control(control_id)
        if not control:
            return ''
        params_lines = writer.get_params(control)

        tree = MarkdownNode.build_tree_from_markdown(params_lines)
        tree.change_header_level_by(level)
        return tree.content.raw_text
Exemplo n.º 3
0
    def get_statement_label_if_exists(self, control_id: str,
                                      statement_id: str) -> Tuple[Optional[str], Optional[common.Part]]:
        """Get statement label if given."""

        def does_part_exists(part: common.Part) -> bool:
            does_match = False
            if part.name and part.name in {'statement', 'item'} and part.id == statement_id:
                does_match = True
            return does_match

        control = self.get_control(control_id)
        if not control:
            return '', None
        label = None
        found_part = None
        if control.parts:
            for part in as_list(control.parts):
                # Performance OSCAL assumption, ids are nested so recurse only if prefix
                if part.id and statement_id.startswith(part.id):
                    part = self.find_part_with_condition(part, does_part_exists)
                    if part:
                        label = ControlIOWriter.get_label(part)
                        found_part = part
                        break

        return label, found_part
Exemplo n.º 4
0
 def delete_withdrawn_controls(self) -> None:
     """Delete all withdrawn controls from the catalog."""
     delete_list = []
     for control in self.get_all_controls_from_dict():
         if ControlIOWriter.is_withdrawn(control):
             delete_list.append(control.id)
     for id_ in delete_list:
         self.delete_control(id_)
Exemplo n.º 5
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')
Exemplo n.º 6
0
    def get_control_part_prose(self, control_id: str, part_name: str) -> str:
        """
        Get the prose for a named part in the control.

        Args:
            control_id: id of the control
            part_name: name of the part

        Returns:
            Single string concatenating prose from all parts and sub-parts in control with that name.
        """
        control = self.get_control(control_id)
        return ControlIOWriter.get_part_prose(control, part_name)
Exemplo n.º 7
0
    def get_control_statement(self, control_id: str, level: int) -> str:
        """
        Get the control statement for an ssp - to be printed in markdown as a structured list.

        Args:
            control_id: The control_id to use.

        Returns:
            A markdown blob as a string.
        """
        if not self._resolved_catalog:
            raise TrestleError(
                'Cannot get control statement, set resolved catalog first.')

        writer = ControlIOWriter()
        control = self._catalog_interface.get_control(control_id)
        if not control:
            return ''

        control_lines = writer.get_control_statement(control)

        return self._build_tree_and_adjust(control_lines, level)
Exemplo n.º 8
0
def test_merge_dicts_deep(overwrite_header_values) -> None:
    """Test deep merge of dicts."""
    dest = {
        'trestle': {
            'foo': {
                'hello': 1
            }
        },
        'fedramp': {
            'roles': [5, 6],
            'values': 8
        },
        'orig': 11
    }
    src = {
        'trestle': {
            'foo': {
                'hello': 3
            },
            'bar': 4
        },
        'fedramp': {
            'roles': 7,
            'values': 10
        },
        'extra': 12
    }
    ControlIOWriter.merge_dicts_deep(dest, src, overwrite_header_values)
    if not overwrite_header_values:
        assert dest['trestle'] == {'foo': {'hello': 1}, 'bar': 4}
        assert dest['fedramp'] == {'roles': [5, 6], 'values': 8}
        assert dest['orig'] == 11
        assert dest['extra'] == 12
    else:
        assert dest['trestle'] == {'foo': {'hello': 3}, 'bar': 4}
        assert dest['fedramp'] == {'roles': 7, 'values': 10}
        assert dest['orig'] == 11
        assert dest['extra'] == 12
Exemplo n.º 9
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')
Exemplo n.º 10
0
def test_merge_dicts_deep_empty() -> None:
    """Test that empty items are left alone."""
    dest = {'foo': ''}
    src = {'foo': 'fancy value'}
    ControlIOWriter.merge_dicts_deep(dest, src, False)
    assert dest['foo'] == ''
    dest['foo'] = None
    ControlIOWriter.merge_dicts_deep(dest, src, False)
    assert dest['foo'] is None
    ControlIOWriter.merge_dicts_deep(dest, src, True)
    assert dest['foo'] == 'fancy value'
Exemplo n.º 11
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
            )
Exemplo n.º 12
0
    def jinja_ify(
        trestle_root: pathlib.Path,
        r_input_file: pathlib.Path,
        r_output_file: pathlib.Path,
        ssp: Optional[str],
        profile: Optional[str],
        lut: Optional[Dict[str, Any]] = None,
        number_captions: Optional[bool] = False,
        parameters_formatting: Optional[str] = None
    ) -> int:
        """Run jinja over an input file with additional booleans."""
        if lut is None:
            lut = {}
        template_folder = pathlib.Path.cwd()
        jinja_env = Environment(
            loader=FileSystemLoader(template_folder),
            extensions=[MDSectionInclude, MDCleanInclude, MDDatestamp],
            trim_blocks=True,
            autoescape=True
        )
        template = jinja_env.get_template(str(r_input_file))
        # create boolean dict
        if operator.xor(bool(ssp), bool(profile)):
            raise TrestleIncorrectArgsError('Both SSP and profile should be provided or not at all')
        if ssp:
            # name lookup
            ssp_data, _ = ModelUtils.load_top_level_model(trestle_root, ssp, SystemSecurityPlan)
            lut['ssp'] = ssp_data
            _, profile_path = ModelUtils.load_top_level_model(trestle_root, profile, Profile)
            profile_resolver = ProfileResolver()
            resolved_catalog = profile_resolver.get_resolved_profile_catalog(
                trestle_root, profile_path, False, False, parameters_formatting
            )

            ssp_writer = SSPMarkdownWriter(trestle_root)
            ssp_writer.set_ssp(ssp_data)
            ssp_writer.set_catalog(resolved_catalog)
            lut['catalog'] = resolved_catalog
            lut['catalog_interface'] = CatalogInterface(resolved_catalog)
            lut['control_io_writer'] = ControlIOWriter()
            lut['ssp_md_writer'] = ssp_writer

        new_output = template.render(**lut)
        output = ''
        # This recursion allows nesting within expressions (e.g. an expression can contain jinja templates).
        error_countdown = JinjaCmd.max_recursion_depth
        while new_output != output and error_countdown > 0:
            error_countdown = error_countdown - 1
            output = new_output
            random_name = uuid.uuid4()  # Should be random and not used.
            dict_loader = DictLoader({str(random_name): new_output})
            jinja_env = Environment(
                loader=ChoiceLoader([dict_loader, FileSystemLoader(template_folder)]),
                extensions=[MDCleanInclude, MDSectionInclude, MDDatestamp],
                autoescape=True,
                trim_blocks=True
            )
            template = jinja_env.get_template(str(random_name))
            new_output = template.render(**lut)

        output_file = trestle_root / r_output_file
        if number_captions:
            output_file.open('w', encoding=const.FILE_ENCODING).write(_number_captions(output))
        else:
            output_file.open('w', encoding=const.FILE_ENCODING).write(output)

        return CmdReturnCodes.SUCCESS.value
Exemplo n.º 13
0
        part_b2.parts = [part_b2i]
        part_b.parts = [part_b1, part_b2]
        parts = [part_a, part_b, part_c]
    elif case == case_3:
        part_b2.parts = [part_b2i]
        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: