Repository: oasis-open/cti-python-stix2
Pull request #315 (2019-12-17 17:00:14)

Fix issue 308


Patch:
diff --git a/README.rst b/README.rst
index 0bf30fd..0613a15 100644
--- a/README.rst
+++ b/README.rst
@@ -52,7 +52,6 @@ To parse a STIX JSON string into a Python STIX object, use ``parse()``:
         "indicator_types": [
             "malicious-activity"
         ],
-        "pattern_type": "stix",
         "pattern": "[file:hashes.md5 ='d41d8cd98f00b204e9800998ecf8427e']",
         "valid_from": "2017-09-26T23:33:39.829952Z"
     }""")
diff --git a/stix2/base.py b/stix2/base.py
index b2e20de..dba5d71 100644
--- a/stix2/base.py
+++ b/stix2/base.py
@@ -143,7 +143,7 @@ class _STIXBase(collections.Mapping):
 
     def __init__(self, allow_custom=False, **kwargs):
         cls = self.__class__
-        self.__allow_custom = allow_custom
+        self._allow_custom = allow_custom
 
         # Use the same timestamp for any auto-generated datetimes
         self.__now = get_timestamp()
@@ -152,12 +152,12 @@ class _STIXBase(collections.Mapping):
         custom_props = kwargs.pop('custom_properties', {})
         if custom_props and not isinstance(custom_props, dict):
             raise ValueError("'custom_properties' must be a dictionary")
-        if not self.__allow_custom:
+        if not self._allow_custom:
             extra_kwargs = list(set(kwargs) - set(self._properties))
             if extra_kwargs:
                 raise ExtraPropertiesError(cls, extra_kwargs)
         if custom_props:
-            self.__allow_custom = True
+            self._allow_custom = True
 
         # Remove any keyword arguments whose value is None or [] (i.e. empty list)
         setting_kwargs = {}
@@ -235,7 +235,7 @@ class _STIXBase(collections.Mapping):
         if isinstance(self, _Observable):
             # Assume: valid references in the original object are still valid in the new version
             new_inner['_valid_refs'] = {'*': '*'}
-        new_inner['allow_custom'] = self.__allow_custom
+        new_inner['allow_custom'] = self._allow_custom
         return cls(**new_inner)
 
     def properties_populated(self):
@@ -306,7 +306,7 @@ class _Observable(_STIXBase):
         # the constructor might be called independently of an observed data object
         self._STIXBase__valid_refs = kwargs.pop('_valid_refs', [])
 
-        self.__allow_custom = kwargs.get('allow_custom', False)
+        self._allow_custom = kwargs.get('allow_custom', False)
         self._properties['extensions'].allow_custom = kwargs.get('allow_custom', False)
 
         try:
diff --git a/stix2/core.py b/stix2/core.py
index aa9044d..1031d61 100644
--- a/stix2/core.py
+++ b/stix2/core.py
@@ -8,7 +8,7 @@ import re
 import stix2
 
 from .base import _STIXBase
-from .exceptions import ParseError
+from .exceptions import CustomContentError, ParseError
 from .markings import _MarkingsMixin
 from .utils import _get_dict
 
@@ -109,7 +109,7 @@ def dict_to_stix2(stix_dict, allow_custom=False, version=None):
         # '2.0' representation.
         v = 'v20'
 
-    OBJ_MAP = dict(STIX2_OBJ_MAPS[v]['objects'], **STIX2_OBJ_MAPS[v]['observables'])
+    OBJ_MAP = STIX2_OBJ_MAPS[v]['objects']
 
     try:
         obj_class = OBJ_MAP[stix_dict['type']]
@@ -166,8 +166,8 @@ def parse_observable(data, _valid_refs=None, allow_custom=False, version=None):
             # flag allows for unknown custom objects too, but will not
             # be parsed into STIX observable object, just returned as is
             return obj
-        raise ParseError("Can't parse unknown observable type '%s'! For custom observables, "
-                         "use the CustomObservable decorator." % obj['type'])
+        raise CustomContentError("Can't parse unknown observable type '%s'! For custom observables, "
+                                 "use the CustomObservable decorator." % obj['type'])
 
     return obj_class(allow_custom=allow_custom, **obj)
 
diff --git a/stix2/custom.py b/stix2/custom.py
index a00498b..484cbb0 100644
--- a/stix2/custom.py
+++ b/stix2/custom.py
@@ -67,34 +67,19 @@ def _custom_observable_builder(cls, type, properties, version):
         if not properties or not isinstance(properties, list):
             raise ValueError("Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]")
 
-        if version == "2.0":
-            # If using STIX2.0, check properties ending in "_ref/s" are ObjectReferenceProperties
-            for prop_name, prop in properties:
-                if prop_name.endswith('_ref') and ('ObjectReferenceProperty' not in get_class_hierarchy_names(prop)):
-                    raise ValueError(
-                        "'%s' is named like an object reference property but "
-                        "is not an ObjectReferenceProperty." % prop_name,
-                    )
-                elif (prop_name.endswith('_refs') and ('ListProperty' not in get_class_hierarchy_names(prop) or
-                                                       'ObjectReferenceProperty' not in get_class_hierarchy_names(prop.contained))):
-                    raise ValueError(
-                        "'%s' is named like an object reference list property but "
-                        "is not a ListProperty containing ObjectReferenceProperty." % prop_name,
-                    )
-        else:
-            # If using STIX2.1 (or newer...), check properties ending in "_ref/s" are ReferenceProperties
-            for prop_name, prop in properties:
-                if prop_name.endswith('_ref') and ('ReferenceProperty' not in get_class_hierarchy_names(prop)):
-                    raise ValueError(
-                        "'%s' is named like a reference property but "
-                        "is not a ReferenceProperty." % prop_name,
-                    )
-                elif (prop_name.endswith('_refs') and ('ListProperty' not in get_class_hierarchy_names(prop) or
-                                                       'ReferenceProperty' not in get_class_hierarchy_names(prop.contained))):
-                    raise ValueError(
-                        "'%s' is named like a reference list property but "
-                        "is not a ListProperty containing ReferenceProperty." % prop_name,
-                    )
+        # Check properties ending in "_ref/s" are ObjectReferenceProperties
+        for prop_name, prop in properties:
+            if prop_name.endswith('_ref') and ('ObjectReferenceProperty' not in get_class_hierarchy_names(prop)):
+                raise ValueError(
+                    "'%s' is named like an object reference property but "
+                    "is not an ObjectReferenceProperty." % prop_name,
+                )
+            elif (prop_name.endswith('_refs') and ('ListProperty' not in get_class_hierarchy_names(prop)
+                                                   or 'ObjectReferenceProperty' not in get_class_hierarchy_names(prop.contained))):
+                raise ValueError(
+                    "'%s' is named like an object reference list property but "
+                    "is not a ListProperty containing ObjectReferenceProperty." % prop_name,
+                )
 
         _type = type
         _properties = OrderedDict(properties)
diff --git a/stix2/patterns.py b/stix2/patterns.py
index 2e149be..9656ff1 100644
--- a/stix2/patterns.py
+++ b/stix2/patterns.py
@@ -135,7 +135,6 @@ _HASH_REGEX = {
     "SHA3512": ("^[a-fA-F0-9]{128}$", "SHA3-512"),
     "SSDEEP": ("^[a-zA-Z0-9/+:.]{1,128}$", "ssdeep"),
     "WHIRLPOOL": ("^[a-fA-F0-9]{128}$", "WHIRLPOOL"),
-    "TLSH": ("^[a-fA-F0-9]{70}$", "TLSH"),
 }
 
 
diff --git a/stix2/properties.py b/stix2/properties.py
index 9ddfb82..b1199b8 100644
--- a/stix2/properties.py
+++ b/stix2/properties.py
@@ -200,7 +200,12 @@ class ListProperty(Property):
                 obj_type = self.contained
 
             if isinstance(valid, collections.Mapping):
-                result.append(obj_type(**valid))
+                try:
+                    valid._allow_custom
+                except AttributeError:
+                    result.append(obj_type(**valid))
+                else:
+                    result.append(obj_type(allow_custom=True, **valid))
             else:
                 result.append(obj_type(valid))
 
@@ -363,10 +368,6 @@ class DictionaryProperty(Property):
                     "underscore (_)"
                 )
                 raise DictionaryKeyError(k, msg)
-
-        if len(dictified) < 1:
-            raise ValueError("must not be empty.")
-
         return dictified
 
 
@@ -385,7 +386,6 @@ HASHES_REGEX = {
     "SHA3512": (r"^[a-fA-F0-9]{128}$", "SHA3-512"),
     "SSDEEP": (r"^[a-zA-Z0-9/+:.]{1,128}$", "ssdeep"),
     "WHIRLPOOL": (r"^[a-fA-F0-9]{128}$", "WHIRLPOOL"),
-    "TLSH": (r"^[a-fA-F0-9]{70}$", "TLSH"),
 }
 
 
@@ -454,19 +454,22 @@ class ReferenceProperty(Property):
             value = value.id
         value = str(value)
 
-        possible_prefix = value[:value.index('--')]
+        possible_prefix = value[:value.index('--') + 2]
 
         if self.valid_types:
-            ref_valid_types = enumerate_types(self.valid_types, 'v' + self.spec_version.replace(".", ""))
-
-            if possible_prefix in ref_valid_types:
+            if self.valid_types == ["only_SDO"]:
+                self.valid_types = STIX2_OBJ_MAPS['v21']['objects'].keys()
+            elif self.valid_types == ["only_SCO"]:
+                self.valid_types = STIX2_OBJ_MAPS['v21']['observables'].keys()
+            elif self.valid_types == ["only_SCO_&_SRO"]:
+                self.valid_types = list(STIX2_OBJ_MAPS['v21']['observables'].keys()) + ['relationship', 'sighting']
+
+            if possible_prefix[:-2] in self.valid_types:
                 required_prefix = possible_prefix
             else:
                 raise ValueError("The type-specifying prefix '%s' for this property is not valid" % (possible_prefix))
         elif self.invalid_types:
-            ref_invalid_types = enumerate_types(self.invalid_types, 'v' + self.spec_version.replace(".", ""))
-
-            if possible_prefix not in ref_invalid_types:
+            if possible_prefix[:-2] not in self.invalid_types:
                 required_prefix = possible_prefix
             else:
                 raise ValueError("An invalid type-specifying prefix '%s' was specified for this property" % (possible_prefix, value))
@@ -476,31 +479,6 @@ class ReferenceProperty(Property):
         return value
 
 
-def enumerate_types(types, spec_version):
-    """
-    `types` is meant to be a list; it may contain specific object types and/or
-        the any of the words "SCO", "SDO", or "SRO"
-
-    Since "SCO", "SDO", and "SRO" are general types that encompass various specific object types,
-        once each of those words is being processed, that word will be removed from `return_types`,
-        so as not to mistakenly allow objects to be created of types "SCO", "SDO", or "SRO"
-    """
-    return_types = []
-    return_types += types
-
-    if "SDO" in types:
-        return_types.remove("SDO")
-        return_types += STIX2_OBJ_MAPS[spec_version]['objects'].keys()
-    if "SCO" in types:
-        return_types.remove("SCO")
-        return_types += STIX2_OBJ_MAPS[spec_version]['observables'].keys()
-    if "SRO" in types:
-        return_types.remove("SRO")
-        return_types += ['relationship', 'sighting']
-
-    return return_types
-
-
 SELECTOR_REGEX = re.compile(r"^[a-z0-9_-]{3,250}(\.(\[\d+\]|[a-z0-9_-]{1,250}))*$")
 
 
diff --git a/stix2/utils.py b/stix2/utils.py
index b23b0e4..ffabed0 100644
--- a/stix2/utils.py
+++ b/stix2/utils.py
@@ -1,9 +1,6 @@
 """Utility functions and classes for the STIX2 library."""
 
-try:
-    from collections.abc import Mapping
-except ImportError:
-    from collections import Mapping
+from collections import Mapping
 import copy
 import datetime as dt
 import json
diff --git a/stix2/v20/bundle.py b/stix2/v20/bundle.py
index 1383757..ffd70ea 100644
--- a/stix2/v20/bundle.py
+++ b/stix2/v20/bundle.py
@@ -31,7 +31,7 @@ class Bundle(_STIXBase):
             else:
                 kwargs['objects'] = list(args) + kwargs.get('objects', [])
 
-        self.__allow_custom = kwargs.get('allow_custom', False)
+        self._allow_custom = kwargs.get('allow_custom', False)
         self._properties['objects'].contained.allow_custom = kwargs.get('allow_custom', False)
 
         super(Bundle, self).__init__(**kwargs)
diff --git a/stix2/v20/sdo.py b/stix2/v20/sdo.py
index 9ba1860..19d5437 100644
--- a/stix2/v20/sdo.py
+++ b/stix2/v20/sdo.py
@@ -212,7 +212,7 @@ class ObservedData(STIXDomainObject):
     ])
 
     def __init__(self, *args, **kwargs):
-        self.__allow_custom = kwargs.get('allow_custom', False)
+        self._allow_custom = kwargs.get('allow_custom', False)
         self._properties['objects'].allow_custom = kwargs.get('allow_custom', False)
 
         super(ObservedData, self).__init__(*args, **kwargs)
@@ -233,7 +233,7 @@ class Report(STIXDomainObject):
         ('name', StringProperty(required=True)),
         ('description', StringProperty()),
         ('published', TimestampProperty(required=True)),
-        ('object_refs', ListProperty(ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.0'), required=True)),
+        ('object_refs', ListProperty(ReferenceProperty(invalid_types=[""], spec_version='2.0'), required=True)),
         ('revoked', BooleanProperty(default=lambda: False)),
         ('labels', ListProperty(StringProperty, required=True)),
         ('external_references', ListProperty(ExternalReference)),
diff --git a/stix2/v20/sro.py b/stix2/v20/sro.py
index b85eb68..3f561b8 100644
--- a/stix2/v20/sro.py
+++ b/stix2/v20/sro.py
@@ -67,7 +67,7 @@ class Sighting(STIXRelationshipObject):
         ('first_seen', TimestampProperty()),
         ('last_seen', TimestampProperty()),
         ('count', IntegerProperty(min=0, max=999999999)),
-        ('sighting_of_ref', ReferenceProperty(valid_types="SDO", spec_version='2.0', required=True)),
+        ('sighting_of_ref', ReferenceProperty(valid_types="only_SDO", spec_version='2.0', required=True)),
         ('observed_data_refs', ListProperty(ReferenceProperty(valid_types='observed-data', spec_version='2.0'))),
         ('where_sighted_refs', ListProperty(ReferenceProperty(valid_types='identity', spec_version='2.0'))),
         ('summary', BooleanProperty(default=lambda: False)),
diff --git a/stix2/v21/bundle.py b/stix2/v21/bundle.py
index fb35be2..168771f 100644
--- a/stix2/v21/bundle.py
+++ b/stix2/v21/bundle.py
@@ -29,7 +29,7 @@ class Bundle(_STIXBase):
             else:
                 kwargs['objects'] = list(args) + kwargs.get('objects', [])
 
-        self.__allow_custom = kwargs.get('allow_custom', False)
+        self._allow_custom = kwargs.get('allow_custom', False)
         self._properties['objects'].contained.allow_custom = kwargs.get('allow_custom', False)
 
         super(Bundle, self).__init__(**kwargs)
diff --git a/stix2/v21/common.py b/stix2/v21/common.py
index 4a71308..a31e710 100644
--- a/stix2/v21/common.py
+++ b/stix2/v21/common.py
@@ -76,7 +76,7 @@ class LanguageContent(_STIXBase):
         ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')),
         ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')),
         ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')),
-        ('object_ref', ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.1', required=True)),
+        ('object_ref', ReferenceProperty(invalid_types=[""], spec_version='2.1', required=True)),
         # TODO: 'object_modified' it MUST be an exact match for the modified time of the STIX Object (SRO or SDO) being referenced.
         ('object_modified', TimestampProperty(precision='millisecond')),
         # TODO: 'contents' https://docs.google.com/document/d/1ShNq4c3e1CkfANmD9O--mdZ5H0O_GLnjN28a_yrEaco/edit#heading=h.cfz5hcantmvx
diff --git a/stix2/v21/observables.py b/stix2/v21/observables.py
index b08869e..0d27bb4 100644
--- a/stix2/v21/observables.py
+++ b/stix2/v21/observables.py
@@ -385,7 +385,7 @@ class File(_Observable):
         ('mtime', TimestampProperty()),
         ('atime', TimestampProperty()),
         ('parent_directory_ref', ReferenceProperty(valid_types='directory', spec_version='2.1')),
-        ('contains_refs', ListProperty(ReferenceProperty(valid_types=["SCO"], spec_version='2.1'))),
+        ('contains_refs', ListProperty(ReferenceProperty(invalid_types="", spec_version='2.1'))),
         ('content_ref', ReferenceProperty(valid_types='artifact', spec_version='2.1')),
         ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)),
         ('spec_version', StringProperty(fixed='2.1')),
@@ -592,18 +592,6 @@ class SocketExt(_Extension):
         ('socket_handle', IntegerProperty()),
     ])
 
-    def _check_object_constraints(self):
-        super(SocketExt, self)._check_object_constraints()
-
-        options = self.get('options')
-
-        if options is not None:
-            for key, val in options.items():
-                if key[:3] != "SO_":
-                    raise ValueError("Incorrect options key")
-                if not isinstance(val, int):
-                    raise ValueError("Options value must be an integer")
-
 
 class TCPExt(_Extension):
     # TODO: Add link
@@ -998,18 +986,6 @@ class X509Certificate(_Observable):
     ])
     _id_contributing_properties = ["hashes", "serial_number"]
 
-    def _check_object_constraints(self):
-        super(X509Certificate, self)._check_object_constraints()
-
-        att_list = [
-                'is_self_signed', 'hashes', 'version', 'serial_number',
-                'signature_algorithm', 'issuer', 'validity_not_before',
-                'validity_not_after', 'subject', 'subject_public_key_algorithm',
-                'subject_public_key_modulus', 'subject_public_key_exponent',
-                'x509_v3_extensions',
-        ]
-        self._check_at_least_one_property(att_list)
-
 
 def CustomObservable(type='x-custom-observable', properties=None):
     """Custom STIX Cyber Observable Object type decorator.
