Skip to content

control_writer

trestle.core.control_writer ¤

Handle writing of controls to markdown.

Attributes¤

logger = logging.getLogger(__name__) module-attribute ¤

Classes¤

ControlWriter ¤

Class to write controls as markdown.

Source code in trestle/core/control_writer.py
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
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 self._include_component(info):
                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 _process_main_component(self, part_id: str, part_label: str, comp_dict: CompDict) -> None:
        """Handle prompts for This System component."""
        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}: {part_id} -->'
        status = ImplementationStatus(state=const.STATUS_PLANNED)
        sys_comp_dict: Dict[str, ComponentImpInfo] = comp_dict.get(const.SSP_MAIN_COMP_NAME, {})
        if sys_comp_dict and part_label in sys_comp_dict:
            comp_info = sys_comp_dict[part_label]
            if comp_info.prose:
                prose = comp_info.prose
            status = comp_info.status
        self._md_file.new_paraline(prose)
        self._insert_status(status, 4)

    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:
            # Passing in the control id and the empty key ('') for the label
            self._process_main_component(control_id, '', comp_dict)
            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

                        # Determine if this part should be written out
                        if self._skip_part(context, part_label, comp_dict):
                            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
                        # To reduce verbosity by default, the main component
                        # is conditionally written for each part dependant on the control context.
                        if context.purpose == ContextPurpose.SSP and context.include_all_parts:
                            self._process_main_component(prt.id, part_label, comp_dict)
                            wrote_label_content = True
                        sorted_comp_names = sorted(comp_dict.keys())
                        for comp_name in sorted_comp_names:
                            # This System already handled
                            if comp_name == const.SSP_MAIN_COMP_NAME:
                                continue
                            dic = comp_dict[comp_name]
                            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 _include_component(self, comp_info: ComponentImpInfo) -> bool:
        """
        Check if a component has the required Markdown fields.

        Notes: This is a simple function to centralize logic to check
        when a component meets the requirement to get written to Markdown.
        """
        if comp_info.rules or comp_info.prose:
            return True
        return False

    def _skip_part(self, context: ControlContext, part_label: str, comp_dict: CompDict) -> bool:
        """
        Check if a part should be skipped based on rules and context.

        Notes: The default logic is to conditionally add control parts based
        on whether the component has rules or existing prose associated with that part. This can be
        changed using the control context for SSP markdown.
        """
        if context.purpose == ContextPurpose.SSP and context.include_all_parts:
            return False
        else:
            skip_item = True
            for _, dic in comp_dict.items():
                if part_label in dic:
                    if self._include_component(dic[part_label]):
                        skip_item = False
                        break
        return skip_item

    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
                # The add props information to SSP only. It is added to the profile later.
                if const.TRESTLE_ADD_PROPS_TAG not in md_header:
                    md_header[const.TRESTLE_ADD_PROPS_TAG] = []
            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()
Functions¤
__init__() ¤

Initialize the class.

Source code in trestle/core/control_writer.py
36
37
38
def __init__(self):
    """Initialize the class."""
    self._md_file: Optional[MDWriter] = None
write_control_for_editing(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[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
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
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
            # The add props information to SSP only. It is added to the profile later.
            if const.TRESTLE_ADD_PROPS_TAG not in md_header:
                md_header[const.TRESTLE_ADD_PROPS_TAG] = []
        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()

Functions¤

handler: python