diff --git a/stix2/base.py b/stix2/base.py index 3e0d670..1b3227d 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -2,6 +2,7 @@ import collections.abc import copy +import itertools import re import uuid @@ -13,7 +14,7 @@ from stix2.canonicalization.Canonicalize import canonicalize from .exceptions import ( AtLeastOnePropertyError, DependentPropertiesError, ExtraPropertiesError, ImmutableError, InvalidObjRefError, InvalidValueError, - MissingPropertiesError, MutuallyExclusivePropertiesError, + MissingPropertiesError, MutuallyExclusivePropertiesError, STIXError, ) from .markings import _MarkingsMixin from .markings.utils import validate @@ -43,7 +44,7 @@ class _STIXBase(collections.abc.Mapping): return all_properties - def _check_property(self, prop_name, prop, kwargs): + def _check_property(self, prop_name, prop, kwargs, allow_custom): if prop_name not in kwargs: if hasattr(prop, 'default'): value = prop.default() @@ -51,9 +52,12 @@ class _STIXBase(collections.abc.Mapping): value = self.__now kwargs[prop_name] = value + has_custom = False if prop_name in kwargs: try: - kwargs[prop_name] = prop.clean(kwargs[prop_name]) + kwargs[prop_name], has_custom = prop.clean( + kwargs[prop_name], allow_custom, + ) except InvalidValueError: # No point in wrapping InvalidValueError in another # InvalidValueError... so let those propagate. @@ -63,7 +67,9 @@ class _STIXBase(collections.abc.Mapping): self.__class__, prop_name, reason=str(exc), ) from exc - # inter-property constraint methods + return has_custom + + # interproperty constraint methods def _check_mutually_exclusive_properties(self, list_of_properties, at_least_one=True): current_properties = self.properties_populated() @@ -102,7 +108,6 @@ class _STIXBase(collections.abc.Mapping): def __init__(self, allow_custom=False, **kwargs): cls = self.__class__ - self._allow_custom = allow_custom # Use the same timestamp for any auto-generated datetimes self.__now = get_timestamp() @@ -112,12 +117,11 @@ class _STIXBase(collections.abc.Mapping): if custom_props and not isinstance(custom_props, dict): raise ValueError("'custom_properties' must be a dictionary") - extra_kwargs = list(set(kwargs) - set(self._properties)) + extra_kwargs = kwargs.keys() - self._properties.keys() if extra_kwargs and issubclass(cls, stix2.v21._Extension): - props_to_remove = ['extension_type'] - extra_kwargs = [prop for prop in extra_kwargs if prop not in props_to_remove] + extra_kwargs = [prop for prop in extra_kwargs if prop != 'extension_type'] - if extra_kwargs and not self._allow_custom: + if extra_kwargs and not allow_custom: ext_found = False # This section performs a check on top-level objects that support extensions. # If extra_kwargs is not empty, allow_custom False, and the extension_type is not @@ -132,11 +136,14 @@ class _STIXBase(collections.abc.Mapping): if ext_found is False: raise ExtraPropertiesError(cls, extra_kwargs) - if custom_props or extra_kwargs: - self._allow_custom = True - if isinstance(self, stix2.v21._STIXBase21): - all_custom_prop_names = extra_kwargs - all_custom_prop_names.extend(list(custom_props.keys())) + if custom_props: + # loophole for custom_properties... + allow_custom = True + + all_custom_prop_names = extra_kwargs | custom_props.keys() - \ + self._properties.keys() + if all_custom_prop_names: + if not isinstance(self, stix2.v20._STIXBase20): for prop_name in all_custom_prop_names: if not re.match(PREFIX_21_REGEX, prop_name): raise InvalidValueError( @@ -145,12 +152,11 @@ class _STIXBase(collections.abc.Mapping): ) # Remove any keyword arguments whose value is None or [] (i.e. empty list) - setting_kwargs = {} - props = kwargs.copy() - props.update(custom_props) - for prop_name, prop_value in props.items(): - if prop_value is not None and prop_value != []: - setting_kwargs[prop_name] = prop_value + setting_kwargs = { + k: v + for k, v in itertools.chain(kwargs.items(), custom_props.items()) + if v is not None and v != [] + } # Detect any missing required properties required_properties = set(get_required_properties(self._properties)) @@ -167,8 +173,13 @@ class _STIXBase(collections.abc.Mapping): if new_ext_check is False: raise MissingPropertiesError(cls, missing_kwargs) + has_custom = bool(all_custom_prop_names) for prop_name, prop_metadata in self._properties.items(): - self._check_property(prop_name, prop_metadata, setting_kwargs) + temp_custom = self._check_property( + prop_name, prop_metadata, setting_kwargs, allow_custom, + ) + + has_custom = has_custom or temp_custom # Cache defaulted optional properties for serialization defaulted = [] @@ -187,6 +198,22 @@ class _STIXBase(collections.abc.Mapping): self._check_object_constraints() + if allow_custom: + self.__has_custom = has_custom + + else: + # The simple case: our property cleaners are supposed to do their + # job and prevent customizations, so we just set to False. But + # this sanity check is helpful for finding bugs in those clean() + # methods. + if has_custom: + raise STIXError( + "Internal error: a clean() method did not properly enforce " + "allow_custom=False!", + ) + + self.__has_custom = False + def __getitem__(self, key): return self._inner[key] @@ -231,13 +258,16 @@ class _STIXBase(collections.abc.Mapping): if isinstance(self, _Observable): # Assume: valid references in the original object are still valid in the new version new_inner['_valid_refs'] = {'*': '*'} - new_inner['allow_custom'] = self._allow_custom - return cls(**new_inner) + return cls(allow_custom=True, **new_inner) def properties_populated(self): return list(self._inner.keys()) -# Versioning API + @property + def has_custom(self): + return self.__has_custom + + # Versioning API def new_version(self, **kwargs): return _new_version(self, **kwargs) @@ -346,20 +376,21 @@ class _Observable(_STIXBase): if ref_type not in allowed_types: raise InvalidObjRefError(self.__class__, prop_name, "object reference '%s' is of an invalid type '%s'" % (ref, ref_type)) - def _check_property(self, prop_name, prop, kwargs): - super(_Observable, self)._check_property(prop_name, prop, kwargs) - if prop_name not in kwargs: - return + def _check_property(self, prop_name, prop, kwargs, allow_custom): + has_custom = super(_Observable, self)._check_property(prop_name, prop, kwargs, allow_custom) - from .properties import ObjectReferenceProperty - if prop_name.endswith('_ref'): - if isinstance(prop, ObjectReferenceProperty): - ref = kwargs[prop_name] - self._check_ref(ref, prop, prop_name) - elif prop_name.endswith('_refs'): - if isinstance(prop.contained, ObjectReferenceProperty): - for ref in kwargs[prop_name]: + if prop_name in kwargs: + from .properties import ObjectReferenceProperty + if prop_name.endswith('_ref'): + if isinstance(prop, ObjectReferenceProperty): + ref = kwargs[prop_name] self._check_ref(ref, prop, prop_name) + elif prop_name.endswith('_refs'): + if isinstance(prop.contained, ObjectReferenceProperty): + for ref in kwargs[prop_name]: + self._check_ref(ref, prop, prop_name) + + return has_custom def _generate_id(self): """ diff --git a/stix2/hashes.py b/stix2/hashes.py new file mode 100644 index 0000000..198cef6 --- /dev/null +++ b/stix2/hashes.py @@ -0,0 +1,95 @@ +""" +Library support for hash algorithms, independent of STIX specs. +""" + +import enum +import re + + +class Hash(enum.Enum): + """ + Instances represent a hash algorithm, independent of STIX spec version. + Different spec versions may have different requirements for naming; this + allows us to refer to and use hash algorithms in a spec-agnostic way. + """ + MD5 = 0 + MD6 = 1 + RIPEMD160 = 2 + SHA1 = 3 + SHA224 = 4 + SHA256 = 5 + SHA384 = 6 + SHA512 = 7 + SHA3224 = 8 + SHA3256 = 9 + SHA3384 = 10 + SHA3512 = 11 + SSDEEP = 12 + WHIRLPOOL = 13 + TLSH = 14 + + +# Regexes used to sanity check hash values. Could also be combined with the +# enum values themselves using enum definition tricks, but... this seems +# simpler. +_HASH_REGEXES = { + Hash.MD5: r"^[a-f0-9]{32}$", + Hash.MD6: r"^[a-f0-9]{32}|[a-f0-9]{40}|[a-f0-9]{56}|[a-f0-9]{64}|[a-f0-9]{96}|[a-f0-9]{128}$", + Hash.RIPEMD160: r"^[a-f0-9]{40}$", + Hash.SHA1: r"^[a-f0-9]{40}$", + Hash.SHA224: r"^[a-f0-9]{56}$", + Hash.SHA256: r"^[a-f0-9]{64}$", + Hash.SHA384: r"^[a-f0-9]{96}$", + Hash.SHA512: r"^[a-f0-9]{128}$", + Hash.SHA3224: r"^[a-f0-9]{56}$", + Hash.SHA3256: r"^[a-f0-9]{64}$", + Hash.SHA3384: r"^[a-f0-9]{96}$", + Hash.SHA3512: r"^[a-f0-9]{128}$", + Hash.SSDEEP: r"^[a-z0-9/+:.]{1,128}$", + Hash.WHIRLPOOL: r"^[a-f0-9]{128}$", + Hash.TLSH: r"^[a-f0-9]{70}$", +} + + +# compile all the regexes; be case-insensitive +for hash_, re_str in _HASH_REGEXES.items(): + _HASH_REGEXES[hash_] = re.compile(re_str, re.I) + + +def infer_hash_algorithm(name): + """ + Given a hash algorithm name, try to figure out which hash algorithm it + refers to. This primarily enables some user flexibility in naming hash + algorithms when creating STIX content. + + :param name: A hash algorithm name + :return: A Hash enum value if the name was recognized, or None if it was + not recognized. + """ + enum_name = name.replace("-", "").upper() + + try: + enum_obj = Hash[enum_name] + except KeyError: + enum_obj = None + + return enum_obj + + +def check_hash(hash_, value): + """ + Sanity check the given hash value, against the given hash algorithm. + + :param hash_: The hash algorithm, as one of the Hash enums + :param value: A hash value as string + :return: True if the value seems okay; False if not + """ + + # I guess there's no need to require a regex mapping for the algorithm... + # Just assume it's okay if we have no way to check it. + result = True + regex = _HASH_REGEXES.get(hash_) + if regex: + result = bool(regex.match(value)) + + return result diff --git a/stix2/properties.py b/stix2/properties.py index 1c0a9d8..0f68f6a 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -8,14 +8,18 @@ import inspect import re import uuid -from . import registry, version +import stix2 +import stix2.hashes + from .base import _STIXBase -from .exceptions import ( - CustomContentError, DictionaryKeyError, MissingPropertiesError, - MutuallyExclusivePropertiesError, STIXError, -) +from .exceptions import CustomContentError, DictionaryKeyError, STIXError from .parsing import parse, parse_observable -from .utils import _get_dict, get_class_hierarchy_names, parse_into_datetime +from .registry import STIX2_OBJ_MAPS +from .utils import ( + STIXTypeClass, _get_dict, get_class_hierarchy_names, get_type_from_id, + is_object, is_stix_type, parse_into_datetime, to_enum, +) +from .version import DEFAULT_VERSION TYPE_REGEX = re.compile(r'^-?[a-z0-9]+(-[a-z0-9]+)*-?$') TYPE_21_REGEX = re.compile(r'^([a-z][a-z0-9]*)+([a-z0-9-]+)*-?$') @@ -129,11 +133,23 @@ class Property(object): 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 an exception. + - ``def clean(self, value, allow_custom) -> (any, has_custom):`` + - Return a value that is valid for this property, and enforce and + detect value customization. If ``value`` is not valid for this + property, you may attempt to transform it first. If ``value`` is not + valid and no such transformation is possible, it must raise an + exception. The method is also responsible for enforcing and + detecting customizations. If allow_custom is False, no customizations + must be allowed. If any are encountered, an exception must be raised + (e.g. CustomContentError). If none are encountered, False must be + returned for has_custom. If allow_custom is True, then the clean() + method is responsible for detecting any customizations in the value + (just because the user has elected to allow customizations doesn't + mean there actually are any). The method must return an appropriate + value for has_custom. Customization may not be applicable/possible + for a property. In that case, allow_custom can be ignored, and + has_custom must be returned as False. + - ``def default(self):`` - provide a default value for this property. - ``default()`` can return the special value ``NOW`` to use the current @@ -154,10 +170,10 @@ class Property(object): """ - def _default_clean(self, value): + def _default_clean(self, value, allow_custom=False): if value != self._fixed_value: raise ValueError("must equal '{}'.".format(self._fixed_value)) - return value + return value, False def __init__(self, required=False, fixed=None, default=None): self.required = required @@ -175,14 +191,8 @@ class Property(object): 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 + def clean(self, value, allow_custom=False): + return value, False class ListProperty(Property): @@ -214,7 +224,7 @@ class ListProperty(Property): super(ListProperty, self).__init__(**kwargs) - def clean(self, value): + def clean(self, value, allow_custom): try: iter(value) except TypeError: @@ -223,21 +233,22 @@ class ListProperty(Property): if isinstance(value, (_STIXBase, str)): value = [value] + result = [] + has_custom = False if isinstance(self.contained, Property): - result = [ - self.contained.clean(item) - for item in value - ] + for item in value: + valid, temp_custom = self.contained.clean(item, allow_custom) + result.append(valid) + has_custom = has_custom or temp_custom else: # self.contained must be a _STIXBase subclass - result = [] for item in value: if isinstance(item, self.contained): valid = item elif isinstance(item, collections.abc.Mapping): # attempt a mapping-like usage... - valid = self.contained(**item) + valid = self.contained(allow_custom=allow_custom, **item) else: raise ValueError( @@ -247,12 +258,16 @@ class ListProperty(Property): ) result.append(valid) + has_custom = has_custom or valid.has_custom + + if not allow_custom and has_custom: + raise CustomContentError("custom content encountered") # STIX spec forbids empty lists if len(result) < 1: raise ValueError("must not be empty.") - return result + return result, has_custom class StringProperty(Property): @@ -260,15 +275,15 @@ class StringProperty(Property): def __init__(self, **kwargs): super(StringProperty, self).__init__(**kwargs) - def clean(self, value): + def clean(self, value, allow_custom=False): if not isinstance(value, str): - return str(value) - return value + value = str(value) + return value, False class TypeProperty(Property): - def __init__(self, type, spec_version=version.DEFAULT_VERSION): + def __init__(self, type, spec_version=DEFAULT_VERSION): _validate_type(type, spec_version) self.spec_version = spec_version super(TypeProperty, self).__init__(fixed=type) @@ -276,14 +291,14 @@ class TypeProperty(Property): class IDProperty(Property): - def __init__(self, type, spec_version=version.DEFAULT_VERSION): + def __init__(self, type, spec_version=DEFAULT_VERSION): self.required_prefix = type + "--" self.spec_version = spec_version super(IDProperty, self).__init__() - def clean(self, value): + def clean(self, value, allow_custom=False): _validate_id(value, self.spec_version, self.required_prefix) - return value + return value, False def default(self): return self.required_prefix + str(uuid.uuid4()) @@ -296,7 +311,7 @@ class IntegerProperty(Property): self.max = max super(IntegerProperty, self).__init__(**kwargs) - def clean(self, value): + def clean(self, value, allow_custom=False): try: value = int(value) except Exception: @@ -310,7 +325,7 @@ class IntegerProperty(Property): msg = "maximum value is {}. received {}".format(self.max, value) raise ValueError(msg) - return value + return value, False class FloatProperty(Property): @@ -320,7 +335,7 @@ class FloatProperty(Property): self.max = max super(FloatProperty, self).__init__(**kwargs) - def clean(self, value): + def clean(self, value, allow_custom=False): try: value = float(value) except Exception: @@ -334,29 +349,26 @@ class FloatProperty(Property): msg = "maximum value is {}. received {}".format(self.max, value) raise ValueError(msg) - return value + return value, False class BooleanProperty(Property): + _trues = ['true', 't', '1', 1, True] + _falses = ['false', 'f', '0', 0, False] - def clean(self, value): - if isinstance(value, bool): - return value + def clean(self, value, allow_custom=False): - trues = ['true', 't', '1'] - falses = ['false', 'f', '0'] - 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 + if isinstance(value, str): + value = value.lower() - raise ValueError("must be a boolean value.") + if value in self._trues: + result = True + elif value in self._falses: + result = False + else: + raise ValueError("must be a boolean value.") + + return result, False class TimestampProperty(Property): @@ -367,19 +379,19 @@ class TimestampProperty(Property): super(TimestampProperty, self).__init__(**kwargs) - def clean(self, value): + def clean(self, value, allow_custom=False): return parse_into_datetime( value, self.precision, self.precision_constraint, - ) + ), False class DictionaryProperty(Property): - def __init__(self, spec_version=version.DEFAULT_VERSION, **kwargs): + def __init__(self, spec_version=DEFAULT_VERSION, **kwargs): self.spec_version = spec_version super(DictionaryProperty, self).__init__(**kwargs) - def clean(self, value): + def clean(self, value, allow_custom=False): try: dictified = _get_dict(value) except ValueError: @@ -404,140 +416,202 @@ class DictionaryProperty(Property): if len(dictified) < 1: raise ValueError("must not be empty.") - return dictified - - -HASHES_REGEX = { - "MD5": (r"^[a-fA-F0-9]{32}$", "MD5"), - "MD6": (r"^[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": (r"^[a-fA-F0-9]{40}$", "RIPEMD-160"), - "SHA1": (r"^[a-fA-F0-9]{40}$", "SHA-1"), - "SHA224": (r"^[a-fA-F0-9]{56}$", "SHA-224"), - "SHA256": (r"^[a-fA-F0-9]{64}$", "SHA-256"), - "SHA384": (r"^[a-fA-F0-9]{96}$", "SHA-384"), - "SHA512": (r"^[a-fA-F0-9]{128}$", "SHA-512"), - "SHA3224": (r"^[a-fA-F0-9]{56}$", "SHA3-224"), - "SHA3256": (r"^[a-fA-F0-9]{64}$", "SHA3-256"), - "SHA3384": (r"^[a-fA-F0-9]{96}$", "SHA3-384"), - "SHA3512": (r"^[a-fA-F0-9]{128}$", "SHA3-512"), - "SSDEEP": (r"^[a-zA-Z0-9/+:.]{1,128}$", "SSDEEP"), - "WHIRLPOOL": (r"^[a-fA-F0-9]{128}$", "WHIRLPOOL"), - "TLSH": (r"^[a-fA-F0-9]{70}$", "TLSH"), -} + return dictified, False class HashesProperty(DictionaryProperty): - def clean(self, value): - clean_dict = super(HashesProperty, self).clean(value) - for k, v in copy.deepcopy(clean_dict).items(): - key = k.upper().replace('-', '') - if key in HASHES_REGEX: - vocab_key = HASHES_REGEX[key][1] - if vocab_key == "SSDEEP" and self.spec_version == "2.0": - vocab_key = vocab_key.lower() - if not re.match(HASHES_REGEX[key][0], v): - raise ValueError("'{0}' is not a valid {1} hash".format(v, vocab_key)) - if k != vocab_key: - clean_dict[vocab_key] = clean_dict[k] - del clean_dict[k] - return clean_dict + def __init__(self, spec_hash_names, spec_version=DEFAULT_VERSION, **kwargs): + super().__init__(spec_version=spec_version, **kwargs) + + self.__spec_hash_names = spec_hash_names + + # Map hash algorithm enum to the given spec mandated name, for those + # names which are recognized as hash algorithms by this library. + self.__alg_to_spec_name = {} + for spec_hash_name in spec_hash_names: + alg = stix2.hashes.infer_hash_algorithm(spec_hash_name) + if alg: + self.__alg_to_spec_name[alg] = spec_hash_name + + def clean(self, value, allow_custom): + # ignore the has_custom return value here; there is no customization + # of DictionaryProperties. + clean_dict, _ = super().clean(value, allow_custom) + + spec_dict = {} + + has_custom = False + for hash_k, hash_v in clean_dict.items(): + hash_alg = stix2.hashes.infer_hash_algorithm(hash_k) + + if hash_alg: + # Library-supported hash algorithm: sanity check the value. + if not stix2.hashes.check_hash(hash_alg, hash_v): + raise ValueError( + "'{0}' is not a valid {1} hash".format( + hash_v, hash_alg.name, + ), + ) + + spec_name = self.__alg_to_spec_name.get(hash_alg) + if not spec_name: + # There is library support for the hash algorithm, but it's + # not in the spec. So it's custom. Just use the user's + # name as-is. + has_custom = True + spec_name = hash_k + + else: + # Unrecognized hash algorithm; use as-is. Hash algorithm name + # must be an exact match from spec, or it will be considered + # custom. + spec_name = hash_k + if spec_name not in self.__spec_hash_names: + has_custom = True + + if not allow_custom and has_custom: + raise CustomContentError( + "custom hash algorithm: " + hash_k, + ) + + spec_dict[spec_name] = hash_v + + return spec_dict, has_custom class BinaryProperty(Property): - def clean(self, value): + def clean(self, value, allow_custom=False): try: base64.b64decode(value) except (binascii.Error, TypeError): raise ValueError("must contain a base64 encoded string") - return value + return value, False class HexProperty(Property): - def clean(self, value): + def clean(self, value, allow_custom=False): if not re.match(r"^([a-fA-F0-9]{2})+$", value): raise ValueError("must contain an even number of hexadecimal characters") - return value + return value, False class ReferenceProperty(Property): - def __init__(self, valid_types=None, invalid_types=None, spec_version=version.DEFAULT_VERSION, **kwargs): + _WHITELIST, _BLACKLIST = range(2) + + def __init__(self, valid_types=None, invalid_types=None, spec_version=DEFAULT_VERSION, **kwargs): """ references sometimes must be to a specific object type """ self.spec_version = spec_version - # These checks need to be done prior to the STIX object finishing construction - # and thus we can't use base.py's _check_mutually_exclusive_properties() - # in the typical location of _check_object_constraints() in sdo.py - if valid_types and invalid_types: - raise MutuallyExclusivePropertiesError(self.__class__, ['invalid_types', 'valid_types']) - elif valid_types is None and invalid_types is None: - raise MissingPropertiesError(self.__class__, ['invalid_types', 'valid_types']) + if (valid_types is not None and invalid_types is not None) or \ + (valid_types is None and invalid_types is None): + raise ValueError( + "Exactly one of 'valid_types' and 'invalid_types' must be " + "given", + ) - if valid_types and type(valid_types) is not list: + if valid_types and not isinstance(valid_types, list): valid_types = [valid_types] - elif invalid_types and type(invalid_types) is not list: + elif invalid_types and not isinstance(invalid_types, list): invalid_types = [invalid_types] - self.valid_types = valid_types - self.invalid_types = invalid_types + if valid_types is not None and len(valid_types) == 0: + raise ValueError("Impossible type constraint: empty whitelist") + + self.auth_type = self._WHITELIST if valid_types else self._BLACKLIST + + # Divide type requirements into generic type classes and specific + # types. With respect to strings, values recognized as STIXTypeClass + # enum names are generic; all else are specifics. + self.generics = set() + self.specifics = set() + types = valid_types or invalid_types + for type_ in types: + try: + enum_value = to_enum(type_, STIXTypeClass) + except KeyError: + self.specifics.add(type_) + else: + self.generics.add(enum_value) super(ReferenceProperty, self).__init__(**kwargs) - def clean(self, value): + def clean(self, value, allow_custom): if isinstance(value, _STIXBase): value = value.id value = str(value) - possible_prefix = value[:value.index('--')] + _validate_id(value, self.spec_version, None) - if self.valid_types: - ref_valid_types = enumerate_types(self.valid_types, self.spec_version) + obj_type = get_type_from_id(value) - if possible_prefix in ref_valid_types: - required_prefix = possible_prefix + # Only comes into play when inverting a hybrid whitelist. + # E.g. if the possible generic categories are A, B, C, then the + # inversion of whitelist constraint "A or x" (where x is a specific + # type) is something like "[not (B or C)] or x". In other words, we + # invert the generic categories to produce a blacklist, but leave the + # specific categories alone; they essentially become exceptions to our + # blacklist. + blacklist_exceptions = set() + + generics = self.generics + specifics = self.specifics + auth_type = self.auth_type + if allow_custom and auth_type == self._WHITELIST and generics: + # If allowing customization and using a whitelist, and if generic + # "category" types were given, we need to allow custom object types + # of those categories. Unless registered, it's impossible to know + # whether a given type is within a given category. So we take a + # permissive approach and allow any type which is not known to be + # in the wrong category. I.e. flip the whitelist set to a + # blacklist of a complementary set. + auth_type = self._BLACKLIST + generics = set(STIXTypeClass) - generics + blacklist_exceptions, specifics = specifics, blacklist_exceptions + + if auth_type == self._WHITELIST: + type_ok = is_stix_type( + obj_type, self.spec_version, *generics + ) or obj_type in specifics + + else: + type_ok = ( + not is_stix_type( + obj_type, self.spec_version, *generics + ) and obj_type not in specifics + ) or obj_type in blacklist_exceptions + + # We need to figure out whether the referenced object is custom or + # not. No good way to do that at present... just check if + # unregistered and for the "x-" type prefix, for now? + has_custom = not is_object(obj_type, self.spec_version) \ + or obj_type.startswith("x-") + + if not type_ok: + types = self.specifics.union(self.generics) + types = ", ".join(x.name if isinstance(x, STIXTypeClass) else x for x in types) + if self.auth_type == self._WHITELIST: + msg = "not one of the valid types for this property: %s." % types else: - raise ValueError("The type-specifying prefix '%s' for this property is not valid" % (possible_prefix)) - elif self.invalid_types: - ref_invalid_types = enumerate_types(self.invalid_types, self.spec_version) + msg = "one of the invalid types for this property: %s." % types + if not allow_custom and has_custom: + msg += " A custom object type may be allowed with allow_custom=True." + raise ValueError( + "The type-specifying prefix '%s' for this property is %s" + % (obj_type, msg), + ) - if possible_prefix not in ref_invalid_types: - required_prefix = possible_prefix - else: - raise ValueError("An invalid type-specifying prefix '%s' was specified for this property" % (possible_prefix)) + if not allow_custom and has_custom: + raise CustomContentError( + "reference to custom object type: " + obj_type, + ) - _validate_id(value, self.spec_version, required_prefix) - - return value - - -def enumerate_types(types, spec_version): - """ - `types` is meant to be a list; it may contain specific object types and/or - the any of the words "SCO", "SDO", or "SRO" - - Since "SCO", "SDO", and "SRO" are general types that encompass various specific object types, - once each of those words is being processed, that word will be removed from `return_types`, - so as not to mistakenly allow objects to be created of types "SCO", "SDO", or "SRO" - """ - return_types = [] - return_types += types - - if "SDO" in types: - return_types.remove("SDO") - return_types += registry.STIX2_OBJ_MAPS[spec_version]['objects'].keys() - if "SCO" in types: - return_types.remove("SCO") - return_types += registry.STIX2_OBJ_MAPS[spec_version]['observables'].keys() - if "SRO" in types: - return_types.remove("SRO") - return_types += ['relationship', 'sighting'] - - return return_types + return value, has_custom SELECTOR_REGEX = re.compile(r"^([a-z0-9_-]{3,250}(\.(\[\d+\]|[a-z0-9_-]{1,250}))*|id)$") @@ -545,10 +619,10 @@ SELECTOR_REGEX = re.compile(r"^([a-z0-9_-]{3,250}(\.(\[\d+\]|[a-z0-9_-]{1,250})) class SelectorProperty(Property): - def clean(self, value): + def clean(self, value, allow_custom=False): if not SELECTOR_REGEX.match(value): raise ValueError("must adhere to selector syntax.") - return value + return value, False class ObjectReferenceProperty(StringProperty): @@ -566,28 +640,74 @@ class EmbeddedObjectProperty(Property): self.type = type super(EmbeddedObjectProperty, self).__init__(**kwargs) - def clean(self, value): - if type(value) is dict: - value = self.type(**value) + def clean(self, value, allow_custom): + if isinstance(value, dict): + value = self.type(allow_custom=allow_custom, **value) elif not isinstance(value, self.type): raise ValueError("must be of type {}.".format(self.type.__name__)) - return value + + has_custom = False + if isinstance(value, _STIXBase): + has_custom = value.has_custom + + if not allow_custom and has_custom: + raise CustomContentError("custom content encountered") + + return value, has_custom class EnumProperty(StringProperty): + """ + Used for enumeration type properties. Properties of this type do not allow + customization. + """ def __init__(self, allowed, **kwargs): - if type(allowed) is not list: - allowed = list(allowed) + if isinstance(allowed, str): + allowed = [allowed] self.allowed = allowed super(EnumProperty, self).__init__(**kwargs) - def clean(self, value): - cleaned_value = super(EnumProperty, self).clean(value) + def clean(self, value, allow_custom): + cleaned_value, _ = super(EnumProperty, self).clean(value, allow_custom) + if cleaned_value not in self.allowed: raise ValueError("value '{}' is not valid for this enumeration.".format(cleaned_value)) - return cleaned_value + return cleaned_value, False + + +class OpenVocabProperty(StringProperty): + """ + Used for open vocab type properties. + """ + + def __init__(self, allowed, **kwargs): + super(OpenVocabProperty, self).__init__(**kwargs) + + if isinstance(allowed, str): + allowed = [allowed] + self.allowed = allowed + + def clean(self, value, allow_custom): + cleaned_value, _ = super(OpenVocabProperty, self).clean( + value, allow_custom, + ) + + # Disabled: it was decided that enforcing this is too strict (might + # break too much user code). Revisit when we have the capability for + # more granular config settings when creating objects. + # + # has_custom = cleaned_value not in self.allowed + # + # if not allow_custom and has_custom: + # raise CustomContentError( + # "custom value in open vocab: '{}'".format(cleaned_value), + # ) + + has_custom = False + + return cleaned_value, has_custom class PatternProperty(StringProperty): @@ -598,12 +718,11 @@ class ObservableProperty(Property): """Property for holding Cyber Observable Objects. """ - def __init__(self, spec_version=version.DEFAULT_VERSION, allow_custom=False, *args, **kwargs): - self.allow_custom = allow_custom + def __init__(self, spec_version=DEFAULT_VERSION, *args, **kwargs): self.spec_version = spec_version super(ObservableProperty, self).__init__(*args, **kwargs) - def clean(self, value): + def clean(self, value, allow_custom): try: dictified = _get_dict(value) # get deep copy since we are going modify the dict and might @@ -617,27 +736,41 @@ class ObservableProperty(Property): valid_refs = {k: v['type'] for (k, v) in dictified.items()} + has_custom = False for key, obj in dictified.items(): parsed_obj = parse_observable( obj, valid_refs, - allow_custom=self.allow_custom, + allow_custom=allow_custom, version=self.spec_version, ) + + if isinstance(parsed_obj, _STIXBase): + has_custom = has_custom or parsed_obj.has_custom + else: + # we get dicts for unregistered custom objects + has_custom = True + + if not allow_custom and has_custom: + raise CustomContentError( + "customized {} observable found".format( + parsed_obj["type"], + ), + ) + dictified[key] = parsed_obj - return dictified + return dictified, has_custom class ExtensionsProperty(DictionaryProperty): """Property for representing extensions on Observable objects. """ - def __init__(self, spec_version=version.DEFAULT_VERSION, allow_custom=False, required=False): - self.allow_custom = allow_custom + def __init__(self, spec_version=DEFAULT_VERSION, required=False): super(ExtensionsProperty, self).__init__(spec_version=spec_version, required=required) - def clean(self, value): + def clean(self, value, allow_custom): try: dictified = _get_dict(value) # get deep copy since we are going modify the dict and might @@ -647,40 +780,50 @@ class ExtensionsProperty(DictionaryProperty): except ValueError: raise ValueError("The extensions property must contain a dictionary") - extension_type_map = registry.STIX2_OBJ_MAPS[self.spec_version].get('extensions', {}) + has_custom = False + extension_type_map = STIX2_OBJ_MAPS[self.spec_version].get('extensions', {}) for key, subvalue in dictified.items(): if key in extension_type_map: cls = extension_type_map[key] if type(subvalue) is dict: - if self.allow_custom: - subvalue['allow_custom'] = True - dictified[key] = cls(**subvalue) - else: - dictified[key] = cls(**subvalue) + ext = cls(allow_custom=allow_custom, **subvalue) elif type(subvalue) is cls: # If already an instance of an _Extension class, assume it's valid - dictified[key] = subvalue + ext = subvalue else: raise ValueError("Cannot determine extension type.") + + has_custom = has_custom or ext.has_custom + + if not allow_custom and has_custom: + raise CustomContentError( + "custom content found in {} extension".format( + key, + ), + ) + + dictified[key] = ext + else: - if self.allow_custom: + if allow_custom: + has_custom = True dictified[key] = subvalue elif key.startswith('extension-definition--'): _validate_id(key, '2.1', 'extension-definition') dictified[key] = subvalue else: raise CustomContentError("Can't parse unknown extension type: {}".format(key)) - return dictified + + return dictified, has_custom class STIXObjectProperty(Property): - def __init__(self, spec_version=version.DEFAULT_VERSION, allow_custom=False, *args, **kwargs): - self.allow_custom = allow_custom + def __init__(self, spec_version=DEFAULT_VERSION, *args, **kwargs): self.spec_version = spec_version super(STIXObjectProperty, self).__init__(*args, **kwargs) - def clean(self, value): + def clean(self, value, allow_custom): # Any STIX Object (SDO, SRO, or Marking Definition) can be added to # a bundle with no further checks. stix2_classes = {'_DomainObject', '_RelationshipObject', 'MarkingDefinition'} @@ -700,7 +843,11 @@ class STIXObjectProperty(Property): "containing objects of a different spec " "version.", ) - return value + + if not allow_custom and value.has_custom: + raise CustomContentError("custom content encountered") + + return value, value.has_custom try: dictified = _get_dict(value) except ValueError: @@ -716,6 +863,22 @@ class STIXObjectProperty(Property): "containing objects of a different spec version.", ) - parsed_obj = parse(dictified, allow_custom=self.allow_custom) + parsed_obj = parse(dictified, allow_custom=allow_custom) - return parsed_obj + if isinstance(parsed_obj, _STIXBase): + has_custom = parsed_obj.has_custom + else: + # we get dicts for unregistered custom objects + has_custom = True + + if not allow_custom and has_custom: + # parse() will ignore the caller's allow_custom=False request if + # the object type is registered and dictified has a + # "custom_properties" key. So we have to do another check here. + raise CustomContentError( + "customized {} object found".format( + parsed_obj["type"], + ), + ) + + return parsed_obj, has_custom diff --git a/stix2/test/test_hashes.py b/stix2/test/test_hashes.py new file mode 100644 index 0000000..40ace38 --- /dev/null +++ b/stix2/test/test_hashes.py @@ -0,0 +1,65 @@ +import pytest + +from stix2.hashes import Hash, check_hash, infer_hash_algorithm + + +@pytest.mark.parametrize( + "hash_name, expected_alg", [ + ("md5", Hash.MD5), + ("md6", Hash.MD6), + ("ripemd160", Hash.RIPEMD160), + ("sha1", Hash.SHA1), + ("sha224", Hash.SHA224), + ("sha256", Hash.SHA256), + ("sha384", Hash.SHA384), + ("sha512", Hash.SHA512), + ("sha3224", Hash.SHA3224), + ("sha3256", Hash.SHA3256), + ("sha3384", Hash.SHA3384), + ("sha3512", Hash.SHA3512), + ("ssdeep", Hash.SSDEEP), + ("whirlpool", Hash.WHIRLPOOL), + ("tlsh", Hash.TLSH), + ("xxxx", None), + ], +) +def test_hash_inference(hash_name, expected_alg): + alg = infer_hash_algorithm(hash_name) + assert alg == expected_alg + + # Try some other name variations + alg = infer_hash_algorithm(hash_name[0].upper() + hash_name[1:]) + assert alg == expected_alg + + alg = infer_hash_algorithm("-"+hash_name) + assert alg == expected_alg + + +@pytest.mark.parametrize( + "hash_alg, hash_value", [ + (Hash.MD5, "f9e40b9aa5464f3dae711ca524fceb63"), + (Hash.MD6, "f9e40b9aa5464f3dae711ca524fceb63"), + (Hash.RIPEMD160, "8ae5d2e6b1f3a514257f2469b637454931844aeb"), + (Hash.SHA1, "f2c7d4185880c0adcbb4a01d020a69498b16210e"), + (Hash.SHA224, "6743ed70cc26e750ad0108b6b8ad7fc2780c550f7d78adefa04dda05"), + (Hash.SHA256, "a2d1c2081aa932fe72307ab076b9739455bc7a21b3bed367bd9a86ae27af5a40"), + (Hash.SHA384, "bc846457de707f97bce93cca23b5ea58c0326fd8b79ef7b523ba1d0a792f22868732e53a5dcf2f9e3b89eecca9c9b4e3"), + (Hash.SHA512, "896e45c82f9d8ba917d4f95891c967b88304b0a67ccc59aac813ee7ab3bc700bf9ce559e283c35ddba619755f6b70bdff2a07dc9cd337576a143a2aa361d08b1"), + (Hash.SHA3224, "37cb283bc9f6ecf0f94e92d5bd4c1e061ae00d7ed85804d18f981f53"), + (Hash.SHA3256, "d5fc146e37d4fddaeaa57aa88390be5c9ca6bcb18ae1bf2346cbfc36d3310ea2"), + (Hash.SHA3384, "ac97414589b2ef59a87dc5277d156b6cfc8f6b92b7c0e889d8f38a235dd9c1ba4030321beddd13f29519390ba914f70f"), + (Hash.SHA3512, "8dc580ad3abc6305ce5ada7c5920c763720c7733c2a94d28dd5351ffbc162b6b6d21371d91d6559124159025172e19896e09889047aac4ef555cc55456e14b0a"), + (Hash.SSDEEP, "3:AXGBicFlgVNhBGcL6wCrFQEv:AXGHsNhxLsr2C"), + (Hash.WHIRLPOOL, "b752b6eeb497a8bebfc1be1649ca41d57fd1973bffc2261ca196b5474e0f353762f354c1d743581f61c51f4d86921360bc2e8ad35e830578b68b12e884a50894"), + (Hash.TLSH, "6FF02BEF718027B0160B4391212923ED7F1A463D563B1549B86CF62973B197AD2731F8"), + ("foo", "bar"), # unrecognized hash type is accepted as-is + ], +) +def test_hash_check(hash_alg, hash_value): + assert check_hash(hash_alg, hash_value) + assert check_hash(hash_alg, hash_value.upper()) # check case sensitivity + + +def test_hash_check_fail(): + for hash_alg in Hash: + assert not check_hash(hash_alg, "x"*200) diff --git a/stix2/test/test_properties.py b/stix2/test/test_properties.py index dab713e..5116a68 100644 --- a/stix2/test/test_properties.py +++ b/stix2/test/test_properties.py @@ -3,12 +3,15 @@ import datetime as dt import pytest import pytz -import stix2 -from stix2.exceptions import ExtraPropertiesError, STIXError +from stix2.base import _STIXBase +from stix2.exceptions import ( + CustomContentError, ExtraPropertiesError, STIXError, +) from stix2.properties import ( BinaryProperty, BooleanProperty, EmbeddedObjectProperty, EnumProperty, - FloatProperty, HexProperty, IntegerProperty, ListProperty, Property, - StringProperty, TimestampProperty, TypeProperty, + FloatProperty, HashesProperty, HexProperty, IntegerProperty, ListProperty, + OpenVocabProperty, Property, StringProperty, TimestampProperty, + TypeProperty, ) @@ -16,8 +19,8 @@ def test_property(): p = Property() assert p.required is False - assert p.clean('foo') == 'foo' - assert p.clean(3) == 3 + assert p.clean('foo') == ('foo', False) + assert p.clean(3) == (3, False) def test_basic_clean(): @@ -47,7 +50,7 @@ def test_property_default(): assert p.default() == 77 -def test_property_fixed(): +def test_fixed_property(): p = Property(fixed="2.0") assert p.clean("2.0") @@ -65,16 +68,18 @@ def test_property_fixed_and_required(): Property(default=lambda: 3, required=True) -def test_list_property(): +def test_list_property_property_type(): p = ListProperty(StringProperty) - assert p.clean(['abc', 'xyz']) + result = p.clean(['abc', 'xyz'], False) + assert result == (['abc', 'xyz'], False) + with pytest.raises(ValueError): - p.clean([]) + p.clean([], False) def test_list_property_property_type_custom(): - class TestObj(stix2.base._STIXBase): + class TestObj(_STIXBase): _type = "test" _properties = { "foo": StringProperty(), @@ -86,20 +91,26 @@ def test_list_property_property_type_custom(): TestObj(foo="xyz"), ] - assert p.clean(objs_custom) + result = p.clean(objs_custom, True) + assert result == (objs_custom, True) + + with pytest.raises(CustomContentError): + p.clean(objs_custom, False) dicts_custom = [ {"foo": "abc", "bar": 123}, {"foo": "xyz"}, ] - # no opportunity to set allow_custom=True when using dicts + result = p.clean(dicts_custom, True) + assert result == (objs_custom, True) + with pytest.raises(ExtraPropertiesError): - p.clean(dicts_custom) + p.clean(dicts_custom, False) def test_list_property_object_type(): - class TestObj(stix2.base._STIXBase): + class TestObj(_STIXBase): _type = "test" _properties = { "foo": StringProperty(), @@ -107,14 +118,16 @@ def test_list_property_object_type(): p = ListProperty(TestObj) objs = [TestObj(foo="abc"), TestObj(foo="xyz")] - assert p.clean(objs) + result = p.clean(objs, False) + assert result == (objs, False) dicts = [{"foo": "abc"}, {"foo": "xyz"}] - assert p.clean(dicts) + result = p.clean(dicts, False) + assert result == (objs, False) def test_list_property_object_type_custom(): - class TestObj(stix2.base._STIXBase): + class TestObj(_STIXBase): _type = "test" _properties = { "foo": StringProperty(), @@ -126,16 +139,22 @@ def test_list_property_object_type_custom(): TestObj(foo="xyz"), ] - assert p.clean(objs_custom) + result = p.clean(objs_custom, True) + assert result == (objs_custom, True) + + with pytest.raises(CustomContentError): + p.clean(objs_custom, False) dicts_custom = [ {"foo": "abc", "bar": 123}, {"foo": "xyz"}, ] - # no opportunity to set allow_custom=True when using dicts + result = p.clean(dicts_custom, True) + assert result == (objs_custom, True) + with pytest.raises(ExtraPropertiesError): - p.clean(dicts_custom) + p.clean(dicts_custom, False) def test_list_property_bad_element_type(): @@ -144,7 +163,7 @@ def test_list_property_bad_element_type(): def test_list_property_bad_value_type(): - class TestObj(stix2.base._STIXBase): + class TestObj(_STIXBase): _type = "test" _properties = { "foo": StringProperty(), @@ -152,7 +171,7 @@ def test_list_property_bad_value_type(): list_prop = ListProperty(TestObj) with pytest.raises(ValueError): - list_prop.clean([1]) + list_prop.clean([1], False) def test_string_property(): @@ -296,7 +315,7 @@ def test_boolean_property_invalid(value): ) def test_timestamp_property_valid(value): ts_prop = TimestampProperty() - assert ts_prop.clean(value) == dt.datetime(2017, 1, 1, 12, 34, 56, tzinfo=pytz.utc) + assert ts_prop.clean(value) == (dt.datetime(2017, 1, 1, 12, 34, 56, tzinfo=pytz.utc), False) def test_timestamp_property_invalid(): @@ -332,15 +351,103 @@ def test_hex_property(): ) def test_enum_property_valid(value): enum_prop = EnumProperty(value) - assert enum_prop.clean('b') + assert enum_prop.clean('b', False) def test_enum_property_clean(): enum_prop = EnumProperty(['1']) - assert enum_prop.clean(1) == '1' + assert enum_prop.clean(1, False) == ('1', False) def test_enum_property_invalid(): enum_prop = EnumProperty(['a', 'b', 'c']) with pytest.raises(ValueError): - enum_prop.clean('z') + enum_prop.clean('z', False) + + with pytest.raises(ValueError): + enum_prop.clean('z', True) + + +@pytest.mark.xfail( + reason="Temporarily disabled custom open vocab enforcement", + strict=True, +) +@pytest.mark.parametrize( + "vocab", [ + ['a', 'b', 'c'], + ('a', 'b', 'c'), + 'b', + ], +) +def test_openvocab_property(vocab): + ov_prop = OpenVocabProperty(vocab) + + assert ov_prop.clean("b", False) == ("b", False) + assert ov_prop.clean("b", True) == ("b", False) + + with pytest.raises(CustomContentError): + ov_prop.clean("d", False) + + assert ov_prop.clean("d", True) == ("d", True) + + +@pytest.mark.parametrize( + "value", [ + {"sha256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b"}, + [('MD5', '2dfb1bcc980200c6706feee399d41b3f'), ('RIPEMD-160', 'b3a8cd8a27c90af79b3c81754f267780f443dfef')], + ], +) +def test_hashes_property_valid(value): + hash_prop = HashesProperty(["sha256", "md5", "ripemd160"]) + _, has_custom = hash_prop.clean(value, False) + assert not has_custom + + +@pytest.mark.parametrize( + "value", [ + {"MD5": "a"}, + {"SHA-256": "2dfb1bcc980200c6706feee399d41b3f"}, + ], +) +def test_hashes_property_invalid(value): + hash_prop = HashesProperty(["sha256", "md5"]) + + with pytest.raises(ValueError): + hash_prop.clean(value, False) + + +def test_hashes_property_custom(): + value = { + "sha256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b", + "abc-123": "aaaaaaaaaaaaaaaaaaaaa", + } + expected_cleaned_value = { + # cleaning transforms recognized hash algorithm names to the spec- + # mandated name. + "SHA-256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b", + "abc-123": "aaaaaaaaaaaaaaaaaaaaa", + } + + hash_prop = HashesProperty(["SHA-256"]) + result = hash_prop.clean(value, True) + assert result == (expected_cleaned_value, True) + + with pytest.raises(CustomContentError): + hash_prop.clean(value, False) + + +def test_hashes_no_library_support(): + prop = HashesProperty(["foo"]) + + result = prop.clean({"foo": "bar"}, False) + assert result == ({"foo": "bar"}, False) + + result = prop.clean({"foo": "bar"}, True) + assert result == ({"foo": "bar"}, False) + + with pytest.raises(CustomContentError): + # require exact name match for unsupported hash algorithms + prop.clean({"FOO": "bar"}, False) + + result = prop.clean({"FOO": "bar"}, True) + assert result == ({"FOO": "bar"}, True) diff --git a/stix2/test/test_workbench.py b/stix2/test/test_workbench.py index 433bf81..84f97a5 100644 --- a/stix2/test/test_workbench.py +++ b/stix2/test/test_workbench.py @@ -369,6 +369,7 @@ def test_workbench_custom_property_object_in_observable_extension(): x_foo='bar', ) artifact = File( + allow_custom=True, name='test', extensions={'ntfs-ext': ntfs}, ) @@ -390,7 +391,6 @@ def test_workbench_custom_property_dict_in_observable_extension(): name='test', extensions={ 'ntfs-ext': { - 'allow_custom': True, 'sid': 1, 'x_foo': 'bar', }, diff --git a/stix2/test/v20/conftest.py b/stix2/test/v20/conftest.py index 6bb8fae..0e4bc4c 100644 --- a/stix2/test/v20/conftest.py +++ b/stix2/test/v20/conftest.py @@ -55,7 +55,7 @@ def stix_objs1(): "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "labels": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", @@ -67,7 +67,7 @@ def stix_objs1(): "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "labels": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", @@ -79,7 +79,7 @@ def stix_objs1(): "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "labels": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.936Z", "name": "Malicious site hosting downloader", @@ -91,7 +91,7 @@ def stix_objs1(): "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "labels": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", @@ -103,7 +103,7 @@ def stix_objs1(): "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "labels": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", @@ -156,7 +156,7 @@ def stix_objs2(): "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "labels": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-31T13:49:53.935Z", "name": "Malicious site hosting downloader", @@ -168,7 +168,7 @@ def stix_objs2(): "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "labels": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", @@ -180,7 +180,7 @@ def stix_objs2(): "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "labels": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", diff --git a/stix2/test/v20/test_bundle.py b/stix2/test/v20/test_bundle.py index 3843375..07fa24d 100644 --- a/stix2/test/v20/test_bundle.py +++ b/stix2/test/v20/test_bundle.py @@ -223,7 +223,7 @@ def test_stix_object_property(): prop = stix2.properties.STIXObjectProperty(spec_version='2.0') identity = stix2.v20.Identity(name="test", identity_class="individual") - assert prop.clean(identity) is identity + assert prop.clean(identity, False) == (identity, False) def test_bundle_with_different_spec_objects(): diff --git a/stix2/test/v20/test_custom.py b/stix2/test/v20/test_custom.py index b5d86b5..fd45ff4 100644 --- a/stix2/test/v20/test_custom.py +++ b/stix2/test/v20/test_custom.py @@ -157,10 +157,11 @@ def test_custom_properties_dict_in_bundled_object(): 'x_foo': 'bar', }, } - bundle = stix2.v20.Bundle(custom_identity) - assert bundle.objects[0].x_foo == "bar" - assert '"x_foo": "bar"' in str(bundle) + # must not succeed: allow_custom was not set to True when creating + # the bundle, so it must reject the customized identity object. + with pytest.raises(InvalidValueError): + stix2.v20.Bundle(custom_identity) def test_custom_property_in_observed_data(): @@ -188,6 +189,7 @@ def test_custom_property_object_in_observable_extension(): x_foo='bar', ) artifact = stix2.v20.File( + allow_custom=True, name='test', extensions={'ntfs-ext': ntfs}, ) @@ -220,7 +222,6 @@ def test_custom_property_dict_in_observable_extension(): name='test', extensions={ 'ntfs-ext': { - 'allow_custom': True, 'sid': 1, 'x_foo': 'bar', }, @@ -384,6 +385,47 @@ def test_custom_object_invalid_type_name(): assert "Invalid type name 'x_new_object':" in str(excinfo.value) +def test_custom_subobject_dict(): + obj_dict = { + "type": "bundle", + "spec_version": "2.0", + "objects": [ + { + "type": "identity", + "name": "alice", + "identity_class": "individual", + "x_foo": 123, + }, + ], + } + + obj = stix2.parse(obj_dict, allow_custom=True) + assert obj["objects"][0]["x_foo"] == 123 + assert obj.has_custom + + with pytest.raises(InvalidValueError): + stix2.parse(obj_dict, allow_custom=False) + + +def test_custom_subobject_obj(): + ident = stix2.v20.Identity( + name="alice", identity_class=123, x_foo=123, allow_custom=True, + ) + + obj_dict = { + "type": "bundle", + "spec_version": "2.0", + "objects": [ident], + } + + obj = stix2.parse(obj_dict, allow_custom=True) + assert obj["objects"][0]["x_foo"] == 123 + assert obj.has_custom + + with pytest.raises(InvalidValueError): + stix2.parse(obj_dict, allow_custom=False) + + def test_parse_custom_object_type(): nt_string = """{ "type": "x-new-type", @@ -866,6 +908,35 @@ def test_parse_observable_with_custom_extension(): assert parsed.extensions['x-new-ext'].property2 == 12 +def test_parse_observable_with_custom_extension_property(): + input_str = """{ + "type": "observed-data", + "first_observed": "1976-09-09T01:50:24.000Z", + "last_observed": "1988-01-18T15:22:10.000Z", + "number_observed": 5, + "objects": { + "0": { + "type": "file", + "name": "cats.png", + "extensions": { + "raster-image-ext": { + "image_height": 1024, + "image_width": 768, + "x-foo": false + } + } + } + } + }""" + + parsed = stix2.parse(input_str, version='2.0', allow_custom=True) + assert parsed.has_custom + assert parsed["objects"]["0"]["extensions"]["raster-image-ext"]["x-foo"] is False + + with pytest.raises(InvalidValueError): + stix2.parse(input_str, version="2.0", allow_custom=False) + + def test_custom_and_spec_extension_mix(): """ Try to make sure that when allow_custom=True, encountering a custom diff --git a/stix2/test/v20/test_datastore_filters.py b/stix2/test/v20/test_datastore_filters.py index e8945d1..bcbd08f 100644 --- a/stix2/test/v20/test_datastore_filters.py +++ b/stix2/test/v20/test_datastore_filters.py @@ -20,7 +20,7 @@ stix_objs = [ "created": "2014-05-08T09:00:00.000Z", "id": "indicator--a932fcc6-e032-476c-826f-cb970a5a1ade", "labels": [ - "file-hash-watchlist", + "compromised", ], "modified": "2014-05-08T09:00:00.000Z", "name": "File hash for Poison Ivy variant", diff --git a/stix2/test/v20/test_datastore_memory.py b/stix2/test/v20/test_datastore_memory.py index 3357e2c..23b3966 100644 --- a/stix2/test/v20/test_datastore_memory.py +++ b/stix2/test/v20/test_datastore_memory.py @@ -19,7 +19,7 @@ IND1 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "labels": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", @@ -31,7 +31,7 @@ IND2 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "labels": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", @@ -43,7 +43,7 @@ IND3 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "labels": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.936Z", "name": "Malicious site hosting downloader", @@ -55,7 +55,7 @@ IND4 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "labels": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", @@ -67,7 +67,7 @@ IND5 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "labels": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", @@ -79,7 +79,7 @@ IND6 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "labels": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-31T13:49:53.935Z", "name": "Malicious site hosting downloader", @@ -91,7 +91,7 @@ IND7 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "labels": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", @@ -103,7 +103,7 @@ IND8 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "labels": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", @@ -285,7 +285,7 @@ def test_memory_store_object_creator_of_present(mem_store): iden = Identity( id=IDENTITY_ID, name="Foo Corp.", - identity_class="corporation", + identity_class="organization", ) mem_store.add(camp) diff --git a/stix2/test/v20/test_pickle.py b/stix2/test/v20/test_pickle.py index 416341c..36e4337 100644 --- a/stix2/test/v20/test_pickle.py +++ b/stix2/test/v20/test_pickle.py @@ -13,7 +13,7 @@ def test_pickling(): id=IDENTITY_ID, name="alice", description="this is a pickle test", - identity_class="some_class", + identity_class="individual", ) pickle.loads(pickle.dumps(identity)) diff --git a/stix2/test/v20/test_properties.py b/stix2/test/v20/test_properties.py index 87f14c9..2429a7b 100644 --- a/stix2/test/v20/test_properties.py +++ b/stix2/test/v20/test_properties.py @@ -3,14 +3,14 @@ import uuid import pytest import stix2 -import stix2.base from stix2.exceptions import ( AtLeastOnePropertyError, CustomContentError, DictionaryKeyError, + ExtraPropertiesError, ParseError, ) from stix2.properties import ( DictionaryProperty, EmbeddedObjectProperty, ExtensionsProperty, - HashesProperty, IDProperty, ListProperty, ReferenceProperty, - STIXObjectProperty, + HashesProperty, IDProperty, ListProperty, ObservableProperty, + ReferenceProperty, STIXObjectProperty, ) from stix2.v20.common import MarkingProperty @@ -27,7 +27,7 @@ MY_ID = 'my-type--232c9d3f-49fc-4440-bb01-607f638778e7' ], ) def test_id_property_valid(value): - assert ID_PROP.clean(value) == value + assert ID_PROP.clean(value) == (value, False) CONSTANT_IDS = [ @@ -54,7 +54,7 @@ CONSTANT_IDS.extend(constants.RELATIONSHIP_IDS) @pytest.mark.parametrize("value", CONSTANT_IDS) def test_id_property_valid_for_type(value): type = value.split('--', 1)[0] - assert IDProperty(type=type, spec_version="2.0").clean(value) == value + assert IDProperty(type=type, spec_version="2.0").clean(value) == (value, False) def test_id_property_wrong_type(): @@ -80,29 +80,208 @@ def test_id_property_not_a_valid_hex_uuid(value): def test_id_property_default(): default = ID_PROP.default() - assert ID_PROP.clean(default) == default + assert ID_PROP.clean(default) == (default, False) -def test_reference_property(): - ref_prop = ReferenceProperty(valid_types="my-type", spec_version="2.0") +def test_reference_property_whitelist_standard_type(): + ref_prop = ReferenceProperty(valid_types="identity", spec_version="2.0") + result = ref_prop.clean( + "identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False, + ) + assert result == ("identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) - assert ref_prop.clean("my-type--00000000-0000-4000-8000-000000000000") with pytest.raises(ValueError): - ref_prop.clean("foo") + ref_prop.clean("foo--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) - # This is not a valid V4 UUID with pytest.raises(ValueError): - ref_prop.clean("my-type--00000000-0000-0000-0000-000000000000") + ref_prop.clean("foo--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) -def test_reference_property_specific_type(): +def test_reference_property_whitelist_custom_type(): ref_prop = ReferenceProperty(valid_types="my-type", spec_version="2.0") with pytest.raises(ValueError): - ref_prop.clean("not-my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf") + ref_prop.clean("not-my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) - assert ref_prop.clean("my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf") == \ - "my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf" + with pytest.raises(ValueError): + ref_prop.clean("not-my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + with pytest.raises(CustomContentError): + # This is the whitelisted type, but it's still custom, and + # customization is disallowed here. + ref_prop.clean("my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + result = ref_prop.clean("my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + assert result == ("my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + +def test_reference_property_whitelist_generic_type(): + ref_prop = ReferenceProperty( + valid_types=["SCO", "SRO"], spec_version="2.0", + ) + + result = ref_prop.clean("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + result = ref_prop.clean("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + result = ref_prop.clean( + "sighting--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False, + ) + assert result == ("sighting--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + result = ref_prop.clean( + "sighting--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True, + ) + assert result == ("sighting--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + # The prop assumes some-type is a custom type of one of the generic + # type categories. + result = ref_prop.clean( + "some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True, + ) + assert result == ("some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + with pytest.raises(ValueError): + ref_prop.clean("some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + with pytest.raises(ValueError): + ref_prop.clean("identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + with pytest.raises(ValueError): + ref_prop.clean("identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + +def test_reference_property_blacklist_standard_type(): + ref_prop = ReferenceProperty(invalid_types="identity", spec_version="2.0") + result = ref_prop.clean( + "malware--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False, + ) + assert result == ("malware--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + result = ref_prop.clean( + "malware--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True, + ) + assert result == ("malware--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + with pytest.raises(CustomContentError): + ref_prop.clean( + "some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False, + ) + + result = ref_prop.clean( + "some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True, + ) + assert result == ("some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + with pytest.raises(ValueError): + ref_prop.clean( + "identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False, + ) + + with pytest.raises(ValueError): + ref_prop.clean( + "identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True, + ) + + +def test_reference_property_blacklist_generic_type(): + ref_prop = ReferenceProperty( + invalid_types=["SDO", "SRO"], spec_version="2.0", + ) + + result = ref_prop.clean( + "file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False, + ) + assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + result = ref_prop.clean( + "file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True, + ) + assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + with pytest.raises(CustomContentError): + ref_prop.clean( + "some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False, + ) + + result = ref_prop.clean( + "some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True, + ) + assert result == ("some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + with pytest.raises(ValueError): + ref_prop.clean( + "identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False, + ) + + with pytest.raises(ValueError): + ref_prop.clean( + "identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True, + ) + + with pytest.raises(ValueError): + ref_prop.clean( + "relationship--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False, + ) + + with pytest.raises(ValueError): + ref_prop.clean( + "relationship--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True, + ) + + +def test_reference_property_whitelist_hybrid_type(): + p = ReferenceProperty(valid_types=["a", "SCO"], spec_version="2.0") + + result = p.clean("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + result = p.clean("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + with pytest.raises(CustomContentError): + # although whitelisted, "a" is a custom type + p.clean("a--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + result = p.clean("a--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + assert result == ("a--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + with pytest.raises(ValueError): + p.clean("b--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + # should just assume "b" is a custom SCO type. + result = p.clean("b--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + assert result == ("b--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + +def test_reference_property_blacklist_hybrid_type(): + p = ReferenceProperty(invalid_types=["a", "SCO"], spec_version="2.0") + + with pytest.raises(ValueError): + p.clean("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + with pytest.raises(ValueError): + p.clean("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + with pytest.raises(ValueError): + p.clean("a--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + with pytest.raises(ValueError): + p.clean("a--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + with pytest.raises(CustomContentError): + p.clean("b--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + # should just assume "b" is a custom type which is not an SCO + result = p.clean("b--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + assert result == ("b--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + +def test_reference_property_impossible_constraint(): + with pytest.raises(ValueError): + ReferenceProperty(valid_types=[], spec_version="2.0") @pytest.mark.parametrize( @@ -176,27 +355,29 @@ def test_property_list_of_dictionary(): @pytest.mark.parametrize( - "value", [ - {"sha256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b"}, - [('MD5', '2dfb1bcc980200c6706feee399d41b3f'), ('RIPEMD-160', 'b3a8cd8a27c90af79b3c81754f267780f443dfef')], + "key", [ + "aaa", + "a"*256, + "a-1_b", ], ) -def test_hashes_property_valid(value): - hash_prop = HashesProperty() - assert hash_prop.clean(value) +def test_hash_property_valid_key(key): + p = HashesProperty(["foo"], spec_version="2.0") + result = p.clean({key: "bar"}, True) + assert result == ({key: "bar"}, True) @pytest.mark.parametrize( - "value", [ - {"MD5": "a"}, - {"SHA-256": "2dfb1bcc980200c6706feee399d41b3f"}, + "key", [ + "aa", + "a"*257, + "funny%chars?", ], ) -def test_hashes_property_invalid(value): - hash_prop = HashesProperty() - - with pytest.raises(ValueError): - hash_prop.clean(value) +def test_hash_property_invalid_key(key): + p = HashesProperty(["foo"], spec_version="2.0") + with pytest.raises(DictionaryKeyError): + p.clean({key: "foo"}, True) def test_embedded_property(): @@ -206,25 +387,103 @@ def test_embedded_property(): content_disposition="inline", body="Cats are funny!", ) - assert emb_prop.clean(mime) + result = emb_prop.clean(mime, False) + assert result == (mime, False) + + result = emb_prop.clean(mime, True) + assert result == (mime, False) with pytest.raises(ValueError): - emb_prop.clean("string") + emb_prop.clean("string", False) + + +def test_embedded_property_dict(): + emb_prop = EmbeddedObjectProperty(type=stix2.v20.EmailMIMEComponent) + mime = { + "content_type": "text/plain; charset=utf-8", + "content_disposition": "inline", + "body": "Cats are funny!", + } + + result = emb_prop.clean(mime, False) + assert isinstance(result[0], stix2.v20.EmailMIMEComponent) + assert result[0]["body"] == "Cats are funny!" + assert not result[1] + + result = emb_prop.clean(mime, True) + assert isinstance(result[0], stix2.v20.EmailMIMEComponent) + assert result[0]["body"] == "Cats are funny!" + assert not result[1] + + +def test_embedded_property_custom(): + emb_prop = EmbeddedObjectProperty(type=stix2.v20.EmailMIMEComponent) + mime = stix2.v20.EmailMIMEComponent( + content_type="text/plain; charset=utf-8", + content_disposition="inline", + body="Cats are funny!", + foo=123, + allow_custom=True, + ) + + with pytest.raises(CustomContentError): + emb_prop.clean(mime, False) + + result = emb_prop.clean(mime, True) + assert result == (mime, True) + + +def test_embedded_property_dict_custom(): + emb_prop = EmbeddedObjectProperty(type=stix2.v20.EmailMIMEComponent) + mime = { + "content_type": "text/plain; charset=utf-8", + "content_disposition": "inline", + "body": "Cats are funny!", + "foo": 123, + } + + with pytest.raises(ExtraPropertiesError): + emb_prop.clean(mime, False) + + result = emb_prop.clean(mime, True) + assert isinstance(result[0], stix2.v20.EmailMIMEComponent) + assert result[0]["body"] == "Cats are funny!" + assert result[1] def test_extension_property_valid(): ext_prop = ExtensionsProperty(spec_version="2.0") - assert ext_prop({ - 'windows-pebinary-ext': { - 'pe_type': 'exe', - }, - }) + result = ext_prop.clean( + { + 'windows-pebinary-ext': { + 'pe_type': 'exe', + }, + }, False, + ) + + assert isinstance( + result[0]["windows-pebinary-ext"], stix2.v20.WindowsPEBinaryExt, + ) + assert not result[1] + + result = ext_prop.clean( + { + 'windows-pebinary-ext': { + 'pe_type': 'exe', + }, + }, True, + ) + + assert isinstance( + result[0]["windows-pebinary-ext"], stix2.v20.WindowsPEBinaryExt, + ) + assert not result[1] def test_extension_property_invalid1(): ext_prop = ExtensionsProperty(spec_version="2.0") with pytest.raises(ValueError): - ext_prop.clean(1) + ext_prop.clean(1, False) def test_extension_property_invalid2(): @@ -236,8 +495,61 @@ def test_extension_property_invalid2(): 'pe_type': 'exe', }, }, + False, ) + result = ext_prop.clean( + { + 'foobar-ext': { + 'pe_type': 'exe', + }, + }, True, + ) + assert result == ({"foobar-ext": {"pe_type": "exe"}}, True) + + +def test_extension_property_invalid3(): + ext_prop = ExtensionsProperty(spec_version="2.0") + with pytest.raises(ExtraPropertiesError): + ext_prop.clean( + { + 'windows-pebinary-ext': { + 'pe_type': 'exe', + 'abc': 123, + }, + }, + False, + ) + + result = ext_prop.clean( + { + 'windows-pebinary-ext': { + 'pe_type': 'exe', + 'abc': 123, + }, + }, True, + ) + + assert isinstance( + result[0]["windows-pebinary-ext"], stix2.v20.WindowsPEBinaryExt, + ) + assert result[0]["windows-pebinary-ext"]["abc"] == 123 + assert result[1] + + +def test_extension_property_invalid_type(): + ext_prop = ExtensionsProperty(spec_version="2.0") + with pytest.raises(CustomContentError) as excinfo: + ext_prop.clean( + { + 'windows-pebinary-ext': { + 'pe_type': 'exe', + }, + }, + False, + ) + assert "Can't parse unknown extension" in str(excinfo.value) + def test_extension_at_least_one_property_constraint(): with pytest.raises(AtLeastOnePropertyError): @@ -259,6 +571,116 @@ def test_stix_property_not_compliant_spec(): stix_prop = STIXObjectProperty(spec_version="2.0") with pytest.raises(ValueError) as excinfo: - stix_prop.clean(indicator) + stix_prop.clean(indicator, False) assert "Spec version 2.0 bundles don't yet support containing objects of a different spec version." in str(excinfo.value) + + +def test_observable_property_obj(): + prop = ObservableProperty(spec_version="2.0") + + obs = stix2.v20.File(name="data.dat") + obs_dict = { + "0": obs, + } + + result = prop.clean(obs_dict, False) + assert result[0]["0"] == obs + assert not result[1] + + result = prop.clean(obs_dict, True) + assert result[0]["0"] == obs + assert not result[1] + + +def test_observable_property_dict(): + prop = ObservableProperty(spec_version="2.0") + + obs_dict = { + "0": { + "type": "file", + "name": "data.dat", + }, + } + + result = prop.clean(obs_dict, False) + assert isinstance(result[0]["0"], stix2.v20.File) + assert result[0]["0"]["name"] == "data.dat" + assert not result[1] + + result = prop.clean(obs_dict, True) + assert isinstance(result[0]["0"], stix2.v20.File) + assert result[0]["0"]["name"] == "data.dat" + assert not result[1] + + +def test_observable_property_obj_custom(): + prop = ObservableProperty(spec_version="2.0") + + obs = stix2.v20.File(name="data.dat", foo=True, allow_custom=True) + obs_dict = { + "0": obs, + } + + with pytest.raises(ExtraPropertiesError): + prop.clean(obs_dict, False) + + result = prop.clean(obs_dict, True) + assert result[0]["0"] == obs + assert result[1] + + +def test_observable_property_dict_custom(): + prop = ObservableProperty(spec_version="2.0") + + obs_dict = { + "0": { + "type": "file", + "name": "data.dat", + "foo": True, + }, + } + + with pytest.raises(ExtraPropertiesError): + prop.clean(obs_dict, False) + + result = prop.clean(obs_dict, True) + assert isinstance(result[0]["0"], stix2.v20.File) + assert result[0]["0"]["foo"] + assert result[1] + + +def test_stix_object_property_custom_prop(): + prop = STIXObjectProperty(spec_version="2.0") + + obj_dict = { + "type": "identity", + "name": "alice", + "identity_class": "supergirl", + "foo": "bar", + } + + with pytest.raises(ExtraPropertiesError): + prop.clean(obj_dict, False) + + result = prop.clean(obj_dict, True) + assert isinstance(result[0], stix2.v20.Identity) + assert result[0]["foo"] == "bar" + assert result[1] + + +def test_stix_object_property_custom_obj(): + prop = STIXObjectProperty(spec_version="2.0") + + obj_dict = { + "type": "something", + "abc": 123, + "xyz": ["a", 1], + } + + with pytest.raises(ParseError): + prop.clean(obj_dict, False) + + result = prop.clean(obj_dict, True) + assert result[0] == {"type": "something", "abc": 123, "xyz": ["a", 1]} + assert result[1] diff --git a/stix2/test/v20/test_report.py b/stix2/test/v20/test_report.py index 557a374..6ef5c08 100644 --- a/stix2/test/v20/test_report.py +++ b/stix2/test/v20/test_report.py @@ -4,6 +4,7 @@ import pytest import pytz import stix2 +from stix2.exceptions import InvalidValueError from .constants import ( CAMPAIGN_ID, IDENTITY_ID, INDICATOR_ID, INDICATOR_KWARGS, RELATIONSHIP_ID, @@ -133,3 +134,30 @@ def test_parse_report(data): assert rept.name == "The Black Vine Cyberespionage Group" # TODO: Add other examples + + +def test_report_on_custom(): + with pytest.raises(InvalidValueError): + stix2.v20.Report( + name="my report", + labels=["a label"], + published="2016-01-20T17:00:00Z", + object_refs=[ + "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "some-type--2672975a-ce1e-4473-a1c6-0d79868930c7", + ], + ) + + report = stix2.v20.Report( + name="my report", + labels=["a label"], + published="2016-01-20T17:00:00Z", + object_refs=[ + "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "some-type--2672975a-ce1e-4473-a1c6-0d79868930c7", + ], + allow_custom=True, + ) + + assert "some-type--2672975a-ce1e-4473-a1c6-0d79868930c7" \ + in report.object_refs diff --git a/stix2/test/v20/test_versioning.py b/stix2/test/v20/test_versioning.py index 665ac4c..d3973f0 100644 --- a/stix2/test/v20/test_versioning.py +++ b/stix2/test/v20/test_versioning.py @@ -433,7 +433,7 @@ def test_version_marking(): def test_version_disable_custom(): m = stix2.v20.Malware( - name="foo", labels=["label"], description="Steals your identity!", + name="foo", labels=["spyware"], description="Steals your identity!", x_custom=123, allow_custom=True, ) @@ -450,7 +450,7 @@ def test_version_disable_custom(): def test_version_enable_custom(): m = stix2.v20.Malware( - name="foo", labels=["label"], description="Steals your identity!", + name="foo", labels=["spyware"], description="Steals your identity!", ) # Add a custom property to an object for which it was previously disallowed @@ -464,7 +464,7 @@ def test_version_enable_custom(): def test_version_propagate_custom(): m = stix2.v20.Malware( - name="foo", labels=["label"], + name="foo", labels=["spyware"], ) # Remember custom-not-allowed setting from original; produce error @@ -476,7 +476,7 @@ def test_version_propagate_custom(): assert m2.description == "Steals your identity!" m_custom = stix2.v20.Malware( - name="foo", labels=["label"], x_custom=123, allow_custom=True, + name="foo", labels=["spyware"], x_custom=123, allow_custom=True, ) # Remember custom-allowed setting from original; should work diff --git a/stix2/test/v21/conftest.py b/stix2/test/v21/conftest.py index 6efcf39..9e37d92 100644 --- a/stix2/test/v21/conftest.py +++ b/stix2/test/v21/conftest.py @@ -66,7 +66,7 @@ def stix_objs1(): "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "indicator_types": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", @@ -80,7 +80,7 @@ def stix_objs1(): "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "indicator_types": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", @@ -94,7 +94,7 @@ def stix_objs1(): "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "indicator_types": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.936Z", "name": "Malicious site hosting downloader", @@ -108,7 +108,7 @@ def stix_objs1(): "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "indicator_types": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", @@ -122,7 +122,7 @@ def stix_objs1(): "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "indicator_types": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", @@ -183,7 +183,7 @@ def stix_objs2(): "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "indicator_types": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-31T13:49:53.935Z", "name": "Malicious site hosting downloader", @@ -197,7 +197,7 @@ def stix_objs2(): "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "indicator_types": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", @@ -211,7 +211,7 @@ def stix_objs2(): "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "indicator_types": [ - "url-watchlist", + "malicious-activity", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", diff --git a/stix2/test/v21/test_bundle.py b/stix2/test/v21/test_bundle.py index 008fcf1..35dd52c 100644 --- a/stix2/test/v21/test_bundle.py +++ b/stix2/test/v21/test_bundle.py @@ -234,7 +234,7 @@ def test_stix_object_property(): prop = stix2.properties.STIXObjectProperty(spec_version='2.1') identity = stix2.v21.Identity(name="test", identity_class="individual") - assert prop.clean(identity) is identity + assert prop.clean(identity, False) == (identity, False) def test_bundle_obj_id_found(): diff --git a/stix2/test/v21/test_custom.py b/stix2/test/v21/test_custom.py index 2fc8db0..4f60129 100644 --- a/stix2/test/v21/test_custom.py +++ b/stix2/test/v21/test_custom.py @@ -206,8 +206,10 @@ def test_custom_properties_dict_in_bundled_object(): 'x_foo': 'bar', }, } - bundle = stix2.v21.Bundle(custom_identity) + with pytest.raises(InvalidValueError): + stix2.v21.Bundle(custom_identity) + bundle = stix2.v21.Bundle(custom_identity, allow_custom=True) assert bundle.objects[0].x_foo == "bar" assert '"x_foo": "bar"' in str(bundle) @@ -251,6 +253,7 @@ def test_custom_property_object_in_observable_extension(): x_foo='bar', ) artifact = stix2.v21.File( + allow_custom=True, name='test', extensions={'ntfs-ext': ntfs}, ) @@ -283,7 +286,6 @@ def test_custom_property_dict_in_observable_extension(): name='test', extensions={ 'ntfs-ext': { - 'allow_custom': True, 'sid': 1, 'x_foo': 'bar', }, @@ -506,6 +508,48 @@ def test_custom_object_invalid_type_name(): assert "Invalid type name '7x-new-object':" in str(excinfo.value) +def test_custom_subobject_dict(): + obj_dict = { + "type": "bundle", + "id": "bundle--78d99c4a-4eda-4c59-b264-60807f05d799", + "objects": [ + { + "type": "identity", + "spec_version": "2.1", + "name": "alice", + "identity_class": "individual", + "x_foo": 123, + }, + ], + } + + obj = stix2.parse(obj_dict, allow_custom=True) + assert obj["objects"][0]["x_foo"] == 123 + assert obj.has_custom + + with pytest.raises(InvalidValueError): + stix2.parse(obj_dict, allow_custom=False) + + +def test_custom_subobject_obj(): + ident = stix2.v21.Identity( + name="alice", identity_class=123, x_foo=123, allow_custom=True, + ) + + obj_dict = { + "type": "bundle", + "id": "bundle--78d99c4a-4eda-4c59-b264-60807f05d799", + "objects": [ident], + } + + obj = stix2.parse(obj_dict, allow_custom=True) + assert obj["objects"][0]["x_foo"] == 123 + assert obj.has_custom + + with pytest.raises(InvalidValueError): + stix2.parse(obj_dict, allow_custom=False) + + def test_parse_custom_object_type(): nt_string = """{ "type": "x-new-type", @@ -1075,6 +1119,37 @@ def test_parse_observable_with_custom_extension(): assert parsed.extensions['x-new-ext'].property2 == 12 +def test_parse_observable_with_custom_extension_property(): + input_str = """{ + "type": "observed-data", + "spec_version": "2.1", + "first_observed": "1976-09-09T01:50:24.000Z", + "last_observed": "1988-01-18T15:22:10.000Z", + "number_observed": 5, + "objects": { + "0": { + "type": "file", + "spec_version": "2.1", + "name": "cats.png", + "extensions": { + "raster-image-ext": { + "image_height": 1024, + "image_width": 768, + "x-foo": false + } + } + } + } + }""" + + parsed = stix2.parse(input_str, version='2.1', allow_custom=True) + assert parsed.has_custom + assert parsed["objects"]["0"]["extensions"]["raster-image-ext"]["x-foo"] is False + + with pytest.raises(InvalidValueError): + stix2.parse(input_str, version="2.1", allow_custom=False) + + def test_custom_and_spec_extension_mix(): """ Try to make sure that when allow_custom=True, encountering a custom @@ -1563,3 +1638,30 @@ def test_registered_new_extension_marking_allow_custom_false(): marking_serialized = marking_object.serialize(sort_keys=True) assert '"extensions": {"extension-definition--a932fcc6-e032-176c-126f-cb970a5a1fff": ' \ '{"extension_type": "property-extension", "some_marking_field": "value"}}' in marking_serialized + + +def test_allow_custom_propagation(): + obj_dict = { + "type": "bundle", + "objects": [ + { + "type": "file", + "spec_version": "2.1", + "name": "data.dat", + "extensions": { + "archive-ext": { + "contains_refs": [ + "file--3d4da5f6-31d8-4a66-a172-f31af9bf5238", + "file--4bb16def-cdfc-40d1-b6a4-815de6c60b74", + ], + "x_foo": "bar", + }, + }, + }, + ], + } + + # allow_custom=False at the top level should catch the custom property way + # down in the SCO extension. + with pytest.raises(InvalidValueError): + stix2.parse(obj_dict, allow_custom=False) diff --git a/stix2/test/v21/test_datastore_filters.py b/stix2/test/v21/test_datastore_filters.py index a6a50a7..0e531fd 100644 --- a/stix2/test/v21/test_datastore_filters.py +++ b/stix2/test/v21/test_datastore_filters.py @@ -24,7 +24,7 @@ stix_objs = [ "created": "2014-05-08T09:00:00.000Z", "id": "indicator--a932fcc6-e032-476c-826f-cb970a5a1ade", "indicator_types": [ - "file-hash-watchlist", + "compromised", ], "modified": "2014-05-08T09:00:00.000Z", "name": "File hash for Poison Ivy variant", diff --git a/stix2/test/v21/test_datastore_memory.py b/stix2/test/v21/test_datastore_memory.py index 870f82e..f2c9a8c 100644 --- a/stix2/test/v21/test_datastore_memory.py +++ b/stix2/test/v21/test_datastore_memory.py @@ -300,7 +300,7 @@ def test_memory_store_object_creator_of_present(mem_store): iden = Identity( id=IDENTITY_ID, name="Foo Corp.", - identity_class="corporation", + identity_class="organization", ) mem_store.add(camp) diff --git a/stix2/test/v21/test_deterministic_ids.py b/stix2/test/v21/test_deterministic_ids.py index 9a83c73..710ee7a 100644 --- a/stix2/test/v21/test_deterministic_ids.py +++ b/stix2/test/v21/test_deterministic_ids.py @@ -14,6 +14,7 @@ from stix2.properties import ( TypeProperty, ) import stix2.v21 +from stix2.v21.vocab import HASHING_ALGORITHM SCO_DET_ID_NAMESPACE = uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7") @@ -155,7 +156,7 @@ def test_empty_hash(): ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('extensions', ExtensionsProperty(spec_version='2.1')), - ('hashes', HashesProperty()), + ('hashes', HashesProperty(HASHING_ALGORITHM)), )) _id_contributing_properties = ['hashes'] diff --git a/stix2/test/v21/test_environment.py b/stix2/test/v21/test_environment.py index 7f6b71c..883ff01 100644 --- a/stix2/test/v21/test_environment.py +++ b/stix2/test/v21/test_environment.py @@ -490,7 +490,7 @@ def test_object_similarity_on_same_identity2(): IDEN_KWARGS = dict( name="John Smith", identity_class="individual", - sectors=["government", "critical-infrastructure"], + sectors=["government", "infrastructure"], ) iden1 = stix2.v21.Identity(id=IDENTITY_ID, **IDEN_KWARGS) iden2 = stix2.v21.Identity(id=IDENTITY_ID, **IDEN_KWARGS) @@ -723,7 +723,7 @@ def test_object_similarity_different_spec_version_raises(): def test_object_similarity_zero_match(): IND_KWARGS = dict( - indicator_types=["malicious-activity", "bar"], + indicator_types=["anomalous-activity"], pattern="[ipv4-addr:value = '192.168.1.1']", pattern_type="stix", valid_from="2019-01-01T12:34:56Z", @@ -743,14 +743,14 @@ def test_object_similarity_zero_match(): ind1 = stix2.v21.Indicator(id=INDICATOR_ID, **INDICATOR_KWARGS) ind2 = stix2.v21.Indicator(id=INDICATOR_ID, **IND_KWARGS) env = stix2.Environment().object_similarity(ind1, ind2, **weights) - assert round(env) == 8 + assert round(env) == 0 env = stix2.Environment().object_similarity(ind2, ind1, **weights) - assert round(env) == 8 + assert round(env) == 0 def test_object_similarity_different_spec_version(): IND_KWARGS = dict( - labels=["APTX"], + labels=["malicious-activity"], pattern="[ipv4-addr:value = '192.168.1.1']", ) weights = { diff --git a/stix2/test/v21/test_indicator.py b/stix2/test/v21/test_indicator.py index c6220d7..e42ffba 100644 --- a/stix2/test/v21/test_indicator.py +++ b/stix2/test/v21/test_indicator.py @@ -223,6 +223,7 @@ def test_indicator_with_custom_embedded_objs(): valid_from=epoch, indicator_types=['malicious-activity'], external_references=[ext_ref], + allow_custom=True, ) assert ind.indicator_types == ['malicious-activity'] diff --git a/stix2/test/v21/test_location.py b/stix2/test/v21/test_location.py index 0fc5bcb..c2df5d3 100644 --- a/stix2/test/v21/test_location.py +++ b/stix2/test/v21/test_location.py @@ -36,7 +36,7 @@ EXPECTED_LOCATION_2 = """{ "id": "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64", "created": "2016-04-06T20:03:00.000Z", "modified": "2016-04-06T20:03:00.000Z", - "region": "north-america" + "region": "northern-america" } """ @@ -47,7 +47,7 @@ EXPECTED_LOCATION_2_REPR = "Location(" + " ".join( id='location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64', created='2016-04-06T20:03:00.000Z', modified='2016-04-06T20:03:00.000Z', - region='north-america'""".split(), + region='northern-america'""".split(), ) + ")" @@ -76,7 +76,7 @@ def test_location_with_some_required_properties(): "id": LOCATION_ID, "created": "2016-04-06T20:03:00.000Z", "modified": "2016-04-06T20:03:00.000Z", - "region": "north-america", + "region": "northern-america", }, ], ) @@ -88,7 +88,7 @@ def test_parse_location(data): assert location.id == LOCATION_ID assert location.created == dt.datetime(2016, 4, 6, 20, 3, 0, tzinfo=pytz.utc) assert location.modified == dt.datetime(2016, 4, 6, 20, 3, 0, tzinfo=pytz.utc) - assert location.region == 'north-america' + assert location.region == 'northern-america' rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(location)) assert rep == EXPECTED_LOCATION_2_REPR @@ -302,6 +302,7 @@ def test_google_map_url_multiple_props_no_long_lat_provided(): region="North America", country="United States of America", street_address="1410 Museum Campus Drive, Chicago, IL 60605", + allow_custom=True, ) loc_url = loc.to_maps_url() @@ -312,7 +313,7 @@ def test_google_map_url_multiple_props_and_long_lat_provided(): expected_url = "https://www.google.com/maps/search/?api=1&query=41.862401%2C-87.616001" loc = stix2.v21.Location( - region="North America", + region="northern-america", country="United States of America", street_address="1410 Museum Campus Drive, Chicago, IL 60605", latitude=41.862401, @@ -354,6 +355,7 @@ def test_bing_map_url_multiple_props_no_long_lat_provided(): region="North America", country="United States of America", street_address="1410 Museum Campus Drive, Chicago, IL 60605", + allow_custom=True, ) loc_url = loc.to_maps_url("Bing Maps") @@ -364,7 +366,7 @@ def test_bing_map_url_multiple_props_and_long_lat_provided(): expected_url = "https://bing.com/maps/default.aspx?where1=41.862401%2C-87.616001&lvl=16" loc = stix2.v21.Location( - region="North America", + region="northern-america", country="United States of America", street_address="1410 Museum Campus Drive, Chicago, IL 60605", latitude=41.862401, diff --git a/stix2/test/v21/test_malware.py b/stix2/test/v21/test_malware.py index d71b273..3c8b39c 100644 --- a/stix2/test/v21/test_malware.py +++ b/stix2/test/v21/test_malware.py @@ -181,7 +181,7 @@ def test_malware_family_no_name(): "id": MALWARE_ID, "spec_version": "2.1", "is_family": True, - "malware_types": ["a type"], + "malware_types": ["spyware"], }) @@ -191,7 +191,7 @@ def test_malware_non_family_no_name(): "id": MALWARE_ID, "spec_version": "2.1", "is_family": False, - "malware_types": ["something"], + "malware_types": ["spyware"], }) @@ -207,7 +207,7 @@ def test_malware_with_os_refs(): "id": MALWARE_ID, "spec_version": "2.1", "is_family": False, - "malware_types": ["something"], + "malware_types": ["spyware"], "operating_system_refs": [software], }) diff --git a/stix2/test/v21/test_malware_analysis.py b/stix2/test/v21/test_malware_analysis.py index 1390203..c0e4ca2 100644 --- a/stix2/test/v21/test_malware_analysis.py +++ b/stix2/test/v21/test_malware_analysis.py @@ -84,3 +84,38 @@ def test_malware_analysis_constraint(): stix2.v21.MalwareAnalysis( product="Acme Malware Analyzer", ) + + +def test_malware_analysis_custom_sco_refs(): + ma = stix2.v21.MalwareAnalysis( + product="super scanner", + analysis_sco_refs=[ + "file--6e8c78cf-4bcc-4729-9265-86a97bfc91ba", + "some-object--f6bfc147-e844-4578-ae01-847979890239", + ], + allow_custom=True, + ) + + assert "some-object--f6bfc147-e844-4578-ae01-847979890239" in \ + ma["analysis_sco_refs"] + assert ma.has_custom + + with pytest.raises(stix2.exceptions.InvalidValueError): + stix2.v21.MalwareAnalysis( + product="super scanner", + analysis_sco_refs=[ + "file--6e8c78cf-4bcc-4729-9265-86a97bfc91ba", + "some-object--f6bfc147-e844-4578-ae01-847979890239", + ], + ) + + with pytest.raises(stix2.exceptions.InvalidValueError): + stix2.v21.MalwareAnalysis( + product="super scanner", + analysis_sco_refs=[ + "file--6e8c78cf-4bcc-4729-9265-86a97bfc91ba", + # standard object type; wrong category (not SCO) + "identity--56977a19-49ef-49d7-b259-f733fa4b7bbc", + ], + allow_custom=True, + ) diff --git a/stix2/test/v21/test_observed_data.py b/stix2/test/v21/test_observed_data.py index ccf9a70..28c40c0 100644 --- a/stix2/test/v21/test_observed_data.py +++ b/stix2/test/v21/test_observed_data.py @@ -208,7 +208,7 @@ def test_observed_data_example_with_bad_refs(): assert excinfo.value.cls == stix2.v21.Directory assert excinfo.value.prop_name == "contains_refs" - assert "The type-specifying prefix 'monkey' for this property is not valid" in excinfo.value.reason + assert "The type-specifying prefix 'monkey' for this property is not" in excinfo.value.reason def test_observed_data_example_with_non_dictionary(): diff --git a/stix2/test/v21/test_pickle.py b/stix2/test/v21/test_pickle.py index faef4c4..55e8139 100644 --- a/stix2/test/v21/test_pickle.py +++ b/stix2/test/v21/test_pickle.py @@ -13,7 +13,7 @@ def test_pickling(): id=IDENTITY_ID, name="alice", description="this is a pickle test", - identity_class="some_class", + identity_class="individual", ) pickle.loads(pickle.dumps(identity)) diff --git a/stix2/test/v21/test_properties.py b/stix2/test/v21/test_properties.py index 2096ccb..936b7c1 100644 --- a/stix2/test/v21/test_properties.py +++ b/stix2/test/v21/test_properties.py @@ -3,11 +3,12 @@ import pytest import stix2 from stix2.exceptions import ( AtLeastOnePropertyError, CustomContentError, DictionaryKeyError, + ExtraPropertiesError, ParseError, ) from stix2.properties import ( DictionaryProperty, EmbeddedObjectProperty, ExtensionsProperty, - HashesProperty, IDProperty, ListProperty, ReferenceProperty, - StringProperty, TypeProperty, + HashesProperty, IDProperty, ListProperty, ObservableProperty, + ReferenceProperty, STIXObjectProperty, StringProperty, ) from stix2.v21.common import MarkingProperty @@ -22,23 +23,6 @@ def test_dictionary_property(): p.clean({}) -def test_string_property(): - prop = StringProperty() - - assert prop.clean('foobar') - assert prop.clean(1) - assert prop.clean([1, 2, 3]) - - -def test_type_property(): - prop = TypeProperty('my-type') - - assert prop.clean('my-type') - with pytest.raises(ValueError): - prop.clean('not-my-type') - assert prop.clean(prop.default()) - - ID_PROP = IDProperty('my-type', spec_version="2.1") MY_ID = 'my-type--232c9d3f-49fc-4440-bb01-607f638778e7' @@ -50,7 +34,7 @@ MY_ID = 'my-type--232c9d3f-49fc-4440-bb01-607f638778e7' ], ) def test_id_property_valid(value): - assert ID_PROP.clean(value) == value + assert ID_PROP.clean(value) == (value, False) CONSTANT_IDS = [ @@ -77,7 +61,7 @@ CONSTANT_IDS.extend(constants.RELATIONSHIP_IDS) @pytest.mark.parametrize("value", CONSTANT_IDS) def test_id_property_valid_for_type(value): type = value.split('--', 1)[0] - assert IDProperty(type=type, spec_version="2.1").clean(value) == value + assert IDProperty(type=type, spec_version="2.1").clean(value) == (value, False) def test_id_property_wrong_type(): @@ -100,29 +84,229 @@ def test_id_property_not_a_valid_hex_uuid(value): def test_id_property_default(): default = ID_PROP.default() - assert ID_PROP.clean(default) == default + assert ID_PROP.clean(default) == (default, False) -def test_reference_property(): - ref_prop = ReferenceProperty(valid_types="my-type", spec_version="2.1") +def test_reference_property_whitelist_standard_type(): + ref_prop = ReferenceProperty(valid_types="identity", spec_version="2.1") + result = ref_prop.clean( + "identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False, + ) + assert result == ("identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) - assert ref_prop.clean("my-type--00000000-0000-4000-8000-000000000000") with pytest.raises(ValueError): - ref_prop.clean("foo") + ref_prop.clean("foo--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) - # This is not a valid RFC 4122 UUID with pytest.raises(ValueError): - ref_prop.clean("my-type--00000000-0000-0000-0000-000000000000") + ref_prop.clean("foo--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) -def test_reference_property_specific_type(): +def test_reference_property_whitelist_custom_type(): ref_prop = ReferenceProperty(valid_types="my-type", spec_version="2.1") with pytest.raises(ValueError): - ref_prop.clean("not-my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf") + ref_prop.clean("not-my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) - assert ref_prop.clean("my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf") == \ - "my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf" + with pytest.raises(ValueError): + ref_prop.clean("not-my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + with pytest.raises(CustomContentError): + # This is the whitelisted type, but it's still custom, and + # customization is disallowed here. + ref_prop.clean("my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + result = ref_prop.clean("my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + assert result == ("my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + +def test_reference_property_whitelist_generic_type(): + ref_prop = ReferenceProperty( + valid_types=["SCO", "SRO"], spec_version="2.1", + ) + + result = ref_prop.clean("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + result = ref_prop.clean("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + result = ref_prop.clean( + "sighting--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False, + ) + assert result == ("sighting--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + result = ref_prop.clean( + "sighting--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True, + ) + assert result == ("sighting--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + # The prop assumes some-type is a custom type of one of the generic + # type categories. + result = ref_prop.clean( + "some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True, + ) + assert result == ("some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + with pytest.raises(ValueError): + ref_prop.clean("some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + with pytest.raises(ValueError): + ref_prop.clean("identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + with pytest.raises(ValueError): + ref_prop.clean("identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + +def test_reference_property_blacklist_standard_type(): + ref_prop = ReferenceProperty(invalid_types="identity", spec_version="2.1") + result = ref_prop.clean( + "location--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False, + ) + assert result == ("location--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + result = ref_prop.clean( + "location--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True, + ) + assert result == ("location--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + with pytest.raises(CustomContentError): + ref_prop.clean( + "some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False, + ) + + result = ref_prop.clean( + "some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True, + ) + assert result == ("some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + with pytest.raises(ValueError): + ref_prop.clean( + "identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False, + ) + + with pytest.raises(ValueError): + ref_prop.clean( + "identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True, + ) + + +def test_reference_property_blacklist_custom_type(): + ref_prop = ReferenceProperty(invalid_types="my-type", spec_version="2.1") + + result = ref_prop.clean("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + with pytest.raises(ValueError): + ref_prop.clean("my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + with pytest.raises(ValueError): + ref_prop.clean("my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + with pytest.raises(CustomContentError): + # This is not the blacklisted type, but it's still custom, and + # customization is disallowed here. + ref_prop.clean("not-my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + result = ref_prop.clean("not-my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + assert result == ("not-my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + +def test_reference_property_blacklist_generic_type(): + ref_prop = ReferenceProperty( + invalid_types=["SDO", "SRO"], spec_version="2.1", + ) + + result = ref_prop.clean( + "file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False, + ) + assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + result = ref_prop.clean( + "file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True, + ) + assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + with pytest.raises(CustomContentError): + ref_prop.clean( + "some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False, + ) + + result = ref_prop.clean( + "some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True, + ) + assert result == ("some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + with pytest.raises(ValueError): + ref_prop.clean( + "identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False, + ) + + with pytest.raises(ValueError): + ref_prop.clean( + "identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True, + ) + + with pytest.raises(ValueError): + ref_prop.clean( + "relationship--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False, + ) + + with pytest.raises(ValueError): + ref_prop.clean( + "relationship--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True, + ) + + +def test_reference_property_whitelist_hybrid_type(): + p = ReferenceProperty(valid_types=["a", "SCO"], spec_version="2.1") + + result = p.clean("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + result = p.clean("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + with pytest.raises(CustomContentError): + # although whitelisted, "a" is a custom type + p.clean("a--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + result = p.clean("a--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + assert result == ("a--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + with pytest.raises(ValueError): + p.clean("b--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + # should just assume "b" is a custom SCO type. + result = p.clean("b--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + assert result == ("b--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + +def test_reference_property_blacklist_hybrid_type(): + p = ReferenceProperty(invalid_types=["a", "SCO"], spec_version="2.1") + + with pytest.raises(ValueError): + p.clean("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + with pytest.raises(ValueError): + p.clean("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + with pytest.raises(ValueError): + p.clean("a--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + with pytest.raises(ValueError): + p.clean("a--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + with pytest.raises(CustomContentError): + p.clean("b--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False) + + # should just assume "b" is a custom type which is not an SCO + result = p.clean("b--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + assert result == ("b--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True) + + +def test_reference_property_impossible_constraint(): + with pytest.raises(ValueError): + ReferenceProperty(valid_types=[], spec_version="2.1") @pytest.mark.parametrize( @@ -205,29 +389,29 @@ def test_property_list_of_dictionary(): @pytest.mark.parametrize( - "value", [ - {"sha256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b"}, - [('MD5', '2dfb1bcc980200c6706feee399d41b3f'), ('RIPEMD-160', 'b3a8cd8a27c90af79b3c81754f267780f443dfef')], - [('TLSH', '6FF02BEF718027B0160B4391212923ED7F1A463D563B1549B86CF62973B197AD2731F8')], + "key", [ + "a", + "a"*250, + "a-1_b", ], ) -def test_hashes_property_valid(value): - hash_prop = HashesProperty() - assert hash_prop.clean(value) +def test_hash_property_valid_key(key): + p = HashesProperty(["foo"], spec_version="2.1") + result = p.clean({key: "bar"}, True) + assert result == ({key: "bar"}, True) @pytest.mark.parametrize( - "value", [ - {"MD5": "a"}, - {"SHA-256": "2dfb1bcc980200c6706feee399d41b3f"}, - {"TLSH": "6FF02BEF718027B0160B4391212923ED7F1A463D563B1549B86CF62973B197AD2731F"}, + "key", [ + "", + "a"*251, + "funny%chars?", ], ) -def test_hashes_property_invalid(value): - hash_prop = HashesProperty() - - with pytest.raises(ValueError): - hash_prop.clean(value) +def test_hash_property_invalid_key(key): + p = HashesProperty(["foo"], spec_version="2.1") + with pytest.raises(DictionaryKeyError): + p.clean({key: "foo"}, True) def test_embedded_property(): @@ -237,25 +421,103 @@ def test_embedded_property(): content_disposition="inline", body="Cats are funny!", ) - assert emb_prop.clean(mime) + result = emb_prop.clean(mime, False) + assert result == (mime, False) + + result = emb_prop.clean(mime, True) + assert result == (mime, False) with pytest.raises(ValueError): - emb_prop.clean("string") + emb_prop.clean("string", False) + + +def test_embedded_property_dict(): + emb_prop = EmbeddedObjectProperty(type=stix2.v21.EmailMIMEComponent) + mime = { + "content_type": "text/plain; charset=utf-8", + "content_disposition": "inline", + "body": "Cats are funny!", + } + + result = emb_prop.clean(mime, False) + assert isinstance(result[0], stix2.v21.EmailMIMEComponent) + assert result[0]["body"] == "Cats are funny!" + assert not result[1] + + result = emb_prop.clean(mime, True) + assert isinstance(result[0], stix2.v21.EmailMIMEComponent) + assert result[0]["body"] == "Cats are funny!" + assert not result[1] + + +def test_embedded_property_custom(): + emb_prop = EmbeddedObjectProperty(type=stix2.v21.EmailMIMEComponent) + mime = stix2.v21.EmailMIMEComponent( + content_type="text/plain; charset=utf-8", + content_disposition="inline", + body="Cats are funny!", + foo=123, + allow_custom=True, + ) + + with pytest.raises(CustomContentError): + emb_prop.clean(mime, False) + + result = emb_prop.clean(mime, True) + assert result == (mime, True) + + +def test_embedded_property_dict_custom(): + emb_prop = EmbeddedObjectProperty(type=stix2.v21.EmailMIMEComponent) + mime = { + "content_type": "text/plain; charset=utf-8", + "content_disposition": "inline", + "body": "Cats are funny!", + "foo": 123, + } + + with pytest.raises(ExtraPropertiesError): + emb_prop.clean(mime, False) + + result = emb_prop.clean(mime, True) + assert isinstance(result[0], stix2.v21.EmailMIMEComponent) + assert result[0]["body"] == "Cats are funny!" + assert result[1] def test_extension_property_valid(): ext_prop = ExtensionsProperty(spec_version='2.1') - assert ext_prop({ - 'windows-pebinary-ext': { - 'pe_type': 'exe', - }, - }) + result = ext_prop.clean( + { + 'windows-pebinary-ext': { + 'pe_type': 'exe', + }, + }, False, + ) + + assert isinstance( + result[0]["windows-pebinary-ext"], stix2.v21.WindowsPEBinaryExt, + ) + assert not result[1] + + result = ext_prop.clean( + { + 'windows-pebinary-ext': { + 'pe_type': 'exe', + }, + }, True, + ) + + assert isinstance( + result[0]["windows-pebinary-ext"], stix2.v21.WindowsPEBinaryExt, + ) + assert not result[1] def test_extension_property_invalid1(): ext_prop = ExtensionsProperty(spec_version='2.1') with pytest.raises(ValueError): - ext_prop.clean(1) + ext_prop.clean(1, False) def test_extension_property_invalid2(): @@ -267,8 +529,61 @@ def test_extension_property_invalid2(): 'pe_type': 'exe', }, }, + False, ) + result = ext_prop.clean( + { + 'foobar-ext': { + 'pe_type': 'exe', + }, + }, True, + ) + assert result == ({"foobar-ext": {"pe_type": "exe"}}, True) + + +def test_extension_property_invalid3(): + ext_prop = ExtensionsProperty(spec_version="2.1") + with pytest.raises(ExtraPropertiesError): + ext_prop.clean( + { + 'windows-pebinary-ext': { + 'pe_type': 'exe', + 'abc': 123, + }, + }, + False, + ) + + result = ext_prop.clean( + { + 'windows-pebinary-ext': { + 'pe_type': 'exe', + 'abc': 123, + }, + }, True, + ) + + assert isinstance( + result[0]["windows-pebinary-ext"], stix2.v21.WindowsPEBinaryExt, + ) + assert result[0]["windows-pebinary-ext"]["abc"] == 123 + assert result[1] + + +def test_extension_property_invalid_type(): + ext_prop = ExtensionsProperty(spec_version='2.1', enclosing_type='indicator') + with pytest.raises(CustomContentError) as excinfo: + ext_prop.clean( + { + 'windows-pebinary-ext': { + 'pe_type': 'exe', + }, + }, + False, + ) + assert "Can't parse unknown extension" in str(excinfo.value) + def test_extension_at_least_one_property_constraint(): with pytest.raises(AtLeastOnePropertyError): @@ -282,3 +597,115 @@ def test_marking_property_error(): mark_prop.clean('my-marking') assert str(excinfo.value) == "must be a Statement, TLP Marking or a registered marking." + + +def test_observable_property_obj(): + prop = ObservableProperty(spec_version="2.1") + + obs = stix2.v21.File(name="data.dat") + obs_dict = { + "0": obs, + } + + result = prop.clean(obs_dict, False) + assert result[0]["0"] == obs + assert not result[1] + + result = prop.clean(obs_dict, True) + assert result[0]["0"] == obs + assert not result[1] + + +def test_observable_property_dict(): + prop = ObservableProperty(spec_version="2.1") + + obs_dict = { + "0": { + "type": "file", + "name": "data.dat", + }, + } + + result = prop.clean(obs_dict, False) + assert isinstance(result[0]["0"], stix2.v21.File) + assert result[0]["0"]["name"] == "data.dat" + assert not result[1] + + result = prop.clean(obs_dict, True) + assert isinstance(result[0]["0"], stix2.v21.File) + assert result[0]["0"]["name"] == "data.dat" + assert not result[1] + + +def test_observable_property_obj_custom(): + prop = ObservableProperty(spec_version="2.1") + + obs = stix2.v21.File(name="data.dat", foo=True, allow_custom=True) + obs_dict = { + "0": obs, + } + + with pytest.raises(ExtraPropertiesError): + prop.clean(obs_dict, False) + + result = prop.clean(obs_dict, True) + assert result[0]["0"] == obs + assert result[1] + + +def test_observable_property_dict_custom(): + prop = ObservableProperty(spec_version="2.1") + + obs_dict = { + "0": { + "type": "file", + "name": "data.dat", + "foo": True, + }, + } + + with pytest.raises(ExtraPropertiesError): + prop.clean(obs_dict, False) + + result = prop.clean(obs_dict, True) + assert isinstance(result[0]["0"], stix2.v21.File) + assert result[0]["0"]["foo"] + assert result[1] + + +def test_stix_object_property_custom_prop(): + prop = STIXObjectProperty(spec_version="2.1") + + obj_dict = { + "type": "identity", + "spec_version": "2.1", + "name": "alice", + "identity_class": "supergirl", + "foo": "bar", + } + + with pytest.raises(ExtraPropertiesError): + prop.clean(obj_dict, False) + + result = prop.clean(obj_dict, True) + assert isinstance(result[0], stix2.v21.Identity) + assert result[0].has_custom + assert result[0]["foo"] == "bar" + assert result[1] + + +def test_stix_object_property_custom_obj(): + prop = STIXObjectProperty(spec_version="2.1") + + obj_dict = { + "type": "something", + "abc": 123, + "xyz": ["a", 1], + } + + with pytest.raises(ParseError): + prop.clean(obj_dict, False) + + result = prop.clean(obj_dict, True) + assert result[0] == {"type": "something", "abc": 123, "xyz": ["a", 1]} + assert result[1] diff --git a/stix2/test/v21/test_report.py b/stix2/test/v21/test_report.py index 87ac1c1..d2854b6 100644 --- a/stix2/test/v21/test_report.py +++ b/stix2/test/v21/test_report.py @@ -4,6 +4,7 @@ import pytest import pytz import stix2 +from stix2.exceptions import InvalidValueError from .constants import ( CAMPAIGN_ID, IDENTITY_ID, INDICATOR_ID, INDICATOR_KWARGS, RELATIONSHIP_ID, @@ -135,4 +136,27 @@ def test_parse_report(data): assert rept.report_types == ["campaign"] assert rept.name == "The Black Vine Cyberespionage Group" -# TODO: Add other examples + +def test_report_on_custom(): + with pytest.raises(InvalidValueError): + stix2.v21.Report( + name="my report", + published="2016-01-20T17:00:00Z", + object_refs=[ + "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "some-type--2672975a-ce1e-4473-a1c6-0d79868930c7", + ], + ) + + report = stix2.v21.Report( + name="my report", + published="2016-01-20T17:00:00Z", + object_refs=[ + "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "some-type--2672975a-ce1e-4473-a1c6-0d79868930c7", + ], + allow_custom=True, + ) + + assert "some-type--2672975a-ce1e-4473-a1c6-0d79868930c7" \ + in report.object_refs diff --git a/stix2/test/v21/test_threat_actor.py b/stix2/test/v21/test_threat_actor.py index 94dea8e..b53512f 100644 --- a/stix2/test/v21/test_threat_actor.py +++ b/stix2/test/v21/test_threat_actor.py @@ -76,7 +76,7 @@ def test_seen_ordering_constraint(): with pytest.raises(ValueError): stix2.v21.ThreatActor( name="Bad Person", - threat_actor_types=["bad person", "evil person"], + threat_actor_types=["hacker", "criminal"], first_seen="2010-04-21T09:31:11Z", last_seen="2009-02-06T03:39:31Z", ) @@ -84,7 +84,7 @@ def test_seen_ordering_constraint(): # equal timestamps is okay. stix2.v21.ThreatActor( name="Bad Person", - threat_actor_types=["bad person", "evil person"], + threat_actor_types=["hacker", "criminal"], first_seen="2010-04-21T09:31:11Z", last_seen="2010-04-21T09:31:11Z", ) diff --git a/stix2/test/v21/test_timestamp_precision.py b/stix2/test/v21/test_timestamp_precision.py index 8cb9735..831bd7a 100644 --- a/stix2/test/v21/test_timestamp_precision.py +++ b/stix2/test/v21/test_timestamp_precision.py @@ -5,8 +5,8 @@ import pytest import stix2 from stix2.utils import ( - Precision, PrecisionConstraint, STIXdatetime, _to_enum, format_datetime, - parse_into_datetime, + Precision, PrecisionConstraint, STIXdatetime, format_datetime, + parse_into_datetime, to_enum, ) _DT = datetime.datetime.utcnow() @@ -27,7 +27,7 @@ _DT_STR = _DT.strftime("%Y-%m-%dT%H:%M:%S") ], ) def test_to_enum(value, enum_type, enum_default, enum_expected): - result = _to_enum(value, enum_type, enum_default) + result = to_enum(value, enum_type, enum_default) assert result == enum_expected @@ -41,7 +41,7 @@ def test_to_enum(value, enum_type, enum_default, enum_expected): ) def test_to_enum_errors(value, err_type): with pytest.raises(err_type): - _to_enum(value, Precision) + to_enum(value, Precision) @pytest.mark.xfail( diff --git a/stix2/utils.py b/stix2/utils.py index 08e272d..647a89f 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -45,7 +45,7 @@ class PrecisionConstraint(enum.Enum): # no need for a MAX constraint yet -def _to_enum(value, enum_type, enum_default=None): +def to_enum(value, enum_type, enum_default=None): """ Detect and convert strings to enums and None to a default enum. This allows use of strings and None in APIs, while enforcing the enum type: if @@ -88,11 +88,11 @@ class STIXdatetime(dt.datetime): """ def __new__(cls, *args, **kwargs): - precision = _to_enum( + precision = to_enum( kwargs.pop("precision", Precision.ANY), Precision, ) - precision_constraint = _to_enum( + precision_constraint = to_enum( kwargs.pop("precision_constraint", PrecisionConstraint.EXACT), PrecisionConstraint, ) @@ -233,8 +233,8 @@ def parse_into_datetime( :return: A STIXdatetime instance, which is a datetime but also carries the precision info necessary to properly JSON-serialize it. """ - precision = _to_enum(precision, Precision) - precision_constraint = _to_enum(precision_constraint, PrecisionConstraint) + precision = to_enum(precision, Precision) + precision_constraint = to_enum(precision_constraint, PrecisionConstraint) if isinstance(value, dt.date): if hasattr(value, 'hour'): diff --git a/stix2/v20/bundle.py b/stix2/v20/bundle.py index e05522e..42b6876 100644 --- a/stix2/v20/bundle.py +++ b/stix2/v20/bundle.py @@ -35,9 +35,6 @@ class Bundle(_STIXBase20): kwargs['objects'] = obj_list + kwargs.get('objects', []) - self._allow_custom = kwargs.get('allow_custom', False) - self._properties['objects'].contained.allow_custom = kwargs.get('allow_custom', False) - super(Bundle, self).__init__(**kwargs) def get_obj(self, obj_uuid): diff --git a/stix2/v20/common.py b/stix2/v20/common.py index 4b7c299..feaa3ef 100644 --- a/stix2/v20/common.py +++ b/stix2/v20/common.py @@ -12,6 +12,7 @@ from ..properties import ( ) from ..utils import NOW, _get_dict from .base import _STIXBase20 +from .vocab import HASHING_ALGORITHM def _should_set_millisecond(cr, marking_type): @@ -38,7 +39,7 @@ class ExternalReference(_STIXBase20): ('source_name', StringProperty(required=True)), ('description', StringProperty()), ('url', StringProperty()), - ('hashes', HashesProperty(spec_version='2.0')), + ('hashes', HashesProperty(HASHING_ALGORITHM, spec_version='2.0')), ('external_id', StringProperty()), ]) @@ -103,9 +104,9 @@ class MarkingProperty(Property): marking-definition objects. """ - def clean(self, value): + def clean(self, value, allow_custom=False): if type(value) in OBJ_MAP_MARKING.values(): - return value + return value, False else: raise ValueError("must be a Statement, TLP Marking or a registered marking.") diff --git a/stix2/v20/observables.py b/stix2/v20/observables.py index 67c1e77..2b6c81c 100644 --- a/stix2/v20/observables.py +++ b/stix2/v20/observables.py @@ -17,6 +17,7 @@ from ..properties import ( ObjectReferenceProperty, StringProperty, TimestampProperty, TypeProperty, ) from .base import _Extension, _Observable, _STIXBase20 +from .vocab import HASHING_ALGORITHM class Artifact(_Observable): @@ -30,7 +31,7 @@ class Artifact(_Observable): ('mime_type', StringProperty()), ('payload_bin', BinaryProperty()), ('url', StringProperty()), - ('hashes', HashesProperty(spec_version='2.0')), + ('hashes', HashesProperty(HASHING_ALGORITHM, spec_version='2.0')), ('extensions', ExtensionsProperty(spec_version='2.0')), ]) @@ -173,7 +174,7 @@ class AlternateDataStream(_STIXBase20): _properties = OrderedDict([ ('name', StringProperty(required=True)), - ('hashes', HashesProperty(spec_version='2.0')), + ('hashes', HashesProperty(HASHING_ALGORITHM, spec_version="2.0")), ('size', IntegerProperty()), ]) @@ -256,7 +257,7 @@ class WindowsPEOptionalHeaderType(_STIXBase20): ('size_of_heap_commit', IntegerProperty()), ('loader_flags_hex', HexProperty()), ('number_of_rva_and_sizes', IntegerProperty()), - ('hashes', HashesProperty(spec_version='2.0')), + ('hashes', HashesProperty(HASHING_ALGORITHM, spec_version="2.0")), ]) def _check_object_constraints(self): @@ -273,7 +274,7 @@ class WindowsPESection(_STIXBase20): ('name', StringProperty(required=True)), ('size', IntegerProperty()), ('entropy', FloatProperty()), - ('hashes', HashesProperty(spec_version='2.0')), + ('hashes', HashesProperty(HASHING_ALGORITHM, spec_version="2.0")), ]) @@ -293,7 +294,7 @@ class WindowsPEBinaryExt(_Extension): ('number_of_symbols', IntegerProperty()), ('size_of_optional_header', IntegerProperty()), ('characteristics_hex', HexProperty()), - ('file_header_hashes', HashesProperty(spec_version='2.0')), + ('file_header_hashes', HashesProperty(HASHING_ALGORITHM, spec_version="2.0")), ('optional_header', EmbeddedObjectProperty(type=WindowsPEOptionalHeaderType)), ('sections', ListProperty(EmbeddedObjectProperty(type=WindowsPESection))), ]) @@ -307,7 +308,7 @@ class File(_Observable): _type = 'file' _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.0')), - ('hashes', HashesProperty(spec_version='2.0')), + ('hashes', HashesProperty(HASHING_ALGORITHM, spec_version="2.0")), ('size', IntegerProperty()), ('name', StringProperty()), ('name_enc', StringProperty()), @@ -771,7 +772,7 @@ class X509Certificate(_Observable): _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.0')), ('is_self_signed', BooleanProperty()), - ('hashes', HashesProperty(spec_version='2.0')), + ('hashes', HashesProperty(HASHING_ALGORITHM, spec_version="2.0")), ('version', StringProperty()), ('serial_number', StringProperty()), ('signature_algorithm', StringProperty()), diff --git a/stix2/v20/sdo.py b/stix2/v20/sdo.py index bd18e8d..57013b2 100644 --- a/stix2/v20/sdo.py +++ b/stix2/v20/sdo.py @@ -9,12 +9,17 @@ from ..custom import _custom_object_builder from ..exceptions import InvalidValueError from ..properties import ( BooleanProperty, IDProperty, IntegerProperty, ListProperty, - ObservableProperty, PatternProperty, ReferenceProperty, StringProperty, - TimestampProperty, TypeProperty, + ObservableProperty, OpenVocabProperty, PatternProperty, ReferenceProperty, + StringProperty, TimestampProperty, TypeProperty, ) from ..utils import NOW from .base import _DomainObject from .common import ExternalReference, GranularMarking, KillChainPhase +from .vocab import ( + ATTACK_MOTIVATION, ATTACK_RESOURCE_LEVEL, IDENTITY_CLASS, INDICATOR_LABEL, + INDUSTRY_SECTOR, MALWARE_LABEL, REPORT_LABEL, THREAT_ACTOR_LABEL, + THREAT_ACTOR_ROLE, THREAT_ACTOR_SOPHISTICATION, TOOL_LABEL, +) class AttackPattern(_DomainObject): @@ -102,8 +107,8 @@ class Identity(_DomainObject): ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('name', StringProperty(required=True)), ('description', StringProperty()), - ('identity_class', StringProperty(required=True)), - ('sectors', ListProperty(StringProperty)), + ('identity_class', OpenVocabProperty(IDENTITY_CLASS, required=True)), + ('sectors', ListProperty(OpenVocabProperty(INDUSTRY_SECTOR))), ('contact_information', StringProperty()), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), @@ -132,7 +137,7 @@ class Indicator(_DomainObject): ('valid_until', TimestampProperty()), ('kill_chain_phases', ListProperty(KillChainPhase)), ('revoked', BooleanProperty(default=lambda: False)), - ('labels', ListProperty(StringProperty, required=True)), + ('labels', ListProperty(OpenVocabProperty(INDICATOR_LABEL), required=True)), ('external_references', ListProperty(ExternalReference)), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.0'))), ('granular_markings', ListProperty(GranularMarking)), @@ -163,8 +168,8 @@ class IntrusionSet(_DomainObject): ('last_seen', TimestampProperty()), ('goals', ListProperty(StringProperty)), ('resource_level', StringProperty()), - ('primary_motivation', StringProperty()), - ('secondary_motivations', ListProperty(StringProperty)), + ('primary_motivation', OpenVocabProperty(ATTACK_MOTIVATION)), + ('secondary_motivations', ListProperty(OpenVocabProperty(ATTACK_MOTIVATION))), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('external_references', ListProperty(ExternalReference)), @@ -189,7 +194,7 @@ class Malware(_DomainObject): ('description', StringProperty()), ('kill_chain_phases', ListProperty(KillChainPhase)), ('revoked', BooleanProperty(default=lambda: False)), - ('labels', ListProperty(StringProperty, required=True)), + ('labels', ListProperty(OpenVocabProperty(MALWARE_LABEL), required=True)), ('external_references', ListProperty(ExternalReference)), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.0'))), ('granular_markings', ListProperty(GranularMarking)), @@ -219,12 +224,6 @@ class ObservedData(_DomainObject): ('granular_markings', ListProperty(GranularMarking)), ]) - def __init__(self, *args, **kwargs): - self._allow_custom = kwargs.get('allow_custom', False) - self._properties['objects'].allow_custom = kwargs.get('allow_custom', False) - - super(ObservedData, self).__init__(*args, **kwargs) - class Report(_DomainObject): """For more detailed information on this object's properties, see @@ -243,7 +242,7 @@ class Report(_DomainObject): ('published', TimestampProperty(required=True)), ('object_refs', ListProperty(ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.0'), required=True)), ('revoked', BooleanProperty(default=lambda: False)), - ('labels', ListProperty(StringProperty, required=True)), + ('labels', ListProperty(OpenVocabProperty(REPORT_LABEL), required=True)), ('external_references', ListProperty(ExternalReference)), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.0'))), ('granular_markings', ListProperty(GranularMarking)), @@ -265,15 +264,15 @@ class ThreatActor(_DomainObject): ('name', StringProperty(required=True)), ('description', StringProperty()), ('aliases', ListProperty(StringProperty)), - ('roles', ListProperty(StringProperty)), + ('roles', ListProperty(OpenVocabProperty(THREAT_ACTOR_ROLE))), ('goals', ListProperty(StringProperty)), - ('sophistication', StringProperty()), - ('resource_level', StringProperty()), - ('primary_motivation', StringProperty()), - ('secondary_motivations', ListProperty(StringProperty)), - ('personal_motivations', ListProperty(StringProperty)), + ('sophistication', OpenVocabProperty(THREAT_ACTOR_SOPHISTICATION)), + ('resource_level', OpenVocabProperty(ATTACK_RESOURCE_LEVEL)), + ('primary_motivation', OpenVocabProperty(ATTACK_MOTIVATION)), + ('secondary_motivations', ListProperty(OpenVocabProperty(ATTACK_MOTIVATION))), + ('personal_motivations', ListProperty(OpenVocabProperty(ATTACK_MOTIVATION))), ('revoked', BooleanProperty(default=lambda: False)), - ('labels', ListProperty(StringProperty, required=True)), + ('labels', ListProperty(OpenVocabProperty(THREAT_ACTOR_LABEL), required=True)), ('external_references', ListProperty(ExternalReference)), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.0'))), ('granular_markings', ListProperty(GranularMarking)), @@ -297,7 +296,7 @@ class Tool(_DomainObject): ('kill_chain_phases', ListProperty(KillChainPhase)), ('tool_version', StringProperty()), ('revoked', BooleanProperty(default=lambda: False)), - ('labels', ListProperty(StringProperty, required=True)), + ('labels', ListProperty(OpenVocabProperty(TOOL_LABEL), required=True)), ('external_references', ListProperty(ExternalReference)), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.0'))), ('granular_markings', ListProperty(GranularMarking)), diff --git a/stix2/v20/vocab.py b/stix2/v20/vocab.py new file mode 100644 index 0000000..765c796 --- /dev/null +++ b/stix2/v20/vocab.py @@ -0,0 +1,330 @@ +""" +STIX 2.0 open vocabularies and enums +""" + +ATTACK_MOTIVATION_ACCIDENTAL = "accidental" +ATTACK_MOTIVATION_COERCION = "coercion" +ATTACK_MOTIVATION_DOMINANCE = "dominance" +ATTACK_MOTIVATION_IDEOLOGY = "ideology" +ATTACK_MOTIVATION_NOTORIETY = "notoriety" +ATTACK_MOTIVATION_ORGANIZATIONAL_GAIN = "organizational-gain" +ATTACK_MOTIVATION_PERSONAL_GAIN = "personal-gain" +ATTACK_MOTIVATION_PERSONAL_SATISFACTION = "personal-satisfaction" +ATTACK_MOTIVATION_REVENGE = "revenge" +ATTACK_MOTIVATION_UNPREDICTABLE = "unpredictable" + + +ATTACK_MOTIVATION = [ + ATTACK_MOTIVATION_ACCIDENTAL, + ATTACK_MOTIVATION_COERCION, + ATTACK_MOTIVATION_DOMINANCE, + ATTACK_MOTIVATION_IDEOLOGY, + ATTACK_MOTIVATION_NOTORIETY, + ATTACK_MOTIVATION_ORGANIZATIONAL_GAIN, + ATTACK_MOTIVATION_PERSONAL_GAIN, + ATTACK_MOTIVATION_PERSONAL_SATISFACTION, + ATTACK_MOTIVATION_REVENGE, + ATTACK_MOTIVATION_UNPREDICTABLE, +] + + +ATTACK_RESOURCE_LEVEL_INDIVIDUAL = "individual" +ATTACK_RESOURCE_LEVEL_CLUB = "club" +ATTACK_RESOURCE_LEVEL_CONTEST = "contest" +ATTACK_RESOURCE_LEVEL_TEAM = "team" +ATTACK_RESOURCE_LEVEL_ORGANIZATION = "organization" +ATTACK_RESOURCE_LEVEL_GOVERNMENT = "government" + + +ATTACK_RESOURCE_LEVEL = [ + ATTACK_RESOURCE_LEVEL_INDIVIDUAL, + ATTACK_RESOURCE_LEVEL_CLUB, + ATTACK_RESOURCE_LEVEL_CONTEST, + ATTACK_RESOURCE_LEVEL_TEAM, + ATTACK_RESOURCE_LEVEL_ORGANIZATION, + ATTACK_RESOURCE_LEVEL_GOVERNMENT, +] + + +HASHING_ALGORITHM_MD5 = "MD5" +HASHING_ALGORITHM_MD6 = "MD6" +HASHING_ALGORITHM_RIPEMD_160 = "RIPEMD-160" +HASHING_ALGORITHM_SHA_1 = "SHA-1" +HASHING_ALGORITHM_SHA_224 = "SHA-224" +HASHING_ALGORITHM_SHA_256 = "SHA-256" +HASHING_ALGORITHM_SHA_384 = "SHA-384" +HASHING_ALGORITHM_SHA_512 = "SHA-512" +HASHING_ALGORITHM_SHA3_224 = "SHA3-224" +HASHING_ALGORITHM_SHA3_256 = "SHA3-256" +HASHING_ALGORITHM_SHA3_384 = "SHA3-384" +HASHING_ALGORITHM_SHA3_512 = "SHA3-512" +HASHING_ALGORITHM_SSDEEP = "ssdeep" +HASHING_ALGORITHM_WHIRLPOOL = "WHIRLPOOL" + + +HASHING_ALGORITHM = [ + HASHING_ALGORITHM_MD5, + HASHING_ALGORITHM_MD6, + HASHING_ALGORITHM_RIPEMD_160, + HASHING_ALGORITHM_SHA_1, + HASHING_ALGORITHM_SHA_224, + HASHING_ALGORITHM_SHA_256, + HASHING_ALGORITHM_SHA_384, + HASHING_ALGORITHM_SHA_512, + HASHING_ALGORITHM_SHA3_224, + HASHING_ALGORITHM_SHA3_256, + HASHING_ALGORITHM_SHA3_384, + HASHING_ALGORITHM_SHA3_512, + HASHING_ALGORITHM_SSDEEP, + HASHING_ALGORITHM_WHIRLPOOL, +] + + +IDENTITY_CLASS_INDIVIDUAL = "individual" +IDENTITY_CLASS_GROUP = "group" +IDENTITY_CLASS_ORGANIZATION = "organization" +IDENTITY_CLASS_CLASS = "class" +IDENTITY_CLASS_UNKNOWN = "unknown" + + +IDENTITY_CLASS = [ + IDENTITY_CLASS_INDIVIDUAL, + IDENTITY_CLASS_GROUP, + IDENTITY_CLASS_ORGANIZATION, + IDENTITY_CLASS_CLASS, + IDENTITY_CLASS_UNKNOWN, +] + + +INDICATOR_LABEL_ANOMALOUS_ACTIVITY = "anomalous-activity" +INDICATOR_LABEL_ANONYMIZATION = "anonymization" +INDICATOR_LABEL_BENIGN = "benign" +INDICATOR_LABEL_COMPROMISED = "compromised" +INDICATOR_LABEL_MALICIOUS_ACTIVITY = "malicious-activity" +INDICATOR_LABEL_ATTRIBUTION = "attribution" + + +INDICATOR_LABEL = [ + INDICATOR_LABEL_ANOMALOUS_ACTIVITY, + INDICATOR_LABEL_ANONYMIZATION, + INDICATOR_LABEL_BENIGN, + INDICATOR_LABEL_COMPROMISED, + INDICATOR_LABEL_MALICIOUS_ACTIVITY, + INDICATOR_LABEL_ATTRIBUTION, +] + + +INDUSTRY_SECTOR_AGRICULTURE = "agriculture" +INDUSTRY_SECTOR_AEROSPACE = "aerospace" +INDUSTRY_SECTOR_AUTOMOTIVE = "automotive" +INDUSTRY_SECTOR_COMMUNICATIONS = "communications" +INDUSTRY_SECTOR_CONSTRUCTION = "construction" +INDUSTRY_SECTOR_DEFENCE = "defence" +INDUSTRY_SECTOR_EDUCATION = "education" +INDUSTRY_SECTOR_ENERGY = "energy" +INDUSTRY_SECTOR_ENTERTAINMENT = "entertainment" +INDUSTRY_SECTOR_FINANCIAL_SERVICES = "financial-services" +INDUSTRY_SECTOR_GOVERNMENT_NATIONAL = "government-national" +INDUSTRY_SECTOR_GOVERNMENT_REGIONAL = "government-regional" +INDUSTRY_SECTOR_GOVERNMENT_LOCAL = "government-local" +INDUSTRY_SECTOR_GOVERNMENT_PUBLIC_SERVICES = "government-public-services" +INDUSTRY_SECTOR_HEALTHCARE = "healthcare" +INDUSTRY_SECTOR_HOSPITALITY_LEISURE = "hospitality-leisure" +INDUSTRY_SECTOR_INFRASTRUCTURE = "infrastructure" +INDUSTRY_SECTOR_INSURANCE = "insurance" +INDUSTRY_SECTOR_MANUFACTURING = "manufacturing" +INDUSTRY_SECTOR_MINING = "mining" +INDUSTRY_SECTOR_NON_PROFIT = "non-profit" +INDUSTRY_SECTOR_PHARMACEUTICALS = "pharmaceuticals" +INDUSTRY_SECTOR_RETAIL = "retail" +INDUSTRY_SECTOR_TECHNOLOGY = "technology" +INDUSTRY_SECTOR_TELECOMMUNICATIONS = "telecommunications" +INDUSTRY_SECTOR_TRANSPORTATION = "transportation" +INDUSTRY_SECTOR_UTILITIES = "utilities" + + +INDUSTRY_SECTOR = [ + INDUSTRY_SECTOR_AGRICULTURE, + INDUSTRY_SECTOR_AEROSPACE, + INDUSTRY_SECTOR_AUTOMOTIVE, + INDUSTRY_SECTOR_COMMUNICATIONS, + INDUSTRY_SECTOR_CONSTRUCTION, + INDUSTRY_SECTOR_DEFENCE, + INDUSTRY_SECTOR_EDUCATION, + INDUSTRY_SECTOR_ENERGY, + INDUSTRY_SECTOR_ENTERTAINMENT, + INDUSTRY_SECTOR_FINANCIAL_SERVICES, + INDUSTRY_SECTOR_GOVERNMENT_NATIONAL, + INDUSTRY_SECTOR_GOVERNMENT_REGIONAL, + INDUSTRY_SECTOR_GOVERNMENT_LOCAL, + INDUSTRY_SECTOR_GOVERNMENT_PUBLIC_SERVICES, + INDUSTRY_SECTOR_HEALTHCARE, + INDUSTRY_SECTOR_HOSPITALITY_LEISURE, + INDUSTRY_SECTOR_INFRASTRUCTURE, + INDUSTRY_SECTOR_INSURANCE, + INDUSTRY_SECTOR_MANUFACTURING, + INDUSTRY_SECTOR_MINING, + INDUSTRY_SECTOR_NON_PROFIT, + INDUSTRY_SECTOR_PHARMACEUTICALS, + INDUSTRY_SECTOR_RETAIL, + INDUSTRY_SECTOR_TECHNOLOGY, + INDUSTRY_SECTOR_TELECOMMUNICATIONS, + INDUSTRY_SECTOR_TRANSPORTATION, + INDUSTRY_SECTOR_UTILITIES, +] + + +MALWARE_LABEL_ADWARE = "adware" +MALWARE_LABEL_BACKDOOR = "backdoor" +MALWARE_LABEL_BOT = "bot" +MALWARE_LABEL_DDOS = "ddos" +MALWARE_LABEL_DROPPER = "dropper" +MALWARE_LABEL_EXPLOIT_KIT = "exploit-kit" +MALWARE_LABEL_KEYLOGGER = "keylogger" +MALWARE_LABEL_RANSOMWARE = "ransomware" +MALWARE_LABEL_REMOTE_ACCESS_TROJAN = "remote-access-trojan" +MALWARE_LABEL_RESOURCE_EXPLOITATION = "resource-exploitation" +MALWARE_LABEL_ROGUE_SECURITY_SOFTWARE = "rogue-security-software" +MALWARE_LABEL_ROOTKIT = "rootkit" +MALWARE_LABEL_SCREEN_CAPTURE = "screen-capture" +MALWARE_LABEL_SPYWARE = "spyware" +MALWARE_LABEL_TROJAN = "trojan" +MALWARE_LABEL_VIRUS = "virus" +MALWARE_LABEL_WORM = "worm" + + +MALWARE_LABEL = [ + MALWARE_LABEL_ADWARE, + MALWARE_LABEL_BACKDOOR, + MALWARE_LABEL_BOT, + MALWARE_LABEL_DDOS, + MALWARE_LABEL_DROPPER, + MALWARE_LABEL_EXPLOIT_KIT, + MALWARE_LABEL_KEYLOGGER, + MALWARE_LABEL_RANSOMWARE, + MALWARE_LABEL_REMOTE_ACCESS_TROJAN, + MALWARE_LABEL_RESOURCE_EXPLOITATION, + MALWARE_LABEL_ROGUE_SECURITY_SOFTWARE, + MALWARE_LABEL_ROOTKIT, + MALWARE_LABEL_SCREEN_CAPTURE, + MALWARE_LABEL_SPYWARE, + MALWARE_LABEL_TROJAN, + MALWARE_LABEL_VIRUS, + MALWARE_LABEL_WORM, +] + + +REPORT_LABEL_THREAT_REPORT = "threat-report" +REPORT_LABEL_ATTACK_PATTERN = "attack-pattern" +REPORT_LABEL_CAMPAIGN = "campaign" +REPORT_LABEL_IDENTITY = "identity" +REPORT_LABEL_INDICATOR = "indicator" +REPORT_LABEL_INTRUSION_SET = "intrusion-set" +REPORT_LABEL_MALWARE = "malware" +REPORT_LABEL_OBSERVED_DATA = "observed-data" +REPORT_LABEL_THREAT_ACTOR = "threat-actor" +REPORT_LABEL_TOOL = "tool" +REPORT_LABEL_VULNERABILITY = "vulnerability" + + +REPORT_LABEL = [ + REPORT_LABEL_THREAT_REPORT, + REPORT_LABEL_ATTACK_PATTERN, + REPORT_LABEL_CAMPAIGN, + REPORT_LABEL_IDENTITY, + REPORT_LABEL_INDICATOR, + REPORT_LABEL_INTRUSION_SET, + REPORT_LABEL_MALWARE, + REPORT_LABEL_OBSERVED_DATA, + REPORT_LABEL_THREAT_ACTOR, + REPORT_LABEL_TOOL, + REPORT_LABEL_VULNERABILITY, +] + + +THREAT_ACTOR_LABEL_ACTIVIST = "activist" +THREAT_ACTOR_LABEL_COMPETITOR = "competitor" +THREAT_ACTOR_LABEL_CRIME_SYNDICATE = "crime-syndicate" +THREAT_ACTOR_LABEL_CRIMINAL = "criminal" +THREAT_ACTOR_LABEL_HACKER = "hacker" +THREAT_ACTOR_LABEL_INSIDER_ACCIDENTAL = "insider-accidental" +THREAT_ACTOR_LABEL_INSIDER_DISGRUNTLED = "insider-disgruntled" +THREAT_ACTOR_LABEL_NATION_STATE = "nation-state" +THREAT_ACTOR_LABEL_SENSATIONALIST = "sensationalist" +THREAT_ACTOR_LABEL_SPY = "spy" +THREAT_ACTOR_LABEL_TERRORIST = "terrorist" + + +THREAT_ACTOR_LABEL = [ + THREAT_ACTOR_LABEL_ACTIVIST, + THREAT_ACTOR_LABEL_COMPETITOR, + THREAT_ACTOR_LABEL_CRIME_SYNDICATE, + THREAT_ACTOR_LABEL_CRIMINAL, + THREAT_ACTOR_LABEL_HACKER, + THREAT_ACTOR_LABEL_INSIDER_ACCIDENTAL, + THREAT_ACTOR_LABEL_INSIDER_DISGRUNTLED, + THREAT_ACTOR_LABEL_NATION_STATE, + THREAT_ACTOR_LABEL_SENSATIONALIST, + THREAT_ACTOR_LABEL_SPY, + THREAT_ACTOR_LABEL_TERRORIST, +] + + +THREAT_ACTOR_ROLE_AGENT = "agent" +THREAT_ACTOR_ROLE_DIRECTOR = "director" +THREAT_ACTOR_ROLE_INDEPENDENT = "independent" +THREAT_ACTOR_ROLE_INFRASTRUCTURE_ARCHITECT = "infrastructure-architect" +THREAT_ACTOR_ROLE_INFRASTRUCTURE_OPERATOR = "infrastructure-operator" +THREAT_ACTOR_ROLE_MALWARE_AUTHOR = "malware-author" +THREAT_ACTOR_ROLE_SPONSOR = "sponsor" + + +THREAT_ACTOR_ROLE = [ + THREAT_ACTOR_ROLE_AGENT, + THREAT_ACTOR_ROLE_DIRECTOR, + THREAT_ACTOR_ROLE_INDEPENDENT, + THREAT_ACTOR_ROLE_INFRASTRUCTURE_ARCHITECT, + THREAT_ACTOR_ROLE_INFRASTRUCTURE_OPERATOR, + THREAT_ACTOR_ROLE_MALWARE_AUTHOR, + THREAT_ACTOR_ROLE_SPONSOR, +] + + +THREAT_ACTOR_SOPHISTICATION_NONE = "none" +THREAT_ACTOR_SOPHISTICATION_MINIMAL = "minimal" +THREAT_ACTOR_SOPHISTICATION_INTERMEDIATE = "intermediate" +THREAT_ACTOR_SOPHISTICATION_ADVANCED = "advanced" +THREAT_ACTOR_SOPHISTICATION_EXPERT = "expert" +THREAT_ACTOR_SOPHISTICATION_INNOVATOR = "innovator" +THREAT_ACTOR_SOPHISTICATION_STRATEGIC = "strategic" + + +THREAT_ACTOR_SOPHISTICATION = [ + THREAT_ACTOR_SOPHISTICATION_NONE, + THREAT_ACTOR_SOPHISTICATION_MINIMAL, + THREAT_ACTOR_SOPHISTICATION_INTERMEDIATE, + THREAT_ACTOR_SOPHISTICATION_ADVANCED, + THREAT_ACTOR_SOPHISTICATION_EXPERT, + THREAT_ACTOR_SOPHISTICATION_INNOVATOR, + THREAT_ACTOR_SOPHISTICATION_STRATEGIC, +] + + +TOOL_LABEL_DENIAL_OF_SERVICE = "denial-of-service" +TOOL_LABEL_EXPLOITATION = "exploitation" +TOOL_LABEL_INFORMATION_GATHERING = "information-gathering" +TOOL_LABEL_NETWORK_CAPTURE = "network-capture" +TOOL_LABEL_CREDENTIAL_EXPLOITATION = "credential-exploitation" +TOOL_LABEL_REMOTE_ACCESS = "remote-access" +TOOL_LABEL_VULNERABILITY_SCANNING = "vulnerability-scanning" + + +TOOL_LABEL = [ + TOOL_LABEL_DENIAL_OF_SERVICE, + TOOL_LABEL_EXPLOITATION, + TOOL_LABEL_INFORMATION_GATHERING, + TOOL_LABEL_NETWORK_CAPTURE, + TOOL_LABEL_CREDENTIAL_EXPLOITATION, + TOOL_LABEL_REMOTE_ACCESS, + TOOL_LABEL_VULNERABILITY_SCANNING, +] diff --git a/stix2/v21/bundle.py b/stix2/v21/bundle.py index 0d073ca..ec9777a 100644 --- a/stix2/v21/bundle.py +++ b/stix2/v21/bundle.py @@ -32,9 +32,6 @@ class Bundle(_STIXBase21): kwargs['objects'] = obj_list + kwargs.get('objects', []) - self._allow_custom = kwargs.get('allow_custom', False) - self._properties['objects'].contained.allow_custom = kwargs.get('allow_custom', False) - super(Bundle, self).__init__(**kwargs) def get_obj(self, obj_uuid): diff --git a/stix2/v21/common.py b/stix2/v21/common.py index a18774d..b512c88 100644 --- a/stix2/v21/common.py +++ b/stix2/v21/common.py @@ -14,6 +14,7 @@ from ..properties import ( ) from ..utils import NOW, _get_dict from .base import _STIXBase21 +from .vocab import HASHING_ALGORITHM class ExternalReference(_STIXBase21): @@ -25,7 +26,7 @@ class ExternalReference(_STIXBase21): ('source_name', StringProperty(required=True)), ('description', StringProperty()), ('url', StringProperty()), - ('hashes', HashesProperty(spec_version='2.1')), + ('hashes', HashesProperty(HASHING_ALGORITHM, spec_version="2.1")), ('external_id', StringProperty()), ]) @@ -178,9 +179,9 @@ class MarkingProperty(Property): marking-definition objects. """ - def clean(self, value): + def clean(self, value, allow_custom=False): if type(value) in OBJ_MAP_MARKING.values(): - return value + return value, False else: raise ValueError("must be a Statement, TLP Marking or a registered marking.") diff --git a/stix2/v21/observables.py b/stix2/v21/observables.py index a9cf2ae..4e5876d 100644 --- a/stix2/v21/observables.py +++ b/stix2/v21/observables.py @@ -14,10 +14,17 @@ from ..properties import ( BinaryProperty, BooleanProperty, DictionaryProperty, EmbeddedObjectProperty, EnumProperty, ExtensionsProperty, FloatProperty, HashesProperty, HexProperty, IDProperty, IntegerProperty, ListProperty, - ReferenceProperty, StringProperty, TimestampProperty, TypeProperty, + OpenVocabProperty, ReferenceProperty, StringProperty, TimestampProperty, + TypeProperty, ) from .base import _Extension, _Observable, _STIXBase21 from .common import GranularMarking +from .vocab import ( + ACCOUNT_TYPE, ENCRYPTION_ALGORITHM, HASHING_ALGORITHM, + NETWORK_SOCKET_ADDRESS_FAMILY, NETWORK_SOCKET_TYPE, + WINDOWS_INTEGRITY_LEVEL, WINDOWS_PEBINARY_TYPE, WINDOWS_REGISTRY_DATATYPE, + WINDOWS_SERVICE_START_TYPE, WINDOWS_SERVICE_STATUS, WINDOWS_SERVICE_TYPE, +) class Artifact(_Observable): @@ -33,8 +40,8 @@ class Artifact(_Observable): ('mime_type', StringProperty()), ('payload_bin', BinaryProperty()), ('url', StringProperty()), - ('hashes', HashesProperty(spec_version='2.1')), - ('encryption_algorithm', StringProperty()), + ('hashes', HashesProperty(HASHING_ALGORITHM, spec_version="2.1")), + ('encryption_algorithm', EnumProperty(ENCRYPTION_ALGORITHM)), ('decryption_key', StringProperty()), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), @@ -212,7 +219,7 @@ class AlternateDataStream(_STIXBase21): _properties = OrderedDict([ ('name', StringProperty(required=True)), - ('hashes', HashesProperty(spec_version='2.1')), + ('hashes', HashesProperty(HASHING_ALGORITHM, spec_version="2.1")), ('size', IntegerProperty()), ]) @@ -294,7 +301,7 @@ class WindowsPEOptionalHeaderType(_STIXBase21): ('size_of_heap_commit', IntegerProperty()), ('loader_flags_hex', HexProperty()), ('number_of_rva_and_sizes', IntegerProperty()), - ('hashes', HashesProperty(spec_version='2.1')), + ('hashes', HashesProperty(HASHING_ALGORITHM, spec_version="2.1")), ]) def _check_object_constraints(self): @@ -311,7 +318,7 @@ class WindowsPESection(_STIXBase21): ('name', StringProperty(required=True)), ('size', IntegerProperty(min=0)), ('entropy', FloatProperty()), - ('hashes', HashesProperty(spec_version='2.1')), + ('hashes', HashesProperty(HASHING_ALGORITHM, spec_version="2.1")), ]) @@ -322,7 +329,7 @@ class WindowsPEBinaryExt(_Extension): _type = 'windows-pebinary-ext' _properties = OrderedDict([ - ('pe_type', StringProperty(required=True)), # open_vocab + ('pe_type', OpenVocabProperty(WINDOWS_PEBINARY_TYPE, required=True)), ('imphash', StringProperty()), ('machine_hex', HexProperty()), ('number_of_sections', IntegerProperty(min=0)), @@ -331,7 +338,7 @@ class WindowsPEBinaryExt(_Extension): ('number_of_symbols', IntegerProperty(min=0)), ('size_of_optional_header', IntegerProperty(min=0)), ('characteristics_hex', HexProperty()), - ('file_header_hashes', HashesProperty(spec_version='2.1')), + ('file_header_hashes', HashesProperty(HASHING_ALGORITHM, spec_version="2.1")), ('optional_header', EmbeddedObjectProperty(type=WindowsPEOptionalHeaderType)), ('sections', ListProperty(EmbeddedObjectProperty(type=WindowsPESection))), ]) @@ -347,7 +354,7 @@ class File(_Observable): ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), - ('hashes', HashesProperty(spec_version='2.1')), + ('hashes', HashesProperty(HASHING_ALGORITHM, spec_version="2.1")), ('size', IntegerProperty(min=0)), ('name', StringProperty()), ('name_enc', StringProperty()), @@ -486,34 +493,11 @@ class SocketExt(_Extension): _type = 'socket-ext' _properties = OrderedDict([ - ( - 'address_family', EnumProperty( - allowed=[ - "AF_UNSPEC", - "AF_INET", - "AF_IPX", - "AF_APPLETALK", - "AF_NETBIOS", - "AF_INET6", - "AF_IRDA", - "AF_BTH", - ], required=True, - ), - ), + ('address_family', EnumProperty(NETWORK_SOCKET_ADDRESS_FAMILY, required=True)), ('is_blocking', BooleanProperty()), ('is_listening', BooleanProperty()), ('options', DictionaryProperty(spec_version='2.1')), - ( - 'socket_type', EnumProperty( - allowed=[ - "SOCK_STREAM", - "SOCK_DGRAM", - "SOCK_RAW", - "SOCK_RDM", - "SOCK_SEQPACKET", - ], - ), - ), + ('socket_type', EnumProperty(NETWORK_SOCKET_TYPE)), ('socket_descriptor', IntegerProperty(min=0)), ('socket_handle', IntegerProperty()), ]) @@ -612,16 +596,7 @@ class WindowsProcessExt(_Extension): ('owner_sid', StringProperty()), ('window_title', StringProperty()), ('startup_info', DictionaryProperty(spec_version='2.1')), - ( - 'integrity_level', EnumProperty( - allowed=[ - "low", - "medium", - "high", - "system", - ], - ), - ), + ('integrity_level', EnumProperty(WINDOWS_INTEGRITY_LEVEL)), ]) @@ -636,41 +611,10 @@ class WindowsServiceExt(_Extension): ('descriptions', ListProperty(StringProperty)), ('display_name', StringProperty()), ('group_name', StringProperty()), - ( - 'start_type', EnumProperty( - allowed=[ - "SERVICE_AUTO_START", - "SERVICE_BOOT_START", - "SERVICE_DEMAND_START", - "SERVICE_DISABLED", - "SERVICE_SYSTEM_ALERT", - ], - ), - ), + ('start_type', EnumProperty(WINDOWS_SERVICE_START_TYPE)), ('service_dll_refs', ListProperty(ReferenceProperty(valid_types='file', spec_version='2.1'))), - ( - 'service_type', EnumProperty( - allowed=[ - "SERVICE_KERNEL_DRIVER", - "SERVICE_FILE_SYSTEM_DRIVER", - "SERVICE_WIN32_OWN_PROCESS", - "SERVICE_WIN32_SHARE_PROCESS", - ], - ), - ), - ( - 'service_status', EnumProperty( - allowed=[ - "SERVICE_CONTINUE_PENDING", - "SERVICE_PAUSE_PENDING", - "SERVICE_PAUSED", - "SERVICE_RUNNING", - "SERVICE_START_PENDING", - "SERVICE_STOP_PENDING", - "SERVICE_STOPPED", - ], - ), - ), + ('service_type', EnumProperty(WINDOWS_SERVICE_TYPE)), + ('service_status', EnumProperty(WINDOWS_SERVICE_STATUS)), ]) @@ -788,7 +732,7 @@ class UserAccount(_Observable): ('user_id', StringProperty()), ('credential', StringProperty()), ('account_login', StringProperty()), - ('account_type', StringProperty()), # open vocab + ('account_type', OpenVocabProperty(ACCOUNT_TYPE)), ('display_name', StringProperty()), ('is_service_account', BooleanProperty()), ('is_privileged', BooleanProperty()), @@ -816,25 +760,7 @@ class WindowsRegistryValueType(_STIXBase21): _properties = OrderedDict([ ('name', StringProperty()), ('data', StringProperty()), - ( - 'data_type', EnumProperty( - allowed=[ - "REG_NONE", - "REG_SZ", - "REG_EXPAND_SZ", - "REG_BINARY", - "REG_DWORD", - "REG_DWORD_BIG_ENDIAN", - "REG_LINK", - "REG_MULTI_SZ", - "REG_RESOURCE_LIST", - "REG_FULL_RESOURCE_DESCRIPTION", - "REG_RESOURCE_REQUIREMENTS_LIST", - "REG_QWORD", - "REG_INVALID_TYPE", - ], - ), - ), + ('data_type', EnumProperty(WINDOWS_REGISTRY_DATATYPE)), ]) @@ -899,7 +825,7 @@ class X509Certificate(_Observable): ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('is_self_signed', BooleanProperty()), - ('hashes', HashesProperty(spec_version='2.1')), + ('hashes', HashesProperty(HASHING_ALGORITHM, spec_version="2.1")), ('version', StringProperty()), ('serial_number', StringProperty()), ('signature_algorithm', StringProperty()), diff --git a/stix2/v21/sdo.py b/stix2/v21/sdo.py index a834ce5..bfa741f 100644 --- a/stix2/v21/sdo.py +++ b/stix2/v21/sdo.py @@ -15,12 +15,20 @@ from ..exceptions import ( from ..properties import ( BooleanProperty, EnumProperty, ExtensionsProperty, FloatProperty, IDProperty, IntegerProperty, ListProperty, ObservableProperty, - PatternProperty, ReferenceProperty, StringProperty, TimestampProperty, - TypeProperty, + OpenVocabProperty, PatternProperty, ReferenceProperty, StringProperty, + TimestampProperty, TypeProperty, ) from ..utils import NOW from .base import _DomainObject from .common import ExternalReference, GranularMarking, KillChainPhase +from .vocab import ( + ATTACK_MOTIVATION, ATTACK_RESOURCE_LEVEL, GROUPING_CONTEXT, IDENTITY_CLASS, + IMPLEMENTATION_LANGUAGE, INDICATOR_TYPE, INDUSTRY_SECTOR, + INFRASTRUCTURE_TYPE, MALWARE_CAPABILITIES, MALWARE_RESULT, MALWARE_TYPE, + OPINION, PATTERN_TYPE, PROCESSOR_ARCHITECTURE, REGION, REPORT_TYPE, + THREAT_ACTOR_ROLE, THREAT_ACTOR_SOPHISTICATION, THREAT_ACTOR_TYPE, + TOOL_TYPE, +) class AttackPattern(_DomainObject): @@ -132,7 +140,7 @@ class Grouping(_DomainObject): ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('name', StringProperty()), ('description', StringProperty()), - ('context', StringProperty(required=True)), + ('context', OpenVocabProperty(GROUPING_CONTEXT, required=True)), ('object_refs', ListProperty(ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.1'), required=True)), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), @@ -161,8 +169,8 @@ class Identity(_DomainObject): ('name', StringProperty(required=True)), ('description', StringProperty()), ('roles', ListProperty(StringProperty)), - ('identity_class', StringProperty()), - ('sectors', ListProperty(StringProperty)), + ('identity_class', OpenVocabProperty(IDENTITY_CLASS)), + ('sectors', ListProperty(OpenVocabProperty(INDUSTRY_SECTOR))), ('contact_information', StringProperty()), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), @@ -217,9 +225,9 @@ class Indicator(_DomainObject): ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('name', StringProperty()), ('description', StringProperty()), - ('indicator_types', ListProperty(StringProperty)), + ('indicator_types', ListProperty(OpenVocabProperty(INDICATOR_TYPE))), ('pattern', PatternProperty(required=True)), - ('pattern_type', StringProperty(required=True)), + ('pattern_type', OpenVocabProperty(PATTERN_TYPE, required=True)), ('pattern_version', StringProperty()), ('valid_from', TimestampProperty(default=lambda: NOW)), ('valid_until', TimestampProperty()), @@ -277,7 +285,7 @@ class Infrastructure(_DomainObject): ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('name', StringProperty(required=True)), ('description', StringProperty()), - ('infrastructure_types', ListProperty(StringProperty)), + ('infrastructure_types', ListProperty(OpenVocabProperty(INFRASTRUCTURE_TYPE))), ('aliases', ListProperty(StringProperty)), ('kill_chain_phases', ListProperty(KillChainPhase)), ('first_seen', TimestampProperty()), @@ -322,9 +330,9 @@ class IntrusionSet(_DomainObject): ('first_seen', TimestampProperty()), ('last_seen', TimestampProperty()), ('goals', ListProperty(StringProperty)), - ('resource_level', StringProperty()), - ('primary_motivation', StringProperty()), - ('secondary_motivations', ListProperty(StringProperty)), + ('resource_level', OpenVocabProperty(ATTACK_RESOURCE_LEVEL)), + ('primary_motivation', OpenVocabProperty(ATTACK_MOTIVATION)), + ('secondary_motivations', ListProperty(OpenVocabProperty(ATTACK_MOTIVATION))), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('confidence', IntegerProperty()), @@ -364,7 +372,7 @@ class Location(_DomainObject): ('latitude', FloatProperty(min=-90.0, max=90.0)), ('longitude', FloatProperty(min=-180.0, max=180.0)), ('precision', FloatProperty(min=0.0)), - ('region', StringProperty()), + ('region', OpenVocabProperty(REGION)), ('country', StringProperty()), ('administrative_area', StringProperty()), ('city', StringProperty()), @@ -469,16 +477,16 @@ class Malware(_DomainObject): ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('name', StringProperty()), ('description', StringProperty()), - ('malware_types', ListProperty(StringProperty)), + ('malware_types', ListProperty(OpenVocabProperty(MALWARE_TYPE))), ('is_family', BooleanProperty(required=True)), ('aliases', ListProperty(StringProperty)), ('kill_chain_phases', ListProperty(KillChainPhase)), ('first_seen', TimestampProperty()), ('last_seen', TimestampProperty()), ('operating_system_refs', ListProperty(ReferenceProperty(valid_types='software', spec_version='2.1'))), - ('architecture_execution_envs', ListProperty(StringProperty)), - ('implementation_languages', ListProperty(StringProperty)), - ('capabilities', ListProperty(StringProperty)), + ('architecture_execution_envs', ListProperty(OpenVocabProperty(PROCESSOR_ARCHITECTURE))), + ('implementation_languages', ListProperty(OpenVocabProperty(IMPLEMENTATION_LANGUAGE))), + ('capabilities', ListProperty(OpenVocabProperty(MALWARE_CAPABILITIES))), ('sample_refs', ListProperty(ReferenceProperty(valid_types=['artifact', 'file'], spec_version='2.1'))), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), @@ -533,7 +541,7 @@ class MalwareAnalysis(_DomainObject): ('analysis_started', TimestampProperty()), ('analysis_ended', TimestampProperty()), ('result_name', StringProperty()), - ('result', StringProperty()), + ('result', OpenVocabProperty(MALWARE_RESULT)), ('analysis_sco_refs', ListProperty(ReferenceProperty(valid_types="SCO", spec_version='2.1'))), ('sample_ref', ReferenceProperty(valid_types="SCO", spec_version='2.1')), ('revoked', BooleanProperty(default=lambda: False)), @@ -609,8 +617,6 @@ class ObservedData(_DomainObject): ]) def __init__(self, *args, **kwargs): - self._allow_custom = kwargs.get('allow_custom', False) - self._properties['objects'].allow_custom = kwargs.get('allow_custom', False) if "objects" in kwargs: warnings.warn( @@ -651,17 +657,7 @@ class Opinion(_DomainObject): ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('explanation', StringProperty()), ('authors', ListProperty(StringProperty)), - ( - 'opinion', EnumProperty( - allowed=[ - 'strongly-disagree', - 'disagree', - 'neutral', - 'agree', - 'strongly-agree', - ], required=True, - ), - ), + ('opinion', EnumProperty(OPINION, required=True)), ('object_refs', ListProperty(ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.1'), required=True)), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), @@ -689,7 +685,7 @@ class Report(_DomainObject): ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('name', StringProperty(required=True)), ('description', StringProperty()), - ('report_types', ListProperty(StringProperty)), + ('report_types', ListProperty(OpenVocabProperty(REPORT_TYPE))), ('published', TimestampProperty(required=True)), ('object_refs', ListProperty(ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.1'), required=True)), ('revoked', BooleanProperty(default=lambda: False)), @@ -718,17 +714,17 @@ class ThreatActor(_DomainObject): ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('name', StringProperty(required=True)), ('description', StringProperty()), - ('threat_actor_types', ListProperty(StringProperty)), + ('threat_actor_types', ListProperty(OpenVocabProperty(THREAT_ACTOR_TYPE))), ('aliases', ListProperty(StringProperty)), ('first_seen', TimestampProperty()), ('last_seen', TimestampProperty()), - ('roles', ListProperty(StringProperty)), + ('roles', ListProperty(OpenVocabProperty(THREAT_ACTOR_ROLE))), ('goals', ListProperty(StringProperty)), - ('sophistication', StringProperty()), - ('resource_level', StringProperty()), - ('primary_motivation', StringProperty()), - ('secondary_motivations', ListProperty(StringProperty)), - ('personal_motivations', ListProperty(StringProperty)), + ('sophistication', OpenVocabProperty(THREAT_ACTOR_SOPHISTICATION)), + ('resource_level', OpenVocabProperty(ATTACK_RESOURCE_LEVEL)), + ('primary_motivation', OpenVocabProperty(ATTACK_MOTIVATION)), + ('secondary_motivations', ListProperty(OpenVocabProperty(ATTACK_MOTIVATION))), + ('personal_motivations', ListProperty(OpenVocabProperty(ATTACK_MOTIVATION))), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('confidence', IntegerProperty()), @@ -765,7 +761,7 @@ class Tool(_DomainObject): ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('name', StringProperty(required=True)), ('description', StringProperty()), - ('tool_types', ListProperty(StringProperty)), + ('tool_types', ListProperty(OpenVocabProperty(TOOL_TYPE))), ('aliases', ListProperty(StringProperty)), ('kill_chain_phases', ListProperty(KillChainPhase)), ('tool_version', StringProperty()), diff --git a/stix2/v21/vocab.py b/stix2/v21/vocab.py new file mode 100644 index 0000000..dc790d7 --- /dev/null +++ b/stix2/v21/vocab.py @@ -0,0 +1,841 @@ +""" +STIX 2.1 open vocabularies and enums +""" + + +ACCOUNT_TYPE_FACEBOOK = "facebook" +ACCOUNT_TYPE_LDAP = "ldap" +ACCOUNT_TYPE_NIS = "nis" +ACCOUNT_TYPE_OPENID = "openid" +ACCOUNT_TYPE_RADIUS = "radius" +ACCOUNT_TYPE_SKYPE = "skype" +ACCOUNT_TYPE_TACACS = "tacacs" +ACCOUNT_TYPE_TWITTER = "twitter" +ACCOUNT_TYPE_UNIX = "unix" +ACCOUNT_TYPE_WINDOWS_LOCAL = "windows-local" +ACCOUNT_TYPE_WINDOWS_DOMAIN = "windows-domain" + + +ACCOUNT_TYPE = [ + ACCOUNT_TYPE_FACEBOOK, + ACCOUNT_TYPE_LDAP, + ACCOUNT_TYPE_NIS, + ACCOUNT_TYPE_OPENID, + ACCOUNT_TYPE_RADIUS, + ACCOUNT_TYPE_SKYPE, + ACCOUNT_TYPE_TACACS, + ACCOUNT_TYPE_TWITTER, + ACCOUNT_TYPE_UNIX, + ACCOUNT_TYPE_WINDOWS_LOCAL, + ACCOUNT_TYPE_WINDOWS_DOMAIN, +] + + +ATTACK_MOTIVATION_ACCIDENTAL = "accidental" +ATTACK_MOTIVATION_COERCION = "coercion" +ATTACK_MOTIVATION_DOMINANCE = "dominance" +ATTACK_MOTIVATION_IDEOLOGY = "ideology" +ATTACK_MOTIVATION_NOTORIETY = "notoriety" +ATTACK_MOTIVATION_ORGANIZATIONAL_GAIN = "organizational-gain" +ATTACK_MOTIVATION_PERSONAL_GAIN = "personal-gain" +ATTACK_MOTIVATION_PERSONAL_SATISFACTION = "personal-satisfaction" +ATTACK_MOTIVATION_REVENGE = "revenge" +ATTACK_MOTIVATION_UNPREDICTABLE = "unpredictable" + + +ATTACK_MOTIVATION = [ + ATTACK_MOTIVATION_ACCIDENTAL, + ATTACK_MOTIVATION_COERCION, + ATTACK_MOTIVATION_DOMINANCE, + ATTACK_MOTIVATION_IDEOLOGY, + ATTACK_MOTIVATION_NOTORIETY, + ATTACK_MOTIVATION_ORGANIZATIONAL_GAIN, + ATTACK_MOTIVATION_PERSONAL_GAIN, + ATTACK_MOTIVATION_PERSONAL_SATISFACTION, + ATTACK_MOTIVATION_REVENGE, + ATTACK_MOTIVATION_UNPREDICTABLE, +] + + +ATTACK_RESOURCE_LEVEL_INDIVIDUAL = "individual" +ATTACK_RESOURCE_LEVEL_CLUB = "club" +ATTACK_RESOURCE_LEVEL_CONTEST = "contest" +ATTACK_RESOURCE_LEVEL_TEAM = "team" +ATTACK_RESOURCE_LEVEL_ORGANIZATION = "organization" +ATTACK_RESOURCE_LEVEL_GOVERNMENT = "government" + + +ATTACK_RESOURCE_LEVEL = [ + ATTACK_RESOURCE_LEVEL_INDIVIDUAL, + ATTACK_RESOURCE_LEVEL_CLUB, + ATTACK_RESOURCE_LEVEL_CONTEST, + ATTACK_RESOURCE_LEVEL_TEAM, + ATTACK_RESOURCE_LEVEL_ORGANIZATION, + ATTACK_RESOURCE_LEVEL_GOVERNMENT, +] + + +ENCRYPTION_ALGORITHM_AES_256_GCM = "AES-256-GCM" +ENCRYPTION_ALGORITHM_CHACHA20_POLY1305 = "ChaCha20-Poly1305" +ENCRYPTION_ALGORITHM_MIME_TYPE_INDICATED = "mime-type-indicated" + + +ENCRYPTION_ALGORITHM = [ + ENCRYPTION_ALGORITHM_AES_256_GCM, + ENCRYPTION_ALGORITHM_CHACHA20_POLY1305, + ENCRYPTION_ALGORITHM_MIME_TYPE_INDICATED, +] + + +GROUPING_CONTEXT_SUSPICIOUS_ACTIVITY = "suspicious-activity" +GROUPING_CONTEXT_MALWARE_ANALYSIS = "malware-analysis" +GROUPING_CONTEXT_UNSPECIFIED = "unspecified" + + +GROUPING_CONTEXT = [ + GROUPING_CONTEXT_SUSPICIOUS_ACTIVITY, + GROUPING_CONTEXT_MALWARE_ANALYSIS, + GROUPING_CONTEXT_UNSPECIFIED, +] + + +HASHING_ALGORITHM_MD5 = "MD5" +HASHING_ALGORITHM_SHA_1 = "SHA-1" +HASHING_ALGORITHM_SHA_256 = "SHA-256" +HASHING_ALGORITHM_SHA_512 = "SHA-512" +HASHING_ALGORITHM_SHA3_256 = "SHA3-256" +HASHING_ALGORITHM_SHA3_512 = "SHA3-512" +HASHING_ALGORITHM_SSDEEP = "SSDEEP" +HASHING_ALGORITHM_TLSH = "TLSH" + + +HASHING_ALGORITHM = [ + HASHING_ALGORITHM_MD5, + HASHING_ALGORITHM_SHA_1, + HASHING_ALGORITHM_SHA_256, + HASHING_ALGORITHM_SHA_512, + HASHING_ALGORITHM_SHA3_256, + HASHING_ALGORITHM_SHA3_512, + HASHING_ALGORITHM_SSDEEP, + HASHING_ALGORITHM_TLSH, +] + + +IDENTITY_CLASS_INDIVIDUAL = "individual" +IDENTITY_CLASS_GROUP = "group" +IDENTITY_CLASS_SYSTEM = "system" +IDENTITY_CLASS_ORGANIZATION = "organization" +IDENTITY_CLASS_CLASS = "class" +IDENTITY_CLASS_UNKNOWN = "unknown" + + +IDENTITY_CLASS = [ + IDENTITY_CLASS_INDIVIDUAL, + IDENTITY_CLASS_GROUP, + IDENTITY_CLASS_SYSTEM, + IDENTITY_CLASS_ORGANIZATION, + IDENTITY_CLASS_CLASS, + IDENTITY_CLASS_UNKNOWN, +] + + +IMPLEMENTATION_LANGUAGE_APPLESCRIPT = "applescript" +IMPLEMENTATION_LANGUAGE_BASH = "bash" +IMPLEMENTATION_LANGUAGE_C = "c" +IMPLEMENTATION_LANGUAGE_CPLUSPLUS = "c++" +IMPLEMENTATION_LANGUAGE_CSHARP = "c#" +IMPLEMENTATION_LANGUAGE_GO = "go" +IMPLEMENTATION_LANGUAGE_JAVA = "java" +IMPLEMENTATION_LANGUAGE_JAVASCRIPT = "javascript" +IMPLEMENTATION_LANGUAGE_LUA = "lua" +IMPLEMENTATION_LANGUAGE_OBJECTIVE_C = "objective-c" +IMPLEMENTATION_LANGUAGE_PERL = "perl" +IMPLEMENTATION_LANGUAGE_PHP = "php" +IMPLEMENTATION_LANGUAGE_POWERSHELL = "powershell" +IMPLEMENTATION_LANGUAGE_PYTHON = "python" +IMPLEMENTATION_LANGUAGE_RUBY = "ruby" +IMPLEMENTATION_LANGUAGE_SCALA = "scala" +IMPLEMENTATION_LANGUAGE_SWIFT = "swift" +IMPLEMENTATION_LANGUAGE_TYPESCRIPT = "typescript" +IMPLEMENTATION_LANGUAGE_VISUAL_BASIC = "visual-basic" +IMPLEMENTATION_LANGUAGE_X86_32 = "x86-32" +IMPLEMENTATION_LANGUAGE_X86_64 = "x86-64" + + +IMPLEMENTATION_LANGUAGE = [ + IMPLEMENTATION_LANGUAGE_APPLESCRIPT, + IMPLEMENTATION_LANGUAGE_BASH, + IMPLEMENTATION_LANGUAGE_C, + IMPLEMENTATION_LANGUAGE_CPLUSPLUS, + IMPLEMENTATION_LANGUAGE_CSHARP, + IMPLEMENTATION_LANGUAGE_GO, + IMPLEMENTATION_LANGUAGE_JAVA, + IMPLEMENTATION_LANGUAGE_JAVASCRIPT, + IMPLEMENTATION_LANGUAGE_LUA, + IMPLEMENTATION_LANGUAGE_OBJECTIVE_C, + IMPLEMENTATION_LANGUAGE_PERL, + IMPLEMENTATION_LANGUAGE_PHP, + IMPLEMENTATION_LANGUAGE_POWERSHELL, + IMPLEMENTATION_LANGUAGE_PYTHON, + IMPLEMENTATION_LANGUAGE_RUBY, + IMPLEMENTATION_LANGUAGE_SCALA, + IMPLEMENTATION_LANGUAGE_SWIFT, + IMPLEMENTATION_LANGUAGE_TYPESCRIPT, + IMPLEMENTATION_LANGUAGE_VISUAL_BASIC, + IMPLEMENTATION_LANGUAGE_X86_32, + IMPLEMENTATION_LANGUAGE_X86_64, +] + + +INDICATOR_TYPE_ANOMALOUS_ACTIVITY = "anomalous-activity" +INDICATOR_TYPE_ANONYMIZATION = "anonymization" +INDICATOR_TYPE_BENIGN = "benign" +INDICATOR_TYPE_COMPROMISED = "compromised" +INDICATOR_TYPE_MALICIOUS_ACTIVITY = "malicious-activity" +INDICATOR_TYPE_ATTRIBUTION = "attribution" +INDICATOR_TYPE_UNKNOWN = "unknown" + + +INDICATOR_TYPE = [ + INDICATOR_TYPE_ANOMALOUS_ACTIVITY, + INDICATOR_TYPE_ANONYMIZATION, + INDICATOR_TYPE_BENIGN, + INDICATOR_TYPE_COMPROMISED, + INDICATOR_TYPE_MALICIOUS_ACTIVITY, + INDICATOR_TYPE_ATTRIBUTION, + INDICATOR_TYPE_UNKNOWN, +] + + +INDUSTRY_SECTOR_AGRICULTURE = "agriculture" +INDUSTRY_SECTOR_AEROSPACE = "aerospace" +INDUSTRY_SECTOR_AUTOMOTIVE = "automotive" +INDUSTRY_SECTOR_CHEMICAL = "chemical" +INDUSTRY_SECTOR_COMMERCIAL = "commercial" +INDUSTRY_SECTOR_COMMUNICATIONS = "communications" +INDUSTRY_SECTOR_CONSTRUCTION = "construction" +INDUSTRY_SECTOR_DEFENSE = "defense" +INDUSTRY_SECTOR_EDUCATION = "education" +INDUSTRY_SECTOR_ENERGY = "energy" +INDUSTRY_SECTOR_ENTERTAINMENT = "entertainment" +INDUSTRY_SECTOR_FINANCIAL_SERVICES = "financial-services" +INDUSTRY_SECTOR_GOVERNMENT = "government" +INDUSTRY_SECTOR_EMERGENCY_SERVICES = "emergency-services" +INDUSTRY_SECTOR_GOVERNMENT_NATIONAL = "government-national" +INDUSTRY_SECTOR_GOVERNMENT_REGIONAL = "government-regional" +INDUSTRY_SECTOR_GOVERNMENT_LOCAL = "government-local" +INDUSTRY_SECTOR_GOVERNMENT_PUBLIC_SERVICES = "government-public-services" +INDUSTRY_SECTOR_HEALTHCARE = "healthcare" +INDUSTRY_SECTOR_HOSPITALITY_LEISURE = "hospitality-leisure" +INDUSTRY_SECTOR_INFRASTRUCTURE = "infrastructure" +INDUSTRY_SECTOR_DAMS = "dams" +INDUSTRY_SECTOR_NUCLEAR = "nuclear" +INDUSTRY_SECTOR_WATER = "water" +INDUSTRY_SECTOR_INSURANCE = "insurance" +INDUSTRY_SECTOR_MANUFACTURING = "manufacturing" +INDUSTRY_SECTOR_MINING = "mining" +INDUSTRY_SECTOR_NON_PROFIT = "non-profit" +INDUSTRY_SECTOR_PHARMACEUTICALS = "pharmaceuticals" +INDUSTRY_SECTOR_RETAIL = "retail" +INDUSTRY_SECTOR_TECHNOLOGY = "technology" +INDUSTRY_SECTOR_TELECOMMUNICATIONS = "telecommunications" +INDUSTRY_SECTOR_TRANSPORTATION = "transportation" +INDUSTRY_SECTOR_UTILITIES = "utilities" + + +INDUSTRY_SECTOR = [ + INDUSTRY_SECTOR_AGRICULTURE, + INDUSTRY_SECTOR_AEROSPACE, + INDUSTRY_SECTOR_AUTOMOTIVE, + INDUSTRY_SECTOR_CHEMICAL, + INDUSTRY_SECTOR_COMMERCIAL, + INDUSTRY_SECTOR_COMMUNICATIONS, + INDUSTRY_SECTOR_CONSTRUCTION, + INDUSTRY_SECTOR_DEFENSE, + INDUSTRY_SECTOR_EDUCATION, + INDUSTRY_SECTOR_ENERGY, + INDUSTRY_SECTOR_ENTERTAINMENT, + INDUSTRY_SECTOR_FINANCIAL_SERVICES, + INDUSTRY_SECTOR_GOVERNMENT, + INDUSTRY_SECTOR_EMERGENCY_SERVICES, + INDUSTRY_SECTOR_GOVERNMENT_NATIONAL, + INDUSTRY_SECTOR_GOVERNMENT_REGIONAL, + INDUSTRY_SECTOR_GOVERNMENT_LOCAL, + INDUSTRY_SECTOR_GOVERNMENT_PUBLIC_SERVICES, + INDUSTRY_SECTOR_HEALTHCARE, + INDUSTRY_SECTOR_HOSPITALITY_LEISURE, + INDUSTRY_SECTOR_INFRASTRUCTURE, + INDUSTRY_SECTOR_DAMS, + INDUSTRY_SECTOR_NUCLEAR, + INDUSTRY_SECTOR_WATER, + INDUSTRY_SECTOR_INSURANCE, + INDUSTRY_SECTOR_MANUFACTURING, + INDUSTRY_SECTOR_MINING, + INDUSTRY_SECTOR_NON_PROFIT, + INDUSTRY_SECTOR_PHARMACEUTICALS, + INDUSTRY_SECTOR_RETAIL, + INDUSTRY_SECTOR_TECHNOLOGY, + INDUSTRY_SECTOR_TELECOMMUNICATIONS, + INDUSTRY_SECTOR_TRANSPORTATION, + INDUSTRY_SECTOR_UTILITIES, +] + + +INFRASTRUCTURE_TYPE_AMPLIFICATION = "amplification" +INFRASTRUCTURE_TYPE_ANONYMIZATION = "anonymization" +INFRASTRUCTURE_TYPE_BOTNET = "botnet" +INFRASTRUCTURE_TYPE_COMMAND_AND_CONTROL = "command-and-control" +INFRASTRUCTURE_TYPE_EXFILTRATION = "exfiltration" +INFRASTRUCTURE_TYPE_HOSTING_MALWARE = "hosting-malware" +INFRASTRUCTURE_TYPE_HOSTING_TARGET_LISTS = "hosting-target-lists" +INFRASTRUCTURE_TYPE_PHISHING = "phishing" +INFRASTRUCTURE_TYPE_RECONNAISSANCE = "reconnaissance" +INFRASTRUCTURE_TYPE_STAGING = "staging" +INFRASTRUCTURE_TYPE_UNKNOWN = "unknown" + + +INFRASTRUCTURE_TYPE = [ + INFRASTRUCTURE_TYPE_AMPLIFICATION, + INFRASTRUCTURE_TYPE_ANONYMIZATION, + INFRASTRUCTURE_TYPE_BOTNET, + INFRASTRUCTURE_TYPE_COMMAND_AND_CONTROL, + INFRASTRUCTURE_TYPE_EXFILTRATION, + INFRASTRUCTURE_TYPE_HOSTING_MALWARE, + INFRASTRUCTURE_TYPE_HOSTING_TARGET_LISTS, + INFRASTRUCTURE_TYPE_PHISHING, + INFRASTRUCTURE_TYPE_RECONNAISSANCE, + INFRASTRUCTURE_TYPE_STAGING, + INFRASTRUCTURE_TYPE_UNKNOWN, +] + + +MALWARE_RESULT_MALICIOUS = "malicious" +MALWARE_RESULT_SUSPICIOUS = "suspicious" +MALWARE_RESULT_BENIGN = "benign" +MALWARE_RESULT_UNKNOWN = "unknown" + + +MALWARE_RESULT = [ + MALWARE_RESULT_MALICIOUS, + MALWARE_RESULT_SUSPICIOUS, + MALWARE_RESULT_BENIGN, + MALWARE_RESULT_UNKNOWN, +] + + +MALWARE_CAPABILITIES_ACCESSES_REMOTE_MACHINES = "accesses-remote-machines" +MALWARE_CAPABILITIES_ANTI_DEBUGGING = "anti-debugging" +MALWARE_CAPABILITIES_ANTI_DISASSEMBLY = "anti-disassembly" +MALWARE_CAPABILITIES_ANTI_EMULATION = "anti-emulation" +MALWARE_CAPABILITIES_ANTI_MEMORY_FORENSICS = "anti-memory-forensics" +MALWARE_CAPABILITIES_ANTI_SANDBOX = "anti-sandbox" +MALWARE_CAPABILITIES_ANTI_VM = "anti-vm" +MALWARE_CAPABILITIES_CAPTURES_INPUT_PERIPHERALS = "captures-input-peripherals" +MALWARE_CAPABILITIES_CAPTURES_OUTPUT_PERIPHERALS = "captures-output-peripherals" +MALWARE_CAPABILITIES_CAPTURES_SYSTEM_STATE_DATA = "captures-system-state-data" +MALWARE_CAPABILITIES_CLEANS_TRACES_OF_INFECTION = "cleans-traces-of-infection" +MALWARE_CAPABILITIES_COMMITS_FRAUD = "commits-fraud" +MALWARE_CAPABILITIES_COMMUNICATES_WITH_C2 = "communicates-with-c2" +MALWARE_CAPABILITIES_COMPROMISES_DATA_AVAILABILITY = "compromises-data-availability" +MALWARE_CAPABILITIES_COMPROMISES_DATA_INTEGRITY = "compromises-data-integrity" +MALWARE_CAPABILITIES_COMPROMISES_SYSTEM_AVAILABILITY = "compromises-system-availability" +MALWARE_CAPABILITIES_CONTROLS_LOCAL_MACHINE = "controls-local-machine" +MALWARE_CAPABILITIES_DEGRADES_SECURITY_SOFTWARE = "degrades-security-software" +MALWARE_CAPABILITIES_DEGRADES_SYSTEM_UPDATES = "degrades-system-updates" +MALWARE_CAPABILITIES_DETERMINES_C2_SERVER = "determines-c2-server" +MALWARE_CAPABILITIES_EMAILS_SPAM = "emails-spam" +MALWARE_CAPABILITIES_ESCALATES_PRIVILEGES = "escalates-privileges" +MALWARE_CAPABILITIES_EVADES_AV = "evades-av" +MALWARE_CAPABILITIES_EXFILTRATES_DATA = "exfiltrates-data" +MALWARE_CAPABILITIES_FINGERPRINTS_HOST = "fingerprints-host" +MALWARE_CAPABILITIES_HIDES_ARTIFACTS = "hides-artifacts" +MALWARE_CAPABILITIES_HIDES_EXECUTING_CODE = "hides-executing-code" +MALWARE_CAPABILITIES_INFECTS_FILES = "infects-files" +MALWARE_CAPABILITIES_INFECTS_REMOTE_MACHINES = "infects-remote-machines" +MALWARE_CAPABILITIES_INSTALLS_OTHER_COMPONENTS = "installs-other-components" +MALWARE_CAPABILITIES_PERSISTS_AFTER_SYSTEM_REBOOT = "persists-after-system-reboot" +MALWARE_CAPABILITIES_PREVENTS_ARTIFACT_ACCESS = "prevents-artifact-access" +MALWARE_CAPABILITIES_PREVENTS_ARTIFACT_DELETION = "prevents-artifact-deletion" +MALWARE_CAPABILITIES_PROBES_NETWORK_ENVIRONMENT = "probes-network-environment" +MALWARE_CAPABILITIES_SELF_MODIFIES = "self-modifies" +MALWARE_CAPABILITIES_STEALS_AUTHENTICATION_CREDENTIALS = "steals-authentication-credentials" +MALWARE_CAPABILITIES_VIOLATES_SYSTEM_OPERATIONAL_INTEGRITY = "violates-system-operational-integrity" + + +MALWARE_CAPABILITIES = [ + MALWARE_CAPABILITIES_ACCESSES_REMOTE_MACHINES, + MALWARE_CAPABILITIES_ANTI_DEBUGGING, + MALWARE_CAPABILITIES_ANTI_DISASSEMBLY, + MALWARE_CAPABILITIES_ANTI_EMULATION, + MALWARE_CAPABILITIES_ANTI_MEMORY_FORENSICS, + MALWARE_CAPABILITIES_ANTI_SANDBOX, + MALWARE_CAPABILITIES_ANTI_VM, + MALWARE_CAPABILITIES_CAPTURES_INPUT_PERIPHERALS, + MALWARE_CAPABILITIES_CAPTURES_OUTPUT_PERIPHERALS, + MALWARE_CAPABILITIES_CAPTURES_SYSTEM_STATE_DATA, + MALWARE_CAPABILITIES_CLEANS_TRACES_OF_INFECTION, + MALWARE_CAPABILITIES_COMMITS_FRAUD, + MALWARE_CAPABILITIES_COMMUNICATES_WITH_C2, + MALWARE_CAPABILITIES_COMPROMISES_DATA_AVAILABILITY, + MALWARE_CAPABILITIES_COMPROMISES_DATA_INTEGRITY, + MALWARE_CAPABILITIES_COMPROMISES_SYSTEM_AVAILABILITY, + MALWARE_CAPABILITIES_CONTROLS_LOCAL_MACHINE, + MALWARE_CAPABILITIES_DEGRADES_SECURITY_SOFTWARE, + MALWARE_CAPABILITIES_DEGRADES_SYSTEM_UPDATES, + MALWARE_CAPABILITIES_DETERMINES_C2_SERVER, + MALWARE_CAPABILITIES_EMAILS_SPAM, + MALWARE_CAPABILITIES_ESCALATES_PRIVILEGES, + MALWARE_CAPABILITIES_EVADES_AV, + MALWARE_CAPABILITIES_EXFILTRATES_DATA, + MALWARE_CAPABILITIES_FINGERPRINTS_HOST, + MALWARE_CAPABILITIES_HIDES_ARTIFACTS, + MALWARE_CAPABILITIES_HIDES_EXECUTING_CODE, + MALWARE_CAPABILITIES_INFECTS_FILES, + MALWARE_CAPABILITIES_INFECTS_REMOTE_MACHINES, + MALWARE_CAPABILITIES_INSTALLS_OTHER_COMPONENTS, + MALWARE_CAPABILITIES_PERSISTS_AFTER_SYSTEM_REBOOT, + MALWARE_CAPABILITIES_PREVENTS_ARTIFACT_ACCESS, + MALWARE_CAPABILITIES_PREVENTS_ARTIFACT_DELETION, + MALWARE_CAPABILITIES_PROBES_NETWORK_ENVIRONMENT, + MALWARE_CAPABILITIES_SELF_MODIFIES, + MALWARE_CAPABILITIES_STEALS_AUTHENTICATION_CREDENTIALS, + MALWARE_CAPABILITIES_VIOLATES_SYSTEM_OPERATIONAL_INTEGRITY, +] + + +MALWARE_TYPE_ADWARE = "adware" +MALWARE_TYPE_BACKDOOR = "backdoor" +MALWARE_TYPE_BOT = "bot" +MALWARE_TYPE_BOOTKIT = "bootkit" +MALWARE_TYPE_DDOS = "ddos" +MALWARE_TYPE_DOWNLOADER = "downloader" +MALWARE_TYPE_DROPPER = "dropper" +MALWARE_TYPE_EXPLOIT_KIT = "exploit-kit" +MALWARE_TYPE_KEYLOGGER = "keylogger" +MALWARE_TYPE_RANSOMWARE = "ransomware" +MALWARE_TYPE_REMOTE_ACCESS_TROJAN = "remote-access-trojan" +MALWARE_TYPE_RESOURCE_EXPLOITATION = "resource-exploitation" +MALWARE_TYPE_ROGUE_SECURITY_SOFTWARE = "rogue-security-software" +MALWARE_TYPE_ROOTKIT = "rootkit" +MALWARE_TYPE_SCREEN_CAPTURE = "screen-capture" +MALWARE_TYPE_SPYWARE = "spyware" +MALWARE_TYPE_TROJAN = "trojan" +MALWARE_TYPE_UNKNOWN = "unknown" +MALWARE_TYPE_VIRUS = "virus" +MALWARE_TYPE_WEBSHELL = "webshell" +MALWARE_TYPE_WIPER = "wiper" +MALWARE_TYPE_WORM = "worm" + + +MALWARE_TYPE = [ + MALWARE_TYPE_ADWARE, + MALWARE_TYPE_BACKDOOR, + MALWARE_TYPE_BOT, + MALWARE_TYPE_BOOTKIT, + MALWARE_TYPE_DDOS, + MALWARE_TYPE_DOWNLOADER, + MALWARE_TYPE_DROPPER, + MALWARE_TYPE_EXPLOIT_KIT, + MALWARE_TYPE_KEYLOGGER, + MALWARE_TYPE_RANSOMWARE, + MALWARE_TYPE_REMOTE_ACCESS_TROJAN, + MALWARE_TYPE_RESOURCE_EXPLOITATION, + MALWARE_TYPE_ROGUE_SECURITY_SOFTWARE, + MALWARE_TYPE_ROOTKIT, + MALWARE_TYPE_SCREEN_CAPTURE, + MALWARE_TYPE_SPYWARE, + MALWARE_TYPE_TROJAN, + MALWARE_TYPE_UNKNOWN, + MALWARE_TYPE_VIRUS, + MALWARE_TYPE_WEBSHELL, + MALWARE_TYPE_WIPER, + MALWARE_TYPE_WORM, +] + + +NETWORK_SOCKET_ADDRESS_FAMILY_AF_UNSPEC = "AF_UNSPEC" +NETWORK_SOCKET_ADDRESS_FAMILY_AF_INET = "AF_INET" +NETWORK_SOCKET_ADDRESS_FAMILY_AF_IPX = "AF_IPX" +NETWORK_SOCKET_ADDRESS_FAMILY_AF_APPLETALK = "AF_APPLETALK" +NETWORK_SOCKET_ADDRESS_FAMILY_AF_NETBIOS = "AF_NETBIOS" +NETWORK_SOCKET_ADDRESS_FAMILY_AF_INET6 = "AF_INET6" +NETWORK_SOCKET_ADDRESS_FAMILY_AF_IRDA = "AF_IRDA" +NETWORK_SOCKET_ADDRESS_FAMILY_AF_BTH = "AF_BTH" + + +NETWORK_SOCKET_ADDRESS_FAMILY = [ + NETWORK_SOCKET_ADDRESS_FAMILY_AF_UNSPEC, + NETWORK_SOCKET_ADDRESS_FAMILY_AF_INET, + NETWORK_SOCKET_ADDRESS_FAMILY_AF_IPX, + NETWORK_SOCKET_ADDRESS_FAMILY_AF_APPLETALK, + NETWORK_SOCKET_ADDRESS_FAMILY_AF_NETBIOS, + NETWORK_SOCKET_ADDRESS_FAMILY_AF_INET6, + NETWORK_SOCKET_ADDRESS_FAMILY_AF_IRDA, + NETWORK_SOCKET_ADDRESS_FAMILY_AF_BTH, +] + + +NETWORK_SOCKET_TYPE_SOCK_STREAM = "SOCK_STREAM" +NETWORK_SOCKET_TYPE_SOCK_DGRAM = "SOCK_DGRAM" +NETWORK_SOCKET_TYPE_SOCK_RAW = "SOCK_RAW" +NETWORK_SOCKET_TYPE_SOCK_RDM = "SOCK_RDM" +NETWORK_SOCKET_TYPE_SOCK_SEQPACKET = "SOCK_SEQPACKET" + + +NETWORK_SOCKET_TYPE = [ + NETWORK_SOCKET_TYPE_SOCK_STREAM, + NETWORK_SOCKET_TYPE_SOCK_DGRAM, + NETWORK_SOCKET_TYPE_SOCK_RAW, + NETWORK_SOCKET_TYPE_SOCK_RDM, + NETWORK_SOCKET_TYPE_SOCK_SEQPACKET, +] + + +OPINION_STRONGLY_DISAGREE = "strongly-disagree" +OPINION_DISAGREE = "disagree" +OPINION_NEUTRAL = "neutral" +OPINION_AGREE = "agree" +OPINION_STRONGLY_AGREE = "strongly-agree" + + +OPINION = [ + OPINION_STRONGLY_DISAGREE, + OPINION_DISAGREE, + OPINION_NEUTRAL, + OPINION_AGREE, + OPINION_STRONGLY_AGREE, +] + + +PATTERN_TYPE_STIX = "stix" +PATTERN_TYPE_PCRE = "pcre" +PATTERN_TYPE_SIGMA = "sigma" +PATTERN_TYPE_SNORT = "snort" +PATTERN_TYPE_SURICATA = "suricata" +PATTERN_TYPE_YARA = "yara" + + +PATTERN_TYPE = [ + PATTERN_TYPE_STIX, + PATTERN_TYPE_PCRE, + PATTERN_TYPE_SIGMA, + PATTERN_TYPE_SNORT, + PATTERN_TYPE_SURICATA, + PATTERN_TYPE_YARA, +] + + +PROCESSOR_ARCHITECTURE_ALPHA = "alpha" +PROCESSOR_ARCHITECTURE_ARM = "arm" +PROCESSOR_ARCHITECTURE_IA_64 = "ia-64" +PROCESSOR_ARCHITECTURE_MIPS = "mips" +PROCESSOR_ARCHITECTURE_POWERPC = "powerpc" +PROCESSOR_ARCHITECTURE_SPARC = "sparc" +PROCESSOR_ARCHITECTURE_X86 = "x86" +PROCESSOR_ARCHITECTURE_X86_64 = "x86-64" + + +PROCESSOR_ARCHITECTURE = [ + PROCESSOR_ARCHITECTURE_ALPHA, + PROCESSOR_ARCHITECTURE_ARM, + PROCESSOR_ARCHITECTURE_IA_64, + PROCESSOR_ARCHITECTURE_MIPS, + PROCESSOR_ARCHITECTURE_POWERPC, + PROCESSOR_ARCHITECTURE_SPARC, + PROCESSOR_ARCHITECTURE_X86, + PROCESSOR_ARCHITECTURE_X86_64, +] + + +REGION_AFRICA = "africa" +REGION_EASTERN_AFRICA = "eastern-africa" +REGION_MIDDLE_AFRICA = "middle-africa" +REGION_NORTHERN_AFRICA = "northern-africa" +REGION_SOUTHERN_AFRICA = "southern-africa" +REGION_WESTERN_AFRICA = "western-africa" +REGION_AMERICAS = "americas" +REGION_LATIN_AMERICA_CARIBBEAN = "latin-america-caribbean" +REGION_SOUTH_AMERICA = "south-america" +REGION_CARIBBEAN = "caribbean" +REGION_CENTRAL_AMERICA = "central-america" +REGION_NORTHERN_AMERICA = "northern-america" +REGION_ASIA = "asia" +REGION_CENTRAL_ASIA = "central-asia" +REGION_EASTERN_ASIA = "eastern-asia" +REGION_SOUTHERN_ASIA = "southern-asia" +REGION_SOUTH_EASTERN_ASIA = "south-eastern-asia" +REGION_WESTERN_ASIA = "western-asia" +REGION_EUROPE = "europe" +REGION_EASTERN_EUROPE = "eastern-europe" +REGION_NORTHERN_EUROPE = "northern-europe" +REGION_SOUTHERN_EUROPE = "southern-europe" +REGION_WESTERN_EUROPE = "western-europe" +REGION_OCEANIA = "oceania" +REGION_ANTARCTICA = "antarctica" +REGION_AUSTRALIA_NEW_ZEALAND = "australia-new-zealand" +REGION_MELANESIA = "melanesia" +REGION_MICRONESIA = "micronesia" +REGION_POLYNESIA = "polynesia" + + +REGION = [ + REGION_AFRICA, + REGION_EASTERN_AFRICA, + REGION_MIDDLE_AFRICA, + REGION_NORTHERN_AFRICA, + REGION_SOUTHERN_AFRICA, + REGION_WESTERN_AFRICA, + REGION_AMERICAS, + REGION_LATIN_AMERICA_CARIBBEAN, + REGION_SOUTH_AMERICA, + REGION_CARIBBEAN, + REGION_CENTRAL_AMERICA, + REGION_NORTHERN_AMERICA, + REGION_ASIA, + REGION_CENTRAL_ASIA, + REGION_EASTERN_ASIA, + REGION_SOUTHERN_ASIA, + REGION_SOUTH_EASTERN_ASIA, + REGION_WESTERN_ASIA, + REGION_EUROPE, + REGION_EASTERN_EUROPE, + REGION_NORTHERN_EUROPE, + REGION_SOUTHERN_EUROPE, + REGION_WESTERN_EUROPE, + REGION_OCEANIA, + REGION_ANTARCTICA, + REGION_AUSTRALIA_NEW_ZEALAND, + REGION_MELANESIA, + REGION_MICRONESIA, + REGION_POLYNESIA, +] + + +REPORT_TYPE_ATTACK_PATTERN = "attack-pattern" +REPORT_TYPE_CAMPAIGN = "campaign" +REPORT_TYPE_IDENTITY = "identity" +REPORT_TYPE_INDICATOR = "indicator" +REPORT_TYPE_INTRUSION_SET = "intrusion-set" +REPORT_TYPE_MALWARE = "malware" +REPORT_TYPE_OBSERVED_DATA = "observed-data" +REPORT_TYPE_THREAT_ACTOR = "threat-actor" +REPORT_TYPE_THREAT_REPORT = "threat-report" +REPORT_TYPE_TOOL = "tool" +REPORT_TYPE_VULNERABILITY = "vulnerability" + + +REPORT_TYPE = [ + REPORT_TYPE_ATTACK_PATTERN, + REPORT_TYPE_CAMPAIGN, + REPORT_TYPE_IDENTITY, + REPORT_TYPE_INDICATOR, + REPORT_TYPE_INTRUSION_SET, + REPORT_TYPE_MALWARE, + REPORT_TYPE_OBSERVED_DATA, + REPORT_TYPE_THREAT_ACTOR, + REPORT_TYPE_THREAT_REPORT, + REPORT_TYPE_TOOL, + REPORT_TYPE_VULNERABILITY, +] + + +THREAT_ACTOR_TYPE_ACTIVIST = "activist" +THREAT_ACTOR_TYPE_COMPETITOR = "competitor" +THREAT_ACTOR_TYPE_CRIME_SYNDICATE = "crime-syndicate" +THREAT_ACTOR_TYPE_CRIMINAL = "criminal" +THREAT_ACTOR_TYPE_HACKER = "hacker" +THREAT_ACTOR_TYPE_INSIDER_ACCIDENTAL = "insider-accidental" +THREAT_ACTOR_TYPE_INSIDER_DISGRUNTLED = "insider-disgruntled" +THREAT_ACTOR_TYPE_NATION_STATE = "nation-state" +THREAT_ACTOR_TYPE_SENSATIONALIST = "sensationalist" +THREAT_ACTOR_TYPE_SPY = "spy" +THREAT_ACTOR_TYPE_TERRORIST = "terrorist" +THREAT_ACTOR_TYPE_UNKNOWN = "unknown" + + +THREAT_ACTOR_TYPE = [ + THREAT_ACTOR_TYPE_ACTIVIST, + THREAT_ACTOR_TYPE_COMPETITOR, + THREAT_ACTOR_TYPE_CRIME_SYNDICATE, + THREAT_ACTOR_TYPE_CRIMINAL, + THREAT_ACTOR_TYPE_HACKER, + THREAT_ACTOR_TYPE_INSIDER_ACCIDENTAL, + THREAT_ACTOR_TYPE_INSIDER_DISGRUNTLED, + THREAT_ACTOR_TYPE_NATION_STATE, + THREAT_ACTOR_TYPE_SENSATIONALIST, + THREAT_ACTOR_TYPE_SPY, + THREAT_ACTOR_TYPE_TERRORIST, + THREAT_ACTOR_TYPE_UNKNOWN, +] + + +THREAT_ACTOR_ROLE_AGENT = "agent" +THREAT_ACTOR_ROLE_DIRECTOR = "director" +THREAT_ACTOR_ROLE_INDEPENDENT = "independent" +THREAT_ACTOR_ROLE_INFRASTRUCTURE_ARCHITECT = "infrastructure-architect" +THREAT_ACTOR_ROLE_INFRASTRUCTURE_OPERATOR = "infrastructure-operator" +THREAT_ACTOR_ROLE_MALWARE_AUTHOR = "malware-author" +THREAT_ACTOR_ROLE_SPONSOR = "sponsor" + + +THREAT_ACTOR_ROLE = [ + THREAT_ACTOR_ROLE_AGENT, + THREAT_ACTOR_ROLE_DIRECTOR, + THREAT_ACTOR_ROLE_INDEPENDENT, + THREAT_ACTOR_ROLE_INFRASTRUCTURE_ARCHITECT, + THREAT_ACTOR_ROLE_INFRASTRUCTURE_OPERATOR, + THREAT_ACTOR_ROLE_MALWARE_AUTHOR, + THREAT_ACTOR_ROLE_SPONSOR, +] + + +THREAT_ACTOR_SOPHISTICATION_NONE = "none" +THREAT_ACTOR_SOPHISTICATION_MINIMAL = "minimal" +THREAT_ACTOR_SOPHISTICATION_INTERMEDIATE = "intermediate" +THREAT_ACTOR_SOPHISTICATION_ADVANCED = "advanced" +THREAT_ACTOR_SOPHISTICATION_EXPERT = "expert" +THREAT_ACTOR_SOPHISTICATION_INNOVATOR = "innovator" +THREAT_ACTOR_SOPHISTICATION_STRATEGIC = "strategic" + + +THREAT_ACTOR_SOPHISTICATION = [ + THREAT_ACTOR_SOPHISTICATION_NONE, + THREAT_ACTOR_SOPHISTICATION_MINIMAL, + THREAT_ACTOR_SOPHISTICATION_INTERMEDIATE, + THREAT_ACTOR_SOPHISTICATION_ADVANCED, + THREAT_ACTOR_SOPHISTICATION_EXPERT, + THREAT_ACTOR_SOPHISTICATION_INNOVATOR, + THREAT_ACTOR_SOPHISTICATION_STRATEGIC, +] + + +TOOL_TYPE_DENIAL_OF_SERVICE = "denial-of-service" +TOOL_TYPE_EXPLOITATION = "exploitation" +TOOL_TYPE_INFORMATION_GATHERING = "information-gathering" +TOOL_TYPE_NETWORK_CAPTURE = "network-capture" +TOOL_TYPE_CREDENTIAL_EXPLOITATION = "credential-exploitation" +TOOL_TYPE_REMOTE_ACCESS = "remote-access" +TOOL_TYPE_VULNERABILITY_SCANNING = "vulnerability-scanning" +TOOL_TYPE_UNKNOWN = "unknown" + + +TOOL_TYPE = [ + TOOL_TYPE_DENIAL_OF_SERVICE, + TOOL_TYPE_EXPLOITATION, + TOOL_TYPE_INFORMATION_GATHERING, + TOOL_TYPE_NETWORK_CAPTURE, + TOOL_TYPE_CREDENTIAL_EXPLOITATION, + TOOL_TYPE_REMOTE_ACCESS, + TOOL_TYPE_VULNERABILITY_SCANNING, + TOOL_TYPE_UNKNOWN, +] + + +WINDOWS_INTEGRITY_LEVEL_LOW = "low" +WINDOWS_INTEGRITY_LEVEL_MEDIUM = "medium" +WINDOWS_INTEGRITY_LEVEL_HIGH = "high" +WINDOWS_INTEGRITY_LEVEL_SYSTEM = "system" + + +WINDOWS_INTEGRITY_LEVEL = [ + WINDOWS_INTEGRITY_LEVEL_LOW, + WINDOWS_INTEGRITY_LEVEL_MEDIUM, + WINDOWS_INTEGRITY_LEVEL_HIGH, + WINDOWS_INTEGRITY_LEVEL_SYSTEM, +] + + +WINDOWS_PEBINARY_TYPE_DLL = "dll" +WINDOWS_PEBINARY_TYPE_EXE = "exe" +WINDOWS_PEBINARY_TYPE_SYS = "sys" + + +WINDOWS_PEBINARY_TYPE = [ + WINDOWS_PEBINARY_TYPE_DLL, + WINDOWS_PEBINARY_TYPE_EXE, + WINDOWS_PEBINARY_TYPE_SYS, +] + + +WINDOWS_REGISTRY_DATATYPE_REG_NONE = "REG_NONE" +WINDOWS_REGISTRY_DATATYPE_REG_SZ = "REG_SZ" +WINDOWS_REGISTRY_DATATYPE_REG_EXPAND_SZ = "REG_EXPAND_SZ" +WINDOWS_REGISTRY_DATATYPE_REG_BINARY = "REG_BINARY" +WINDOWS_REGISTRY_DATATYPE_REG_DWORD = "REG_DWORD" +WINDOWS_REGISTRY_DATATYPE_REG_DWORD_BIG_ENDIAN = "REG_DWORD_BIG_ENDIAN" +WINDOWS_REGISTRY_DATATYPE_REG_DWORD_LITTLE_ENDIAN = "REG_DWORD_LITTLE_ENDIAN" +WINDOWS_REGISTRY_DATATYPE_REG_LINK = "REG_LINK" +WINDOWS_REGISTRY_DATATYPE_REG_MULTI_SZ = "REG_MULTI_SZ" +WINDOWS_REGISTRY_DATATYPE_REG_RESOURCE_LIST = "REG_RESOURCE_LIST" +WINDOWS_REGISTRY_DATATYPE_REG_FULL_RESOURCE_DESCRIPTION = "REG_FULL_RESOURCE_DESCRIPTION" +WINDOWS_REGISTRY_DATATYPE_REG_RESOURCE_REQUIREMENTS_LIST = "REG_RESOURCE_REQUIREMENTS_LIST" +WINDOWS_REGISTRY_DATATYPE_REG_QWORD = "REG_QWORD" +WINDOWS_REGISTRY_DATATYPE_REG_INVALID_TYPE = "REG_INVALID_TYPE" + + +WINDOWS_REGISTRY_DATATYPE = [ + WINDOWS_REGISTRY_DATATYPE_REG_NONE, + WINDOWS_REGISTRY_DATATYPE_REG_SZ, + WINDOWS_REGISTRY_DATATYPE_REG_EXPAND_SZ, + WINDOWS_REGISTRY_DATATYPE_REG_BINARY, + WINDOWS_REGISTRY_DATATYPE_REG_DWORD, + WINDOWS_REGISTRY_DATATYPE_REG_DWORD_BIG_ENDIAN, + WINDOWS_REGISTRY_DATATYPE_REG_DWORD_LITTLE_ENDIAN, + WINDOWS_REGISTRY_DATATYPE_REG_LINK, + WINDOWS_REGISTRY_DATATYPE_REG_MULTI_SZ, + WINDOWS_REGISTRY_DATATYPE_REG_RESOURCE_LIST, + WINDOWS_REGISTRY_DATATYPE_REG_FULL_RESOURCE_DESCRIPTION, + WINDOWS_REGISTRY_DATATYPE_REG_RESOURCE_REQUIREMENTS_LIST, + WINDOWS_REGISTRY_DATATYPE_REG_QWORD, + WINDOWS_REGISTRY_DATATYPE_REG_INVALID_TYPE, +] + + +WINDOWS_SERVICE_START_TYPE_SERVICE_AUTO_START = "SERVICE_AUTO_START" +WINDOWS_SERVICE_START_TYPE_SERVICE_BOOT_START = "SERVICE_BOOT_START" +WINDOWS_SERVICE_START_TYPE_SERVICE_DEMAND_START = "SERVICE_DEMAND_START" +WINDOWS_SERVICE_START_TYPE_SERVICE_DISABLED = "SERVICE_DISABLED" +WINDOWS_SERVICE_START_TYPE_SERVICE_SYSTEM_ALERT = "SERVICE_SYSTEM_ALERT" + + +WINDOWS_SERVICE_START_TYPE = [ + WINDOWS_SERVICE_START_TYPE_SERVICE_AUTO_START, + WINDOWS_SERVICE_START_TYPE_SERVICE_BOOT_START, + WINDOWS_SERVICE_START_TYPE_SERVICE_DEMAND_START, + WINDOWS_SERVICE_START_TYPE_SERVICE_DISABLED, + WINDOWS_SERVICE_START_TYPE_SERVICE_SYSTEM_ALERT, +] + + +WINDOWS_SERVICE_TYPE_SERVICE_KERNEL_DRIVER = "SERVICE_KERNEL_DRIVER" +WINDOWS_SERVICE_TYPE_SERVICE_FILE_SYSTEM_DRIVER = "SERVICE_FILE_SYSTEM_DRIVER" +WINDOWS_SERVICE_TYPE_SERVICE_WIN32_OWN_PROCESS = "SERVICE_WIN32_OWN_PROCESS" +WINDOWS_SERVICE_TYPE_SERVICE_WIN32_SHARE_PROCESS = "SERVICE_WIN32_SHARE_PROCESS" + + +WINDOWS_SERVICE_TYPE = [ + WINDOWS_SERVICE_TYPE_SERVICE_KERNEL_DRIVER, + WINDOWS_SERVICE_TYPE_SERVICE_FILE_SYSTEM_DRIVER, + WINDOWS_SERVICE_TYPE_SERVICE_WIN32_OWN_PROCESS, + WINDOWS_SERVICE_TYPE_SERVICE_WIN32_SHARE_PROCESS, +] + + +WINDOWS_SERVICE_STATUS_SERVICE_CONTINUE_PENDING = "SERVICE_CONTINUE_PENDING" +WINDOWS_SERVICE_STATUS_SERVICE_PAUSE_PENDING = "SERVICE_PAUSE_PENDING" +WINDOWS_SERVICE_STATUS_SERVICE_PAUSED = "SERVICE_PAUSED" +WINDOWS_SERVICE_STATUS_SERVICE_RUNNING = "SERVICE_RUNNING" +WINDOWS_SERVICE_STATUS_SERVICE_START_PENDING = "SERVICE_START_PENDING" +WINDOWS_SERVICE_STATUS_SERVICE_STOP_PENDING = "SERVICE_STOP_PENDING" +WINDOWS_SERVICE_STATUS_SERVICE_STOPPED = "SERVICE_STOPPED" + + +WINDOWS_SERVICE_STATUS = [ + WINDOWS_SERVICE_STATUS_SERVICE_CONTINUE_PENDING, + WINDOWS_SERVICE_STATUS_SERVICE_PAUSE_PENDING, + WINDOWS_SERVICE_STATUS_SERVICE_PAUSED, + WINDOWS_SERVICE_STATUS_SERVICE_RUNNING, + WINDOWS_SERVICE_STATUS_SERVICE_START_PENDING, + WINDOWS_SERVICE_STATUS_SERVICE_STOP_PENDING, + WINDOWS_SERVICE_STATUS_SERVICE_STOPPED, +] diff --git a/stix2/versioning.py b/stix2/versioning.py index a6dc0bd..02a1e13 100644 --- a/stix2/versioning.py +++ b/stix2/versioning.py @@ -192,8 +192,9 @@ def new_version(data, allow_custom=None, **kwargs): or dict. :param allow_custom: Whether to allow custom properties on the new object. If True, allow them (regardless of whether the original had custom - properties); if False disallow them; if None, propagate the preference - from the original object. + properties); if False disallow them; if None, auto-detect from the + object: if it has custom properties, allow them in the new version, + otherwise don't allow them. :param kwargs: The properties to change. Setting to None requests property removal. :return: The new object. @@ -271,7 +272,7 @@ def new_version(data, allow_custom=None, **kwargs): # it for dicts. if isinstance(data, stix2.base._STIXBase): if allow_custom is None: - new_obj_inner["allow_custom"] = data._allow_custom + new_obj_inner["allow_custom"] = data.has_custom else: new_obj_inner["allow_custom"] = allow_custom