diff --git a/openscap_report/scap_results_parser/data_structures/profile_info.py b/openscap_report/scap_results_parser/data_structures/profile_info.py index 1b60c248..2a16818a 100644 --- a/openscap_report/scap_results_parser/data_structures/profile_info.py +++ b/openscap_report/scap_results_parser/data_structures/profile_info.py @@ -29,6 +29,9 @@ def get_applicable_cpe_platforms_for_profile(self): return ", ".join(self.cpe_platforms_for_profile.keys()) def get_cpe_platforms_that_satisfy_evaluation_target(self): - return ", ".join( - [cpe_id for cpe_id, is_satisfy in self.cpe_platforms_for_profile.items() if is_satisfy] - ) + return ", ".join(self.get_list_of_cpe_platforms_that_satisfy_evaluation_target()) + + def get_list_of_cpe_platforms_that_satisfy_evaluation_target(self): + return [ + cpe_id for cpe_id, is_satisfy in self.cpe_platforms_for_profile.items() if is_satisfy + ] diff --git a/openscap_report/scap_results_parser/data_structures/rule.py b/openscap_report/scap_results_parser/data_structures/rule.py index 50921d7e..703907e1 100644 --- a/openscap_report/scap_results_parser/data_structures/rule.py +++ b/openscap_report/scap_results_parser/data_structures/rule.py @@ -47,6 +47,7 @@ class Rule: # pylint: disable=R0902 warnings: List[RuleWarning] = field(default_factory=list) platforms: List[str] = field(default_factory=list) oval_definition_id: str = None + oval_reference: str = None oval_definition: OvalDefinition = None messages: List[str] = field(default_factory=list) remediations: List[Remediation] = field(default_factory=list) diff --git a/openscap_report/scap_results_parser/oval_and_cpe_tree_builder.py b/openscap_report/scap_results_parser/oval_and_cpe_tree_builder.py index 0de8b4fa..3c9b4fac 100644 --- a/openscap_report/scap_results_parser/oval_and_cpe_tree_builder.py +++ b/openscap_report/scap_results_parser/oval_and_cpe_tree_builder.py @@ -9,55 +9,89 @@ class OVALAndCPETreeBuilder: # pylint: disable=R0902 - def __init__(self, root, group_parser, profile_platforms): + def __init__(self, root, group_parser, profile_platforms, oval_definitions_and_results_sources): self.profile_platforms = profile_platforms self.root = root self.group_parser = group_parser + self.oval_definitions_and_results_sources = oval_definitions_and_results_sources + self.cpe_source = "" self.missing_oval_results = False self.cpe_al = True - self.oval_definitions = {} - self.oval_cpe_definitions = {} + self.reports_with_oval_definitions = None self.platform_to_oval_cpe_id = {} self.cpe_platforms = {} + self.dict_of_oval_cpe_definitions = {} self.load_oval_definitions() def load_oval_definitions(self): try: - self.cpe_al_parser = CPEApplicabilityLanguageParser(self.root) - self.platform_to_oval_cpe_id = self.cpe_al_parser.platform_to_oval_cpe_id - self.oval_definition_parser = OVALDefinitionParser( - self.root, self.platform_to_oval_cpe_id + self.oval_definition_parser = OVALDefinitionParser(self.root) + self.reports_with_oval_definitions = self.oval_definition_parser.get_oval_definitions() + self._determine_cpe_source() + self.dict_of_oval_cpe_definitions = self._get_dict_of_oval_cpe_definitions() + self.cpe_al_parser = CPEApplicabilityLanguageParser( + self.root, self.dict_of_oval_cpe_definitions ) - self.oval_definitions = self.oval_definition_parser.get_oval_definitions() - self.oval_cpe_definitions = self.oval_definition_parser.get_oval_cpe_definitions() + self.platform_to_oval_cpe_id = self.cpe_al_parser.platform_to_oval_cpe_id self._load_cpe_platforms() except MissingOVALResult as error: logging.warning(( - "The given input doesn't contain OVAL results (\"%s\")," - " OVAL details won't be shown in the report."), error) - if str(error) != "oval1": - self.missing_oval_results = True + "The given input doesn't contain OVAL results (\"%s\"), " + "OVAL details won't be shown in the report."), error) + self.missing_oval_results = True + + def _determine_cpe_source(self): + source_id = set(self.reports_with_oval_definitions.keys()).difference( + self.oval_definitions_and_results_sources + ) + if len(source_id) == 1: + self.cpe_source = source_id.pop() + + def _get_dict_of_oval_cpe_definitions(self): + if self.cpe_source in self.reports_with_oval_definitions: + return self.reports_with_oval_definitions[self.cpe_source] + logging.warning(( + "The given input does not contain a clear mapping of the OVAL definition used " + "for CPE checks. The results of the OVAL definition in the CPE checks could " + "be biased." + )) + all_oval_definition = {} + for report in self.reports_with_oval_definitions.values(): + for id_definition, definition in report.items(): + if id_definition not in all_oval_definition: + all_oval_definition[id_definition] = definition + else: + if definition.oval_tree.evaluate_tree() != "not evaluated": + all_oval_definition[id_definition] = definition + return all_oval_definition + + def _get_oval_definition_of_cpe(self, platform): + cpe_oval_id = self.platform_to_oval_cpe_id.get(platform) + return self.dict_of_oval_cpe_definitions.get(cpe_oval_id) def _load_cpe_platforms(self): try: - self.cpe_platforms = self.cpe_al_parser.get_cpe_platforms(self.oval_cpe_definitions) + self.cpe_platforms = self.cpe_al_parser.get_cpe_platforms() for platform in self.profile_platforms: - if platform in self.platform_to_oval_cpe_id: - cpe_oval_id = self.platform_to_oval_cpe_id[platform] - if cpe_oval_id in self.oval_cpe_definitions: - oval_tree = self.oval_cpe_definitions[cpe_oval_id].oval_tree - self.cpe_platforms[platform] = Platform( - platform_id=platform, - logical_test=LogicalTest( - node_type="AND", - children=[LogicalTest( - node_type="frac-ref", - value=cpe_oval_id, - oval_tree=oval_tree - )], - ), - title="Profile platform", - ) + oval_definition = self._get_oval_definition_of_cpe(platform) + if oval_definition is None: + logging.warning( + "Platform (\"%s\") doesn't exist, Platform won't be shown in the report.", + platform + ) + continue + self.cpe_platforms[platform] = Platform( + platform_id=platform, + logical_test=LogicalTest( + node_type="AND", + children=[LogicalTest( + node_type="frac-ref", + value=oval_definition.definition_id, + oval_tree=oval_definition.oval_tree + )], + ), + title="Profile platform", + ) self._evaluate_all_cpe_platforms() except ExceptionNoCPEApplicabilityLanguage: self.cpe_al = False @@ -71,8 +105,8 @@ def _get_oval_tree_from_oval_cpe_definition(self, platform): cpe_platform = platform.lstrip("#") if cpe_platform in self.platform_to_oval_cpe_id: cpe_oval_id = self.platform_to_oval_cpe_id[cpe_platform] - if cpe_oval_id in self.oval_cpe_definitions: - return self.oval_cpe_definitions[cpe_oval_id].oval_tree + if cpe_oval_id in self.dict_of_oval_cpe_definitions: + return self.dict_of_oval_cpe_definitions[cpe_oval_id].oval_tree if cpe_platform in self.cpe_platforms: return None logging.warning("There is no CPE check for the platform \"%s\".", platform) @@ -100,13 +134,29 @@ def _remove_double_cpe_requirement(rule, group_platforms): if platform in group_platforms: group_platforms.remove(platform) + def get_oval_definition(self, rule): + report = self.reports_with_oval_definitions.get(rule.oval_reference, None) + if report is not None: + return report.get(rule.oval_definition_id) + oval_def = None + for report in self.reports_with_oval_definitions.values(): + if oval_def is not None and rule.oval_definition_id in report: + logging.warning( + ("The given input contains the duplicate results of " + "the OVAL definition (\"%s\")."), + rule.oval_definition_id + ) + if rule.oval_definition_id in report: + oval_def = report[rule.oval_definition_id] + return oval_def + def insert_oval_and_cpe_trees_to_rules(self, rules): if self.missing_oval_results: return for rule in rules.values(): - if rule.oval_definition_id in self.oval_definitions: - rule.oval_definition = self.oval_definitions[rule.oval_definition_id] + rule.oval_definition = self.get_oval_definition(rule) + rule_group = self.group_parser.rule_to_group_id.get(rule.rule_id, "") group_platforms = self.group_parser.group_to_platforms.get(rule_group, []) self._remove_double_cpe_requirement(rule, group_platforms) diff --git a/openscap_report/scap_results_parser/parsers/cpe_al_parser.py b/openscap_report/scap_results_parser/parsers/cpe_al_parser.py index 10259c78..edb8f833 100644 --- a/openscap_report/scap_results_parser/parsers/cpe_al_parser.py +++ b/openscap_report/scap_results_parser/parsers/cpe_al_parser.py @@ -9,11 +9,11 @@ class CPEApplicabilityLanguageParser: - def __init__(self, root): + def __init__(self, root, oval_cpe_definitions): self.root = root self.platform_to_oval_cpe_id = self.get_platform_to_oval_cpe_id_dict() self.full_text_parser = FullTextParser({}) - self.oval_cpe_definitions = {} + self.oval_cpe_definitions = oval_cpe_definitions def get_platform_to_oval_cpe_id_dict(self): cpe_list = self.root.find(".//ds:component/cpe-dict:cpe-list", NAMESPACES) @@ -69,9 +69,8 @@ def get_logical_test(self, logical_test_el): logical_test.children.append(self.get_logical_test(child_logical_test_el)) return logical_test - def get_cpe_platforms(self, oval_cpe_definitions): + def get_cpe_platforms(self): out = {} - self.oval_cpe_definitions = oval_cpe_definitions for platform, platform_el in self._get_cpe_platform_elements().items(): title_el = platform_el.find(".//cpe-lang:title", NAMESPACES) title_str = "" diff --git a/openscap_report/scap_results_parser/parsers/oval_definition_parser.py b/openscap_report/scap_results_parser/parsers/oval_definition_parser.py index 5be06e5a..9f5d4c46 100644 --- a/openscap_report/scap_results_parser/parsers/oval_definition_parser.py +++ b/openscap_report/scap_results_parser/parsers/oval_definition_parser.py @@ -8,11 +8,10 @@ class OVALDefinitionParser: - def __init__(self, root, platform_to_oval_cpe_id): + def __init__(self, root): self.root = root - self.oval_result_parser = OVALResultParser(self.root, platform_to_oval_cpe_id) - self.oval_trees = self.oval_result_parser.get_oval_trees() - self.oval_cpe_trees = self.oval_result_parser.get_oval_cpe_trees() + self.oval_result_parser = OVALResultParser(self.root) + self.oval_trees_by_oval_reports = self.oval_result_parser.get_oval_trees_by_oval_reports() self.oval_reports = self.oval_result_parser.oval_reports self.oval_definitions = self._get_xml_elements_of_oval_definitions() @@ -20,7 +19,7 @@ def __init__(self, root, platform_to_oval_cpe_id): def _get_xml_elements_of_oval_definitions(self): out = {} for oval, oval_report in self.oval_reports.items(): - out[oval] = oval_report.find( + out[oval] = oval_report.oval_report_element.find( './/oval-definitions:oval_definitions/oval-definitions:definitions', NAMESPACES) return out @@ -64,10 +63,10 @@ def _get_oval_definitions(self, oval): return definitions def get_oval_definitions(self): - return self._get_oval_definitions("oval0") - - def get_oval_cpe_definitions(self): - return self._get_oval_definitions("oval1") + oval_definitions_by_reports = {} + for report_id in self.oval_trees_by_oval_reports: + oval_definitions_by_reports[report_id] = self._get_oval_definitions(report_id) + return oval_definitions_by_reports def _get_test_criteria(self, criterion): out = {"comment": criterion.get("comment")} @@ -90,11 +89,9 @@ def _create_dict_from_criteria(self, criteria): criteria_dict["child_criteria"].append(self._get_test_criteria(criterion)) return criteria_dict - def _add_oval_tree_to_definition(self, definitions, oval): + def _add_oval_tree_to_definition(self, definitions, oval_report_id): + oval_tree_source = self.oval_trees_by_oval_reports.get(oval_report_id, {}) for definition_id in definitions: - oval_tree_source = self.oval_trees - if oval == "oval1": - oval_tree_source = self.oval_cpe_trees self._set_oval_tree_to_definition(definitions, definition_id, oval_tree_source) def _set_oval_tree_to_definition(self, definitions, definition_id, oval_tree_source): @@ -126,11 +123,9 @@ def _fill_oval_tree_with_comments(self, oval_tree, criteria_id, criteria_dict, c self._fill_oval_tree_with_comments( oval_node, criterion.get("extend_definition"), criteria_dict) - def _add_comments_to_oval_tree(self, dict_of_criteria, oval): + def _add_comments_to_oval_tree(self, dict_of_criteria, oval_report_id): + oval_tree_source = self.oval_trees_by_oval_reports.get(oval_report_id, {}) for id_ in dict_of_criteria: - oval_tree_source = self.oval_trees - if oval == "oval1": - oval_tree_source = self.oval_cpe_trees if id_ not in oval_tree_source: continue diff --git a/openscap_report/scap_results_parser/parsers/oval_result_parser.py b/openscap_report/scap_results_parser/parsers/oval_result_parser.py index 521c1a48..5f23b460 100644 --- a/openscap_report/scap_results_parser/parsers/oval_result_parser.py +++ b/openscap_report/scap_results_parser/parsers/oval_result_parser.py @@ -3,6 +3,9 @@ import logging import uuid +from dataclasses import dataclass + +from lxml.etree import Element from ..data_structures import OvalNode from ..exceptions import MissingOVALResult @@ -13,18 +16,19 @@ STR_NEGATION_BOOL = {'true': 'false', 'false': 'true'} +@dataclass +class OVALReport: + oval_report_id: str + oval_report_element: Element + oval_results_element: Element + parser_info_of_oval_test: OVALTestInfoParser + + class OVALResultParser: - def __init__(self, root, platform_to_oval_cpe_id): + def __init__(self, root): self.root = root - self.platform_to_oval_cpe_id = platform_to_oval_cpe_id self.oval_reports = self._get_oval_reports() logging.info(self.oval_reports) - self.oval_results = self._get_oval_results("oval0") - self.parser_info_of_test = OVALTestInfoParser(self.oval_reports.get("oval0", None)) - self.oval_cpe_results = self._get_oval_results("oval1") - self.parser_info_of_cpe_test = None - if "oval1" in self.oval_reports: - self.parser_info_of_cpe_test = OVALTestInfoParser(self.oval_reports["oval1"]) def _get_oval_reports(self): oval_reports = {} @@ -32,50 +36,39 @@ def _get_oval_reports(self): if reports is None: raise MissingOVALResult("all_OVAL_results") - for report in reports: - report_id = report.get("id") + for report_element in reports: + report_id = report_element.get("id") if "oval" in report_id: - oval_reports[report_id] = report + oval_results = self._get_oval_results(report_element) + parser_info_of_test = OVALTestInfoParser(report_element) + oval_reports[report_id] = OVALReport( + report_id, + report_element, + oval_results, + parser_info_of_test + ) return oval_reports - def _get_oval_results(self, oval_id): - oval_report = self.oval_reports.get(oval_id, None) - if oval_report is None: - return None + def _get_oval_results(self, oval_report): return oval_report.find( ('.//XMLSchema:oval_results/XMLSchema:results/' 'XMLSchema:system/XMLSchema:definitions'), NAMESPACES) - def get_oval_trees(self): - dict_of_oval_definitions = {} - for definition in self.oval_results: - id_definition = definition.get('definition_id') - dict_of_oval_definitions[id_definition] = self._build_node( - definition[0], - "Definition", - id_definition - ) - return self._fill_extend_definition(dict_of_oval_definitions) - - def get_oval_cpe_trees(self): - dict_of_oval_definitions = {} - if self.oval_cpe_results is None: - return dict_of_oval_definitions - - for definition in self.oval_cpe_results: - id_definition = definition.get('definition_id') - dict_of_oval_definitions[id_definition] = self._build_node( - definition[0], - "OVAL definition of CPE platform", - id_definition, - True - ) - oval_cpe_trees = self._fill_extend_definition(dict_of_oval_definitions) - out = {} - for oval_id in self.platform_to_oval_cpe_id.values(): - if oval_id in oval_cpe_trees: - out[oval_id] = oval_cpe_trees[oval_id] - return oval_cpe_trees + def get_oval_trees_by_oval_reports(self): + dict_of_oval_reports = {} + for report_id, report in self.oval_reports.items(): + dict_of_oval_results = {} + for definition in report.oval_results_element: + id_definition = definition.get('definition_id') + criteria_result = definition[0] + dict_of_oval_results[id_definition] = self._build_node( + criteria_result, + "Definition", + id_definition, + report_id + ) + dict_of_oval_reports[report_id] = self._fill_extend_definition(dict_of_oval_results) + return dict_of_oval_reports @staticmethod def _get_negation(node): @@ -107,13 +100,11 @@ def _get_extend_definition_node(self, child): tag="Extend definition", ) - def _get_test_node(self, child, is_cpe=False): + def _get_test_node(self, child, oval_report_id): negation = self._get_negation(child) result_of_node = self._get_result(negation, child) test_id = child.get('test_ref') - parser_of_test_info = self.parser_info_of_test - if is_cpe: - parser_of_test_info = self.parser_info_of_cpe_test + parser_of_test_info = self.oval_reports[oval_report_id].parser_info_of_oval_test return OvalNode( node_id=test_id, node_type="value", @@ -123,7 +114,7 @@ def _get_test_node(self, child, is_cpe=False): test_info=parser_of_test_info.get_test_info(test_id), ) - def _build_node(self, tree, tag, id_definition, is_cpe=False): + def _build_node(self, tree, tag, id_definition, oval_report_id): negation = self._get_negation(tree) node = OvalNode( node_id=id_definition, @@ -140,14 +131,14 @@ def _build_node(self, tree, tag, id_definition, is_cpe=False): child, "Criteria", f"no-id-criteria-{uuid.uuid4()}", - is_cpe + oval_report_id ) ) else: if child.get('definition_ref') is not None: node.children.append(self._get_extend_definition_node(child)) else: - node.children.append(self._get_test_node(child, is_cpe)) + node.children.append(self._get_test_node(child, oval_report_id)) return node def _fill_extend_definition(self, dict_of_oval_definitions): diff --git a/openscap_report/scap_results_parser/parsers/rule_parser.py b/openscap_report/scap_results_parser/parsers/rule_parser.py index 3aee24d6..c6fba77c 100644 --- a/openscap_report/scap_results_parser/parsers/rule_parser.py +++ b/openscap_report/scap_results_parser/parsers/rule_parser.py @@ -146,6 +146,18 @@ def _add_message_about_oval(rule_id, rules): msg = "The OVAL graph of the rule as it was displayed before the fix was performed." rules[rule_id].messages.append(msg) + @staticmethod + def set_oval_definition_id_if_is_none(rule, check_name): + if rule.oval_definition_id is None: + rule.oval_definition_id = check_name + + @staticmethod + def get_oval_check_href_name(rule_result_el): + check_ref = rule_result_el.find('.//xccdf:check/xccdf:check-content-ref', NAMESPACES) + if check_ref is None: + return None, None + return check_ref.get("href").lstrip("#"), check_ref.get("name") + def _insert_rules_results(self, rules): rules_results = self.test_results.findall('.//xccdf:rule-result', NAMESPACES) for rule_result in rules_results: @@ -154,6 +166,11 @@ def _insert_rules_results(self, rules): rules[rule_id].result = rule_result.find('.//xccdf:result', NAMESPACES).text rules[rule_id].weight = float(rule_result.get('weight')) + rules[rule_id].oval_reference, check_name = self.get_oval_check_href_name( + rule_result + ) + self.set_oval_definition_id_if_is_none(rules[rule_id], check_name) + messages = rule_result.findall('.//xccdf:message', NAMESPACES) if messages is not None: rules[rule_id].messages = [] diff --git a/openscap_report/scap_results_parser/scap_results_parser.py b/openscap_report/scap_results_parser/scap_results_parser.py index a4458f13..7ed871eb 100644 --- a/openscap_report/scap_results_parser/scap_results_parser.py +++ b/openscap_report/scap_results_parser/scap_results_parser.py @@ -62,19 +62,20 @@ def _debug_show_rules(rules): logging.debug(rule_id) logging.debug(rule) - @staticmethod - def _get_applicable_cpe_ids_for_machine(cpe_platforms_for_profile): - return [ - cpe_id for cpe_id, applicable_for_machine in cpe_platforms_for_profile.items() - if applicable_for_machine - ] - def _get_benchmark_element(self): benchmark_el = self.root.find(".//xccdf:Benchmark", NAMESPACES) if "Benchmark" in self.root.tag: benchmark_el = self.root return benchmark_el + @staticmethod + def _get_oval_definition_references(rules): + references = [] + for rule in rules.values(): + if rule.oval_reference is not None: + references.append(rule.oval_reference) + return set(tuple(references)) + def parse_report(self): test_results_el = self.root.find('.//xccdf:TestResult', NAMESPACES) benchmark_el = self._get_benchmark_element() @@ -88,10 +89,11 @@ def parse_report(self): rule_parser = RuleParser(self.root, test_results_el, self.ref_values) rules = rule_parser.get_rules() - + oval_definitions_and_results_sources = self._get_oval_definition_references(rules) OVAL_and_CPE_tree_builder = OVALAndCPETreeBuilder( # pylint: disable=C0103 self.root, group_parser, - self._get_applicable_cpe_ids_for_machine(report.profile_info.cpe_platforms_for_profile) + report.profile_info.get_list_of_cpe_platforms_that_satisfy_evaluation_target(), + oval_definitions_and_results_sources ) OVAL_and_CPE_tree_builder.insert_oval_and_cpe_trees_to_rules(rules) diff --git a/tests/test_utils.py b/tests/test_utils.py index b77035ed..c637bb5b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -91,7 +91,7 @@ def get_rules(file_path=None): def get_cpe_al_parser(file_path=PATH_TO_ARF): root = get_root(file_path) - return CPEApplicabilityLanguageParser(root) + return CPEApplicabilityLanguageParser(root, get_dummy_cpe_oval_definition()) def get_dummy_cpe_oval_definition(): diff --git a/tests/unit_tests/test_cpe_al_parser.py b/tests/unit_tests/test_cpe_al_parser.py index 43440038..b5ee84f5 100644 --- a/tests/unit_tests/test_cpe_al_parser.py +++ b/tests/unit_tests/test_cpe_al_parser.py @@ -4,7 +4,7 @@ import pytest from lxml import etree -from ..test_utils import get_cpe_al_parser, get_dummy_cpe_oval_definition +from ..test_utils import get_cpe_al_parser @pytest.mark.unit_test @@ -101,7 +101,6 @@ ) def test_get_logical_test(str_xml_element, evaluation_result): parser = get_cpe_al_parser() - parser.oval_cpe_definitions = get_dummy_cpe_oval_definition() xml_element = etree.XML( f'{str_xml_element}' )