@@ -1028,7 +1004,6 @@ def CustomObservable(type='x-custom-observable', properties=None):
     def wrapper(cls):
         _properties = list(itertools.chain.from_iterable([
             [('type', TypeProperty(type))],
-            [('id', IDProperty(type, spec_version='2.1'))],
             properties,
             [('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=type))],
         ]))
diff --git a/stix2/v21/sdo.py b/stix2/v21/sdo.py
index db45ed6..9317825 100644
--- a/stix2/v21/sdo.py
+++ b/stix2/v21/sdo.py
@@ -149,7 +149,7 @@ class Grouping(STIXDomainObject):
         ('name', StringProperty()),
         ('description', StringProperty()),
         ('context', StringProperty(required=True)),
-        ('object_refs', ListProperty(ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.1'), required=True)),
+        ('object_refs', ListProperty(ReferenceProperty(invalid_types=[""], spec_version='2.1'), required=True)),
     ])
 
 
@@ -215,13 +215,6 @@ class Indicator(STIXDomainObject):
         ('granular_markings', ListProperty(GranularMarking)),
     ])
 
-    def __init__(self, *args, **kwargs):
-
-        if kwargs.get('pattern') and kwargs.get('pattern_type') == 'stix' and not kwargs.get('pattern_version'):
-            kwargs['pattern_version'] = '2.1'
-
-        super(STIXDomainObject, self).__init__(*args, **kwargs)
-
     def _check_object_constraints(self):
         super(Indicator, self)._check_object_constraints()
 
