Skip to content

control_writer

trestle.core.control_writer ¤

Handle writing of controls to markdown.

logger ¤

Classes¤

ControlWriter ¤

Class to write controls as markdown.

Source code in trestle/core/control_writer.py
class ControlWriter():
    """Class to write controls as markdown."""

    def __init__(self):
        """Initialize the class."""
        self._md_file: Optional[MDWriter] = None

    def _add_part_and_its_items(self, control: cat.Control, name: str, item_type: str) -> None:
        """For a given control add its one statement and its items to the md file after replacing params."""
        items = []
        if control.parts:
            for part in control.parts:
                if part.name == name:
                    # If the part has prose write it as a raw line and not list element
                    skip_id = part.id
                    if part.prose:
                        self._md_file.new_line(part.prose)
                    items.append(ControlInterface.get_part(part, item_type, skip_id))
            # unwrap the list if it is many levels deep
            while not isinstance(items, str) and len(items) == 1:
                items = items[0]
            self._md_file.new_paragraph()
            self._md_file.new_list(items)

    def _add_yaml_header(self, yaml_header: Optional[Dict]) -> None:
        if yaml_header:
            self._md_file.add_yaml_header(yaml_header)

    def _add_control_statement(self, control: cat.Control, group_title: str, print_group_title=True) -> None:
        """Add the control statement and items to the md file."""
        self._md_file.new_paragraph()
        control_id = control.id
        group_name = ''
        control_title = control.title

        if print_group_title:
            group_name = ' \[' + group_title + '\]'

        title = f'{control_id} -{group_name} {control_title}'

        header_title = 'Control Statement'
        self._md_file.new_header(level=1, title=title)
        self._md_file.new_header(level=2, title=header_title)
        self._md_file.set_indent_level(-1)
        self._add_part_and_its_items(control, const.STATEMENT, const.ITEM)
        self._md_file.set_indent_level(-1)

    def _add_control_objective(self, control: cat.Control) -> None:
        if control.parts:
            for part in control.parts:
                if part.name == const.OBJECTIVE_PART or part.name == const.ASSESMENT_OBJECTIVE_PART:
                    self._md_file.new_paragraph()
                    heading_title = 'Control Objective'
                    if part.name == const.ASSESMENT_OBJECTIVE_PART:
                        heading_title = 'Control Assessment Objective'
                    self._md_file.new_header(level=2, title=heading_title)
                    self._md_file.set_indent_level(-1)
                    self._add_part_and_its_items(control, part.name, part.name)
                    self._md_file.set_indent_level(-1)
                    return

    def _add_sections(self, control: cat.Control, allowed_sections: Optional[List[str]]) -> None:
        """Add the extra control sections after the main ones."""
        skip_section_list = [const.STATEMENT, const.ITEM, const.OBJECTIVE_PART, const.ASSESMENT_OBJECTIVE_PART]
        while True:
            _, name, title, prose = ControlInterface.get_section(control, skip_section_list)
            if not name:
                return
            if allowed_sections and name not in allowed_sections:
                skip_section_list.append(name)
                continue
            if prose:
                # section title will be from the section_dict, the part title, or the part name in that order
                # this way the user-provided section title can override the part title
                section_title = self._sections_dict.get(name, title) if self._sections_dict else title
                section_title = section_title if section_title else name
                skip_section_list.append(name)
                self._md_file.new_header(level=2, title=f'Control {section_title}')
                self._md_file.new_line(prose)
                self._md_file.new_paragraph()

    def _insert_status(self, status: ImplementationStatus, level: int) -> None:
        self._md_file.new_header(level=level, title=f'{const.IMPLEMENTATION_STATUS_HEADER}: {status.state}')
        # this used to output remarks also

    def _insert_rules(self, rules: List[str], level: int) -> None:
        if rules:
            self._md_file.new_header(level=level, title='Rules:')
            self._md_file.set_indent_level(0)
            self._md_file.new_list(rules)
            self._md_file.set_indent_level(-1)

    def _insert_comp_info(
        self, part_label: str, comp_info: Dict[str, ComponentImpInfo], context: ControlContext
    ) -> None:
        """Insert prose and status from the component info."""
        level = 3 if context.purpose == ContextPurpose.COMPONENT else 4
        if part_label in comp_info:
            info = comp_info[part_label]
            if context.purpose in [ContextPurpose.COMPONENT, ContextPurpose.SSP] and not info.rules:
                return
            self._md_file.new_paragraph()
            if info.prose:
                self._md_file.new_line(info.prose)
            else:
                self._md_file.new_line(f'{const.SSP_ADD_IMPLEMENTATION_FOR_ITEM_TEXT} {part_label} -->')

            self._insert_rules(info.rules, level)
            self._insert_status(info.status, level)
        else:
            self._insert_status(ImplementationStatus(state=const.STATUS_PLANNED), level)

    def _add_component_control_prompts(self, control_id: str, comp_dict: CompDict, context: ControlContext) -> bool:
        """Add prompts to the markdown for the control itself, per component."""
        if context.purpose not in [ContextPurpose.COMPONENT, ContextPurpose.SSP]:
            return False
        self._md_file.new_paraline(const.STATUS_PROMPT)
        self._md_file.new_paraline(const.RULES_WARNING)
        did_write = False
        # do special handling for This System
        if context.purpose == ContextPurpose.SSP:
            self._md_file.new_paragraph()
            self._md_file.new_header(3, const.SSP_MAIN_COMP_NAME)
            self._md_file.new_paragraph()
            prose = f'{const.SSP_ADD_THIS_SYSTEM_IMPLEMENTATION_FOR_CONTROL_TEXT}: {control_id} -->'
            status = ImplementationStatus(state=const.STATUS_PLANNED)
            if const.SSP_MAIN_COMP_NAME in comp_dict:
                comp_info = list(comp_dict[const.SSP_MAIN_COMP_NAME].values())[0]
                if comp_info.prose:
                    prose = comp_info.prose
                status = comp_info.status
            self._md_file.new_paraline(prose)
            self._insert_status(status, 4)
            did_write = True
        sorted_comp_names = sorted(comp_dict.keys())
        for comp_name in sorted_comp_names:
            dic = comp_dict[comp_name]
            # This System already handled
            if comp_name == const.SSP_MAIN_COMP_NAME:
                continue
            for comp_info in [val for key, val in dic.items() if key == '']:
                # don't output component name for component markdown since only one component
                if context.purpose != ContextPurpose.COMPONENT:
                    self._md_file.new_header(3, comp_name)
                prose = comp_info.prose if comp_info.prose != control_id else ''
                if not prose:
                    prose = f'{const.SSP_ADD_IMPLEMENTATION_FOR_CONTROL_TEXT}: {control_id} -->'
                self._md_file.new_paraline(prose)
                level = 3 if context.purpose == ContextPurpose.COMPONENT else 4
                self._insert_rules(comp_info.rules, level)
                self._insert_status(comp_info.status, level)
                did_write = True
        return did_write

    def _add_implementation_response_prompts(
        self, control: cat.Control, comp_dict: CompDict, context: ControlContext
    ) -> None:
        """Add the response request text for all parts to the markdown along with the header."""
        self._md_file.new_hr()
        self._md_file.new_paragraph()
        # top level request for implementation details
        self._md_file.new_header(level=2, title=f'{const.SSP_MD_IMPLEMENTATION_QUESTION}')

        # write out control level prose and status
        did_write_part = self._add_component_control_prompts(control.id, comp_dict, context)

        # if the control has no parts written out then enter implementation in the top level entry
        # but if it does have parts written out, leave top level blank and provide details in the parts
        # Note that parts corresponding to sections don't get written out here so a check is needed
        # If we have responses per component then enter them in separate ### sections
        if control.parts:
            for part in control.parts:
                if part.parts and part.name == const.STATEMENT:
                    for prt in part.parts:
                        if prt.name != const.ITEM:
                            continue
                        # if no label guess the label from the sub-part id
                        part_label = ControlInterface.get_label(prt)
                        part_label = prt.id.split('.')[-1] if not part_label else part_label
                        # only write out part if rules apply to it
                        rules_apply = False
                        for _, dic in comp_dict.items():
                            if part_label in dic and dic[part_label].rules:
                                rules_apply = True
                                break
                        if not rules_apply:
                            continue
                        if not did_write_part:
                            self._md_file.new_line(const.SSP_MD_LEAVE_BLANK_TEXT)
                            # insert extra line to make mdformat happy
                            self._md_file._add_line_raw('')
                        self._md_file.new_hr()
                        self._md_file.new_header(level=2, title=f'Implementation for part {part_label}')
                        wrote_label_content = False
                        sorted_comp_names = sorted(comp_dict.keys())
                        for comp_name in sorted_comp_names:
                            dic = comp_dict[comp_name]
                            if comp_name == const.SSP_MAIN_COMP_NAME:
                                continue
                            if part_label in dic:
                                # insert the component name for ssp but not for comp_def
                                # because there should only be one component in generated comp_def markdown
                                if context.purpose != ContextPurpose.COMPONENT:
                                    self._md_file.new_header(level=3, title=comp_name)
                                self._insert_comp_info(part_label, dic, context)
                                wrote_label_content = True
                        if not wrote_label_content:
                            level = 3 if context.purpose == ContextPurpose.COMPONENT else 4
                            self._insert_status(ImplementationStatus(state=const.STATUS_PLANNED), level)
                        self._md_file.new_paragraph()
                        did_write_part = True
        # if we loaded nothing for this control yet then it must need a fresh prompt for the control statement
        if not comp_dict and not did_write_part:
            self._md_file.new_line(f'{const.SSP_ADD_IMPLEMENTATION_FOR_CONTROL_TEXT}: {control.id} -->')
            if context.purpose in [ContextPurpose.COMPONENT, ContextPurpose.SSP]:
                status = ControlInterface.get_status_from_props(control)
                self._insert_status(status, 3)
        if not did_write_part:
            part_label = ''
            for comp_name, dic in comp_dict.items():
                if part_label in dic:
                    if comp_name != const.SSP_MAIN_COMP_NAME:
                        self._md_file.new_header(level=3, title=comp_name)
                    self._insert_comp_info(part_label, dic, context)
        self._md_file.new_hr()

    def _dump_subpart_infos(self, level: int, part: Dict[str, Any]) -> None:
        name = part['name']
        title = self._sections_dict.get(name, name) if self._sections_dict else name
        self._md_file.new_header(level=level, title=title)
        if 'prose' in part:
            self._md_file.new_paraline(part['prose'])
        for subpart in as_list(part.get('parts', None)):
            self._dump_subpart_infos(level + 1, subpart)

    def _dump_subparts(self, level: int, part: Part) -> None:
        name = part.name
        title = self._sections_dict.get(name, name) if self._sections_dict else name
        self._md_file.new_header(level=level, title=title)
        if part.prose:
            self._md_file.new_paraline(part.prose)
        for subpart in as_list(part.parts):
            self._dump_subparts(level + 1, subpart)

    def _dump_section(self, level: int, part: Part, added_sections: List[str], prefix: str) -> None:
        title = self._sections_dict.get(part.name, part.name) if self._sections_dict else part.name
        title = f'{prefix} {title}' if prefix else title
        self._md_file.new_header(level=level, title=title)
        if part.prose:
            self._md_file.new_paraline(part.prose)
        for subpart in as_list(part.parts):
            self._dump_subparts(level + 1, subpart)
        added_sections.append(part.name)

    def _dump_section_info(self, level: int, part: Dict[str, Any], added_sections: List[str], prefix: str) -> None:
        part_prose = part.get('prose', None)
        part_subparts = part.get('parts', None)
        name = part['name']
        title = self._sections_dict.get(name, name) if self._sections_dict else name
        title = f'{prefix} {title}' if prefix else title
        self._md_file.new_header(level=level, title=title)
        if part_prose:
            self._md_file.new_paraline(part_prose)
        for subpart in as_list(part_subparts):
            self._dump_subpart_infos(level + 1, subpart)
        added_sections.append(name)

    def _add_additional_content(
        self,
        control: cat.Control,
        profile: prof.Profile,
        header: Dict[str, Any],
        part_id_map: Dict[str, str],
        found_alters: List[prof.Alter]
    ) -> List[str]:
        # get part and subpart info from adds of the profile
        part_infos = ControlInterface.get_all_add_info(control.id, profile)
        has_content = len(part_infos) > 0

        self._md_file.new_header(level=1, title=const.EDITABLE_CONTENT)
        self._md_file.new_line('<!-- Make additions and edits below -->')
        self._md_file.new_line(
            '<!-- The above represents the contents of the control as received by the profile, prior to additions. -->'  # noqa E501
        )
        self._md_file.new_line(
            '<!-- If the profile makes additions to the control, they will appear below. -->'  # noqa E501
        )
        self._md_file.new_line(
            '<!-- The above markdown may not be edited but you may edit the content below, and/or introduce new additions to be made by the profile. -->'  # noqa E501
        )
        self._md_file.new_line(
            '<!-- If there is a yaml header at the top, parameter values may be edited. Use --set-parameters to incorporate the changes during assembly. -->'  # noqa E501
        )
        self._md_file.new_line(
            '<!-- The content here will then replace what is in the profile for this control, after running profile-assemble. -->'  # noqa E501
        )
        if has_content:
            self._md_file.new_line(
                '<!-- The added parts in the profile for this control are below.  You may edit them and/or add new ones. -->'  # noqa E501
            )
        else:
            self._md_file.new_line(
                '<!-- The current profile has no added parts for this control, but you may add new ones here. -->'
            )
        self._md_file.new_line(
            '<!-- Each addition must have a heading either of the form ## Control my_addition_name -->'
        )
        self._md_file.new_line('<!-- or ## Part a. (where the a. refers to one of the control statement labels.) -->')
        self._md_file.new_line('<!-- "## Control" parts are new parts added after the statement part. -->')
        self._md_file.new_line(
            '<!-- "## Part" parts are new parts added into the top-level statement part with that label. -->'
        )
        self._md_file.new_line('<!-- Subparts may be added with nested hash levels of the form ### My Subpart Name -->')
        self._md_file.new_line('<!-- underneath the parent ## Control or ## Part being added -->')
        self._md_file.new_line(
            '<!-- See https://oscal-compass.github.io/compliance-trestle/tutorials/ssp_profile_catalog_authoring/ssp_profile_catalog_authoring for guidance. -->'  # noqa E501
        )
        # next is to make mdformat happy
        self._md_file._add_line_raw('')

        added_sections: List[str] = []

        control_part_id_map = part_id_map.get(control.id, {})
        statement_id = ControlInterface.get_statement_id(control)

        # if the file already has markdown content, read its alters
        if self._md_file.exists():
            if const.TRESTLE_ADD_PROPS_TAG in header:
                header.pop(const.TRESTLE_ADD_PROPS_TAG)
            for alter in found_alters:
                for add in as_list(alter.adds):
                    # by_id could refer to statement (Control) or part (Part)
                    if add.by_id:
                        # is this a part that goes after the control statement
                        if add.by_id == statement_id:
                            for part in as_list(add.parts):
                                if part.prose or part.parts:
                                    self._dump_section(2, part, added_sections, 'Control')
                        else:
                            # or is it a sub-part of a statement part
                            part_label = control_part_id_map.get(add.by_id, add.by_id)
                            if add.parts:
                                self._md_file.new_header(level=2, title=f'Part {part_label}')
                                for part in as_list(add.parts):
                                    if part.prose or part.parts:
                                        name = part.name
                                        # need special handling for statement parts because their name is 'item'
                                        # get the short name as last piece of the part id after the '.'
                                        if name == const.ITEM:
                                            name = part.id.split('.')[-1]
                                        title = self._sections_dict.get(name, name) if self._sections_dict else name
                                        self._md_file.new_header(level=3, title=title)
                                        if part.prose:
                                            self._md_file.new_paraline(part.prose)
                                        for subpart in as_list(part.parts):
                                            self._dump_subparts(3, subpart)
                                        added_sections.append(name)
                    else:
                        # if not by_id just add at end of control's parts
                        for part in as_list(add.parts):
                            if part.prose or part.parts:
                                self._dump_section(2, part, added_sections, 'Control')
                    if add.props:
                        if const.TRESTLE_ADD_PROPS_TAG not in header:
                            header[const.TRESTLE_ADD_PROPS_TAG] = []
                        by_id = add.by_id
                        part_info = PartInfo(name='', prose='', props=add.props, smt_part=by_id)
                        _, prop_list = part_info.to_dicts(part_id_map.get(control.id, {}))
                        header[const.TRESTLE_ADD_PROPS_TAG].extend(prop_list)
        else:
            # md does not already exist so fill in directly
            in_part = ''
            for part_info in part_infos:
                part, prop_list = part_info.to_dicts(part_id_map.get(control.id, {}))
                # is this part of a statement part
                if part_info.smt_part and part_info.prose and part_info.smt_part in control_part_id_map:
                    # avoid outputting ## Part again if in same part
                    if not part_info.smt_part == in_part:
                        in_part = part_info.smt_part
                        part_label = control_part_id_map.get(part_info.smt_part, part_info.smt_part)
                        self._md_file.new_header(level=2, title=f'Part {part_label}')
                    self._dump_section_info(3, part, added_sections, '')
                # is it a control part
                elif part_info.prose or part_info.parts:
                    in_part = ''
                    self._dump_section_info(2, part, added_sections, 'Control')
                elif prop_list:
                    in_part = ''
                    if const.TRESTLE_ADD_PROPS_TAG not in header:
                        header[const.TRESTLE_ADD_PROPS_TAG] = []
                    header[const.TRESTLE_ADD_PROPS_TAG].extend(prop_list)
        return added_sections

    def _prompt_required_sections(self, required_sections: List[str], added_sections: List[str]) -> None:
        """Add prompts for any required sections that haven't already been written out."""
        missing_sections = set(required_sections).difference(added_sections)
        for section in sorted(missing_sections):
            section_title = self._sections_dict.get(section, section)
            self._md_file.new_header(2, f'Control {section_title}')
            self._md_file.new_line(f'{const.PROFILE_ADD_REQUIRED_SECTION_FOR_CONTROL_TEXT}: {section_title} -->')

    @staticmethod
    def _merge_headers(memory_header: Dict[str, Any], md_header: Dict[str, Any],
                       context: ControlContext) -> Dict[str, Any]:
        merged_header = copy.deepcopy(md_header)
        ControlInterface.merge_dicts_deep(merged_header, memory_header, False, 1)
        return merged_header

    def write_control_for_editing(
        self,
        context: ControlContext,
        control: cat.Control,
        dest_path: pathlib.Path,
        group_title: str,
        part_id_map: Dict[str, str],
        found_alters: List[prof.Alter]
    ) -> None:
        """
        Write out the control in markdown format into the specified directory.

        Args:
            context: The context of the control usage
            control: The control to write as markdown
            dest_path: Path to the directory where the control will be written
            group_title: Title of the group containing the control
            part_id_map: Mapping of part_id to label
            found_alters: List of alters read from the markdown file - if it exists

        Returns:
            None

        Notes:
            The filename is constructed from the control's id and created in the dest_path.
            If a yaml header is present in the file, new values in provided header will not replace those in the
            markdown header unless overwrite_header_values is true.  If it is true then overwrite any existing values,
            but in all cases new items from the provided header will be added to the markdown header.
            If the markdown file already exists, its current header and prose are read.
            Controls are checked if they are marked withdrawn, and if so they are not written out.
        """
        if ControlInterface.is_withdrawn(control):
            logger.debug(f'Not writing out control {control.id} since it is marked Withdrawn.')
            return

        control_file = dest_path / (control.id + const.MARKDOWN_FILE_EXT)
        # read the existing markdown header and content if it exists
        md_header, comp_dict = ControlReader.read_control_info_from_md(control_file, context)
        # replace the memory comp_dict with the md one if control exists
        if comp_dict:
            context.comp_dict = comp_dict

        header_comment_dict = {const.TRESTLE_ADD_PROPS_TAG: const.YAML_PROPS_COMMENT}
        if context.to_markdown:
            if context.purpose == ContextPurpose.PROFILE:
                header_comment_dict[const.SET_PARAMS_TAG] = const.YAML_PROFILE_VALUES_COMMENT
            elif context.purpose == ContextPurpose.SSP:
                header_comment_dict[const.SET_PARAMS_TAG] = const.YAML_SSP_VALUES_COMMENT
                header_comment_dict[const.COMP_DEF_RULES_PARAM_VALS_TAG] = const.YAML_RULE_PARAM_VALUES_SSP_COMMENT
            elif context.purpose == ContextPurpose.COMPONENT:
                header_comment_dict[const.COMP_DEF_RULES_PARAM_VALS_TAG
                                    ] = const.YAML_RULE_PARAM_VALUES_COMPONENT_COMMENT

        # begin adding info to the md file
        self._md_file = MDWriter(control_file, header_comment_dict)
        self._sections_dict = context.sections_dict

        context.merged_header = ControlWriter._merge_headers(context.merged_header, md_header, context)
        # if the control has an explicitly defined sort-id and there is none in the yaml_header, then insert it
        # in the yaml header and allow overwrite_header_values to control whether it overwrites an existing one
        # in the markdown header
        context.cli_yaml_header = as_dict(context.cli_yaml_header)
        if context.purpose != ContextPurpose.PROFILE:
            ControlInterface.merge_dicts_deep(
                context.merged_header, context.cli_yaml_header, context.overwrite_header_values
            )
        # the global contents are special and get overwritten on generate
        set_or_pop(
            context.merged_header,
            const.TRESTLE_GLOBAL_TAG,
            context.cli_yaml_header.get(const.TRESTLE_GLOBAL_TAG, None)
        )
        sort_id = ControlInterface.get_sort_id(control, True)
        if sort_id:
            deep_set(context.merged_header, [const.TRESTLE_GLOBAL_TAG, const.SORT_ID], sort_id)

        # merge any provided sections with sections in the header, with priority to the one from context (e.g. CLI)
        header_sections_dict = context.merged_header.get(const.SECTIONS_TAG, {})
        merged_sections_dict = merge_dicts(header_sections_dict, context.sections_dict)
        set_or_pop(context.merged_header, const.SECTIONS_TAG, merged_sections_dict)

        # now begin filling in content from the control in memory
        self._add_control_statement(control, group_title)

        self._add_control_objective(control)

        # add allowed sections to the markdown
        self._add_sections(control, context.allowed_sections)

        # prompt responses for imp reqs using special format if comp_def mode
        if context.prompt_responses:
            self._add_implementation_response_prompts(control, context.comp_dict, context)

        # for profile generate
        # add sections corresponding to added parts in the profile
        added_sections: List[str] = []
        if context.purpose == ContextPurpose.PROFILE:
            added_sections = self._add_additional_content(
                control, context.profile, context.merged_header, part_id_map, found_alters
            )

        self._add_yaml_header(context.merged_header)

        if context.required_sections:
            self._prompt_required_sections(context.required_sections, added_sections)

        self._md_file.write_out()
