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

Prepare OVAL object model for integration #11206

Merged
10 changes: 2 additions & 8 deletions ssg/build_ovals.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from . import utils, products
from .utils import mkdir_p
from .xml import ElementTree, oval_generated_header
from .oval_object_model import get_product_name


def _create_subtree(shorthand_tree, category):
Expand Down Expand Up @@ -74,14 +75,7 @@ def _check_is_applicable_for_product(oval_check_def, product):

for afftype in affected_type_elements:
# Get official name for product (prefixed with content of afftype)
product_name = afftype + utils.map_name(product)
# Append the product version to the official name
if product_version is not None:
# Some product versions have a dot in between the numbers
# While the prodtype doesn't have the dot, the full product name does
if product == "ubuntu" or product == "macos":
product_version = product_version[:2] + "." + product_version[2:]
product_name += ' ' + product_version
product_name = afftype + get_product_name(product, product_version)

# Test if this OVAL check is for the concrete product version
if product_name in oval_check_def:
Expand Down
10 changes: 4 additions & 6 deletions ssg/oval_object_model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@
OVALComponent,
OVALEntity,
OVALEntityProperty,
load_oval_entity_property,
load_notes,
get_product_name,
load_oval_entity_property,
)
from .oval_document import (
ExceptionDuplicateOVALEntity,
OVALDocument,
load_oval_document,
)
from .oval_container import ExceptionDuplicateOVALEntity
from .oval_document import MissingOVALComponent, OVALDocument, load_oval_document
from .oval_entities import (
ExceptionDuplicateObjectReferenceInTest,
ExceptionMissingObjectReferenceInTest,
Expand Down
41 changes: 40 additions & 1 deletion ssg/oval_object_model/general.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import re

from ..constants import BOOL_TO_STR, xsi_namespace
from ..xml import ElementTree

from .. import utils

# ----- General functions

Expand All @@ -14,6 +15,28 @@ def required_attribute(_xml_el, _key):
)


def get_product_name(product, product_version=None):
# Current SSG checks aren't unified which element of '<platform>'
# and '<product>' to use as OVAL AffectedType metadata element,
# e.g. Chromium content uses both of them across the various checks
# Thus for now check both of them when checking concrete platform / product

# Get official name for product (prefixed with content of afftype)
product_name = utils.map_name(product)

# Append the product version to the official name
if product_version is not None:
product_name += " " + utils.get_fixed_product_version(product, product_version)
return product_name


def is_product_name_in(list_, product_name):
for item in list_ if list_ is not None else []:
if product_name in item:
return True
return False


# ----- General Objects


Expand Down Expand Up @@ -79,6 +102,12 @@ def __init__(self, tag, id_, properties):
super(OVALEntity, self).__init__(tag, id_)
self.properties = properties

def _get_references(self, key):
out = []
for property_ in self.properties:
out.extend(property_.get_values_by_key(key))
return out

def get_xml_element(self, **attributes):
el = super(OVALEntity, self).get_xml_element()

Expand Down Expand Up @@ -179,3 +208,13 @@ def get_xml_element(self):
property_el.append(child.get_xml_element())

return property_el

def get_values_by_key(self, key):
out = []
if self.attributes and key in self.attributes:
out.append(self.attributes.get(key))
if key in self.tag:
out.append(self.text)
for property_ in self.properties:
out.extend(property_.get_values_by_key(key))
return out
224 changes: 224 additions & 0 deletions ssg/oval_object_model/oval_container.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import logging

from .general import OVALBaseObject
from .oval_definition_references import OVALDefinitionReference
from .oval_entities import (
load_definition,
load_object,
load_state,
load_test,
load_variable,
)


class ExceptionDuplicateOVALEntity(Exception):
pass


def _is_external_variable(component):
return "external_variable" in component.tag


def _handle_existing_id(component, component_dict):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function looks very similar to a part of append function in ssg/build_ovals.py. You should first extract the common code out to a function and then call that function both here and there to prevent unwanted code duplication and responsibility duplication.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the next PR, I plan to rework ssg/build_ovals.py and combine_ovals.py and remove some duplicated and unused code. So this duplication will be resolved.