@@ -512,7 +505,7 @@ class MalwareAnalysis(STIXDomainObject):
         ('analysis_started', TimestampProperty()),
         ('analysis_ended', TimestampProperty()),
         ('av_result', StringProperty()),
-        ('analysis_sco_refs', ListProperty(ReferenceProperty(valid_types="SCO", spec_version='2.1'))),
+        ('analysis_sco_refs', ListProperty(ReferenceProperty(valid_types="only_SCO", spec_version='2.1'))),
     ])
 
     def _check_object_constraints(self):
@@ -538,7 +531,7 @@ class Note(STIXDomainObject):
         ('abstract', StringProperty()),
         ('content', StringProperty(required=True)),
         ('authors', ListProperty(StringProperty)),
-        ('object_refs', ListProperty(ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.1'), required=True)),
+        ('object_refs', ListProperty(ReferenceProperty(invalid_types=[""], spec_version='2.1'), required=True)),
         ('revoked', BooleanProperty(default=lambda: False)),
         ('labels', ListProperty(StringProperty)),
         ('confidence', IntegerProperty()),
@@ -567,7 +560,7 @@ class ObservedData(STIXDomainObject):
         ('last_observed', TimestampProperty(required=True)),
         ('number_observed', IntegerProperty(min=1, max=999999999, required=True)),
         ('objects', ObservableProperty(spec_version='2.1')),
-        ('object_refs', ListProperty(ReferenceProperty(valid_types=["SCO", "SRO"], spec_version="2.1"))),
+        ('object_refs', ListProperty(ReferenceProperty(valid_types="only_SCO_&_SRO", spec_version="2.1"))),
         ('revoked', BooleanProperty(default=lambda: False)),
         ('labels', ListProperty(StringProperty)),
         ('confidence', IntegerProperty()),
@@ -578,7 +571,7 @@ class ObservedData(STIXDomainObject):
     ])
 
     def __init__(self, *args, **kwargs):
