From b9eba77008aedffbf91dff829ad3da95526760ee Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Fri, 2 Jul 2021 20:54:54 -0400 Subject: [PATCH 1/5] Move the CustomExtension decorator from the v21.observables module to v21.common. Custom extensions are not specific to SCOs, so I don't know why it was in that module. Now, ExtensionDefinition and CustomExtension are together in the same module, just like MarkingDefinition and CustomMarking are together. Made sense to me. --- stix2/v21/__init__.py | 4 ++-- stix2/v21/common.py | 30 ++++++++++++++++++++++++++++-- stix2/v21/observables.py | 29 +++-------------------------- stix2/v21/sdo.py | 7 ++++--- 4 files changed, 37 insertions(+), 33 deletions(-) diff --git a/stix2/v21/__init__.py b/stix2/v21/__init__.py index 977fdf7..ece8b36 100644 --- a/stix2/v21/__init__.py +++ b/stix2/v21/__init__.py @@ -19,13 +19,13 @@ from .base import ( ) from .bundle import Bundle from .common import ( - TLP_AMBER, TLP_GREEN, TLP_RED, TLP_WHITE, CustomMarking, + TLP_AMBER, TLP_GREEN, TLP_RED, TLP_WHITE, CustomExtension, CustomMarking, ExtensionDefinition, ExternalReference, GranularMarking, KillChainPhase, LanguageContent, MarkingDefinition, StatementMarking, TLPMarking, ) from .observables import ( URL, AlternateDataStream, ArchiveExt, Artifact, AutonomousSystem, - CustomExtension, CustomObservable, Directory, DomainName, EmailAddress, + CustomObservable, Directory, DomainName, EmailAddress, EmailMessage, EmailMIMEComponent, File, HTTPRequestExt, ICMPExt, IPv4Address, IPv6Address, MACAddress, Mutex, NetworkTraffic, NTFSExt, PDFExt, Process, RasterImageExt, SocketExt, Software, TCPExt, diff --git a/stix2/v21/common.py b/stix2/v21/common.py index e62bdae..6b2922b 100644 --- a/stix2/v21/common.py +++ b/stix2/v21/common.py @@ -1,8 +1,10 @@ """STIX 2.1 Common Data Types and Properties.""" from collections import OrderedDict +from collections.abc import Mapping -from ..custom import _custom_marking_builder +from . import _Extension +from ..custom import _custom_marking_builder, _custom_extension_builder from ..exceptions import InvalidValueError, PropertyPresenceError from ..markings import _MarkingsMixin from ..markings.utils import check_tlp_marking @@ -139,6 +141,30 @@ class ExtensionDefinition(_STIXBase21): ]) +def CustomExtension(type='x-custom-ext', properties=None): + """Custom STIX Object Extension decorator. + """ + def wrapper(cls): + + # Auto-create an "extension_type" property from the class attribute, if + # it exists. + extension_type = getattr(cls, "extension_type", None) + if extension_type: + extension_type_prop = EnumProperty( + EXTENSION_TYPE, + required=False, + fixed=extension_type, + ) + + if isinstance(properties, Mapping): + properties["extension_type"] = extension_type_prop + else: + properties.append(("extension_type", extension_type_prop)) + + return _custom_extension_builder(cls, type, properties, '2.1', _Extension) + return wrapper + + class TLPMarking(_STIXBase21): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. @@ -260,7 +286,7 @@ def CustomMarking(type='x-custom-marking', properties=None, extension_name=None) if extension_name: from . import observables - @observables.CustomExtension(type=extension_name, properties=properties) + @CustomExtension(type=extension_name, properties=properties) class NameExtension: extension_type = 'property-extension' diff --git a/stix2/v21/observables.py b/stix2/v21/observables.py index 44b346c..943e59f 100644 --- a/stix2/v21/observables.py +++ b/stix2/v21/observables.py @@ -6,10 +6,9 @@ _Observable and do not have a ``_type`` attribute. """ from collections import OrderedDict -from collections.abc import Mapping import itertools -from ..custom import _custom_extension_builder, _custom_observable_builder +from ..custom import _custom_observable_builder from ..exceptions import AtLeastOnePropertyError, DependentPropertiesError from ..properties import ( BinaryProperty, BooleanProperty, DictionaryProperty, @@ -19,9 +18,9 @@ from ..properties import ( TypeProperty, ) from .base import _Extension, _Observable, _STIXBase21 -from .common import GranularMarking +from .common import GranularMarking, CustomExtension from .vocab import ( - ACCOUNT_TYPE, ENCRYPTION_ALGORITHM, EXTENSION_TYPE, HASHING_ALGORITHM, + ACCOUNT_TYPE, ENCRYPTION_ALGORITHM, HASHING_ALGORITHM, NETWORK_SOCKET_ADDRESS_FAMILY, NETWORK_SOCKET_TYPE, WINDOWS_INTEGRITY_LEVEL, WINDOWS_PEBINARY_TYPE, WINDOWS_REGISTRY_DATATYPE, WINDOWS_SERVICE_START_TYPE, WINDOWS_SERVICE_STATUS, WINDOWS_SERVICE_TYPE, @@ -898,25 +897,3 @@ def CustomObservable(type='x-custom-observable', properties=None, id_contrib_pro return wrapper -def CustomExtension(type='x-custom-observable-ext', properties=None): - """Custom STIX Object Extension decorator. - """ - def wrapper(cls): - - # Auto-create an "extension_type" property from the class attribute, if - # it exists. - extension_type = getattr(cls, "extension_type", None) - if extension_type: - extension_type_prop = EnumProperty( - EXTENSION_TYPE, - required=False, - fixed=extension_type, - ) - - if isinstance(properties, Mapping): - properties["extension_type"] = extension_type_prop - else: - properties.append(("extension_type", extension_type_prop)) - - return _custom_extension_builder(cls, type, properties, '2.1', _Extension) - return wrapper diff --git a/stix2/v21/sdo.py b/stix2/v21/sdo.py index bfa741f..d4ff34e 100644 --- a/stix2/v21/sdo.py +++ b/stix2/v21/sdo.py @@ -7,7 +7,6 @@ import warnings from stix2patterns.validator import run_validator -from . import observables from ..custom import _custom_object_builder from ..exceptions import ( InvalidValueError, PropertyPresenceError, STIXDeprecationWarning, @@ -20,7 +19,9 @@ from ..properties import ( ) from ..utils import NOW from .base import _DomainObject -from .common import ExternalReference, GranularMarking, KillChainPhase +from .common import ( + CustomExtension, ExternalReference, GranularMarking, KillChainPhase +) from .vocab import ( ATTACK_MOTIVATION, ATTACK_RESOURCE_LEVEL, GROUPING_CONTEXT, IDENTITY_CLASS, IMPLEMENTATION_LANGUAGE, INDICATOR_TYPE, INDUSTRY_SECTOR, @@ -858,7 +859,7 @@ def CustomObject(type='x-custom-type', properties=None, extension_name=None, is_ ]), ) if extension_name: - @observables.CustomExtension(type=extension_name, properties=extension_properties) + @CustomExtension(type=extension_name, properties=extension_properties) class NameExtension: if is_sdo: extension_type = 'new-sdo' From 93d2524d45b964ec4d16d76bd7d3c7a10604a513 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Fri, 2 Jul 2021 21:10:52 -0400 Subject: [PATCH 2/5] Remove excessive nested lists from CusomObservable decorator. Remove iterable chaining from CustomObject decorator. If all values are guaranteed lists now, it no longer makes sense to use it. Simpler and clearer to use plain old list concatenation. --- stix2/v21/observables.py | 22 +++++++++++-------- stix2/v21/sdo.py | 46 +++++++++++++++++++--------------------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/stix2/v21/observables.py b/stix2/v21/observables.py index 943e59f..414a0b0 100644 --- a/stix2/v21/observables.py +++ b/stix2/v21/observables.py @@ -873,16 +873,20 @@ def CustomObservable(type='x-custom-observable', properties=None, id_contrib_pro """ def wrapper(cls): _properties = list( - itertools.chain.from_iterable([ - [('type', TypeProperty(type, spec_version='2.1'))], - [('spec_version', StringProperty(fixed='2.1'))], - [('id', IDProperty(type, spec_version='2.1'))], + itertools.chain( + [ + ('type', TypeProperty(type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(type, spec_version='2.1')) + ], properties, - [('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1')))], - [('granular_markings', ListProperty(GranularMarking))], - [('defanged', BooleanProperty(default=lambda: False))], - [('extensions', ExtensionsProperty(spec_version='2.1'))], - ]), + [ + ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), + ('granular_markings', ListProperty(GranularMarking)), + ('defanged', BooleanProperty(default=lambda: False)), + ('extensions', ExtensionsProperty(spec_version='2.1')), + ], + ), ) if extension_name: @CustomExtension(type=extension_name, properties=properties) diff --git a/stix2/v21/sdo.py b/stix2/v21/sdo.py index d4ff34e..5782252 100644 --- a/stix2/v21/sdo.py +++ b/stix2/v21/sdo.py @@ -1,7 +1,6 @@ """STIX 2.1 Domain Objects.""" from collections import OrderedDict -import itertools from urllib.parse import quote_plus import warnings @@ -834,30 +833,29 @@ def CustomObject(type='x-custom-type', properties=None, extension_name=None, is_ """ def wrapper(cls): extension_properties = [x for x in properties if not x[0].startswith('x_')] - _properties = list( - itertools.chain.from_iterable([ - [ - ('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', precision_constraint='min')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), - ], - extension_properties, - [ - ('revoked', BooleanProperty(default=lambda: False)), - ('labels', ListProperty(StringProperty)), - ('confidence', IntegerProperty()), - ('lang', StringProperty()), - ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), - ('granular_markings', ListProperty(GranularMarking)), - ('extensions', ExtensionsProperty(spec_version='2.1')), - ], - sorted([x for x in properties if x[0].startswith('x_')], key=lambda x: x[0]), - ]), + _properties = ( + [ + ('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', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ] + + extension_properties + + [ + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), + ('granular_markings', ListProperty(GranularMarking)), + ('extensions', ExtensionsProperty(spec_version='2.1')), + ] + + sorted((x for x in properties if x[0].startswith('x_')), key=lambda x: x[0]) ) + if extension_name: @CustomExtension(type=extension_name, properties=extension_properties) class NameExtension: From 8bbf5fa4616efafb93bc089c8f1d9c8c0e6c2043 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Tue, 6 Jul 2021 14:27:40 -0400 Subject: [PATCH 3/5] Make extension instances work the same as other objects, with respect to properties. Before, properties were declared on toplevel-property-extension extensions as if they were going to be used in the normal way (as actual properties on instances of the extension), but they were not used that way, and there was some ugly hackage to make it work. Despite the fact that property instances were given during extension registration, they were not used to typecheck, set defaults, etc on toplevel property extension properties. I changed how registration and object initialization works with respect to properties associated with extensions. Now, extensions work the same as any other object and code is cleaner. Property instances associated with registered toplevel extensions are used to enforce requirements like any other object. Added some unit tests specifically for property cleaning for extensions. Property order (for those contexts where it matters) is updated to be spec-defined, toplevel extension, custom. --- stix2/base.py | 104 +++++++++++------- stix2/custom.py | 39 ++++++- stix2/test/v21/test_custom.py | 193 +++++++++++++++++++++++++++++++++- stix2/v21/common.py | 18 +--- stix2/v21/observables.py | 4 +- stix2/v21/sdo.py | 2 +- 6 files changed, 297 insertions(+), 63 deletions(-) diff --git a/stix2/base.py b/stix2/base.py index 7c68002..69d21b3 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -1,5 +1,6 @@ """Base classes for type definitions in the STIX2 library.""" +import collections import collections.abc import copy import itertools @@ -36,14 +37,32 @@ class _STIXBase(collections.abc.Mapping): """Base class for STIX object types""" def object_properties(self): - props = set(self._properties.keys()) - custom_props = list(set(self._inner.keys()) - props) - custom_props.sort() + """ + Get a list of property names in a particular order: spec order for + spec defined properties, followed by toplevel-property-extension + properties (any order), followed by custom properties (any order). - all_properties = list(self._properties.keys()) - all_properties.extend(custom_props) # Any custom properties to the bottom + The returned list doesn't include only defined+extension properties, + nor does it include only assigned properties (i.e. those this object + actually possesses). It's a mix of both: the spec defined property + group and extension group include all of them, regardless of whether + they're present on this object; the custom group include only names of + properties present on this object. - return all_properties + :return: A list of property names + """ + if self.__property_order is None: + custom_props = sorted( + self.keys() - self._properties.keys() + - self.__ext_property_names + ) + + # Any custom properties to the bottom + self.__property_order = list(self._properties) \ + + list(self.__ext_property_names) \ + + custom_props + + return self.__property_order def _check_property(self, prop_name, prop, kwargs, allow_custom): if prop_name not in kwargs: @@ -131,46 +150,45 @@ class _STIXBase(collections.abc.Mapping): if custom_props and not isinstance(custom_props, dict): raise ValueError("'custom_properties' must be a dictionary") - # Detect any keyword arguments not allowed for a specific type. + # Detect any keyword arguments representing customization. # In STIX 2.1, this is complicated by "toplevel-property-extension" # type extensions, which can add extra properties which are *not* # considered custom. - extra_kwargs = kwargs.keys() - self._properties.keys() - extensions = kwargs.get("extensions") + registered_toplevel_extension_props = {} + has_unregistered_toplevel_extension = False if extensions: - has_unregistered_toplevel_extension = False - registered_toplevel_extension_props = set() - for ext_id, ext in extensions.items(): if ext.get("extension_type") == "toplevel-property-extension": registered_ext_class = class_for_type( ext_id, "2.1", "extensions", ) if registered_ext_class: - registered_toplevel_extension_props |= \ - registered_ext_class._properties.keys() + registered_toplevel_extension_props.update( + registered_ext_class._toplevel_properties + ) else: has_unregistered_toplevel_extension = True - if has_unregistered_toplevel_extension: - # Must assume all extras are extension properties, not custom. - extra_kwargs.clear() + if has_unregistered_toplevel_extension: + # Must assume all extras are extension properties, not custom. + custom_kwargs = set() - else: - # All toplevel property extensions (if any) have been - # registered. So we can tell what their properties are and - # treat only those as not custom. - extra_kwargs -= registered_toplevel_extension_props + else: + # All toplevel property extensions (if any) have been + # registered. So we can tell what their properties are and + # treat only those as not custom. + custom_kwargs = kwargs.keys() - self._properties.keys() \ + - registered_toplevel_extension_props.keys() - if extra_kwargs and not allow_custom: - raise ExtraPropertiesError(cls, extra_kwargs) + if custom_kwargs and not allow_custom: + raise ExtraPropertiesError(cls, custom_kwargs) if custom_props: # loophole for custom_properties... allow_custom = True - all_custom_prop_names = (extra_kwargs | custom_props.keys()) - \ + all_custom_prop_names = (custom_kwargs | custom_props.keys()) - \ self._properties.keys() if all_custom_prop_names: if not isinstance(self, stix2.v20._STIXBase20): @@ -181,6 +199,21 @@ class _STIXBase(collections.abc.Mapping): reason="Property name '%s' must begin with an alpha character." % prop_name, ) + # defined_properties = all properties defined on this type, plus all + # properties defined on this instance as a result of toplevel property + # extensions. + defined_properties = collections.ChainMap( + self._properties, registered_toplevel_extension_props + ) + + # object_properties() needs this; cache it here to avoid needing to + # recompute. + self.__ext_property_names = set(registered_toplevel_extension_props) + # object_properties() will compute this on first call, based on + # __ext_property_names above. Maybe it makes sense to not compute this + # unless really necessary. + self.__property_order = None + # Remove any keyword arguments whose value is None or [] (i.e. empty list) setting_kwargs = { k: v @@ -189,22 +222,15 @@ class _STIXBase(collections.abc.Mapping): } # Detect any missing required properties - required_properties = set(get_required_properties(self._properties)) - missing_kwargs = required_properties - set(setting_kwargs) + required_properties = set( + get_required_properties(defined_properties) + ) + missing_kwargs = required_properties - setting_kwargs.keys() if missing_kwargs: - # In this scenario, we are inside within the scope of the extension. - # It is possible to check if this is a new Extension Class by - # querying "extension_type". Note: There is an API limitation currently - # because a toplevel-property-extension cannot validate its parent properties - new_ext_check = ( - bool(getattr(self, "extension_type", None)) - and issubclass(cls, stix2.v21._Extension) - ) - if new_ext_check is False: - raise MissingPropertiesError(cls, missing_kwargs) + raise MissingPropertiesError(cls, missing_kwargs) has_custom = bool(all_custom_prop_names) - for prop_name, prop_metadata in self._properties.items(): + for prop_name, prop_metadata in defined_properties.items(): temp_custom = self._check_property( prop_name, prop_metadata, setting_kwargs, allow_custom, ) @@ -213,7 +239,7 @@ class _STIXBase(collections.abc.Mapping): # Cache defaulted optional properties for serialization defaulted = [] - for name, prop in self._properties.items(): + for name, prop in defined_properties.items(): try: if ( not prop.required and not hasattr(prop, '_fixed_value') and diff --git a/stix2/custom.py b/stix2/custom.py index 9b1654e..7806179 100644 --- a/stix2/custom.py +++ b/stix2/custom.py @@ -1,6 +1,7 @@ from collections import OrderedDict from .base import _cls_init +from .properties import EnumProperty from .registration import ( _get_extension_class, _register_extension, _register_marking, _register_object, _register_observable, @@ -93,12 +94,46 @@ def _custom_observable_builder(cls, type, properties, version, base_class, id_co def _custom_extension_builder(cls, type, properties, version, base_class): - prop_dict = _get_properties_dict(properties) + + properties = _get_properties_dict(properties) + toplevel_properties = None + + # Auto-create an "extension_type" property from the class attribute, if + # it exists. How to treat the other properties which were given depends on + # the extension type. + extension_type = getattr(cls, "extension_type", None) + if extension_type: + # I suppose I could also go with a plain string property, since the + # value is fixed... but an enum property seems more true to the + # property's semantics. Also, I can't import a vocab module for the + # enum values without circular import errors. :( + extension_type_prop = EnumProperty( + [ + "new-sdo", "new-sco", "new-sro", "property-extension", + "toplevel-property-extension" + ], + required=False, + fixed=extension_type, + ) + + nested_properties = { + "extension_type": extension_type_prop + } + + if extension_type == "toplevel-property-extension": + toplevel_properties = properties + else: + nested_properties.update(properties) + + else: + nested_properties = properties class _CustomExtension(cls, base_class): _type = type - _properties = prop_dict + _properties = nested_properties + if extension_type == "toplevel-property-extension": + _toplevel_properties = toplevel_properties def __init__(self, **kwargs): base_class.__init__(self, **kwargs) diff --git a/stix2/test/v21/test_custom.py b/stix2/test/v21/test_custom.py index 1b3166f..7aed1b7 100644 --- a/stix2/test/v21/test_custom.py +++ b/stix2/test/v21/test_custom.py @@ -1,3 +1,4 @@ +import contextlib import uuid import pytest @@ -8,7 +9,9 @@ import stix2.registration import stix2.registry import stix2.v21 -from ...exceptions import DuplicateRegistrationError, InvalidValueError +from ...exceptions import ( + DuplicateRegistrationError, InvalidValueError, MissingPropertiesError +) from .constants import FAKE_TIME, IDENTITY_ID, MARKING_DEFINITION_ID # Custom Properties in SDOs @@ -1675,6 +1678,194 @@ def test_registered_new_extension_marking_allow_custom_false(): '{"extension_type": "property-extension", "some_marking_field": "value"}}' in marking_serialized +@contextlib.contextmanager +def _register_extension(ext, props): + + ext_def_id = "extension-definition--" + str(uuid.uuid4()) + + stix2.v21.CustomExtension( + ext_def_id, + props + )(ext) + + try: + yield ext_def_id + finally: + # "unregister" the extension + del stix2.registry.STIX2_OBJ_MAPS["2.1"]["extensions"][ext_def_id] + + +def test_nested_ext_prop_meta(): + + class TestExt: + extension_type = "property-extension" + + props = { + "intprop": stix2.properties.IntegerProperty(required=True), + "strprop": stix2.properties.StringProperty( + required=False, default=lambda: "foo" + ) + } + + with _register_extension(TestExt, props) as ext_def_id: + + obj = stix2.v21.Identity( + name="test", + extensions={ + ext_def_id: { + "extension_type": "property-extension", + "intprop": "1", + "strprop": 2 + } + } + ) + + assert obj.extensions[ext_def_id].extension_type == "property-extension" + assert obj.extensions[ext_def_id].intprop == 1 + assert obj.extensions[ext_def_id].strprop == "2" + + obj = stix2.v21.Identity( + name="test", + extensions={ + ext_def_id: { + "extension_type": "property-extension", + "intprop": "1", + } + } + ) + + # Ensure default kicked in + assert obj.extensions[ext_def_id].strprop == "foo" + + with pytest.raises(InvalidValueError): + stix2.v21.Identity( + name="test", + extensions={ + ext_def_id: { + "extension_type": "property-extension", + # wrong value type + "intprop": "foo" + } + } + ) + + with pytest.raises(InvalidValueError): + stix2.v21.Identity( + name="test", + extensions={ + ext_def_id: { + "extension_type": "property-extension", + # missing required property + "strprop": "foo" + } + } + ) + + with pytest.raises(InvalidValueError): + stix2.v21.Identity( + name="test", + extensions={ + ext_def_id: { + "extension_type": "property-extension", + "intprop": 1, + # Use of undefined property + "foo": False, + } + } + ) + + with pytest.raises(InvalidValueError): + stix2.v21.Identity( + name="test", + extensions={ + ext_def_id: { + # extension_type doesn't match with registration + "extension_type": "new-sdo", + "intprop": 1, + "strprop": "foo", + } + } + ) + + +def test_toplevel_ext_prop_meta(): + + class TestExt: + extension_type = "toplevel-property-extension" + + props = { + "intprop": stix2.properties.IntegerProperty(required=True), + "strprop": stix2.properties.StringProperty( + required=False, default=lambda: "foo" + ) + } + + with _register_extension(TestExt, props) as ext_def_id: + + obj = stix2.v21.Identity( + name="test", + intprop="1", + strprop=2, + extensions={ + ext_def_id: { + "extension_type": "toplevel-property-extension" + } + } + ) + + assert obj.extensions[ext_def_id].extension_type == "toplevel-property-extension" + assert obj.intprop == 1 + assert obj.strprop == "2" + + obj = stix2.v21.Identity( + name="test", + intprop=1, + extensions={ + ext_def_id: { + "extension_type": "toplevel-property-extension" + } + } + ) + + # Ensure default kicked in + assert obj.strprop == "foo" + + with pytest.raises(InvalidValueError): + stix2.v21.Identity( + name="test", + intprop="foo", # wrong value type + extensions={ + ext_def_id: { + "extension_type": "toplevel-property-extension" + } + } + ) + + with pytest.raises(InvalidValueError): + stix2.v21.Identity( + name="test", + intprop=1, + extensions={ + ext_def_id: { + "extension_type": "toplevel-property-extension", + # Use of undefined property + "foo": False, + } + } + ) + + with pytest.raises(MissingPropertiesError): + stix2.v21.Identity( + name="test", + strprop="foo", # missing required property + extensions={ + ext_def_id: { + "extension_type": "toplevel-property-extension" + } + } + ) + + def test_allow_custom_propagation(): obj_dict = { "type": "bundle", diff --git a/stix2/v21/common.py b/stix2/v21/common.py index 6b2922b..9ba1096 100644 --- a/stix2/v21/common.py +++ b/stix2/v21/common.py @@ -1,7 +1,6 @@ """STIX 2.1 Common Data Types and Properties.""" from collections import OrderedDict -from collections.abc import Mapping from . import _Extension from ..custom import _custom_marking_builder, _custom_extension_builder @@ -145,23 +144,8 @@ def CustomExtension(type='x-custom-ext', properties=None): """Custom STIX Object Extension decorator. """ def wrapper(cls): - - # Auto-create an "extension_type" property from the class attribute, if - # it exists. - extension_type = getattr(cls, "extension_type", None) - if extension_type: - extension_type_prop = EnumProperty( - EXTENSION_TYPE, - required=False, - fixed=extension_type, - ) - - if isinstance(properties, Mapping): - properties["extension_type"] = extension_type_prop - else: - properties.append(("extension_type", extension_type_prop)) - return _custom_extension_builder(cls, type, properties, '2.1', _Extension) + return wrapper diff --git a/stix2/v21/observables.py b/stix2/v21/observables.py index 414a0b0..3b0d35a 100644 --- a/stix2/v21/observables.py +++ b/stix2/v21/observables.py @@ -889,7 +889,7 @@ def CustomObservable(type='x-custom-observable', properties=None, id_contrib_pro ), ) if extension_name: - @CustomExtension(type=extension_name, properties=properties) + @CustomExtension(type=extension_name, properties={}) class NameExtension: extension_type = 'new-sco' @@ -899,5 +899,3 @@ def CustomObservable(type='x-custom-observable', properties=None, id_contrib_pro cls.with_extension = extension_name return _custom_observable_builder(cls, type, _properties, '2.1', _Observable, id_contrib_props) return wrapper - - diff --git a/stix2/v21/sdo.py b/stix2/v21/sdo.py index 5782252..4a37e21 100644 --- a/stix2/v21/sdo.py +++ b/stix2/v21/sdo.py @@ -857,7 +857,7 @@ def CustomObject(type='x-custom-type', properties=None, extension_name=None, is_ ) if extension_name: - @CustomExtension(type=extension_name, properties=extension_properties) + @CustomExtension(type=extension_name, properties={}) class NameExtension: if is_sdo: extension_type = 'new-sdo' From 2cda97cf5e51403c43a96ceb9073afab7706638a Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Tue, 6 Jul 2021 20:32:58 -0400 Subject: [PATCH 4/5] Changed STIX object initialization to formulate a property order and process properties in that order. This establishes iteration order on object properties, making the object_properties() method unnecessary. So the latter method has been deleted. All uses of that method have been removed. Removed unnecessary deepcopy() in STIXJSONEncoder, to improve efficiency. This uncovered a bug which had been affecting STIXdatetime instances. Not deepcopying doesn't trip the bug, which can change serialization format. This caused a unit test to fail, which was checking serialization format. I fixed the unit test. Fixed a bug in _STIXBase.__repr__ which caused it to omit all properties with falsey values. This caused several unit tests to break, since they were written against the old buggy repr format. Notably, 'revoked=False' was never included in reprs before, but it is now. --- stix2/base.py | 77 ++++++++------------- stix2/serialization.py | 4 +- stix2/test/v20/test_identity.py | 2 +- stix2/test/v20/test_indicator.py | 1 + stix2/test/v20/test_markings.py | 2 +- stix2/test/v21/test_extension_definition.py | 1 - stix2/test/v21/test_identity.py | 1 - stix2/test/v21/test_incident.py | 1 - stix2/test/v21/test_indicator.py | 3 +- stix2/test/v21/test_location.py | 6 +- stix2/test/v21/test_note.py | 1 + stix2/test/v21/test_opinion.py | 3 +- 12 files changed, 41 insertions(+), 61 deletions(-) diff --git a/stix2/base.py b/stix2/base.py index 69d21b3..24176f4 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -36,34 +36,6 @@ def get_required_properties(properties): class _STIXBase(collections.abc.Mapping): """Base class for STIX object types""" - def object_properties(self): - """ - Get a list of property names in a particular order: spec order for - spec defined properties, followed by toplevel-property-extension - properties (any order), followed by custom properties (any order). - - The returned list doesn't include only defined+extension properties, - nor does it include only assigned properties (i.e. those this object - actually possesses). It's a mix of both: the spec defined property - group and extension group include all of them, regardless of whether - they're present on this object; the custom group include only names of - properties present on this object. - - :return: A list of property names - """ - if self.__property_order is None: - custom_props = sorted( - self.keys() - self._properties.keys() - - self.__ext_property_names - ) - - # Any custom properties to the bottom - self.__property_order = list(self._properties) \ - + list(self.__ext_property_names) \ - + custom_props - - return self.__property_order - def _check_property(self, prop_name, prop, kwargs, allow_custom): if prop_name not in kwargs: if hasattr(prop, 'default'): @@ -206,20 +178,33 @@ class _STIXBase(collections.abc.Mapping): self._properties, registered_toplevel_extension_props ) - # object_properties() needs this; cache it here to avoid needing to - # recompute. - self.__ext_property_names = set(registered_toplevel_extension_props) - # object_properties() will compute this on first call, based on - # __ext_property_names above. Maybe it makes sense to not compute this - # unless really necessary. - self.__property_order = None + assigned_properties = collections.ChainMap(kwargs, custom_props) - # Remove any keyword arguments whose value is None or [] (i.e. empty list) - setting_kwargs = { - k: v - for k, v in itertools.chain(kwargs.items(), custom_props.items()) - if v is not None and v != [] - } + # Establish property order: spec-defined, toplevel extension, custom. + toplevel_extension_props = registered_toplevel_extension_props.keys() \ + | (kwargs.keys() - self._properties.keys() - custom_kwargs) + property_order = itertools.chain( + self._properties, + toplevel_extension_props, + sorted(all_custom_prop_names) + ) + + setting_kwargs = {} + + has_custom = bool(all_custom_prop_names) + for prop_name in property_order: + + prop_val = assigned_properties.get(prop_name) + if prop_val not in (None, []): + setting_kwargs[prop_name] = prop_val + + prop = defined_properties.get(prop_name) + if prop: + temp_custom = self._check_property( + prop_name, prop, setting_kwargs, allow_custom, + ) + + has_custom = has_custom or temp_custom # Detect any missing required properties required_properties = set( @@ -229,14 +214,6 @@ class _STIXBase(collections.abc.Mapping): if missing_kwargs: raise MissingPropertiesError(cls, missing_kwargs) - has_custom = bool(all_custom_prop_names) - for prop_name, prop_metadata in defined_properties.items(): - temp_custom = self._check_property( - prop_name, prop_metadata, setting_kwargs, allow_custom, - ) - - has_custom = has_custom or temp_custom - # Cache defaulted optional properties for serialization defaulted = [] for name, prop in defined_properties.items(): @@ -304,7 +281,7 @@ class _STIXBase(collections.abc.Mapping): return self.serialize() def __repr__(self): - props = ', '.join([f"{k}={self[k]!r}" for k in self.object_properties() if self.get(k)]) + props = ', '.join([f"{k}={self[k]!r}" for k in self]) return f'{self.__class__.__name__}({props})' def __deepcopy__(self, memo): diff --git a/stix2/serialization.py b/stix2/serialization.py index 2784d39..236b987 100644 --- a/stix2/serialization.py +++ b/stix2/serialization.py @@ -24,7 +24,7 @@ class STIXJSONEncoder(json.JSONEncoder): if isinstance(obj, (dt.date, dt.datetime)): return format_datetime(obj) elif isinstance(obj, stix2.base._STIXBase): - tmp_obj = dict(copy.deepcopy(obj)) + tmp_obj = dict(obj) for prop_name in obj._defaulted_optional_properties: del tmp_obj[prop_name] return tmp_obj @@ -177,7 +177,7 @@ def find_property_index(obj, search_key, search_value): if isinstance(obj, stix2.base._STIXBase): if search_key in obj and obj[search_key] == search_value: - idx = _find(obj.object_properties(), search_key) + idx = _find(list(obj), search_key) else: idx = _find_property_in_seq(obj.values(), search_key, search_value) elif isinstance(obj, dict): diff --git a/stix2/test/v20/test_identity.py b/stix2/test/v20/test_identity.py index c62da46..93787b3 100644 --- a/stix2/test/v20/test_identity.py +++ b/stix2/test/v20/test_identity.py @@ -74,6 +74,6 @@ def test_identity_with_custom(): ) assert identity.x_foo == "bar" - assert "x_foo" in identity.object_properties() + assert "x_foo" in identity # TODO: Add other examples diff --git a/stix2/test/v20/test_indicator.py b/stix2/test/v20/test_indicator.py index 47f4812..10a7015 100644 --- a/stix2/test/v20/test_indicator.py +++ b/stix2/test/v20/test_indicator.py @@ -28,6 +28,7 @@ EXPECTED_INDICATOR_REPR = "Indicator(" + " ".join( modified='2017-01-01T00:00:01.000Z', pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", valid_from='1970-01-01T00:00:01Z', + revoked=False, labels=['malicious-activity'] """.split(), ) + ")" diff --git a/stix2/test/v20/test_markings.py b/stix2/test/v20/test_markings.py index f7d15fa..70b46a6 100644 --- a/stix2/test/v20/test_markings.py +++ b/stix2/test/v20/test_markings.py @@ -21,7 +21,7 @@ EXPECTED_TLP_MARKING_DEFINITION = """{ EXPECTED_STATEMENT_MARKING_DEFINITION = """{ "type": "marking-definition", "id": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", - "created": "2017-01-20T00:00:00Z", + "created": "2017-01-20T00:00:00.000Z", "definition_type": "statement", "definition": { "statement": "Copyright 2016, Example Corp" diff --git a/stix2/test/v21/test_extension_definition.py b/stix2/test/v21/test_extension_definition.py index 97513c4..af13425 100644 --- a/stix2/test/v21/test_extension_definition.py +++ b/stix2/test/v21/test_extension_definition.py @@ -105,4 +105,3 @@ def test_extension_definition_with_custom(): ) assert extension_definition.x_foo == "bar" - assert "x_foo" in extension_definition.object_properties() diff --git a/stix2/test/v21/test_identity.py b/stix2/test/v21/test_identity.py index c235b4d..2f8747c 100644 --- a/stix2/test/v21/test_identity.py +++ b/stix2/test/v21/test_identity.py @@ -77,6 +77,5 @@ def test_identity_with_custom(): ) assert identity.x_foo == "bar" - assert "x_foo" in identity.object_properties() # TODO: Add other examples diff --git a/stix2/test/v21/test_incident.py b/stix2/test/v21/test_incident.py index 27bc254..fac9238 100644 --- a/stix2/test/v21/test_incident.py +++ b/stix2/test/v21/test_incident.py @@ -78,4 +78,3 @@ def test_incident_with_custom(): ) assert incident.x_foo == "bar" - assert "x_foo" in incident.object_properties() diff --git a/stix2/test/v21/test_indicator.py b/stix2/test/v21/test_indicator.py index e42ffba..6175948 100644 --- a/stix2/test/v21/test_indicator.py +++ b/stix2/test/v21/test_indicator.py @@ -30,7 +30,8 @@ EXPECTED_INDICATOR_REPR = "Indicator(" + " ".join( pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", pattern_type='stix', pattern_version='2.1', - valid_from='1970-01-01T00:00:01Z' + valid_from='1970-01-01T00:00:01Z', + revoked=False """.split(), ) + ")" diff --git a/stix2/test/v21/test_location.py b/stix2/test/v21/test_location.py index c2df5d3..5d3eab8 100644 --- a/stix2/test/v21/test_location.py +++ b/stix2/test/v21/test_location.py @@ -27,7 +27,8 @@ EXPECTED_LOCATION_1_REPR = "Location(" + " ".join( created='2016-04-06T20:03:00.000Z', modified='2016-04-06T20:03:00.000Z', latitude=48.8566, - longitude=2.3522""".split(), + longitude=2.3522, + revoked=False""".split(), ) + ")" EXPECTED_LOCATION_2 = """{ @@ -47,7 +48,8 @@ EXPECTED_LOCATION_2_REPR = "Location(" + " ".join( id='location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64', created='2016-04-06T20:03:00.000Z', modified='2016-04-06T20:03:00.000Z', - region='northern-america'""".split(), + region='northern-america', + revoked=False""".split(), ) + ")" diff --git a/stix2/test/v21/test_note.py b/stix2/test/v21/test_note.py index 2f20c8d..ca1fc6d 100644 --- a/stix2/test/v21/test_note.py +++ b/stix2/test/v21/test_note.py @@ -48,6 +48,7 @@ EXPECTED_OPINION_REPR = "Note(" + " ".join(( content='%s', authors=['John Doe'], object_refs=['campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f'], + revoked=False, external_references=[ExternalReference(source_name='job-tracker', external_id='job-id-1234')] """ % CONTENT ).split()) + ")" diff --git a/stix2/test/v21/test_opinion.py b/stix2/test/v21/test_opinion.py index 21c96d7..31cd0af 100644 --- a/stix2/test/v21/test_opinion.py +++ b/stix2/test/v21/test_opinion.py @@ -38,7 +38,8 @@ EXPECTED_OPINION_REPR = "Opinion(" + " ".join(( modified='2016-05-12T08:17:27.000Z', explanation="%s", opinion='strongly-disagree', - object_refs=['relationship--16d2358f-3b0d-4c88-b047-0da2f7ed4471'] + object_refs=['relationship--16d2358f-3b0d-4c88-b047-0da2f7ed4471'], + revoked=False """ % EXPLANATION ).split()) + ")" From 99a8ade4cd3ebc69d62b688da5c21e56c93106f7 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Tue, 6 Jul 2021 20:40:50 -0400 Subject: [PATCH 5/5] pre-commit stylistic fixes --- stix2/base.py | 8 ++-- stix2/custom.py | 4 +- stix2/serialization.py | 1 - stix2/test/v21/test_custom.py | 70 +++++++++++++++++------------------ stix2/v21/__init__.py | 15 ++++---- stix2/v21/common.py | 4 +- stix2/v21/observables.py | 4 +- stix2/v21/sdo.py | 2 +- 8 files changed, 52 insertions(+), 56 deletions(-) diff --git a/stix2/base.py b/stix2/base.py index 24176f4..3ff01d9 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -137,7 +137,7 @@ class _STIXBase(collections.abc.Mapping): ) if registered_ext_class: registered_toplevel_extension_props.update( - registered_ext_class._toplevel_properties + registered_ext_class._toplevel_properties, ) else: has_unregistered_toplevel_extension = True @@ -175,7 +175,7 @@ class _STIXBase(collections.abc.Mapping): # properties defined on this instance as a result of toplevel property # extensions. defined_properties = collections.ChainMap( - self._properties, registered_toplevel_extension_props + self._properties, registered_toplevel_extension_props, ) assigned_properties = collections.ChainMap(kwargs, custom_props) @@ -186,7 +186,7 @@ class _STIXBase(collections.abc.Mapping): property_order = itertools.chain( self._properties, toplevel_extension_props, - sorted(all_custom_prop_names) + sorted(all_custom_prop_names), ) setting_kwargs = {} @@ -208,7 +208,7 @@ class _STIXBase(collections.abc.Mapping): # Detect any missing required properties required_properties = set( - get_required_properties(defined_properties) + get_required_properties(defined_properties), ) missing_kwargs = required_properties - setting_kwargs.keys() if missing_kwargs: diff --git a/stix2/custom.py b/stix2/custom.py index 7806179..adef768 100644 --- a/stix2/custom.py +++ b/stix2/custom.py @@ -110,14 +110,14 @@ def _custom_extension_builder(cls, type, properties, version, base_class): extension_type_prop = EnumProperty( [ "new-sdo", "new-sco", "new-sro", "property-extension", - "toplevel-property-extension" + "toplevel-property-extension", ], required=False, fixed=extension_type, ) nested_properties = { - "extension_type": extension_type_prop + "extension_type": extension_type_prop, } if extension_type == "toplevel-property-extension": diff --git a/stix2/serialization.py b/stix2/serialization.py index 236b987..7510b37 100644 --- a/stix2/serialization.py +++ b/stix2/serialization.py @@ -1,6 +1,5 @@ """STIX2 core serialization methods.""" -import copy import datetime as dt import io diff --git a/stix2/test/v21/test_custom.py b/stix2/test/v21/test_custom.py index 7aed1b7..f35b02d 100644 --- a/stix2/test/v21/test_custom.py +++ b/stix2/test/v21/test_custom.py @@ -10,7 +10,7 @@ import stix2.registry import stix2.v21 from ...exceptions import ( - DuplicateRegistrationError, InvalidValueError, MissingPropertiesError + DuplicateRegistrationError, InvalidValueError, MissingPropertiesError, ) from .constants import FAKE_TIME, IDENTITY_ID, MARKING_DEFINITION_ID @@ -1685,7 +1685,7 @@ def _register_extension(ext, props): stix2.v21.CustomExtension( ext_def_id, - props + props, )(ext) try: @@ -1703,8 +1703,8 @@ def test_nested_ext_prop_meta(): props = { "intprop": stix2.properties.IntegerProperty(required=True), "strprop": stix2.properties.StringProperty( - required=False, default=lambda: "foo" - ) + required=False, default=lambda: "foo", + ), } with _register_extension(TestExt, props) as ext_def_id: @@ -1715,9 +1715,9 @@ def test_nested_ext_prop_meta(): ext_def_id: { "extension_type": "property-extension", "intprop": "1", - "strprop": 2 - } - } + "strprop": 2, + }, + }, ) assert obj.extensions[ext_def_id].extension_type == "property-extension" @@ -1730,8 +1730,8 @@ def test_nested_ext_prop_meta(): ext_def_id: { "extension_type": "property-extension", "intprop": "1", - } - } + }, + }, ) # Ensure default kicked in @@ -1744,9 +1744,9 @@ def test_nested_ext_prop_meta(): ext_def_id: { "extension_type": "property-extension", # wrong value type - "intprop": "foo" - } - } + "intprop": "foo", + }, + }, ) with pytest.raises(InvalidValueError): @@ -1756,9 +1756,9 @@ def test_nested_ext_prop_meta(): ext_def_id: { "extension_type": "property-extension", # missing required property - "strprop": "foo" - } - } + "strprop": "foo", + }, + }, ) with pytest.raises(InvalidValueError): @@ -1770,8 +1770,8 @@ def test_nested_ext_prop_meta(): "intprop": 1, # Use of undefined property "foo": False, - } - } + }, + }, ) with pytest.raises(InvalidValueError): @@ -1783,8 +1783,8 @@ def test_nested_ext_prop_meta(): "extension_type": "new-sdo", "intprop": 1, "strprop": "foo", - } - } + }, + }, ) @@ -1796,8 +1796,8 @@ def test_toplevel_ext_prop_meta(): props = { "intprop": stix2.properties.IntegerProperty(required=True), "strprop": stix2.properties.StringProperty( - required=False, default=lambda: "foo" - ) + required=False, default=lambda: "foo", + ), } with _register_extension(TestExt, props) as ext_def_id: @@ -1808,9 +1808,9 @@ def test_toplevel_ext_prop_meta(): strprop=2, extensions={ ext_def_id: { - "extension_type": "toplevel-property-extension" - } - } + "extension_type": "toplevel-property-extension", + }, + }, ) assert obj.extensions[ext_def_id].extension_type == "toplevel-property-extension" @@ -1822,9 +1822,9 @@ def test_toplevel_ext_prop_meta(): intprop=1, extensions={ ext_def_id: { - "extension_type": "toplevel-property-extension" - } - } + "extension_type": "toplevel-property-extension", + }, + }, ) # Ensure default kicked in @@ -1836,9 +1836,9 @@ def test_toplevel_ext_prop_meta(): intprop="foo", # wrong value type extensions={ ext_def_id: { - "extension_type": "toplevel-property-extension" - } - } + "extension_type": "toplevel-property-extension", + }, + }, ) with pytest.raises(InvalidValueError): @@ -1850,8 +1850,8 @@ def test_toplevel_ext_prop_meta(): "extension_type": "toplevel-property-extension", # Use of undefined property "foo": False, - } - } + }, + }, ) with pytest.raises(MissingPropertiesError): @@ -1860,9 +1860,9 @@ def test_toplevel_ext_prop_meta(): strprop="foo", # missing required property extensions={ ext_def_id: { - "extension_type": "toplevel-property-extension" - } - } + "extension_type": "toplevel-property-extension", + }, + }, ) diff --git a/stix2/v21/__init__.py b/stix2/v21/__init__.py index ece8b36..2d29322 100644 --- a/stix2/v21/__init__.py +++ b/stix2/v21/__init__.py @@ -25,14 +25,13 @@ from .common import ( ) from .observables import ( URL, AlternateDataStream, ArchiveExt, Artifact, AutonomousSystem, - CustomObservable, Directory, DomainName, EmailAddress, - EmailMessage, EmailMIMEComponent, File, HTTPRequestExt, ICMPExt, - IPv4Address, IPv6Address, MACAddress, Mutex, NetworkTraffic, NTFSExt, - PDFExt, Process, RasterImageExt, SocketExt, Software, TCPExt, - UNIXAccountExt, UserAccount, WindowsPEBinaryExt, - WindowsPEOptionalHeaderType, WindowsPESection, WindowsProcessExt, - WindowsRegistryKey, WindowsRegistryValueType, WindowsServiceExt, - X509Certificate, X509V3ExtensionsType, + CustomObservable, Directory, DomainName, EmailAddress, EmailMessage, + EmailMIMEComponent, File, HTTPRequestExt, ICMPExt, IPv4Address, + IPv6Address, MACAddress, Mutex, NetworkTraffic, NTFSExt, PDFExt, Process, + RasterImageExt, SocketExt, Software, TCPExt, UNIXAccountExt, UserAccount, + WindowsPEBinaryExt, WindowsPEOptionalHeaderType, WindowsPESection, + WindowsProcessExt, WindowsRegistryKey, WindowsRegistryValueType, + WindowsServiceExt, X509Certificate, X509V3ExtensionsType, ) from .sdo import ( AttackPattern, Campaign, CourseOfAction, CustomObject, Grouping, Identity, diff --git a/stix2/v21/common.py b/stix2/v21/common.py index 9ba1096..b5f9c62 100644 --- a/stix2/v21/common.py +++ b/stix2/v21/common.py @@ -3,7 +3,7 @@ from collections import OrderedDict from . import _Extension -from ..custom import _custom_marking_builder, _custom_extension_builder +from ..custom import _custom_extension_builder, _custom_marking_builder from ..exceptions import InvalidValueError, PropertyPresenceError from ..markings import _MarkingsMixin from ..markings.utils import check_tlp_marking @@ -268,8 +268,6 @@ def CustomMarking(type='x-custom-marking', properties=None, extension_name=None) """ def wrapper(cls): if extension_name: - from . import observables - @CustomExtension(type=extension_name, properties=properties) class NameExtension: extension_type = 'property-extension' diff --git a/stix2/v21/observables.py b/stix2/v21/observables.py index 3b0d35a..4398772 100644 --- a/stix2/v21/observables.py +++ b/stix2/v21/observables.py @@ -18,7 +18,7 @@ from ..properties import ( TypeProperty, ) from .base import _Extension, _Observable, _STIXBase21 -from .common import GranularMarking, CustomExtension +from .common import CustomExtension, GranularMarking from .vocab import ( ACCOUNT_TYPE, ENCRYPTION_ALGORITHM, HASHING_ALGORITHM, NETWORK_SOCKET_ADDRESS_FAMILY, NETWORK_SOCKET_TYPE, @@ -877,7 +877,7 @@ def CustomObservable(type='x-custom-observable', properties=None, id_contrib_pro [ ('type', TypeProperty(type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), - ('id', IDProperty(type, spec_version='2.1')) + ('id', IDProperty(type, spec_version='2.1')), ], properties, [ diff --git a/stix2/v21/sdo.py b/stix2/v21/sdo.py index 4a37e21..3fd4e04 100644 --- a/stix2/v21/sdo.py +++ b/stix2/v21/sdo.py @@ -19,7 +19,7 @@ from ..properties import ( from ..utils import NOW from .base import _DomainObject from .common import ( - CustomExtension, ExternalReference, GranularMarking, KillChainPhase + CustomExtension, ExternalReference, GranularMarking, KillChainPhase, ) from .vocab import ( ATTACK_MOTIVATION, ATTACK_RESOURCE_LEVEL, GROUPING_CONTEXT, IDENTITY_CLASS,