Repository: oasis-open/cti-python-stix2

Issue:
Create an Extension with Dict annidate inside List
Hi,
I'm trying to create a CyberObservable Extension for UserAccount which have to contain a DictionaryProperty() inside a ListProperty(). It is possible? Because when I try to create an extension like this one

```
@CustomExtension(UserAccount, 'ssh_keys', {
   keys': ListProperty(DictionaryProperty(), required=True)
 })
 class SSHKeysExtension:
     pass
```

and use it with example = SSHKeysExtension(keys=[{'test123':123, 'test345','aaaa'}])
I obtain a lot of strange errors (the library seems to interpreter the dict as parameters for __init__()


Discussion:

Patch:
diff --git a/stix2/properties.py b/stix2/properties.py
index ca7f04c..41841b6 100644
--- a/stix2/properties.py
+++ b/stix2/properties.py
@@ -129,6 +129,8 @@ class ListProperty(Property):
                 # constructor again
                 result.append(valid)
                 continue
+            elif type(self.contained) is DictionaryProperty:
+                obj_type = dict
             else:
                 obj_type = self.contained
 
diff --git a/stix2/utils.py b/stix2/utils.py
index 9febd78..4ef3d23 100644
--- a/stix2/utils.py
+++ b/stix2/utils.py
@@ -166,7 +166,7 @@ def get_dict(data):
 def find_property_index(obj, properties, tuple_to_find):
     """Recursively find the property in the object model, return the index
     according to the _properties OrderedDict. If it's a list look for
-    individual objects.
+    individual objects. Returns and integer indicating its location
     """
     from .base import _STIXBase
     try:
@@ -183,6 +183,11 @@ def find_property_index(obj, properties, tuple_to_find):
                                                   tuple_to_find)
                         if val is not None:
                             return val
+                    elif isinstance(item, dict):
+                        for idx, val in enumerate(sorted(item)):
+                            if (tuple_to_find[0] == val and
+                                    item.get(val) == tuple_to_find[1]):
+                                return idx
             elif isinstance(pv, dict):
                 if pv.get(tuple_to_find[0]) is not None:
                     try:


Test patch:
diff --git a/stix2/test/test_custom.py b/stix2/test/test_custom.py
index a14503f..b45670f 100644
--- a/stix2/test/test_custom.py
+++ b/stix2/test/test_custom.py
@@ -479,6 +479,27 @@ def test_custom_extension_wrong_observable_type():
     assert 'Cannot determine extension type' in excinfo.value.reason
 
 
+@pytest.mark.parametrize("data", [
+    """{
+    "keys": [
+        {
+            "test123": 123,
+            "test345": "aaaa"
+        }
+    ]
+}""",
+])
+def test_custom_extension_with_list_and_dict_properties_observable_type(data):
+    @stix2.observables.CustomExtension(stix2.UserAccount, 'some-extension', [
+        ('keys', stix2.properties.ListProperty(stix2.properties.DictionaryProperty, required=True))
+    ])
+    class SomeCustomExtension:
+        pass
+
+    example = SomeCustomExtension(keys=[{'test123': 123, 'test345': 'aaaa'}])
+    assert data == str(example)
+
+
 def test_custom_extension_invalid_observable():
     # These extensions are being applied to improperly-created Observables.
     # The Observable classes should have been created with the CustomObservable decorator.
diff --git a/stix2/test/test_properties.py b/stix2/test/test_properties.py
index 34edc96..16ff06a 100644
--- a/stix2/test/test_properties.py
+++ b/stix2/test/test_properties.py
@@ -1,6 +1,6 @@
 import pytest
 
-from stix2 import EmailMIMEComponent, ExtensionsProperty, TCPExt
+from stix2 import CustomObject, EmailMIMEComponent, ExtensionsProperty, TCPExt
 from stix2.exceptions import AtLeastOnePropertyError, DictionaryKeyError
 from stix2.properties import (BinaryProperty, BooleanProperty,
                               DictionaryProperty, EmbeddedObjectProperty,
@@ -266,6 +266,17 @@ def test_dictionary_property_invalid(d):
     assert str(excinfo.value) == d[1]
 
 
+def test_property_list_of_dictionary():
+    @CustomObject('x-new-obj', [
+        ('property1', ListProperty(DictionaryProperty(), required=True)),
+    ])
+    class NewObj():
+        pass
+
+    test_obj = NewObj(property1=[{'foo': 'bar'}])
+    assert test_obj.property1[0]['foo'] == 'bar'
+
+
 @pytest.mark.parametrize("value", [
     {"sha256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b"},
     [('MD5', '2dfb1bcc980200c6706feee399d41b3f'), ('RIPEMD-160', 'b3a8cd8a27c90af79b3c81754f267780f443dfef')],