Repository: oasis-open/cti-python-stix2

Issue:
stix2.exceptions.InvalidValueError: Invalid value for ObservedData 'objects': Invalid value for URL 'extensions': The enclosing type 'url' has no extensions defined
According to the [stix2 standard](http://docs.oasis-open.org/cti/stix/v2.0/cs01/part4-cyber-observable-objects/stix-v2.0-cs01-part4-cyber-observable-objects.html#_Toc496716284), URL observables have `extensions` Common Properties. But I encountered this error when I tried to add a custom extension to a URL observable.

Discussion:

Patch:
diff --git a/stix2/v20/observables.py b/stix2/v20/observables.py
index 36def28..faa5868 100644
--- a/stix2/v20/observables.py
+++ b/stix2/v20/observables.py
@@ -73,26 +73,23 @@ class ExtensionsProperty(DictionaryProperty):
         if dictified == {}:
             raise ValueError("The extensions property must contain a non-empty dictionary")
 
-        if self.enclosing_type in EXT_MAP:
-            specific_type_map = EXT_MAP[self.enclosing_type]
-            for key, subvalue in dictified.items():
-                if key in specific_type_map:
-                    cls = specific_type_map[key]
-                    if type(subvalue) is dict:
-                        if self.allow_custom:
-                            subvalue['allow_custom'] = True
-                            dictified[key] = cls(**subvalue)
-                        else:
-                            dictified[key] = cls(**subvalue)
-                    elif type(subvalue) is cls:
-                        # If already an instance of an _Extension class, assume it's valid
-                        dictified[key] = subvalue
+        specific_type_map = EXT_MAP.get(self.enclosing_type, {})
+        for key, subvalue in dictified.items():
+            if key in specific_type_map:
+                cls = specific_type_map[key]
+                if type(subvalue) is dict:
+                    if self.allow_custom:
+                        subvalue['allow_custom'] = True
+                        dictified[key] = cls(**subvalue)
                     else:
-                        raise ValueError("Cannot determine extension type.")
+                        dictified[key] = cls(**subvalue)
+                elif type(subvalue) is cls:
+                    # If already an instance of an _Extension class, assume it's valid
+                    dictified[key] = subvalue
                 else:
-                    raise CustomContentError("Can't parse unknown extension type: {}".format(key))
-        else:
-            raise ValueError("The enclosing type '%s' has no extensions defined" % self.enclosing_type)
+                    raise ValueError("Cannot determine extension type.")
+            else:
+                raise CustomContentError("Can't parse unknown extension type: {}".format(key))
         return dictified
 
 


Test patch:
diff --git a/stix2/test/test_custom.py b/stix2/test/test_custom.py
index 18ad03a..18b8f18 100644
--- a/stix2/test/test_custom.py
+++ b/stix2/test/test_custom.py
@@ -826,9 +826,10 @@ def test_parse_observable_with_custom_extension():
     assert parsed.extensions['x-new-ext'].property2 == 12
 
 
-def test_parse_observable_with_unregistered_custom_extension():
-    input_str = """{
-        "type": "domain-name",
+@pytest.mark.parametrize("data", [
+    # URL is not in EXT_MAP
+    """{
+        "type": "url",
         "value": "example.com",
         "extensions": {
             "x-foobar-ext": {
@@ -836,13 +837,25 @@ def test_parse_observable_with_unregistered_custom_extension():
                 "property2": 12
             }
         }
-    }"""
-
+    }""",
+    # File is in EXT_MAP
+    """{
+        "type": "file",
+        "name": "foo.txt",
+        "extensions": {
+            "x-foobar-ext": {
+                "property1": "foo",
+                "property2": 12
+            }
+        }
+    }""",
+])
+def test_parse_observable_with_unregistered_custom_extension(data):
     with pytest.raises(ValueError) as excinfo:
-        stix2.parse_observable(input_str)
+        stix2.parse_observable(data)
     assert "Can't parse unknown extension type" in str(excinfo.value)
 
-    parsed_ob = stix2.parse_observable(input_str, allow_custom=True)
+    parsed_ob = stix2.parse_observable(data, allow_custom=True)
     assert parsed_ob['extensions']['x-foobar-ext']['property1'] == 'foo'
     assert not isinstance(parsed_ob['extensions']['x-foobar-ext'], stix2.core._STIXBase)
 
diff --git a/stix2/test/test_properties.py b/stix2/test/test_properties.py
index 1106bcb..19419bb 100644
--- a/stix2/test/test_properties.py
+++ b/stix2/test/test_properties.py
@@ -412,7 +412,7 @@ def test_extension_property_invalid_type():
                 'pe_type': 'exe'
             }}
         )
-    assert 'no extensions defined' in str(excinfo.value)
+    assert "Can't parse unknown extension" in str(excinfo.value)
 
 
 def test_extension_at_least_one_property_constraint():