diff --git a/stix2/base.py b/stix2/base.py index 0fcc4c4..c26f60a 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -56,7 +56,7 @@ class STIXJSONIncludeOptionalDefaultsEncoder(json.JSONEncoder): elif isinstance(obj, _STIXBase): return dict(obj) else: - return super(STIXJSONEncoder, self).default(obj) + return super(STIXJSONIncludeOptionalDefaultsEncoder, self).default(obj) def get_required_properties(properties): diff --git a/stix2/core.py b/stix2/core.py index 3c5fcba..934dfc4 100644 --- a/stix2/core.py +++ b/stix2/core.py @@ -3,7 +3,7 @@ import pkgutil import stix2 -from . import exceptions +from .exceptions import ParseError from .utils import _get_dict STIX2_OBJ_MAPS = {} @@ -74,7 +74,7 @@ def dict_to_stix2(stix_dict, allow_custom=False, version=None): """ if 'type' not in stix_dict: - raise exceptions.ParseError("Can't parse object with no 'type' property: %s" % str(stix_dict)) + raise ParseError("Can't parse object with no 'type' property: %s" % str(stix_dict)) if "spec_version" in stix_dict: # For STIX 2.0, applies to bundles only. @@ -87,7 +87,7 @@ def dict_to_stix2(stix_dict, allow_custom=False, version=None): else: v = 'v' + stix2.DEFAULT_VERSION.replace('.', '') else: - v = 'v20' + v = 'v' + stix2.DEFAULT_VERSION.replace('.', '') OBJ_MAP = STIX2_OBJ_MAPS[v] @@ -98,7 +98,7 @@ def dict_to_stix2(stix_dict, allow_custom=False, version=None): # flag allows for unknown custom objects too, but will not # be parsed into STIX object, returned as is return stix_dict - raise exceptions.ParseError("Can't parse unknown object type '%s'! For custom types, use the CustomObject decorator." % stix_dict['type']) + raise ParseError("Can't parse unknown object type '%s'! For custom types, use the CustomObject decorator." % stix_dict['type']) return obj_class(allow_custom=allow_custom, **stix_dict) diff --git a/stix2/test/test_bundle.py b/stix2/test/test_bundle.py index 5b184a3..029bebe 100644 --- a/stix2/test/test_bundle.py +++ b/stix2/test/test_bundle.py @@ -3,8 +3,8 @@ import json import pytest import stix2 -import stix2.v20.sdo import stix2.v21.bundle +import stix2.v21.sdo EXPECTED_BUNDLE = """{ "type": "bundle", @@ -179,8 +179,7 @@ def test_parse_bundle(version): assert bundle.type == "bundle" assert bundle.id.startswith("bundle--") - # TODO: update this to a STIX 2.1 indicator - assert type(bundle.objects[0]) is stix2.v20.sdo.Indicator + assert type(bundle.objects[0]) is stix2.v21.sdo.Indicator assert bundle.objects[0].type == 'indicator' assert bundle.objects[1].type == 'malware' assert bundle.objects[2].type == 'relationship' diff --git a/stix2/test/test_custom.py b/stix2/test/test_custom.py index 27a8c5b..df3edbc 100644 --- a/stix2/test/test_custom.py +++ b/stix2/test/test_custom.py @@ -2,7 +2,7 @@ import pytest import stix2 import stix2.base -import stix2.v20.sdo +import stix2.v21.sdo from .constants import FAKE_TIME, MARKING_DEFINITION_ID @@ -95,8 +95,7 @@ def test_identity_custom_property_allowed(): def test_parse_identity_custom_property(data): with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: identity = stix2.parse(data) - # TODO: update to create and check a STIX 2.1 Identity object - assert excinfo.value.cls == stix2.v20.sdo.Identity + assert excinfo.value.cls == stix2.v21.sdo.Identity assert excinfo.value.properties == ['foo'] assert "Unexpected properties for" in str(excinfo.value) diff --git a/stix2/test/test_language_content.py b/stix2/test/test_language_content.py index 2a75acc..d67abd8 100644 --- a/stix2/test/test_language_content.py +++ b/stix2/test/test_language_content.py @@ -2,7 +2,6 @@ import datetime as dt -import pytest import pytz import stix2 @@ -21,7 +20,7 @@ TEST_CAMPAIGN = """{ "description": "More information about bank attack" }""" -TEST_LANGUAGE_CONTENT = """{ +TEST_LANGUAGE_CONTENT = u"""{ "type": "language-content", "id": "language-content--b86bd89f-98bb-4fa9-8cb2-9ad421da981d", "created": "2017-02-08T21:31:22.007Z", @@ -30,18 +29,17 @@ TEST_LANGUAGE_CONTENT = """{ "object_modified": "2017-02-08T21:31:22.007Z", "contents": { "de": { - "name": "Bank Angriff 1", - "description": "Weitere Informationen über Banküberfall" + "description": "Weitere Informationen über Banküberfall", + "name": "Bank Angriff 1" }, "fr": { - "name": "Attaque Bank 1", - "description": "Plus d'informations sur la crise bancaire" + "description": "Plus d'informations sur la crise bancaire", + "name": "Attaque Bank 1" } } }""" -@pytest.mark.xfail(reason="Dictionary keys are too short") def test_language_content_campaign(): now = dt.datetime(2017, 2, 8, 21, 31, 22, microsecond=7000, tzinfo=pytz.utc) @@ -66,5 +64,8 @@ def test_language_content_campaign(): camp = stix2.parse(TEST_CAMPAIGN) - assert str(lc) in TEST_LANGUAGE_CONTENT + # In order to provide the same representation, we need to disable escaping + # in json.dumps(). https://docs.python.org/3/library/json.html#json.dumps + # or https://docs.python.org/2/library/json.html#json.dumps + assert lc.serialize(pretty=True, ensure_ascii=False) == TEST_LANGUAGE_CONTENT assert lc.modified == camp.modified diff --git a/stix2/test/test_properties.py b/stix2/test/test_properties.py index 16ff06a..cd7723a 100644 --- a/stix2/test/test_properties.py +++ b/stix2/test/test_properties.py @@ -2,12 +2,13 @@ import pytest from stix2 import CustomObject, EmailMIMEComponent, ExtensionsProperty, TCPExt from stix2.exceptions import AtLeastOnePropertyError, DictionaryKeyError -from stix2.properties import (BinaryProperty, BooleanProperty, - DictionaryProperty, EmbeddedObjectProperty, - EnumProperty, FloatProperty, HashesProperty, - HexProperty, IDProperty, IntegerProperty, - ListProperty, Property, ReferenceProperty, - StringProperty, TimestampProperty, TypeProperty) +from stix2.v20.properties import (BinaryProperty, BooleanProperty, + DictionaryProperty, EmbeddedObjectProperty, + EnumProperty, FloatProperty, HashesProperty, + HexProperty, IDProperty, IntegerProperty, + ListProperty, Property, ReferenceProperty, + StringProperty, TimestampProperty, + TypeProperty) from .constants import FAKE_TIME diff --git a/stix2/v20/bundle.py b/stix2/v20/bundle.py index 81df0f6..05764b0 100644 --- a/stix2/v20/bundle.py +++ b/stix2/v20/bundle.py @@ -1,10 +1,10 @@ from collections import OrderedDict -from stix2 import parse -from stix2.base import _STIXBase -from stix2.properties import (IDProperty, ListProperty, Property, - StringProperty, TypeProperty) -from stix2.utils import _get_dict, get_class_hierarchy_names +from ..base import _STIXBase +from ..core import parse +from ..utils import _get_dict, get_class_hierarchy_names +from .properties import (IDProperty, ListProperty, Property, StringProperty, + TypeProperty) class STIXObjectProperty(Property): diff --git a/stix2/v20/common.py b/stix2/v20/common.py index 44a5267..47a464a 100644 --- a/stix2/v20/common.py +++ b/stix2/v20/common.py @@ -4,10 +4,10 @@ from collections import OrderedDict from ..base import _STIXBase from ..markings import _MarkingsMixin -from ..properties import (HashesProperty, IDProperty, ListProperty, Property, - ReferenceProperty, SelectorProperty, StringProperty, - TimestampProperty, TypeProperty) from ..utils import NOW, _get_dict +from .properties import (HashesProperty, IDProperty, ListProperty, Property, + ReferenceProperty, SelectorProperty, StringProperty, + TimestampProperty, TypeProperty) class ExternalReference(_STIXBase): diff --git a/stix2/v20/observables.py b/stix2/v20/observables.py index 659929b..1cfebad 100644 --- a/stix2/v20/observables.py +++ b/stix2/v20/observables.py @@ -12,12 +12,12 @@ import re from ..base import _Extension, _Observable, _STIXBase from ..exceptions import (AtLeastOnePropertyError, CustomContentError, DependentPropertiesError, ParseError) -from ..properties import (BinaryProperty, BooleanProperty, DictionaryProperty, - EmbeddedObjectProperty, EnumProperty, FloatProperty, - HashesProperty, HexProperty, IntegerProperty, - ListProperty, ObjectReferenceProperty, Property, - StringProperty, TimestampProperty, TypeProperty) from ..utils import TYPE_REGEX, _get_dict +from .properties import (BinaryProperty, BooleanProperty, DictionaryProperty, + EmbeddedObjectProperty, EnumProperty, FloatProperty, + HashesProperty, HexProperty, IntegerProperty, + ListProperty, ObjectReferenceProperty, Property, + StringProperty, TimestampProperty, TypeProperty) class ObservableProperty(Property): diff --git a/stix2/properties.py b/stix2/v20/properties.py similarity index 99% rename from stix2/properties.py rename to stix2/v20/properties.py index 39d383a..528cd76 100644 --- a/stix2/properties.py +++ b/stix2/v20/properties.py @@ -10,9 +10,9 @@ import uuid from six import string_types, text_type from stix2patterns.validator import run_validator -from .base import _STIXBase -from .exceptions import DictionaryKeyError -from .utils import _get_dict, parse_into_datetime +from ..base import _STIXBase +from ..exceptions import DictionaryKeyError +from ..utils import _get_dict, parse_into_datetime class Property(object): diff --git a/stix2/v20/sdo.py b/stix2/v20/sdo.py index abe7df4..5eb0801 100644 --- a/stix2/v20/sdo.py +++ b/stix2/v20/sdo.py @@ -4,16 +4,15 @@ from collections import OrderedDict import re -import stix2 - from ..base import _STIXBase +from ..core import _register_type from ..markings import _MarkingsMixin -from ..properties import (BooleanProperty, IDProperty, IntegerProperty, - ListProperty, PatternProperty, ReferenceProperty, - StringProperty, TimestampProperty, TypeProperty) from ..utils import NOW, TYPE_REGEX from .common import ExternalReference, GranularMarking, KillChainPhase from .observables import ObservableProperty +from .properties import (BooleanProperty, IDProperty, IntegerProperty, + ListProperty, PatternProperty, ReferenceProperty, + StringProperty, TimestampProperty, TypeProperty) class STIXDomainObject(_STIXBase, _MarkingsMixin): @@ -409,7 +408,7 @@ def CustomObject(type='x-custom-type', properties=None): return raise e - stix2._register_type(_Custom, version="2.0") + _register_type(_Custom, version="2.0") return _Custom return custom_builder diff --git a/stix2/v20/sro.py b/stix2/v20/sro.py index e488229..89520ac 100644 --- a/stix2/v20/sro.py +++ b/stix2/v20/sro.py @@ -4,11 +4,11 @@ from collections import OrderedDict from ..base import _STIXBase from ..markings import _MarkingsMixin -from ..properties import (BooleanProperty, IDProperty, IntegerProperty, - ListProperty, ReferenceProperty, StringProperty, - TimestampProperty, TypeProperty) from ..utils import NOW from .common import ExternalReference, GranularMarking +from .properties import (BooleanProperty, IDProperty, IntegerProperty, + ListProperty, ReferenceProperty, StringProperty, + TimestampProperty, TypeProperty) class STIXRelationshipObject(_STIXBase, _MarkingsMixin): diff --git a/stix2/v21/bundle.py b/stix2/v21/bundle.py index 6c20923..1957bf1 100644 --- a/stix2/v21/bundle.py +++ b/stix2/v21/bundle.py @@ -1,9 +1,9 @@ from collections import OrderedDict -from stix2 import parse -from stix2.base import _STIXBase -from stix2.properties import IDProperty, ListProperty, Property, TypeProperty -from stix2.utils import _get_dict, get_class_hierarchy_names +from ..base import _STIXBase +from ..core import parse +from ..utils import _get_dict, get_class_hierarchy_names +from .properties import IDProperty, ListProperty, Property, TypeProperty class STIXObjectProperty(Property): diff --git a/stix2/v21/common.py b/stix2/v21/common.py index f31eeef..2bde828 100644 --- a/stix2/v21/common.py +++ b/stix2/v21/common.py @@ -4,11 +4,11 @@ from collections import OrderedDict from ..base import _STIXBase from ..markings import _MarkingsMixin -from ..properties import (BooleanProperty, DictionaryProperty, HashesProperty, - IDProperty, ListProperty, Property, - ReferenceProperty, SelectorProperty, StringProperty, - TimestampProperty, TypeProperty) from ..utils import NOW, _get_dict +from .properties import (BooleanProperty, DictionaryProperty, HashesProperty, + IDProperty, ListProperty, Property, ReferenceProperty, + SelectorProperty, StringProperty, TimestampProperty, + TypeProperty) class ExternalReference(_STIXBase): diff --git a/stix2/v21/observables.py b/stix2/v21/observables.py index 2da80b0..052d0d9 100644 --- a/stix2/v21/observables.py +++ b/stix2/v21/observables.py @@ -12,12 +12,12 @@ import re from ..base import _Extension, _Observable, _STIXBase from ..exceptions import (AtLeastOnePropertyError, CustomContentError, DependentPropertiesError, ParseError) -from ..properties import (BinaryProperty, BooleanProperty, DictionaryProperty, - EmbeddedObjectProperty, EnumProperty, FloatProperty, - HashesProperty, HexProperty, IntegerProperty, - ListProperty, ObjectReferenceProperty, Property, - StringProperty, TimestampProperty, TypeProperty) from ..utils import TYPE_REGEX, _get_dict +from .properties import (BinaryProperty, BooleanProperty, DictionaryProperty, + EmbeddedObjectProperty, EnumProperty, FloatProperty, + HashesProperty, HexProperty, IntegerProperty, + ListProperty, ObjectReferenceProperty, Property, + StringProperty, TimestampProperty, TypeProperty) class ObservableProperty(Property): diff --git a/stix2/v21/properties.py b/stix2/v21/properties.py new file mode 100644 index 0000000..4e2c0ca --- /dev/null +++ b/stix2/v21/properties.py @@ -0,0 +1,395 @@ +"""Classes for representing properties of STIX Objects and Cyber Observables. +""" +import base64 +import binascii +import collections +import inspect +import re +import uuid + +from six import string_types, text_type +from stix2patterns.validator import run_validator + +from ..base import _STIXBase +from ..exceptions import DictionaryKeyError +from ..utils import _get_dict, parse_into_datetime + + +class Property(object): + """Represent a property of STIX data type. + + Subclasses can define the following attributes as keyword arguments to + ``__init__()``. + + Args: + required (bool): If ``True``, the property must be provided when creating an + object with that property. No default value exists for these properties. + (Default: ``False``) + fixed: This provides a constant default value. Users are free to + provide this value explicity when constructing an object (which allows + you to copy **all** values from an existing object to a new object), but + if the user provides a value other than the ``fixed`` value, it will raise + an error. This is semantically equivalent to defining both: + + - a ``clean()`` function that checks if the value matches the fixed + value, and + - a ``default()`` function that returns the fixed value. + + Subclasses can also define the following functions: + + - ``def clean(self, value) -> any:`` + - Return a value that is valid for this property. If ``value`` is not + valid for this property, this will attempt to transform it first. If + ``value`` is not valid and no such transformation is possible, it should + raise a ValueError. + - ``def default(self):`` + - provide a default value for this property. + - ``default()`` can return the special value ``NOW`` to use the current + time. This is useful when several timestamps in the same object need + to use the same default value, so calling now() for each property-- + likely several microseconds apart-- does not work. + + Subclasses can instead provide a lambda function for ``default`` as a keyword + argument. ``clean`` should not be provided as a lambda since lambdas cannot + raise their own exceptions. + + When instantiating Properties, ``required`` and ``default`` should not be used + together. ``default`` implies that the property is required in the specification + so this function will be used to supply a value if none is provided. + ``required`` means that the user must provide this; it is required in the + specification and we can't or don't want to create a default value. + """ + + def _default_clean(self, value): + if value != self._fixed_value: + raise ValueError("must equal '{0}'.".format(self._fixed_value)) + return value + + def __init__(self, required=False, fixed=None, default=None, type=None): + self.required = required + self.type = type + if fixed: + self._fixed_value = fixed + self.clean = self._default_clean + self.default = lambda: fixed + if default: + self.default = default + + def clean(self, value): + return value + + def __call__(self, value=None): + """Used by ListProperty to handle lists that have been defined with + either a class or an instance. + """ + return value + + +class ListProperty(Property): + + def __init__(self, contained, **kwargs): + """ + ``contained`` should be a function which returns an object from the value. + """ + if inspect.isclass(contained) and issubclass(contained, Property): + # If it's a class and not an instance, instantiate it so that + # clean() can be called on it, and ListProperty.clean() will + # use __call__ when it appends the item. + self.contained = contained() + else: + self.contained = contained + super(ListProperty, self).__init__(**kwargs) + + def clean(self, value): + try: + iter(value) + except TypeError: + raise ValueError("must be an iterable.") + + if isinstance(value, (_STIXBase, string_types)): + value = [value] + + result = [] + for item in value: + try: + valid = self.contained.clean(item) + except ValueError: + raise + except AttributeError: + # type of list has no clean() function (eg. built in Python types) + # TODO Should we raise an error here? + valid = item + + if type(self.contained) is EmbeddedObjectProperty: + obj_type = self.contained.type + elif type(self.contained).__name__ is 'STIXObjectProperty': + # ^ this way of checking doesn't require a circular import + # valid is already an instance of a python-stix2 class; no need + # to turn it into a dictionary and then pass it to the class + # constructor again + result.append(valid) + continue + elif type(self.contained) is DictionaryProperty: + obj_type = dict + else: + obj_type = self.contained + + if isinstance(valid, collections.Mapping): + result.append(obj_type(**valid)) + else: + result.append(obj_type(valid)) + + # STIX spec forbids empty lists + if len(result) < 1: + raise ValueError("must not be empty.") + + return result + + +class StringProperty(Property): + + def __init__(self, **kwargs): + self.string_type = text_type + super(StringProperty, self).__init__(**kwargs) + + def clean(self, value): + return self.string_type(value) + + +class TypeProperty(Property): + + def __init__(self, type): + super(TypeProperty, self).__init__(fixed=type) + + +class IDProperty(Property): + + def __init__(self, type): + self.required_prefix = type + "--" + super(IDProperty, self).__init__() + + def clean(self, value): + if not value.startswith(self.required_prefix): + raise ValueError("must start with '{0}'.".format(self.required_prefix)) + try: + uuid.UUID(value.split('--', 1)[1]) + except Exception: + raise ValueError("must have a valid UUID after the prefix.") + return value + + def default(self): + return self.required_prefix + str(uuid.uuid4()) + + +class IntegerProperty(Property): + + def clean(self, value): + try: + return int(value) + except Exception: + raise ValueError("must be an integer.") + + +class FloatProperty(Property): + def clean(self, value): + try: + return float(value) + except Exception: + raise ValueError("must be a float.") + + +class BooleanProperty(Property): + + def clean(self, value): + if isinstance(value, bool): + return value + + trues = ['true', 't'] + falses = ['false', 'f'] + try: + if value.lower() in trues: + return True + if value.lower() in falses: + return False + except AttributeError: + if value == 1: + return True + if value == 0: + return False + + raise ValueError("must be a boolean value.") + + +class TimestampProperty(Property): + + def __init__(self, precision=None, **kwargs): + self.precision = precision + super(TimestampProperty, self).__init__(**kwargs) + + def clean(self, value): + return parse_into_datetime(value, self.precision) + + +class DictionaryProperty(Property): + + def clean(self, value): + try: + dictified = _get_dict(value) + except ValueError: + raise ValueError("The dictionary property must contain a dictionary") + if dictified == {}: + raise ValueError("The dictionary property must contain a non-empty dictionary") + + for k in dictified.keys(): + if len(k) > 250: + raise DictionaryKeyError(k, "longer than 250 characters") + if not re.match('^[a-zA-Z0-9_-]+$', k): + raise DictionaryKeyError(k, "contains characters other than" + "lowercase a-z, uppercase A-Z, " + "numerals 0-9, hyphen (-), or " + "underscore (_)") + return dictified + + +HASHES_REGEX = { + "MD5": ("^[a-fA-F0-9]{32}$", "MD5"), + "MD6": ("^[a-fA-F0-9]{32}|[a-fA-F0-9]{40}|[a-fA-F0-9]{56}|[a-fA-F0-9]{64}|[a-fA-F0-9]{96}|[a-fA-F0-9]{128}$", "MD6"), + "RIPEMD160": ("^[a-fA-F0-9]{40}$", "RIPEMD-160"), + "SHA1": ("^[a-fA-F0-9]{40}$", "SHA-1"), + "SHA224": ("^[a-fA-F0-9]{56}$", "SHA-224"), + "SHA256": ("^[a-fA-F0-9]{64}$", "SHA-256"), + "SHA384": ("^[a-fA-F0-9]{96}$", "SHA-384"), + "SHA512": ("^[a-fA-F0-9]{128}$", "SHA-512"), + "SHA3224": ("^[a-fA-F0-9]{56}$", "SHA3-224"), + "SHA3256": ("^[a-fA-F0-9]{64}$", "SHA3-256"), + "SHA3384": ("^[a-fA-F0-9]{96}$", "SHA3-384"), + "SHA3512": ("^[a-fA-F0-9]{128}$", "SHA3-512"), + "SSDEEP": ("^[a-zA-Z0-9/+:.]{1,128}$", "ssdeep"), + "WHIRLPOOL": ("^[a-fA-F0-9]{128}$", "WHIRLPOOL"), +} + + +class HashesProperty(DictionaryProperty): + + def clean(self, value): + clean_dict = super(HashesProperty, self).clean(value) + for k, v in clean_dict.items(): + key = k.upper().replace('-', '') + if key in HASHES_REGEX: + vocab_key = HASHES_REGEX[key][1] + if not re.match(HASHES_REGEX[key][0], v): + raise ValueError("'%s' is not a valid %s hash" % (v, vocab_key)) + if k != vocab_key: + clean_dict[vocab_key] = clean_dict[k] + del clean_dict[k] + return clean_dict + + +class BinaryProperty(Property): + + def clean(self, value): + try: + base64.b64decode(value) + except (binascii.Error, TypeError): + raise ValueError("must contain a base64 encoded string") + return value + + +class HexProperty(Property): + + def clean(self, value): + if not re.match('^([a-fA-F0-9]{2})+$', value): + raise ValueError("must contain an even number of hexadecimal characters") + return value + + +REF_REGEX = re.compile("^[a-z][a-z-]+[a-z]--[0-9a-fA-F]{8}-[0-9a-fA-F]{4}" + "-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$") + + +class ReferenceProperty(Property): + + def __init__(self, required=False, type=None): + """ + references sometimes must be to a specific object type + """ + self.type = type + super(ReferenceProperty, self).__init__(required, type=type) + + def clean(self, value): + if isinstance(value, _STIXBase): + value = value.id + value = str(value) + if self.type: + if not value.startswith(self.type): + raise ValueError("must start with '{0}'.".format(self.type)) + if not REF_REGEX.match(value): + raise ValueError("must match --.") + return value + + +SELECTOR_REGEX = re.compile("^[a-z0-9_-]{3,250}(\\.(\\[\\d+\\]|[a-z0-9_-]{1,250}))*$") + + +class SelectorProperty(Property): + + def __init__(self, type=None): + # ignore type + super(SelectorProperty, self).__init__() + + def clean(self, value): + if not SELECTOR_REGEX.match(value): + raise ValueError("must adhere to selector syntax.") + return value + + +class ObjectReferenceProperty(StringProperty): + + def __init__(self, valid_types=None, **kwargs): + if valid_types and type(valid_types) is not list: + valid_types = [valid_types] + self.valid_types = valid_types + super(ObjectReferenceProperty, self).__init__(**kwargs) + + +class EmbeddedObjectProperty(Property): + + def __init__(self, type, required=False): + self.type = type + super(EmbeddedObjectProperty, self).__init__(required, type=type) + + def clean(self, value): + if type(value) is dict: + value = self.type(**value) + elif not isinstance(value, self.type): + raise ValueError("must be of type %s." % self.type.__name__) + return value + + +class EnumProperty(StringProperty): + + def __init__(self, allowed, **kwargs): + if type(allowed) is not list: + allowed = list(allowed) + self.allowed = allowed + super(EnumProperty, self).__init__(**kwargs) + + def clean(self, value): + value = super(EnumProperty, self).clean(value) + if value not in self.allowed: + raise ValueError("value '%s' is not valid for this enumeration." % value) + return self.string_type(value) + + +class PatternProperty(StringProperty): + + def __init__(self, **kwargs): + super(PatternProperty, self).__init__(**kwargs) + + def clean(self, value): + str_value = super(PatternProperty, self).clean(value) + errors = run_validator(str_value) + if errors: + raise ValueError(str(errors[0])) + + return self.string_type(value) diff --git a/stix2/v21/sdo.py b/stix2/v21/sdo.py index 1207a22..98b40fa 100644 --- a/stix2/v21/sdo.py +++ b/stix2/v21/sdo.py @@ -3,18 +3,17 @@ from collections import OrderedDict import re -import stix2 - from ..base import _STIXBase +from ..core import _register_type from ..markings import _MarkingsMixin -from ..properties import (BooleanProperty, DictionaryProperty, - EmbeddedObjectProperty, EnumProperty, FloatProperty, - IDProperty, IntegerProperty, ListProperty, - PatternProperty, ReferenceProperty, StringProperty, - TimestampProperty, TypeProperty) from ..utils import NOW, TYPE_REGEX from .common import ExternalReference, GranularMarking, KillChainPhase from .observables import ObservableProperty +from .properties import (BooleanProperty, DictionaryProperty, + EmbeddedObjectProperty, EnumProperty, FloatProperty, + IDProperty, IntegerProperty, ListProperty, + PatternProperty, ReferenceProperty, StringProperty, + TimestampProperty, TypeProperty) class STIXDomainObject(_STIXBase, _MarkingsMixin): @@ -585,7 +584,7 @@ def CustomObject(type='x-custom-type', properties=None): return raise e - stix2._register_type(_Custom, version="2.1") + _register_type(_Custom, version="2.1") return _Custom return custom_builder diff --git a/stix2/v21/sro.py b/stix2/v21/sro.py index 7565bbb..1440ebe 100644 --- a/stix2/v21/sro.py +++ b/stix2/v21/sro.py @@ -4,11 +4,11 @@ from collections import OrderedDict from ..base import _STIXBase from ..markings import _MarkingsMixin -from ..properties import (BooleanProperty, IDProperty, IntegerProperty, - ListProperty, ReferenceProperty, StringProperty, - TimestampProperty, TypeProperty) from ..utils import NOW from .common import ExternalReference, GranularMarking +from .properties import (BooleanProperty, IDProperty, IntegerProperty, + ListProperty, ReferenceProperty, StringProperty, + TimestampProperty, TypeProperty) class STIXRelationshipObject(_STIXBase, _MarkingsMixin):