Skip to content

Commit

Permalink
fix: add multiple parameters per rule support on component definition (
Browse files Browse the repository at this point in the history
…#1504)

* fix: add multiple parameters per rule in component definition

Signed-off-by: Alejandro Jose Leiva Palomo <alejandro.leiva.palomo@ibm.com>

* fix: correct code linting errors

Signed-off-by: Alejandro Jose Leiva Palomo <alejandro.leiva.palomo@ibm.com>

* fix: adding more testing

Signed-off-by: Alejandro Jose Leiva Palomo <alejandro.leiva.palomo@ibm.com>

* fix: add a value for the rule parameter in tests

Signed-off-by: Alejandro Jose Leiva Palomo <alejandro.leiva.palomo@ibm.com>

* fix: correct tests and add code for dup components validation

Signed-off-by: Alejandro Jose Leiva Palomo <alejandro.leiva.palomo@ibm.com>

* fix: correct quality gate error

Signed-off-by: Alejandro Jose Leiva Palomo <alejandro.leiva.palomo@ibm.com>

* fix: correct typo and fix test description

Signed-off-by: Alejandro Jose Leiva Palomo <alejandro.leiva.palomo@ibm.com>

* fix: correct typo

Signed-off-by: Alejandro Jose Leiva Palomo <alejandro.leiva.palomo@ibm.com>

---------

Signed-off-by: Alejandro Jose Leiva Palomo <alejandro.leiva.palomo@ibm.com>
  • Loading branch information
AleJo2995 committed Feb 22, 2024
1 parent 53d7fd4 commit 96e3f02
Show file tree
Hide file tree
Showing 9 changed files with 1,148 additions and 42 deletions.
503 changes: 503 additions & 0 deletions tests/data/json/comp_def_more_params.json

Large diffs are not rendered by default.

503 changes: 503 additions & 0 deletions tests/data/json/comp_def_more_params_dup.json

Large diffs are not rendered by default.

7 changes: 3 additions & 4 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,9 +369,8 @@ def setup_for_component_definition(tmp_trestle_dir: pathlib.Path, monkeypatch: M
return comp_name


def setup_component_generate(tmp_trestle_dir: pathlib.Path) -> str:
def setup_component_generate(tmp_trestle_dir: pathlib.Path, comp_name='comp_def_a') -> str:
"""Create the compdef, profile and catalog content component-generate."""
comp_name = 'comp_def_a'
load_from_json(tmp_trestle_dir, comp_name, comp_name, comp.ComponentDefinition)
for prof_name in 'comp_prof,comp_prof_aa,comp_prof_ab,comp_prof_ba,comp_prof_bb'.split(','):
load_from_json(tmp_trestle_dir, prof_name, prof_name, prof.Profile)
Expand Down Expand Up @@ -461,10 +460,10 @@ def setup_for_ssp(
prof_name: str,
output_name: str,
use_yaml: bool = False,
leveraged_ssp_name: str = ''
leveraged_ssp_name: str = '',
comp_names='comp_def_a,comp_def_b'
) -> Tuple[argparse.Namespace, pathlib.Path]:
"""Create the comp_def, profile and catalog content needed for ssp-generate."""
comp_names = 'comp_def_a,comp_def_b'
for comp_name in comp_names.split(','):
load_from_json(tmp_trestle_dir, comp_name, comp_name, comp.ComponentDefinition)
prof_name_list = [prof_name]
Expand Down
15 changes: 15 additions & 0 deletions tests/trestle/core/commands/author/component_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,18 @@ def test_component_generate_missing_control(tmp_trestle_dir: pathlib.Path, monke
test_utils.execute_command_and_assert(generate_cmd, 0, monkeypatch)
_, err = capsys.readouterr()
assert "Component comp_aa references controls {'ac-1'} not in profile." in err


def test_component_generate_more_than_one_param(tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch) -> None:
"""Test component generate with more than 1 parameters per rule."""
comp_name = test_utils.setup_component_generate(tmp_trestle_dir, 'comp_def_more_params')

generate_cmd = f'trestle author component-generate -n {comp_name} -o {md_path}'

# generate the md first time
test_utils.execute_command_and_assert(generate_cmd, CmdReturnCodes.SUCCESS.value, monkeypatch)

assem_name = 'assem_comp'
# now assemble component generated
assemble_cmd = f'trestle author component-assemble -m {md_path} -n {comp_name} -o {assem_name}'
test_utils.execute_command_and_assert(assemble_cmd, CmdReturnCodes.SUCCESS.value, monkeypatch)
58 changes: 58 additions & 0 deletions tests/trestle/core/commands/author/ssp_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1055,3 +1055,61 @@ def test_ssp_assemble_no_comps(tmp_trestle_dir: pathlib.Path, capsys) -> None:
_, err = capsys.readouterr()
assert 'Control ac-1 references component Bad Component not defined' in err
assert 'Please specify the names of any component-definitions' in err


def test_ssp_gen_and_assemble_more_than_one_param(tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch) -> None:
"""Test ssp generate and assemble with more than 1 parameters per rule."""
gen_args, _ = setup_for_ssp(tmp_trestle_dir, prof_name, ssp_name, False, '', 'comp_def_more_params')
args_compdefs = gen_args.compdefs

# first create the markdown
ssp_gen = SSPGenerate()
assert ssp_gen._run(gen_args) == 0
new_version = '1.2.3'

md_path = tmp_trestle_dir / ssp_name / 'ac' / 'ac-1.md'
assert md_path.exists()

md_api = MarkdownAPI()
header, tree = md_api.processor.process_markdown(md_path)
rule_parameters = header['x-trestle-comp-def-rules-param-vals']['comp_ca']
rule_parameters.append({'name': 'allowed_admins_per_account2', 'values': ['20']})

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

# verifies a second parameter has beend added to the top shared rule
assert header['x-trestle-rules-params']['comp_ca'][1]['name'] == 'allowed_admins_per_account2'

# verifies the parameter value for the rule has been written down correctly in the markdown file
assert header['x-trestle-comp-def-rules-param-vals']['comp_ca'][1]['values'] == ['20']

# now assemble controls into json ssp
ssp_assemble = SSPAssemble()
args = argparse.Namespace(
trestle_root=tmp_trestle_dir,
markdown=ssp_name,
output=ssp_name,
verbose=0,
regenerate=False,
version=new_version,
name=None,
compdefs=args_compdefs
)
assert ssp_assemble._run(args) == 0

assem_ssp, _ = ModelUtils.load_model_for_class(tmp_trestle_dir, ssp_name, ossp.SystemSecurityPlan)
set_parameters = assem_ssp.control_implementation.implemented_requirements[0].by_components[0].set_parameters
set_params = [
set_param.param_id for set_param in set_parameters if set_param.param_id == 'allowed_admins_per_account2'
]
# this demonstrates there's only one iteration of the parameter and not being repeated
assert len(set_params) == 1


def test_ssp_gen_throw_exception_for_rep_comps(tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch) -> None:
"""Test ssp generate for duplicated component uuids between diff component definition."""
gen_args, _ = setup_for_ssp(tmp_trestle_dir, prof_name, ssp_name, False, '',
'comp_def_more_params,comp_def_more_params_dup')
# first create the markdown
ssp_gen = SSPGenerate()
assert ssp_gen._run(gen_args) == 1
36 changes: 24 additions & 12 deletions trestle/core/catalog/catalog_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -752,16 +752,17 @@ def _get_control_memory_info(self, control_id: str, context: ControlContext) ->
rules_params = {}
rules_param_names = []
for comp_name, rules_params_dict in as_dict(context.rules_params_dict).items():
for rule_id, rules_param in rules_params_dict.items():
for rule_id, rules_parameters in rules_params_dict.items():
if rule_id in rule_ids.get(comp_name, []):
param_name = rules_param['name']
rules_param_names.append(param_name)
rules_param[const.HEADER_RULE_ID] = rule_id_rule_name_map[comp_name].get(rule_id, None)
deep_append(rules_params, [comp_name], rules_param)
deep_set(
param_id_rule_name_map, [comp_name, rules_param['name']],
rule_id_rule_name_map[comp_name][rule_id]
)
for rule_parameter in rules_parameters:
param_name = rule_parameter['name']
rules_param_names.append(param_name)
rule_parameter[const.HEADER_RULE_ID] = rule_id_rule_name_map[comp_name].get(rule_id, None)
deep_append(rules_params, [comp_name], rule_parameter)
deep_set(
param_id_rule_name_map, [comp_name, rule_parameter['name']],
rule_id_rule_name_map[comp_name][rule_id]
)
set_or_pop(header, const.RULES_PARAMS_TAG, rules_params)

self._extend_rules_param_list(control_id, header, param_id_rule_name_map)
Expand Down Expand Up @@ -873,12 +874,15 @@ def generate_control_rule_info(self, part_id_map: Dict[str, Dict[str, str]], con
"""
context.rules_dict = {}
context.rules_params_dict = {}
comps_uuids = []
for comp_def_name in context.comp_def_name_list:
context.comp_def, _ = ModelUtils.load_model_for_class(
context.trestle_root,
comp_def_name,
comp.ComponentDefinition
)
component_uuids = [comp.uuid for comp in context.comp_def.components]
comps_uuids.extend(component_uuids)
for component in as_list(context.comp_def.components):
context.component = component
context.comp_name = component.title
Expand All @@ -891,6 +895,14 @@ def generate_control_rule_info(self, part_id_map: Dict[str, Dict[str, str]], con
self._add_control_imp_comp_info(context, part_id_map, comp_rules_props)
# add the rule_id to the param_dict
for param_comp_name, rule_param_dict in context.rules_params_dict.items():
for rule_tag, param_dict in rule_param_dict.items():
rule_dict = deep_get(context.rules_dict, [param_comp_name, rule_tag], {})
param_dict[const.HEADER_RULE_ID] = rule_dict.get(const.NAME, 'unknown_rule')
for rule_tag, params_list in rule_param_dict.items():
for param in params_list:
rule_dict = deep_get(context.rules_dict, [param_comp_name, rule_tag], {})
param[const.HEADER_RULE_ID] = rule_dict.get(const.NAME, 'unknown_rule')
# determine if there are duplicated uuids and throw an exception
dup_comp_uuids = set({comp_uuid for comp_uuid in comps_uuids if comps_uuids.count(comp_uuid) > 1})
if len(dup_comp_uuids) > 0:
# throw an exception if there are repeated component uuids
for comp_uuid in dup_comp_uuids:
logger.error(f'Component uuid { comp_uuid } is duplicated')
raise TrestleError('Component uuids cannot be duplicated between different component definitions')
11 changes: 6 additions & 5 deletions trestle/core/catalog/catalog_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,10 @@ def _fixup_param_dicts(context: ControlContext) -> None:
"""Merge info in the rules params dict and the rules param vals dict."""
for comp_name, comp_dict in context.rules_params_dict.items():
rules_dict = context.rules_dict.get(comp_name, {})
for rule_id, param_dict in comp_dict.items():
rule_name = deep_get(rules_dict, [rule_id, 'name'], 'unknown_rule_name')
param_dict[const.HEADER_RULE_ID] = rule_name
for rule_id, params_list in comp_dict.items():
for param in params_list:
rule_name = deep_get(rules_dict, [rule_id, 'name'], 'unknown_rule_name')
param[const.HEADER_RULE_ID] = rule_name

def write_catalog_as_ssp_markdown(self, context: ControlContext, part_id_map: Dict[str, Dict[str, str]]) -> None:
"""
Expand Down Expand Up @@ -340,13 +341,13 @@ def _update_values(set_param: comp.SetParameter, control_param_dict) -> None:
# get top level rule info applying to all controls
comp_rules_dict, comp_rules_params_dict, _ = ControlInterface.get_rules_and_params_dict_from_item(context.component) # noqa E501
context.rules_dict[context.comp_name] = comp_rules_dict
context.rules_params_dict.update(comp_rules_params_dict)
context.rules_params_dict[context.comp_name] = comp_rules_params_dict
for control_imp in as_list(context.component.control_implementations):
control_imp_rules_dict, control_imp_rules_params_dict, _ = ControlInterface.get_rules_and_params_dict_from_item(control_imp) # noqa E501
context.rules_dict[context.comp_name].update(control_imp_rules_dict)
comp_rules_params_dict = context.rules_params_dict.get(context.comp_name, {})
comp_rules_params_dict.update(control_imp_rules_params_dict)
context.rules_params_dict[context.comp_name] = comp_rules_params_dict
context.rules_params_dict[context.comp_name].update(comp_rules_params_dict)
ci_set_params = ControlInterface.get_set_params_from_item(control_imp)
for imp_req in as_list(control_imp.implemented_requirements):
control_part_id_map = part_id_map.get(imp_req.control_id, {})
Expand Down
6 changes: 4 additions & 2 deletions trestle/core/commands/author/ssp.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,11 @@ def _get_params_for_rules(context: ControlContext, rules_list: List[str],
# find param_ids needed by rules
for rule_id in rules_list:
# get list of param_ids associated with this rule_id
param_ids = [param['name'] for param in rule_dict.values() if param['rule-id'] == rule_id]
param_ids = [
param['name'] for params in rule_dict.values() for param in params if param['rule-id'] == rule_id
]
needed_param_ids.update(param_ids)
all_rule_param_ids = [param['name'] for param in rule_dict.values()]
all_rule_param_ids = [param['name'] for params in rule_dict.values() for param in params]
# any set_param that isn't associated with a rule should be included as a normal control set param with no rule
for set_param in set_params:
if set_param.param_id not in all_rule_param_ids:
Expand Down
51 changes: 32 additions & 19 deletions trestle/core/control_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,39 +454,50 @@ def get_params_dict_from_item(item: TypeWithProps) -> Tuple[Dict[str, Dict[str,
"""Get all params found in this item with rule_id as key."""
# id, description, options - where options is a string containing comma-sep list of items
# params is dict with rule_id as key and value contains: param_name, description and choices
params: Dict[str, Dict[str, str]] = {}
params: Dict[str, List[Dict[str, str]]] = {}
props = []
for prop in as_list(item.props):
if prop.name == const.PARAMETER_ID:
if const.PARAMETER_ID in prop.name:
rule_id = prop.remarks
param_name = prop.value
if rule_id in params:
raise TrestleError(f'Duplicate param {param_name} found for rule {rule_id}')
# create new param for this rule
params[rule_id] = {'name': param_name}
# rule already exists in parameters dict
if rule_id in params.keys():
existing_param = next((prm for prm in params[rule_id] if prm['name'] == param_name), None)
if existing_param is not None:
raise TrestleError(f'Param id for rule {rule_id} already exists')
else:
# append a new parameter for the current rule
params[rule_id].append({'name': param_name})
else:
# create new param for this rule for the first parameter
params[rule_id] = [{'name': param_name}]
props.append(prop)
elif prop.name == const.PARAMETER_DESCRIPTION:
elif const.PARAMETER_DESCRIPTION in prop.name:
rule_id = prop.remarks
if rule_id in params:
params[rule_id]['description'] = prop.value
param = next((prm for prm in params[rule_id] if prm['name'] == param_name), None)
param['description'] = prop.value
props.append(prop)
else:
raise TrestleError(f'Param description for rule {rule_id} found with no param_id')
elif prop.name == const.PARAMETER_VALUE_ALTERNATIVES:
elif const.PARAMETER_VALUE_ALTERNATIVES in prop.name:
rule_id = prop.remarks
if rule_id in params:
params[rule_id]['options'] = prop.value
param = next((prm for prm in params[rule_id] if prm['name'] == param_name), None)
param['options'] = prop.value
props.append(prop)
else:
raise TrestleError(f'Param options for rule {rule_id} found with no param_id')
new_params = {}
for rule_id, param in params.items():
if 'name' not in param:
logger.warning(f'Parameter for rule_id {rule_id} has no matching name. Ignoring the param.')
else:
param['description'] = param.get('description', '')
param['options'] = param.get('options', '')
new_params[rule_id] = param
for rule_id, rule_params in params.items():
new_params[rule_id] = []
for param in rule_params:
if 'name' not in param:
logger.warning(f'Parameter for rule_id {rule_id} has no matching name. Ignoring the param.')
else:
param['description'] = param.get('description', '')
param['options'] = param.get('options', '')
new_params[rule_id].append(param)
return new_params, props

@staticmethod
Expand Down Expand Up @@ -1102,14 +1113,16 @@ def insert_imp_req_into_component(
"""
for control_imp in as_list(component.control_implementations):
_, control_imp_param_dict, _ = ControlInterface.get_rules_and_params_dict_from_item(control_imp)
control_imp_rule_param_ids = [d['name'] for d in control_imp_param_dict.values()]
control_imp_rule_param_ids = [
param['name'] for params in control_imp_param_dict.values() for param in params
]
if profile_title != ModelUtils.get_title_from_model_uri(trestle_root, control_imp.source):
continue
for imp_req in as_list(control_imp.implemented_requirements):
if imp_req.control_id != new_imp_req.control_id:
continue
_, imp_req_param_dict, _ = ControlInterface.get_rules_and_params_dict_from_item(imp_req)
imp_req_rule_param_ids = [d['name'] for d in imp_req_param_dict]
imp_req_rule_param_ids = [param['name'] for params in imp_req_param_dict.values() for param in params]
status = ControlInterface.get_status_from_props(new_imp_req)
ControlInterface.insert_status_in_props(imp_req, status)
imp_req.description = new_imp_req.description
Expand Down

0 comments on commit 96e3f02

Please sign in to comment.