ComplianceAsCode/content

View on GitHub
ssg/boolean_expression.py

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
from ssg.ext.boolean import boolean
from ssg import requirement_specs

# We don't support ~= to avoid confusion with boolean operator NOT (~)
SPEC_SYMBOLS = ['<', '>', '=', '!', ',', '[', ']']
VERSION_SYMBOLS = ['.', '-', '_', ":"]


class Function(boolean.Function):
    """
    Base class for boolean functions

    Subclass it and pass to the `Algebra` as `function_cls` to enrich
    expression elements with domain-specific methods.

    Provides `is_and`, `is_or` and `is_not` methods to distinguish instances
    between different boolean functions.

    The `as_id` method will generate a unique string identifier usable as
    an XML id based on the properties of the entity.
    """

    def is_and(self):
        return isinstance(self, boolean.AND)

    def is_or(self):
        return isinstance(self, boolean.OR)

    def is_not(self):
        return isinstance(self, boolean.NOT)

    def as_id(self):
        if self.is_not():
            return 'not_{0}'.format(self.args[0].as_id())
        op = 'unknown_bool_op'
        if self.is_and():
            op = 'and'
        if self.is_or():
            op = 'or'
        return '_{0}_'.format(op).join([arg.as_id() for arg in self.args])


class Symbol(boolean.Symbol):
    """
    Base class for boolean symbols

    Subclass it and pass to the `Algebra` as `symbol_cls` to enrich
    expression elements with domain-specific methods.

    The `as_id` method will generate a unique string identifier usable as
    an XML id based on the properties of the entity.
    """

    def __init__(self, obj):
        super(Symbol, self).__init__(obj)
        self.requirement = requirement_specs.Requirement(obj)
        self.obj = self.requirement

    def __call__(self, **kwargs):
        full_name = self.name
        if self.arg:
            full_name += '[' + self.arg + ']'
        val = kwargs.get(full_name, False)
        if self.requirement.has_version_specs():
            if type(val) is str:
                return val in self.requirement
            return False
        return bool(val)

    def __hash__(self):
        return hash(self.as_id())

    def __eq__(self, other):
        return hash(self) == hash(other)

    def __lt__(self, other):
        return self.as_id() < other.as_id()

    def as_id(self):
        id_str = self.name
        if self.arg:
            id_str += '_' + self.arg
        if self.requirement.has_version_specs():
            id_str += '_' + self.requirement.ver_specs.oval_id
        return id_str

    def as_dict(self):
        res = {
            'id': self.as_id(),
            'name': self.name,
            'arg': self.arg,
            'ver_specs': [],
            'ver_specs_id': '',
            'ver_specs_cpe': '',
            'ver_specs_title': '',
        }

        if self.requirement.has_version_specs():
            for ver_spec in sorted(self.requirement.ver_specs):
                res['ver_specs'].append({
                    'id': ver_spec.oval_id,
                    'op': ver_spec.op,
                    'ver': ver_spec.ver,
                    'evr_op': ver_spec.evr_op,
                    'evr_ver': ver_spec.evr_ver,
                    'ev_ver': ver_spec.ev_ver
                })
            res['ver_specs_id'] = self.requirement.ver_specs.oval_id
            res['ver_specs_cpe'] = self.requirement.ver_specs.cpe_id
            res['ver_specs_title'] = self.requirement.ver_specs.title

        return res

    def has_version_specs(self):
        return self.requirement.has_version_specs()

    @property
    def arg(self):
        return self.requirement.arg or ''

    @property
    def name(self):
        return self.requirement.name

    @staticmethod
    def is_parametrized(name):
        return requirement_specs.Requirement.is_parametrized(name)

    @staticmethod
    def get_base_of_parametrized_name(name):
        return requirement_specs.Requirement.get_base_for_parametrized(name)


class Algebra(boolean.BooleanAlgebra):
    """
    Base class for boolean algebra

    Algebra class will parse and evaluate boolean expressions,
    where operators could be any combination of "~, &, |, !, *, +, not, and, or"
    and variable symbols could contain version specifiers as described
    in PEP440 and PEP508.

    Limitations:
    - no white space is allowed inside specifier expressions;
    - ~= specifier operator is not supported.

    For example: "(oranges>=2.0.8,<=5 | fried[banana]) and !pie[apple]"
    """

    def __init__(self, symbol_cls, function_cls):
        not_cls = type('FunctionNOT', (function_cls, boolean.NOT), {})
        and_cls = type('FunctionAND', (function_cls, boolean.AND), {})
        or_cls = type('FunctionOR', (function_cls, boolean.OR), {})
        super(Algebra, self).__init__(allowed_in_token=VERSION_SYMBOLS+SPEC_SYMBOLS,
                                      Symbol_class=symbol_cls,
                                      NOT_class=not_cls, AND_class=and_cls, OR_class=or_cls)