# ID is identical, but OVAL entities are semantically different =>
# report and error and exit with failure
# Fixes: https://github.com/ComplianceAsCode/content/issues/1275
if (
component != component_dict[component.id_]
and not _is_external_variable(component)
and not _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 _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
logging.info(
(
"OVAL ID {} is used multiple times and should represent "
"the same elements.\nRewrite the OVAL checks. Place the identical IDs"
" into their own definition and extend this definition by it.\n"
).format(component.id_)
)


def add_oval_component(component, component_dict):
if component.id_ not in component_dict:
component_dict[component.id_] = component
else:
_handle_existing_id(component, component_dict)


def _copy_component(destination, source_of_components):
for component in source_of_components.values():
add_oval_component(component, destination)


def _remove_keys_from_dict(to_remove, dict_):
for k in to_remove:
del dict_[k]


def _keep_keys_in_dict(dict_, to_keep):
to_remove = [key for key in dict_ if key not in to_keep]
_remove_keys_from_dict(to_remove, dict_)


def _save_referenced_vars(ref, entity):
ref.save_variables(entity.get_variable_references())


def _save_definitions_references(ref, definition):
if definition.criteria:
ref.save_tests(definition.criteria.get_test_references())
ref.save_definitions(definition.criteria.get_extend_definition_references())


def _save_test_references(ref, test):
ref.save_object(test.object_ref)
ref.save_states(test.state_refs)


def _save_object_references(ref, object_):
_save_referenced_vars(ref, object_)
ref.save_states(object_.get_state_references())
ref.save_objects(object_.get_object_references())


def _save_variable_references(ref, variable):
_save_referenced_vars(ref, variable)
ref.save_objects(variable.get_object_references())


class OVALContainer(OVALBaseObject):
def __init__(self):
self.definitions = {}
self.tests = {}
self.objects = {}
self.states = {}
self.variables = {}

def _call_function_for_every_component(self, _function, object_):
_function(self.definitions, object_.definitions)
_function(self.tests, object_.tests)
_function(self.objects, object_.objects)
_function(self.states, object_.states)
_function(self.variables, object_.variables)

def load_definition(self, oval_definition_xml_el):
definition = load_definition(oval_definition_xml_el)
add_oval_component(definition, self.definitions)

def load_test(self, oval_test_xml_el):
test = load_test(oval_test_xml_el)
add_oval_component(test, self.tests)

def load_object(self, oval_object_xml_el):
object_ = load_object(oval_object_xml_el)
add_oval_component(object_, self.objects)

def load_state(self, oval_state_xml_element):
state = load_state(oval_state_xml_element)
add_oval_component(state, self.states)

def load_variable(self, oval_variable_xml_element):
variable = load_variable(oval_variable_xml_element)
add_oval_component(variable, self.variables)

def add_content_of_container(self, container):
self._call_function_for_every_component(_copy_component, container)

@staticmethod
def _skip_if_is_none(value, component_id):
raise NotImplementedError()

def _process_component(self, ref, type_, function_save_refs):
MAP_COMPONENT_DICT = {
"definitions": self.definitions,
"tests": self.tests,
"objects": self.objects,
"states": self.states,
"variables": self.variables,
}
source = MAP_COMPONENT_DICT.get(type_)
to_process, id_getter = ref.get_to_process_dict_and_id_getter(type_)
while to_process:
id_ = id_getter()
entity = source.get(id_)
if self._skip_if_is_none(entity, id_):
continue
function_save_refs(ref, entity)

def _process_definition_references(self, ref):
self._process_component(
ref,
"definitions",
_save_definitions_references,
)

def _process_test_references(self, ref):
self._process_component(
ref,
"tests",
_save_test_references,
)

def _process_object_references(self, ref):
self._process_component(
ref,
"objects",
_save_object_references,
)

def _process_state_references(self, ref):
self._process_component(
ref,
"states",
_save_referenced_vars,
)

def _process_variable_references(self, ref):
self._process_component(
ref,
"variables",
_save_variable_references,
)

def _process_objects_states_variables_references(self, ref):
while (
ref.to_process_objects or ref.to_process_states or ref.to_process_variables
):
self._process_object_references(ref)
self._process_state_references(ref)
self._process_variable_references(ref)

def get_all_references_of_definition(self, definition_id):
if definition_id not in self.definitions:
raise ValueError(
"ERROR: OVAL definition '{}' doesn't exist.".format(definition_id)
)
ref = OVALDefinitionReference(definition_id)
self._process_definition_references(ref)
self._process_test_references(ref)
self._process_objects_states_variables_references(ref)
return ref

def keep_referenced_components(self, ref):
self._call_function_for_every_component(_keep_keys_in_dict, ref)
Loading