def test_profile_resolver(tmp_trestle_dir: pathlib.Path) -> None: """Test the resolver.""" test_utils.setup_for_multi_profile(tmp_trestle_dir, False, True) prof_a_path = ModelUtils.path_for_top_level_model( tmp_trestle_dir, 'test_profile_a', prof.Profile, FileContentType.JSON ) cat = ProfileResolver.get_resolved_profile_catalog(tmp_trestle_dir, prof_a_path) interface = CatalogInterface(cat) # added part ac-1_expevid from prof a list1 = find_string_in_all_controls_prose(interface, 'Detailed evidence logs') # modify param ac-3.3_prm_2 in prof b list2 = find_string_in_all_controls_prose(interface, 'full and complete compliance') assert len(list1) == 1 assert len(list2) == 1 assert interface.get_count_of_controls_in_catalog(False) == 6 assert interface.get_count_of_controls_in_catalog(True) == 7 assert len(cat.controls) == 4 assert interface.get_dependent_control_ids('ac-3') == ['ac-3.3'] control = interface.get_control('a-1') assert control.parts[0].parts[0].id == 'a-1_deep' assert control.parts[0].parts[0].prose == 'Extra added part in subpart'
def test_get_control_and_group_info_from_catalog(tmp_trestle_dir: pathlib.Path) -> None: """Test get all groups from the catalog.""" test_utils.setup_for_multi_profile(tmp_trestle_dir, False, True) prof_a_path = ModelUtils.path_for_top_level_model( tmp_trestle_dir, 'test_profile_a', prof.Profile, FileContentType.JSON ) catalog = ProfileResolver.get_resolved_profile_catalog(tmp_trestle_dir, prof_a_path) cat_interface = CatalogInterface(catalog) all_groups_top = cat_interface.get_all_controls_from_catalog(recurse=False) assert len(list(all_groups_top)) == 6 all_groups_rec = cat_interface.get_all_controls_from_catalog(recurse=True) assert len(list(all_groups_rec)) == 7 all_group_ids = cat_interface.get_group_ids() assert len(all_group_ids) == 1 statement_label, part = cat_interface.get_statement_label_if_exists('ac-1', 'ac-1_smt.c.2') assert statement_label == '2.' assert part.id == 'ac-1_smt.c.2' cat_path = cat_interface._get_control_path('ac-2') assert cat_path[0] == 'ac' assert len(cat_path) == 1
def test_ssp_generate_resolved_catalog(tmp_trestle_dir: pathlib.Path) -> None: """Test the ssp generator to create a resolved profile catalog.""" _, _, _ = setup_for_ssp(False, True, tmp_trestle_dir, prof_name, ssp_name) profile_path = tmp_trestle_dir / f'profiles/{prof_name}/profile.json' new_catalog_dir = tmp_trestle_dir / f'catalogs/{prof_name}_resolved_catalog' new_catalog_dir.mkdir(parents=True, exist_ok=True) new_catalog_path = new_catalog_dir / 'catalog.json' profile_resolver = ProfileResolver() resolved_catalog = profile_resolver.get_resolved_profile_catalog( tmp_trestle_dir, profile_path) assert resolved_catalog # FIXME this should test with a more complex catalog assert len(resolved_catalog.groups) == 1 resolved_catalog.oscal_write(new_catalog_path)
def test_get_control_param_dict(tmp_trestle_dir: pathlib.Path) -> None: """Test getting the param dict of a control.""" test_utils.setup_for_multi_profile(tmp_trestle_dir, False, True) prof_a_path = ModelUtils.path_for_top_level_model(tmp_trestle_dir, 'test_profile_a', prof.Profile, FileContentType.JSON) catalog = ProfileResolver.get_resolved_profile_catalog( tmp_trestle_dir, prof_a_path) catalog_interface = CatalogInterface(catalog) control = catalog_interface.get_control('ac-1') param_dict = ControlIOReader.get_control_param_dict(control, False) # confirm profile value is used assert ControlIOReader.param_values_as_str( param_dict['ac-1_prm_1']) == 'all alert personnel' # confirm original param label is used since no value was assigned assert ControlIOReader.param_to_str(param_dict['ac-1_prm_7'], ParameterRep.VALUE_OR_LABEL_OR_CHOICES ) == 'organization-defined events' param = control.params[0] param.values = None param.select = common.ParameterSelection( how_many=common.HowMany.one_or_more, choice=['choice 1', 'choice 2']) param_dict = ControlIOReader.get_control_param_dict(control, False) assert ControlIOReader.param_to_str( param_dict['ac-1_prm_1'], ParameterRep.VALUE_OR_LABEL_OR_CHOICES) == 'choice 1, choice 2'
def test_profile_missing_position(tmp_trestle_dir: pathlib.Path) -> None: """Test when alter adds parts is missing position it defaults to after.""" cat_path = test_utils.JSON_TEST_DATA_PATH / test_utils.SIMPLIFIED_NIST_CATALOG_NAME repo = Repository(tmp_trestle_dir) repo.load_and_import_model(cat_path, 'nist_cat') prof_path = test_utils.JSON_TEST_DATA_PATH / 'profile_missing_position.json' repo.load_and_import_model(prof_path, 'profile_missing_position') catalog = ProfileResolver.get_resolved_profile_catalog(tmp_trestle_dir, prof_path) assert catalog
def test_ok_when_props_added(tmp_trestle_dir: pathlib.Path) -> None: """Test when by_id is not given and position is set to after or before it defaults to after.""" cat_path = test_utils.JSON_TEST_DATA_PATH / test_utils.SIMPLIFIED_NIST_CATALOG_NAME repo = Repository(tmp_trestle_dir) repo.load_and_import_model(cat_path, 'nist_cat') prof_path = test_utils.JSON_TEST_DATA_PATH / 'profile_with_alter_props.json' repo.load_and_import_model(prof_path, 'profile_with_alter_props') catalog = ProfileResolver.get_resolved_profile_catalog(tmp_trestle_dir, prof_path) assert catalog
def test_ok_when_reference_id_is_not_given_after_or_before(tmp_trestle_dir: pathlib.Path) -> None: """Test when by_id is not given and position is set to after or before it fails.""" cat_path = test_utils.JSON_TEST_DATA_PATH / test_utils.SIMPLIFIED_NIST_CATALOG_NAME repo = Repository(tmp_trestle_dir) repo.load_and_import_model(cat_path, 'nist_cat') prof_path = test_utils.JSON_TEST_DATA_PATH / 'profile_with_incorrect_alter.json' repo.load_and_import_model(prof_path, 'incorrect_profile') # this originally failed but now it is OK based on OSCAL saying to default to starting or ending if no by_id catalog = ProfileResolver.get_resolved_profile_catalog(tmp_trestle_dir, prof_path) assert catalog
def test_add_props_before_after_ok(tmp_trestle_dir: pathlib.Path) -> None: """ Test for property addition behavior with before or after. Properties added with before or after will default to starting or ending. """ test_utils.setup_for_multi_profile(tmp_trestle_dir, False, True) prof_g_path = ModelUtils.path_for_top_level_model( tmp_trestle_dir, 'test_profile_g', prof.Profile, FileContentType.JSON ) _ = ProfileResolver.get_resolved_profile_catalog(tmp_trestle_dir, prof_g_path)
def test_get_profile_param_dict(tmp_trestle_dir: pathlib.Path) -> None: """Test get profile param dict for control.""" test_utils.setup_for_multi_profile(tmp_trestle_dir, False, True) profile, profile_path = ModelUtils.load_top_level_model( tmp_trestle_dir, 'test_profile_a', prof.Profile, FileContentType.JSON) profile_resolver = ProfileResolver() catalog = profile_resolver.get_resolved_profile_catalog( tmp_trestle_dir, profile_path) catalog_interface = CatalogInterface(catalog) control = catalog_interface.get_control('ac-1') full_param_dict = CatalogInterface._get_full_profile_param_dict(profile) control_param_dict = CatalogInterface._get_profile_param_dict( control, full_param_dict, False) assert ControlIOReader.param_to_str( control_param_dict['ac-1_prm_1'], ParameterRep.VALUE_OR_LABEL_OR_CHOICES) == 'all alert personnel' assert ControlIOReader.param_to_str( control_param_dict['ac-1_prm_6'], ParameterRep.VALUE_OR_LABEL_OR_CHOICES) == 'monthly' # param 7 has no value so its label will be used assert ControlIOReader.param_to_str(control_param_dict['ac-1_prm_7'], ParameterRep.VALUE_OR_LABEL_OR_CHOICES ) == 'organization-defined events'
def generate_markdown(self, trestle_root: pathlib.Path, profile_path: pathlib.Path, markdown_path: pathlib.Path, yaml_header: dict, overwrite_header_values: bool, sections_dict: Optional[Dict[str, str]], required_sections: Optional[str]) -> int: """Generate markdown for the controls in the profile. Args: trestle_root: Root directory of the trestle workspace profile_path: Path of the profile json file markdown_path: Path to the directory into which the markdown will be written yaml_header: Dict to merge into the yaml header of the control markdown overwrite_header_values: Overwrite values in the markdown header but allow new items to be added sections_dict: Optional dict mapping section short names to long required_sections: Optional comma-sep list of sections that get prompted for prose if not in the profile Returns: 0 on success, 1 on error """ try: if sections_dict and 'statement' in sections_dict: logger.warning('statement is not allowed as a section name.') return CmdReturnCodes.COMMAND_ERROR.value _, _, profile = ModelUtils.load_distributed( profile_path, trestle_root) catalog = ProfileResolver().get_resolved_profile_catalog( trestle_root, profile_path, True, True, None, ParameterRep.LEAVE_MOUSTACHE) catalog_interface = CatalogInterface(catalog) catalog_interface.write_catalog_as_markdown( md_path=markdown_path, yaml_header=yaml_header, sections_dict=sections_dict, prompt_responses=False, additional_content=True, profile=profile, overwrite_header_values=overwrite_header_values, set_parameters=True, required_sections=required_sections, allowed_sections=None) except TrestleNotFoundError as e: raise TrestleError(f'Profile {profile_path} not found, error {e}') except TrestleError as e: raise TrestleError( f'Error generating the catalog as markdown: {e}') return CmdReturnCodes.SUCCESS.value
def test_parameter_resolution(tmp_trestle_dir: pathlib.Path) -> None: """Test whether expected order of operations is preserved for parameter substution.""" test_utils.setup_for_multi_profile(tmp_trestle_dir, False, True) prof_e_path = ModelUtils.path_for_top_level_model( tmp_trestle_dir, 'test_profile_e', prof.Profile, FileContentType.JSON ) profile_e_parameter_string = '## Override value ##' profile_a_value = 'all alert personnel' # based on 800-53 rev 5 cat = ProfileResolver.get_resolved_profile_catalog(tmp_trestle_dir, prof_e_path) interface = CatalogInterface(cat) control = interface.get_control('ac-1') locations = interface.find_string_in_control(control, profile_e_parameter_string) locations_a = interface.find_string_in_control(control, profile_a_value) assert len(locations) == 1 assert len(locations_a) == 0 assert len(control.params[1].constraints) == 1
def test_add_props(tmp_trestle_dir: pathlib.Path) -> None: """Test all types of property additions.""" test_utils.setup_for_multi_profile(tmp_trestle_dir, False, True) prof_f_path = ModelUtils.path_for_top_level_model( tmp_trestle_dir, 'test_profile_f', prof.Profile, FileContentType.JSON ) cat = ProfileResolver.get_resolved_profile_catalog(tmp_trestle_dir, prof_f_path) interface = CatalogInterface(cat) ac_3 = interface.get_control('ac-3') assert len(ac_3.props) == 6 assert ac_3.props[-1].value == 'four' for part in ac_3.parts: if part.id == 'ac-3_stmt': assert len(part.props) == 4 ac_5 = interface.get_control('ac-5') for part in ac_5.parts: if part.id == 'ac-5_stmt': for sub_part in part.parts: if sub_part.id == 'ac-5_smt.a': assert len(sub_part.props) == 4
def test_all_positions_for_alter_can_be_resolved(tmp_trestle_dir: pathlib.Path) -> None: """Test that all alter adds positions can be resolved.""" cat_path = test_utils.JSON_TEST_DATA_PATH / test_utils.SIMPLIFIED_NIST_CATALOG_NAME repo = Repository(tmp_trestle_dir) repo.load_and_import_model(cat_path, 'nist_cat') prof_d_path = test_utils.JSON_TEST_DATA_PATH / 'test_profile_d.json' repo.load_and_import_model(prof_d_path, 'test_profile_d') cat = ProfileResolver.get_resolved_profile_catalog(tmp_trestle_dir, prof_d_path) interface = CatalogInterface(cat) control_a1 = interface.get_control('ac-1') control_a2 = interface.get_control('ac-2') assert control_a1.parts[0].id == 'ac-1_first_lev1' assert control_a1.parts[1].parts[3].id == 'ac-1_last_lev2' assert control_a1.parts[2].id == 'ac-1_after1_ac-1_smt_lev1' assert control_a1.parts[3].id == 'ac-1_after2_ac-1_smt_lev1' assert control_a1.parts[1].parts[0].parts[1].id == 'ac-1_smt_before1_a.2_lev3' assert control_a1.parts[1].parts[0].parts[2].id == 'ac-1_smt_before2_a.2_lev3' assert control_a1.parts[1].parts[0].parts[3].parts[0].id == 'ac-1_smt_inside1_at_the_end_a.2_lev4' assert control_a1.parts[1].parts[0].parts[3].parts[1].id == 'ac-1_smt_inside2_at_the_end_a.2_lev4' assert control_a2.parts[0].id == 'ac-2_implgdn_lev1'
def test_profile_ohv(required_sections: Optional[str], success: bool, ohv: bool, tmp_trestle_dir: pathlib.Path) -> None: """Test profile generate assemble with overwrite-header-values.""" ac1_path, assembled_prof_dir, profile_path, markdown_path = setup_profile_generate( tmp_trestle_dir) yaml_header_path = test_utils.YAML_TEST_DATA_PATH / 'good_simple.yaml' new_version = '1.2.3' # convert resolved profile catalog to markdown then assemble it after adding an item to a control # if set_parameters is true, the yaml header will contain all the parameters profile_generate = ProfileGenerate() yaml = YAML() yaml_header = yaml.load(yaml_header_path.open('r')) profile_generate.generate_markdown(tmp_trestle_dir, profile_path, markdown_path, yaml_header, ohv, None, None) edit_files(ac1_path, True, multi_guidance_dict) markdown_path = tmp_trestle_dir / md_name # change guidance in the other two controls but don't change header ac2_path = markdown_path / 'ac/ac-2.md' ac21_path = markdown_path / 'ac/ac-2.1.md' edit_files(ac2_path, False, multi_guidance_dict) edit_files(ac21_path, False, multi_guidance_dict) if success: assert ProfileAssemble.assemble_profile(tmp_trestle_dir, prof_name, md_name, assembled_prof_name, True, False, new_version, required_sections, None) == 0 # check the assembled profile is as expected profile: prof.Profile profile, _ = ModelUtils.load_top_level_model(tmp_trestle_dir, assembled_prof_name, prof.Profile, FileContentType.JSON) set_params = profile.modify.set_parameters assert len(set_params) == 14 assert set_params[0].values[0].__root__ == 'all personnel' assert set_params[1].param_id == 'ac-1_prm_2' assert set_params[1].values[0].__root__ == 'Organization-level' assert set_params[1].values[1].__root__ == 'System-level' assert set_params[2].values[0].__root__ == 'new value' assert profile.metadata.version.__root__ == new_version if ohv: assert set_params[3].values[0].__root__ == 'no meetings' assert set_params[3].label == 'meetings cancelled' else: assert set_params[3].values[0].__root__ == 'all meetings' assert set_params[3].label == 'organization-defined events' catalog = ProfileResolver.get_resolved_profile_catalog( tmp_trestle_dir, assembled_prof_dir / 'profile.json') catalog_interface = CatalogInterface(catalog) # confirm presence of all expected strings in the control named parts for name, exp_str in multi_guidance_dict['name_exp']: prose = catalog_interface.get_control_part_prose('ac-1', name) assert prose.find(exp_str) >= 0 else: with pytest.raises(TrestleError): ProfileAssemble.assemble_profile(tmp_trestle_dir, prof_name, md_name, assembled_prof_name, True, False, new_version, required_sections, None)
def test_profile_generate_assemble(add_header: bool, guid_dict: Dict, use_cli: bool, dir_exists: bool, set_parameters: bool, tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch) -> None: """Test the profile markdown generator.""" ac1_path, assembled_prof_dir, profile_path, markdown_path = setup_profile_generate( tmp_trestle_dir) yaml_header_path = test_utils.YAML_TEST_DATA_PATH / 'good_simple.yaml' # convert resolved profile catalog to markdown then assemble it after adding an item to a control if use_cli: test_args = f'trestle author profile-generate -n {prof_name} -o {md_name} -rs NeededExtra'.split( ) if add_header: test_args.extend(['-y', str(yaml_header_path)]) test_args.extend(['-s', all_sections_str]) monkeypatch.setattr(sys, 'argv', test_args) assert Trestle().run() == 0 edit_files(ac1_path, set_parameters, guid_dict) test_args = f'trestle author profile-assemble -n {prof_name} -m {md_name} -o {assembled_prof_name}'.split( ) if set_parameters: test_args.append('-sp') if dir_exists: assembled_prof_dir.mkdir() monkeypatch.setattr(sys, 'argv', test_args) assert Trestle().run() == 0 else: profile_generate = ProfileGenerate() yaml_header = {} if add_header: yaml = YAML() yaml_header = yaml.load(yaml_header_path.open('r')) sections_dict = sections_to_dict(all_sections_str) profile_generate.generate_markdown(tmp_trestle_dir, profile_path, markdown_path, yaml_header, False, sections_dict, 'NeededExtra') edit_files(ac1_path, set_parameters, guid_dict) if dir_exists: assembled_prof_dir.mkdir() assert ProfileAssemble.assemble_profile(tmp_trestle_dir, prof_name, md_name, assembled_prof_name, set_parameters, False, None, None, None) == 0 # check the assembled profile is as expected profile: prof.Profile profile, _ = ModelUtils.load_top_level_model(tmp_trestle_dir, assembled_prof_name, prof.Profile, FileContentType.JSON) assert ModelUtils.model_age(profile) < test_utils.NEW_MODEL_AGE_SECONDS # get the set_params in the assembled profile set_params = profile.modify.set_parameters if set_parameters: assert set_params[0].param_id == 'ac-1_prm_1' assert set_params[0].values[0].__root__ == 'all personnel' assert set_params[1].param_id == 'ac-1_prm_2' assert set_params[1].values[0].__root__ == 'Organization-level' assert set_params[1].values[1].__root__ == 'System-level' assert set_params[2].param_id == 'ac-1_prm_3' assert set_params[2].values[0].__root__ == 'new value' else: assert len(set_params) == 15 # now create the resolved profile catalog from the assembled json profile and confirm the addition is there catalog = ProfileResolver.get_resolved_profile_catalog( tmp_trestle_dir, assembled_prof_dir / 'profile.json') catalog_interface = CatalogInterface(catalog) # confirm presence of all expected strings in the control named parts for name, exp_str in guid_dict['name_exp']: prose = catalog_interface.get_control_part_prose('ac-1', name) assert prose.find(exp_str) >= 0
def _run(self, args: argparse.Namespace) -> int: try: log.set_log_level_from_args(args) trestle_root = args.trestle_root if not file_utils.is_directory_name_allowed(args.output): raise TrestleError( f'{args.output} is not an allowed directory name') profile_path = trestle_root / f'profiles/{args.profile}/profile.json' yaml_header: dict = {} if args.yaml_header: try: logging.debug( f'Loading yaml header file {args.yaml_header}') yaml = YAML() yaml_header = yaml.load( pathlib.Path(args.yaml_header).open('r')) except YAMLError as e: raise TrestleError( f'YAML error loading yaml header for ssp generation: {e}' ) markdown_path = trestle_root / args.output profile_resolver = ProfileResolver() resolved_catalog = profile_resolver.get_resolved_profile_catalog( trestle_root, profile_path) catalog_interface = CatalogInterface(resolved_catalog) sections_dict: Dict[str, str] = {} if args.sections: sections_dict = sections_to_dict(args.sections) if 'statement' in sections_dict: raise TrestleError( 'Statement is not allowed as a section name.') # add any existing sections from the controls but only have short names control_section_short_names = catalog_interface.get_sections() for short_name in control_section_short_names: if short_name not in sections_dict: sections_dict[short_name] = short_name logger.debug(f'ssp sections dict: {sections_dict}') catalog_interface.write_catalog_as_markdown( md_path=markdown_path, yaml_header=yaml_header, sections_dict=sections_dict, prompt_responses=True, additional_content=False, profile=None, overwrite_header_values=args.overwrite_header_values, set_parameters=False, required_sections=None, allowed_sections=args.allowed_sections) return CmdReturnCodes.SUCCESS.value except Exception as e: # pragma: no cover return handle_generic_command_exception( e, logger, 'Error while writing markdown from catalog')
def filter_ssp(self, trestle_root: pathlib.Path, ssp_name: str, profile_name: str, out_name: str, regenerate: bool, version: Optional[str]) -> int: """ Filter the ssp based on controls included by the profile and output new ssp. Args: trestle_root: root directory of the trestle project ssp_name: name of the ssp model profile_name: name of the profile model used for filtering out_name: name of the output ssp model with filtered controls regenerate: whether to regenerate the uuid's in the ssp version: new version for the model Returns: 0 on success, 1 otherwise """ ssp: ossp.SystemSecurityPlan ssp, _ = ModelUtils.load_top_level_model(trestle_root, ssp_name, ossp.SystemSecurityPlan, FileContentType.JSON) profile_path = ModelUtils.path_for_top_level_model( trestle_root, profile_name, prof.Profile, FileContentType.JSON) prof_resolver = ProfileResolver() catalog = prof_resolver.get_resolved_profile_catalog( trestle_root, profile_path) catalog_interface = CatalogInterface(catalog) # The input ssp should reference a superset of the controls referenced by the profile # Need to cull references in the ssp to controls not in the profile # Also make sure the output ssp contains imp reqs for all controls in the profile control_imp = ssp.control_implementation ssp_control_ids: Set[str] = set() new_set_params: List[ossp.SetParameter] = [] for set_param in as_list(control_imp.set_parameters): control = catalog_interface.get_control_by_param_id( set_param.param_id) if control is not None: new_set_params.append(set_param) ssp_control_ids.add(control.id) control_imp.set_parameters = new_set_params if new_set_params else None new_imp_requirements: List[ossp.ImplementedRequirement] = [] for imp_requirement in as_list(control_imp.implemented_requirements): control = catalog_interface.get_control(imp_requirement.control_id) if control is not None: new_imp_requirements.append(imp_requirement) ssp_control_ids.add(control.id) control_imp.implemented_requirements = new_imp_requirements # make sure all controls in the profile have implemented reqs in the final ssp if not ssp_control_ids.issuperset(catalog_interface.get_control_ids()): raise TrestleError( 'Unable to filter the ssp because the profile references controls not in it.' ) ssp.control_implementation = control_imp if regenerate: ssp, _, _ = ModelUtils.regenerate_uuids(ssp) if version: ssp.metadata.version = com.Version(__root__=version) ModelUtils.update_last_modified(ssp) ModelUtils.save_top_level_model(ssp, trestle_root, out_name, FileContentType.JSON) return CmdReturnCodes.SUCCESS.value
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