From 55ea84ece2ba1e01de882f5ae280b4741c5dc39b Mon Sep 17 00:00:00 2001 From: Chris Lenk Date: Mon, 20 Jul 2020 00:04:32 -0400 Subject: [PATCH 01/16] Fix bug when adding custom obj to FileSystemSink ... if the object type hasn't been registered. Related: #439. --- stix2/base.py | 52 ++++++++++++++++----- stix2/datastore/filesystem.py | 24 ++++++---- stix2/test/v20/test_datastore_filesystem.py | 20 ++++++++ stix2/test/v21/test_datastore_filesystem.py | 21 +++++++++ 4 files changed, 96 insertions(+), 21 deletions(-) diff --git a/stix2/base.py b/stix2/base.py index 33374bc..31f5a42 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -30,7 +30,7 @@ except ImportError: from collections import Mapping -__all__ = ['STIXJSONEncoder', '_STIXBase'] +__all__ = ['STIXJSONEncoder', '_STIXBase', 'serialize'] DEFAULT_ERROR = "{type} must have {property}='{expected}'." SCO_DET_ID_NAMESPACE = uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7") @@ -74,6 +74,43 @@ class STIXJSONIncludeOptionalDefaultsEncoder(json.JSONEncoder): return super(STIXJSONIncludeOptionalDefaultsEncoder, self).default(obj) +def serialize(obj, pretty=False, include_optional_defaults=False, **kwargs): + """ + Serialize a STIX object. + + Args: + obj: The STIX object to be serialized. + pretty (bool): If True, output properties following the STIX specs + formatting. This includes indentation. Refer to notes for more + details. (Default: ``False``) + include_optional_defaults (bool): Determines whether to include + optional properties set to the default value defined in the spec. + **kwargs: The arguments for a json.dumps() call. + + Returns: + str: The serialized JSON object. + + Note: + The argument ``pretty=True`` will output the STIX object following + spec order. Using this argument greatly impacts object serialization + performance. If your use case is centered across machine-to-machine + operation it is recommended to set ``pretty=False``. + + When ``pretty=True`` the following key-value pairs will be added or + overridden: indent=4, separators=(",", ": "), item_sort_key=sort_by. + """ + if pretty: + def sort_by(element): + return find_property_index(obj, *element) + + kwargs.update({'indent': 4, 'separators': (',', ': '), 'item_sort_key': sort_by}) + + if include_optional_defaults: + return json.dumps(obj, cls=STIXJSONIncludeOptionalDefaultsEncoder, **kwargs) + else: + return json.dumps(obj, cls=STIXJSONEncoder, **kwargs) + + def get_required_properties(properties): return (k for k, v in properties.items() if v.required) @@ -270,7 +307,7 @@ class _STIXBase(Mapping): def revoke(self): return _revoke(self) - def serialize(self, pretty=False, include_optional_defaults=False, **kwargs): + def serialize(self, *args, **kwargs): """ Serialize a STIX object. @@ -309,16 +346,7 @@ class _STIXBase(Mapping): When ``pretty=True`` the following key-value pairs will be added or overridden: indent=4, separators=(",", ": "), item_sort_key=sort_by. """ - if pretty: - def sort_by(element): - return find_property_index(self, *element) - - kwargs.update({'indent': 4, 'separators': (',', ': '), 'item_sort_key': sort_by}) - - if include_optional_defaults: - return json.dumps(self, cls=STIXJSONIncludeOptionalDefaultsEncoder, **kwargs) - else: - return json.dumps(self, cls=STIXJSONEncoder, **kwargs) + return serialize(self, *args, **kwargs) class _DomainObject(_STIXBase, _MarkingsMixin): diff --git a/stix2/datastore/filesystem.py b/stix2/datastore/filesystem.py index 5a5844a..c0ef773 100644 --- a/stix2/datastore/filesystem.py +++ b/stix2/datastore/filesystem.py @@ -9,13 +9,13 @@ import stat import six from stix2 import v20, v21 -from stix2.base import _STIXBase +from stix2.base import _STIXBase, serialize 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 +from stix2.utils import format_datetime, get_type_from_id, parse_into_datetime def _timestamp2filename(timestamp): @@ -24,10 +24,12 @@ def _timestamp2filename(timestamp): "modified" property value. This should not include an extension. Args: - timestamp: A timestamp, as a datetime.datetime object. + timestamp: A timestamp, as a datetime.datetime object or string. """ # The format_datetime will determine the correct level of precision. + if isinstance(timestamp, str): + timestamp = parse_into_datetime(timestamp) ts = format_datetime(timestamp) ts = re.sub(r"[-T:\.Z ]", "", ts) return ts @@ -582,10 +584,10 @@ class FileSystemSink(DataSink): if os.path.isfile(file_path): raise DataSourceError("Attempted to overwrite file (!) at: {}".format(file_path)) - else: - with io.open(file_path, 'w', encoding=encoding) as f: - stix_obj = stix_obj.serialize(pretty=True, encoding=encoding, ensure_ascii=False) - f.write(stix_obj) + + with io.open(file_path, 'w', encoding=encoding) as f: + stix_obj = serialize(stix_obj, pretty=True, encoding=encoding, ensure_ascii=False) + f.write(stix_obj) def add(self, stix_data=None, version=None): """Add STIX objects to file directory. @@ -614,8 +616,12 @@ class FileSystemSink(DataSink): self._check_path_and_write(stix_data) elif isinstance(stix_data, (str, dict)): - stix_data = parse(stix_data, allow_custom=self.allow_custom, version=version) - self.add(stix_data, version=version) + parsed_data = parse(stix_data, allow_custom=self.allow_custom, version=version) + if isinstance(parsed_data, _STIXBase): + self.add(parsed_data, version=version) + else: + # custom unregistered object type + self._check_path_and_write(parsed_data) elif isinstance(stix_data, list): # recursively add individual STIX objects diff --git a/stix2/test/v20/test_datastore_filesystem.py b/stix2/test/v20/test_datastore_filesystem.py index 25207dc..7ce3ecf 100644 --- a/stix2/test/v20/test_datastore_filesystem.py +++ b/stix2/test/v20/test_datastore_filesystem.py @@ -633,6 +633,26 @@ def test_filesystem_object_with_custom_property_in_bundle(fs_store): assert camp_r.x_empire == camp.x_empire +def test_filesystem_custom_object_dict(fs_store): + fs_store.sink.allow_custom = True + newobj = { + "type": "x-new-obj-2", + "id": "x-new-obj-2--d08dc866-6149-47db-aae6-7b58a827e7f0", + "created": "2020-07-20T03:45:02.879Z", + "modified": "2020-07-20T03:45:02.879Z", + "property1": "something", + } + fs_store.add(newobj) + + newobj_r = fs_store.get(newobj["id"]) + assert newobj_r["id"] == newobj["id"] + assert newobj_r["property1"] == 'something' + + # remove dir + shutil.rmtree(os.path.join(FS_PATH, "x-new-obj-2"), True) + fs_store.sink.allow_custom = False + + def test_filesystem_custom_object(fs_store): @stix2.v20.CustomObject( 'x-new-obj-2', [ diff --git a/stix2/test/v21/test_datastore_filesystem.py b/stix2/test/v21/test_datastore_filesystem.py index 123fd7a..3eb20b5 100644 --- a/stix2/test/v21/test_datastore_filesystem.py +++ b/stix2/test/v21/test_datastore_filesystem.py @@ -654,6 +654,27 @@ def test_filesystem_object_with_custom_property_in_bundle(fs_store): assert camp_r.x_empire == camp.x_empire +def test_filesystem_custom_object_dict(fs_store): + fs_store.sink.allow_custom = True + newobj = { + "type": "x-new-obj-2", + "id": "x-new-obj-2--d08dc866-6149-47db-aae6-7b58a827e7f0", + "spec_version": "2.1", + "created": "2020-07-20T03:45:02.879Z", + "modified": "2020-07-20T03:45:02.879Z", + "property1": "something", + } + fs_store.add(newobj) + + newobj_r = fs_store.get(newobj["id"]) + assert newobj_r["id"] == newobj["id"] + assert newobj_r["property1"] == 'something' + + # remove dir + shutil.rmtree(os.path.join(FS_PATH, "x-new-obj-2"), True) + fs_store.sink.allow_custom = False + + def test_filesystem_custom_object(fs_store): @stix2.v21.CustomObject( 'x-new-obj-2', [ From 806389117f96414a84678a72d6c8996337102485 Mon Sep 17 00:00:00 2001 From: Chris Lenk Date: Mon, 20 Jul 2020 00:24:36 -0400 Subject: [PATCH 02/16] Allow mixing single objects and lists in bundles ...in bundle constructor Related: #429. --- stix2/v20/bundle.py | 12 ++++++++---- stix2/v21/bundle.py | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/stix2/v20/bundle.py b/stix2/v20/bundle.py index 25b948d..6a663d6 100644 --- a/stix2/v20/bundle.py +++ b/stix2/v20/bundle.py @@ -26,10 +26,14 @@ class Bundle(_STIXBase20): def __init__(self, *args, **kwargs): # Add any positional arguments to the 'objects' kwarg. if args: - if isinstance(args[0], list): - kwargs['objects'] = args[0] + list(args[1:]) + kwargs.get('objects', []) - else: - kwargs['objects'] = list(args) + kwargs.get('objects', []) + obj_list = [] + for arg in args: + if isinstance(arg, list): + obj_list = obj_list + arg + else: + obj_list.append(arg) + + kwargs['objects'] = obj_list + kwargs.get('objects', []) self._allow_custom = kwargs.get('allow_custom', False) self._properties['objects'].contained.allow_custom = kwargs.get('allow_custom', False) diff --git a/stix2/v21/bundle.py b/stix2/v21/bundle.py index a1ca054..5497da5 100644 --- a/stix2/v21/bundle.py +++ b/stix2/v21/bundle.py @@ -23,10 +23,14 @@ class Bundle(_STIXBase21): def __init__(self, *args, **kwargs): # Add any positional arguments to the 'objects' kwarg. if args: - if isinstance(args[0], list): - kwargs['objects'] = args[0] + list(args[1:]) + kwargs.get('objects', []) - else: - kwargs['objects'] = list(args) + kwargs.get('objects', []) + obj_list = [] + for arg in args: + if isinstance(arg, list): + obj_list = obj_list + arg + else: + obj_list.append(arg) + + kwargs['objects'] = obj_list + kwargs.get('objects', []) self._allow_custom = kwargs.get('allow_custom', False) self._properties['objects'].contained.allow_custom = kwargs.get('allow_custom', False) From 37f0238fc6da15c723533b7a0ef305486b5be2d3 Mon Sep 17 00:00:00 2001 From: Emmanuelle Vargas-Gonzalez Date: Wed, 22 Jul 2020 13:37:41 -0400 Subject: [PATCH 03/16] add serialize.py module --- stix2/serialize.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 stix2/serialize.py diff --git a/stix2/serialize.py b/stix2/serialize.py new file mode 100644 index 0000000..e69de29 From 853bd0da215f965f13a492cac8d28f2b38a98fe7 Mon Sep 17 00:00:00 2001 From: Emmanuelle Vargas-Gonzalez Date: Wed, 22 Jul 2020 13:56:24 -0400 Subject: [PATCH 04/16] move classes and methods from base.py to serialize.py --- stix2/base.py | 86 ++++------------------------------------------ stix2/serialize.py | 84 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 80 deletions(-) diff --git a/stix2/base.py b/stix2/base.py index 31f5a42..2374bf5 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -1,7 +1,6 @@ """Base classes for type definitions in the STIX2 library.""" import copy -import datetime as dt import re import uuid @@ -18,9 +17,10 @@ from .exceptions import ( ) from .markings import _MarkingsMixin from .markings.utils import validate -from .utils import ( - NOW, PREFIX_21_REGEX, find_property_index, format_datetime, get_timestamp, +from .serialize import ( + STIXJSONEncoder, STIXJSONIncludeOptionalDefaultsEncoder, serialize, ) +from .utils import NOW, PREFIX_21_REGEX, get_timestamp from .versioning import new_version as _new_version from .versioning import revoke as _revoke @@ -29,88 +29,14 @@ try: except ImportError: from collections import Mapping - -__all__ = ['STIXJSONEncoder', '_STIXBase', 'serialize'] +# TODO: Remove STIXJSONEncoder, STIXJSONIncludeOptionalDefaultsEncoder, serialize from __all__ on next major release. +# Kept for backwards compatibility. +__all__ = ['STIXJSONEncoder', 'STIXJSONIncludeOptionalDefaultsEncoder', '_STIXBase', 'serialize'] DEFAULT_ERROR = "{type} must have {property}='{expected}'." SCO_DET_ID_NAMESPACE = uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7") -class STIXJSONEncoder(json.JSONEncoder): - """Custom JSONEncoder subclass for serializing Python ``stix2`` objects. - - If an optional property with a default value specified in the STIX 2 spec - is set to that default value, it will be left out of the serialized output. - - An example of this type of property include the ``revoked`` common property. - """ - - def default(self, obj): - if isinstance(obj, (dt.date, dt.datetime)): - return format_datetime(obj) - elif isinstance(obj, _STIXBase): - tmp_obj = dict(copy.deepcopy(obj)) - for prop_name in obj._defaulted_optional_properties: - del tmp_obj[prop_name] - return tmp_obj - else: - return super(STIXJSONEncoder, self).default(obj) - - -class STIXJSONIncludeOptionalDefaultsEncoder(json.JSONEncoder): - """Custom JSONEncoder subclass for serializing Python ``stix2`` objects. - - Differs from ``STIXJSONEncoder`` in that if an optional property with a default - value specified in the STIX 2 spec is set to that default value, it will be - included in the serialized output. - """ - - def default(self, obj): - if isinstance(obj, (dt.date, dt.datetime)): - return format_datetime(obj) - elif isinstance(obj, _STIXBase): - return dict(obj) - else: - return super(STIXJSONIncludeOptionalDefaultsEncoder, self).default(obj) - - -def serialize(obj, pretty=False, include_optional_defaults=False, **kwargs): - """ - Serialize a STIX object. - - Args: - obj: The STIX object to be serialized. - pretty (bool): If True, output properties following the STIX specs - formatting. This includes indentation. Refer to notes for more - details. (Default: ``False``) - include_optional_defaults (bool): Determines whether to include - optional properties set to the default value defined in the spec. - **kwargs: The arguments for a json.dumps() call. - - Returns: - str: The serialized JSON object. - - Note: - The argument ``pretty=True`` will output the STIX object following - spec order. Using this argument greatly impacts object serialization - performance. If your use case is centered across machine-to-machine - operation it is recommended to set ``pretty=False``. - - When ``pretty=True`` the following key-value pairs will be added or - overridden: indent=4, separators=(",", ": "), item_sort_key=sort_by. - """ - if pretty: - def sort_by(element): - return find_property_index(obj, *element) - - kwargs.update({'indent': 4, 'separators': (',', ': '), 'item_sort_key': sort_by}) - - if include_optional_defaults: - return json.dumps(obj, cls=STIXJSONIncludeOptionalDefaultsEncoder, **kwargs) - else: - return json.dumps(obj, cls=STIXJSONEncoder, **kwargs) - - def get_required_properties(properties): return (k for k, v in properties.items() if v.required) diff --git a/stix2/serialize.py b/stix2/serialize.py index e69de29..9a55598 100644 --- a/stix2/serialize.py +++ b/stix2/serialize.py @@ -0,0 +1,84 @@ +"""STIX2 core serialization methods.""" + +import copy +import datetime as dt + +import simplejson as json + +from .base import _STIXBase +from .utils import find_property_index, format_datetime + + +class STIXJSONEncoder(json.JSONEncoder): + """Custom JSONEncoder subclass for serializing Python ``stix2`` objects. + + If an optional property with a default value specified in the STIX 2 spec + is set to that default value, it will be left out of the serialized output. + + An example of this type of property include the ``revoked`` common property. + """ + + def default(self, obj): + if isinstance(obj, (dt.date, dt.datetime)): + return format_datetime(obj) + elif isinstance(obj, _STIXBase): + tmp_obj = dict(copy.deepcopy(obj)) + for prop_name in obj._defaulted_optional_properties: + del tmp_obj[prop_name] + return tmp_obj + else: + return super(STIXJSONEncoder, self).default(obj) + + +class STIXJSONIncludeOptionalDefaultsEncoder(json.JSONEncoder): + """Custom JSONEncoder subclass for serializing Python ``stix2`` objects. + + Differs from ``STIXJSONEncoder`` in that if an optional property with a default + value specified in the STIX 2 spec is set to that default value, it will be + included in the serialized output. + """ + + def default(self, obj): + if isinstance(obj, (dt.date, dt.datetime)): + return format_datetime(obj) + elif isinstance(obj, _STIXBase): + return dict(obj) + else: + return super(STIXJSONIncludeOptionalDefaultsEncoder, self).default(obj) + + +def serialize(obj, pretty=False, include_optional_defaults=False, **kwargs): + """ + Serialize a STIX object. + + Args: + obj: The STIX object to be serialized. + pretty (bool): If True, output properties following the STIX specs + formatting. This includes indentation. Refer to notes for more + details. (Default: ``False``) + include_optional_defaults (bool): Determines whether to include + optional properties set to the default value defined in the spec. + **kwargs: The arguments for a json.dumps() call. + + Returns: + str: The serialized JSON object. + + Note: + The argument ``pretty=True`` will output the STIX object following + spec order. Using this argument greatly impacts object serialization + performance. If your use case is centered across machine-to-machine + operation it is recommended to set ``pretty=False``. + + When ``pretty=True`` the following key-value pairs will be added or + overridden: indent=4, separators=(",", ": "), item_sort_key=sort_by. + """ + if pretty: + def sort_by(element): + return find_property_index(obj, *element) + + kwargs.update({'indent': 4, 'separators': (',', ': '), 'item_sort_key': sort_by}) + + if include_optional_defaults: + return json.dumps(obj, cls=STIXJSONIncludeOptionalDefaultsEncoder, **kwargs) + else: + return json.dumps(obj, cls=STIXJSONEncoder, **kwargs) From c760e04c9a2702191ff4bacc349165ed20653b9e Mon Sep 17 00:00:00 2001 From: Emmanuelle Vargas-Gonzalez Date: Wed, 22 Jul 2020 14:31:26 -0400 Subject: [PATCH 05/16] rename module to serialization.py --- stix2/base.py | 2 +- stix2/datastore/filesystem.py | 3 ++- stix2/{serialize.py => serialization.py} | 0 3 files changed, 3 insertions(+), 2 deletions(-) rename stix2/{serialize.py => serialization.py} (100%) diff --git a/stix2/base.py b/stix2/base.py index 2374bf5..8be31d7 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -17,7 +17,7 @@ from .exceptions import ( ) from .markings import _MarkingsMixin from .markings.utils import validate -from .serialize import ( +from .serialization import ( STIXJSONEncoder, STIXJSONIncludeOptionalDefaultsEncoder, serialize, ) from .utils import NOW, PREFIX_21_REGEX, get_timestamp diff --git a/stix2/datastore/filesystem.py b/stix2/datastore/filesystem.py index c0ef773..d865768 100644 --- a/stix2/datastore/filesystem.py +++ b/stix2/datastore/filesystem.py @@ -9,12 +9,13 @@ import stat import six from stix2 import v20, v21 -from stix2.base import _STIXBase, serialize +from stix2.base import _STIXBase 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.serialization import serialize from stix2.utils import format_datetime, get_type_from_id, parse_into_datetime diff --git a/stix2/serialize.py b/stix2/serialization.py similarity index 100% rename from stix2/serialize.py rename to stix2/serialization.py From 978aee9a8e2219863fb9dc60146567ea1ede178f Mon Sep 17 00:00:00 2001 From: Emmanuelle Vargas-Gonzalez Date: Wed, 22 Jul 2020 14:53:37 -0400 Subject: [PATCH 06/16] fix circular import problem --- stix2/serialization.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/stix2/serialization.py b/stix2/serialization.py index 9a55598..f0defd9 100644 --- a/stix2/serialization.py +++ b/stix2/serialization.py @@ -5,7 +5,8 @@ import datetime as dt import simplejson as json -from .base import _STIXBase +import stix2.base + from .utils import find_property_index, format_datetime @@ -21,7 +22,7 @@ class STIXJSONEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, (dt.date, dt.datetime)): return format_datetime(obj) - elif isinstance(obj, _STIXBase): + elif isinstance(obj, stix2.base._STIXBase): tmp_obj = dict(copy.deepcopy(obj)) for prop_name in obj._defaulted_optional_properties: del tmp_obj[prop_name] @@ -41,7 +42,7 @@ class STIXJSONIncludeOptionalDefaultsEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, (dt.date, dt.datetime)): return format_datetime(obj) - elif isinstance(obj, _STIXBase): + elif isinstance(obj, stix2.base._STIXBase): return dict(obj) else: return super(STIXJSONIncludeOptionalDefaultsEncoder, self).default(obj) From ca56a74e12aa9a056064c3bf74e87da766deba12 Mon Sep 17 00:00:00 2001 From: Emmanuelle Vargas-Gonzalez Date: Wed, 22 Jul 2020 15:20:39 -0400 Subject: [PATCH 07/16] update docstrings for _STIXBase method --- stix2/base.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/stix2/base.py b/stix2/base.py index 8be31d7..21b6011 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -237,14 +237,6 @@ class _STIXBase(Mapping): """ Serialize a STIX object. - Args: - pretty (bool): If True, output properties following the STIX specs - formatting. This includes indentation. Refer to notes for more - details. (Default: ``False``) - include_optional_defaults (bool): Determines whether to include - optional properties set to the default value defined in the spec. - **kwargs: The arguments for a json.dumps() call. - Examples: >>> import stix2 >>> identity = stix2.Identity(name='Example Corp.', identity_class='organization') @@ -263,14 +255,8 @@ class _STIXBase(Mapping): Returns: str: The serialized JSON object. - Note: - The argument ``pretty=True`` will output the STIX object following - spec order. Using this argument greatly impacts object serialization - performance. If your use case is centered across machine-to-machine - operation it is recommended to set ``pretty=False``. - - When ``pretty=True`` the following key-value pairs will be added or - overridden: indent=4, separators=(",", ": "), item_sort_key=sort_by. + See Also: + ``stix2.serialization.serialize`` for options. """ return serialize(self, *args, **kwargs) From 8093898a3d80316ee4e2e9f9997e84091b354131 Mon Sep 17 00:00:00 2001 From: Emmanuelle Vargas-Gonzalez Date: Wed, 22 Jul 2020 15:36:48 -0400 Subject: [PATCH 08/16] move serialization-related methods to serialization.py update tests that call specific methods from this area --- stix2/serialization.py | 79 +++++++++++++++++++++++++++++++++++- stix2/test/v20/test_utils.py | 5 ++- stix2/test/v21/test_utils.py | 5 ++- stix2/utils.py | 77 ----------------------------------- 4 files changed, 84 insertions(+), 82 deletions(-) diff --git a/stix2/serialization.py b/stix2/serialization.py index f0defd9..7488eb5 100644 --- a/stix2/serialization.py +++ b/stix2/serialization.py @@ -7,7 +7,7 @@ import simplejson as json import stix2.base -from .utils import find_property_index, format_datetime +from .utils import format_datetime class STIXJSONEncoder(json.JSONEncoder): @@ -83,3 +83,80 @@ def serialize(obj, pretty=False, include_optional_defaults=False, **kwargs): return json.dumps(obj, cls=STIXJSONIncludeOptionalDefaultsEncoder, **kwargs) else: return json.dumps(obj, cls=STIXJSONEncoder, **kwargs) + + +def _find(seq, val): + """ + Search sequence 'seq' for val. This behaves like str.find(): if not found, + -1 is returned instead of throwing an exception. + + Args: + seq: The sequence to search + val: The value to search for + + Returns: + int: The index of the value if found, or -1 if not found + """ + try: + return seq.index(val) + except ValueError: + return -1 + + +def _find_property_in_seq(seq, search_key, search_value): + """ + Helper for find_property_index(): search for the property in all elements + of the given sequence. + + Args: + seq: The sequence + search_key: Property name to find + search_value: Property value to find + + Returns: + int: A property index, or -1 if the property was not found + """ + idx = -1 + for elem in seq: + idx = find_property_index(elem, search_key, search_value) + if idx >= 0: + break + + return idx + + +def find_property_index(obj, search_key, search_value): + """ + Search (recursively) for the given key and value in the given object. + Return an index for the key, relative to whatever object it's found in. + + Args: + obj: The object to search (list, dict, or stix object) + search_key: A search key + search_value: A search value + + Returns: + int: An index; -1 if the key and value aren't found + """ + # 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, stix2.base._STIXBase): + if search_key in obj and obj[search_key] == search_value: + idx = _find(obj.object_properties(), search_key) + else: + idx = _find_property_in_seq(obj.values(), search_key, search_value) + elif isinstance(obj, dict): + if search_key in obj and obj[search_key] == search_value: + idx = _find(sorted(obj), search_key) + else: + idx = _find_property_in_seq(obj.values(), search_key, search_value) + elif isinstance(obj, list): + idx = _find_property_in_seq(obj, search_key, search_value) + else: + # Don't know how to search this type + idx = -1 + + return idx diff --git a/stix2/test/v20/test_utils.py b/stix2/test/v20/test_utils.py index 67750de..9372bbb 100644 --- a/stix2/test/v20/test_utils.py +++ b/stix2/test/v20/test_utils.py @@ -6,6 +6,7 @@ from io import StringIO import pytest import pytz +import stix2.serialization import stix2.utils from .constants import IDENTITY_ID @@ -198,7 +199,7 @@ def test_deduplicate(stix_objs1): ], ) def test_find_property_index(object, tuple_to_find, expected_index): - assert stix2.utils.find_property_index( + assert stix2.serialization.find_property_index( object, *tuple_to_find ) == expected_index @@ -235,4 +236,4 @@ def test_find_property_index(object, tuple_to_find, expected_index): ], ) def test_iterate_over_values(dict_value, tuple_to_find, expected_index): - assert stix2.utils._find_property_in_seq(dict_value.values(), *tuple_to_find) == expected_index + assert stix2.serialization._find_property_in_seq(dict_value.values(), *tuple_to_find) == expected_index diff --git a/stix2/test/v21/test_utils.py b/stix2/test/v21/test_utils.py index f81c93f..03477aa 100644 --- a/stix2/test/v21/test_utils.py +++ b/stix2/test/v21/test_utils.py @@ -6,6 +6,7 @@ from io import StringIO import pytest import pytz +import stix2.serialization import stix2.utils from .constants import IDENTITY_ID @@ -201,7 +202,7 @@ def test_deduplicate(stix_objs1): ], ) def test_find_property_index(object, tuple_to_find, expected_index): - assert stix2.utils.find_property_index( + assert stix2.serialization.find_property_index( object, *tuple_to_find ) == expected_index @@ -238,4 +239,4 @@ def test_find_property_index(object, tuple_to_find, expected_index): ], ) def test_iterate_over_values(dict_value, tuple_to_find, expected_index): - assert stix2.utils._find_property_in_seq(dict_value.values(), *tuple_to_find) == expected_index + assert stix2.serialization._find_property_in_seq(dict_value.values(), *tuple_to_find) == expected_index diff --git a/stix2/utils.py b/stix2/utils.py index 7a8d8cb..f741581 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -298,83 +298,6 @@ def _get_dict(data): raise ValueError("Cannot convert '%s' to dictionary." % str(data)) -def _find(seq, val): - """ - Search sequence 'seq' for val. This behaves like str.find(): if not found, - -1 is returned instead of throwing an exception. - - Args: - seq: The sequence to search - val: The value to search for - - Returns: - int: The index of the value if found, or -1 if not found - """ - try: - return seq.index(val) - except ValueError: - return -1 - - -def _find_property_in_seq(seq, search_key, search_value): - """ - Helper for find_property_index(): search for the property in all elements - of the given sequence. - - Args: - seq: The sequence - search_key: Property name to find - search_value: Property value to find - - Returns: - int: A property index, or -1 if the property was not found - """ - idx = -1 - for elem in seq: - idx = find_property_index(elem, search_key, search_value) - if idx >= 0: - break - - return idx - - -def find_property_index(obj, search_key, search_value): - """ - Search (recursively) for the given key and value in the given object. - Return an index for the key, relative to whatever object it's found in. - - Args: - obj: The object to search (list, dict, or stix object) - search_key: A search key - search_value: A search value - - Returns: - int: An index; -1 if the key and value aren't found - """ - # 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, stix2.base._STIXBase): - if search_key in obj and obj[search_key] == search_value: - idx = _find(obj.object_properties(), search_key) - else: - idx = _find_property_in_seq(obj.values(), search_key, search_value) - elif isinstance(obj, dict): - if search_key in obj and obj[search_key] == search_value: - idx = _find(sorted(obj), search_key) - else: - idx = _find_property_in_seq(obj.values(), search_key, search_value) - elif isinstance(obj, list): - idx = _find_property_in_seq(obj, search_key, search_value) - else: - # Don't know how to search this type - idx = -1 - - return idx - - def get_class_hierarchy_names(obj): """Given an object, return the names of the class hierarchy.""" names = [] From 08137ff6be12d0f9890c4df64d777991eb1b7bf7 Mon Sep 17 00:00:00 2001 From: Emmanuelle Vargas-Gonzalez Date: Wed, 22 Jul 2020 15:38:17 -0400 Subject: [PATCH 09/16] add serialization to API documentation --- docs/api/stix2.serialization.rst | 5 +++++ stix2/__init__.py | 1 + 2 files changed, 6 insertions(+) create mode 100644 docs/api/stix2.serialization.rst diff --git a/docs/api/stix2.serialization.rst b/docs/api/stix2.serialization.rst new file mode 100644 index 0000000..bc182d8 --- /dev/null +++ b/docs/api/stix2.serialization.rst @@ -0,0 +1,5 @@ +serialization +================ + +.. automodule:: stix2.serialization + :members: diff --git a/stix2/__init__.py b/stix2/__init__.py index d0051ee..97790aa 100644 --- a/stix2/__init__.py +++ b/stix2/__init__.py @@ -12,6 +12,7 @@ pattern_visitor patterns properties + serialization utils v20 v21 From 084941dd418d7215cdc076423d739c228a38f3a0 Mon Sep 17 00:00:00 2001 From: Rich Piazza Date: Fri, 24 Jul 2020 11:40:21 -0400 Subject: [PATCH 10/16] handle mixed boolean expressions --- stix2/pattern_visitor.py | 5 ++++- stix2/test/v20/test_pattern_expressions.py | 10 ++++++++++ stix2/test/v21/test_pattern_expressions.py | 10 ++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/stix2/pattern_visitor.py b/stix2/pattern_visitor.py index 5b8300f..c4deb64 100644 --- a/stix2/pattern_visitor.py +++ b/stix2/pattern_visitor.py @@ -49,6 +49,9 @@ def check_for_valid_timetamp_syntax(timestamp_string): return _TIMESTAMP_RE.match(timestamp_string) +def same_boolean_operator(current_op, op_token): + return current_op == op_token.symbol.text + class STIXPatternVisitorForSTIX2(): classes = {} @@ -131,7 +134,7 @@ class STIXPatternVisitorForSTIX2(): if len(children) == 1: return children[0] else: - if isinstance(children[0], _BooleanExpression): + if isinstance(children[0], _BooleanExpression) and same_boolean_operator(children[0].operator, children[1]): children[0].operands.append(children[2]) return children[0] else: diff --git a/stix2/test/v20/test_pattern_expressions.py b/stix2/test/v20/test_pattern_expressions.py index a96d3b8..0e0a9ca 100644 --- a/stix2/test/v20/test_pattern_expressions.py +++ b/stix2/test/v20/test_pattern_expressions.py @@ -511,6 +511,16 @@ def test_parsing_start_stop_qualified_expression(): ) == "[ipv4-addr:value = '1.2.3.4'] START '2016-06-01T00:00:00Z' STOP '2017-03-12T08:30:00Z'" +def test_parsing_mixed_boolean_expression_1(): + patt_obj = create_pattern_object("[a:b = 1 AND a:b = 2 OR a:b = 3]",) + assert str(patt_obj) == "[a:b = 1 AND a:b = 2 OR a:b = 3]" + + +def test_parsing_mixed_boolean_expression_2(): + patt_obj = create_pattern_object("[a:b = 1 OR a:b = 2 AND a:b = 3]",) + assert str(patt_obj) == "[a:b = 1 OR a:b = 2 AND a:b = 3]" + + def test_parsing_illegal_start_stop_qualified_expression(): with pytest.raises(ValueError): create_pattern_object("[ipv4-addr:value = '1.2.3.4'] START '2016-06-01' STOP '2017-03-12T08:30:00Z'", version="2.0") diff --git a/stix2/test/v21/test_pattern_expressions.py b/stix2/test/v21/test_pattern_expressions.py index 8294a41..b574e05 100644 --- a/stix2/test/v21/test_pattern_expressions.py +++ b/stix2/test/v21/test_pattern_expressions.py @@ -644,6 +644,16 @@ def test_parsing_boolean(): assert str(patt_obj) == "[network-traffic:is_active = true]" +def test_parsing_mixed_boolean_expression_1(): + patt_obj = create_pattern_object("[a:b = 1 AND a:b = 2 OR a:b = 3]",) + assert str(patt_obj) == "[a:b = 1 AND a:b = 2 OR a:b = 3]" + + +def test_parsing_mixed_boolean_expression_2(): + patt_obj = create_pattern_object("[a:b = 1 OR a:b = 2 AND a:b = 3]",) + assert str(patt_obj) == "[a:b = 1 OR a:b = 2 AND a:b = 3]" + + def test_parsing_multiple_slashes_quotes(): patt_obj = create_pattern_object("[ file:name = 'weird_name\\'' ]", version="2.1") assert str(patt_obj) == "[file:name = 'weird_name\\'']" From 0fc2befd6ad82ceffb4c4bcf190bf96a99f5cf53 Mon Sep 17 00:00:00 2001 From: Rich Piazza Date: Sat, 25 Jul 2020 14:22:03 -0400 Subject: [PATCH 11/16] hack for issue_435 --- stix2/pattern_visitor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/stix2/pattern_visitor.py b/stix2/pattern_visitor.py index c4deb64..10f70bb 100644 --- a/stix2/pattern_visitor.py +++ b/stix2/pattern_visitor.py @@ -259,6 +259,9 @@ class STIXPatternVisitorForSTIX2(): if isinstance(next, TerminalNode): property_path.append(self.instantiate("ListObjectPathComponent", current.property_name, next.getText())) i += 2 + if isinstance(next, IntegerConstant): + property_path.append(self.instantiate("ListObjectPathComponent", current.property_name, next.value)) + i += 2 else: property_path.append(current) i += 1 From b7a30befdcbb9a9c7dc93243cdee21eb695f4d2b Mon Sep 17 00:00:00 2001 From: Rich Piazza Date: Sat, 25 Jul 2020 14:47:40 -0400 Subject: [PATCH 12/16] add tests and fix introduced bug --- stix2/pattern_visitor.py | 2 +- stix2/test/v20/test_pattern_expressions.py | 9 +++++++-- stix2/test/v21/test_pattern_expressions.py | 5 +++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/stix2/pattern_visitor.py b/stix2/pattern_visitor.py index 10f70bb..6da41b5 100644 --- a/stix2/pattern_visitor.py +++ b/stix2/pattern_visitor.py @@ -259,7 +259,7 @@ class STIXPatternVisitorForSTIX2(): if isinstance(next, TerminalNode): property_path.append(self.instantiate("ListObjectPathComponent", current.property_name, next.getText())) i += 2 - if isinstance(next, IntegerConstant): + elif isinstance(next, IntegerConstant): property_path.append(self.instantiate("ListObjectPathComponent", current.property_name, next.value)) i += 2 else: diff --git a/stix2/test/v20/test_pattern_expressions.py b/stix2/test/v20/test_pattern_expressions.py index 0e0a9ca..cca327b 100644 --- a/stix2/test/v20/test_pattern_expressions.py +++ b/stix2/test/v20/test_pattern_expressions.py @@ -512,15 +512,20 @@ def test_parsing_start_stop_qualified_expression(): def test_parsing_mixed_boolean_expression_1(): - patt_obj = create_pattern_object("[a:b = 1 AND a:b = 2 OR a:b = 3]",) + patt_obj = create_pattern_object("[a:b = 1 AND a:b = 2 OR a:b = 3]") assert str(patt_obj) == "[a:b = 1 AND a:b = 2 OR a:b = 3]" def test_parsing_mixed_boolean_expression_2(): - patt_obj = create_pattern_object("[a:b = 1 OR a:b = 2 AND a:b = 3]",) + patt_obj = create_pattern_object("[a:b = 1 OR a:b = 2 AND a:b = 3]") assert str(patt_obj) == "[a:b = 1 OR a:b = 2 AND a:b = 3]" +def test_parsing_integer_index(): + patt_obj = create_pattern_object("[a:b[1]=2]") + assert str(patt_obj) == "[a:b[1] = 2]" + + def test_parsing_illegal_start_stop_qualified_expression(): with pytest.raises(ValueError): create_pattern_object("[ipv4-addr:value = '1.2.3.4'] START '2016-06-01' STOP '2017-03-12T08:30:00Z'", version="2.0") diff --git a/stix2/test/v21/test_pattern_expressions.py b/stix2/test/v21/test_pattern_expressions.py index b574e05..56273c0 100644 --- a/stix2/test/v21/test_pattern_expressions.py +++ b/stix2/test/v21/test_pattern_expressions.py @@ -654,6 +654,11 @@ def test_parsing_mixed_boolean_expression_2(): assert str(patt_obj) == "[a:b = 1 OR a:b = 2 AND a:b = 3]" +def test_parsing_integer_index(): + patt_obj = create_pattern_object("[a:b[1]=2]") + assert str(patt_obj) == "[a:b[1] = 2]" + + def test_parsing_multiple_slashes_quotes(): patt_obj = create_pattern_object("[ file:name = 'weird_name\\'' ]", version="2.1") assert str(patt_obj) == "[file:name = 'weird_name\\'']" From 15344527aa8556798776aa0b3565af36e6c3f3a3 Mon Sep 17 00:00:00 2001 From: maybe-sybr <58414429+maybe-sybr@users.noreply.github.com> Date: Mon, 27 Jul 2020 11:08:19 +1000 Subject: [PATCH 13/16] fix: Respect name of `@Custom*` decorated defs --- stix2/custom.py | 8 ++++++++ stix2/test/v20/test_custom.py | 2 +- stix2/test/v21/test_custom.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/stix2/custom.py b/stix2/custom.py index f749b04..08574ef 100644 --- a/stix2/custom.py +++ b/stix2/custom.py @@ -35,6 +35,8 @@ def _custom_object_builder(cls, type, properties, version, base_class): base_class.__init__(self, **kwargs) _cls_init(cls, self, kwargs) + _CustomObject.__name__ = cls.__name__ + _register_object(_CustomObject, version=version) return _CustomObject @@ -51,6 +53,8 @@ def _custom_marking_builder(cls, type, properties, version, base_class): base_class.__init__(self, **kwargs) _cls_init(cls, self, kwargs) + _CustomMarking.__name__ = cls.__name__ + _register_marking(_CustomMarking, version=version) return _CustomMarking @@ -72,6 +76,8 @@ def _custom_observable_builder(cls, type, properties, version, base_class, id_co base_class.__init__(self, **kwargs) _cls_init(cls, self, kwargs) + _CustomObservable.__name__ = cls.__name__ + _register_observable(_CustomObservable, version=version) return _CustomObservable @@ -88,5 +94,7 @@ def _custom_extension_builder(cls, observable, type, properties, version, base_c base_class.__init__(self, **kwargs) _cls_init(cls, self, kwargs) + _CustomExtension.__name__ = cls.__name__ + _register_observable_extension(observable, _CustomExtension, version=version) return _CustomExtension diff --git a/stix2/test/v20/test_custom.py b/stix2/test/v20/test_custom.py index 33be3e2..70835c1 100644 --- a/stix2/test/v20/test_custom.py +++ b/stix2/test/v20/test_custom.py @@ -723,7 +723,7 @@ def test_custom_extension(): with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: NewExtension(property2=42) assert excinfo.value.properties == ['property1'] - assert str(excinfo.value) == "No values for required properties for _CustomExtension: (property1)." + assert str(excinfo.value) == "No values for required properties for NewExtension: (property1)." with pytest.raises(ValueError) as excinfo: NewExtension(property1='something', property2=4) diff --git a/stix2/test/v21/test_custom.py b/stix2/test/v21/test_custom.py index 146abcd..ea6d3a8 100644 --- a/stix2/test/v21/test_custom.py +++ b/stix2/test/v21/test_custom.py @@ -920,7 +920,7 @@ def test_custom_extension(): with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: NewExtension(property2=42) assert excinfo.value.properties == ['property1'] - assert str(excinfo.value) == "No values for required properties for _CustomExtension: (property1)." + assert str(excinfo.value) == "No values for required properties for NewExtension: (property1)." with pytest.raises(ValueError) as excinfo: NewExtension(property1='something', property2=4) From 8f76a84bbfb5ea91bd97b96f2bccae02e3dd0f85 Mon Sep 17 00:00:00 2001 From: Rich Piazza Date: Thu, 30 Jul 2020 15:32:06 -0400 Subject: [PATCH 14/16] handle quoted path components --- stix2/pattern_visitor.py | 18 +++++++++++++----- stix2/patterns.py | 5 ++++- stix2/test/v20/test_pattern_expressions.py | 11 +++++++++++ stix2/test/v21/test_pattern_expressions.py | 10 ++++++++++ 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/stix2/pattern_visitor.py b/stix2/pattern_visitor.py index 6da41b5..a9d43c5 100644 --- a/stix2/pattern_visitor.py +++ b/stix2/pattern_visitor.py @@ -2,6 +2,7 @@ import importlib import inspect +from six import text_type from stix2patterns.exceptions import ParseException from stix2patterns.grammars.STIXPatternParser import TerminalNode @@ -50,7 +51,7 @@ def check_for_valid_timetamp_syntax(timestamp_string): def same_boolean_operator(current_op, op_token): - return current_op == op_token.symbol.text + return current_op == op_token.getText() class STIXPatternVisitorForSTIX2(): @@ -260,7 +261,9 @@ class STIXPatternVisitorForSTIX2(): property_path.append(self.instantiate("ListObjectPathComponent", current.property_name, next.getText())) i += 2 elif isinstance(next, IntegerConstant): - property_path.append(self.instantiate("ListObjectPathComponent", current.property_name, next.value)) + property_path.append(self.instantiate("ListObjectPathComponent", + current.property_name if isinstance(current, BasicObjectPathComponent) else text_type(current), + next.value)) i += 2 else: property_path.append(current) @@ -275,7 +278,12 @@ class STIXPatternVisitorForSTIX2(): # Visit a parse tree produced by STIXPatternParser#firstPathComponent. def visitFirstPathComponent(self, ctx): children = self.visitChildren(ctx) - step = children[0].getText() + first_component = children[0] + # hack for when the first component isn't a TerminalNode (see issue #438) + if isinstance(first_component, TerminalNode): + step = first_component.getText() + else: + step = text_type(first_component) # if step.endswith("_ref"): # return stix2.ReferenceObjectPathComponent(step) # else: @@ -294,8 +302,8 @@ class STIXPatternVisitorForSTIX2(): def visitKeyPathStep(self, ctx): children = self.visitChildren(ctx) if isinstance(children[1], StringConstant): - # special case for hashes - return children[1].value + # special case for hashes and quoted steps + return children[1] else: return self.instantiate("BasicObjectPathComponent", children[1].getText(), True) diff --git a/stix2/patterns.py b/stix2/patterns.py index edcf017..bbee7ac 100644 --- a/stix2/patterns.py +++ b/stix2/patterns.py @@ -248,7 +248,10 @@ def make_constant(value): class _ObjectPathComponent(object): @staticmethod def create_ObjectPathComponent(component_name): - if component_name.endswith("_ref"): + # first case is to handle if component_name was quoted + if isinstance(component_name, StringConstant): + return BasicObjectPathComponent(component_name.value, False) + elif component_name.endswith("_ref"): return ReferenceObjectPathComponent(component_name) elif component_name.find("[") != -1: parse1 = component_name.split("[") diff --git a/stix2/test/v20/test_pattern_expressions.py b/stix2/test/v20/test_pattern_expressions.py index cca327b..fa9000e 100644 --- a/stix2/test/v20/test_pattern_expressions.py +++ b/stix2/test/v20/test_pattern_expressions.py @@ -526,6 +526,17 @@ def test_parsing_integer_index(): assert str(patt_obj) == "[a:b[1] = 2]" +# This should never occur, because the first component will always be a property_name, and they should not be quoted. +def test_parsing_quoted_first_path_component(): + patt_obj = create_pattern_object("[a:'b'[1]=2]") + assert str(patt_obj) == "[a:'b'[1] = 2]" + + +def test_parsing_quoted_second_path_component(): + patt_obj = create_pattern_object("[a:b.'b'[1]=2]") + assert str(patt_obj) == "[a:b.'b'[1] = 2]" + + def test_parsing_illegal_start_stop_qualified_expression(): with pytest.raises(ValueError): create_pattern_object("[ipv4-addr:value = '1.2.3.4'] START '2016-06-01' STOP '2017-03-12T08:30:00Z'", version="2.0") diff --git a/stix2/test/v21/test_pattern_expressions.py b/stix2/test/v21/test_pattern_expressions.py index 56273c0..3ba0aa6 100644 --- a/stix2/test/v21/test_pattern_expressions.py +++ b/stix2/test/v21/test_pattern_expressions.py @@ -658,6 +658,16 @@ def test_parsing_integer_index(): patt_obj = create_pattern_object("[a:b[1]=2]") assert str(patt_obj) == "[a:b[1] = 2]" +# This should never occur, because the first component will always be a property_name, and they should not be quoted. +def test_parsing_quoted_first_path_component(): + patt_obj = create_pattern_object("[a:'b'[1]=2]") + assert str(patt_obj) == "[a:'b'[1] = 2]" + + +def test_parsing_quoted_second_path_component(): + patt_obj = create_pattern_object("[a:b.'b'[1]=2]") + assert str(patt_obj) == "[a:b.'b'[1] = 2]" + def test_parsing_multiple_slashes_quotes(): patt_obj = create_pattern_object("[ file:name = 'weird_name\\'' ]", version="2.1") From 2d64bc86bb210c7899f371bfe532545775fbde31 Mon Sep 17 00:00:00 2001 From: Emmanuelle Vargas-Gonzalez Date: Mon, 17 Aug 2020 10:49:07 -0400 Subject: [PATCH 15/16] Update setup.py Add version restriction to ensure reasonably updated dependency --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3a5ef8d..a4a9c2c 100644 --- a/setup.py +++ b/setup.py @@ -60,7 +60,7 @@ setup( 'Bug Tracker': 'https://github.com/oasis-open/cti-python-stix2/issues/', }, extras_require={ - 'taxii': ['taxii2-client'], + 'taxii': ['taxii2-client>=2.2.1'], 'semantic': ['haversine', 'rapidfuzz'], }, ) From 241d253a299e6609ecb9a69d515b2d948e293cce Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Mon, 17 Aug 2020 13:49:19 -0400 Subject: [PATCH 16/16] Add a version restriction to the stix2-patterns dependency, to ensure users aren't tripped up by old versions. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a4a9c2c..2fc5d70 100644 --- a/setup.py +++ b/setup.py @@ -52,7 +52,7 @@ setup( 'requests', 'simplejson', 'six>=1.13.0', - 'stix2-patterns', + 'stix2-patterns>=1.2.0', ], project_urls={ 'Documentation': 'https://stix2.readthedocs.io/',