-        self.__allow_custom = kwargs.get('allow_custom', False)
+        self._allow_custom = kwargs.get('allow_custom', False)
         self._properties['objects'].allow_custom = kwargs.get('allow_custom', False)
 
         if "objects" in kwargs:
@@ -632,7 +625,7 @@ class Opinion(STIXDomainObject):
                 ], required=True,
             ),
         ),
-        ('object_refs', ListProperty(ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.1'), required=True)),
+        ('object_refs', ListProperty(ReferenceProperty(invalid_types=[""], spec_version='2.1'), required=True)),
         ('revoked', BooleanProperty(default=lambda: False)),
         ('labels', ListProperty(StringProperty)),
         ('confidence', IntegerProperty()),
@@ -661,7 +654,7 @@ class Report(STIXDomainObject):
         ('description', StringProperty()),
         ('report_types', ListProperty(StringProperty, required=True)),
         ('published', TimestampProperty(required=True)),
-        ('object_refs', ListProperty(ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.1'), required=True)),
+        ('object_refs', ListProperty(ReferenceProperty(invalid_types=[""], spec_version='2.1'), required=True)),
         ('revoked', BooleanProperty(default=lambda: False)),
         ('labels', ListProperty(StringProperty)),
         ('confidence', IntegerProperty()),
diff --git a/stix2/v21/sro.py b/stix2/v21/sro.py
index 059bb66..57c7719 100644
--- a/stix2/v21/sro.py
+++ b/stix2/v21/sro.py
@@ -86,7 +86,7 @@ class Sighting(STIXRelationshipObject):
         ('first_seen', TimestampProperty()),
         ('last_seen', TimestampProperty()),
         ('count', IntegerProperty(min=0, max=999999999)),
-        ('sighting_of_ref', ReferenceProperty(valid_types="SDO", spec_version='2.1', required=True)),
+        ('sighting_of_ref', ReferenceProperty(valid_types="only_SDO", spec_version='2.1', required=True)),
         ('observed_data_refs', ListProperty(ReferenceProperty(valid_types='observed-data', spec_version='2.1'))),
         ('where_sighted_refs', ListProperty(ReferenceProperty(valid_types='identity', spec_version='2.1'))),
         ('summary', BooleanProperty()),


Test patch:
diff --git a/stix2/test/v20/test_custom.py b/stix2/test/v20/test_custom.py
index ce1aac3..6d127f2 100644
--- a/stix2/test/v20/test_custom.py
+++ b/stix2/test/v20/test_custom.py
@@ -583,7 +583,7 @@ def test_parse_unregistered_custom_observable_object():
         "property1": "something"
     }"""
 
-    with pytest.raises(stix2.exceptions.ParseError) as excinfo:
+    with pytest.raises(stix2.exceptions.CustomContentError) as excinfo:
         stix2.parse_observable(nt_string, version='2.0')
     assert "Can't parse unknown observable type" in str(excinfo.value)
 
diff --git a/stix2/test/v21/test_base.py b/stix2/test/v21/test_base.py
index 325cf4b..d753ab1 100644
--- a/stix2/test/v21/test_base.py
+++ b/stix2/test/v21/test_base.py
@@ -29,7 +29,7 @@ def test_encode_json_object():
 
 def test_deterministic_id_unicode():
     mutex = {'name': u'D*Fl#Ed*\u00a3\u00a8', 'type': 'mutex'}
-    obs = stix2.parse(mutex, version="2.1")
+    obs = stix2.parse_observable(mutex, version="2.1")
 
     dd_idx = obs.id.index("--")
     id_uuid = uuid.UUID(obs.id[dd_idx+2:])
diff --git a/stix2/test/v21/test_bundle.py b/stix2/test/v21/test_bundle.py
index 4e30c84..54ef318 100644
--- a/stix2/test/v21/test_bundle.py
+++ b/stix2/test/v21/test_bundle.py
@@ -22,7 +22,6 @@ EXPECTED_BUNDLE = """{
             ],
             "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']",
             "pattern_type": "stix",
-            "pattern_version": "2.1",
             "valid_from": "2017-01-01T12:34:56Z"
         },
         {
@@ -62,7 +61,6 @@ EXPECTED_BUNDLE_DICT = {
             "modified": "2017-01-01T12:34:56.000Z",
             "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']",
             "pattern_type": "stix",
-            "pattern_version": "2.1",
             "valid_from": "2017-01-01T12:34:56Z",
             "indicator_types": [
                 "malicious-activity",
diff --git a/stix2/test/v21/test_custom.py b/stix2/test/v21/test_custom.py
index 9c650eb..47d484a 100644
--- a/stix2/test/v21/test_custom.py
+++ b/stix2/test/v21/test_custom.py
@@ -508,7 +508,7 @@ def test_custom_observable_object_invalid_ref_property():
         )
         class NewObs():
             pass
-    assert "is named like a reference property but is not a ReferenceProperty" in str(excinfo.value)
+    assert "is named like an object reference property but is not an ObjectReferenceProperty" in str(excinfo.value)
 
 
 def test_custom_observable_object_invalid_refs_property():
@@ -520,7 +520,7 @@ def test_custom_observable_object_invalid_refs_property():
         )
         class NewObs():
             pass
-    assert "is named like a reference list property but is not a ListProperty containing ReferenceProperty" in str(excinfo.value)
+    assert "is named like an object reference list property but is not a ListProperty containing ObjectReferenceProperty" in str(excinfo.value)
 
 
 def test_custom_observable_object_invalid_refs_list_property():
@@ -532,7 +532,26 @@ def test_custom_observable_object_invalid_refs_list_property():
         )
         class NewObs():
             pass
-    assert "is named like a reference list property but is not a ListProperty containing ReferenceProperty" in str(excinfo.value)
+    assert "is named like an object reference list property but is not a ListProperty containing ObjectReferenceProperty" in str(excinfo.value)
+
+
+def test_custom_observable_object_invalid_valid_refs():
+    @stix2.v21.CustomObservable(
+        'x-new-obs', [
+            ('property1', stix2.properties.StringProperty(required=True)),
+            ('property_ref', stix2.properties.ObjectReferenceProperty(valid_types='email-addr')),
+        ],
+    )
+    class NewObs():
+        pass
+
+    with pytest.raises(Exception) as excinfo:
+        NewObs(
+            _valid_refs=['1'],
+            property1='something',
+            property_ref='1',
+        )
+    assert "must be created with _valid_refs as a dict, not a list" in str(excinfo.value)
 
 
 def test_custom_no_properties_raises_exception():
@@ -556,7 +575,8 @@ def test_parse_custom_observable_object():
         "type": "x-new-observable",
         "property1": "something"
     }"""
-    nt = stix2.parse(nt_string, [], version='2.1')
+
+    nt = stix2.parse_observable(nt_string, [], version='2.1')
     assert isinstance(nt, stix2.base._STIXBase)
     assert nt.property1 == 'something'
 
@@ -567,10 +587,11 @@ def test_parse_unregistered_custom_observable_object():
         "property1": "something"
     }"""
 
-    with pytest.raises(stix2.exceptions.ParseError) as excinfo:
-        stix2.parse(nt_string, version='2.1')
-    assert "Can't parse unknown object type" in str(excinfo.value)
-    parsed_custom = stix2.parse(nt_string, allow_custom=True, version='2.1')
+    with pytest.raises(stix2.exceptions.CustomContentError) as excinfo:
+        stix2.parse_observable(nt_string, version='2.1')
+    assert "Can't parse unknown observable type" in str(excinfo.value)
+
+    parsed_custom = stix2.parse_observable(nt_string, allow_custom=True, version='2.1')
     assert parsed_custom['property1'] == 'something'
     with pytest.raises(AttributeError) as excinfo:
         assert parsed_custom.property1 == 'something'
@@ -583,8 +604,8 @@ def test_parse_unregistered_custom_observable_object_with_no_type():
     }"""
 
     with pytest.raises(stix2.exceptions.ParseError) as excinfo:
-        stix2.parse(nt_string, allow_custom=True, version='2.1')
-    assert "Can't parse object with no 'type' property" in str(excinfo.value)
+        stix2.parse_observable(nt_string, allow_custom=True, version='2.1')
+    assert "Can't parse observable with no 'type' property" in str(excinfo.value)
 
 
 def test_parse_observed_data_with_custom_observable():
@@ -613,8 +634,8 @@ def test_parse_invalid_custom_observable_object():
     }"""
 
     with pytest.raises(stix2.exceptions.ParseError) as excinfo:
