Source code for stix2generator.generation.constraints

import random
import re

import stix2generator.exceptions
import stix2generator.utils


[docs]class ValueConstraintOperators: """ A mixin/base-class for the constraint classes, which contains some convenience constants et al for operators. """ # Legal operators EQ, NE, GT, GE, LT, LE = OPERATORS = range(6) # Reversed operators, aligned with OPERATORS such that the reverse of # OPERATORS[i] is REVERSE_OPERATORS[i]. (EQ and NE are symmetric, so they # are their own reverses.) REVERSE_OPERATORS = EQ, NE, LT, LE, GT, GE STRING_OPERATOR_MAP = { "=": EQ, "!=": NE, ">": GT, ">=": GE, "<": LT, "<=": LE } OPERATOR_STRING_MAP = { v: k for (k, v) in STRING_OPERATOR_MAP.items() }
[docs] @staticmethod def to_operator(operator): """ Normalize a value to an operator constant. If a string is passed, convert it to an operator constant. Otherwise, assume it is already an operator constant and just return it unchanged. :param operator: An operator string or operator constant :return: An operator constant """ if isinstance(operator, str): return ValueConstraintOperators.STRING_OPERATOR_MAP[operator] else: return operator
[docs]class ValueConstraint(ValueConstraintOperators): """ Represents a constraint on a property. E.g. that it be less than some value. Currently this just consists of an operator and a value. The relationship to the property to be constrained is maintained externally, so it is not stored in these instances. """ def __init__(self, operator, value): self.operator = self.to_operator(operator) self.value = value def __str__(self): return "{} {}".format( self.OPERATOR_STRING_MAP[self.operator], self.value )
[docs]class ValueCoconstraint(ValueConstraintOperators): """ Represents a co-constraint between two properties. This just stores two property names and an operator. When the value of one property becomes known, a constraint can be derived for the other property. """ def __init__(self, prop_name_left, operator, prop_name_right): self.operator = self.to_operator(operator) self.prop_name_left = prop_name_left self.prop_name_right = prop_name_right
[docs] def get_constraint(self, prop_name, prop_value): """ Given the value of one property of this co-constraint, derive a constraint on the other. :param prop_name: The name of one property of this co-constraint :param prop_value: The value of prop_name :return: A ValueConstraint object representing a constraint on the other property. :raises ValueError: If the property name given isn't a part of this co-constraint. """ if prop_name == self.prop_name_left: constraint = ValueConstraint( self.REVERSE_OPERATORS[self.operator], prop_value ) elif prop_name == self.prop_name_right: constraint = ValueConstraint(self.operator, prop_value) else: # Leave this as a ValueError instead of a ObjectGenerationError # subclass. This error would be the result of an internal bug # (i.e. should not happen in a correctly-working library). Should # there be a special InternalError exception class? raise ValueError( "Property name '{}' not present in coconstraint '{}'".format( prop_name, self ) ) return constraint
[docs] def involves_property(self, prop_name): return prop_name in (self.prop_name_right, self.prop_name_left)
[docs] def get_other_property(self, prop_name): if prop_name == self.prop_name_right: return self.prop_name_left return self.prop_name_right
def __str__(self): return "{} {} {}".format( self.prop_name_left, self.OPERATOR_STRING_MAP[self.operator], self.prop_name_right )
[docs]class PresenceCoconstraint: ONE, ALL, AT_LEAST_ONE = range(3) STRING_CONSTRAINT_TYPE_MAP = { "one": ONE, "all": ALL, "at-least-one": AT_LEAST_ONE } CONSTRAINT_TYPE_STRING_MAP = { v: k for (k, v) in STRING_CONSTRAINT_TYPE_MAP.items() } def __init__(self, property_names, constraint_type): self.constraint_type = self.to_constraint_type(constraint_type) self.property_names = set(property_names) # dedupes property names
[docs] @staticmethod def to_constraint_type(constraint_type): if isinstance(constraint_type, str): return PresenceCoconstraint.STRING_CONSTRAINT_TYPE_MAP[ constraint_type ] else: return constraint_type
[docs] def choose_properties(self, probability, minimize_ref_properties): """ Choose some properties from the group, according to its constraint type. :param probability: This is used only for the at-least-one constraint type. For that type, one is randomly chosen as the required property; the rest are optional and included with this probability. :param minimize_ref_properties: Modify how property selection is done: if True, minimize reference properties in the selection. Reference properties may still be chosen, if there is no other way to satisfy the constraint. :return: A list of property names. """ assert self.property_names, "Property group must not be empty!" non_ref_prop_names = [ name for name in self.property_names if not _is_ref_prop(name) ] if self.constraint_type == self.ONE: if minimize_ref_properties and non_ref_prop_names: chosen_props = [random.choice(non_ref_prop_names)] else: # Or should I have just stored the names as a list...? chosen_props = [ stix2generator.utils.rand_iterable(self.property_names) ] elif self.constraint_type == self.AT_LEAST_ONE: # Choose one required prop; make the rest optional. If minimizing # reference properties and all are reference properties, we should # behave like "one" and choose exactly one. Choosing more would # not be "minimal". if minimize_ref_properties and non_ref_prop_names: required_prop = random.choice(non_ref_prop_names) else: required_prop = stix2generator.utils.rand_iterable( self.property_names ) chosen_props = [required_prop] candidate_other_props = \ non_ref_prop_names if minimize_ref_properties \ else self.property_names chosen_props.extend( prop for prop in candidate_other_props if prop != required_prop and random.random() < probability ) else: # ALL chosen_props = list(self.property_names) return chosen_props
[docs] def can_satisfy_without_refs(self): """ Determine whether it is possible to satisfy this presence co-constraint with only non-reference properties. """ if self.constraint_type in (self.ONE, self.AT_LEAST_ONE): result = any(not _is_ref_prop(name) for name in self.property_names) else: # ALL result = all(not _is_ref_prop(name) for name in self.property_names) return result
def __str__(self): # On python2, this produces the weird set syntax: set(["a","b",...]) # Should I care about that? return "{} {}".format( self.CONSTRAINT_TYPE_STRING_MAP[self.constraint_type], self.property_names )
def _is_ref_prop(name): """Determine whether the given name names a "reference" property.""" return name.endswith("_ref") or name.endswith("_refs") # This regex just splits a string into an operator and the stuff to its # left and right. Trying to avoid a full-fledged parser for this. It's more # complicated since I want to avoid an expression like "a!=b" being # accidentally misinterpreted as "a!" = "b", or "a<=b" misinterpreted as # "a" < "=b". So the stuff on the left and right is defined as one or more # non-operator characters. The operator starts at the first operator # character. An "operator character" is any character appearing in an # operator. Operators have symbology which is unlikely to appear in a property # name, so I hope this is sufficient for now. _OP_CHARS = "".join(set("".join(ValueConstraintOperators.STRING_OPERATOR_MAP))) _VALUE_COCONSTRAINT_RE = re.compile( "^([^{0}]+)({1})([^{0}]+)$".format( re.escape(_OP_CHARS), "|".join( re.escape(op) for op in ValueConstraintOperators.STRING_OPERATOR_MAP ) ), re.S )
[docs]def make_value_coconstraint(value_coconstraint_expr): """ Make a ValueCoconstraint object from a co-constraint expression. The expression syntax is simple: <prop_name> <op> <prop_name>. E.g. "a < b". :param value_coconstraint_expr: The expression, as a string :return: The ValueCoconstraint object :raises ValueError: if the expression is invalid """ match = _VALUE_COCONSTRAINT_RE.match(value_coconstraint_expr) if not match: raise stix2generator.exceptions.ValueCoconstraintError( value_coconstraint_expr, "Invalid expression syntax" ) prop_name_left = match.group(1).strip() operator = match.group(2) prop_name_right = match.group(3).strip() if prop_name_right == prop_name_left: raise stix2generator.exceptions.ValueCoconstraintError( value_coconstraint_expr, "Can't relate a property to itself" ) return ValueCoconstraint(prop_name_left, operator, prop_name_right)