diff --git a/.travis.yml b/.travis.yml index 018aefe..81141d5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -sudo: false +os: linux language: python cache: pip dist: xenial diff --git a/CHANGELOG b/CHANGELOG index dc0d91e..fd22828 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,12 @@ CHANGELOG ========= +1.4.0 - 2020-04-03 + +* #347, #355, #356, #357, #358, #360, #362, #369, #370, #379, #374, #384 Updates STIX 2.1 support to CS01 +* #376 Fixes bug where registering object of same name would overwrite it; will + now raise an error + 1.3.1 - 2020-03-06 * #322 Adds encoding option FileSystemSource and MemorySource diff --git a/setup.cfg b/setup.cfg index 7e89c66..4fefdcc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.3.1 +current_version = 1.4.0 commit = True tag = True diff --git a/setup.py b/setup.py index 66bf302..b028ffe 100644 --- a/setup.py +++ b/setup.py @@ -51,6 +51,7 @@ setup( keywords='stix stix2 json cti cyber threat intelligence', packages=find_packages(exclude=['*.test', '*.test.*']), install_requires=[ + 'enum34 ; python_version<"3.4"', 'python-dateutil', 'pytz', 'requests', diff --git a/stix2/__init__.py b/stix2/__init__.py index c9384a0..f3e02b4 100644 --- a/stix2/__init__.py +++ b/stix2/__init__.py @@ -23,7 +23,6 @@ DEFAULT_VERSION = '2.0' # Default version will always be the latest STIX 2.X version from .confidence import scales -from .core import _collect_stix2_mappings, parse, parse_observable from .datastore import CompositeDataSource from .datastore.filesystem import ( FileSystemSink, FileSystemSource, FileSystemStore, @@ -38,6 +37,7 @@ from .markings import ( add_markings, clear_markings, get_markings, is_marked, remove_markings, set_markings, ) +from .parsing import _collect_stix2_mappings, parse, parse_observable from .patterns import ( AndBooleanExpression, AndObservationExpression, BasicObjectPathComponent, BinaryConstant, BooleanConstant, EqualityComparisonExpression, diff --git a/stix2/base.py b/stix2/base.py index 80bee39..bfb2b88 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -2,11 +2,13 @@ import copy import datetime as dt +import re import uuid import simplejson as json import six +import stix2 from stix2.canonicalization.Canonicalize import canonicalize from .exceptions import ( @@ -14,8 +16,11 @@ from .exceptions import ( ImmutableError, InvalidObjRefError, InvalidValueError, MissingPropertiesError, MutuallyExclusivePropertiesError, ) +from .markings import _MarkingsMixin from .markings.utils import validate -from .utils import NOW, find_property_index, format_datetime, get_timestamp +from .utils import ( + NOW, PREFIX_21_REGEX, find_property_index, format_datetime, get_timestamp, +) from .utils import new_version as _new_version from .utils import revoke as _revoke @@ -158,12 +163,23 @@ class _STIXBase(Mapping): custom_props = kwargs.pop('custom_properties', {}) if custom_props and not isinstance(custom_props, dict): raise ValueError("'custom_properties' must be a dictionary") - if not self._allow_custom: - extra_kwargs = list(set(kwargs) - set(self._properties)) - if extra_kwargs: - raise ExtraPropertiesError(cls, extra_kwargs) - if custom_props: + + extra_kwargs = list(set(kwargs) - set(self._properties)) + if extra_kwargs and not self._allow_custom: + raise ExtraPropertiesError(cls, extra_kwargs) + + # because allow_custom is true, any extra kwargs are custom + 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())) + for prop_name in all_custom_prop_names: + if not re.match(PREFIX_21_REGEX, prop_name): + raise InvalidValueError( + self.__class__, prop_name, + reason="Property name '%s' must begin with an alpha character." % prop_name, + ) # Remove any keyword arguments whose value is None or [] (i.e. empty list) setting_kwargs = {} @@ -307,6 +323,14 @@ class _STIXBase(Mapping): return json.dumps(self, cls=STIXJSONEncoder, **kwargs) +class _DomainObject(_STIXBase, _MarkingsMixin): + pass + + +class _RelationshipObject(_STIXBase, _MarkingsMixin): + pass + + class _Observable(_STIXBase): def __init__(self, **kwargs): diff --git a/stix2/custom.py b/stix2/custom.py index f3c89cf..f749b04 100644 --- a/stix2/custom.py +++ b/stix2/custom.py @@ -1,140 +1,91 @@ from collections import OrderedDict -import re import six -from .base import _cls_init, _Extension, _Observable, _STIXBase -from .core import ( - STIXDomainObject, _register_marking, _register_object, - _register_observable, _register_observable_extension, +from .base import _cls_init +from .parsing import ( + _register_marking, _register_object, _register_observable, + _register_observable_extension, ) -from .utils import TYPE_REGEX, get_class_hierarchy_names -def _custom_object_builder(cls, type, properties, version): - class _CustomObject(cls, STIXDomainObject): - - if not re.match(TYPE_REGEX, type): - raise ValueError( - "Invalid type name '%s': must only contain the " - "characters a-z (lowercase ASCII), 0-9, and hyphen (-)." % type, - ) - elif len(type) < 3 or len(type) > 250: - raise ValueError( - "Invalid type name '%s': must be between 3 and 250 characters." % type, - ) - - if not properties or not isinstance(properties, list): - raise ValueError("Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]") - - _type = type - _properties = OrderedDict(properties) - - def __init__(self, **kwargs): - _STIXBase.__init__(self, **kwargs) - _cls_init(cls, self, kwargs) - - _register_object(_CustomObject, version=version) - return _CustomObject - - -def _custom_marking_builder(cls, type, properties, version): - class _CustomMarking(cls, _STIXBase): - - if not properties or not isinstance(properties, list): - raise ValueError("Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]") - - _type = type - _properties = OrderedDict(properties) - - def __init__(self, **kwargs): - _STIXBase.__init__(self, **kwargs) - _cls_init(cls, self, kwargs) - - _register_marking(_CustomMarking, version=version) - return _CustomMarking - - -def _custom_observable_builder(cls, type, properties, version, id_contrib_props=None): - if id_contrib_props is None: - id_contrib_props = [] - - class _CustomObservable(cls, _Observable): - - if not re.match(TYPE_REGEX, type): - raise ValueError( - "Invalid observable type name '%s': must only contain the " - "characters a-z (lowercase ASCII), 0-9, and hyphen (-)." % type, - ) - elif len(type) < 3 or len(type) > 250: - raise ValueError("Invalid observable type name '%s': must be between 3 and 250 characters." % type) - - if not properties or not isinstance(properties, list): - raise ValueError("Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]") - - if version == "2.0": - # If using STIX2.0, check properties ending in "_ref/s" are ObjectReferenceProperties - for prop_name, prop in properties: - if prop_name.endswith('_ref') and ('ObjectReferenceProperty' not in get_class_hierarchy_names(prop)): - raise ValueError( - "'%s' is named like an object reference property but " - "is not an ObjectReferenceProperty." % prop_name, - ) - elif (prop_name.endswith('_refs') and ('ListProperty' not in get_class_hierarchy_names(prop) or - 'ObjectReferenceProperty' not in get_class_hierarchy_names(prop.contained))): - raise ValueError( - "'%s' is named like an object reference list property but " - "is not a ListProperty containing ObjectReferenceProperty." % prop_name, - ) - else: - # If using STIX2.1 (or newer...), check properties ending in "_ref/s" are ReferenceProperties - for prop_name, prop in properties: - if prop_name.endswith('_ref') and ('ReferenceProperty' not in get_class_hierarchy_names(prop)): - raise ValueError( - "'%s' is named like a reference property but " - "is not a ReferenceProperty." % prop_name, - ) - elif (prop_name.endswith('_refs') and ('ListProperty' not in get_class_hierarchy_names(prop) or - 'ReferenceProperty' not in get_class_hierarchy_names(prop.contained))): - raise ValueError( - "'%s' is named like a reference list property but " - "is not a ListProperty containing ReferenceProperty." % prop_name, - ) - - _type = type - _properties = OrderedDict(properties) - if version != '2.0': - _id_contributing_properties = id_contrib_props - - def __init__(self, **kwargs): - _Observable.__init__(self, **kwargs) - _cls_init(cls, self, kwargs) - - _register_observable(_CustomObservable, version=version) - return _CustomObservable - - -def _custom_extension_builder(cls, observable, type, properties, version): - +def _get_properties_dict(properties): try: - prop_dict = OrderedDict(properties) + return OrderedDict(properties) except TypeError as e: six.raise_from( ValueError( - "Extension properties must be dict-like, e.g. a list " + "properties must be dict-like, e.g. a list " "containing tuples. For example, " "[('property1', IntegerProperty())]", ), e, ) - class _CustomExtension(cls, _Extension): + +def _custom_object_builder(cls, type, properties, version, base_class): + prop_dict = _get_properties_dict(properties) + + class _CustomObject(cls, base_class): _type = type _properties = prop_dict def __init__(self, **kwargs): - _Extension.__init__(self, **kwargs) + base_class.__init__(self, **kwargs) + _cls_init(cls, self, kwargs) + + _register_object(_CustomObject, version=version) + return _CustomObject + + +def _custom_marking_builder(cls, type, properties, version, base_class): + prop_dict = _get_properties_dict(properties) + + class _CustomMarking(cls, base_class): + + _type = type + _properties = prop_dict + + def __init__(self, **kwargs): + base_class.__init__(self, **kwargs) + _cls_init(cls, self, kwargs) + + _register_marking(_CustomMarking, version=version) + return _CustomMarking + + +def _custom_observable_builder(cls, type, properties, version, base_class, id_contrib_props=None): + if id_contrib_props is None: + id_contrib_props = [] + + prop_dict = _get_properties_dict(properties) + + class _CustomObservable(cls, base_class): + + _type = type + _properties = prop_dict + if version != '2.0': + _id_contributing_properties = id_contrib_props + + def __init__(self, **kwargs): + base_class.__init__(self, **kwargs) + _cls_init(cls, self, kwargs) + + _register_observable(_CustomObservable, version=version) + return _CustomObservable + + +def _custom_extension_builder(cls, observable, type, properties, version, base_class): + prop_dict = _get_properties_dict(properties) + + class _CustomExtension(cls, base_class): + + _type = type + _properties = prop_dict + + def __init__(self, **kwargs): + base_class.__init__(self, **kwargs) _cls_init(cls, self, kwargs) _register_observable_extension(observable, _CustomExtension, version=version) diff --git a/stix2/datastore/filesystem.py b/stix2/datastore/filesystem.py index d5acc24..5a5844a 100644 --- a/stix2/datastore/filesystem.py +++ b/stix2/datastore/filesystem.py @@ -10,11 +10,11 @@ import six from stix2 import v20, v21 from stix2.base import _STIXBase -from stix2.core import parse from stix2.datastore import ( DataSink, DataSource, DataSourceError, DataStoreMixin, ) from stix2.datastore.filters import Filter, FilterSet, apply_common_filters +from stix2.parsing import parse from stix2.utils import format_datetime, get_type_from_id diff --git a/stix2/datastore/memory.py b/stix2/datastore/memory.py index 52da168..f71b763 100644 --- a/stix2/datastore/memory.py +++ b/stix2/datastore/memory.py @@ -7,9 +7,9 @@ import os from stix2 import v20, v21 from stix2.base import _STIXBase -from stix2.core import parse from stix2.datastore import DataSink, DataSource, DataStoreMixin from stix2.datastore.filters import FilterSet, apply_common_filters +from stix2.parsing import parse def _add(store, stix_data, allow_custom=True, version=None): diff --git a/stix2/datastore/taxii.py b/stix2/datastore/taxii.py index 41c968f..b5c0827 100644 --- a/stix2/datastore/taxii.py +++ b/stix2/datastore/taxii.py @@ -4,15 +4,15 @@ from requests.exceptions import HTTPError from stix2 import v20, v21 from stix2.base import _STIXBase -from stix2.core import parse from stix2.datastore import ( DataSink, DataSource, DataSourceError, DataStoreMixin, ) from stix2.datastore.filters import Filter, FilterSet, apply_common_filters +from stix2.parsing import parse from stix2.utils import deduplicate try: - from taxii2client import ValidationError + from taxii2client.exceptions import ValidationError _taxii2_client = True except ImportError: _taxii2_client = False diff --git a/stix2/environment.py b/stix2/environment.py index ada5f33..6e3c76a 100644 --- a/stix2/environment.py +++ b/stix2/environment.py @@ -4,8 +4,8 @@ import copy import logging import time -from .core import parse as _parse from .datastore import CompositeDataSource, DataStoreMixin +from .parsing import parse as _parse from .utils import STIXdatetime, parse_into_datetime logger = logging.getLogger(__name__) diff --git a/stix2/exceptions.py b/stix2/exceptions.py index d2ec3fc..edcc352 100644 --- a/stix2/exceptions.py +++ b/stix2/exceptions.py @@ -233,3 +233,16 @@ class STIXDeprecationWarning(DeprecationWarning): Represents usage of a deprecated component of a STIX specification. """ pass + + +class DuplicateRegistrationError(STIXError): + """A STIX object with the same type as an existing object is being registered""" + + def __init__(self, obj_type, reg_obj_type): + super(DuplicateRegistrationError, self).__init__() + self.obj_type = obj_type + self.reg_obj_type = reg_obj_type + + def __str__(self): + msg = "A(n) {0} with type '{1}' already exists and cannot be registered again" + return msg.format(self.obj_type, self.reg_obj_type) diff --git a/stix2/core.py b/stix2/parsing.py similarity index 72% rename from stix2/core.py rename to stix2/parsing.py index 4be3b1c..2e05d54 100644 --- a/stix2/core.py +++ b/stix2/parsing.py @@ -7,39 +7,13 @@ import re import stix2 -from .base import _Observable, _STIXBase -from .exceptions import ParseError -from .markings import _MarkingsMixin -from .utils import SCO21_EXT_REGEX, TYPE_REGEX, _get_dict +from .base import _DomainObject, _Observable +from .exceptions import DuplicateRegistrationError, ParseError +from .utils import PREFIX_21_REGEX, _get_dict, get_class_hierarchy_names STIX2_OBJ_MAPS = {} -class STIXDomainObject(_STIXBase, _MarkingsMixin): - def __init__(self, *args, **kwargs): - interoperability = kwargs.get('interoperability', False) - self.__interoperability = interoperability - self._properties['id'].interoperability = interoperability - self._properties['created_by_ref'].interoperability = interoperability - if kwargs.get('object_marking_refs'): - self._properties['object_marking_refs'].contained.interoperability = interoperability - - super(STIXDomainObject, self).__init__(*args, **kwargs) - - -class STIXRelationshipObject(_STIXBase, _MarkingsMixin): - def __init__(self, *args, **kwargs): - interoperability = kwargs.get('interoperability', False) - self.__interoperability = interoperability - self._properties['id'].interoperability = interoperability - if kwargs.get('created_by_ref'): - self._properties['created_by_ref'].interoperability = interoperability - if kwargs.get('object_marking_refs'): - self._properties['object_marking_refs'].contained.interoperability = interoperability - - super(STIXRelationshipObject, self).__init__(*args, **kwargs) - - def parse(data, allow_custom=False, interoperability=False, version=None): """Convert a string, dict or file-like object into a STIX object. @@ -218,7 +192,7 @@ def parse_observable(data, _valid_refs=None, allow_custom=False, version=None): return obj_class(allow_custom=allow_custom, **obj) -def _register_object(new_type, version=None): +def _register_object(new_type, version=stix2.DEFAULT_VERSION): """Register a custom STIX Object type. Args: @@ -226,7 +200,26 @@ def _register_object(new_type, version=None): version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If None, use latest version. + Raises: + ValueError: If the class being registered wasn't created with the + @CustomObject decorator. + DuplicateRegistrationError: If the class has already been registered. + """ + + if not issubclass(new_type, _DomainObject): + raise ValueError( + "'%s' must be created with the @CustomObject decorator." % + new_type.__name__, + ) + + properties = new_type._properties + + if version == "2.1": + for prop_name, prop in properties.items(): + if not re.match(PREFIX_21_REGEX, prop_name): + raise ValueError("Property name '%s' must begin with an alpha character" % prop_name) + if version: v = 'v' + version.replace('.', '') else: @@ -234,10 +227,12 @@ def _register_object(new_type, version=None): v = 'v' + stix2.DEFAULT_VERSION.replace('.', '') OBJ_MAP = STIX2_OBJ_MAPS[v]['objects'] + if new_type._type in OBJ_MAP.keys(): + raise DuplicateRegistrationError("STIX Object", new_type._type) OBJ_MAP[new_type._type] = new_type -def _register_marking(new_marking, version=None): +def _register_marking(new_marking, version=stix2.DEFAULT_VERSION): """Register a custom STIX Marking Definition type. Args: @@ -246,6 +241,17 @@ def _register_marking(new_marking, version=None): None, use latest version. """ + + mark_type = new_marking._type + properties = new_marking._properties + + stix2.properties._validate_type(mark_type, version) + + if version == "2.1": + for prop_name, prop_value in properties.items(): + if not re.match(PREFIX_21_REGEX, prop_name): + raise ValueError("Property name '%s' must begin with an alpha character." % prop_name) + if version: v = 'v' + version.replace('.', '') else: @@ -253,10 +259,12 @@ def _register_marking(new_marking, version=None): v = 'v' + stix2.DEFAULT_VERSION.replace('.', '') OBJ_MAP_MARKING = STIX2_OBJ_MAPS[v]['markings'] - OBJ_MAP_MARKING[new_marking._type] = new_marking + if mark_type in OBJ_MAP_MARKING.keys(): + raise DuplicateRegistrationError("STIX Marking", mark_type) + OBJ_MAP_MARKING[mark_type] = new_marking -def _register_observable(new_observable, version=None): +def _register_observable(new_observable, version=stix2.DEFAULT_VERSION): """Register a custom STIX Cyber Observable type. Args: @@ -265,6 +273,39 @@ def _register_observable(new_observable, version=None): None, use latest version. """ + properties = new_observable._properties + + if version == "2.0": + # If using STIX2.0, check properties ending in "_ref/s" are ObjectReferenceProperties + for prop_name, prop in properties.items(): + if prop_name.endswith('_ref') and ('ObjectReferenceProperty' not in get_class_hierarchy_names(prop)): + raise ValueError( + "'%s' is named like an object reference property but " + "is not an ObjectReferenceProperty." % prop_name, + ) + elif (prop_name.endswith('_refs') and ('ListProperty' not in get_class_hierarchy_names(prop) or + 'ObjectReferenceProperty' not in get_class_hierarchy_names(prop.contained))): + raise ValueError( + "'%s' is named like an object reference list property but " + "is not a ListProperty containing ObjectReferenceProperty." % prop_name, + ) + else: + # If using STIX2.1 (or newer...), check properties ending in "_ref/s" are ReferenceProperties + for prop_name, prop in properties.items(): + if not re.match(PREFIX_21_REGEX, prop_name): + raise ValueError("Property name '%s' must begin with an alpha character." % prop_name) + elif prop_name.endswith('_ref') and ('ReferenceProperty' not in get_class_hierarchy_names(prop)): + raise ValueError( + "'%s' is named like a reference property but " + "is not a ReferenceProperty." % prop_name, + ) + elif (prop_name.endswith('_refs') and ('ListProperty' not in get_class_hierarchy_names(prop) or + 'ReferenceProperty' not in get_class_hierarchy_names(prop.contained))): + raise ValueError( + "'%s' is named like a reference list property but " + "is not a ListProperty containing ReferenceProperty." % prop_name, + ) + if version: v = 'v' + version.replace('.', '') else: @@ -272,6 +313,8 @@ def _register_observable(new_observable, version=None): v = 'v' + stix2.DEFAULT_VERSION.replace('.', '') OBJ_MAP_OBSERVABLE = STIX2_OBJ_MAPS[v]['observables'] + if new_observable._type in OBJ_MAP_OBSERVABLE.keys(): + raise DuplicateRegistrationError("Cyber Observable", new_observable._type) OBJ_MAP_OBSERVABLE[new_observable._type] = new_observable @@ -291,30 +334,12 @@ def _register_observable_extension( obs_class = observable if isinstance(observable, type) else \ type(observable) ext_type = new_extension._type + properties = new_extension._properties if not issubclass(obs_class, _Observable): raise ValueError("'observable' must be a valid Observable class!") - if version == "2.0": - if not re.match(TYPE_REGEX, ext_type): - raise ValueError( - "Invalid extension type name '%s': must only contain the " - "characters a-z (lowercase ASCII), 0-9, and hyphen (-)." % - ext_type, - ) - else: # 2.1+ - if not re.match(SCO21_EXT_REGEX, ext_type): - raise ValueError( - "Invalid extension type name '%s': must only contain the " - "characters a-z (lowercase ASCII), 0-9, hyphen (-), and end " - "with '-ext'." % ext_type, - ) - - if len(ext_type) < 3 or len(ext_type) > 250: - raise ValueError( - "Invalid extension type name '%s': must be between 3 and 250" - " characters." % ext_type, - ) + stix2.properties._validate_type(ext_type, version) if not new_extension._properties: raise ValueError( @@ -322,6 +347,17 @@ def _register_observable_extension( ext_type, ) + if version == "2.1": + if not ext_type.endswith('-ext'): + raise ValueError( + "Invalid extension type name '%s': must end with '-ext'." % + ext_type, + ) + + for prop_name, prop_value in properties.items(): + if not re.match(PREFIX_21_REGEX, prop_name): + raise ValueError("Property name '%s' must begin with an alpha character." % prop_name) + v = 'v' + version.replace('.', '') try: @@ -336,6 +372,8 @@ def _register_observable_extension( EXT_MAP = STIX2_OBJ_MAPS[v]['observable-extensions'] try: + if ext_type in EXT_MAP[observable_type].keys(): + raise DuplicateRegistrationError("Observable Extension", ext_type) EXT_MAP[observable_type][ext_type] = new_extension except KeyError: if observable_type not in OBJ_MAP_OBSERVABLE: diff --git a/stix2/pattern_visitor.py b/stix2/pattern_visitor.py index 6ac3e98..c0a0fdb 100644 --- a/stix2/pattern_visitor.py +++ b/stix2/pattern_visitor.py @@ -2,11 +2,19 @@ import importlib import inspect from stix2patterns.exceptions import ParseException -from stix2patterns.grammars.STIXPatternParser import ( - STIXPatternParser, TerminalNode, -) -from stix2patterns.grammars.STIXPatternVisitor import STIXPatternVisitor -from stix2patterns.v20.pattern import Pattern +from stix2patterns.grammars.STIXPatternParser import TerminalNode +from stix2patterns.v20.grammars.STIXPatternParser import \ + STIXPatternParser as STIXPatternParser20 +from stix2patterns.v20.grammars.STIXPatternVisitor import \ + STIXPatternVisitor as STIXPatternVisitor20 +from stix2patterns.v20.pattern import Pattern as Pattern20 +from stix2patterns.v21.grammars.STIXPatternParser import \ + STIXPatternParser as STIXPatternParser21 +from stix2patterns.v21.grammars.STIXPatternVisitor import \ + STIXPatternVisitor as STIXPatternVisitor21 +from stix2patterns.v21.pattern import Pattern as Pattern21 + +import stix2 from .patterns import * from .patterns import _BooleanExpression @@ -32,23 +40,12 @@ def remove_terminal_nodes(parse_tree_nodes): return values -# This class defines a complete generic visitor for a parse tree produced by STIXPatternParser. -class STIXPatternVisitorForSTIX2(STIXPatternVisitor): + +class STIXPatternVisitorForSTIX2(): classes = {} - def __init__(self, module_suffix, module_name): - if module_suffix and module_name: - self.module_suffix = module_suffix - if not STIXPatternVisitorForSTIX2.classes: - module = importlib.import_module(module_name) - for k, c in inspect.getmembers(module, inspect.isclass): - STIXPatternVisitorForSTIX2.classes[k] = c - else: - self.module_suffix = None - super(STIXPatternVisitor, self).__init__() - def get_class(self, class_name): if class_name in STIXPatternVisitorForSTIX2.classes: return STIXPatternVisitorForSTIX2.classes[class_name] @@ -106,7 +103,10 @@ class STIXPatternVisitorForSTIX2(STIXPatternVisitor): # Visit a parse tree produced by STIXPatternParser#observationExpressionCompound. def visitObservationExpressionCompound(self, ctx): children = self.visitChildren(ctx) - return self.instantiate("ObservationExpression", children[1]) + if isinstance(children[0], TerminalNode) and children[0].symbol.type == self.parser_class.LPAREN: + return self.instantiate("ParentheticalExpression", children[1]) + else: + return self.instantiate("ObservationExpression", children[0]) # Visit a parse tree produced by STIXPatternParser#observationExpressionWithin. def visitObservationExpressionWithin(self, ctx): @@ -147,7 +147,7 @@ class STIXPatternVisitorForSTIX2(STIXPatternVisitor): def visitPropTestEqual(self, ctx): children = self.visitChildren(ctx) operator = children[1].symbol.type - negated = operator != STIXPatternParser.EQ + negated = operator != self.parser_class.EQ return self.instantiate( "EqualityComparisonExpression", children[0], children[3 if len(children) > 3 else 2], negated, @@ -157,22 +157,22 @@ class STIXPatternVisitorForSTIX2(STIXPatternVisitor): def visitPropTestOrder(self, ctx): children = self.visitChildren(ctx) operator = children[1].symbol.type - if operator == STIXPatternParser.GT: + if operator == self.parser_class.GT: return self.instantiate( "GreaterThanComparisonExpression", children[0], children[3 if len(children) > 3 else 2], False, ) - elif operator == STIXPatternParser.LT: + elif operator == self.parser_class.LT: return self.instantiate( "LessThanComparisonExpression", children[0], children[3 if len(children) > 3 else 2], False, ) - elif operator == STIXPatternParser.GE: + elif operator == self.parser_class.GE: return self.instantiate( "GreaterThanEqualComparisonExpression", children[0], children[3 if len(children) > 3 else 2], False, ) - elif operator == STIXPatternParser.LE: + elif operator == self.parser_class.LE: return self.instantiate( "LessThanEqualComparisonExpression", children[0], children[3 if len(children) > 3 else 2], False, @@ -294,22 +294,22 @@ class STIXPatternVisitorForSTIX2(STIXPatternVisitor): return children[0] def visitTerminal(self, node): - if node.symbol.type == STIXPatternParser.IntPosLiteral or node.symbol.type == STIXPatternParser.IntNegLiteral: + if node.symbol.type == self.parser_class.IntPosLiteral or node.symbol.type == self.parser_class.IntNegLiteral: return IntegerConstant(node.getText()) - elif node.symbol.type == STIXPatternParser.FloatPosLiteral or node.symbol.type == STIXPatternParser.FloatNegLiteral: + elif node.symbol.type == self.parser_class.FloatPosLiteral or node.symbol.type == self.parser_class.FloatNegLiteral: return FloatConstant(node.getText()) - elif node.symbol.type == STIXPatternParser.HexLiteral: + elif node.symbol.type == self.parser_class.HexLiteral: return HexConstant(node.getText(), from_parse_tree=True) - elif node.symbol.type == STIXPatternParser.BinaryLiteral: + elif node.symbol.type == self.parser_class.BinaryLiteral: return BinaryConstant(node.getText(), from_parse_tree=True) - elif node.symbol.type == STIXPatternParser.StringLiteral: + elif node.symbol.type == self.parser_class.StringLiteral: if node.getText()[0] == "'" and node.getText()[-1] == "'": return StringConstant(node.getText()[1:-1], from_parse_tree=True) else: raise ParseException("The pattern does not start and end with a single quote") - elif node.symbol.type == STIXPatternParser.BoolLiteral: + elif node.symbol.type == self.parser_class.BoolLiteral: return BooleanConstant(node.getText()) - elif node.symbol.type == STIXPatternParser.TimestampLiteral: + elif node.symbol.type == self.parser_class.TimestampLiteral: return TimestampConstant(node.getText()) else: return node @@ -321,12 +321,51 @@ class STIXPatternVisitorForSTIX2(STIXPatternVisitor): aggregate = [nextResult] return aggregate +# This class defines a complete generic visitor for a parse tree produced by STIXPatternParser. +class STIXPatternVisitorForSTIX21(STIXPatternVisitorForSTIX2, STIXPatternVisitor21): + classes = {} -def create_pattern_object(pattern, module_suffix="", module_name=""): + def __init__(self, module_suffix, module_name): + if module_suffix and module_name: + self.module_suffix = module_suffix + if not STIXPatternVisitorForSTIX2.classes: + module = importlib.import_module(module_name) + for k, c in inspect.getmembers(module, inspect.isclass): + STIXPatternVisitorForSTIX2.classes[k] = c + else: + self.module_suffix = None + self.parser_class = STIXPatternParser21 + super(STIXPatternVisitor21, self).__init__() + + +class STIXPatternVisitorForSTIX20(STIXPatternVisitorForSTIX2, STIXPatternVisitor20): + classes = {} + + def __init__(self, module_suffix, module_name): + if module_suffix and module_name: + self.module_suffix = module_suffix + if not STIXPatternVisitorForSTIX2.classes: + module = importlib.import_module(module_name) + for k, c in inspect.getmembers(module, inspect.isclass): + STIXPatternVisitorForSTIX2.classes[k] = c + else: + self.module_suffix = None + self.parser_class = STIXPatternParser20 + super(STIXPatternVisitor20, self).__init__() + + +def create_pattern_object(pattern, module_suffix="", module_name="", version=stix2.DEFAULT_VERSION): """ Create a STIX pattern AST from a pattern string. """ - pattern_obj = Pattern(pattern) - builder = STIXPatternVisitorForSTIX2(module_suffix, module_name) + if version == "2.1": + pattern_class = Pattern21 + visitor_class = STIXPatternVisitorForSTIX21 + else: + pattern_class = Pattern20 + visitor_class = STIXPatternVisitorForSTIX20 + + pattern_obj = pattern_class(pattern) + builder = visitor_class(module_suffix, module_name) return pattern_obj.visit(builder) diff --git a/stix2/patterns.py b/stix2/patterns.py index 2e149be..f0cceb8 100644 --- a/stix2/patterns.py +++ b/stix2/patterns.py @@ -551,7 +551,7 @@ class ObservationExpression(_PatternExpression): self.operand = operand def __str__(self): - return "[%s]" % self.operand + return "%s" % self.operand if isinstance(self.operand, (ObservationExpression, _CompoundObservationExpression)) else "[%s]" % self.operand class _CompoundObservationExpression(_PatternExpression): diff --git a/stix2/properties.py b/stix2/properties.py index 3daa35b..37796c2 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -12,12 +12,15 @@ from six import string_types, text_type import stix2 from .base import _STIXBase -from .core import STIX2_OBJ_MAPS, parse, parse_observable from .exceptions import ( CustomContentError, DictionaryKeyError, MissingPropertiesError, MutuallyExclusivePropertiesError, ) -from .utils import _get_dict, get_class_hierarchy_names, parse_into_datetime +from .parsing import STIX2_OBJ_MAPS, parse, parse_observable +from .utils import ( + TYPE_21_REGEX, TYPE_REGEX, _get_dict, get_class_hierarchy_names, + parse_into_datetime, +) ID_REGEX_interoperability = re.compile(r"[0-9a-fA-F]{8}-" "[0-9a-fA-F]{4}-" @@ -90,6 +93,36 @@ def _validate_id(id_, spec_version, required_prefix, interoperability): raise ValueError(ERROR_INVALID_ID.format(id_)) +def _validate_type(type_, spec_version): + """ + Check the STIX type name for correctness, raise an exception if there are + errors. + + :param type_: The STIX type name + :param spec_version: The STIX specification version to use + :raises ValueError: If there are any errors with the identifier + """ + if spec_version == "2.0": + if not re.match(TYPE_REGEX, type_): + raise ValueError( + "Invalid type name '%s': must only contain the " + "characters a-z (lowercase ASCII), 0-9, and hyphen (-)." % + type_, + ) + else: # 2.1+ + if not re.match(TYPE_21_REGEX, type_): + raise ValueError( + "Invalid type name '%s': must only contain the " + "characters a-z (lowercase ASCII), 0-9, and hyphen (-) " + "and must begin with an a-z character" % type_, + ) + + if len(type_) < 3 or len(type_) > 250: + raise ValueError( + "Invalid type name '%s': must be between 3 and 250 characters." % type_, + ) + + class Property(object): """Represent a property of STIX data type. @@ -241,7 +274,9 @@ class StringProperty(Property): class TypeProperty(Property): - def __init__(self, type): + def __init__(self, type, spec_version=stix2.DEFAULT_VERSION): + _validate_type(type, spec_version) + self.spec_version = spec_version super(TypeProperty, self).__init__(fixed=type) @@ -333,12 +368,16 @@ class BooleanProperty(Property): class TimestampProperty(Property): - def __init__(self, precision=None, **kwargs): + def __init__(self, precision="any", precision_constraint="exact", **kwargs): self.precision = precision + self.precision_constraint = precision_constraint + super(TimestampProperty, self).__init__(**kwargs) def clean(self, value): - return parse_into_datetime(value, self.precision) + return parse_into_datetime( + value, self.precision, self.precision_constraint, + ) class DictionaryProperty(Property): @@ -650,7 +689,7 @@ class STIXObjectProperty(Property): def clean(self, value): # Any STIX Object (SDO, SRO, or Marking Definition) can be added to # a bundle with no further checks. - if any(x in ('STIXDomainObject', 'STIXRelationshipObject', 'MarkingDefinition') + if any(x in ('_DomainObject', '_RelationshipObject', 'MarkingDefinition') for x in get_class_hierarchy_names(value)): # A simple "is this a spec version 2.1+ object" test. For now, # limit 2.0 bundles to 2.0 objects. It's not possible yet to diff --git a/stix2/test/test_spec_version_detect.py b/stix2/test/test_spec_version_detect.py index d2a9da8..7039024 100644 --- a/stix2/test/test_spec_version_detect.py +++ b/stix2/test/test_spec_version_detect.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import pytest -from stix2.core import _detect_spec_version +from stix2.parsing import _detect_spec_version @pytest.mark.parametrize( diff --git a/stix2/test/v20/conftest.py b/stix2/test/v20/conftest.py index 48e4532..6bb8fae 100644 --- a/stix2/test/v20/conftest.py +++ b/stix2/test/v20/conftest.py @@ -114,6 +114,42 @@ def stix_objs1(): return [ind1, ind2, ind3, ind4, ind5] +@pytest.fixture +def stix_objs1_manifests(): + # Tests against latest medallion (TAXII 2.1) + ind1 = { + "date_added": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "media_type": "application/stix+json;version=2.1", + "version": "2017-01-27T13:49:53.935Z", + } + ind2 = { + "date_added": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "media_type": "application/stix+json;version=2.1", + "version": "2017-01-27T13:49:53.935Z", + } + ind3 = { + "date_added": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "media_type": "application/stix+json;version=2.1", + "version": "2017-01-27T13:49:53.936Z", + } + ind4 = { + "date_added": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000002", + "media_type": "application/stix+json;version=2.1", + "version": "2017-01-27T13:49:53.935Z", + } + ind5 = { + "date_added": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000002", + "media_type": "application/stix+json;version=2.1", + "version": "2017-01-27T13:49:53.935Z", + } + return [ind1, ind2, ind3, ind4, ind5] + + @pytest.fixture def stix_objs2(): ind6 = { diff --git a/stix2/test/v20/test_core.py b/stix2/test/v20/test_core.py deleted file mode 100644 index d2efa22..0000000 --- a/stix2/test/v20/test_core.py +++ /dev/null @@ -1,176 +0,0 @@ -import pytest - -import stix2 -from stix2 import core, exceptions - -from .constants import IDENTITY_ID - -BUNDLE = { - "type": "bundle", - "spec_version": "2.0", - "id": "bundle--00000000-0000-4000-8000-000000000007", - "objects": [ - { - "type": "indicator", - "id": "indicator--00000000-0000-4000-8000-000000000001", - "created": "2017-01-01T12:34:56.000Z", - "modified": "2017-01-01T12:34:56.000Z", - "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", - "valid_from": "2017-01-01T12:34:56Z", - "labels": [ - "malicious-activity", - ], - }, - { - "type": "malware", - "id": "malware--00000000-0000-4000-8000-000000000003", - "created": "2017-01-01T12:34:56.000Z", - "modified": "2017-01-01T12:34:56.000Z", - "name": "Cryptolocker", - "labels": [ - "ransomware", - ], - }, - { - "type": "relationship", - "id": "relationship--00000000-0000-4000-8000-000000000005", - "created": "2017-01-01T12:34:56.000Z", - "modified": "2017-01-01T12:34:56.000Z", - "relationship_type": "indicates", - "source_ref": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", - "target_ref": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e", - }, - ], -} - - -def test_dict_to_stix2_bundle_with_version(): - with pytest.raises(exceptions.ExtraPropertiesError) as excinfo: - core.dict_to_stix2(BUNDLE, version='2.1') - - assert str(excinfo.value) == "Unexpected properties for Bundle: (spec_version)." - - -def test_parse_observable_with_version(): - observable = {"type": "file", "name": "foo.exe"} - obs_obj = core.parse_observable(observable, version='2.0') - v = 'v20' - - assert v in str(obs_obj.__class__) - - -@pytest.mark.xfail(reason="The default version is no longer 2.0", condition=stix2.DEFAULT_VERSION != "2.0") -def test_parse_observable_with_no_version(): - observable = {"type": "file", "name": "foo.exe"} - obs_obj = core.parse_observable(observable) - v = 'v20' - - assert v in str(obs_obj.__class__) - - -def test_register_object_with_version(): - bundle = core.dict_to_stix2(BUNDLE, version='2.0') - core._register_object(bundle.objects[0].__class__, version='2.0') - v = 'v20' - - assert bundle.objects[0].type in core.STIX2_OBJ_MAPS[v]['objects'] - # spec_version is not in STIX 2.0, and is required in 2.1, so this - # suffices as a test for a STIX 2.0 object. - assert "spec_version" not in bundle.objects[0] - - -def test_register_marking_with_version(): - core._register_marking(stix2.v20.TLP_WHITE.__class__, version='2.0') - v = 'v20' - - assert stix2.v20.TLP_WHITE.definition._type in core.STIX2_OBJ_MAPS[v]['markings'] - assert v in str(stix2.v20.TLP_WHITE.__class__) - - -@pytest.mark.xfail(reason="The default version is no longer 2.0", condition=stix2.DEFAULT_VERSION != "2.0") -def test_register_marking_with_no_version(): - # Uses default version (2.0 in this case) - core._register_marking(stix2.v20.TLP_WHITE.__class__) - v = 'v20' - - assert stix2.v20.TLP_WHITE.definition._type in core.STIX2_OBJ_MAPS[v]['markings'] - assert v in str(stix2.v20.TLP_WHITE.__class__) - - -def test_register_observable_with_version(): - observed_data = stix2.v20.ObservedData( - id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", - created_by_ref=IDENTITY_ID, - created="2016-04-06T19:58:16.000Z", - modified="2016-04-06T19:58:16.000Z", - first_observed="2015-12-21T19:00:00Z", - last_observed="2015-12-21T19:00:00Z", - number_observed=50, - objects={ - "0": { - "name": "foo.exe", - "type": "file", - "extensions": { - "ntfs-ext": { - "alternate_data_streams": [ - { - "name": "second.stream", - "size": 25536, - }, - ], - }, - }, - }, - "1": { - "type": "directory", - "path": "/usr/home", - "contains_refs": ["0"], - }, - }, - ) - core._register_observable(observed_data.objects['0'].__class__, version='2.0') - v = 'v20' - - assert observed_data.objects['0'].type in core.STIX2_OBJ_MAPS[v]['observables'] - assert v in str(observed_data.objects['0'].__class__) - - -def test_register_observable_extension_with_version(): - observed_data = stix2.v20.ObservedData( - id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", - created_by_ref=IDENTITY_ID, - created="2016-04-06T19:58:16.000Z", - modified="2016-04-06T19:58:16.000Z", - first_observed="2015-12-21T19:00:00Z", - last_observed="2015-12-21T19:00:00Z", - number_observed=50, - objects={ - "0": { - "name": "foo.exe", - "type": "file", - "extensions": { - "ntfs-ext": { - "alternate_data_streams": [ - { - "name": "second.stream", - "size": 25536, - }, - ], - }, - }, - }, - "1": { - "type": "directory", - "path": "/usr/home", - "contains_refs": ["0"], - }, - }, - ) - core._register_observable_extension(observed_data.objects['0'], observed_data.objects['0'].extensions['ntfs-ext'].__class__, version='2.0') - v = 'v20' - - assert observed_data.objects['0'].type in core.STIX2_OBJ_MAPS[v]['observables'] - assert v in str(observed_data.objects['0'].__class__) - - assert observed_data.objects['0'].extensions['ntfs-ext']._type in core.STIX2_OBJ_MAPS[v]['observable-extensions']['file'] - assert v in str(observed_data.objects['0'].extensions['ntfs-ext'].__class__) diff --git a/stix2/test/v20/test_custom.py b/stix2/test/v20/test_custom.py index b986777..57f8aac 100644 --- a/stix2/test/v20/test_custom.py +++ b/stix2/test/v20/test_custom.py @@ -1,9 +1,10 @@ import pytest import stix2 +from stix2 import parsing import stix2.v20 -from ...exceptions import InvalidValueError +from ...exceptions import DuplicateRegistrationError, InvalidValueError from .constants import FAKE_TIME, IDENTITY_ID, MARKING_DEFINITION_ID IDENTITY_CUSTOM_PROP = stix2.v20.Identity( @@ -449,7 +450,7 @@ def test_custom_observable_raises_exception(): def test_custom_observable_object_no_init_1(): @stix2.v20.CustomObservable( - 'x-new-observable', [ + 'x-new-observable-1', [ ('property1', stix2.properties.StringProperty()), ], ) @@ -482,7 +483,7 @@ def test_custom_observable_object_invalid_type_name(): ) class NewObs(object): pass # pragma: no cover - assert "Invalid observable type name 'x':" in str(excinfo.value) + assert "Invalid type name 'x':" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: @stix2.v20.CustomObservable( @@ -492,7 +493,7 @@ def test_custom_observable_object_invalid_type_name(): ) class NewObs2(object): pass # pragma: no cover - assert "Invalid observable type name 'x_new_obs':" in str(excinfo.value) + assert "Invalid type name 'x_new_obs':" in str(excinfo.value) def test_custom_observable_object_invalid_ref_property(): @@ -807,7 +808,7 @@ def test_custom_extension_invalid_type_name(): ) class FooExtension(): pass # pragma: no cover - assert "Invalid extension type name 'x':" in str(excinfo.value) + assert "Invalid type name 'x':" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: @stix2.v20.CustomExtension( @@ -817,7 +818,7 @@ def test_custom_extension_invalid_type_name(): ) class BlaExtension(): pass # pragma: no cover - assert "Invalid extension type name 'x_new_ext':" in str(excinfo.value) + assert "Invalid type name 'x_new_ext':" in str(excinfo.value) def test_custom_extension_no_properties(): @@ -967,9 +968,8 @@ def test_register_custom_object(): class CustomObject2(object): _type = 'awesome-object' - stix2.core._register_object(CustomObject2, version="2.0") - # Note that we will always check against newest OBJ_MAP. - assert (CustomObject2._type, CustomObject2) in stix2.v20.OBJ_MAP.items() + with pytest.raises(ValueError): + stix2.parsing._register_object(CustomObject2, version="2.0") def test_extension_property_location(): @@ -1011,3 +1011,123 @@ def test_custom_object_nested_dictionary(data): ) assert data == str(example) + + +@stix2.v20.CustomObject( + 'x-new-type-2', [ + ('property1', stix2.properties.StringProperty()), + ('property2', stix2.properties.IntegerProperty()), + ], +) +class NewType2(object): + pass + + +def test_register_custom_object_with_version(): + custom_obj_1 = { + "type": "x-new-type-2", + "id": "x-new-type-2--00000000-0000-4000-8000-000000000007", + } + + cust_obj_1 = parsing.dict_to_stix2(custom_obj_1, version='2.0') + v = 'v20' + + assert cust_obj_1.type in parsing.STIX2_OBJ_MAPS[v]['objects'] + # spec_version is not in STIX 2.0, and is required in 2.1, so this + # suffices as a test for a STIX 2.0 object. + assert "spec_version" not in cust_obj_1 + + +def test_register_duplicate_object_with_version(): + with pytest.raises(DuplicateRegistrationError) as excinfo: + @stix2.v20.CustomObject( + 'x-new-type-2', [ + ('property1', stix2.properties.StringProperty()), + ('property2', stix2.properties.IntegerProperty()), + ], + ) + class NewType2(object): + pass + assert "cannot be registered again" in str(excinfo.value) + + +@stix2.v20.CustomObservable( + 'x-new-observable-2', [ + ('property1', stix2.properties.StringProperty()), + ], +) +class NewObservable2(object): + pass + + +def test_register_observable_with_version(): + custom_obs = NewObservable2(property1="Test Observable") + v = 'v20' + + assert custom_obs.type in parsing.STIX2_OBJ_MAPS[v]['observables'] + + +def test_register_duplicate_observable_with_version(): + with pytest.raises(DuplicateRegistrationError) as excinfo: + @stix2.v20.CustomObservable( + 'x-new-observable-2', [ + ('property1', stix2.properties.StringProperty()), + ], + ) + class NewObservable2(object): + pass + assert "cannot be registered again" in str(excinfo.value) + + +def test_register_marking_with_version(): + @stix2.v20.CustomMarking( + 'x-new-obj-2', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewObj2(): + pass + v = 'v20' + + no = NewObj2(property1='something') + assert no._type in parsing.STIX2_OBJ_MAPS[v]['markings'] + + +def test_register_observable_extension_with_version(): + @stix2.v20.CustomExtension( + stix2.v20.UserAccount, 'some-extension-2', [ + ('keys', stix2.properties.StringProperty(required=True)), + ], + ) + class SomeCustomExtension2: + pass + + v = 'v20' + example = SomeCustomExtension2(keys='test123') + + assert example._type in parsing.STIX2_OBJ_MAPS[v]['observable-extensions']['user-account'] + + +def test_register_duplicate_observable_extension(): + with pytest.raises(DuplicateRegistrationError) as excinfo: + @stix2.v20.CustomExtension( + stix2.v20.UserAccount, 'some-extension-2', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), + ], + ) + class NewExtension2(): + pass + assert "cannot be registered again" in str(excinfo.value) + + +def test_register_duplicate_marking(): + with pytest.raises(DuplicateRegistrationError) as excinfo: + @stix2.v20.CustomMarking( + 'x-new-obj-2', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewObj2(): + pass + assert "cannot be registered again" in str(excinfo.value) diff --git a/stix2/test/v20/test_datastore_filesystem.py b/stix2/test/v20/test_datastore_filesystem.py index 317f927..25207dc 100644 --- a/stix2/test/v20/test_datastore_filesystem.py +++ b/stix2/test/v20/test_datastore_filesystem.py @@ -635,7 +635,7 @@ def test_filesystem_object_with_custom_property_in_bundle(fs_store): def test_filesystem_custom_object(fs_store): @stix2.v20.CustomObject( - 'x-new-obj', [ + 'x-new-obj-2', [ ('property1', stix2.properties.StringProperty(required=True)), ], ) @@ -650,7 +650,7 @@ def test_filesystem_custom_object(fs_store): assert newobj_r["property1"] == 'something' # remove dir - shutil.rmtree(os.path.join(FS_PATH, "x-new-obj"), True) + shutil.rmtree(os.path.join(FS_PATH, "x-new-obj-2"), True) def test_relationships(rel_fs_store): diff --git a/stix2/test/v20/test_datastore_memory.py b/stix2/test/v20/test_datastore_memory.py index 7852746..28d8e52 100644 --- a/stix2/test/v20/test_datastore_memory.py +++ b/stix2/test/v20/test_datastore_memory.py @@ -329,7 +329,7 @@ def test_memory_store_object_with_custom_property_in_bundle(mem_store): def test_memory_store_custom_object(mem_store): @CustomObject( - 'x-new-obj', [ + 'x-new-obj-3', [ ('property1', properties.StringProperty(required=True)), ], ) diff --git a/stix2/test/v20/test_datastore_taxii.py b/stix2/test/v20/test_datastore_taxii.py index 5a3c0cb..b0c6b77 100644 --- a/stix2/test/v20/test_datastore_taxii.py +++ b/stix2/test/v20/test_datastore_taxii.py @@ -4,11 +4,13 @@ from medallion.filters.basic_filter import BasicFilter import pytest from requests.models import Response import six -from taxii2client import Collection, _filter_kwargs_to_query_params +from taxii2client.common import _filter_kwargs_to_query_params +from taxii2client.v20 import Collection import stix2 from stix2.datastore import DataSourceError from stix2.datastore.filters import Filter +from stix2.utils import get_timestamp COLLECTION_URL = 'https://example.com/api1/collections/91a7b528-80eb-42ed-a74d-c6fbd5a26116/' @@ -21,6 +23,7 @@ class MockTAXIICollectionEndpoint(Collection): url, collection_info=collection_info, ) self.objects = [] + self.manifests = [] def add_objects(self, bundle): self._verify_can_write() @@ -28,6 +31,14 @@ class MockTAXIICollectionEndpoint(Collection): bundle = json.loads(bundle, encoding='utf-8') for object in bundle.get("objects", []): self.objects.append(object) + self.manifests.append( + { + "date_added": get_timestamp(), + "id": object["id"], + "media_type": "application/stix+json;version=2.1", + "version": object.get("modified", object.get("created", get_timestamp())), + }, + ) def get_objects(self, **filter_kwargs): self._verify_can_read() @@ -37,8 +48,9 @@ class MockTAXIICollectionEndpoint(Collection): objs = full_filter.process_filter( self.objects, ("id", "type", "version"), - [], - ) + self.manifests, + 100, + )[0] if objs: return stix2.v20.Bundle(objects=objs) else: @@ -58,8 +70,9 @@ class MockTAXIICollectionEndpoint(Collection): filtered_objects = full_filter.process_filter( objects, ("version",), - [], - ) + self.manifests, + 100, + )[0] else: filtered_objects = [] if filtered_objects: @@ -71,7 +84,7 @@ class MockTAXIICollectionEndpoint(Collection): @pytest.fixture -def collection(stix_objs1): +def collection(stix_objs1, stix_objs1_manifests): mock = MockTAXIICollectionEndpoint( COLLECTION_URL, { "id": "91a7b528-80eb-42ed-a74d-c6fbd5a26116", @@ -86,11 +99,12 @@ def collection(stix_objs1): ) mock.objects.extend(stix_objs1) + mock.manifests.extend(stix_objs1_manifests) return mock @pytest.fixture -def collection_no_rw_access(stix_objs1): +def collection_no_rw_access(stix_objs1, stix_objs1_manifests): mock = MockTAXIICollectionEndpoint( COLLECTION_URL, { "id": "91a7b528-80eb-42ed-a74d-c6fbd5a26116", @@ -105,6 +119,7 @@ def collection_no_rw_access(stix_objs1): ) mock.objects.extend(stix_objs1) + mock.manifests.extend(stix_objs1_manifests) return mock diff --git a/stix2/test/v20/test_markings.py b/stix2/test/v20/test_markings.py index b34ef43..0753e86 100644 --- a/stix2/test/v20/test_markings.py +++ b/stix2/test/v20/test_markings.py @@ -249,14 +249,12 @@ def test_not_registered_marking_raises_exception(): def test_marking_wrong_type_construction(): - with pytest.raises(ValueError) as excinfo: + with pytest.raises(ValueError): # Test passing wrong type for properties. @stix2.v20.CustomMarking('x-new-marking-type2', ("a", "b")) class NewObject3(object): pass - assert str(excinfo.value) == "Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]" - def test_campaign_add_markings(): campaign = stix2.v20.Campaign( diff --git a/stix2/test/v20/test_parsing.py b/stix2/test/v20/test_parsing.py new file mode 100644 index 0000000..32c3e8b --- /dev/null +++ b/stix2/test/v20/test_parsing.py @@ -0,0 +1,74 @@ +import pytest + +import stix2 +from stix2 import exceptions, parsing + +BUNDLE = { + "type": "bundle", + "spec_version": "2.0", + "id": "bundle--00000000-0000-4000-8000-000000000007", + "objects": [ + { + "type": "indicator", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", + "valid_from": "2017-01-01T12:34:56Z", + "labels": [ + "malicious-activity", + ], + }, + { + "type": "malware", + "id": "malware--00000000-0000-4000-8000-000000000003", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "name": "Cryptolocker", + "labels": [ + "ransomware", + ], + }, + { + "type": "relationship", + "id": "relationship--00000000-0000-4000-8000-000000000005", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "relationship_type": "indicates", + "source_ref": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "target_ref": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e", + }, + ], +} + + +def test_dict_to_stix2_bundle_with_version(): + with pytest.raises(exceptions.ExtraPropertiesError) as excinfo: + parsing.dict_to_stix2(BUNDLE, version='2.1') + + assert str(excinfo.value) == "Unexpected properties for Bundle: (spec_version)." + + +def test_parse_observable_with_version(): + observable = {"type": "file", "name": "foo.exe"} + obs_obj = parsing.parse_observable(observable, version='2.0') + v = 'v20' + + assert v in str(obs_obj.__class__) + + +@pytest.mark.xfail(reason="The default version is no longer 2.0", condition=stix2.DEFAULT_VERSION != "2.0") +def test_parse_observable_with_no_version(): + observable = {"type": "file", "name": "foo.exe"} + obs_obj = parsing.parse_observable(observable) + v = 'v20' + + assert v in str(obs_obj.__class__) + + +def test_register_marking_with_version(): + parsing._register_marking(stix2.v20.TLP_WHITE.__class__, version='2.0') + v = 'v20' + + assert stix2.v20.TLP_WHITE.definition._type in parsing.STIX2_OBJ_MAPS[v]['markings'] + assert v in str(stix2.v20.TLP_WHITE.__class__) diff --git a/stix2/test/v20/test_pattern_expressions.py b/stix2/test/v20/test_pattern_expressions.py index 23a401b..d5cbb5b 100644 --- a/stix2/test/v20/test_pattern_expressions.py +++ b/stix2/test/v20/test_pattern_expressions.py @@ -494,13 +494,14 @@ def test_make_constant_already_a_constant(): def test_parsing_comparison_expression(): - patt_obj = create_pattern_object("[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f']") + patt_obj = create_pattern_object("[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f']", version="2.0") assert str(patt_obj) == "[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f']" def test_parsing_qualified_expression(): patt_obj = create_pattern_object( "[network-traffic:dst_ref.type = 'domain-name' AND network-traffic:dst_ref.value = 'example.com'] REPEATS 5 TIMES WITHIN 1800 SECONDS", + version="2.0", ) assert str( patt_obj, @@ -508,5 +509,5 @@ def test_parsing_qualified_expression(): def test_list_constant(): - patt_obj = create_pattern_object("[network-traffic:src_ref.value IN ('10.0.0.0', '10.0.0.1', '10.0.0.2')]") + patt_obj = create_pattern_object("[network-traffic:src_ref.value IN ('10.0.0.0', '10.0.0.1', '10.0.0.2')]", version="2.0") assert str(patt_obj) == "[network-traffic:src_ref.value IN ('10.0.0.0', '10.0.0.1', '10.0.0.2')]" diff --git a/stix2/test/v20/test_properties.py b/stix2/test/v20/test_properties.py index f71d829..1d1474a 100644 --- a/stix2/test/v20/test_properties.py +++ b/stix2/test/v20/test_properties.py @@ -392,7 +392,7 @@ def test_dictionary_property_invalid(d): def test_property_list_of_dictionary(): @stix2.v20.CustomObject( - 'x-new-obj', [ + 'x-new-obj-4', [ ('property1', ListProperty(DictionaryProperty(spec_version="2.0"), required=True)), ], ) diff --git a/stix2/test/v21/conftest.py b/stix2/test/v21/conftest.py index 103db34..d602f42 100644 --- a/stix2/test/v21/conftest.py +++ b/stix2/test/v21/conftest.py @@ -135,6 +135,42 @@ def stix_objs1(): return [ind1, ind2, ind3, ind4, ind5] +@pytest.fixture +def stix_objs1_manifests(): + # Tests against latest medallion (TAXII 2.1) + ind1 = { + "date_added": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "media_type": "application/stix+json;version=2.1", + "version": "2017-01-27T13:49:53.935Z", + } + ind2 = { + "date_added": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "media_type": "application/stix+json;version=2.1", + "version": "2017-01-27T13:49:53.935Z", + } + ind3 = { + "date_added": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "media_type": "application/stix+json;version=2.1", + "version": "2017-01-27T13:49:53.936Z", + } + ind4 = { + "date_added": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000002", + "media_type": "application/stix+json;version=2.1", + "version": "2017-01-27T13:49:53.935Z", + } + ind5 = { + "date_added": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000002", + "media_type": "application/stix+json;version=2.1", + "version": "2017-01-27T13:49:53.935Z", + } + return [ind1, ind2, ind3, ind4, ind5] + + @pytest.fixture def stix_objs2(): ind6 = { diff --git a/stix2/test/v21/test_attack_pattern.py b/stix2/test/v21/test_attack_pattern.py index 165581c..b826f1e 100644 --- a/stix2/test/v21/test_attack_pattern.py +++ b/stix2/test/v21/test_attack_pattern.py @@ -88,8 +88,8 @@ def test_attack_pattern_invalid_labels(): def test_overly_precise_timestamps(): ap = stix2.v21.AttackPattern( id=ATTACK_PATTERN_ID, - created="2016-05-12T08:17:27.0000342Z", - modified="2016-05-12T08:17:27.000287Z", + created="2016-05-12T08:17:27.000000342Z", + modified="2016-05-12T08:17:27.000000287Z", name="Spear Phishing", external_references=[{ "source_name": "capec", diff --git a/stix2/test/v21/test_core.py b/stix2/test/v21/test_core.py deleted file mode 100644 index 2018395..0000000 --- a/stix2/test/v21/test_core.py +++ /dev/null @@ -1,179 +0,0 @@ -import pytest - -import stix2 -from stix2 import core, exceptions - -from .constants import IDENTITY_ID, OBSERVED_DATA_ID - -BUNDLE = { - "type": "bundle", - "id": "bundle--00000000-0000-4000-8000-000000000007", - "objects": [ - { - "type": "indicator", - "spec_version": "2.1", - "id": "indicator--00000000-0000-4000-8000-000000000001", - "created": "2017-01-01T12:34:56.000Z", - "modified": "2017-01-01T12:34:56.000Z", - "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", - "pattern_type": "stix", - "valid_from": "2017-01-01T12:34:56Z", - "indicator_types": [ - "malicious-activity", - ], - }, - { - "type": "malware", - "spec_version": "2.1", - "id": "malware--00000000-0000-4000-8000-000000000003", - "created": "2017-01-01T12:34:56.000Z", - "modified": "2017-01-01T12:34:56.000Z", - "name": "Cryptolocker", - "malware_types": [ - "ransomware", - ], - "is_family": False, - }, - { - "type": "relationship", - "spec_version": "2.1", - "id": "relationship--00000000-0000-4000-8000-000000000005", - "created": "2017-01-01T12:34:56.000Z", - "modified": "2017-01-01T12:34:56.000Z", - "relationship_type": "indicates", - "source_ref": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", - "target_ref": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e", - }, - ], -} - - -def test_dict_to_stix2_bundle_with_version(): - with pytest.raises(exceptions.InvalidValueError) as excinfo: - core.dict_to_stix2(BUNDLE, version='2.0') - - msg = "Invalid value for Bundle 'objects': Spec version 2.0 bundles don't yet support containing objects of a different spec version." - assert str(excinfo.value) == msg - - -def test_parse_observable_with_version(): - observable = {"type": "file", "name": "foo.exe"} - obs_obj = core.parse_observable(observable, version='2.1') - v = 'v21' - - assert v in str(obs_obj.__class__) - - -@pytest.mark.xfail(reason="The default version is not 2.1", condition=stix2.DEFAULT_VERSION != "2.1") -def test_parse_observable_with_no_version(): - observable = {"type": "file", "name": "foo.exe"} - obs_obj = core.parse_observable(observable) - v = 'v21' - - assert v in str(obs_obj.__class__) - - -def test_register_object_with_version(): - bundle = core.dict_to_stix2(BUNDLE, version='2.1') - core._register_object(bundle.objects[0].__class__) - v = 'v21' - - assert bundle.objects[0].type in core.STIX2_OBJ_MAPS[v]['objects'] - assert bundle.objects[0].spec_version == "2.1" - - -def test_register_marking_with_version(): - core._register_marking(stix2.v21.TLP_WHITE.__class__, version='2.1') - v = 'v21' - - assert stix2.v21.TLP_WHITE.definition._type in core.STIX2_OBJ_MAPS[v]['markings'] - assert v in str(stix2.v21.TLP_WHITE.__class__) - - -@pytest.mark.xfail(reason="The default version is not 2.1", condition=stix2.DEFAULT_VERSION != "2.1") -def test_register_marking_with_no_version(): - # Uses default version (2.0 in this case) - core._register_marking(stix2.v21.TLP_WHITE.__class__) - v = 'v21' - - assert stix2.v21.TLP_WHITE.definition._type in core.STIX2_OBJ_MAPS[v]['markings'] - assert v in str(stix2.v21.TLP_WHITE.__class__) - - -def test_register_observable_with_default_version(): - observed_data = stix2.v21.ObservedData( - id=OBSERVED_DATA_ID, - created_by_ref=IDENTITY_ID, - created="2016-04-06T19:58:16.000Z", - modified="2016-04-06T19:58:16.000Z", - first_observed="2015-12-21T19:00:00Z", - last_observed="2015-12-21T19:00:00Z", - number_observed=50, - objects={ - "0": { - "name": "foo.exe", - "type": "file", - "extensions": { - "ntfs-ext": { - "alternate_data_streams": [ - { - "name": "second.stream", - "size": 25536, - }, - ], - }, - }, - }, - "1": { - "type": "directory", - "path": "/usr/home", - "contains_refs": ["file--420bc087-8b53-5ae9-8210-20d27d5e96c8"], - }, - }, - ) - core._register_observable(observed_data.objects['0'].__class__) - v = 'v21' - - assert observed_data.objects['0'].type in core.STIX2_OBJ_MAPS[v]['observables'] - assert v in str(observed_data.objects['0'].__class__) - - -def test_register_observable_extension_with_default_version(): - observed_data = stix2.v21.ObservedData( - id=OBSERVED_DATA_ID, - created_by_ref=IDENTITY_ID, - created="2016-04-06T19:58:16.000Z", - modified="2016-04-06T19:58:16.000Z", - first_observed="2015-12-21T19:00:00Z", - last_observed="2015-12-21T19:00:00Z", - number_observed=50, - objects={ - "0": { - "name": "foo.exe", - "type": "file", - "extensions": { - "ntfs-ext": { - "alternate_data_streams": [ - { - "name": "second.stream", - "size": 25536, - }, - ], - }, - }, - }, - "1": { - "type": "directory", - "path": "/usr/home", - "contains_refs": ["file--420bc087-8b53-5ae9-8210-20d27d5e96c8"], - }, - }, - ) - core._register_observable_extension(observed_data.objects['0'], observed_data.objects['0'].extensions['ntfs-ext'].__class__) - v = 'v21' - - assert observed_data.objects['0'].type in core.STIX2_OBJ_MAPS[v]['observables'] - assert v in str(observed_data.objects['0'].__class__) - - assert observed_data.objects['0'].extensions['ntfs-ext']._type in core.STIX2_OBJ_MAPS[v]['observable-extensions']['file'] - assert v in str(observed_data.objects['0'].extensions['ntfs-ext'].__class__) diff --git a/stix2/test/v21/test_custom.py b/stix2/test/v21/test_custom.py index 1e6f629..3646f11 100644 --- a/stix2/test/v21/test_custom.py +++ b/stix2/test/v21/test_custom.py @@ -6,9 +6,11 @@ import stix2 import stix2.base import stix2.v21 -from ...exceptions import InvalidValueError +from ...exceptions import DuplicateRegistrationError, InvalidValueError from .constants import FAKE_TIME, IDENTITY_ID, MARKING_DEFINITION_ID +# Custom Properties in SDOs + IDENTITY_CUSTOM_PROP = stix2.v21.Identity( name="John Smith", identity_class="individual", @@ -18,6 +20,18 @@ IDENTITY_CUSTOM_PROP = stix2.v21.Identity( def test_identity_custom_property(): + identity = stix2.v21.Identity( + id=IDENTITY_ID, + created="2015-12-21T19:59:11Z", + modified="2015-12-21T19:59:11Z", + name="John Smith", + identity_class="individual", + custom_properties={ + "foo": "bar", + }, + ) + assert identity.foo == "bar" + with pytest.raises(ValueError) as excinfo: stix2.v21.Identity( id=IDENTITY_ID, @@ -43,17 +57,47 @@ def test_identity_custom_property(): ) assert "Unexpected properties for Identity" in str(excinfo.value) - identity = stix2.v21.Identity( - id=IDENTITY_ID, - created="2015-12-21T19:59:11Z", - modified="2015-12-21T19:59:11Z", - name="John Smith", - identity_class="individual", - custom_properties={ - "foo": "bar", - }, - ) - assert identity.foo == "bar" + # leading numeric character is illegal in 2.1 + + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.Identity( + id=IDENTITY_ID, + created="2015-12-21T19:59:11Z", + modified="2015-12-21T19:59:11Z", + name="John Smith", + identity_class="individual", + custom_properties={ + "7foo": "bar", + }, + ) + assert "must begin with an alpha character." in str(excinfo.value) + + # leading "_" is illegal in 2.1 + + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.Identity( + id=IDENTITY_ID, + created="2015-12-21T19:59:11Z", + modified="2015-12-21T19:59:11Z", + name="John Smith", + identity_class="individual", + custom_properties={ + "_foo": "bar", + }, + ) + assert "must begin with an alpha character." in str(excinfo.value) + + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + identity = stix2.v21.Identity( + id=IDENTITY_ID, + created="2015-12-21T19:59:11Z", + modified="2015-12-21T19:59:11Z", + name="John Smith", + identity_class="individual", + _x_foo="bar", + allow_custom=True, + ) + assert "must begin with an alpha character." in str(excinfo.value) def test_identity_custom_property_invalid(): @@ -165,6 +209,8 @@ def test_custom_properties_dict_in_bundled_object(): assert bundle.objects[0].x_foo == "bar" assert '"x_foo": "bar"' in str(bundle) +# Custom properties in SCOs + def test_custom_property_in_observed_data(): artifact = stix2.v21.File( @@ -184,6 +230,18 @@ def test_custom_property_in_observed_data(): assert '"x_foo": "bar"' in str(observed_data) +def test_invalid_custom_property_in_observed_data(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.File( + custom_properties={"8foo": 1}, + allow_custom=True, + name='test', + x_foo='bar', + ) + + assert "must begin with an alpha character." in str(excinfo.value) + + def test_custom_property_object_in_observable_extension(): ntfs = stix2.v21.NTFSExt( allow_custom=True, @@ -245,6 +303,8 @@ def test_identity_custom_property_revoke(): identity = IDENTITY_CUSTOM_PROP.revoke() assert identity.x_foo == "bar" +# Custom markings + def test_identity_custom_property_edit_markings(): marking_obj = stix2.v21.MarkingDefinition( @@ -267,6 +327,19 @@ def test_identity_custom_property_edit_markings(): identity2.clear_markings('x_foo') +def test_invalid_custom_property_in_marking(): + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomMarking( + 'x-new-obj', [ + ('9property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewObj(): + pass + + assert "must begin with an alpha character." in str(excinfo.value) + + def test_custom_marking_no_init_1(): @stix2.v21.CustomMarking( 'x-new-obj', [ @@ -293,6 +366,40 @@ def test_custom_marking_no_init_2(): assert no2.property1 == 'something' +def test_custom_marking_invalid_type_name(): + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomMarking( + 'x', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewObj(object): + pass # pragma: no cover + assert "Invalid type name 'x': " in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomMarking( + 'x_new_marking', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewObj2(object): + pass # pragma: no cover + assert "Invalid type name 'x_new_marking':" in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomMarking( + '7x-new-marking', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewObj3(object): + pass # pragma: no cover + assert "Invalid type name '7x-new-marking':" in str(excinfo.value) + +# Custom Objects + + @stix2.v21.CustomObject( 'x-new-type', [ ('property1', stix2.properties.StringProperty(required=True)), @@ -374,6 +481,16 @@ def test_custom_object_invalid_type_name(): pass # pragma: no cover assert "Invalid type name 'x_new_object':" in str(excinfo.value) + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomObject( + '7x-new-object', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewObj3(object): + pass # pragma: no cover + assert "Invalid type name '7x-new-object':" in str(excinfo.value) + def test_parse_custom_object_type(): nt_string = """{ @@ -412,6 +529,8 @@ def test_parse_unregistered_custom_object_type_w_allow_custom(): custom_obj = stix2.parse(nt_string, version="2.1", allow_custom=True) assert custom_obj["type"] == "x-foobar-observable" +# Custom SCOs + @stix2.v21.CustomObservable( 'x-new-observable', [ @@ -455,7 +574,7 @@ def test_custom_observable_raises_exception(): def test_custom_observable_object_no_init_1(): @stix2.v21.CustomObservable( - 'x-new-observable', [ + 'x-new-observable-2', [ ('property1', stix2.properties.StringProperty()), ], ) @@ -479,6 +598,18 @@ def test_custom_observable_object_no_init_2(): assert no2.property1 == 'something' +def test_invalid_custom_property_in_custom_observable_object(): + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomObservable( + 'x-new-sco', [ + ('5property1', stix2.properties.StringProperty()), + ], + ) + class NewObs(object): + pass # pragma: no cover + assert "must begin with an alpha character." in str(excinfo.value) + + def test_custom_observable_object_invalid_type_name(): with pytest.raises(ValueError) as excinfo: @stix2.v21.CustomObservable( @@ -488,7 +619,7 @@ def test_custom_observable_object_invalid_type_name(): ) class NewObs(object): pass # pragma: no cover - assert "Invalid observable type name 'x':" in str(excinfo.value) + assert "Invalid type name 'x':" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: @stix2.v21.CustomObservable( @@ -498,7 +629,17 @@ def test_custom_observable_object_invalid_type_name(): ) class NewObs2(object): pass # pragma: no cover - assert "Invalid observable type name 'x_new_obs':" in str(excinfo.value) + assert "Invalid type name 'x_new_obs':" in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomObservable( + '7x-new-obs', [ + ('property1', stix2.properties.StringProperty()), + ], + ) + class NewObs3(object): + pass # pragma: no cover + assert "Invalid type name '7x-new-obs':" in str(excinfo.value) def test_custom_observable_object_invalid_ref_property(): @@ -736,6 +877,8 @@ def test_custom_observable_object_no_id_contrib_props(): assert uuid_obj.variant == uuid.RFC_4122 assert uuid_obj.version == 4 +# Custom Extensions + @stix2.v21.CustomExtension( stix2.v21.DomainName, 'x-new-ext', [ @@ -862,7 +1005,7 @@ def test_custom_extension_invalid_type_name(): ) class FooExtension(): pass # pragma: no cover - assert "Invalid extension type name 'x':" in str(excinfo.value) + assert "Invalid type name 'x':" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: @stix2.v21.CustomExtension( @@ -872,7 +1015,17 @@ def test_custom_extension_invalid_type_name(): ) class BlaExtension(): pass # pragma: no cover - assert "Invalid extension type name 'x_new_ext':" in str(excinfo.value) + assert "Invalid type name 'x_new_ext':" in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomExtension( + stix2.v21.File, '7x-new-ext', { + 'property1': stix2.properties.StringProperty(required=True), + }, + ) + class Bla2Extension(): + pass # pragma: no cover + assert "Invalid type name '7x-new-ext':" in str(excinfo.value) def test_custom_extension_no_properties(): @@ -922,6 +1075,19 @@ def test_custom_extension_no_init_2(): assert ne2.property1 == "foobar" +def test_invalid_custom_property_in_extension(): + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomExtension( + stix2.v21.DomainName, 'x-new3-ext', [ + ('6property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewExt(): + pass + + assert "must begin with an alpha character." in str(excinfo.value) + + def test_parse_observable_with_custom_extension(): input_str = """{ "type": "domain-name", @@ -1020,9 +1186,9 @@ def test_register_custom_object(): class CustomObject2(object): _type = 'awesome-object' - stix2.core._register_object(CustomObject2, version="2.1") - # Note that we will always check against newest OBJ_MAP. - assert (CustomObject2._type, CustomObject2) in stix2.v21.OBJ_MAP.items() + with pytest.raises(ValueError) as excinfo: + stix2.parsing._register_object(CustomObject2, version="2.1") + assert '@CustomObject decorator' in str(excinfo) def test_extension_property_location(): @@ -1065,3 +1231,110 @@ def test_custom_object_nested_dictionary(data): ) assert data == str(example) + + +@stix2.v21.CustomObject( + 'x-new-type-2', [ + ('property1', stix2.properties.StringProperty()), + ('property2', stix2.properties.IntegerProperty()), + ], +) +class NewType3(object): + pass + + +def test_register_custom_object_with_version(): + custom_obj_1 = { + "type": "x-new-type-2", + "id": "x-new-type-2--00000000-0000-4000-8000-000000000007", + "spec_version": "2.1", + } + + cust_obj_1 = stix2.parsing.dict_to_stix2(custom_obj_1, version='2.1') + v = 'v21' + + assert cust_obj_1.type in stix2.parsing.STIX2_OBJ_MAPS[v]['objects'] + assert cust_obj_1.spec_version == "2.1" + + +def test_register_duplicate_object_with_version(): + with pytest.raises(DuplicateRegistrationError) as excinfo: + @stix2.v21.CustomObject( + 'x-new-type-2', [ + ('property1', stix2.properties.StringProperty()), + ('property2', stix2.properties.IntegerProperty()), + ], + ) + class NewType2(object): + pass + assert "cannot be registered again" in str(excinfo.value) + + +@stix2.v21.CustomObservable( + 'x-new-observable-3', [ + ('property1', stix2.properties.StringProperty()), + ], +) +class NewObservable3(object): + pass + + +def test_register_observable(): + custom_obs = NewObservable3(property1="Test Observable") + v = 'v21' + + assert custom_obs.type in stix2.parsing.STIX2_OBJ_MAPS[v]['observables'] + + +def test_register_duplicate_observable(): + with pytest.raises(DuplicateRegistrationError) as excinfo: + @stix2.v21.CustomObservable( + 'x-new-observable-2', [ + ('property1', stix2.properties.StringProperty()), + ], + ) + class NewObservable2(object): + pass + assert "cannot be registered again" in str(excinfo.value) + + +def test_register_observable_custom_extension(): + @stix2.v21.CustomExtension( + stix2.v21.DomainName, 'x-new-2-ext', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), + ], + ) + class NewExtension2(): + pass + + example = NewExtension2(property1="Hi there") + v = 'v21' + + assert 'domain-name' in stix2.parsing.STIX2_OBJ_MAPS[v]['observables'] + assert example._type in stix2.parsing.STIX2_OBJ_MAPS[v]['observable-extensions']['domain-name'] + + +def test_register_duplicate_observable_extension(): + with pytest.raises(DuplicateRegistrationError) as excinfo: + @stix2.v21.CustomExtension( + stix2.v21.DomainName, 'x-new-2-ext', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), + ], + ) + class NewExtension2(): + pass + assert "cannot be registered again" in str(excinfo.value) + + +def test_register_duplicate_marking(): + with pytest.raises(DuplicateRegistrationError) as excinfo: + @stix2.v21.CustomMarking( + 'x-new-obj', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewObj2(): + pass + assert "cannot be registered again" in str(excinfo.value) diff --git a/stix2/test/v21/test_datastore_filesystem.py b/stix2/test/v21/test_datastore_filesystem.py index 3eb8aaa..123fd7a 100644 --- a/stix2/test/v21/test_datastore_filesystem.py +++ b/stix2/test/v21/test_datastore_filesystem.py @@ -656,7 +656,7 @@ def test_filesystem_object_with_custom_property_in_bundle(fs_store): def test_filesystem_custom_object(fs_store): @stix2.v21.CustomObject( - 'x-new-obj', [ + 'x-new-obj-2', [ ('property1', stix2.properties.StringProperty(required=True)), ], ) @@ -671,7 +671,7 @@ def test_filesystem_custom_object(fs_store): assert newobj_r["property1"] == 'something' # remove dir - shutil.rmtree(os.path.join(FS_PATH, "x-new-obj"), True) + shutil.rmtree(os.path.join(FS_PATH, "x-new-obj-2"), True) def test_relationships(rel_fs_store): diff --git a/stix2/test/v21/test_datastore_memory.py b/stix2/test/v21/test_datastore_memory.py index e07943c..60f577e 100644 --- a/stix2/test/v21/test_datastore_memory.py +++ b/stix2/test/v21/test_datastore_memory.py @@ -344,7 +344,7 @@ def test_memory_store_object_with_custom_property_in_bundle(mem_store): def test_memory_store_custom_object(mem_store): @CustomObject( - 'x-new-obj', [ + 'x-new-obj-3', [ ('property1', properties.StringProperty(required=True)), ], ) diff --git a/stix2/test/v21/test_datastore_taxii.py b/stix2/test/v21/test_datastore_taxii.py index 528546a..28a7368 100644 --- a/stix2/test/v21/test_datastore_taxii.py +++ b/stix2/test/v21/test_datastore_taxii.py @@ -4,11 +4,13 @@ from medallion.filters.basic_filter import BasicFilter import pytest from requests.models import Response import six -from taxii2client import Collection, _filter_kwargs_to_query_params +from taxii2client.common import _filter_kwargs_to_query_params +from taxii2client.v21 import Collection import stix2 from stix2.datastore import DataSourceError from stix2.datastore.filters import Filter +from stix2.utils import get_timestamp COLLECTION_URL = 'https://example.com/api1/collections/91a7b528-80eb-42ed-a74d-c6fbd5a26116/' @@ -21,6 +23,7 @@ class MockTAXIICollectionEndpoint(Collection): url, collection_info=collection_info, ) self.objects = [] + self.manifests = [] def add_objects(self, bundle): self._verify_can_write() @@ -28,6 +31,14 @@ class MockTAXIICollectionEndpoint(Collection): bundle = json.loads(bundle, encoding='utf-8') for object in bundle.get("objects", []): self.objects.append(object) + self.manifests.append( + { + "date_added": get_timestamp(), + "id": object["id"], + "media_type": "application/stix+json;version=2.1", + "version": object.get("modified", object.get("created", get_timestamp())), + }, + ) def get_objects(self, **filter_kwargs): self._verify_can_read() @@ -37,8 +48,9 @@ class MockTAXIICollectionEndpoint(Collection): objs = full_filter.process_filter( self.objects, ("id", "type", "version"), - [], - ) + self.manifests, + 100, + )[0] if objs: return stix2.v21.Bundle(objects=objs) else: @@ -58,8 +70,9 @@ class MockTAXIICollectionEndpoint(Collection): filtered_objects = full_filter.process_filter( objects, ("version",), - [], - ) + self.manifests, + 100, + )[0] else: filtered_objects = [] if filtered_objects: @@ -71,7 +84,7 @@ class MockTAXIICollectionEndpoint(Collection): @pytest.fixture -def collection(stix_objs1): +def collection(stix_objs1, stix_objs1_manifests): mock = MockTAXIICollectionEndpoint( COLLECTION_URL, { "id": "91a7b528-80eb-42ed-a74d-c6fbd5a26116", @@ -86,11 +99,12 @@ def collection(stix_objs1): ) mock.objects.extend(stix_objs1) + mock.manifests.extend(stix_objs1_manifests) return mock @pytest.fixture -def collection_no_rw_access(stix_objs1): +def collection_no_rw_access(stix_objs1, stix_objs1_manifests): mock = MockTAXIICollectionEndpoint( COLLECTION_URL, { "id": "91a7b528-80eb-42ed-a74d-c6fbd5a26116", @@ -105,6 +119,7 @@ def collection_no_rw_access(stix_objs1): ) mock.objects.extend(stix_objs1) + mock.manifests.extend(stix_objs1_manifests) return mock diff --git a/stix2/test/v21/test_markings.py b/stix2/test/v21/test_markings.py index a2fca51..b0a0d09 100644 --- a/stix2/test/v21/test_markings.py +++ b/stix2/test/v21/test_markings.py @@ -277,14 +277,12 @@ def test_not_registered_marking_raises_exception(): def test_marking_wrong_type_construction(): - with pytest.raises(ValueError) as excinfo: + with pytest.raises(ValueError): # Test passing wrong type for properties. @stix2.v21.CustomMarking('x-new-marking-type2', ("a", "b")) class NewObject3(object): pass - assert str(excinfo.value) == "Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]" - def test_campaign_add_markings(): campaign = stix2.v21.Campaign( diff --git a/stix2/test/v21/test_parsing.py b/stix2/test/v21/test_parsing.py new file mode 100644 index 0000000..1d930e6 --- /dev/null +++ b/stix2/test/v21/test_parsing.py @@ -0,0 +1,89 @@ +import pytest + +import stix2 +from stix2 import exceptions, parsing + +BUNDLE = { + "type": "bundle", + "id": "bundle--00000000-0000-4000-8000-000000000007", + "objects": [ + { + "type": "indicator", + "spec_version": "2.1", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", + "pattern_type": "stix", + "valid_from": "2017-01-01T12:34:56Z", + "indicator_types": [ + "malicious-activity", + ], + }, + { + "type": "malware", + "spec_version": "2.1", + "id": "malware--00000000-0000-4000-8000-000000000003", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "name": "Cryptolocker", + "malware_types": [ + "ransomware", + ], + "is_family": False, + }, + { + "type": "relationship", + "spec_version": "2.1", + "id": "relationship--00000000-0000-4000-8000-000000000005", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "relationship_type": "indicates", + "source_ref": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "target_ref": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e", + }, + ], +} + + +def test_dict_to_stix2_bundle_with_version(): + with pytest.raises(exceptions.InvalidValueError) as excinfo: + parsing.dict_to_stix2(BUNDLE, version='2.0') + + msg = "Invalid value for Bundle 'objects': Spec version 2.0 bundles don't yet support containing objects of a different spec version." + assert str(excinfo.value) == msg + + +def test_parse_observable_with_version(): + observable = {"type": "file", "name": "foo.exe"} + obs_obj = parsing.parse_observable(observable, version='2.1') + v = 'v21' + + assert v in str(obs_obj.__class__) + + +@pytest.mark.xfail(reason="The default version is not 2.1", condition=stix2.DEFAULT_VERSION != "2.1") +def test_parse_observable_with_no_version(): + observable = {"type": "file", "name": "foo.exe"} + obs_obj = parsing.parse_observable(observable) + v = 'v21' + + assert v in str(obs_obj.__class__) + + +def test_register_marking_with_version(): + parsing._register_marking(stix2.v21.TLP_WHITE.__class__, version='2.1') + v = 'v21' + + assert stix2.v21.TLP_WHITE.definition._type in parsing.STIX2_OBJ_MAPS[v]['markings'] + assert v in str(stix2.v21.TLP_WHITE.__class__) + + +@pytest.mark.xfail(reason="The default version is not 2.1", condition=stix2.DEFAULT_VERSION != "2.1") +def test_register_marking_with_no_version(): + # Uses default version (2.0 in this case) + parsing._register_marking(stix2.v21.TLP_WHITE.__class__) + v = 'v21' + + assert stix2.v21.TLP_WHITE.definition._type in parsing.STIX2_OBJ_MAPS[v]['markings'] + assert v in str(stix2.v21.TLP_WHITE.__class__) diff --git a/stix2/test/v21/test_pattern_expressions.py b/stix2/test/v21/test_pattern_expressions.py index 0c298f8..198edac 100644 --- a/stix2/test/v21/test_pattern_expressions.py +++ b/stix2/test/v21/test_pattern_expressions.py @@ -175,20 +175,34 @@ def test_greater_than(): assert str(exp) == "[file:extensions.'windows-pebinary-ext'.sections[*].entropy > 7.0]" +def test_parsing_greater_than(): + patt_obj = create_pattern_object("[file:extensions.'windows-pebinary-ext'.sections[*].entropy > 7.478901]", version="2.1") + assert str(patt_obj) == "[file:extensions.'windows-pebinary-ext'.sections[*].entropy > 7.478901]" + + def test_less_than(): exp = stix2.LessThanComparisonExpression("file:size", 1024) assert str(exp) == "file:size < 1024" +def test_parsing_less_than(): + patt_obj = create_pattern_object("[file:size < 1024]", version="2.1") + assert str(patt_obj) == "[file:size < 1024]" + + def test_greater_than_or_equal(): exp = stix2.GreaterThanEqualComparisonExpression( "file:size", 1024, ) - assert str(exp) == "file:size >= 1024" +def test_parsing_greater_than_or_equal(): + patt_obj = create_pattern_object("[file:size >= 1024]", version="2.1") + assert str(patt_obj) == "[file:size >= 1024]" + + def test_less_than_or_equal(): exp = stix2.LessThanEqualComparisonExpression( "file:size", @@ -197,6 +211,36 @@ def test_less_than_or_equal(): assert str(exp) == "file:size <= 1024" +def test_parsing_less_than_or_equal(): + patt_obj = create_pattern_object("[file:size <= 1024]", version="2.1") + assert str(patt_obj) == "[file:size <= 1024]" + + +def test_parsing_issubset(): + patt_obj = create_pattern_object("[network-traffic:dst_ref.value ISSUBSET '2001:0db8:dead:beef:0000:0000:0000:0000/64']", version="2.1") + assert str(patt_obj) == "[network-traffic:dst_ref.value ISSUBSET '2001:0db8:dead:beef:0000:0000:0000:0000/64']" + + +def test_parsing_issuperset(): + patt_obj = create_pattern_object("[network-traffic:dst_ref.value ISSUPERSET '2001:0db8:dead:beef:0000:0000:0000:0000/64']", version="2.1") + assert str(patt_obj) == "[network-traffic:dst_ref.value ISSUPERSET '2001:0db8:dead:beef:0000:0000:0000:0000/64']" + + +def test_parsing_like(): + patt_obj = create_pattern_object("[directory:path LIKE 'C:\\\\Windows\\\\%\\\\foo']", version="2.1") + assert str(patt_obj) == "[directory:path LIKE 'C:\\\\Windows\\\\%\\\\foo']" + + +def test_parsing_match(): + patt_obj = create_pattern_object("[process:command_line MATCHES '^.+>-add GlobalSign.cer -c -s -r localMachine Root$'] FOLLOWEDBY [process:command_line MATCHES '^.+>-add GlobalSign.cer -c -s -r localMachineTrustedPublisher$'] WITHIN 300 SECONDS", version="2.1") # noqa + assert str(patt_obj) == "[process:command_line MATCHES '^.+>-add GlobalSign.cer -c -s -r localMachine Root$'] FOLLOWEDBY [process:command_line MATCHES '^.+>-add GlobalSign.cer -c -s -r localMachineTrustedPublisher$'] WITHIN 300 SECONDS" # noqa + + +def test_parsing_followed_by(): + patt_obj = create_pattern_object("([file:hashes.MD5 = '79054025255fb1a26e4bc422aef54eb4'] FOLLOWEDBY [windows-registry-key:key = 'HKEY_LOCAL_MACHINE\\\\foo\\\\bar']) WITHIN 300 SECONDS", version="2.1") # noqa + assert str(patt_obj) == "([file:hashes.MD5 = '79054025255fb1a26e4bc422aef54eb4'] FOLLOWEDBY [windows-registry-key:key = 'HKEY_LOCAL_MACHINE\\\\foo\\\\bar']) WITHIN 300 SECONDS" # noqa + + def test_not(): exp = stix2.LessThanComparisonExpression( "file:size", @@ -257,6 +301,67 @@ def test_and_observable_expression(): assert str(exp) == "[user-account:account_type = 'unix' AND user-account:user_id = '1007' AND user-account:account_login = 'Peter'] AND [user-account:account_type = 'unix' AND user-account:user_id = '1008' AND user-account:account_login = 'Paul'] AND [user-account:account_type = 'unix' AND user-account:user_id = '1009' AND user-account:account_login = 'Mary']" # noqa +def test_parsing_and_observable_expression(): + exp = create_pattern_object("[user-account:account_type = 'unix' AND user-account:user_id = '1007' AND user-account:account_login = 'Peter'] AND [user-account:account_type = 'unix' AND user-account:user_id = '1008' AND user-account:account_login = 'Paul']", version="2.1") # noqa + assert str(exp) == "[user-account:account_type = 'unix' AND user-account:user_id = '1007' AND user-account:account_login = 'Peter'] AND [user-account:account_type = 'unix' AND user-account:user_id = '1008' AND user-account:account_login = 'Paul']" # noqa + + +def test_or_observable_expression(): + exp1 = stix2.AndBooleanExpression([ + stix2.EqualityComparisonExpression( + "user-account:account_type", + "unix", + ), + stix2.EqualityComparisonExpression( + "user-account:user_id", + stix2.StringConstant("1007"), + ), + stix2.EqualityComparisonExpression( + "user-account:account_login", + "Peter", + ), + ]) + exp2 = stix2.AndBooleanExpression([ + stix2.EqualityComparisonExpression( + "user-account:account_type", + "unix", + ), + stix2.EqualityComparisonExpression( + "user-account:user_id", + stix2.StringConstant("1008"), + ), + stix2.EqualityComparisonExpression( + "user-account:account_login", + "Paul", + ), + ]) + exp3 = stix2.AndBooleanExpression([ + stix2.EqualityComparisonExpression( + "user-account:account_type", + "unix", + ), + stix2.EqualityComparisonExpression( + "user-account:user_id", + stix2.StringConstant("1009"), + ), + stix2.EqualityComparisonExpression( + "user-account:account_login", + "Mary", + ), + ]) + exp = stix2.OrObservationExpression([ + stix2.ObservationExpression(exp1), + stix2.ObservationExpression(exp2), + stix2.ObservationExpression(exp3), + ]) + assert str(exp) == "[user-account:account_type = 'unix' AND user-account:user_id = '1007' AND user-account:account_login = 'Peter'] OR [user-account:account_type = 'unix' AND user-account:user_id = '1008' AND user-account:account_login = 'Paul'] OR [user-account:account_type = 'unix' AND user-account:user_id = '1009' AND user-account:account_login = 'Mary']" # noqa + + +def test_parsing_or_observable_expression(): + exp = create_pattern_object("[user-account:account_type = 'unix' AND user-account:user_id = '1007' AND user-account:account_login = 'Peter'] OR [user-account:account_type = 'unix' AND user-account:user_id = '1008' AND user-account:account_login = 'Paul']", version="2.1") # noqa + assert str(exp) == "[user-account:account_type = 'unix' AND user-account:user_id = '1007' AND user-account:account_login = 'Peter'] OR [user-account:account_type = 'unix' AND user-account:user_id = '1008' AND user-account:account_login = 'Paul']" # noqa + + def test_invalid_and_observable_expression(): with pytest.raises(ValueError): stix2.AndBooleanExpression([ @@ -286,6 +391,11 @@ def test_hex(): assert str(exp) == "[file:mime_type = 'image/bmp' AND file:magic_number_hex = h'ffd8']" +def test_parsing_hex(): + patt_obj = create_pattern_object("[file:magic_number_hex = h'ffd8']", version="2.1") + assert str(patt_obj) == "[file:magic_number_hex = h'ffd8']" + + def test_multiple_qualifiers(): exp_and = stix2.AndBooleanExpression([ stix2.EqualityComparisonExpression( @@ -334,6 +444,11 @@ def test_binary(): assert str(exp) == "artifact:payload_bin = b'dGhpcyBpcyBhIHRlc3Q='" +def test_parsing_binary(): + patt_obj = create_pattern_object("[artifact:payload_bin = b'dGhpcyBpcyBhIHRlc3Q=']", version="2.1") + assert str(patt_obj) == "[artifact:payload_bin = b'dGhpcyBpcyBhIHRlc3Q=']" + + def test_list(): exp = stix2.InComparisonExpression( "process:name", @@ -495,29 +610,45 @@ def test_make_constant_already_a_constant(): def test_parsing_comparison_expression(): - patt_obj = create_pattern_object("[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f']") + patt_obj = create_pattern_object("[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f']", version="2.1") assert str(patt_obj) == "[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f']" -def test_parsing_qualified_expression(): +def test_parsing_repeat_and_within_qualified_expression(): patt_obj = create_pattern_object( "[network-traffic:dst_ref.type = 'domain-name' AND network-traffic:dst_ref.value = 'example.com'] REPEATS 5 TIMES WITHIN 1800 SECONDS", + version="2.1", ) assert str( patt_obj, ) == "[network-traffic:dst_ref.type = 'domain-name' AND network-traffic:dst_ref.value = 'example.com'] REPEATS 5 TIMES WITHIN 1800 SECONDS" +def test_parsing_start_stop_qualified_expression(): + patt_obj = create_pattern_object( + "[network-traffic:dst_ref.type = 'domain-name' AND network-traffic:dst_ref.value = 'example.com'] START t'2016-06-01T00:00:00Z' STOP t'2017-03-12T08:30:00Z'", # noqa + version="2.1", + ) + assert str( + patt_obj, + ) == "[network-traffic:dst_ref.type = 'domain-name' AND network-traffic:dst_ref.value = 'example.com'] START t'2016-06-01T00:00:00Z' STOP t'2017-03-12T08:30:00Z'" # noqa + + def test_list_constant(): - patt_obj = create_pattern_object("[network-traffic:src_ref.value IN ('10.0.0.0', '10.0.0.1', '10.0.0.2')]") + patt_obj = create_pattern_object("[network-traffic:src_ref.value IN ('10.0.0.0', '10.0.0.1', '10.0.0.2')]", version="2.1") assert str(patt_obj) == "[network-traffic:src_ref.value IN ('10.0.0.0', '10.0.0.1', '10.0.0.2')]" +def test_parsing_boolean(): + patt_obj = create_pattern_object("[network-traffic:is_active = true]", version="2.1") + assert str(patt_obj) == "[network-traffic:is_active = true]" + + def test_parsing_multiple_slashes_quotes(): - patt_obj = create_pattern_object("[ file:name = 'weird_name\\'' ]") + patt_obj = create_pattern_object("[ file:name = 'weird_name\\'' ]", version="2.1") assert str(patt_obj) == "[file:name = 'weird_name\\'']" def test_parse_error(): with pytest.raises(ParseException): - create_pattern_object("[ file: name = 'weirdname]") + create_pattern_object("[ file: name = 'weirdname]", version="2.1") diff --git a/stix2/test/v21/test_properties.py b/stix2/test/v21/test_properties.py index 50bce17..31dd941 100644 --- a/stix2/test/v21/test_properties.py +++ b/stix2/test/v21/test_properties.py @@ -404,7 +404,7 @@ def test_dictionary_property_invalid(d): def test_property_list_of_dictionary(): @stix2.v21.CustomObject( - 'x-new-obj', [ + 'x-new-obj-4', [ ('property1', ListProperty(DictionaryProperty(spec_version='2.1'), required=True)), ], ) diff --git a/stix2/test/v21/test_timestamp_precision.py b/stix2/test/v21/test_timestamp_precision.py new file mode 100644 index 0000000..8cb9735 --- /dev/null +++ b/stix2/test/v21/test_timestamp_precision.py @@ -0,0 +1,164 @@ +import datetime +import sys + +import pytest + +import stix2 +from stix2.utils import ( + Precision, PrecisionConstraint, STIXdatetime, _to_enum, format_datetime, + parse_into_datetime, +) + +_DT = datetime.datetime.utcnow() +# intentionally omit microseconds from the following. We add it in as +# needed for each test. +_DT_STR = _DT.strftime("%Y-%m-%dT%H:%M:%S") + + +@pytest.mark.parametrize( + "value, enum_type, enum_default, enum_expected", [ + ("second", Precision, None, Precision.SECOND), + ( + "eXaCt", PrecisionConstraint, PrecisionConstraint.MIN, + PrecisionConstraint.EXACT, + ), + (None, Precision, Precision.MILLISECOND, Precision.MILLISECOND), + (Precision.ANY, Precision, None, Precision.ANY), + ], +) +def test_to_enum(value, enum_type, enum_default, enum_expected): + result = _to_enum(value, enum_type, enum_default) + assert result == enum_expected + + +@pytest.mark.parametrize( + "value, err_type", [ + ("foo", KeyError), + (1, TypeError), + (PrecisionConstraint.EXACT, TypeError), + (None, TypeError), + ], +) +def test_to_enum_errors(value, err_type): + with pytest.raises(err_type): + _to_enum(value, Precision) + + +@pytest.mark.xfail( + sys.version_info[:2] == (3, 6), strict=True, + reason="https://bugs.python.org/issue32404", +) +def test_stix_datetime_now(): + dt = STIXdatetime.utcnow() + assert dt.precision is Precision.ANY + assert dt.precision_constraint is PrecisionConstraint.EXACT + + +def test_stix_datetime(): + dt = datetime.datetime.utcnow() + + sdt = STIXdatetime(dt, precision=Precision.SECOND) + assert sdt.precision is Precision.SECOND + assert sdt == dt + + sdt = STIXdatetime( + dt, + precision_constraint=PrecisionConstraint.EXACT, + ) + assert sdt.precision_constraint is PrecisionConstraint.EXACT + assert sdt == dt + + +@pytest.mark.parametrize( + "us, precision, precision_constraint, expected_truncated_us", [ + (123456, Precision.ANY, PrecisionConstraint.EXACT, 123456), + (123456, Precision.SECOND, PrecisionConstraint.EXACT, 0), + (123456, Precision.SECOND, PrecisionConstraint.MIN, 123456), + (123456, Precision.MILLISECOND, PrecisionConstraint.EXACT, 123000), + (123456, Precision.MILLISECOND, PrecisionConstraint.MIN, 123456), + (1234, Precision.MILLISECOND, PrecisionConstraint.EXACT, 1000), + (123, Precision.MILLISECOND, PrecisionConstraint.EXACT, 0), + ], +) +def test_parse_datetime( + us, precision, precision_constraint, expected_truncated_us, +): + + # complete the datetime string with microseconds + dt_us_str = "{}.{:06d}Z".format(_DT_STR, us) + + sdt = parse_into_datetime( + dt_us_str, + precision=precision, + precision_constraint=precision_constraint, + ) + + assert sdt.precision is precision + assert sdt.precision_constraint is precision_constraint + assert sdt.microsecond == expected_truncated_us + + +@pytest.mark.parametrize( + "us, precision, precision_constraint, expected_us_str", [ + (123456, Precision.ANY, PrecisionConstraint.EXACT, ".123456"), + (123456, Precision.SECOND, PrecisionConstraint.EXACT, ""), + (123456, Precision.SECOND, PrecisionConstraint.MIN, ".123456"), + (123456, Precision.MILLISECOND, PrecisionConstraint.EXACT, ".123"), + (123456, Precision.MILLISECOND, PrecisionConstraint.MIN, ".123456"), + (0, Precision.SECOND, PrecisionConstraint.MIN, ""), + (0, Precision.MILLISECOND, PrecisionConstraint.MIN, ".000"), + (0, Precision.MILLISECOND, PrecisionConstraint.EXACT, ".000"), + (1000, Precision.MILLISECOND, PrecisionConstraint.EXACT, ".001"), + (10000, Precision.MILLISECOND, PrecisionConstraint.EXACT, ".010"), + (100000, Precision.MILLISECOND, PrecisionConstraint.EXACT, ".100"), + (1000, Precision.ANY, PrecisionConstraint.EXACT, ".001"), + (10000, Precision.ANY, PrecisionConstraint.EXACT, ".01"), + (100000, Precision.ANY, PrecisionConstraint.EXACT, ".1"), + (1001, Precision.MILLISECOND, PrecisionConstraint.MIN, ".001001"), + (10010, Precision.MILLISECOND, PrecisionConstraint.MIN, ".01001"), + (100100, Precision.MILLISECOND, PrecisionConstraint.MIN, ".1001"), + ], +) +def test_format_datetime(us, precision, precision_constraint, expected_us_str): + + dt = _DT.replace(microsecond=us) + expected_dt_str = "{}{}Z".format(_DT_STR, expected_us_str) + + sdt = STIXdatetime( + dt, + precision=precision, + precision_constraint=precision_constraint, + ) + s = format_datetime(sdt) + assert s == expected_dt_str + + +def test_sdo_extra_precision(): + # add extra precision for "modified", ensure it's not lost + identity_dict = { + "type": "identity", + "id": "identity--4a457eeb-6639-4aa3-be81-5930a3000c39", + "created": "2015-12-21T19:59:11.000Z", + "modified": "2015-12-21T19:59:11.0001Z", + "name": "John Smith", + "identity_class": "individual", + "spec_version": "2.1", + } + + identity_obj = stix2.parse(identity_dict) + assert identity_obj.modified.microsecond == 100 + assert identity_obj.modified.precision is Precision.MILLISECOND + assert identity_obj.modified.precision_constraint is PrecisionConstraint.MIN + + identity_str = identity_obj.serialize(pretty=True) + + # ensure precision is retained in JSON + assert identity_str == """{ + "type": "identity", + "spec_version": "2.1", + "id": "identity--4a457eeb-6639-4aa3-be81-5930a3000c39", + "created": "2015-12-21T19:59:11.000Z", + "modified": "2015-12-21T19:59:11.0001Z", + "name": "John Smith", + "identity_class": "individual" +}""" diff --git a/stix2/test/v21/test_versioning.py b/stix2/test/v21/test_versioning.py index c46183c..bee0c07 100644 --- a/stix2/test/v21/test_versioning.py +++ b/stix2/test/v21/test_versioning.py @@ -1,6 +1,9 @@ +import datetime + import pytest import stix2 +import stix2.utils from .constants import CAMPAIGN_MORE_KWARGS @@ -236,8 +239,7 @@ def test_remove_custom_stix_property(): mal_nc = stix2.utils.remove_custom_stix(mal) assert "x_custom" not in mal_nc - assert (stix2.utils.parse_into_datetime(mal["modified"], precision="millisecond") < - stix2.utils.parse_into_datetime(mal_nc["modified"], precision="millisecond")) + assert mal["modified"] < mal_nc["modified"] def test_remove_custom_stix_object(): @@ -264,3 +266,33 @@ def test_remove_custom_stix_no_custom(): assert len(campaign_v1.keys()) == len(campaign_v2.keys()) assert campaign_v1.id == campaign_v2.id assert campaign_v1.description == campaign_v2.description + + +@pytest.mark.parametrize( + "old, candidate_new, expected_new, use_stix21", [ + ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.001Z", "1999-08-15T00:19:07.001Z", False), + ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.001Z", False), + ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:06.000Z", "1999-08-15T00:19:07.001Z", False), + ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:06.999Z", "1999-08-15T00:19:07.001Z", False), + ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.0001Z", "1999-08-15T00:19:07.001Z", False), + ("1999-08-15T00:19:07.999Z", "1999-08-15T00:19:07.9999Z", "1999-08-15T00:19:08.000Z", False), + + ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.001Z", "1999-08-15T00:19:07.001Z", True), + ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.000001Z", True), + ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:06.000Z", "1999-08-15T00:19:07.000001Z", True), + ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:06.999999Z", "1999-08-15T00:19:07.000001Z", True), + ("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.000001Z", "1999-08-15T00:19:07.000001Z", True), + ("1999-08-15T00:19:07.999Z", "1999-08-15T00:19:07.999999Z", "1999-08-15T00:19:07.999999Z", True), + ], +) +def test_fudge_modified(old, candidate_new, expected_new, use_stix21): + old_dt = datetime.datetime.strptime(old, "%Y-%m-%dT%H:%M:%S.%fZ") + candidate_new_dt = datetime.datetime.strptime( + candidate_new, "%Y-%m-%dT%H:%M:%S.%fZ", + ) + expected_new_dt = datetime.datetime.strptime( + expected_new, "%Y-%m-%dT%H:%M:%S.%fZ", + ) + + fudged = stix2.utils._fudge_modified(old_dt, candidate_new_dt, use_stix21) + assert fudged == expected_new_dt diff --git a/stix2/utils.py b/stix2/utils.py index 7b3b6cf..47e89a1 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -6,12 +6,15 @@ except ImportError: from collections import Mapping import copy import datetime as dt +import enum import json +import re from dateutil import parser import pytz +import six -import stix2.base +import stix2 from .exceptions import ( InvalidValueError, RevokeError, UnmodifiablePropertyError, @@ -25,13 +28,84 @@ NOW = object() # STIX object properties that cannot be modified STIX_UNMOD_PROPERTIES = ['created', 'created_by_ref', 'id', 'type'] -TYPE_REGEX = r'^\-?[a-z0-9]+(-[a-z0-9]+)*\-?$' -SCO21_EXT_REGEX = r'^\-?[a-z0-9]+(-[a-z0-9]+)*\-ext$' +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]+)*\-?$') +PREFIX_21_REGEX = re.compile(r'^[a-z].*') + + +class Precision(enum.Enum): + """ + Timestamp format precisions. + """ + # auto() wasn't introduced until Python 3.6. + ANY = 1 + SECOND = 2 + MILLISECOND = 3 + + +class PrecisionConstraint(enum.Enum): + """ + Timestamp precision constraints. These affect how the Precision + values are applied when formatting a timestamp. + + These constraints don't really make sense with the ANY precision, so they + have no effect in that case. + """ + EXACT = 1 # format must have exactly the given precision + MIN = 2 # format must have at least the given precision + # no need for a MAX constraint yet + + +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 + you use a string, it must name a valid enum value. This implementation is + case-insensitive. + + :param value: A value to be interpreted as an enum (string, Enum instance, + or None). If an Enum instance, it must be an instance of enum_type. + :param enum_type: The enum type which strings will be interpreted against + :param enum_default: The default enum to use if value is None. Must be + an instance of enum_type, or None. If None, you are disallowing a + default and requiring that value be non-None. + :return: An instance of enum_type + :raises TypeError: If value was neither an instance of enum_type, None, nor + a string + :raises KeyError: If value was a string which couldn't be interpreted as an + enum value from enum_type + """ + assert enum_default is None or isinstance(enum_default, enum_type) + + if not isinstance(value, enum_type): + if value is None and enum_default is not None: + value = enum_default + elif isinstance(value, six.string_types): + value = enum_type[value.upper()] + else: + raise TypeError("Not a valid {}: {}".format( + enum_type.__name__, value, + )) + + return value class STIXdatetime(dt.datetime): + """ + Bundle a datetime with some format-related metadata, so that JSON + serialization has the info it needs to produce compliant timestamps. + """ + def __new__(cls, *args, **kwargs): - precision = kwargs.pop('precision', None) + precision = _to_enum( + kwargs.pop("precision", Precision.ANY), + Precision, + ) + precision_constraint = _to_enum( + kwargs.pop("precision_constraint", PrecisionConstraint.EXACT), + PrecisionConstraint, + ) + if isinstance(args[0], dt.datetime): # Allow passing in a datetime object dttm = args[0] args = ( @@ -41,6 +115,7 @@ class STIXdatetime(dt.datetime): # self will be an instance of STIXdatetime, not dt.datetime self = dt.datetime.__new__(cls, *args, **kwargs) self.precision = precision + self.precision_constraint = precision_constraint return self def __repr__(self): @@ -90,7 +165,7 @@ def format_datetime(dttm): 2. Convert to UTC 3. Format in ISO format 4. Ensure correct precision - a. Add subsecond value if non-zero and precision not defined + a. Add subsecond value if warranted, according to precision settings 5. Add "Z" """ @@ -101,20 +176,74 @@ def format_datetime(dttm): else: zoned = dttm.astimezone(pytz.utc) ts = zoned.strftime('%Y-%m-%dT%H:%M:%S') - ms = zoned.strftime('%f') - precision = getattr(dttm, 'precision', None) - if precision == 'second': - pass # Already precise to the second - elif precision == 'millisecond': - ts = ts + '.' + ms[:3] - elif zoned.microsecond > 0: - ts = ts + '.' + ms.rstrip('0') - return ts + 'Z' + precision = getattr(dttm, 'precision', Precision.ANY) + precision_constraint = getattr( + dttm, 'precision_constraint', PrecisionConstraint.EXACT, + ) + + frac_seconds_str = "" + if precision == Precision.ANY: + # No need to truncate; ignore constraint + if zoned.microsecond: + frac_seconds_str = "{:06d}".format(zoned.microsecond).rstrip("0") + + elif precision == Precision.SECOND: + if precision_constraint == PrecisionConstraint.MIN: + # second precision, or better. Winds up being the same as ANY: + # just use all our digits + if zoned.microsecond: + frac_seconds_str = "{:06d}".format(zoned.microsecond)\ + .rstrip("0") + # exact: ignore microseconds entirely + + else: + # precision == millisecond + if precision_constraint == PrecisionConstraint.EXACT: + # can't rstrip() here or we may lose precision + frac_seconds_str = "{:06d}".format(zoned.microsecond)[:3] + + else: + # millisecond precision, or better. So we can rstrip() zeros, but + # only to a length of at least 3 digits (ljust() adds zeros back, + # if it stripped too far.) + frac_seconds_str = "{:06d}"\ + .format(zoned.microsecond)\ + .rstrip("0")\ + .ljust(3, "0") + + ts = "{}{}{}Z".format( + ts, + "." if frac_seconds_str else "", + frac_seconds_str, + ) + + return ts -def parse_into_datetime(value, precision=None): - """Parse a value into a valid STIX timestamp object. +def parse_into_datetime( + value, precision=Precision.ANY, + precision_constraint=PrecisionConstraint.EXACT, +): """ + Parse a value into a valid STIX timestamp object. Also, optionally adjust + precision of fractional seconds. This allows alignment with JSON + serialization requirements, and helps ensure we're not using extra + precision which would be lost upon JSON serialization. The precision + info will be embedded in the returned object, so that JSON serialization + will format it correctly. + + :param value: A datetime.datetime or datetime.date instance, or a string + :param precision: A precision value: either an instance of the Precision + enum, or a string naming one of the enum values (case-insensitive) + :param precision_constraint: A precision constraint value: either an + instance of the PrecisionConstraint enum, or a string naming one of + the enum values (case-insensitive) + :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) + if isinstance(value, dt.date): if hasattr(value, 'hour'): ts = value @@ -138,20 +267,23 @@ def parse_into_datetime(value, precision=None): ts = pytz.utc.localize(parsed) # Ensure correct precision - if not precision: - return STIXdatetime(ts, precision=precision) - ms = ts.microsecond - if precision == 'second': - ts = ts.replace(microsecond=0) - elif precision == 'millisecond': - ms_len = len(str(ms)) - if ms_len > 3: - # Truncate to millisecond precision - factor = 10 ** (ms_len - 3) - ts = ts.replace(microsecond=(ts.microsecond // factor) * factor) - else: + if precision == Precision.SECOND: + if precision_constraint == PrecisionConstraint.EXACT: ts = ts.replace(microsecond=0) - return STIXdatetime(ts, precision=precision) + # else, no need to modify fractional seconds + + elif precision == Precision.MILLISECOND: + if precision_constraint == PrecisionConstraint.EXACT: + us = (ts.microsecond // 1000) * 1000 + ts = ts.replace(microsecond=us) + # else: at least millisecond precision: the constraint will affect JSON + # formatting, but there's nothing we need to do here. + + # else, precision == Precision.ANY: nothing for us to do. + + return STIXdatetime( + ts, precision=precision, precision_constraint=precision_constraint, + ) def _get_dict(data): @@ -230,14 +362,12 @@ def find_property_index(obj, search_key, search_value): Returns: int: An index; -1 if the key and value aren't found """ - from .base import _STIXBase - # Special-case keys which are numbers-as-strings, e.g. for cyber-observable # mappings. Use the int value of the key as the index. if search_key.isdigit(): return int(search_key) - if isinstance(obj, _STIXBase): + if isinstance(obj, stix2.base._STIXBase): if search_key in obj and obj[search_key] == search_value: idx = _find(obj.object_properties(), search_key) else: @@ -256,6 +386,39 @@ def find_property_index(obj, search_key, search_value): return idx +def _fudge_modified(old_modified, new_modified, use_stix21): + """ + Ensures a new modified timestamp is newer than the old. When they are + too close together, new_modified must be pushed further ahead to ensure + it is distinct and later, after JSON serialization (which may mean it's + actually being pushed a little ways into the future). JSON serialization + can remove precision, which can cause distinct timestamps to accidentally + become equal, if we're not careful. + + :param old_modified: A previous "modified" timestamp, as a datetime object + :param new_modified: A candidate new "modified" timestamp, as a datetime + object + :param use_stix21: Whether to use STIX 2.1+ versioning timestamp precision + rules (boolean). This is important so that we are aware of how + timestamp precision will be truncated, so we know how close together + the timestamps can be, and how far ahead to potentially push the new + one. + :return: A suitable new "modified" timestamp. This may be different from + what was passed in, if it had to be pushed ahead. + """ + if use_stix21: + # 2.1+: we can use full precision + if new_modified <= old_modified: + new_modified = old_modified + dt.timedelta(microseconds=1) + else: + # 2.0: we must use millisecond precision + one_ms = dt.timedelta(milliseconds=1) + if new_modified - old_modified < one_ms: + new_modified = old_modified + one_ms + + return new_modified + + def new_version(data, **kwargs): """Create a new version of a STIX object, by modifying properties and updating the ``modified`` property. @@ -283,12 +446,32 @@ def new_version(data, **kwargs): if unchangable_properties: raise UnmodifiablePropertyError(unchangable_properties) + # Different versioning precision rules in STIX 2.0 vs 2.1, so we need + # to know which rules to apply. + is_21 = "spec_version" in data + precision_constraint = "min" if is_21 else "exact" + cls = type(data) if 'modified' not in kwargs: - kwargs['modified'] = get_timestamp() + old_modified = parse_into_datetime( + data["modified"], precision="millisecond", + precision_constraint=precision_constraint, + ) + + new_modified = get_timestamp() + new_modified = _fudge_modified(old_modified, new_modified, is_21) + + kwargs['modified'] = new_modified + elif 'modified' in data: - old_modified_property = parse_into_datetime(data.get('modified'), precision='millisecond') - new_modified_property = parse_into_datetime(kwargs['modified'], precision='millisecond') + old_modified_property = parse_into_datetime( + data.get('modified'), precision='millisecond', + precision_constraint=precision_constraint, + ) + new_modified_property = parse_into_datetime( + kwargs['modified'], precision='millisecond', + precision_constraint=precision_constraint, + ) if new_modified_property <= old_modified_property: raise InvalidValueError( cls, 'modified', @@ -378,11 +561,6 @@ def remove_custom_stix(stix_obj): new_obj = new_version(stix_obj, **(dict(props))) - while parse_into_datetime(new_obj['modified']) == parse_into_datetime(stix_obj['modified']): - # Prevents bug when fast computation allows multiple STIX object - # versions to be created in single unit of time - new_obj = new_version(stix_obj, **(dict(props))) - return new_obj else: diff --git a/stix2/v20/__init__.py b/stix2/v20/__init__.py index 4d0a98f..b77d46c 100644 --- a/stix2/v20/__init__.py +++ b/stix2/v20/__init__.py @@ -14,6 +14,9 @@ # flake8: noqa +from .base import ( + _DomainObject, _Extension, _Observable, _RelationshipObject, _STIXBase20, +) from .bundle import Bundle from .common import ( TLP_AMBER, TLP_GREEN, TLP_RED, TLP_WHITE, CustomMarking, ExternalReference, diff --git a/stix2/v20/base.py b/stix2/v20/base.py new file mode 100644 index 0000000..b5437ca --- /dev/null +++ b/stix2/v20/base.py @@ -0,0 +1,25 @@ +"""Base classes for STIX 2.0 type definitions.""" + +from ..base import ( + _DomainObject, _Extension, _Observable, _RelationshipObject, _STIXBase, +) + + +class _STIXBase20(_STIXBase): + pass + + +class _Observable(_Observable, _STIXBase20): + pass + + +class _Extension(_Extension, _STIXBase20): + pass + + +class _DomainObject(_DomainObject, _STIXBase20): + pass + + +class _RelationshipObject(_RelationshipObject, _STIXBase20): + pass diff --git a/stix2/v20/bundle.py b/stix2/v20/bundle.py index 22cc26d..7d97211 100644 --- a/stix2/v20/bundle.py +++ b/stix2/v20/bundle.py @@ -2,20 +2,20 @@ from collections import OrderedDict -from ..base import _STIXBase from ..properties import ( IDProperty, ListProperty, STIXObjectProperty, StringProperty, TypeProperty, ) +from .base import _STIXBase20 -class Bundle(_STIXBase): +class Bundle(_STIXBase20): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ _type = 'bundle' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('id', IDProperty(_type, spec_version='2.0')), # Not technically correct: STIX 2.0 spec doesn't say spec_version must # have this value, but it's all we support for now. diff --git a/stix2/v20/common.py b/stix2/v20/common.py index 00fd593..e4f69aa 100644 --- a/stix2/v20/common.py +++ b/stix2/v20/common.py @@ -5,7 +5,6 @@ import copy import six -from ..base import _STIXBase from ..custom import _custom_marking_builder from ..markings import _MarkingsMixin from ..markings.utils import check_tlp_marking @@ -14,6 +13,7 @@ from ..properties import ( SelectorProperty, StringProperty, TimestampProperty, TypeProperty, ) from ..utils import NOW, _get_dict +from .base import _STIXBase20 def _should_set_millisecond(cr, marking_type): @@ -31,7 +31,7 @@ def _should_set_millisecond(cr, marking_type): return False -class ExternalReference(_STIXBase): +class ExternalReference(_STIXBase20): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ @@ -49,7 +49,7 @@ class ExternalReference(_STIXBase): self._check_at_least_one_property(['description', 'external_id', 'url']) -class KillChainPhase(_STIXBase): +class KillChainPhase(_STIXBase20): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ @@ -60,7 +60,7 @@ class KillChainPhase(_STIXBase): ]) -class GranularMarking(_STIXBase): +class GranularMarking(_STIXBase20): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ @@ -71,7 +71,7 @@ class GranularMarking(_STIXBase): ]) -class TLPMarking(_STIXBase): +class TLPMarking(_STIXBase20): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ @@ -83,7 +83,7 @@ class TLPMarking(_STIXBase): ]) -class StatementMarking(_STIXBase): +class StatementMarking(_STIXBase20): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ @@ -113,14 +113,14 @@ class MarkingProperty(Property): raise ValueError("must be a Statement, TLP Marking or a registered marking.") -class MarkingDefinition(_STIXBase, _MarkingsMixin): +class MarkingDefinition(_STIXBase20, _MarkingsMixin): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ _type = 'marking-definition' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('id', IDProperty(_type, spec_version='2.0')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.0')), ('created', TimestampProperty(default=lambda: NOW)), @@ -188,7 +188,7 @@ def CustomMarking(type='x-custom-marking', properties=None): """ def wrapper(cls): - return _custom_marking_builder(cls, type, properties, '2.0') + return _custom_marking_builder(cls, type, properties, '2.0', _STIXBase20) return wrapper diff --git a/stix2/v20/observables.py b/stix2/v20/observables.py index 81e2c48..3491c47 100644 --- a/stix2/v20/observables.py +++ b/stix2/v20/observables.py @@ -1,14 +1,13 @@ """STIX 2.0 Cyber Observable Objects. Embedded observable object types, such as Email MIME Component, which is -embedded in Email Message objects, inherit from ``_STIXBase`` instead of -Observable and do not have a ``_type`` attribute. +embedded in Email Message objects, inherit from ``_STIXBase20`` instead of +_Observable and do not have a ``_type`` attribute. """ from collections import OrderedDict import itertools -from ..base import _Extension, _Observable, _STIXBase from ..custom import _custom_extension_builder, _custom_observable_builder from ..exceptions import AtLeastOnePropertyError, DependentPropertiesError from ..properties import ( @@ -17,6 +16,7 @@ from ..properties import ( HashesProperty, HexProperty, IntegerProperty, ListProperty, ObjectReferenceProperty, StringProperty, TimestampProperty, TypeProperty, ) +from .base import _Extension, _Observable, _STIXBase20 class Artifact(_Observable): @@ -26,7 +26,7 @@ class Artifact(_Observable): _type = 'artifact' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('mime_type', StringProperty()), ('payload_bin', BinaryProperty()), ('url', StringProperty()), @@ -47,7 +47,7 @@ class AutonomousSystem(_Observable): _type = 'autonomous-system' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('number', IntegerProperty(required=True)), ('name', StringProperty()), ('rir', StringProperty()), @@ -62,7 +62,7 @@ class Directory(_Observable): _type = 'directory' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('path', StringProperty(required=True)), ('path_enc', StringProperty()), # these are not the created/modified timestamps of the object itself @@ -81,7 +81,7 @@ class DomainName(_Observable): _type = 'domain-name' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('value', StringProperty(required=True)), ('resolves_to_refs', ListProperty(ObjectReferenceProperty(valid_types=['ipv4-addr', 'ipv6-addr', 'domain-name']))), ('extensions', ExtensionsProperty(spec_version="2.0", enclosing_type=_type)), @@ -95,7 +95,7 @@ class EmailAddress(_Observable): _type = 'email-addr' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('value', StringProperty(required=True)), ('display_name', StringProperty()), ('belongs_to_ref', ObjectReferenceProperty(valid_types='user-account')), @@ -103,7 +103,7 @@ class EmailAddress(_Observable): ]) -class EmailMIMEComponent(_STIXBase): +class EmailMIMEComponent(_STIXBase20): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ # noqa @@ -127,7 +127,7 @@ class EmailMessage(_Observable): _type = 'email-message' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('is_multipart', BooleanProperty(required=True)), ('date', TimestampProperty()), ('content_type', StringProperty()), @@ -166,7 +166,7 @@ class ArchiveExt(_Extension): ]) -class AlternateDataStream(_STIXBase): +class AlternateDataStream(_STIXBase20): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ # noqa @@ -220,7 +220,7 @@ class RasterImageExt(_Extension): ]) -class WindowsPEOptionalHeaderType(_STIXBase): +class WindowsPEOptionalHeaderType(_STIXBase20): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ # noqa @@ -264,7 +264,7 @@ class WindowsPEOptionalHeaderType(_STIXBase): self._check_at_least_one_property() -class WindowsPESection(_STIXBase): +class WindowsPESection(_STIXBase20): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ # noqa @@ -306,7 +306,7 @@ class File(_Observable): _type = 'file' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('hashes', HashesProperty()), ('size', IntegerProperty()), ('name', StringProperty()), @@ -339,7 +339,7 @@ class IPv4Address(_Observable): _type = 'ipv4-addr' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('value', StringProperty(required=True)), ('resolves_to_refs', ListProperty(ObjectReferenceProperty(valid_types='mac-addr'))), ('belongs_to_refs', ListProperty(ObjectReferenceProperty(valid_types='autonomous-system'))), @@ -354,7 +354,7 @@ class IPv6Address(_Observable): _type = 'ipv6-addr' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('value', StringProperty(required=True)), ('resolves_to_refs', ListProperty(ObjectReferenceProperty(valid_types='mac-addr'))), ('belongs_to_refs', ListProperty(ObjectReferenceProperty(valid_types='autonomous-system'))), @@ -369,7 +369,7 @@ class MACAddress(_Observable): _type = 'mac-addr' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('value', StringProperty(required=True)), ('extensions', ExtensionsProperty(spec_version="2.0", enclosing_type=_type)), ]) @@ -382,7 +382,7 @@ class Mutex(_Observable): _type = 'mutex' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('name', StringProperty(required=True)), ('extensions', ExtensionsProperty(spec_version="2.0", enclosing_type=_type)), ]) @@ -483,7 +483,7 @@ class NetworkTraffic(_Observable): _type = 'network-traffic' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('start', TimestampProperty()), ('end', TimestampProperty()), ('is_active', BooleanProperty()), @@ -575,7 +575,7 @@ class Process(_Observable): _type = 'process' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('is_hidden', BooleanProperty()), ('pid', IntegerProperty()), ('name', StringProperty()), @@ -615,7 +615,7 @@ class Software(_Observable): _type = 'software' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('name', StringProperty(required=True)), ('cpe', StringProperty()), ('languages', ListProperty(StringProperty)), @@ -632,7 +632,7 @@ class URL(_Observable): _type = 'url' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('value', StringProperty(required=True)), ('extensions', ExtensionsProperty(spec_version="2.0", enclosing_type=_type)), ]) @@ -659,7 +659,7 @@ class UserAccount(_Observable): _type = 'user-account' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('user_id', StringProperty(required=True)), ('account_login', StringProperty()), ('account_type', StringProperty()), # open vocab @@ -677,7 +677,7 @@ class UserAccount(_Observable): ]) -class WindowsRegistryValueType(_STIXBase): +class WindowsRegistryValueType(_STIXBase20): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ # noqa @@ -713,7 +713,7 @@ class WindowsRegistryKey(_Observable): _type = 'windows-registry-key' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('key', StringProperty(required=True)), ('values', ListProperty(EmbeddedObjectProperty(type=WindowsRegistryValueType))), # this is not the modified timestamps of the object itself @@ -724,7 +724,7 @@ class WindowsRegistryKey(_Observable): ]) -class X509V3ExtenstionsType(_STIXBase): +class X509V3ExtenstionsType(_STIXBase20): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ # noqa @@ -757,7 +757,7 @@ class X509Certificate(_Observable): _type = 'x509-certificate' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('is_self_signed', BooleanProperty()), ('hashes', HashesProperty()), ('version', StringProperty()), @@ -791,11 +791,11 @@ def CustomObservable(type='x-custom-observable', properties=None): """ def wrapper(cls): _properties = list(itertools.chain.from_iterable([ - [('type', TypeProperty(type))], + [('type', TypeProperty(type, spec_version='2.0'))], properties, [('extensions', ExtensionsProperty(spec_version="2.0", enclosing_type=type))], ])) - return _custom_observable_builder(cls, type, _properties, '2.0') + return _custom_observable_builder(cls, type, _properties, '2.0', _Observable) return wrapper @@ -803,5 +803,5 @@ def CustomExtension(observable=None, type='x-custom-observable-ext', properties= """Decorator for custom extensions to STIX Cyber Observables. """ def wrapper(cls): - return _custom_extension_builder(cls, observable, type, properties, '2.0') + return _custom_extension_builder(cls, observable, type, properties, '2.0', _Extension) return wrapper diff --git a/stix2/v20/sdo.py b/stix2/v20/sdo.py index b190449..bb69f26 100644 --- a/stix2/v20/sdo.py +++ b/stix2/v20/sdo.py @@ -5,7 +5,6 @@ import itertools from stix2patterns.validator import run_validator -from ..core import STIXDomainObject from ..custom import _custom_object_builder from ..exceptions import InvalidValueError from ..properties import ( @@ -14,17 +13,18 @@ from ..properties import ( TimestampProperty, TypeProperty, ) from ..utils import NOW +from .base import _DomainObject from .common import ExternalReference, GranularMarking, KillChainPhase -class AttackPattern(STIXDomainObject): +class AttackPattern(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ _type = 'attack-pattern' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('id', IDProperty(_type, spec_version='2.0')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.0')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), @@ -40,14 +40,14 @@ class AttackPattern(STIXDomainObject): ]) -class Campaign(STIXDomainObject): +class Campaign(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ _type = 'campaign' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('id', IDProperty(_type, spec_version='2.0')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.0')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), @@ -66,14 +66,14 @@ class Campaign(STIXDomainObject): ]) -class CourseOfAction(STIXDomainObject): +class CourseOfAction(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ _type = 'course-of-action' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('id', IDProperty(_type, spec_version='2.0')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.0')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), @@ -88,14 +88,14 @@ class CourseOfAction(STIXDomainObject): ]) -class Identity(STIXDomainObject): +class Identity(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ _type = 'identity' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('id', IDProperty(_type, spec_version='2.0')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.0')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), @@ -113,14 +113,14 @@ class Identity(STIXDomainObject): ]) -class Indicator(STIXDomainObject): +class Indicator(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ _type = 'indicator' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('id', IDProperty(_type, spec_version='2.0')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.0')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), @@ -144,14 +144,14 @@ class Indicator(STIXDomainObject): raise InvalidValueError(self.__class__, 'pattern', str(errors[0])) -class IntrusionSet(STIXDomainObject): +class IntrusionSet(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ _type = 'intrusion-set' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('id', IDProperty(_type, spec_version='2.0')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.0')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), @@ -173,14 +173,14 @@ class IntrusionSet(STIXDomainObject): ]) -class Malware(STIXDomainObject): +class Malware(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ _type = 'malware' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('id', IDProperty(_type, spec_version='2.0')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.0')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), @@ -196,14 +196,14 @@ class Malware(STIXDomainObject): ]) -class ObservedData(STIXDomainObject): +class ObservedData(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ _type = 'observed-data' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('id', IDProperty(_type, spec_version='2.0')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.0')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), @@ -226,14 +226,14 @@ class ObservedData(STIXDomainObject): super(ObservedData, self).__init__(*args, **kwargs) -class Report(STIXDomainObject): +class Report(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ _type = 'report' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('id', IDProperty(_type, spec_version='2.0')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.0')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), @@ -256,14 +256,14 @@ class Report(STIXDomainObject): super(Report, self).__init__(*args, **kwargs) -class ThreatActor(STIXDomainObject): +class ThreatActor(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ _type = 'threat-actor' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('id', IDProperty(_type, spec_version='2.0')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.0')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), @@ -286,14 +286,14 @@ class ThreatActor(STIXDomainObject): ]) -class Tool(STIXDomainObject): +class Tool(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ _type = 'tool' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('id', IDProperty(_type, spec_version='2.0')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.0')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), @@ -310,14 +310,14 @@ class Tool(STIXDomainObject): ]) -class Vulnerability(STIXDomainObject): +class Vulnerability(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ _type = 'vulnerability' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('id', IDProperty(_type, spec_version='2.0')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.0')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), @@ -364,7 +364,7 @@ def CustomObject(type='x-custom-type', properties=None): def wrapper(cls): _properties = list(itertools.chain.from_iterable([ [ - ('type', TypeProperty(type)), + ('type', TypeProperty(type, spec_version='2.0')), ('id', IDProperty(type, spec_version='2.0')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.0')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), @@ -380,5 +380,5 @@ def CustomObject(type='x-custom-type', properties=None): ], sorted([x for x in properties if x[0].startswith('x_')], key=lambda x: x[0]), ])) - return _custom_object_builder(cls, type, _properties, '2.0') + return _custom_object_builder(cls, type, _properties, '2.0', _DomainObject) return wrapper diff --git a/stix2/v20/sro.py b/stix2/v20/sro.py index 63f177b..688f3d7 100644 --- a/stix2/v20/sro.py +++ b/stix2/v20/sro.py @@ -2,16 +2,16 @@ from collections import OrderedDict -from ..core import STIXRelationshipObject from ..properties import ( BooleanProperty, IDProperty, IntegerProperty, ListProperty, ReferenceProperty, StringProperty, TimestampProperty, TypeProperty, ) from ..utils import NOW +from .base import _RelationshipObject from .common import ExternalReference, GranularMarking -class Relationship(STIXRelationshipObject): +class Relationship(_RelationshipObject): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ @@ -20,7 +20,7 @@ class Relationship(STIXRelationshipObject): _type = 'relationship' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('id', IDProperty(_type, spec_version='2.0')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.0')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), @@ -55,14 +55,14 @@ class Relationship(STIXRelationshipObject): super(Relationship, self).__init__(**kwargs) -class Sighting(STIXRelationshipObject): +class Sighting(_RelationshipObject): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ _type = 'sighting' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.0')), ('id', IDProperty(_type, spec_version='2.0')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.0')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), diff --git a/stix2/v21/__init__.py b/stix2/v21/__init__.py index b2451d2..eea61dd 100644 --- a/stix2/v21/__init__.py +++ b/stix2/v21/__init__.py @@ -14,6 +14,9 @@ # flake8: noqa +from .base import ( + _DomainObject, _Extension, _Observable, _RelationshipObject, _STIXBase21, +) from .bundle import Bundle from .common import ( TLP_AMBER, TLP_GREEN, TLP_RED, TLP_WHITE, CustomMarking, ExternalReference, diff --git a/stix2/v21/base.py b/stix2/v21/base.py new file mode 100644 index 0000000..8b5a5f1 --- /dev/null +++ b/stix2/v21/base.py @@ -0,0 +1,25 @@ +"""Base classes for STIX 2.1 type definitions.""" + +from ..base import ( + _DomainObject, _Extension, _Observable, _RelationshipObject, _STIXBase, +) + + +class _STIXBase21(_STIXBase): + pass + + +class _Observable(_Observable, _STIXBase21): + pass + + +class _Extension(_Extension, _STIXBase21): + pass + + +class _DomainObject(_DomainObject, _STIXBase21): + pass + + +class _RelationshipObject(_RelationshipObject, _STIXBase21): + pass diff --git a/stix2/v21/bundle.py b/stix2/v21/bundle.py index b4b8607..48025ca 100644 --- a/stix2/v21/bundle.py +++ b/stix2/v21/bundle.py @@ -2,20 +2,20 @@ from collections import OrderedDict -from ..base import _STIXBase from ..properties import ( IDProperty, ListProperty, STIXObjectProperty, TypeProperty, ) +from .base import _STIXBase21 -class Bundle(_STIXBase): +class Bundle(_STIXBase21): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'bundle' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('objects', ListProperty(STIXObjectProperty(spec_version='2.1'))), ]) diff --git a/stix2/v21/common.py b/stix2/v21/common.py index f5a50cb..c7a9441 100644 --- a/stix2/v21/common.py +++ b/stix2/v21/common.py @@ -2,7 +2,6 @@ from collections import OrderedDict -from ..base import _STIXBase from ..custom import _custom_marking_builder from ..exceptions import InvalidValueError from ..markings import _MarkingsMixin @@ -13,9 +12,10 @@ from ..properties import ( SelectorProperty, StringProperty, TimestampProperty, TypeProperty, ) from ..utils import NOW, _get_dict +from .base import _STIXBase21 -class ExternalReference(_STIXBase): +class ExternalReference(_STIXBase21): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ @@ -49,7 +49,7 @@ class ExternalReference(_STIXBase): ) -class KillChainPhase(_STIXBase): +class KillChainPhase(_STIXBase21): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ @@ -60,7 +60,7 @@ class KillChainPhase(_STIXBase): ]) -class GranularMarking(_STIXBase): +class GranularMarking(_STIXBase21): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ @@ -76,19 +76,19 @@ class GranularMarking(_STIXBase): self._check_at_least_one_property(['lang', 'marking_ref']) -class LanguageContent(_STIXBase): +class LanguageContent(_STIXBase21): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'language-content' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('object_ref', ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.1', required=True)), # TODO: 'object_modified' it MUST be an exact match for the modified time of the STIX Object (SRO or SDO) being referenced. ('object_modified', TimestampProperty(precision='millisecond')), @@ -103,7 +103,7 @@ class LanguageContent(_STIXBase): ]) -class TLPMarking(_STIXBase): +class TLPMarking(_STIXBase21): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ @@ -114,7 +114,7 @@ class TLPMarking(_STIXBase): ]) -class StatementMarking(_STIXBase): +class StatementMarking(_STIXBase21): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ @@ -144,18 +144,18 @@ class MarkingProperty(Property): raise ValueError("must be a Statement, TLP Marking or a registered marking.") -class MarkingDefinition(_STIXBase, _MarkingsMixin): +class MarkingDefinition(_STIXBase21, _MarkingsMixin): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'marking-definition' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type)), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('external_references', ListProperty(ExternalReference)), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), @@ -214,7 +214,7 @@ def CustomMarking(type='x-custom-marking', properties=None): """ def wrapper(cls): - return _custom_marking_builder(cls, type, properties, '2.1') + return _custom_marking_builder(cls, type, properties, '2.1', _STIXBase21) return wrapper diff --git a/stix2/v21/observables.py b/stix2/v21/observables.py index 89de0fc..d73c1cf 100644 --- a/stix2/v21/observables.py +++ b/stix2/v21/observables.py @@ -1,14 +1,13 @@ """STIX 2.1 Cyber Observable Objects. Embedded observable object types, such as Email MIME Component, which is -embedded in Email Message objects, inherit from ``_STIXBase`` instead of -Observable and do not have a ``_type`` attribute. +embedded in Email Message objects, inherit from ``_STIXBase21`` instead of +_Observable and do not have a ``_type`` attribute. """ from collections import OrderedDict import itertools -from ..base import _Extension, _Observable, _STIXBase from ..custom import _custom_extension_builder, _custom_observable_builder from ..exceptions import AtLeastOnePropertyError, DependentPropertiesError from ..properties import ( @@ -18,6 +17,7 @@ from ..properties import ( ObjectReferenceProperty, ReferenceProperty, StringProperty, TimestampProperty, TypeProperty, ) +from .base import _Extension, _Observable, _STIXBase21 from .common import GranularMarking @@ -28,7 +28,7 @@ class Artifact(_Observable): _type = 'artifact' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('mime_type', StringProperty()), ('payload_bin', BinaryProperty()), @@ -57,7 +57,7 @@ class AutonomousSystem(_Observable): _type = 'autonomous-system' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('number', IntegerProperty(required=True)), ('name', StringProperty()), @@ -78,7 +78,7 @@ class Directory(_Observable): _type = 'directory' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('path', StringProperty(required=True)), ('path_enc', StringProperty()), @@ -103,7 +103,7 @@ class DomainName(_Observable): _type = 'domain-name' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('value', StringProperty(required=True)), ('resolves_to_refs', ListProperty(ReferenceProperty(valid_types=['ipv4-addr', 'ipv6-addr', 'domain-name'], spec_version='2.1'))), @@ -123,7 +123,7 @@ class EmailAddress(_Observable): _type = 'email-addr' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('value', StringProperty(required=True)), ('display_name', StringProperty()), @@ -137,7 +137,7 @@ class EmailAddress(_Observable): _id_contributing_properties = ["value"] -class EmailMIMEComponent(_STIXBase): +class EmailMIMEComponent(_STIXBase21): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ @@ -161,7 +161,7 @@ class EmailMessage(_Observable): _type = 'email-message' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('is_multipart', BooleanProperty(required=True)), ('date', TimestampProperty()), @@ -206,7 +206,7 @@ class ArchiveExt(_Extension): ]) -class AlternateDataStream(_STIXBase): +class AlternateDataStream(_STIXBase21): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ @@ -259,7 +259,7 @@ class RasterImageExt(_Extension): ]) -class WindowsPEOptionalHeaderType(_STIXBase): +class WindowsPEOptionalHeaderType(_STIXBase21): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ @@ -303,7 +303,7 @@ class WindowsPEOptionalHeaderType(_STIXBase): self._check_at_least_one_property() -class WindowsPESection(_STIXBase): +class WindowsPESection(_STIXBase21): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ @@ -345,7 +345,7 @@ class File(_Observable): _type = 'file' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('hashes', HashesProperty(spec_version='2.1')), ('size', IntegerProperty(min=0)), @@ -380,7 +380,7 @@ class IPv4Address(_Observable): _type = 'ipv4-addr' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('value', StringProperty(required=True)), ('resolves_to_refs', ListProperty(ReferenceProperty(valid_types='mac-addr', spec_version='2.1'))), @@ -401,7 +401,7 @@ class IPv6Address(_Observable): _type = 'ipv6-addr' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('value', StringProperty(required=True)), ('resolves_to_refs', ListProperty(ReferenceProperty(valid_types='mac-addr', spec_version='2.1'))), @@ -422,7 +422,7 @@ class MACAddress(_Observable): _type = 'mac-addr' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('value', StringProperty(required=True)), ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), @@ -441,7 +441,7 @@ class Mutex(_Observable): _type = 'mutex' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('name', StringProperty(required=True)), ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), @@ -562,7 +562,7 @@ class NetworkTraffic(_Observable): _type = 'network-traffic' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('start', TimestampProperty()), ('end', TimestampProperty()), @@ -684,7 +684,7 @@ class Process(_Observable): _type = 'process' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('is_hidden', BooleanProperty()), ('pid', IntegerProperty()), @@ -728,7 +728,7 @@ class Software(_Observable): _type = 'software' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('name', StringProperty(required=True)), ('cpe', StringProperty()), @@ -752,7 +752,7 @@ class URL(_Observable): _type = 'url' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('value', StringProperty(required=True)), ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), @@ -785,7 +785,7 @@ class UserAccount(_Observable): _type = 'user-account' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('user_id', StringProperty()), ('credential', StringProperty()), @@ -810,7 +810,7 @@ class UserAccount(_Observable): _id_contributing_properties = ["account_type", "user_id", "account_login"] -class WindowsRegistryValueType(_STIXBase): +class WindowsRegistryValueType(_STIXBase21): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ @@ -846,7 +846,7 @@ class WindowsRegistryKey(_Observable): _type = 'windows-registry-key' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('key', StringProperty()), ('values', ListProperty(EmbeddedObjectProperty(type=WindowsRegistryValueType))), @@ -863,7 +863,7 @@ class WindowsRegistryKey(_Observable): _id_contributing_properties = ["key", "values"] -class X509V3ExtenstionsType(_STIXBase): +class X509V3ExtenstionsType(_STIXBase21): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ @@ -896,7 +896,7 @@ class X509Certificate(_Observable): _type = 'x509-certificate' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('is_self_signed', BooleanProperty()), ('hashes', HashesProperty(spec_version='2.1')), @@ -948,12 +948,12 @@ def CustomObservable(type='x-custom-observable', properties=None, id_contrib_pro """ def wrapper(cls): _properties = list(itertools.chain.from_iterable([ - [('type', TypeProperty(type))], + [('type', TypeProperty(type, spec_version='2.1'))], [('id', IDProperty(type, spec_version='2.1'))], properties, [('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=type))], ])) - return _custom_observable_builder(cls, type, _properties, '2.1', id_contrib_props) + return _custom_observable_builder(cls, type, _properties, '2.1', _Observable, id_contrib_props) return wrapper @@ -961,5 +961,5 @@ def CustomExtension(observable=None, type='x-custom-observable-ext', properties= """Decorator for custom extensions to STIX Cyber Observables. """ def wrapper(cls): - return _custom_extension_builder(cls, observable, type, properties, '2.1') + return _custom_extension_builder(cls, observable, type, properties, '2.1', _Extension) return wrapper diff --git a/stix2/v21/sdo.py b/stix2/v21/sdo.py index bc09d0e..80f2d40 100644 --- a/stix2/v21/sdo.py +++ b/stix2/v21/sdo.py @@ -7,7 +7,6 @@ import warnings from six.moves.urllib.parse import quote_plus from stix2patterns.validator import run_validator -from ..core import STIXDomainObject from ..custom import _custom_object_builder from ..exceptions import ( InvalidValueError, PropertyPresenceError, STIXDeprecationWarning, @@ -18,22 +17,23 @@ from ..properties import ( StringProperty, TimestampProperty, TypeProperty, ) from ..utils import NOW +from .base import _DomainObject from .common import ExternalReference, GranularMarking, KillChainPhase -class AttackPattern(STIXDomainObject): +class AttackPattern(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'attack-pattern' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('name', StringProperty(required=True)), ('description', StringProperty()), ('aliases', ListProperty(StringProperty)), @@ -48,19 +48,19 @@ class AttackPattern(STIXDomainObject): ]) -class Campaign(STIXDomainObject): +class Campaign(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'campaign' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('name', StringProperty(required=True)), ('description', StringProperty()), ('aliases', ListProperty(StringProperty)), @@ -87,19 +87,19 @@ class Campaign(STIXDomainObject): raise ValueError(msg.format(self)) -class CourseOfAction(STIXDomainObject): +class CourseOfAction(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'course-of-action' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('name', StringProperty(required=True)), ('description', StringProperty()), ('revoked', BooleanProperty(default=lambda: False)), @@ -112,18 +112,18 @@ class CourseOfAction(STIXDomainObject): ]) -class Grouping(STIXDomainObject): +class Grouping(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'grouping' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), @@ -139,19 +139,19 @@ class Grouping(STIXDomainObject): ]) -class Identity(STIXDomainObject): +class Identity(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'identity' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('name', StringProperty(required=True)), ('description', StringProperty()), ('roles', ListProperty(StringProperty)), @@ -168,19 +168,19 @@ class Identity(STIXDomainObject): ]) -class Indicator(STIXDomainObject): +class Indicator(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'indicator' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('name', StringProperty()), ('description', StringProperty()), ('indicator_types', ListProperty(StringProperty)), @@ -204,7 +204,7 @@ class Indicator(STIXDomainObject): if kwargs.get('pattern') and kwargs.get('pattern_type') == 'stix' and not kwargs.get('pattern_version'): kwargs['pattern_version'] = '2.1' - super(STIXDomainObject, self).__init__(*args, **kwargs) + super(_DomainObject, self).__init__(*args, **kwargs) def _check_object_constraints(self): super(Indicator, self)._check_object_constraints() @@ -227,19 +227,19 @@ class Indicator(STIXDomainObject): raise InvalidValueError(self.__class__, 'pattern', str(errors[0])) -class Infrastructure(STIXDomainObject): +class Infrastructure(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'infrastructure' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('confidence', IntegerProperty()), @@ -267,19 +267,19 @@ class Infrastructure(STIXDomainObject): raise ValueError(msg.format(self)) -class IntrusionSet(STIXDomainObject): +class IntrusionSet(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'intrusion-set' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('name', StringProperty(required=True)), ('description', StringProperty()), ('aliases', ListProperty(StringProperty)), @@ -309,19 +309,19 @@ class IntrusionSet(STIXDomainObject): raise ValueError(msg.format(self)) -class Location(STIXDomainObject): +class Location(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'location' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('name', StringProperty()), ('description', StringProperty()), ('latitude', FloatProperty(min=-90.0, max=90.0)), @@ -416,19 +416,19 @@ class Location(STIXDomainObject): return final_url -class Malware(STIXDomainObject): +class Malware(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'malware' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('name', StringProperty()), ('description', StringProperty()), ('malware_types', ListProperty(StringProperty)), @@ -468,18 +468,18 @@ class Malware(STIXDomainObject): ) -class MalwareAnalysis(STIXDomainObject): +class MalwareAnalysis(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'malware-analysis' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), @@ -512,19 +512,19 @@ class MalwareAnalysis(STIXDomainObject): self._check_at_least_one_property(["result", "analysis_sco_refs"]) -class Note(STIXDomainObject): +class Note(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'note' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('abstract', StringProperty()), ('content', StringProperty(required=True)), ('authors', ListProperty(StringProperty)), @@ -539,19 +539,19 @@ class Note(STIXDomainObject): ]) -class ObservedData(STIXDomainObject): +class ObservedData(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'observed-data' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('first_observed', TimestampProperty(required=True)), ('last_observed', TimestampProperty(required=True)), ('number_observed', IntegerProperty(min=1, max=999999999, required=True)), @@ -594,19 +594,19 @@ class ObservedData(STIXDomainObject): ) -class Opinion(STIXDomainObject): +class Opinion(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'opinion' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('explanation', StringProperty()), ('authors', ListProperty(StringProperty)), ( @@ -631,19 +631,19 @@ class Opinion(STIXDomainObject): ]) -class Report(STIXDomainObject): +class Report(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'report' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('name', StringProperty(required=True)), ('description', StringProperty()), ('report_types', ListProperty(StringProperty)), @@ -664,19 +664,19 @@ class Report(STIXDomainObject): super(Report, self).__init__(*args, **kwargs) -class ThreatActor(STIXDomainObject): +class ThreatActor(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'threat-actor' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('name', StringProperty(required=True)), ('description', StringProperty()), ('threat_actor_types', ListProperty(StringProperty)), @@ -710,19 +710,19 @@ class ThreatActor(STIXDomainObject): raise ValueError(msg.format(self)) -class Tool(STIXDomainObject): +class Tool(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'tool' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('name', StringProperty(required=True)), ('description', StringProperty()), ('tool_types', ListProperty(StringProperty)), @@ -739,19 +739,19 @@ class Tool(STIXDomainObject): ]) -class Vulnerability(STIXDomainObject): +class Vulnerability(_DomainObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'vulnerability' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('name', StringProperty(required=True)), ('description', StringProperty()), ('revoked', BooleanProperty(default=lambda: False)), @@ -796,12 +796,12 @@ def CustomObject(type='x-custom-type', properties=None): def wrapper(cls): _properties = list(itertools.chain.from_iterable([ [ - ('type', TypeProperty(type)), + ('type', TypeProperty(type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ], [x for x in properties if not x[0].startswith('x_')], [ @@ -815,6 +815,6 @@ def CustomObject(type='x-custom-type', properties=None): ], sorted([x for x in properties if x[0].startswith('x_')], key=lambda x: x[0]), ])) - return _custom_object_builder(cls, type, _properties, '2.1') + return _custom_object_builder(cls, type, _properties, '2.1', _DomainObject) return wrapper diff --git a/stix2/v21/sro.py b/stix2/v21/sro.py index f7f8c57..2047623 100644 --- a/stix2/v21/sro.py +++ b/stix2/v21/sro.py @@ -2,16 +2,16 @@ from collections import OrderedDict -from ..core import STIXRelationshipObject from ..properties import ( BooleanProperty, IDProperty, IntegerProperty, ListProperty, ReferenceProperty, StringProperty, TimestampProperty, TypeProperty, ) from ..utils import NOW +from .base import _RelationshipObject from .common import ExternalReference, GranularMarking -class Relationship(STIXRelationshipObject): +class Relationship(_RelationshipObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ @@ -20,12 +20,12 @@ class Relationship(STIXRelationshipObject): _type = 'relationship' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('relationship_type', StringProperty(required=True)), ('description', StringProperty()), ('source_ref', ReferenceProperty(invalid_types=_invalid_source_target_types, spec_version='2.1', required=True)), @@ -70,19 +70,19 @@ class Relationship(STIXRelationshipObject): raise ValueError(msg.format(self)) -class Sighting(STIXRelationshipObject): +class Sighting(_RelationshipObject): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ _type = 'sighting' _properties = OrderedDict([ - ('type', TypeProperty(_type)), + ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('description', StringProperty()), ('first_seen', TimestampProperty()), ('last_seen', TimestampProperty()), diff --git a/stix2/version.py b/stix2/version.py index 9c73af2..3e8d9f9 100644 --- a/stix2/version.py +++ b/stix2/version.py @@ -1 +1 @@ -__version__ = "1.3.1" +__version__ = "1.4.0"