-        stix2.parse(nt_string, version='2.1')
-    assert "Can't parse object with no 'type' property" in str(excinfo.value)
+        stix2.parse_observable(nt_string, version='2.1')
+    assert "Can't parse observable with no 'type' property" in str(excinfo.value)
 
 
 def test_observable_custom_property():
@@ -864,7 +885,8 @@ def test_parse_observable_with_custom_extension():
             }
         }
     }"""
-    parsed = stix2.parse(input_str, version='2.1')
+
+    parsed = stix2.parse_observable(input_str, version='2.1')
     assert parsed.extensions['x-new-ext'].property2 == 12
 
 
@@ -939,9 +961,10 @@ def test_custom_and_spec_extension_mix():
 )
 def test_parse_observable_with_unregistered_custom_extension(data):
     with pytest.raises(InvalidValueError) as excinfo:
-        stix2.parse(data, version='2.1')
+        stix2.parse_observable(data, version='2.1')
     assert "Can't parse unknown extension type" in str(excinfo.value)
-    parsed_ob = stix2.parse(data, allow_custom=True, version='2.1')
+
+    parsed_ob = stix2.parse_observable(data, allow_custom=True, version='2.1')
     assert parsed_ob['extensions']['x-foobar-ext']['property1'] == 'foo'
     assert not isinstance(parsed_ob['extensions']['x-foobar-ext'], stix2.base._STIXBase)
 
diff --git a/stix2/test/v21/test_indicator.py b/stix2/test/v21/test_indicator.py
index 0562dfd..d441ba5 100644
--- a/stix2/test/v21/test_indicator.py
+++ b/stix2/test/v21/test_indicator.py
@@ -19,7 +19,6 @@ EXPECTED_INDICATOR = """{
     ],
     "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']",
     "pattern_type": "stix",
