diff --git a/CHANGELOG.md b/CHANGELOG.md index f67363fdb..0be2b899f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,22 @@ <!--next-version-placeholder--> +## v2.3.0 (2023-09-06) +### Feature +* Extend multiple templates validation to trestle author folders ([#1430](https://github.com/IBM/compliance-trestle/issues/1430)) ([`c7bef58`](https://github.com/IBM/compliance-trestle/commit/c7bef589a6e671b96170e93feb88c6436a094da6)) +* Adds agile authoring functionality to public API in repository.py ([#1432](https://github.com/IBM/compliance-trestle/issues/1432)) ([`08b2559`](https://github.com/IBM/compliance-trestle/commit/08b255902efb911c99422d49920c5ddaea98ef32)) +* Support validation component_type for task csv-to-oscal-cd ([#1431](https://github.com/IBM/compliance-trestle/issues/1431)) ([`80aaa72`](https://github.com/IBM/compliance-trestle/commit/80aaa72fe96217d1c7dd93e4c1d5bd9c34cb012b)) + +### Fix +* Correcting typo ([`1810007`](https://github.com/IBM/compliance-trestle/commit/181000731ada7af1348219581994bd58f2285329)) +* Correcting python semantice release version ([`a8cb9b9`](https://github.com/IBM/compliance-trestle/commit/a8cb9b9f1f11485ac70fa2f35a3e52b917b7a783)) +* Moving watch config a level up ([#1447](https://github.com/IBM/compliance-trestle/issues/1447)) ([`ea5607f`](https://github.com/IBM/compliance-trestle/commit/ea5607f9f404f38da1abf1c40f907196ea79c567)) +* Xccdf parameter type ([#1440](https://github.com/IBM/compliance-trestle/issues/1440)) ([`431670c`](https://github.com/IBM/compliance-trestle/commit/431670cd468693ca4581ec43d8de5d32413ec113)) +* Headings levels validation is not working properly ([#1436](https://github.com/IBM/compliance-trestle/issues/1436)) ([`22b65a9`](https://github.com/IBM/compliance-trestle/commit/22b65a9b84af36d8c12c32c6e5c0dae88208ea49)) +* Default set-parameter values as list ([#1438](https://github.com/IBM/compliance-trestle/issues/1438)) ([`419025d`](https://github.com/IBM/compliance-trestle/commit/419025dfad47cf9f61b5e20a35a9683a84ed26e8)) +* Expected nist profile missing ([#1435](https://github.com/IBM/compliance-trestle/issues/1435)) ([`c96f9ce`](https://github.com/IBM/compliance-trestle/commit/c96f9ce82e453c83a07d9d4c1061833f38c7f104)) +* Provide description and meaning to parameters in markdown ([#1423](https://github.com/IBM/compliance-trestle/issues/1423)) ([`266f67b`](https://github.com/IBM/compliance-trestle/commit/266f67bd220e15922caacb4de0e702f4d0927ceb)) + ## v2.2.1 (2023-07-05) ### Fix diff --git a/setup.cfg b/setup.cfg index 624f3cd6e..3c84d3b32 100644 --- a/setup.cfg +++ b/setup.cfg @@ -66,7 +66,7 @@ dev = setuptools wheel yapf - python-semantic-release + python-semantic-release==7.33.2 pep8-naming pytest-random-order python-dateutil @@ -76,7 +76,7 @@ dev = types-requests types-setuptools # # Docs website - mkdocs + mkdocs==1.5.0 mkdocstrings[python-legacy]==0.19.0 mkdocs-material markdown-include diff --git a/tests/trestle/core/commands/author/profile_test.py b/tests/trestle/core/commands/author/profile_test.py index da75698be..5c2b9047e 100644 --- a/tests/trestle/core/commands/author/profile_test.py +++ b/tests/trestle/core/commands/author/profile_test.py @@ -933,6 +933,7 @@ def test_profile_force_overwrite(tmp_trestle_dir: pathlib.Path, monkeypatch: Mon header, tree = md_api.processor.process_markdown(md_path) assert header + header[const.SET_PARAMS_TAG]['ac-5_prm_1'][const.VALUES] = [] old_value = header[const.SET_PARAMS_TAG]['ac-5_prm_1'][const.VALUES] header[const.SET_PARAMS_TAG]['ac-5_prm_1'][const.VALUES] = 'New value' @@ -948,6 +949,7 @@ def test_profile_force_overwrite(tmp_trestle_dir: pathlib.Path, monkeypatch: Mon test_utils.execute_command_and_assert(prof_generate, 0, monkeypatch) header, _ = md_api.processor.process_markdown(md_path) + header[const.SET_PARAMS_TAG]['ac-5_prm_1'][const.VALUES] = [] assert header[const.SET_PARAMS_TAG]['ac-5_prm_1'][const.VALUES] == old_value # test that file is unchanged @@ -1129,20 +1131,37 @@ def test_profile_generate_assemble_parameter_aggregation( nist_cat, _ = ModelUtils.load_model_for_class(tmp_trestle_dir, 'nist_cat', cat.Catalog, FileContentType.JSON) appended_prop = {'name': 'aggregates', 'value': 'at-02_odp.01'} + second_appended_prop = {'name': 'aggregates', 'value': 'at-02_odp.02'} + third_appended_prop = {'name': 'alt-identifier', 'value': 'this_is_an_identifier'} ac_1 = nist_cat.groups[0].controls[0] - ac_1.params[2].props = [] - ac_1.params[2].props.append(appended_prop) + ac_1.params[6].props = [] + ac_1.params[6].props.append(appended_prop) + ac_1.params[6].props.append(second_appended_prop) + ac_1.params[6].props.append(third_appended_prop) appended_extra_param = { 'id': 'at-02_odp.01', 'props': [{ 'name': 'label', 'value': 'AT-02_ODP[01]', 'class': 'sp800-53a' }], 'label': 'frequency', + 'values': ['value-1', 'value-2'], + 'guidelines': [{ + 'prose': 'blah' + }] + } + second_appended_extra_param = { + 'id': 'at-02_odp.02', + 'props': [{ + 'name': 'label', 'value': 'AT-02_ODP[02]', 'class': 'sp800-53a' + }], + 'label': 'frequency', + 'values': ['value-3', 'value-4'], 'guidelines': [{ 'prose': 'blah' }] } ac_1.params.append(appended_extra_param) + ac_1.params.append(second_appended_extra_param) ModelUtils.save_top_level_model(nist_cat, tmp_trestle_dir, 'nist_cat', FileContentType.JSON) diff --git a/tests/trestle/core/commands/author/ssp_test.py b/tests/trestle/core/commands/author/ssp_test.py index 34288b135..a41dac384 100644 --- a/tests/trestle/core/commands/author/ssp_test.py +++ b/tests/trestle/core/commands/author/ssp_test.py @@ -215,7 +215,6 @@ def test_ssp_generate_header_edit(tmp_trestle_dir: pathlib.Path) -> None: # confirm new items were added from yaml but not when the same key was alread present (values not updated) header, tree = md_api.processor.process_markdown(ac_1) assert 'control-origination' in header - assert header['x-trestle-set-params']['ac-1_prm_5']['values'] is None assert header['x-trestle-set-params']['ac-1_prm_5']['label'] == 'meetings cancelled from cli yaml' # generate again with header and DO overwrite header values diff --git a/tests/trestle/tasks/csv_to_oscal_cd_test.py b/tests/trestle/tasks/csv_to_oscal_cd_test.py index 82913ef83..639db26e8 100644 --- a/tests/trestle/tasks/csv_to_oscal_cd_test.py +++ b/tests/trestle/tasks/csv_to_oscal_cd_test.py @@ -661,6 +661,32 @@ def test_execute_add_rule(tmp_path: pathlib.Path) -> None: assert len(component.control_implementations[1].set_parameters) == 1 +def test_execute_param_duplicate_value(tmp_path: pathlib.Path) -> None: + """Test execute param duplicate default value.""" + _, section = _get_config_section_init(tmp_path, 'test-csv-to-oscal-cd-bp.config') + # duplicate default param default value + rows = _get_rows('tests/data/csv/bp.sample.v2.csv') + row = rows[3] + assert row[13] == 'allowed_admins_per_account' + assert row[15] == '10' + row = rows[2] + row[13] = 'allowed_admins_per_account' + row[15] = '10' + rows[2] = row + with mock.patch('trestle.tasks.csv_to_oscal_cd.csv.reader') as mock_csv_reader: + mock_csv_reader.return_value = rows + tgt = csv_to_oscal_cd.CsvToOscalComponentDefinition(section) + retval = tgt.execute() + assert retval == TaskOutcome.SUCCESS + row[15] = '11' + rows[2] = row + with mock.patch('trestle.tasks.csv_to_oscal_cd.csv.reader') as mock_csv_reader: + mock_csv_reader.return_value = rows + tgt = csv_to_oscal_cd.CsvToOscalComponentDefinition(section) + retval = tgt.execute() + assert retval == TaskOutcome.FAILURE + + def test_execute_missing_param_default_value(tmp_path: pathlib.Path) -> None: """Test execute missing param default_value.""" _, section = _get_config_section_init(tmp_path, 'test-csv-to-oscal-cd-bp.config') diff --git a/trestle/__init__.py b/trestle/__init__.py index 22d3908eb..967548cb7 100644 --- a/trestle/__init__.py +++ b/trestle/__init__.py @@ -23,4 +23,4 @@ opinionated approach to OSCAL adoption. """ -__version__ = '2.2.1' +__version__ = '2.3.0' diff --git a/trestle/common/const.py b/trestle/common/const.py index 02c37b00e..a7e7ee3bc 100644 --- a/trestle/common/const.py +++ b/trestle/common/const.py @@ -559,6 +559,10 @@ CONTROL_IMPLEMENTATION = 'control-implementation' +AGGREGATES = 'aggregates' + +ALT_IDENTIFIER = 'alt-identifier' + IMPLEMENTED_REQUIREMENT = 'implemented-requirement' # Following 5 are allowed control origination values for diff --git a/trestle/common/model_utils.py b/trestle/common/model_utils.py index 09a07bc42..13e93ff5d 100644 --- a/trestle/common/model_utils.py +++ b/trestle/common/model_utils.py @@ -628,6 +628,12 @@ def dict_to_parameter(param_dict: Dict[str, Any]) -> common.Parameter: if const.DISPLAY_NAME in param_dict: display_name = param_dict.pop(const.DISPLAY_NAME) props.append(common.Property(name=const.DISPLAY_NAME, value=display_name, ns=const.TRESTLE_GENERIC_NS)) + if const.AGGREGATES in param_dict: + # removing aggregates as this is prop just informative in markdown + param_dict.pop(const.AGGREGATES) + if const.ALT_IDENTIFIER in param_dict: + # removing alt-identifier as this is prop just informative in markdown + param_dict.pop(const.ALT_IDENTIFIER) if 'ns' in param_dict: param_dict.pop('ns') diff --git a/trestle/core/catalog/catalog_reader.py b/trestle/core/catalog/catalog_reader.py index 49f1415db..423376175 100644 --- a/trestle/core/catalog/catalog_reader.py +++ b/trestle/core/catalog/catalog_reader.py @@ -71,9 +71,16 @@ def read_additional_content( ) alters_map[sort_id] = control_alters for param_id, param_dict in control_param_dict.items(): + param_dict[const.VALUES] = param_dict[const.VALUES] if const.VALUES in param_dict else [] # if profile_values are present, overwrite values with them if const.PROFILE_VALUES in param_dict: - param_dict[const.VALUES] = param_dict.pop(const.PROFILE_VALUES) + if param_dict[const.PROFILE_VALUES] != [] and param_dict[const.PROFILE_VALUES] is not None: + if not write_mode and '<REPLACE_ME>' in param_dict[const.PROFILE_VALUES]: + param_dict[const.PROFILE_VALUES].remove('<REPLACE_ME>') + if param_dict[const.PROFILE_VALUES] != [] and param_dict[const.PROFILE_VALUES] is not None: + param_dict[const.VALUES] = param_dict[const.PROFILE_VALUES] + if not write_mode: + param_dict.pop(const.PROFILE_VALUES) final_param_dict[param_id] = param_dict param_sort_map[param_id] = sort_id new_alters: List[prof.Alter] = [] diff --git a/trestle/core/catalog/catalog_writer.py b/trestle/core/catalog/catalog_writer.py index 07c274041..0a632d2ce 100644 --- a/trestle/core/catalog/catalog_writer.py +++ b/trestle/core/catalog/catalog_writer.py @@ -19,7 +19,7 @@ import trestle.common.const as const import trestle.oscal.catalog as cat -from trestle.common.list_utils import as_list, deep_get, delete_list_from_list, none_if_empty +from trestle.common.list_utils import as_list, deep_get, none_if_empty from trestle.common.model_utils import ModelUtils from trestle.core.catalog.catalog_interface import CatalogInterface from trestle.core.catalog.catalog_merger import CatalogMerger @@ -63,18 +63,6 @@ def write_catalog_as_profile_markdown( # get all params and vals for this control from the resolved profile catalog with block adds in effect control_param_dict = ControlInterface.get_control_param_dict(control, False) - to_delete = [] - # removes aggregate parameters to be non-editable in markdowns - props_by_param_ids = {} - for param_id, values_dict in control_param_dict.items(): - props_by_param_ids[param_id] = [ - prop for prop in as_list(values_dict.props) if prop.name == 'aggregates' - ] - to_delete = list({k: v for k, v in props_by_param_ids.items() if v != []}.keys()) - unique_params_to_del = list(set(to_delete)) - if unique_params_to_del: - delete_list_from_list(control_param_dict, unique_params_to_del) - set_param_dict = self._construct_set_parameters_dict(profile_set_param_dict, control_param_dict, context) if set_param_dict: @@ -174,15 +162,43 @@ def _construct_set_parameters_dict( # all the other elements are from the profile set_param new_dict[const.VALUES] = orig_dict.get(const.VALUES, None) new_dict[const.GUIDELINES] = orig_dict.get(const.GUIDELINES, None) + if new_dict[const.VALUES] is None: + new_dict.pop(const.VALUES) + if new_dict[const.GUIDELINES] is None: + new_dict.pop(const.GUIDELINES) else: # if the profile doesnt change this param at all, show it in the header with values tmp_dict = ModelUtils.parameter_to_dict(param_dict, True) values = tmp_dict.get('values', None) - new_dict = {'id': param_id, 'values': values} + # if values are None then donĀ“t display them in the markdown + if values is not None: + new_dict = {'id': param_id, 'values': values, const.PROFILE_VALUES: ['<REPLACE_ME>']} + else: + new_dict = {'id': param_id, const.PROFILE_VALUES: ['<REPLACE_ME>']} new_dict.pop('id', None) - if display_name: + # validates if there are aggregated parameter values to the current parameter + aggregated_props = [prop for prop in as_list(param_dict.props) if prop.name == const.AGGREGATES] + if aggregated_props != []: + props_to_add = [] + for prop in aggregated_props: + props_to_add.append(prop.value) + new_dict[const.AGGREGATES] = props_to_add + new_dict.pop(const.PROFILE_VALUES, None) + alt_identifier = [prop for prop in as_list(param_dict.props) if prop.name == const.ALT_IDENTIFIER] + if alt_identifier != []: + new_dict[const.ALT_IDENTIFIER] = alt_identifier[0].value + # adds display name, if no display name then do not add to dict + if display_name != '' and display_name is not None: new_dict[const.DISPLAY_NAME] = display_name - key_order = (const.LABEL, const.GUIDELINES, const.PROFILE_VALUES, const.VALUES, const.DISPLAY_NAME) + key_order = ( + const.LABEL, + const.GUIDELINES, + const.VALUES, + const.AGGREGATES, + const.ALT_IDENTIFIER, + const.DISPLAY_NAME, + const.PROFILE_VALUES + ) ordered_dict = {k: new_dict[k] for k in key_order if k in new_dict.keys()} set_param_dict[param_id] = ordered_dict diff --git a/trestle/core/commands/author/profile.py b/trestle/core/commands/author/profile.py index 2f4725d4f..c2469cfe6 100644 --- a/trestle/core/commands/author/profile.py +++ b/trestle/core/commands/author/profile.py @@ -285,33 +285,6 @@ def _replace_modify_set_params( profile.modify.set_parameters = none_if_empty(profile.modify.set_parameters) return changed - @staticmethod - def _add_aggregated_parameter( - param: Any, param_dict: Dict[str, Any], control_id: str, controls: Any, param_map: Dict[str, str] - ) -> None: - """ - Add aggregated parameter value to original parameter. - - Notes: - None - """ - # verifies aggregated param is not on grabbed param dict - if param.id not in list(param_dict.keys()): - param.values = [] - agg_props = [prop for prop in param.props if prop.name == 'aggregates'] - for prop in as_list(agg_props): - if param_dict[prop.value].get('values'): - agg_param_values = param_dict[prop.value].get('values') - for value in as_list(agg_param_values): - param.values.append(value) - else: - agg_param_values = [p for p in controls[control_id] if p.id == prop.value][0] - param.values.append(agg_param_values.props[0].value) - dict_param = ModelUtils.parameter_to_dict(param, False) - param_dict[param.id] = dict_param - param_map[param.id] = control_id - return None - @staticmethod def assemble_profile( trestle_root: pathlib.Path, @@ -381,14 +354,6 @@ def assemble_profile( catalog_api = CatalogAPI(catalog=catalog, context=context) found_alters, param_dict, param_map = catalog_api.read_additional_content_from_md(label_as_key=True) - controls = {} - for group in as_list(catalog.groups): - for control in as_list(group.controls): - controls[control.id] = control.params - for control_id, params in controls.items(): - for param in as_list(params): - ProfileAssemble._add_aggregated_parameter(param, param_dict, control_id, controls, param_map) - # technically if allowed sections is [] it means no sections are allowed if allowed_sections is not None: for bad_part in [ part for alter in found_alters for add in as_list(alter.adds) diff --git a/trestle/core/remote/cache.py b/trestle/core/remote/cache.py index e6b747351..635000451 100644 --- a/trestle/core/remote/cache.py +++ b/trestle/core/remote/cache.py @@ -286,7 +286,7 @@ def _do_fetch(self) -> None: auth = HTTPBasicAuth(self._username, self._password) try: - response = requests.get(self._url, auth=auth, verify=verify, timeout=10) + response = requests.get(self._url, auth=auth, verify=verify, timeout=30) except Exception as e: raise TrestleError(f'Cache update failure to connect via HTTPS: {self._url} ({e})') diff --git a/trestle/tasks/csv_to_oscal_cd.py b/trestle/tasks/csv_to_oscal_cd.py index e792c54cf..0a9fb4053 100644 --- a/trestle/tasks/csv_to_oscal_cd.py +++ b/trestle/tasks/csv_to_oscal_cd.py @@ -331,7 +331,7 @@ def _calculate_set_params(self, mod_rules: List) -> tuple: logger.debug(f'params mod: {key}') else: add_set_params.append(key) - logger.debug(f'prams add: {key}') + logger.debug(f'params add: {key}') return (del_set_params, add_set_params, mod_set_params) def _calculate_control_mappings(self, mod_rules: List) -> tuple: @@ -824,7 +824,17 @@ class _OscalHelper(): @staticmethod def add_set_parameter(set_parameter_list: List[SetParameter], set_parameter: SetParameter) -> None: """Add set parameter.""" - set_parameter_list.append(set_parameter) + add = True + # don't add duplicate + for sp in set_parameter_list: + if sp.param_id == set_parameter.param_id: + add = False + if sp.values != set_parameter.values: + text = f'set-parameter id={sp.param_id} conflicting values' + raise RuntimeError(text) + break + if add: + set_parameter_list.append(set_parameter) @staticmethod def remove_rule_statement(statements: List[Statement], rule_id: str, smt_id: str) -> List[Statement]: