Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: adding parameter aggregation from other parameter values for given control #1412

Merged
merged 7 commits into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 82 additions & 2 deletions tests/trestle/core/commands/author/profile_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ def test_profile_generate_assemble(
if set_parameters_flag:
assert set_params[2].values[0] == 'new value'
assert set_params[1].props[0].ns == const.TRESTLE_GENERIC_NS
assert len(set_params) == 15
assert len(set_params) == 18
else:
# the original profile did not have ns set for this display name
# confirm the namespace is not defined unless set_parameters_flag is True
Expand Down Expand Up @@ -380,7 +380,7 @@ def test_profile_ohv(required_sections: Optional[str], success: bool, ohv: bool,
)
set_params = profile.modify.set_parameters

assert len(set_params) == 15
assert len(set_params) == 18
assert set_params[0].values[0] == 'all personnel'
# the label is present in the header so it ends up in the set_parameter
assert set_params[0].label == 'label from edit'
Expand Down Expand Up @@ -1115,3 +1115,83 @@ def test_profile_inherit(tmp_trestle_dir: pathlib.Path):
args.output = args.profile
prof_inherit = ProfileInherit()
assert prof_inherit._run(args) == 2


def test_profile_generate_assemble_parameter_aggregation(
tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch
) -> None:
"""Test the profile markdown generator."""
_, assembled_prof_dir, _, markdown_path = setup_profile_generate(tmp_trestle_dir, 'simple_test_profile.json')
yaml_header_path = test_utils.YAML_TEST_DATA_PATH / 'good_simple.yaml'
ac_path = markdown_path / 'ac'

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'}
appended_prop_2 = {'name': 'aggregates', 'value': 'at-03_odp.01'}
ac_1 = nist_cat.groups[0].controls[0]
ac_1.params[2].props = []
ac_1.params[2].props.append(appended_prop)
ac_1.params[2].props.append(appended_prop_2)
appended_extra_param = {
'id': 'at-02_odp.01',
'props': [{
'name': 'label', 'value': 'AT-02_ODP[01]', 'class': 'sp800-53a'
}],
'label': 'frequency',
'guidelines': [{
'prose': 'blah'
}]
}
appended_extra_param_2 = {
'id': 'at-03_odp.01',
'props': [{
'name': 'label', 'value': 'AT-03_ODP[01]', 'class': 'sp800-53a'
}],
'label': 'frequency',
'guidelines': [{
'prose': 'blah'
}]
}
ac_1.params.append(appended_extra_param)
ac_1.params.append(appended_extra_param_2)

ModelUtils.save_top_level_model(nist_cat, tmp_trestle_dir, 'nist_cat', FileContentType.JSON)

# convert resolved profile catalog to markdown then assemble it after adding an item to a control
# generate, edit, assemble
test_args = f'trestle author profile-generate -n {prof_name} -o {md_name} -rs NeededExtra'.split( # noqa E501
)
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

fc = test_utils.FileChecker(ac_path)

assert Trestle().run() == 0

assert fc.files_unchanged()

# edit parameter values
md_path = tmp_trestle_dir / 'my_md/ac/ac-1.md'
assert md_path.exists()
md_api = MarkdownAPI()
header, tree = md_api.processor.process_markdown(md_path)

assert header
header[const.SET_PARAMS_TAG]['at-02_odp.01'][const.VALUES] = ['Value 1', 'Value 2']
header[const.SET_PARAMS_TAG]['at-03_odp.01'][const.VALUES] = ['Value 3', 'Value 4', 'Value 5']

md_api.write_markdown_with_header(md_path, header, tree.content.raw_text)

# assemble based on set_parameters_flag
test_args = f'trestle author profile-assemble -n {prof_name} -m {md_name} -o {assembled_prof_name}'.split()
test_args.append('-sp')
assembled_prof_dir.mkdir()
monkeypatch.setattr(sys, 'argv', test_args)
assert Trestle().run() == 0

prof_generate = f'trestle author profile-generate -n {prof_name} -o {md_name} --force-overwrite'
test_utils.execute_command_and_assert(prof_generate, 0, monkeypatch)
4 changes: 2 additions & 2 deletions trestle/core/catalog/catalog_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ def read_additional_content(
# 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)
final_param_dict[param_id] = param_dict
param_sort_map[param_id] = sort_id
final_param_dict[param_id] = param_dict
param_sort_map[param_id] = sort_id
new_alters: List[prof.Alter] = []
# fill the alters according to the control sorting order
for key in sorted(alters_map.keys()):
Expand Down
13 changes: 12 additions & 1 deletion trestle/core/catalog/catalog_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, none_if_empty
from trestle.common.list_utils import as_list, deep_get, delete_list_from_list, 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
Expand Down Expand Up @@ -63,6 +63,17 @@ 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)

Expand Down
35 changes: 35 additions & 0 deletions trestle/core/commands/author/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ def generate_markdown(
catalog, inherited_props = ProfileResolver().get_resolved_profile_catalog_and_inherited_props(
trestle_root, profile_path, True, True, None, ParameterRep.LEAVE_MOUSTACHE
)

deep_set(yaml_header, [const.TRESTLE_GLOBAL_TAG, const.PROFILE, const.TITLE], profile.metadata.title)

context = ControlContext.generate(ContextPurpose.PROFILE, True, trestle_root, markdown_path)
Expand Down Expand Up @@ -284,6 +285,33 @@ 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,
Expand Down Expand Up @@ -353,6 +381,13 @@ 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 [
Expand Down