Repository: oasis-open/cti-python-stix2

Issue:
Custom SCOs don't support `_id_contributing_properties`
When creating custom SCOs, cannot specify the `_id_contributing_properties` property to define which properties should be used for the custom SCOs' deterministic IDs

Discussion:

Patch:
diff --git a/stix2/custom.py b/stix2/custom.py
index a00498b..802fd07 100644
--- a/stix2/custom.py
+++ b/stix2/custom.py
@@ -53,7 +53,10 @@ def _custom_marking_builder(cls, type, properties, version):
     return _CustomMarking
 
 
-def _custom_observable_builder(cls, type, properties, version):
+def _custom_observable_builder(cls, type, properties, version, id_contrib_props=None):
+    if id_contrib_props is None:
+        id_contrib_props = []
+
     class _CustomObservable(cls, _Observable):
 
         if not re.match(TYPE_REGEX, type):
@@ -98,6 +101,8 @@ def _custom_observable_builder(cls, type, properties, version):
 
         _type = type
         _properties = OrderedDict(properties)
+        if version != '2.0':
+            _id_contributing_properties = id_contrib_props
 
         def __init__(self, **kwargs):
             _Observable.__init__(self, **kwargs)
diff --git a/stix2/v21/observables.py b/stix2/v21/observables.py
index ed560a6..e8c1925 100644
--- a/stix2/v21/observables.py
+++ b/stix2/v21/observables.py
@@ -966,7 +966,7 @@ class X509Certificate(_Observable):
         self._check_at_least_one_property(att_list)
 
 
-def CustomObservable(type='x-custom-observable', properties=None):
+def CustomObservable(type='x-custom-observable', properties=None, id_contrib_props=None):
     """Custom STIX Cyber Observable Object type decorator.
 
     Example:
@@ -987,7 +987,7 @@ def CustomObservable(type='x-custom-observable', properties=None):
             properties,
             [('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=type))],
         ]))
-        return _custom_observable_builder(cls, type, _properties, '2.1')
+        return _custom_observable_builder(cls, type, _properties, '2.1', id_contrib_props)
     return wrapper
 
 


Test patch:
diff --git a/stix2/test/v21/test_custom.py b/stix2/test/v21/test_custom.py
index 9c650eb..b46288d 100644
--- a/stix2/test/v21/test_custom.py
+++ b/stix2/test/v21/test_custom.py
@@ -1,3 +1,5 @@
+import uuid
+
 import pytest
 
 import stix2
@@ -665,6 +667,76 @@ def test_observed_data_with_custom_observable_object():
     assert ob_data.objects['0'].property1 == 'something'
 
 
+def test_custom_observable_object_det_id_1():
+    @stix2.v21.CustomObservable(
+        'x-det-id-observable-1', [
+            ('property1', stix2.properties.StringProperty(required=True)),
+            ('property2', stix2.properties.IntegerProperty()),
+        ], [
+            'property1',
+        ],
+    )
+    class DetIdObs1():
+        pass
+
+    dio_1 = DetIdObs1(property1='I am property1!', property2=42)
+    dio_2 = DetIdObs1(property1='I am property1!', property2=24)
+    assert dio_1.property1 == dio_2.property1 == 'I am property1!'
+    assert dio_1.id == dio_2.id
+
+    uuid_obj = uuid.UUID(dio_1.id[-36:])
+    assert uuid_obj.variant == uuid.RFC_4122
+    assert uuid_obj.version == 5
+
+    dio_3 = DetIdObs1(property1='I am property1!', property2=42)
+    dio_4 = DetIdObs1(property1='I am also property1!', property2=24)
+    assert dio_3.property1 == 'I am property1!'
+    assert dio_4.property1 == 'I am also property1!'
+    assert dio_3.id != dio_4.id
+
+
+def test_custom_observable_object_det_id_2():
+    @stix2.v21.CustomObservable(
+        'x-det-id-observable-2', [
+            ('property1', stix2.properties.StringProperty(required=True)),
+            ('property2', stix2.properties.IntegerProperty()),
+        ], [
+            'property1', 'property2',
+        ],
+    )
+    class DetIdObs2():
+        pass
+
+    dio_1 = DetIdObs2(property1='I am property1!', property2=42)
+    dio_2 = DetIdObs2(property1='I am property1!', property2=42)
+    assert dio_1.property1 == dio_2.property1 == 'I am property1!'
+    assert dio_1.property2 == dio_2.property2 == 42
+    assert dio_1.id == dio_2.id
+
+    dio_3 = DetIdObs2(property1='I am property1!', property2=42)
+    dio_4 = DetIdObs2(property1='I am also property1!', property2=42)
+    assert dio_3.property1 == 'I am property1!'
+    assert dio_4.property1 == 'I am also property1!'
+    assert dio_3.property2 == dio_4.property2 == 42
+    assert dio_3.id != dio_4.id
+
+
+def test_custom_observable_object_no_id_contrib_props():
+    @stix2.v21.CustomObservable(
+        'x-det-id-observable-3', [
+            ('property1', stix2.properties.StringProperty(required=True)),
+        ],
+    )
+    class DetIdObs3():
+        pass
+
+    dio = DetIdObs3(property1="I am property1!")
+
+    uuid_obj = uuid.UUID(dio.id[-36:])
+    assert uuid_obj.variant == uuid.RFC_4122
+    assert uuid_obj.version == 4
+
+
 @stix2.v21.CustomExtension(
     stix2.v21.DomainName, 'x-new-ext', [
         ('property1', stix2.properties.StringProperty(required=True)),