Methods¤
__init__(self) special ¤

Initialize the class.

Source code in trestle/core/control_writer.py
def __init__(self):
    """Initialize the class."""
    self._md_file: Optional[MDWriter] = None
write_control_for_editing(self, context, control, dest_path, group_title, part_id_map, found_alters) ¤

Write out the control in markdown format into the specified directory.

Parameters:

Name Type Description Default
context ControlContext

The context of the control usage

required
control Control

The control to write as markdown

required
dest_path Path

Path to the directory where the control will be written

required
group_title str

Title of the group containing the control

required
part_id_map Dict[str, str]

Mapping of part_id to label

required
found_alters List[trestle.oscal.profile.Alter]

List of alters read from the markdown file - if it exists

required

Returns:

Type Description
None

None

Notes

The filename is constructed from the control's id and created in the dest_path. If a yaml header is present in the file, new values in provided header will not replace those in the markdown header unless overwrite_header_values is true. If it is true then overwrite any existing values, but in all cases new items from the provided header will be added to the markdown header. If the markdown file already exists, its current header and prose are read. Controls are checked if they are marked withdrawn, and if so they are not written out.

Source code in trestle/core/control_writer.py
def write_control_for_editing(
    self,
    context: ControlContext,
    control: cat.Control,
    dest_path: pathlib.Path,
    group_title: str,
    part_id_map: Dict[str, str],
    found_alters: List[prof.Alter]
) -> None:
    """
    Write out the control in markdown format into the specified directory.

    Args:
        context: The context of the control usage
        control: The control to write as markdown
        dest_path: Path to the directory where the control will be written
        group_title: Title of the group containing the control
        part_id_map: Mapping of part_id to label
        found_alters: List of alters read from the markdown file - if it exists

    Returns:
        None

    Notes:
        The filename is constructed from the control's id and created in the dest_path.
        If a yaml header is present in the file, new values in provided header will not replace those in the
        markdown header unless overwrite_header_values is true.  If it is true then overwrite any existing values,
        but in all cases new items from the provided header will be added to the markdown header.
        If the markdown file already exists, its current header and prose are read.
        Controls are checked if they are marked withdrawn, and if so they are not written out.
    """
    if ControlInterface.is_withdrawn(control):
        logger.debug(f'Not writing out control {control.id} since it is marked Withdrawn.')
        return

    control_file = dest_path / (control.id + const.MARKDOWN_FILE_EXT)
    # read the existing markdown header and content if it exists
    md_header, comp_dict = ControlReader.read_control_info_from_md(control_file, context)
    # replace the memory comp_dict with the md one if control exists
    if comp_dict:
        context.comp_dict = comp_dict

    header_comment_dict = {const.TRESTLE_ADD_PROPS_TAG: const.YAML_PROPS_COMMENT}
    if context.to_markdown:
        if context.purpose == ContextPurpose.PROFILE:
            header_comment_dict[const.SET_PARAMS_TAG] = const.YAML_PROFILE_VALUES_COMMENT
        elif context.purpose == ContextPurpose.SSP:
            header_comment_dict[const.SET_PARAMS_TAG] = const.YAML_SSP_VALUES_COMMENT
            header_comment_dict[const.COMP_DEF_RULES_PARAM_VALS_TAG] = const.YAML_RULE_PARAM_VALUES_SSP_COMMENT
        elif context.purpose == ContextPurpose.COMPONENT:
            header_comment_dict[const.COMP_DEF_RULES_PARAM_VALS_TAG
                                ] = const.YAML_RULE_PARAM_VALUES_COMPONENT_COMMENT

    # begin adding info to the md file
    self._md_file = MDWriter(control_file, header_comment_dict)
    self._sections_dict = context.sections_dict

    context.merged_header = ControlWriter._merge_headers(context.merged_header, md_header, context)
    # if the control has an explicitly defined sort-id and there is none in the yaml_header, then insert it
    # in the yaml header and allow overwrite_header_values to control whether it overwrites an existing one
    # in the markdown header
    context.cli_yaml_header = as_dict(context.cli_yaml_header)
    if context.purpose != ContextPurpose.PROFILE:
        ControlInterface.merge_dicts_deep(
            context.merged_header, context.cli_yaml_header, context.overwrite_header_values
        )
    # the global contents are special and get overwritten on generate
    set_or_pop(
        context.merged_header,
        const.TRESTLE_GLOBAL_TAG,
        context.cli_yaml_header.get(const.TRESTLE_GLOBAL_TAG, None)
    )
    sort_id = ControlInterface.get_sort_id(control, True)
    if sort_id:
        deep_set(context.merged_header, [const.TRESTLE_GLOBAL_TAG, const.SORT_ID], sort_id)

    # merge any provided sections with sections in the header, with priority to the one from context (e.g. CLI)
    header_sections_dict = context.merged_header.get(const.SECTIONS_TAG, {})
    merged_sections_dict = merge_dicts(header_sections_dict, context.sections_dict)
    set_or_pop(context.merged_header, const.SECTIONS_TAG, merged_sections_dict)

    # now begin filling in content from the control in memory
    self._add_control_statement(control, group_title)

    self._add_control_objective(control)

    # add allowed sections to the markdown
    self._add_sections(control, context.allowed_sections)

    # prompt responses for imp reqs using special format if comp_def mode
    if context.prompt_responses:
        self._add_implementation_response_prompts(control, context.comp_dict, context)

    # for profile generate
    # add sections corresponding to added parts in the profile
    added_sections: List[str] = []
    if context.purpose == ContextPurpose.PROFILE:
        added_sections = self._add_additional_content(
            control, context.profile, context.merged_header, part_id_map, found_alters
        )

    self._add_yaml_header(context.merged_header)

    if context.required_sections:
        self._prompt_required_sections(context.required_sections, added_sections)

    self._md_file.write_out()

handler: python