-    "pattern_version": "2.1",
     "valid_from": "1970-01-01T00:00:01Z"
 }"""
 
@@ -32,7 +31,6 @@ EXPECTED_INDICATOR_REPR = "Indicator(" + " ".join("""
     indicator_types=['malicious-activity'],
     pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']",
     pattern_type='stix',
-    pattern_version='2.1',
     valid_from='1970-01-01T00:00:01Z'
 """.split()) + ")"
 
@@ -207,3 +205,47 @@ def test_invalid_indicator_pattern():
     assert excinfo.value.cls == stix2.v21.Indicator
     assert excinfo.value.prop_name == 'pattern'
     assert 'mismatched input' in excinfo.value.reason
+
+
+def test_indicator_with_custom_embedded_objs():
+    now = dt.datetime(2017, 1, 1, 0, 0, 1, tzinfo=pytz.utc)
+    epoch = dt.datetime(1970, 1, 1, 0, 0, 1, tzinfo=pytz.utc)
+
+    ext_ref = stix2.v21.ExternalReference(
+        source_name="Test",
+        description="Example Custom Ext Ref",
+        random_custom_prop="This is a custom property",
+        allow_custom=True,
+    )
+
+    ind = stix2.v21.Indicator(
+        type="indicator",
+        id=INDICATOR_ID,
+        created=now,
+        modified=now,
+        pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']",
+        pattern_type="stix",
+        valid_from=epoch,
+        indicator_types=['malicious-activity'],
+        external_references=[ext_ref],
+    )
+
+    assert ind.indicator_types == ['malicious-activity']
+    assert len(ind.external_references) == 1
+    assert ind.external_references[0] == ext_ref
+
+
+def test_indicator_with_custom_embed_objs_extra_props_error():
+    ext_ref = stix2.v21.ExternalReference(
+        source_name="Test",
+        description="Example Custom Ext Ref",
+        random_custom_prop="This is a custom property",
+        allow_custom=True,
+    )
+
+    with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo:
+        stix2.v21.Indicator(external_references=[ext_ref], bad_custom_prop="shouldn't be here", **INDICATOR_KWARGS)
+
+    assert excinfo.value.cls == stix2.v21.Indicator
+    assert excinfo.value.properties == ['bad_custom_prop']
+    assert str(excinfo.value) == "Unexpected properties for Indicator: (bad_custom_prop)."
diff --git a/stix2/test/v21/test_observed_data.py b/stix2/test/v21/test_observed_data.py
index 32bd0bf..0074bf7 100644
--- a/stix2/test/v21/test_observed_data.py
+++ b/stix2/test/v21/test_observed_data.py
@@ -209,7 +209,7 @@ def test_observed_data_example_with_bad_refs():
 
     assert excinfo.value.cls == stix2.v21.Directory
     assert excinfo.value.prop_name == "contains_refs"
-    assert "The type-specifying prefix 'monkey' for this property is not valid" in excinfo.value.reason
+    assert "The type-specifying prefix 'monkey--' for this property is not valid" in excinfo.value.reason
 
 
 def test_observed_data_example_with_non_dictionary():
@@ -369,7 +369,7 @@ def test_parse_autonomous_system_valid(data):
     ],
 )
 def test_parse_email_address(data):
-    odata = stix2.parse(data, version='2.1')
+    odata = stix2.parse_observable(data, version='2.1')
     assert odata.type == "email-addr"
 
     odata_str = re.compile(
@@ -378,7 +378,7 @@ def test_parse_email_address(data):
         '"belongs_to_ref": "mutex--9be6365f-b89c-48c0-9340-6953f6595718"', data,
     )
     with pytest.raises(stix2.exceptions.InvalidValueError):
-        stix2.parse(odata_str, version='2.1')
+        stix2.parse_observable(odata_str, version='2.1')
 
 
 @pytest.mark.parametrize(
@@ -424,7 +424,7 @@ def test_parse_email_address(data):
     ],
 )
 def test_parse_email_message(data):
-    odata = stix2.parse(data, version='2.1')
+    odata = stix2.parse_observable(data, version='2.1')
     assert odata.type == "email-message"
     assert odata.body_multipart[0].content_disposition == "inline"
 
@@ -446,7 +446,7 @@ def test_parse_email_message(data):
 )
 def test_parse_email_message_not_multipart(data):
     with pytest.raises(stix2.exceptions.DependentPropertiesError) as excinfo:
-        stix2.parse(data, version='2.1')
+        stix2.parse_observable(data, version='2.1')
 
     assert excinfo.value.cls == stix2.v21.EmailMessage
     assert excinfo.value.dependencies == [("is_multipart", "body")]
@@ -548,7 +548,7 @@ def test_parse_file_archive(data):
 )
 def test_parse_email_message_with_at_least_one_error(data):
     with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
-        stix2.parse(data, version='2.1')
+        stix2.parse_observable(data, version='2.1')
 
     assert excinfo.value.cls == stix2.v21.EmailMessage
     assert "At least one of the" in str(excinfo.value)
@@ -570,7 +570,7 @@ def test_parse_email_message_with_at_least_one_error(data):
     ],
 )
 def test_parse_basic_tcp_traffic(data):
-    odata = stix2.parse(
+    odata = stix2.parse_observable(
         data, version='2.1',
     )
 
@@ -602,7 +602,7 @@ def test_parse_basic_tcp_traffic(data):
 )
 def test_parse_basic_tcp_traffic_with_error(data):
     with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo:
-        stix2.parse(data, version='2.1')
+        stix2.parse_observable(data, version='2.1')
 
     assert excinfo.value.cls == stix2.v21.NetworkTraffic
     assert excinfo.value.properties == ["dst_ref", "src_ref"]
@@ -1117,28 +1117,6 @@ def test_network_traffic_socket_example():
     assert nt.extensions['socket-ext'].socket_type == "SOCK_STREAM"
 
 
-def test_incorrect_socket_options():
-    with pytest.raises(ValueError) as excinfo:
-        stix2.v21.SocketExt(
-            is_listening=True,
-            address_family="AF_INET",
-            protocol_family="PF_INET",
-            socket_type="SOCK_STREAM",
-            options={"RCVTIMEO": 100},
-        )
-    assert "Incorrect options key" == str(excinfo.value)
-
-    with pytest.raises(ValueError) as excinfo:
-        stix2.v21.SocketExt(
-            is_listening=True,
-            address_family="AF_INET",
-            protocol_family="PF_INET",
-            socket_type="SOCK_STREAM",
-            options={"SO_RCVTIMEO": '100'},
-        )
-    assert "Options value must be an integer" == str(excinfo.value)
-
-
 def test_network_traffic_tcp_example():
     h = stix2.v21.TCPExt(src_flags_hex="00000002")
     nt = stix2.v21.NetworkTraffic(
@@ -1388,18 +1366,6 @@ def test_x509_certificate_example():
     assert x509.subject == "C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=baccala@freesoft.org"  # noqa
 
 
-def test_x509_certificate_error():
-
-    with pytest.raises(stix2.exceptions.PropertyPresenceError) as excinfo:
-        stix2.v21.X509Certificate(
-            defanged=True,
-        )
-
-    assert excinfo.value.cls == stix2.v21.X509Certificate
-    assert "At least one of the" in str(excinfo.value)
-    assert "properties for X509Certificate must be populated." in str(excinfo.value)
-
-
 def test_new_version_with_related_objects():
     data = stix2.v21.ObservedData(
         first_observed="2016-03-12T12:00:00Z",
diff --git a/stix2/test/v21/test_properties.py b/stix2/test/v21/test_properties.py
index 50bce17..1fb3cc4 100644
--- a/stix2/test/v21/test_properties.py
+++ b/stix2/test/v21/test_properties.py
@@ -72,14 +72,6 @@ def test_list_property():
         p.clean([])
 
 
-def test_dictionary_property():
-    p = DictionaryProperty(StringProperty)
-
-    assert p.clean({'spec_version': '2.1'})
-    with pytest.raises(ValueError):
-        p.clean({})
-
-
 def test_string_property():
     prop = StringProperty()
 
@@ -419,7 +411,6 @@ def test_property_list_of_dictionary():
     "value", [
         {"sha256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b"},
         [('MD5', '2dfb1bcc980200c6706feee399d41b3f'), ('RIPEMD-160', 'b3a8cd8a27c90af79b3c81754f267780f443dfef')],
-        [('TLSH', '6FF02BEF718027B0160B4391212923ED7F1A463D563B1549B86CF62973B197AD2731F8')],
     ],
 )
 def test_hashes_property_valid(value):
@@ -431,7 +422,6 @@ def test_hashes_property_valid(value):
     "value", [
         {"MD5": "a"},
         {"SHA-256": "2dfb1bcc980200c6706feee399d41b3f"},
-        {"TLSH": "6FF02BEF718027B0160B4391212923ED7F1A463D563B1549B86CF62973B197AD2731F"},
     ],
 )
 def test_hashes_property_invalid(value):