diff --git a/stix2/base.py b/stix2/base.py index 7c68002..3ff01d9 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 @@ -35,16 +36,6 @@ def get_required_properties(properties): 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() - - all_properties = list(self._properties.keys()) - all_properties.extend(custom_props) # Any custom properties to the bottom - - return all_properties - def _check_property(self, prop_name, prop, kwargs, allow_custom): if prop_name not in kwargs: if hasattr(prop, 'default'): @@ -131,46 +122,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,39 +171,52 @@ class _STIXBase(collections.abc.Mapping): reason="Property name '%s' must begin with an alpha character." % prop_name, ) - # Remove any keyword arguments whose value is None or [] (i.e. empty list) - setting_kwargs = { - k: v - for k, v in itertools.chain(kwargs.items(), custom_props.items()) - if v is not None and v != [] - } + # 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, + ) - # Detect any missing required properties - required_properties = set(get_required_properties(self._properties)) - missing_kwargs = required_properties - set(setting_kwargs) - 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) + assigned_properties = collections.ChainMap(kwargs, custom_props) + + # 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, prop_metadata in self._properties.items(): - temp_custom = self._check_property( - prop_name, prop_metadata, setting_kwargs, allow_custom, - ) + for prop_name in property_order: - has_custom = has_custom or temp_custom + 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( + get_required_properties(defined_properties), + ) + missing_kwargs = required_properties - setting_kwargs.keys() + if missing_kwargs: + raise MissingPropertiesError(cls, missing_kwargs) # 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 @@ -278,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/custom.py b/stix2/custom.py index 9b1654e..adef768 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/serialization.py b/stix2/serialization.py index 2784d39..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 @@ -24,7 +23,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 +176,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_custom.py b/stix2/test/v21/test_custom.py index 1b3166f..f35b02d 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/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()) + ")" diff --git a/stix2/v21/__init__.py b/stix2/v21/__init__.py index 977fdf7..2d29322 100644 --- a/stix2/v21/__init__.py +++ b/stix2/v21/__init__.py @@ -19,20 +19,19 @@ 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, - 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 e62bdae..b5f9c62 100644 --- a/stix2/v21/common.py +++ b/stix2/v21/common.py @@ -2,7 +2,8 @@ from collections import OrderedDict -from ..custom import _custom_marking_builder +from . import _Extension +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 @@ -139,6 +140,15 @@ class ExtensionDefinition(_STIXBase21): ]) +def CustomExtension(type='x-custom-ext', properties=None): + """Custom STIX Object Extension decorator. + """ + def wrapper(cls): + 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 `__. @@ -258,9 +268,7 @@ def CustomMarking(type='x-custom-marking', properties=None, extension_name=None) """ def wrapper(cls): 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 9a02150..6691612 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 CustomExtension, GranularMarking 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, @@ -874,19 +873,23 @@ 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) + @CustomExtension(type=extension_name, properties={}) class NameExtension: extension_type = 'new-sco' @@ -896,27 +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 - - -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..3fd4e04 100644 --- a/stix2/v21/sdo.py +++ b/stix2/v21/sdo.py @@ -1,13 +1,11 @@ """STIX 2.1 Domain Objects.""" from collections import OrderedDict -import itertools from urllib.parse import quote_plus 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 +18,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, @@ -833,32 +833,31 @@ 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: - @observables.CustomExtension(type=extension_name, properties=extension_properties) + @CustomExtension(type=extension_name, properties={}) class NameExtension: if is_sdo: extension_type = 'new-sdo'