diff --git a/ssg/constants.py b/ssg/constants.py index 6571e20710d..62f7ddac4be 100644 --- a/ssg/constants.py +++ b/ssg/constants.py @@ -477,3 +477,24 @@ DEFAULT_CHRONY_CONF_PATH = '/etc/chrony.conf' DEFAULT_AUDISP_CONF_PATH = '/etc/audit' DEFAULT_SYSCTL_REMEDIATE_DROP_IN_FILE = 'false' + + +# Constants for OVAL object model +STR_TO_BOOL = { + "false": False, + "False": False, + "true": True, + "True": True, +} + +BOOL_TO_STR = {True: "true", False: "false"} + + +class OvalNamespaces: + oval = "http://oval.mitre.org/XMLSchema/oval-common-5" + definition = oval_namespace + independent = "http://oval.mitre.org/XMLSchema/oval-definitions-5#independent" + linux = "http://oval.mitre.org/XMLSchema/oval-definitions-5#linux" + + +OVAL_NAMESPACES = OvalNamespaces() diff --git a/ssg/oval_object_model/__init__.py b/ssg/oval_object_model/__init__.py new file mode 100644 index 00000000000..bec0260f2cb --- /dev/null +++ b/ssg/oval_object_model/__init__.py @@ -0,0 +1,19 @@ +from .general import ( + ExceptionEmptyNote, + Notes, + OVALBaseObject, + OVALComponent, + OVALEntity, + OVALEntityProperty, + load_oval_entity_property, + load_notes, +) +from .oval_document import ( + ExceptionDuplicateOVALEntity, + OVALDocument, + load_oval_document, +) +from .oval_entities import ( + ExceptionDuplicateObjectReferenceInTest, + ExceptionMissingObjectReferenceInTest, +) diff --git a/ssg/oval_object_model/general.py b/ssg/oval_object_model/general.py new file mode 100644 index 00000000000..48cd4382ad9 --- /dev/null +++ b/ssg/oval_object_model/general.py @@ -0,0 +1,181 @@ +import re +from ..constants import BOOL_TO_STR, xsi_namespace +from ..xml import ElementTree + + +# ----- General functions + + +def required_attribute(_xml_el, _key): + if _key in _xml_el.attrib: + return _xml_el.get(_key) + raise ValueError( + "%s is required but was not found in:\n%s" % (_key, repr(_xml_el.attrib)) + ) + + +# ----- General Objects + + +class OVALBaseObject(object): + __namespace = "" + tag = "" + + def __init__(self, tag): + match_ns = re.match(r"\{.*\}", tag) + self.namespace = match_ns.group(0) if match_ns else "" + self.tag = tag.replace(self.namespace, "") + + @property + def namespace(self): + return self.__namespace + + @namespace.setter + def namespace(self, __value): + if isinstance(__value, str): + if not __value.startswith("{"): + __value = "{" + __value + if not __value.endswith("}"): + __value = __value + "}" + self.__namespace = __value + + def __eq__(self, __value): + return self.__dict__ == __value.__dict__ + + def __repr__(self): + return str(self.__dict__) + + def __str__(self): + return str(self.__dict__) + + def get_xml_element(self): + raise NotImplementedError + + +class OVALComponent(OVALBaseObject): + deprecated = False + notes = None + version = "0" + + def __init__(self, tag, id_): + super(OVALComponent, self).__init__(tag) + self.id_ = id_ + + def get_xml_element(self): + el = ElementTree.Element("{}{}".format(self.namespace, self.tag)) + el.set("id", self.id_) + el.set("version", self.version) + if self.deprecated: + el.set("deprecated", BOOL_TO_STR[self.deprecated]) + if self.notes: + el.append(self.notes.get_xml_element()) + return el + + +class OVALEntity(OVALComponent): + comment = "" + + def __init__(self, tag, id_, properties): + super(OVALEntity, self).__init__(tag, id_) + self.properties = properties + + def get_xml_element(self, **attributes): + el = super(OVALEntity, self).get_xml_element() + + for key, value in attributes.items(): + if "xsi" in key: + key = ElementTree.QName(xsi_namespace, key.split(":")[-1]) + el.set(key, value) + + if self.comment: + el.set("comment", self.comment) + + for property_ in self.properties: + el.append(property_.get_xml_element()) + + return el + + +# ----- OVAL Objects + + +def load_notes(oval_notes_xml_el): + if oval_notes_xml_el is None: + return None + notes = [] + for note_el in oval_notes_xml_el: + notes.append(note_el.text) + return Notes(oval_notes_xml_el.tag, note_el.tag, notes) + + +class ExceptionEmptyNote(Exception): + pass + + +class Notes(OVALBaseObject): + def __init__(self, tag, note_tag, notes): + super(Notes, self).__init__(tag) + self.note_tag = note_tag + if len(notes) == 0: + raise ExceptionEmptyNote( + "Element notes should contain at least one element note." + ) + self.notes = notes + + def get_xml_element(self): + notes_el = ElementTree.Element("{}{}".format(self.namespace, self.tag)) + for note in self.notes: + note_el = ElementTree.Element(self.note_tag) + note_el.text = note + notes_el.append(note_el) + return notes_el + + +# ----- + + +def load_property_and_notes_of_oval_entity(oval_entity_el): + notes = None + object_property = [] + for child_node_el in oval_entity_el: + if "notes" in child_node_el.tag: + notes = load_notes(child_node_el) + else: + object_property.append(load_oval_entity_property(child_node_el)) + return object_property, notes + + +def load_oval_entity_property(end_point_property_el): + data = OVALEntityProperty(end_point_property_el.tag) + data.attributes = ( + end_point_property_el.attrib if end_point_property_el.attrib else None + ) + data.text = end_point_property_el.text + for child_end_point_property_el in end_point_property_el: + data.add_child_property(load_oval_entity_property(child_end_point_property_el)) + return data + + +class OVALEntityProperty(OVALBaseObject): + attributes = None + text = None + + def __init__(self, tag): + super(OVALEntityProperty, self).__init__(tag) + self.properties = [] + + def add_child_property(self, property_): + self.properties.append(property_) + + def get_xml_element(self): + property_el = ElementTree.Element("{}{}".format(self.namespace, self.tag)) + for key, val in self.attributes.items() if self.attributes is not None else {}: + property_el.set(key, val) + + if self.text is not None: + property_el.text = self.text + + for child in self.properties: + property_el.append(child.get_xml_element()) + + return property_el diff --git a/ssg/oval_object_model/oval_document.py b/ssg/oval_object_model/oval_document.py new file mode 100644 index 00000000000..9b4d88e46ce --- /dev/null +++ b/ssg/oval_object_model/oval_document.py @@ -0,0 +1,263 @@ +from __future__ import absolute_import + +import sys + +from ..constants import oval_footer, oval_header, timestamp +from ..xml import ElementTree +from ..constants import OVAL_NAMESPACES, xsi_namespace +from .general import OVALBaseObject, required_attribute +from .oval_entities import ( + load_definition, + load_object, + load_state, + load_test, + load_variable, +) + + +def _get_xml_el(tag_name, xml_el): + el = xml_el.find("./{%s}%s" % (OVAL_NAMESPACES.definition, tag_name)) + return el if el else ElementTree.Element("empty-element") + + +def _load_definitions(oval_document, oval_document_xml_el): + for definition_el in _get_xml_el("definitions", oval_document_xml_el): + oval_document.load_definition(definition_el) + + +def _load_tests(oval_document, oval_document_xml_el): + for test_el in _get_xml_el("tests", oval_document_xml_el): + oval_document.load_test(test_el) + + +def _load_objects(oval_document, oval_document_xml_el): + for object_el in _get_xml_el("objects", oval_document_xml_el): + oval_document.load_object(object_el) + + +def _load_states(oval_document, oval_document_xml_el): + for state_el in _get_xml_el("states", oval_document_xml_el): + oval_document.load_state(state_el) + + +def _load_variables(oval_document, oval_document_xml_el): + for variable_el in _get_xml_el("variables", oval_document_xml_el): + oval_document.load_variable(variable_el) + + +def load_oval_document(oval_document_xml_el): + generator_el = oval_document_xml_el.find( + "./{%s}generator" % OVAL_NAMESPACES.definition + ) + product_name = generator_el.find("./{%s}product_name" % OVAL_NAMESPACES.oval) + schema_version = generator_el.find("./{%s}schema_version" % OVAL_NAMESPACES.oval) + product_version = generator_el.find("./{%s}product_version" % OVAL_NAMESPACES.oval) + oval_document = OVALDocument( + product_name.text, schema_version.text, product_version.text + ) + + _load_definitions(oval_document, oval_document_xml_el) + _load_tests(oval_document, oval_document_xml_el) + _load_objects(oval_document, oval_document_xml_el) + _load_states(oval_document, oval_document_xml_el) + _load_variables(oval_document, oval_document_xml_el) + + return oval_document + + +class ExceptionDuplicateOVALEntity(Exception): + pass + + +class OVALDocument(OVALBaseObject): + def __init__(self, product_name, schema_version, product_version): + self.product_name = product_name + self.schema_version = schema_version + self.product_version = product_version + + self.definitions = {} + self.tests = {} + self.objects = {} + self.states = {} + self.variables = {} + + def _get_xml_element_from_string_shorthand(self, shorthand): + valid_oval_xml_string = "{}{}{}".format( + oval_header, shorthand, oval_footer + ).encode("utf-8") + xml_element = ElementTree.fromstring(valid_oval_xml_string) + return xml_element.findall("./{%s}def-group/*" % OVAL_NAMESPACES.definition) + + def _load_element(self, xml_el): + if xml_el.tag.endswith("definition"): + self.load_definition(xml_el) + elif xml_el.tag.endswith("_test"): + self.load_test(xml_el) + elif xml_el.tag.endswith("_object"): + self.load_object(xml_el) + elif xml_el.tag.endswith("_state"): + self.load_state(xml_el) + elif xml_el.tag.endswith("_variable"): + self.load_variable(xml_el) + elif xml_el.tag is not ElementTree.Comment: + sys.stderr.write("Warning: Unknown element '{}'\n".format(xml_el.tag)) + + def load_shorthand(self, xml_string): + for xml_el in self._get_xml_element_from_string_shorthand(xml_string): + self._load_element(xml_el) + + @staticmethod + def _is_external_variable(component): + return "external_variable" in component.tag + + @staticmethod + def _handle_existing_id(component, component_dict): + # ID is identical, but OVAL entities are semantically difference => + # report and error and exit with failure + # Fixes: https://github.com/ComplianceAsCode/content/issues/1275 + if ( + component != component_dict[component.id_] + and not OVALDocument._is_external_variable(component) + and not OVALDocument._is_external_variable(component_dict[component.id_]) + ): + # This is an error scenario - since by skipping second + # implementation and using the first one for both references, + # we might evaluate wrong requirement for the second entity + # => report an error and exit with failure in that case + # See + # https://github.com/ComplianceAsCode/content/issues/1275 + # for a reproducer and what could happen in this case + raise ExceptionDuplicateOVALEntity( + ( + "ERROR: it's not possible to use the same ID: {} for two semantically" + " different OVAL entities:\nFirst entity:\n{}\nSecond entity:\n{}\n" + "Use different ID for the second entity!!!\n" + ).format( + component.id_, + str(component), + str(component_dict[component.id_]), + ) + ) + elif not OVALDocument._is_external_variable(component): + # If OVAL entity is identical, but not external_variable, the + # implementation should be rewritten each entity to be present + # just once + raise ExceptionDuplicateOVALEntity( + ( + "ERROR: OVAL ID {} is used multiple times and should represent " + "the same elements.\n Rewrite the OVAL checks. Place the identical IDs" + " into their own definition and extend this definition by it.\n" + ).format(component.id_) + ) + + @staticmethod + def _add_oval_component(component, component_dict): + if component.id_ not in component_dict: + component_dict[component.id_] = component + else: + OVALDocument._handle_existing_id(component, component_dict) + + def finalize_affected_platforms(self, env_yaml): + """ + Depending on your use-case of OVAL you may not need the + element. Such use-cases including using OVAL as a check engine for XCCDF + benchmarks. Since the XCCDF Benchmarks use cpe:platform with CPE IDs, + the affected element in OVAL definitions is redundant and just bloats the + files. This function removes all *irrelevant* affected platform elements + from given OVAL tree. It then adds one platform of the product we are + building. + """ + type_ = required_attribute(env_yaml, "type") + full_name = required_attribute(env_yaml, "full_name") + for definition in self.definitions.values(): + definition.metadata.finalize_affected_platforms(type_, full_name) + + def load_definition(self, oval_definition_xml_el): + definition = load_definition(oval_definition_xml_el) + self._add_oval_component(definition, self.definitions) + + def load_test(self, oval_test_xml_el): + test = load_test(oval_test_xml_el) + self._add_oval_component(test, self.tests) + + def load_object(self, oval_object_xml_el): + object_ = load_object(oval_object_xml_el) + self._add_oval_component(object_, self.objects) + + def load_state(self, oval_state_xml_element): + state = load_state(oval_state_xml_element) + self._add_oval_component(state, self.states) + + def load_variable(self, oval_variable_xml_element): + variable = load_variable(oval_variable_xml_element) + self._add_oval_component(variable, self.variables) + + def get_xml_element(self): + root = self._get_oval_definition_el() + root.append(self._get_generator_el()) + root.append(self._get_component_el("definitions", self.definitions.values())) + root.append(self._get_component_el("tests", self.tests.values())) + root.append(self._get_component_el("objects", self.objects.values())) + if self.states: + root.append(self._get_component_el("states", self.states.values())) + if self.variables: + root.append(self._get_component_el("variables", self.variables.values())) + return root + + def save_as_xml(self, fd): + root = self.get_xml_element() + if hasattr(ElementTree, "indent"): + ElementTree.indent(root, space=" ", level=0) + ElementTree.ElementTree(root).write(fd, xml_declaration=True, encoding="utf-8") + + def _get_component_el(self, tag, values): + xml_el = ElementTree.Element("{%s}%s" % (OVAL_NAMESPACES.definition, tag)) + for val in values: + xml_el.append(val.get_xml_element()) + return xml_el + + def _get_generator_el(self): + generator_el = ElementTree.Element("{%s}generator" % OVAL_NAMESPACES.definition) + + product_name_el = ElementTree.Element("{%s}product_name" % OVAL_NAMESPACES.oval) + product_name_el.text = self.product_name + + generator_el.append(product_name_el) + + product_version_el = ElementTree.Element( + "{%s}product_version" % OVAL_NAMESPACES.oval + ) + product_version_el.text = self.product_version + + generator_el.append(product_version_el) + + schema_version_el = ElementTree.Element( + "{%s}schema_version" % OVAL_NAMESPACES.oval + ) + schema_version_el.text = str(self.schema_version) + generator_el.append(schema_version_el) + + timestamp_el = ElementTree.Element("{%s}timestamp" % OVAL_NAMESPACES.oval) + timestamp_el.text = str(timestamp) + generator_el.append(timestamp_el) + + return generator_el + + def _get_oval_definition_el(self): + oval_definition_el = ElementTree.Element( + "{%s}oval_definitions" % OVAL_NAMESPACES.definition + ) + + oval_definition_el.set( + ElementTree.QName(xsi_namespace, "schemaLocation"), + ( + "{0} oval-common-schema.xsd {1} oval-definitions-schema.xsd" + " {1}#independent independent-definitions-schema.xsd" + " {1}#unix unix-definitions-schema.xsd" + " {1}#linux linux-definitions-schema.xsd" + ).format( + OVAL_NAMESPACES.oval, + OVAL_NAMESPACES.definition, + ), + ) + return oval_definition_el diff --git a/ssg/oval_object_model/oval_entities/__init__.py b/ssg/oval_object_model/oval_entities/__init__.py new file mode 100644 index 00000000000..cf6d804d10a --- /dev/null +++ b/ssg/oval_object_model/oval_entities/__init__.py @@ -0,0 +1,21 @@ +from .definition import ( + Affected, + Criteria, + Criterion, + Definition, + ExtendDefinition, + GeneralCriteriaNode, + Metadata, + Reference, + load_definition, +) + +from .object import ObjectOVAL, load_object +from .state import State, load_state +from .test import ( + ExceptionDuplicateObjectReferenceInTest, + ExceptionMissingObjectReferenceInTest, + Test, + load_test, +) +from .variable import Variable, load_variable diff --git a/ssg/oval_object_model/oval_entities/definition.py b/ssg/oval_object_model/oval_entities/definition.py new file mode 100644 index 00000000000..b36400abf57 --- /dev/null +++ b/ssg/oval_object_model/oval_entities/definition.py @@ -0,0 +1,342 @@ +import sys + +from ...constants import BOOL_TO_STR, OVAL_NAMESPACES, STR_TO_BOOL +from ...xml import ElementTree +from ..general import OVALBaseObject, OVALComponent, load_notes, required_attribute + + +class GeneralCriteriaNode(OVALBaseObject): + negate = False + comment = "" + applicability_check = False + + def __init__(self, tag): + super(GeneralCriteriaNode, self).__init__(tag) + + def get_xml_element(self): + el = ElementTree.Element("{}{}".format(self.namespace, self.tag)) + if self.applicability_check: + el.set("applicability_check", BOOL_TO_STR[self.applicability_check]) + if self.negate: + el.set("negate", BOOL_TO_STR[self.negate]) + if self.comment != "": + el.set("comment", self.comment) + + return el + + +def load_terminate_criteria(xml_el, class_, ref_prefix): + terminate_criteria = class_( + xml_el.tag, + required_attribute(xml_el, "{}_ref".format(ref_prefix)), + ) + terminate_criteria.negate = STR_TO_BOOL.get(xml_el.get("negate"), False) + terminate_criteria.comment = xml_el.get("comment", "") + terminate_criteria.applicability_check = STR_TO_BOOL.get( + xml_el.get("applicability_check"), False + ) + return terminate_criteria + + +class TerminateCriteriaNode(GeneralCriteriaNode): + prefix_ref = "" + + def __init__(self, tag, ref): + super(TerminateCriteriaNode, self).__init__(tag) + self.ref = ref + + def get_xml_element(self): + el = super(TerminateCriteriaNode, self).get_xml_element() + el.set("{}_ref".format(self.prefix_ref), self.ref) + return el + + +# ----- + + +class Criterion(TerminateCriteriaNode): + prefix_ref = "test" + + +# ----- + + +class ExtendDefinition(TerminateCriteriaNode): + prefix_ref = "definition" + + +# ----- + + +def load_criteria(oval_criteria_xml_el): + criteria = Criteria(oval_criteria_xml_el.tag) + criteria.operator = oval_criteria_xml_el.get("operator", "AND") + criteria.negate = STR_TO_BOOL.get(oval_criteria_xml_el.get("negate"), False) + criteria.comment = oval_criteria_xml_el.get("comment", "") + criteria.applicability_check = STR_TO_BOOL.get( + oval_criteria_xml_el.get("applicability_check"), False + ) + for child_node_el in oval_criteria_xml_el: + if child_node_el.tag.endswith("criteria"): + criteria.add_child_criteria_node(load_criteria(child_node_el)) + elif child_node_el.tag.endswith("criterion"): + criteria.add_child_criteria_node( + load_terminate_criteria(child_node_el, Criterion, "test") + ) + elif child_node_el.tag.endswith("extend_definition"): + criteria.add_child_criteria_node( + load_terminate_criteria(child_node_el, ExtendDefinition, "definition") + ) + else: + sys.stderr.write( + "Warning: Unknown element '{}'\n".format(child_node_el.tag) + ) + return criteria + + +class Criteria(GeneralCriteriaNode): + operator = "AND" + + def __init__(self, tag): + super(Criteria, self).__init__(tag) + + self.child_criteria_nodes = [] + + def add_child_criteria_node(self, child): + if not isinstance(child, (Criteria, Criterion, ExtendDefinition)): + raise TypeError("Unexpected child type of Criteria!") + self.child_criteria_nodes.append(child) + + def get_xml_element(self): + criteria_el = super(Criteria, self).get_xml_element() + + criteria_el.set("operator", self.operator) + + for child_criteria_node in self.child_criteria_nodes: + criteria_el.append(child_criteria_node.get_xml_element()) + + return criteria_el + + +# ----- + + +def load_references(all_reference_elements): + out = [] + for ref_el in all_reference_elements: + ref = Reference( + ref_el.tag, + required_attribute(ref_el, "source"), + required_attribute(ref_el, "ref_id"), + ) + ref.ref_url = ref_el.get("ref_url", "") + out.append(ref) + return out if out else None + + +class Reference(OVALBaseObject): + ref_url = "" + + def __init__(self, tag, source, ref_id): + super(Reference, self).__init__(tag) + self.source = source + self.ref_id = ref_id + + def get_xml_element(self): + reference_el = ElementTree.Element("{}{}".format(self.namespace, self.tag)) + reference_el.set("ref_id", self.ref_id) + reference_el.set("source", self.source) + if self.ref_url != "": + reference_el.set("ref_url", self.ref_url) + return reference_el + + +# ----- + + +def _get_tag(el): + if el is None: + return None + return el.tag + + +def _get_list_of_affected(affected_el, element_name): + elements = affected_el.findall( + "./{%s}%s" % (OVAL_NAMESPACES.definition, element_name) + ) + if len(elements) == 0: + return None + return [el.text for el in elements] + + +def load_affected(all_affected_elements): + out = [] + for affected_el in all_affected_elements: + affected = Affected( + affected_el.tag, + required_attribute(affected_el, "family"), + ) + affected.platform_tag = _get_tag( + affected_el.find("./{%s}platform" % OVAL_NAMESPACES.definition) + ) + affected.product_tag = _get_tag( + affected_el.find("./{%s}product" % OVAL_NAMESPACES.definition) + ) + affected.platforms = _get_list_of_affected(affected_el, "platform") + affected.products = _get_list_of_affected(affected_el, "product") + out.append(affected) + return out if out else None + + +class Affected(OVALBaseObject): + platform_tag = "platform" + product_tag = "product" + platforms = None + products = None + + def __init__(self, tag, family): + super(Affected, self).__init__(tag) + self.family = family + + def finalize_affected_platforms(self, type_, full_name): + """ + Depending on your use-case of OVAL you may not need the + element. Such use-cases including using OVAL as a check engine for XCCDF + benchmarks. Since the XCCDF Benchmarks use cpe:platform with CPE IDs, + the affected element in OVAL definitions is redundant and just bloats the + files. This function removes all *irrelevant* affected platform elements + from given OVAL tree. It then adds one platform of the product we are + building. + """ + if type_ == "platform": + self.platforms = [full_name] + + if type_ == "product": + self.products = [full_name] + + def _add_to_affected_element(self, affected_el, elements): + for platform in elements if elements is not None else []: + platform_el = ElementTree.Element(self.platform_tag) + platform_el.text = platform + affected_el.append(platform_el) + + def get_xml_element(self): + affected_el = ElementTree.Element("{}{}".format(self.namespace, self.tag)) + affected_el.set("family", self.family) + + self._add_to_affected_element(affected_el, self.platforms) + self._add_to_affected_element(affected_el, self.products) + + return affected_el + + +# ----- + + +def load_metadata(oval_metadata_xml_el): + title_el = oval_metadata_xml_el.find("./{%s}title" % OVAL_NAMESPACES.definition) + title_str = title_el.text + description_el = oval_metadata_xml_el.find( + "./{%s}description" % OVAL_NAMESPACES.definition + ) + description_str = description_el.text + all_affected_elements = oval_metadata_xml_el.findall( + "./{%s}affected" % OVAL_NAMESPACES.definition + ) + all_reference_elements = oval_metadata_xml_el.findall( + "./{%s}reference" % OVAL_NAMESPACES.definition + ) + metadata = Metadata(oval_metadata_xml_el.tag) + metadata.title = title_str + metadata.description = description_str + metadata.array_of_affected = load_affected(all_affected_elements) + metadata.array_of_references = load_references(all_reference_elements) + return metadata + + +class Metadata(OVALBaseObject): + array_of_affected = None + array_of_references = None + title = "" + description = "" + title_tag = "title" + description_tag = "description" + + def finalize_affected_platforms(self, type_, full_name): + """ + Depending on your use-case of OVAL you may not need the + element. Such use-cases including using OVAL as a check engine for XCCDF + benchmarks. Since the XCCDF Benchmarks use cpe:platform with CPE IDs, + the affected element in OVAL definitions is redundant and just bloats the + files. This function removes all *irrelevant* affected platform elements + from given OVAL tree. It then adds one platform of the product we are + building. + """ + for affected in self.array_of_affected: + affected.finalize_affected_platforms(type_, full_name) + + @staticmethod + def _add_sub_elements_from_arrays(el, array): + for item in array if array is not None else []: + el.append(item.get_xml_element()) + + def get_xml_element(self): + metadata_el = ElementTree.Element("{}{}".format(self.namespace, self.tag)) + + title_el = ElementTree.Element("{}{}".format(self.namespace, self.title_tag)) + title_el.text = self.title + metadata_el.append(title_el) + + self._add_sub_elements_from_arrays(metadata_el, self.array_of_affected) + self._add_sub_elements_from_arrays(metadata_el, self.array_of_references) + + description_el = ElementTree.Element( + "{}{}".format(self.namespace, self.description_tag) + ) + description_el.text = self.description + metadata_el.append(description_el) + + return metadata_el + + +# ----- + + +def load_definition(oval_definition_xml_el): + metadata_el = oval_definition_xml_el.find( + "./{%s}metadata" % OVAL_NAMESPACES.definition + ) + notes_el = oval_definition_xml_el.find("./{%s}notes" % OVAL_NAMESPACES.definition) + criteria_el = oval_definition_xml_el.find( + "./{%s}criteria" % OVAL_NAMESPACES.definition + ) + definition = Definition( + oval_definition_xml_el.tag, + required_attribute(oval_definition_xml_el, "id"), + required_attribute(oval_definition_xml_el, "class"), + load_metadata(metadata_el), + ) + definition.deprecated = STR_TO_BOOL.get( + oval_definition_xml_el.get("deprecated", ""), False + ) + definition.notes = load_notes(notes_el) + definition.version = required_attribute(oval_definition_xml_el, "version") + definition.criteria = load_criteria(criteria_el) + return definition + + +class Definition(OVALComponent): + criteria = None + + def __init__(self, tag, id_, class_, metadata): + super(Definition, self).__init__(tag, id_) + self.class_ = class_ + self.metadata = metadata + + def get_xml_element(self): + definition_el = super(Definition, self).get_xml_element() + definition_el.set("class", self.class_) + definition_el.append(self.metadata.get_xml_element()) + if self.criteria: + definition_el.append(self.criteria.get_xml_element()) + return definition_el diff --git a/ssg/oval_object_model/oval_entities/object.py b/ssg/oval_object_model/oval_entities/object.py new file mode 100644 index 00000000000..09c6937b1bd --- /dev/null +++ b/ssg/oval_object_model/oval_entities/object.py @@ -0,0 +1,31 @@ +from ...constants import STR_TO_BOOL +from ..general import ( + OVALEntity, + load_property_and_notes_of_oval_entity, + required_attribute, +) + + +def load_object(oval_object_xml_el): + object_property, notes = load_property_and_notes_of_oval_entity(oval_object_xml_el) + + oval_object = ObjectOVAL( + oval_object_xml_el.tag, + required_attribute(oval_object_xml_el, "id"), + object_property, + ) + oval_object.version = required_attribute(oval_object_xml_el, "version") + oval_object.comment = oval_object_xml_el.get("comment", "") + oval_object.notes = notes + oval_object.deprecated = STR_TO_BOOL.get( + oval_object_xml_el.get("deprecated", ""), False + ) + return oval_object + + +class ObjectOVAL(OVALEntity): + def __init__(self, tag, id_, properties): + super(ObjectOVAL, self).__init__(tag, id_, properties) + + def get_xml_element(self): + return super(ObjectOVAL, self).get_xml_element() diff --git a/ssg/oval_object_model/oval_entities/state.py b/ssg/oval_object_model/oval_entities/state.py new file mode 100644 index 00000000000..bb9660f917a --- /dev/null +++ b/ssg/oval_object_model/oval_entities/state.py @@ -0,0 +1,32 @@ +from ...constants import STR_TO_BOOL +from ..general import ( + OVALEntity, + load_property_and_notes_of_oval_entity, + required_attribute, +) + + +def load_state(oval_state_xml_el): + state_property, notes = load_property_and_notes_of_oval_entity(oval_state_xml_el) + + state = State( + oval_state_xml_el.tag, + required_attribute(oval_state_xml_el, "id"), + state_property, + ) + state.version = required_attribute(oval_state_xml_el, "version") + state.comment = oval_state_xml_el.get("comment", "") + state.deprecated = STR_TO_BOOL.get(oval_state_xml_el.get("deprecated", ""), False) + state.notes = notes + state.operator = oval_state_xml_el.get("operator", "AND") + return state + + +class State(OVALEntity): + operator = "AND" + + def __init__(self, tag, id_, properties): + super(State, self).__init__(tag, id_, properties) + + def get_xml_element(self): + return super(State, self).get_xml_element(operator=self.operator) diff --git a/ssg/oval_object_model/oval_entities/test.py b/ssg/oval_object_model/oval_entities/test.py new file mode 100644 index 00000000000..99775f3c5f0 --- /dev/null +++ b/ssg/oval_object_model/oval_entities/test.py @@ -0,0 +1,92 @@ +import sys + +from ...xml import ElementTree +from ...constants import OVAL_NAMESPACES, STR_TO_BOOL +from ..general import OVALComponent, load_notes, required_attribute + + +def load_test(oval_test_xml_el): + notes_el = oval_test_xml_el.find("./{%s}notes" % OVAL_NAMESPACES.definition) + test = Test( + oval_test_xml_el.tag, + required_attribute(oval_test_xml_el, "id"), + required_attribute(oval_test_xml_el, "check"), + ) + test.check_existence = oval_test_xml_el.get( + "check_existence", "at_least_one_exists" + ) + test.state_operator = oval_test_xml_el.get("state_operator", "AND") + test.comment = required_attribute(oval_test_xml_el, "comment") + test.deprecated = STR_TO_BOOL.get(oval_test_xml_el.get("deprecated", ""), False) + test.notes = load_notes(notes_el) + test.version = required_attribute(oval_test_xml_el, "version") + + for child_node_el in oval_test_xml_el: + if child_node_el.tag.endswith("object"): + test.set_object_ref(child_node_el.get("object_ref")) + test.object_ref_tag = child_node_el.tag + elif child_node_el.tag.endswith("state"): + test.add_state_ref(child_node_el.get("state_ref")) + test.state_ref_tag = child_node_el.tag + else: + sys.stderr.write( + "Warning: Unknown element '{}'\n".format(child_node_el.tag) + ) + return test + + +class ExceptionDuplicateObjectReferenceInTest(Exception): + pass + + +class ExceptionMissingObjectReferenceInTest(Exception): + pass + + +class Test(OVALComponent): + check_existence = "at_least_one_exists" + state_operator = "AND" + comment = "" + object_ref_tag = "object" + state_ref_tag = "state" + + def __init__(self, tag, id_, check): + super(Test, self).__init__(tag, id_) + self.check = check + + self.object_ref = "" + self.state_refs = [] + + def set_object_ref(self, object_ref): + if self.object_ref != "": + raise ExceptionDuplicateObjectReferenceInTest( + "Problematic OVAL test: {}".format(self.id_) + ) + self.object_ref = object_ref + + def add_state_ref(self, state_ref): + self.state_refs.append(state_ref) + + def get_xml_element(self): + test_el = super(Test, self).get_xml_element() + test_el.set("check", self.check) + test_el.set("comment", self.comment) + if self.check_existence != "at_least_one_exists": + test_el.set("check_existence", self.check_existence) + + test_el.set("state_operator", self.state_operator) + + if self.object_ref == "": + raise ExceptionMissingObjectReferenceInTest( + "Problematic OVAL test: {}".format(self.id_) + ) + object_ref_el = ElementTree.Element(self.object_ref_tag) + object_ref_el.set("object_ref", self.object_ref) + test_el.append(object_ref_el) + + for state_ref in self.state_refs: + state_ref_el = ElementTree.Element(self.state_ref_tag) + state_ref_el.set("state_ref", state_ref) + test_el.append(state_ref_el) + + return test_el diff --git a/ssg/oval_object_model/oval_entities/variable.py b/ssg/oval_object_model/oval_entities/variable.py new file mode 100644 index 00000000000..6d4c5f0177b --- /dev/null +++ b/ssg/oval_object_model/oval_entities/variable.py @@ -0,0 +1,35 @@ +from ...constants import STR_TO_BOOL +from ..general import ( + OVALEntity, + load_property_and_notes_of_oval_entity, + required_attribute, +) + + +def load_variable(oval_variable_xml_el): + variable_property, notes = load_property_and_notes_of_oval_entity( + oval_variable_xml_el + ) + + variable = Variable( + oval_variable_xml_el.tag, + required_attribute(oval_variable_xml_el, "id"), + required_attribute(oval_variable_xml_el, "datatype"), + variable_property, + ) + variable.comment = oval_variable_xml_el.get("comment", "") + variable.deprecated = STR_TO_BOOL.get( + oval_variable_xml_el.get("deprecated", ""), False + ) + variable.notes = notes + variable.version = required_attribute(oval_variable_xml_el, "version") + return variable + + +class Variable(OVALEntity): + def __init__(self, tag, id_, data_type, properties): + super(Variable, self).__init__(tag, id_, properties) + self.data_type = data_type + + def get_xml_element(self): + return super(Variable, self).get_xml_element(datatype=self.data_type) diff --git a/ssg/xml.py b/ssg/xml.py index ee0d455a62a..0185431c676 100644 --- a/ssg/xml.py +++ b/ssg/xml.py @@ -37,12 +37,14 @@ def oval_generated_header(product_name, schema_version, ssg_version): schema_version, timestamp) -def register_namespaces(): +def register_namespaces(ns=None): """ Register all possible namespaces """ try: - for prefix, uri in PREFIX_TO_NS.items(): + if ns is None: + ns = PREFIX_TO_NS + for prefix, uri in ns.items(): ElementTree.register_namespace(prefix, uri) except Exception: # Probably an old version of Python @@ -50,6 +52,24 @@ def register_namespaces(): pass +def get_namespaces_from(file): + """ + Return dictionary of namespaces in file. Return empty dictionary in case of error. + """ + result = {} + try: + result = { + key: value + for _, (key, value) in ElementTree.iterparse(file, events=["start-ns"]) + } + except Exception: + # Probably an old version of Python + # Doesn't matter, as this is non-essential. + pass + finally: + return result + + def open_xml(filename): """ Given a filename, register all possible namespaces, and return the XML tree. diff --git a/tests/unit/ssg-module/data/minimal_oval_of_oval_ssg-sshd_rekey_limit_def.xml b/tests/unit/ssg-module/data/minimal_oval_of_oval_ssg-sshd_rekey_limit_def.xml new file mode 100644 index 00000000000..29ceda318dc --- /dev/null +++ b/tests/unit/ssg-module/data/minimal_oval_of_oval_ssg-sshd_rekey_limit_def.xml @@ -0,0 +1,186 @@ + + + + combine_ovals.py from SCAP Security Guide + ssg: [0, 1, 70], python: 3.11.4 + 5.11 + 2023-09-09T16:41:20 + + + + + Force frequent session key renegotiation + + Fedora + + + Ensure RekeyLimit is configured with the appropriate value in /etc/ssh/sshd_config or in /etc/ssh/sshd_config.d + + + + + + + + + + + + + + + + + + + Install the OpenSSH Server Package + + Fedora + + + The RPM package openssh-server should be installed. + + + + + + + + Remove the OpenSSH Server Package + + Fedora + + + The RPM package openssh-server should be removed. + + + + + + + + SSHD is not required to be installed or requirement not set + + Fedora + + + If SSHD is not required, we check it is not installed. If SSH requirement is unset, we are good. + + + + + + + + + SSHD is required to be installed or requirement not set + + Fedora + + + If SSHD is required, we check it is installed. If SSH requirement is unset, we are good. + + + + + + + + + It doesn't matter if sshd is installed or not + + Fedora + + + Test if value sshd_required is 0. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /etc/ssh/sshd_config + ^[\s]*RekeyLimit[\s]+(.*)$ + 1 + + + /etc/ssh/sshd_config.d + .*\.conf$ + ^[\s]*RekeyLimit[\s]+(.*)$ + 1 + + + openssh-server + + + openssh-server + + + oval:ssg-sshd_required:var:1 + + + oval:ssg-sshd_required:var:1 + + + oval:ssg-sshd_required:var:1 + + + + + + + + 1 + + + 2 + + + 0 + + + + + + ^ + + [\s]+ + + [\s]*$ + + + + + + + diff --git a/tests/unit/ssg-module/data/shorthand_with_all_components.xml b/tests/unit/ssg-module/data/shorthand_with_all_components.xml new file mode 100644 index 00000000000..7883bf5f4bd --- /dev/null +++ b/tests/unit/ssg-module/data/shorthand_with_all_components.xml @@ -0,0 +1,62 @@ + + + + + An SELinux Context must be configured for the pam_faillock.so records directory + + Oracle Linux 9 + Fedora + Red Hat Enterprise Linux 8 + Oracle Linux 8 + Red Hat Enterprise Linux 9 + + An SELinux Context must be configured for the Faillock directory. + + + + + + + + /etc/pam.d/password-auth|/etc/pam.d/system-auth|/etc/security/faillock.conf + ^\s*(?:auth.*pam_faillock\.so.*)?dir\s*=\s*(\S+) + 1 + + + + + + + + + + + + + + + + + + faillog_t + + + + + + + + var_account_password_selinux_faillock_dir_collector + + diff --git a/tests/unit/ssg-module/test_oval_object_model/test_load_and_store.py b/tests/unit/ssg-module/test_oval_object_model/test_load_and_store.py new file mode 100644 index 00000000000..cc939e36b4f --- /dev/null +++ b/tests/unit/ssg-module/test_oval_object_model/test_load_and_store.py @@ -0,0 +1,397 @@ +import os +import pytest +import tempfile +from ssg.xml import ElementTree + +from ssg.oval_object_model import load_oval_document, OVALDocument, OVALEntityProperty +from ssg.oval_object_model.oval_entities import Criterion, ExtendDefinition +from ssg.constants import OVAL_NAMESPACES +from ssg.xml import open_xml, get_namespaces_from, register_namespaces + + +DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "data")) +OVAL_DOCUMENT_PATH = os.path.join( + DATA_DIR, "minimal_oval_of_oval_ssg-sshd_rekey_limit_def.xml" +) +TEST_BUILD_DIR = tempfile.mkdtemp() + + +def _load_oval_document(path): + root_el = open_xml(path) + ns = get_namespaces_from(path) + register_namespaces(ns) + return load_oval_document(root_el) + + +def _read_shorthand(path_): + if not os.path.isdir(path_): + with open(path_, "r") as fd: + return fd.read() + return "" + + +def _list_of_shorthands(): + out = [] + dir_with_shorthands = os.path.join(DATA_DIR, "group_dir/rule_dir/oval") + + static_paths_to_shorthands = [ + os.path.join(DATA_DIR, "shorthand_with_all_components.xml"), + ] + for file_name in os.listdir(dir_with_shorthands): + static_paths_to_shorthands.append(os.path.join(dir_with_shorthands, file_name)) + + for path_ in static_paths_to_shorthands: + out.append(_read_shorthand(path_)) + + return out + + +@pytest.fixture +def oval_document(): + return _load_oval_document(OVAL_DOCUMENT_PATH) + + +@pytest.fixture +def oval_document_from_shorthand(): + oval_doc = OVALDocument("Test", "1.2", "1") + + for shorthand in _list_of_shorthands(): + oval_doc.load_shorthand(shorthand) + + return oval_doc + + +@pytest.fixture +def definition(oval_document): + return oval_document.definitions.get("oval:ssg-sshd_rekey_limit:def:1") + + +@pytest.fixture +def metadata(definition): + return definition.metadata + + +@pytest.fixture +def criteria(oval_document): + return oval_document.definitions.get( + "oval:ssg-sshd_not_required_or_unset:def:1" + ).criteria + + +@pytest.fixture +def text_file_content_test(oval_document): + return oval_document.tests.get("oval:ssg-test_sshd_rekey_limit_config_dir:tst:1") + + +@pytest.fixture +def rpm_info_test(oval_document): + return oval_document.tests.get( + "oval:ssg-test_package_openssh-server_installed:tst:1" + ) + + +@pytest.fixture +def text_file_content_object(oval_document): + return oval_document.objects.get("oval:ssg-obj_sshd_rekey_limit:obj:1") + + +@pytest.fixture +def rpm_info_object(oval_document): + return oval_document.objects.get( + "oval:ssg-obj_test_package_openssh-server_installed:obj:1" + ) + + +@pytest.fixture +def local_variable(oval_document): + return oval_document.variables.get("oval:ssg-sshd_line_regex:var:1") + + +@pytest.fixture +def external_variable(oval_document): + return oval_document.variables.get("oval:ssg-var_rekey_limit_size:var:1") + + +@pytest.fixture +def text_file_content_state(oval_document): + return oval_document.states.get("oval:ssg-state_sshd_rekey_limit:ste:1") + + +@pytest.fixture +def variable_state(oval_document): + return oval_document.states.get("oval:ssg-state_sshd_not_required:ste:1") + + +# Tests: + + +def test_load_oval_document(oval_document): + assert "oval:ssg-sshd_rekey_limit:def:1" in oval_document.definitions + assert "oval:ssg-test_sshd_rekey_limit:tst:1" in oval_document.tests + assert "oval:ssg-obj_sshd_rekey_limit:obj:1" in oval_document.objects + assert "oval:ssg-var_rekey_limit_time:var:1" in oval_document.variables + assert "oval:ssg-state_sshd_required:ste:1" in oval_document.states + + +def _assert_oval_document_str(oval_document_str): + assert "oval:ssg-sshd_rekey_limit:def:1" in oval_document_str + assert "oval:ssg-test_sshd_rekey_limit:tst:1" in oval_document_str + assert ( + "oval:ssg-obj_test_package_openssh-server_installed:obj:1" in oval_document_str + ) + assert "combine_ovals.py from SCAP Security Guide" in oval_document_str + assert 'var_ref="oval:ssg-var_rekey_limit_size:var:1" ' in oval_document_str + assert "/etc/ssh/sshd_config" in oval_document_str + + +def test_get_xml_element(oval_document): + oval_document_el = oval_document.get_xml_element() + oval_document_str = ElementTree.tostring(oval_document_el).decode() + _assert_oval_document_str(oval_document_str) + + +def test_save_as_xml(oval_document): + oval_doc_path = os.path.join(TEST_BUILD_DIR, "oval.xml") + with open(oval_doc_path, "wb") as fd: + oval_document.save_as_xml(fd) + + with open(oval_doc_path, "r") as fd: + oval_document_str = fd.read() + _assert_oval_document_str(oval_document_str) + + +def test_load_shorthands(oval_document_from_shorthand): + assert "service_chronyd_or_ntpd_enabled" in oval_document_from_shorthand.definitions + assert "sudo_remove_nopasswd" in oval_document_from_shorthand.definitions + + assert "test_nopasswd_etc_sudoers" in oval_document_from_shorthand.tests + assert "test_nopasswd_etc_sudoers_d" in oval_document_from_shorthand.tests + + assert "object_nopasswd_etc_sudoers_d" in oval_document_from_shorthand.objects + assert "object_nopasswd_etc_sudoers" in oval_document_from_shorthand.objects + + assert ( + "var_account_password_selinux_faillock_dir_collector" + in oval_document_from_shorthand.variables + ) + assert ( + "state_account_password_selinux_faillock_dir" + in oval_document_from_shorthand.states + ) + + +def test_content_definition(definition): + assert definition.id_ == "oval:ssg-sshd_rekey_limit:def:1" + assert definition.class_ == "compliance" + assert definition.version == "1" + assert definition.criteria is not None + + +def test_content_metadata(metadata): + assert "RekeyLimit" in metadata.description + assert "session key" in metadata.title + assert len(metadata.array_of_affected) == 1 + assert len(metadata.array_of_references) == 1 + + affected = metadata.array_of_affected[0] + assert affected.family == "unix" + assert "Fedora" in affected.platforms + assert affected.products is None + + for reference in metadata.array_of_references: + assert reference.source == "ssg" + assert reference.ref_id == "sshd_rekey_limit" + + +def test_content_criteria(criteria, oval_document): + assert criteria.operator == "OR" + assert not criteria.negate + assert criteria.comment == "SSH not required or not set" + assert criteria.child_criteria_nodes + + for child_criteria_node in criteria.child_criteria_nodes: + if isinstance(child_criteria_node, Criterion): + assert child_criteria_node.ref == "oval:ssg-test_sshd_not_required:tst:1" + assert child_criteria_node.prefix_ref == "test" + assert child_criteria_node.ref in oval_document.tests + assert not child_criteria_node.negate + assert child_criteria_node.comment == "" + if isinstance(child_criteria_node, ExtendDefinition): + assert child_criteria_node.ref == "oval:ssg-sshd_requirement_unset:def:1" + assert child_criteria_node.prefix_ref == "definition" + assert child_criteria_node.ref in oval_document.definitions + assert not child_criteria_node.negate + assert child_criteria_node.comment == "SSH requirement is unset" + + +def test_content_text_file_content_test(text_file_content_test): + assert "textfilecontent54_test" in text_file_content_test.tag + assert ( + text_file_content_test.id_ == "oval:ssg-test_sshd_rekey_limit_config_dir:tst:1" + ) + assert text_file_content_test.version == "1" + assert text_file_content_test.check == "all" + assert ( + text_file_content_test.comment + == "tests the value of RekeyLimit setting in SSHD config directory" + ) + assert not text_file_content_test.deprecated + assert text_file_content_test.check_existence == "all_exist" + assert text_file_content_test.state_operator == "AND" + + assert "object" in text_file_content_test.object_ref_tag + assert ( + "oval:ssg-obj_sshd_rekey_limit_config_dir:obj:1" + in text_file_content_test.object_ref + ) + assert "state" in text_file_content_test.state_ref_tag + assert "oval:ssg-state_sshd_rekey_limit:ste:1" in text_file_content_test.state_refs + + +def test_content_rpm_info_test(rpm_info_test): + assert "rpminfo_test" in rpm_info_test.tag + assert rpm_info_test.id_ == "oval:ssg-test_package_openssh-server_installed:tst:1" + assert rpm_info_test.version == "1" + assert rpm_info_test.check == "all" + assert rpm_info_test.comment == "package openssh-server is installed" + assert not rpm_info_test.deprecated + assert rpm_info_test.check_existence == "all_exist" + assert rpm_info_test.state_operator == "AND" + + assert "object" in rpm_info_test.object_ref_tag + assert not rpm_info_test.state_refs + assert ( + "oval:ssg-obj_test_package_openssh-server_installed:obj:1" + == rpm_info_test.object_ref + ) + + +def test_content_info_rpm_object(rpm_info_object): + assert "rpminfo_object" in rpm_info_object.tag + assert ( + rpm_info_object.id_ + == "oval:ssg-obj_test_package_openssh-server_installed:obj:1" + ) + assert rpm_info_object.version == "1" + assert rpm_info_object.comment == "" + assert not rpm_info_object.deprecated + assert len(rpm_info_object.properties) == 1 + + name_property = OVALEntityProperty("{{{}}}name".format(OVAL_NAMESPACES.linux)) + name_property.text = "openssh-server" + name_property.attributes = None + + assert rpm_info_object.properties[0] == name_property + + +def test_content_text_file_content_object(text_file_content_object): + assert "textfilecontent54_object" in text_file_content_object.tag + assert text_file_content_object.id_ == "oval:ssg-obj_sshd_rekey_limit:obj:1" + assert text_file_content_object.version == "1" + assert text_file_content_object.comment == "" + assert not text_file_content_object.deprecated + assert text_file_content_object.properties + + property_file_path = OVALEntityProperty("filepath") + property_file_path.namespace = OVAL_NAMESPACES.independent + property_file_path.attributes = None + property_file_path.text = "/etc/ssh/sshd_config" + property_pattern = OVALEntityProperty("pattern") + property_pattern.namespace = OVAL_NAMESPACES.independent + property_pattern.attributes = {"operation": "pattern match"} + property_pattern.text = r"^[\s]*RekeyLimit[\s]+(.*)$" + property_instance = OVALEntityProperty("instance") + property_instance.namespace = OVAL_NAMESPACES.independent + property_instance.attributes = { + "datatype": "int", + "operation": "greater than or equal", + } + property_instance.text = "1" + properties = [property_file_path, property_pattern, property_instance] + + assert all(i in properties for i in text_file_content_object.properties) + + +def test_content_external_variable(external_variable): + assert external_variable.id_ == "oval:ssg-var_rekey_limit_size:var:1" + assert external_variable.version == "1" + assert external_variable.comment == "Size component of the rekey limit" + assert external_variable.data_type == "string" + assert not external_variable.properties + + +def test_content_local_variable(local_variable): + assert local_variable.id_ == "oval:ssg-sshd_line_regex:var:1" + assert local_variable.version == "1" + assert local_variable.comment == "The regex of the directive" + assert local_variable.data_type == "string" + assert len(local_variable.properties) == 1 + concat = local_variable.properties.pop() + assert "concat" in concat.tag + assert len(concat.properties) == 5 + + property_literal_component = OVALEntityProperty("literal_component") + property_literal_component.namespace = OVAL_NAMESPACES.definition + property_literal_component.attributes = None + property_literal_component.text = "^" + property_variable_component = OVALEntityProperty("variable_component") + property_variable_component.namespace = OVAL_NAMESPACES.definition + property_variable_component.attributes = { + "var_ref": "oval:ssg-var_rekey_limit_size:var:1" + } + property_variable_component.text = None + property_literal_component_1 = OVALEntityProperty("literal_component") + property_literal_component_1.namespace = OVAL_NAMESPACES.definition + property_literal_component_1.attributes = None + property_literal_component_1.text = r"[\s]+" + property_variable_component_1 = OVALEntityProperty("variable_component") + property_variable_component_1.namespace = OVAL_NAMESPACES.definition + property_variable_component_1.attributes = { + "var_ref": "oval:ssg-var_rekey_limit_time:var:1" + } + property_variable_component_1.text = None + property_literal_component_2 = OVALEntityProperty("literal_component") + property_literal_component_2.namespace = OVAL_NAMESPACES.definition + property_literal_component_2.attributes = None + property_literal_component_2.text = r"[\s]*$" + properties = [ + property_literal_component, + property_variable_component, + property_literal_component_1, + property_variable_component_1, + property_literal_component_2, + ] + assert all(i in properties for i in concat.properties) + + +def test_content_text_file_content_state(text_file_content_state): + assert text_file_content_state.id_ == "oval:ssg-state_sshd_rekey_limit:ste:1" + assert text_file_content_state.version == "1" + assert text_file_content_state.comment == "" + assert text_file_content_state.operator == "AND" + assert len(text_file_content_state.properties) == 1 + + property_subexpression = OVALEntityProperty("subexpression") + property_subexpression.namespace = OVAL_NAMESPACES.independent + property_subexpression.attributes = { + "operation": "pattern match", + "var_ref": "oval:ssg-sshd_line_regex:var:1", + } + property_subexpression.text = None + + assert text_file_content_state.properties[0] == property_subexpression + + +def test_content_variable_state(variable_state): + assert variable_state.id_ == "oval:ssg-state_sshd_not_required:ste:1" + assert variable_state.version == "1" + assert variable_state.comment == "" + assert variable_state.operator == "AND" + assert len(variable_state.properties) == 1 + + property_value = OVALEntityProperty("value") + property_value.namespace = OVAL_NAMESPACES.independent + property_value.attributes = {"operation": "equals", "datatype": "int"} + property_value.text = "1" + + assert variable_state.properties[0] == property_value