Repository: oasis-open/cti-stix-validator
Pull request #166 (2021-03-21 01:31:51)

Add support for 2.1 extensions
Also removes checks for the format of custom object types/properties/observable extensions (the new extension mechanism is now favored over using x_org_name style names).

Also adds support for the new Incident stub.


Patch:
diff --git a/docs/best-practices.rst b/docs/best-practices.rst
index 0bca34f..1d0b98a 100644
--- a/docs/best-practices.rst
+++ b/docs/best-practices.rst
@@ -47,7 +47,7 @@ Mandatory Checks - STIX 2.1
 |                                 |                                        |                                        |
 |                                 |                                        | '<object>': '<extension>':             |
 |                                 |                                        | '<property>': '<timestamp>' is not a   |
-|                                 |                                        | a valid timestamp: <message>           |
+|                                 |                                        | valid timestamp: <message>             |
 |                                 |                                        |                                        |
 |                                 |                                        | '<object>': '<property>':              |
 |                                 |                                        | '<embedded-property>' is not a valid   |
@@ -162,11 +162,16 @@ Optional Checks - STIX 2.1
 |   1    | format-checks               | all 1xx checks are run. Specifically:  |                                        |
 |        |                             |                                        |                                        |
 +--------+-----------------------------+----------------------------------------+----------------------------------------+
-|  101   | custom-prefix               | names of custom object types,          | custom object type '<object>' should   |
-|        |                             | properties, observable objects,        | start with 'x-' followed by a source   |
-|        |                             | observable object properties, and      | unique identifier (like a domain name  |
-|        |                             | observable object extensions follow    | with dots replaced by hyphens), a      |
-|        |                             | the correct format                     | hyphen and then the name.              |
+|  101   | custom-prefix               | names of custom object types,          | Note: This checks functionality that   |
+|        |                             | properties, observable objects,        | has been deprecated and replaced by    |
+|        |                             | observable object properties, and      | extensions. Thus, this check only runs |
+|        |                             | observable object extensions follow    | if extensions-use (401) is disabled.   |
+|        |                             | the correct format                     |                                        |
+|        |                             |                                        | custom object type '<object>' should   |
+|        |                             |                                        | start with 'x-' followed by a source   |
+|        |                             |                                        | unique identifier (like a domain name  |
+|        |                             |                                        | with dots replaced by hyphens), a      |
+|        |                             |                                        | hyphen and then the name.              |
 |        |                             |                                        |                                        |
 |        |                             |                                        | custom property '<property>' should    |
 |        |                             |                                        | have a type that starts with 'x\_'     |
@@ -214,8 +219,13 @@ Optional Checks - STIX 2.1
 |        |                             |                                        | with dots replaced by hyphens), a      |
 |        |                             |                                        | hyphen and then the name.              |
 +--------+-----------------------------+----------------------------------------+----------------------------------------+
-|  102   | custom-prefix-lax           | same as 101 but more lenient; no       | custom object type '<object>' should   |
-|        |                             | source identifier needed in prefix     | start with 'x-' in order to be         |
+|  102   | custom-prefix-lax           | same as 101 but more lenient; no       | Note: This checks functionality that   |
+|        |                             | source identifier needed in prefix     | has been deprecated and replaced by    |
+|        |                             |                                        | extensions. Thus, this check only runs |
+|        |                             |                                        | if extensions-use (401) is disabled.   |
+|        |                             |                                        |                                        |
+|        |                             |                                        | custom object type '<object>' should   |
+|        |                             |                                        | start with 'x-' in order to be         |
 |        |                             |                                        | compatible with future versions of the |
 |        |                             |                                        | STIX 2 specification.                  |
 |        |                             |                                        |                                        |
@@ -512,3 +522,26 @@ Optional Checks - STIX 2.1
 |        |                             | deprecated are not being used          | deprecated within the indicated        |
 |        |                             |                                        | spec version.                          |
 +--------+-----------------------------+----------------------------------------+----------------------------------------+
+|  305   | extension-description       | Extension Definitions have a           | The 'description' property SHOULD be   |
+|        |                             | description property                   | populated.                             |
++--------+-----------------------------+----------------------------------------+----------------------------------------+
+|  306   | extension-properties        | Ensure toplevel-property-extensions    | For extensions of the 'toplevel-       |
+|        |                             | include the extension_properties       | property-extension' type, the          |
+|        |                             | property                               | 'extension_properties' property SHOULD |
+|        |                             |                                        | include one or more property names.    |
++--------+-----------------------------+----------------------------------------+----------------------------------------+
+|  401   | extensions-use              | custom objects, properties, and        | Custom object type '<object>' should be|
+|        |                             | observable extensions have been        | implemented using an extension with an |
+|        |                             | implemented with Extension Definitions | 'extension_type' of 'new-sdo'.         |
+|        |                             |                                        |                                        |
+|        |                             |                                        | Custom property '<property>' should be |
+|        |                             |                                        | 'implemented using an extension with an|
+|        |                             |                                        | 'extension_type' of 'property-         |
+|        |                             |                                        | extension' or 'toplevel-property-      |
+|        |                             |                                        | extension'.                            |
+|        |                             |                                        |                                        |
+|        |                             |                                        | Custom Cyber Observable Object         |
+|        |                             |                                        | extension type '<extension>' should be |
+|        |                             |                                        | implemented using an 'extension_type'  |
+|        |                             |                                        | of 'property-extension'.               |
++--------+-----------------------------+----------------------------------------+----------------------------------------+
diff --git a/stix2validator/schemas-2.1 b/stix2validator/schemas-2.1
index 6705f6b..b6fed88 160000
--- a/stix2validator/schemas-2.1
+++ b/stix2validator/schemas-2.1
@@ -1,1 +1,1 @@
-Subproject commit 6705f6b398509746ed68acfe6f45ecc8d95783a1
+Subproject commit b6fed88542d1d093356b0fb12dc0bec6f9c30f23
diff --git a/stix2validator/util.py b/stix2validator/util.py
index ca6e039..efe260b 100644
--- a/stix2validator/util.py
+++ b/stix2validator/util.py
@@ -131,6 +131,14 @@ https://stix2-validator.readthedocs.io/en/latest/best-practices.html.
 |      |                             | description properties                 |
 | 304  | deprecated-properties       | certain properties which have been     |
 |      |                             | deprecated are not being used          |
+| 305  | extension-description       | Extension Definitions have a           |
+|      |                             | description property                   |
+| 306  | extension-properties        | Ensure toplevel-property-extensions    |
+|      |                             | include the extension_properties       |
+|      |                             | property                               |
+| 401  | extensions-use              | custom objects, properties, and        |
+|      |                             | observable extensions have been        |
+|      |                             | implemented with Extension Definitions |
 +------+-----------------------------+----------------------------------------+
 """.format(DEFAULT_VER)
 
diff --git a/stix2validator/v21/enums.py b/stix2validator/v21/enums.py
index add4d79..16615fb 100644
--- a/stix2validator/v21/enums.py
+++ b/stix2validator/v21/enums.py
@@ -411,6 +411,7 @@ TYPES = [
     "course-of-action",
     "grouping",
     "identity",
+    "incident",
     "indicator",
     "infrastructure",
     "intrusion-set",
@@ -429,6 +430,7 @@ TYPES = [
     "sighting",
     "language-content",
     "marking-definition",
+    "extension-definition",
 ]
 
 OBSERVABLE_TYPES = [
@@ -484,6 +486,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'name',
         'description',
         'kill_chain_phases',
@@ -503,6 +506,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'name',
         'description',
         'aliases',
@@ -524,6 +528,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'name',
         'description',
         'action'
@@ -542,6 +547,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'name',
         'description',
         'context',
@@ -561,6 +567,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'name',
         'description',
         'roles',
@@ -568,13 +575,29 @@ PROPERTIES = {
         'sectors',
         'contact_information',
     ],
+    "incident": [
+        'type',
+        'spec_version',
+        'id',
+        'created_by_ref',
+        'created',
+        'modified',
+        'revoked',
+        'labels',
+        'confidence',
+        'lang',
+        'external_references',
+        'object_marking_refs',
+        'granular_markings',
+        'extensions',
+        'name',
+        'description',
+    ],
     "indicator": [
         'type',
         'spec_version',
         'id',
         'created_by_ref',
-        "pattern_type",
-        "pattern_version",
         'created',
         'modified',
         'revoked',
@@ -584,10 +607,13 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'name',
         'description',
         'indicator_types',
         'pattern',
+        "pattern_type",
+        "pattern_version",
         'valid_from',
         'valid_until',
         'kill_chain_phases',
@@ -607,6 +633,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'name',
         'description',
         'infrastructure_types',
@@ -628,6 +655,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'name',
         'description',
         'aliases',
@@ -653,6 +681,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'description',
         'latitude',
         'longitude',
@@ -678,6 +707,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'name',
         'description',
         'malware_types',
@@ -706,6 +736,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'product',
         'version',
         'host_vm_ref',
@@ -737,6 +768,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'abstract',
         'content',
         'authors',
@@ -756,6 +788,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'first_observed',
         'last_observed',
         'number_observed',
@@ -776,6 +809,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'explanation',
         'authors',
         'object_refs',
@@ -795,6 +829,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'name',
         'description',
         'report_types',
@@ -817,6 +852,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'name',
         'description',
         'threat_actor_types',
@@ -843,6 +879,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'name',
         'description',
         'tool_types',
@@ -864,6 +901,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'name',
         'description',
     ],
@@ -886,6 +924,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'relationship_type',
         'description',
         'source_ref',
@@ -908,6 +947,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'first_seen',
         'last_seen',
         'count',
@@ -929,6 +969,7 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'object_ref',
         'object_modified',
         'contents',
@@ -943,10 +984,30 @@ PROPERTIES = {
         'external_references',
         'object_marking_refs',
         'granular_markings',
+        'extensions',
         'name',
         'definition_type',
         'definition',
-    ]
+    ],
+    "extension-definition": {
+        'type',
+        'spec_version',
+        'id',
+        'created_by_ref',
+        'created',
+        'modified',
+        'revoked',
+        'labels',
+        'external_references',
+        'object_marking_refs',
+        'granular_markings',
+        'name',
+        'description',
+        'schema',
+        'version',
+        'extension_types',
+        'extension_properties',
+    }
 }
 # Mappings of official Cyber Observable Objects to their official properties
 OBSERVABLE_PROPERTIES = {
@@ -1595,11 +1656,11 @@ OBSERVABLE_DICT_KEY_EXCEPTIONS = [
 # Reserved properties and objects
 RESERVED_PROPERTIES = [
     'severity',
-    'usernames',
+    'username',
     'phone_numbers',
+    'action',
 ]
 RESERVED_OBJECTS = [
-    'incident',
     'action',
 ]
 OBSERVABLE_RESERVED_OBJECTS = [
@@ -1613,6 +1674,7 @@ NON_SDOS = [
     'marking-definition',
     'sighting',
     'relationship',
+    'extension-definition',
 ]
 
 # List of relationship types common to all object types
@@ -2120,6 +2182,9 @@ CHECK_CODES = {
     '302': 'extref-hashes',
     '303': 'indicator-properties',
     '304': 'deprecated-properties',
+    '305': 'extension-description',
+    '306': 'extension-properties',
+    '401': 'extensions-use',
 }
 
 
diff --git a/stix2validator/v21/musts.py b/stix2validator/v21/musts.py
index e502f41..5d7a247 100644
--- a/stix2validator/v21/musts.py
+++ b/stix2validator/v21/musts.py
@@ -439,14 +439,14 @@ def patterns(instance, options):
             yield PatternError("'%s' is not a valid observable type name"
                                % objtype, instance['id'])
         elif (all(x not in options.disabled for x in ['all', 'format-checks', 'custom-prefix']) and
-              not CUSTOM_TYPE_PREFIX_RE.match(objtype)):
+              'extensions-use' in options.disabled and not CUSTOM_TYPE_PREFIX_RE.match(objtype)):
             yield PatternError("Custom Observable Object type '%s' should start "
                                "with 'x-' followed by a source unique identifier "
                                "(like a domain name with dots replaced by "
                                "hyphens), a hyphen and then the name"
                                % objtype, instance['id'])
         elif (all(x not in options.disabled for x in ['all', 'format-checks', 'custom-prefix-lax']) and
-              not CUSTOM_TYPE_LAX_PREFIX_RE.match(objtype)):
+              'extensions-use' in options.disabled and not CUSTOM_TYPE_LAX_PREFIX_RE.match(objtype)):
             yield PatternError("Custom Observable Object type '%s' should start "
                                "with 'x-'" % objtype, instance['id'])
 
@@ -464,7 +464,7 @@ def patterns(instance, options):
             elif objtype not in enums.OBSERVABLE_TYPES:
                 continue  # custom SCOs aren't required to use x_ prefix on properties
             elif (all(x not in options.disabled for x in ['all', 'format-checks', 'custom-prefix']) and
-                  not CUSTOM_PROPERTY_PREFIX_RE.match(prop)):
+                  'extensions-use' in options.disabled and not CUSTOM_PROPERTY_PREFIX_RE.match(prop)):
                 yield PatternError("Cyber Observable Object custom property '%s' "
                                    "should start with 'x_' followed by a source "
                                    "unique identifier (like a domain name with "
@@ -472,7 +472,7 @@ def patterns(instance, options):
                                    "underscore and then the name"
                                    % prop, instance['id'])
             elif (all(x not in options.disabled for x in ['all', 'format-checks', 'custom-prefix-lax']) and
-                  not CUSTOM_PROPERTY_LAX_PREFIX_RE.match(prop)):
+                  'extensions-use' in options.disabled and not CUSTOM_PROPERTY_LAX_PREFIX_RE.match(prop)):
                 yield PatternError("Cyber Observable Object custom property '%s' "
                                    "should start with 'x_'" % prop, instance['id'])
 
diff --git a/stix2validator/v21/shoulds.py b/stix2validator/v21/shoulds.py
index 86d42a7..718e08e 100644
--- a/stix2validator/v21/shoulds.py
+++ b/stix2validator/v21/shoulds.py
@@ -86,7 +86,6 @@ def custom_object_prefix_lax(instance):
 
 def custom_property_prefix_strict(instance):
     """Ensure custom properties follow strict naming style conventions.
-
     Does not check property names in custom objects.
     """
     for prop_name in instance.keys():
@@ -106,7 +105,6 @@ def custom_property_prefix_strict(instance):
 def custom_property_prefix_lax(instance):
     """Ensure custom properties follow lenient naming style conventions
     for forward-compatibility.
-
     Does not check property names in custom objects.
     """
     for prop_name in instance.keys():
@@ -121,6 +119,149 @@ def custom_property_prefix_lax(instance):
                             'custom-prefix-lax')
 
 
+def _uses_extension(instance, extension_types):
+    """Return true if instance contains an extension using any of the given
+    extension_types.
+    """
+    for identifier, ext in instance.get('extensions', {}).items():
+        if ext.get('extension_type', '') in extension_types:
+            return True
+
+    return False
+
+
+def extensions_use(instance):
+    """Ensure custom objects, properties, and observable extensions have been
+    implemented with Extension Definitions.
+    """
+    for error in chain(custom_object(instance),
+                       custom_property(instance),
+                       custom_observable_embedded_property(instance),
+                       custom_observable_extension_property(instance),
+                       custom_observable_extension(instance),
+                       custom_marking_definition(instance)):
+        yield error
+
+
+def custom_object(instance):
+    """Ensure custom objects use the extension mechanism.
+    """
+    type_ = instance['type']
+    if (type_ not in enums.TYPES and
+            type_ not in enums.RESERVED_OBJECTS and
+            type_ not in enums.OBSERVABLE_TYPES and
+            not _uses_extension(instance, ['new-sdo', 'new-sco', 'new-sro'])):
+        yield JSONError("Custom object type '%s' should be implemented using "
+                        "an extension with an 'extension_type' of 'new-sdo', "
+                        "'new-sco', or 'new-sro'."
+                        % type_, instance['id'], 'extensions-use')
+
+
+def custom_property(instance):
+    """Ensure custom properties are implemented using the 2.1 extension
+    mechanism.
+    """
+    type_ = instance['type']
+    for prop_name in instance.keys():
+        if (((type_ in enums.PROPERTIES and
+                prop_name not in enums.PROPERTIES[type_]) or
+                ((type_ in enums.OBSERVABLE_PROPERTIES and
+                  prop_name not in enums.OBSERVABLE_PROPERTIES[type_]))) and
+                prop_name not in enums.RESERVED_PROPERTIES and
+                not _uses_extension(instance, ['toplevel-property-extension'])):
+
+            yield JSONError("Custom property '%s' should be implemented using "
+                            "an extension with an 'extension_type' of "
+                            "'property-extension' or 'toplevel-property-"
+                            "extension'." % prop_name, instance['id'],
+                            'extensions-use')
+
+
+@cyber_observable_check("2.1")
+def custom_observable_embedded_property(instance):
+    """Ensure custom properties in observable object embedded properties are
+    implemented using the 2.1 extension mechanism.
+    """
+    type_ = instance['type']
+    message = ("Cyber Observable Object custom property '%s' in the %s property"
+               " of %s object should be implemented using an extension with an "
+               "'extension_type' of 'property-extension' or 'toplevel-property-"
+               "extension'.")
+
+    for prop in instance.keys():
+        if (type_ in enums.OBSERVABLE_EMBEDDED_PROPERTIES and
+                prop in enums.OBSERVABLE_EMBEDDED_PROPERTIES[type_]):
+            for embed_prop in instance[prop]:
+                if isinstance(embed_prop, dict):
+                    for embedded in embed_prop:
+                        if (embedded not in enums.OBSERVABLE_EMBEDDED_PROPERTIES[type_][prop]):
+                            yield JSONError(message % (embedded, prop, type_),
+                                            instance['id'], 'extensions-use')
+                elif (embed_prop not in enums.OBSERVABLE_EMBEDDED_PROPERTIES[type_][prop]):
+                    yield JSONError(message % (embed_prop, prop, type_),
+                                    instance['id'], 'extensions-use')
+
+
+@cyber_observable_check("2.1")
+def custom_observable_extension_property(instance):
+    """Ensure custom properties in predefined observable object extensions are
+    implemented using the 2.1 extension mechanism.
+    """
+    type_ = instance['type']
+    if type_ in enums.OBSERVABLE_EXTENSIONS and 'extensions' in instance:
+        for ext_key in instance['extensions']:
+
+            if ext_key in enums.OBSERVABLE_EXTENSIONS[type_]:
+                for ext_prop in instance['extensions'][ext_key]:
+                    if (ext_prop not in enums.OBSERVABLE_EXTENSION_PROPERTIES[ext_key]):
+                        yield JSONError("Cyber Observable Object custom property '%s' in the %s extension "
+                                        "should be implemented using an extension with an 'extension_type' of "
+                                        "'property-extension' or 'toplevel-property-extension'."
+                                        % (ext_prop, ext_key), instance['id'],
+                                        'extensions-use')
+
+                    if (ext_key in enums.OBSERVABLE_EXTENSION_EMBEDDED_PROPERTIES and
+                            ext_prop in enums.OBSERVABLE_EXTENSION_EMBEDDED_PROPERTIES[ext_key]):
+                        for embed_prop in instance['extensions'][ext_key][ext_prop]:
+                            if not (isinstance(embed_prop, Iterable) and not isinstance(embed_prop, str)):
+                                embed_prop = [embed_prop]
+                            for p in embed_prop:
+                                if (p not in enums.OBSERVABLE_EXTENSION_EMBEDDED_PROPERTIES[ext_key][ext_prop]):
+                                    yield JSONError("Cyber Observable Object custom property '%s' in the %s "
+                                                    "property of the %s extension should be implemented using "
+                                                    "an extension with an 'extension_type' of "
+                                                    "'property-extension' or 'toplevel-property-extension'."
+                                                    % (p, ext_prop, ext_key), instance['id'],
+                                                    'extensions-use')
+
+
+@cyber_observable_check("2.1")
+def custom_observable_extension(instance):
+    """Ensure custom observable object extensions are implemented using the 2.1
+    extension mechanism.
+    """
+    type_ = instance['type']
+    if not ('extensions' in instance and isinstance(instance['extensions'], Mapping)
+            and type_ in enums.OBSERVABLE_EXTENSIONS):
+        return
+    for ext_key in instance['extensions']:
+        if (ext_key not in enums.OBSERVABLE_EXTENSIONS[type_] and
+                not _uses_extension(instance, ['property-extension'])):
+            yield JSONError("Custom Cyber Observable Object extension type "
+                            "'%s' should be implemented using an "
+                            "'extension_type' of 'property-extension'."
+                            % ext_key, instance['id'], 'extensions-use')
+
+
+def custom_marking_definition(instance):
+    if (instance['type'] == 'marking-definition' and
+            'extensions' not in instance and
+            instance.get('definition_type') not in ['statement', 'tlp']):
+        yield JSONError("Custom marking definitions should be specified using "
+                        "the 'extensions' property.",
+                        instance['id'], 'extensions-use')
+
+
 def deprecated_property_check(instance):
     """Check to see if any included properties are deprecated within the spec
     """
@@ -1147,12 +1288,6 @@ def properties_strict(instance):
     if instance['type'] not in enums.TYPES and instance['type'] not in enums.OBSERVABLE_TYPES:
         return  # only check properties for official objects
 
-    defined_props = enums.PROPERTIES.get(instance['type'], [])
-    for prop in instance.keys():
-        if prop not in defined_props:
-            yield JSONError("Property '%s' is not one of those defined in the"
-                            " specification." % prop, instance['id'])
-
     if has_cyber_observable_data(instance, "2.1"):
         if instance['type'] == 'observable_data':
             for key, obj in instance['objects'].items():
@@ -1161,6 +1296,12 @@ def properties_strict(instance):
         else:
             for error in properties_strict_helper(instance, instance['id']):
                 yield error
+    else:
+        defined_props = enums.PROPERTIES.get(instance['type'], [])
+        for prop in instance.keys():
+            if prop not in defined_props:
+                yield JSONError("Property '%s' is not one of those defined in the"
+                                " specification." % prop, instance['id'])
 
     if (instance['type'] == 'indicator' and instance.get('pattern_type', '') == 'stix' and
             isinstance(instance.get('pattern', ''), str)):
@@ -1217,25 +1358,45 @@ def properties_strict_helper(obj, obj_id):
             embedded_ext_props = enums.OBSERVABLE_EXTENSION_EMBEDDED_PROPERTIES.get(ext_key, {}).get(ext_prop, [])
             if embedded_ext_props:
                 for embed_ext_prop in obj['extensions'][ext_key].get(ext_prop, []):
-                    if embed_ext_prop not in embedded_ext_props:
-                        yield JSONError("Property '%s' in the %s property of the %s extension "
-                                        "is not one of those defined in the specification."
-                                        % (embed_ext_prop, ext_prop, ext_key), obj_id)
+                    if not (isinstance(embed_ext_prop, Iterable) and not isinstance(embed_ext_prop, str)):
+                        embed_ext_prop = [embed_ext_prop]
+                    for p in embed_ext_prop:
+                        if p not in embedded_ext_props:
+                            yield JSONError("Property '%s' in the %s property of the %s extension "
+                                            "is not one of those defined in the specification."
+                                            % (p, ext_prop, ext_key), obj_id)
+
+
+def extension_description(instance):
+    """Ensure Extension Definitions have a description.
+    """
+    if instance['type'] == 'extension-definition':
+        if 'description' not in instance:
+            yield JSONError("The 'description' property SHOULD be populated.",
+                            instance['id'], 'extension-description')
+
+
+def extension_properties(instance):
+    """Ensure toplevel-property-extensions include at least one extension
+    property.
+    """
+    if instance['type'] == 'extension-definition':
+        if ('toplevel-property-extension' in instance.get('extension_types', [])
+                and 'extension_properties' not in instance):
+            yield JSONError("For extensions of the 'toplevel-property-extension'"
+                            " type, the 'extension_properties' property SHOULD include "
+                            "one or more property names.",
+                            instance['id'], 'extension-properties')
 
 
 # Mapping of check names to the functions which perform the checks
 CHECKS = {
     'all': [
-        custom_object_prefix_strict,
-        custom_property_prefix_strict,
         uuid_check,
         open_vocab_values,
         kill_chain_phase_names,
         observable_object_keys,
         observable_dictionary_keys,
-        custom_observable_object_prefix_strict,
-        custom_object_extension_prefix_strict,
-        custom_observable_properties_prefix_strict,
         malware_analysis_product,
         windows_process_priority_format,
         hash_length,
@@ -1275,18 +1436,16 @@ CHECKS = {
         extref_hashes,
         indicator_property_check,
         deprecated_property_check,
+        extension_description,
+        extension_properties,
+        extensions_use,
     ],
     'format-checks': [
-        custom_object_prefix_strict,
-        custom_property_prefix_strict,
         uuid_check,
         open_vocab_values,
         kill_chain_phase_names,
         observable_object_keys,
         observable_dictionary_keys,
-        custom_observable_object_prefix_strict,
-        custom_object_extension_prefix_strict,
-        custom_observable_properties_prefix_strict,
         malware_analysis_product,
         windows_process_priority_format,
         hash_length,
@@ -1405,6 +1564,9 @@ CHECKS = {
     'extref-hashes': extref_hashes,
     'indicator-properties': indicator_property_check,
     'deprecated-properties': deprecated_property_check,
+    'extension-description': extension_description,
+    'extension-properties': extension_properties,
+    'extensions-use': extensions_use,
 }
 
 
@@ -1435,10 +1597,6 @@ def list_shoulds(options):
     if options.disabled:
         if 'all' not in options.disabled:
             if 'format-checks' not in options.disabled:
-                if 'custom-prefix' not in options.disabled:
-                    validator_list.append(CHECKS['custom-prefix'])
-                elif 'custom-prefix-lax' not in options.disabled:
-                    validator_list.append(CHECKS['custom-prefix-lax'])
                 if 'open-vocab-format' not in options.disabled:
                     validator_list.append(CHECKS['open-vocab-format'])
                 if 'kill-chain-names' not in options.disabled:
@@ -1519,18 +1677,29 @@ def list_shoulds(options):
             if 'deprecated-properties' not in options.disabled:
                 validator_list.append(CHECKS['deprecated-properties'])
 
+            if 'extensions-use' not in options.disabled:
+                validator_list.append(CHECKS['extensions-use'])
+            elif 'format-checks' not in options.disabled:
+                # extensions-use is disabled but format-checks is not
+                if 'custom-prefix' not in options.disabled:
+                    validator_list.append(CHECKS['custom-prefix'])
+                elif 'custom-prefix-lax' not in options.disabled:
+                    validator_list.append(CHECKS['custom-prefix-lax'])
+
     # --enable
     if options.enabled:
         for check in options.enabled:
             try:
-                if CHECKS[check] in validator_list:
-                    continue
-
-                if type(CHECKS[check]) is list:
-                    validator_list.extend(CHECKS[check])
-                else:
-                    validator_list.append(CHECKS[check])
+                check_function = CHECKS[check]
             except KeyError:
                 raise JSONError("%s is not a valid check!" % check)
 
+            if check_function in validator_list:
+                continue
+
+            if type(check_function) is list:
+                validator_list.extend(check_function)
+            else:
+                validator_list.append(check_function)
+
     return validator_list


Test patch:
diff --git a/stix2validator/test/v21/attack_pattern_tests.py b/stix2validator/test/v21/attack_pattern_tests.py
index 2114afb..d594b22 100644
--- a/stix2validator/test/v21/attack_pattern_tests.py
+++ b/stix2validator/test/v21/attack_pattern_tests.py
@@ -47,26 +47,75 @@ class AttackPatternTestCases(ValidatorTest):
     def test_invalid_property_prefix(self):
         attack_pattern = copy.deepcopy(self.valid_attack_pattern)
         attack_pattern['x-something'] = "some value"
-        results = validate_parsed_json(attack_pattern, self.options)
-        self.assertEqual(results.is_valid, False)
+        self.assertFalseWithOptions(attack_pattern)
 
         self.assertFalseWithOptions(attack_pattern, enabled='custom-prefix-lax')
+        self.assertFalseWithOptions(attack_pattern, disabled='extensions-use,custom-prefix')
 
     def test_invalid_property_prefix_lax(self):
         attack_pattern = copy.deepcopy(self.valid_attack_pattern)
         attack_pattern['x_something'] = "some value"
-        results = validate_parsed_json(attack_pattern, self.options)
-        self.assertEqual(results.is_valid, False)
+        self.assertFalseWithOptions(attack_pattern)
 
         self.assertTrueWithOptions(attack_pattern, enabled='custom-prefix-lax')
         self.assertFalseWithOptions(attack_pattern, disabled='custom-prefix-lax')
-        self.check_ignore(attack_pattern, 'custom-prefix')
+        self.assertFalseWithOptions(attack_pattern, disabled='custom-prefix')
+        self.assertFalseWithOptions(attack_pattern, disabled='extensions-use,custom-prefix-lax')
+        self.assertTrueWithOptions(attack_pattern, disabled='extensions-use,custom-prefix')
 
     def test_valid_property_prefix(self):
         attack_pattern = copy.deepcopy(self.valid_attack_pattern)
         attack_pattern['x_source_something'] = "some value"
-        results = validate_parsed_json(attack_pattern, self.options)
-        self.assertTrue(results.is_valid)
+
+        self.assertTrueWithOptions(attack_pattern, enabled='custom-prefix')
+        self.assertTrueWithOptions(attack_pattern, disabled='extensions-use')
+
+    def test_custom_property_without_extension(self):
+        attack_pattern = copy.deepcopy(self.valid_attack_pattern)
+        attack_pattern['something'] = "some value"
+
+        self.assertFalseWithOptions(attack_pattern)
+        self.assertFalseWithOptions(attack_pattern, strict_properties=True)
+        self.assertFalseWithOptions(attack_pattern, strict_properties=True, disabled='extensions-use')
+        self.assertFalseWithOptions(attack_pattern, disabled='extensions-use')
+        self.assertTrueWithOptions(attack_pattern, disabled='extensions-use,custom-prefix,custom-prefix-lax')
+        self.assertFalseWithOptions(attack_pattern, strict_properties=True, disabled='extensions-use,custom-prefix,custom-prefix-lax')
+
+    def test_custom_property_with_extension(self):
+        attack_pattern = copy.deepcopy(self.valid_attack_pattern)
+        attack_pattern['something'] = "some value"
+        attack_pattern['extensions'] = {
+            "extension-definition--d83fce45-ef58-4c6c-a3f4-1fbc32e98c6e": {
+                "extension_type": "property-extension",
+                "rank": 5,
+                "toxicity": 8
+            }
+        }
+
+        self.assertFalseWithOptions(attack_pattern)
+        self.assertFalseWithOptions(attack_pattern, strict_properties=True)
+        self.assertFalseWithOptions(attack_pattern, strict_properties=True, disabled='extensions-use')
+
+        del attack_pattern['something']
+        self.assertTrueWithOptions(attack_pattern, strict_properties=True)
+
+    def test_custom_toplevel_property_with_extension(self):
+        attack_pattern = copy.deepcopy(self.valid_attack_pattern)
+        attack_pattern['something'] = "some value"
+        attack_pattern['rank'] = 5
+        attack_pattern['toxicity'] = 8
+        attack_pattern['extensions'] = {
+            "extension-definition--a932fcc6-e032-176c-826f-cb970a5a1fff": {
+                "extension_type": "toplevel-property-extension",
+            }
+        }
+
+        self.assertTrueWithOptions(attack_pattern)
+        self.assertFalseWithOptions(attack_pattern, strict_properties=True)
+        self.assertFalseWithOptions(attack_pattern, strict_properties=True, disabled='extensions-use')
+        self.assertFalseWithOptions(attack_pattern, disabled='extensions-use')
+        self.assertTrueWithOptions(attack_pattern, disabled='extensions-use,custom-prefix,custom-prefix-lax')
+        self.assertFalseWithOptions(attack_pattern, strict_properties=True, disabled='extensions-use,custom-prefix,custom-prefix-lax')
 
     def test_invalid_timestamp(self):
         attack_pattern = copy.deepcopy(self.valid_attack_pattern)
diff --git a/stix2validator/test/v21/custom_obj_tests.py b/stix2validator/test/v21/custom_obj_tests.py
index 67fd8a9..51660f1 100644
--- a/stix2validator/test/v21/custom_obj_tests.py
+++ b/stix2validator/test/v21/custom_obj_tests.py
@@ -9,10 +9,15 @@ VALID_CUSTOM_OBJECT = u"""
   "type": "x-example-com-customobject",
   "spec_version": "2.1",
   "id": "x-example-com-customobject--4527e5de-8572-446a-a57a-706f15467461",
-  "created": "2016-08-01T00:00:00.000Z",
-  "modified": "2016-08-01T00:00:00.000Z",
+  "created": "2021-02-20T09:16:08.989000Z",
+  "modified": "2021-02-20T09:16:08.989000Z",
   "some_custom_stuff": 14,
-  "other_custom_stuff": "hello"
+  "other_custom_stuff": "hello",
+  "extensions": {
+    "extension-definition--1bba6c39-7ac1-40a2-819a-f33f8ea81a25" : {
+       "extension_type" : "new-sdo"
+    }
+  }
 }
 """
 
@@ -51,26 +56,26 @@ class CustomObjectTestCases(ValidatorTest):
         custom_obj = copy.deepcopy(self.valid_custom_object)
         custom_obj['type'] = "corpo_ration"
         custom_obj['id'] = "corpo_ration--4527e5de-8572-446a-a57a-706f15467461"
-        results = validate_parsed_json(custom_obj, self.options)
-        self.assertEqual(results.is_valid, False)
+        self.assertFalseWithOptions(custom_obj)
 
         custom_obj['type'] = "corpor@tion"
         custom_obj['id'] = "corpor@tion--4527e5de-8572-446a-a57a-706f15467461"
-        results = validate_parsed_json(custom_obj, self.options)
-        self.assertEqual(results.is_valid, False)
+        self.assertFalseWithOptions(custom_obj)
+
+        self.assertFalseWithOptions(custom_obj, enabled='extensions-use')
+        self.assertFalseWithOptions(custom_obj, disabled='extensions-use')
 
         self.assertFalseWithOptions(custom_obj, enabled='custom-prefix-lax')
-        self.assertFalseWithOptions(custom_obj, disabled='custom-prefix-lax')
+        self.assertFalseWithOptions(custom_obj, disabled='extensions-use,custom-prefix')
 
     def test_invalid_type_name_lax(self):
         custom_obj = copy.deepcopy(self.valid_custom_object)
         custom_obj['type'] = "x-corporation"
         custom_obj['id'] = "x-corporation--4527e5de-8572-446a-a57a-706f15467461"
-        results = validate_parsed_json(custom_obj, self.options)
-        self.assertEqual(results.is_valid, False)
 
         self.assertTrueWithOptions(custom_obj, enabled='custom-prefix-lax')
-        self.check_ignore(custom_obj, 'custom-prefix')
+        self.assertFalseWithOptions(custom_obj, disabled='extensions-use')
+        self.assertTrueWithOptions(custom_obj, disabled='extensions-use,custom-prefix')
 
     def test_valid_type_name(self):
         custom_obj = copy.deepcopy(self.valid_custom_object)
diff --git a/stix2validator/test/v21/extensions_tests.py b/stix2validator/test/v21/extensions_tests.py
new file mode 100644
index 0000000..711198d
--- /dev/null
+++ b/stix2validator/test/v21/extensions_tests.py
@@ -0,0 +1,84 @@
+import copy
+import json
+
+from . import ValidatorTest
+from ... import validate_string
+
+VALID_EXTENSION_DEFINITION = u"""
+{
+    "id": "extension-definition--04b2d3ef-d061-4912-ab77-6bbe807a5bd5",
+    "type": "extension-definition",
+    "spec_version": "2.1",
+    "name": "New SDO 1",
+    "description": "This schema creates a new object type called my-favorite-sdo-1",
+    "created": "2014-02-20T09:16:08.989000Z",
+    "modified": "2014-02-20T09:16:08.989000Z",
+    "created_by_ref": "identity--11b76a96-5d2b-45e0-8a5a-f6994f370731",
+    "schema": "https://www.example.com/schema-my-favorite-sdo-1/v1/",
+    "version": "1.2.1",
+    "extension_types": [ "new-sdo" ]
+}
+"""
+VALID_EXTENSION_NEW_SDO = u"""
+{
+    "type": "my-favorite-sdo",
+    "spec_version": "2.1",
+    "id": "my-favorite-sdo--a932fcc6-e032-476c-a26f-cb970a5a1ade",
+    "created": "2014-02-20T09:16:08.989000Z",
+    "modified": "2014-02-20T09:16:08.989000Z",
+    "name": "This is the name of my favorite",
+    "some_property_name1": "value1",
+    "some_property_name2": "value2",
+    "extensions": {
+        "extension-definition--04b2d3ef-d061-4912-ab77-6bbe807a5bd5" : {
+            "extension_type" : "new-sdo"
+        }
+    }
+}
+"""
+
+
+class NewSDOExtensionTestCases(ValidatorTest):
+    valid_ext_definition = json.loads(VALID_EXTENSION_DEFINITION)
+    valid_new_sdo = json.loads(VALID_EXTENSION_NEW_SDO)
+
+    def test_wellformed_ext_definition(self):
+        results = validate_string(VALID_EXTENSION_DEFINITION, self.options)
+        self.assertTrue(results.is_valid)
+
+    def test_ext_defn_invalid_extension_type(self):
+        extdef = copy.deepcopy(self.valid_ext_definition)
+        extdef['extension_types'] = ["something"]
+        self.assertFalseWithOptions(extdef)
+
+    def test_ext_defn_extprops_not_top_level(self):
+        extdef = copy.deepcopy(self.valid_ext_definition)
+        extdef['extension_properties'] = ["something"]
+        self.assertFalseWithOptions(extdef)
+
+        extdef['extension_types'] = ['toplevel-property-extension']
+        self.assertTrueWithOptions(extdef)
+
+    def test_toplvl_extension_without_properties(self):
+        extension = copy.deepcopy(self.valid_ext_definition)
+        extension['extension_types'] = ['toplevel-property-extension']
+        self.assertFalseWithOptions(extension)
+        self.assertTrueWithOptions(extension, disabled='extension-properties')
+
+        extension['extension_properties'] = ['some_custom_stuff', 'other_custom_stuff']
+        self.assertTrueWithOptions(extension)
+
+    def test_no_description(self):
+        extdef = copy.deepcopy(self.valid_ext_definition)
+        del extdef['description']
+        self.assertFalseWithOptions(extdef)
+        self.assertTrueWithOptions(extdef, disabled='extension-description')
+
+    def test_wellformed_new_sdo(self):
+        newsdo = copy.deepcopy(self.valid_new_sdo)
+        self.assertTrueWithOptions(newsdo)
+
+    def test_wellformed_new_sdo_invalid_extension_type(self):
+        newsdo = copy.deepcopy(self.valid_new_sdo)
+        newsdo['extensions']['extension-definition--04b2d3ef-d061-4912-ab77-6bbe807a5bd5']['extension_type'] = ["something"]
+        self.assertFalseWithOptions(newsdo)
diff --git a/stix2validator/test/v21/incident_tests.py b/stix2validator/test/v21/incident_tests.py
new file mode 100644
index 0000000..8b48efc
--- /dev/null
+++ b/stix2validator/test/v21/incident_tests.py
@@ -0,0 +1,30 @@
+import copy
+import json
+
+from . import ValidatorTest
+from ... import validate_string
+
+VALID_INCIDENT = u"""
+{
+  "type": "incident",
+  "spec_version": "2.1",
+  "id": "incident--dd8651ee-d529-44fe-b063-bb6f006fbe5c",
+  "created": "2021-03-20T08:17:27.000Z",
+  "modified": "2021-03-20T08:17:27.000Z",
+  "name": "FUBAR",
+  "description": "Something happened"
+}
+"""
+
+
+class IncidentTestCases(ValidatorTest):
+    valid_incident = json.loads(VALID_INCIDENT)
+
+    def test_wellformed_incident(self):
+        results = validate_string(VALID_INCIDENT, self.options)
+        self.assertTrue(results.is_valid)
+
+    def test_missing_name(self):
+        incident = copy.deepcopy(self.valid_incident)
+        del incident['name']
+        self.assertFalseWithOptions(incident)
diff --git a/stix2validator/test/v21/indicator_tests.py b/stix2validator/test/v21/indicator_tests.py
index 8bf36c0..59d4db2 100644
--- a/stix2validator/test/v21/indicator_tests.py
+++ b/stix2validator/test/v21/indicator_tests.py
@@ -140,32 +140,59 @@ class IndicatorTestCases(ValidatorTest):
         indicator['pattern'] = """[ab:yz = 'something']"""
         self.assertFalseWithOptions(indicator)
 
-    def test_pattern_custom_object_noprefix(self):
+    def test_pattern_custom_object_type_too_short(self):
+        indicator = copy.deepcopy(self.valid_indicator)
+        indicator['pattern'] = """[f:name = 'something']"""
+        self.assertFalseWithOptions(indicator)
+
+    def test_pattern_custom_object_type_valid(self):
         indicator = copy.deepcopy(self.valid_indicator)
         indicator['pattern'] = """[foo:name = 'something']"""
+        self.assertTrueWithOptions(indicator)
+
+    def test_pattern_custom_object_type_strict(self):
+        indicator = copy.deepcopy(self.valid_indicator)
+        indicator['pattern'] = """[x-x-foo:x_x_name = 'something']"""
+        self.assertTrueWithOptions(indicator)
+
+        self.assertFalseWithOptions(indicator, strict_types=True)
+
+    def test_pattern_custom_property_too_short(self):
+        indicator = copy.deepcopy(self.valid_indicator)
+        indicator['pattern'] = """[file:n = 'something']"""
         self.assertFalseWithOptions(indicator)
 
-        self.check_ignore(indicator, 'custom-prefix,custom-prefix-lax')
-        self.assertFalseWithOptions(indicator, disabled='custom-prefix')
+    def test_pattern_custom_property_valid(self):
+        indicator = copy.deepcopy(self.valid_indicator)
+        indicator['pattern'] = """[file:foo = 'something']"""
+        self.assertTrueWithOptions(indicator)
+
+    def test_pattern_custom_object_noprefix(self):
+        indicator = copy.deepcopy(self.valid_indicator)
+        indicator['pattern'] = """[foo:name = 'something']"""
+        self.assertFalseWithOptions(indicator, disabled='extensions-use')
+
+        self.check_ignore(indicator, 'custom-prefix,custom-prefix-lax,extensions-use')
+        self.assertFalseWithOptions(indicator, disabled='custom-prefix,extensions-use')
 
     def test_pattern_custom_object_prefix_strict(self):
         indicator = copy.deepcopy(self.valid_indicator)
         indicator['pattern'] = """[x-x-foo:x_x_name = 'something']"""
-        self.assertTrueWithOptions(indicator)
+        self.assertTrueWithOptions(indicator, disabled='extensions-use')
 
         self.assertFalseWithOptions(indicator, strict_types=True)
 
     def test_pattern_custom_object_prefix_lax(self):
         indicator = copy.deepcopy(self.valid_indicator)
         indicator['pattern'] = """[x-foo:x_name = 'something']"""
-        self.check_ignore(indicator, 'custom-prefix')
+        self.check_ignore(indicator, 'custom-prefix,extensions-use')
 
     def test_pattern_custom_property_prefix_strict(self):
         indicator = copy.deepcopy(self.valid_indicator)
         indicator['pattern'] = """[file:x_x_name = 'something']"""
-        self.assertTrueWithOptions(indicator)
+        self.assertTrueWithOptions(indicator, disabled='extensions-use')
 
-        self.assertFalseWithOptions(indicator, strict_properties=True)
+        self.assertFalseWithOptions(indicator, strict_properties=True, disabled='extensions-use')
 
     def test_pattern_list_object_property(self):
         indicator = copy.deepcopy(self.valid_indicator)
@@ -193,9 +220,15 @@ class IndicatorTestCases(ValidatorTest):
             "created": "2016-04-06T20:03:48.000Z",
             "modified": "2016-04-06T20:03:48.000Z",
             "property1": 10,
-            "property2": "fizzbuzz"
+            "property2": "fizzbuzz",
+            "extensions": {
+                "extension-definition--ba73205e-96bb-40d3-8168-0056d862b229": {
+                    "extension_type": "new-sdo"
+                }
+            }
         }
-        self.assertFalseWithOptions(new_obj, schema_dir=self.custom_schemas)
+        self.assertFalseWithOptions(new_obj, schema_dir=self.custom_schemas, strict_types=True)
+        self.assertTrueWithOptions(new_obj, schema_dir=self.custom_schemas)
 
         # properties are wrong types (str vs int)
         new_obj['type'] = 'x-new-type'
diff --git a/stix2validator/test/v21/marking_definition_tests.py b/stix2validator/test/v21/marking_definition_tests.py
index b543d2c..57a6221 100644
--- a/stix2validator/test/v21/marking_definition_tests.py
+++ b/stix2validator/test/v21/marking_definition_tests.py
@@ -2,7 +2,7 @@ import copy
 import json
 
 from . import ValidatorTest
-from ... import validate_parsed_json, validate_string
+from ... import validate_string
 
 VALID_MARKING_DEFINITION = u"""
 {
@@ -28,10 +28,10 @@ class MarkingDefinitionTestCases(ValidatorTest):
     def test_vocab_marking_definition_label(self):
         marking_definition = copy.deepcopy(self.valid_marking_definition)
         marking_definition['definition_type'] = "something"
-        results = validate_parsed_json(marking_definition, self.options)
-        self.assertEqual(results.is_valid, False)
+        self.assertFalseWithOptions(marking_definition)
+        self.assertFalseWithOptions(marking_definition, disabled='extensions-use')
 
-        self.check_ignore(marking_definition, 'marking-definition-type')
+        self.check_ignore(marking_definition, 'marking-definition-type,extensions-use')
 
     def test_lax_option(self):
         marking_definition = copy.deepcopy(self.valid_marking_definition)
@@ -85,3 +85,27 @@ class MarkingDefinitionTestCases(ValidatorTest):
             "selectors": ["id"]
         }]
         self.assertTrueWithOptions(marking_definition)
+
+    def test_marking_definition_missing_properties(self):
+        marking_definition = {
+            "type": "marking-definition",
+            "spec_version": "2.1",
+            "id": "marking-definition--336abc15-1c2a-423c-a7dd-5a821abd96c2",
+            "created": "2017-01-20T00:00:00.000Z",
+        }
+        self.assertFalseWithOptions(marking_definition)
+
+    def test_marking_definition_with_extension(self):
+        marking_definition = copy.deepcopy(self.valid_marking_definition)
+        del marking_definition['definition']
+        del marking_definition['definition_type']
+        self.assertFalseWithOptions(marking_definition)
+
+        marking_definition['extensions'] = {
+            "extension-definition--9ef47f81-1443-4632-8497-8b2878f8ac21": {
+                "extension_type": "property-extension",
+                "additional_marking_prop": "if this data is leaked then the world will end",
+                "required_data_storage_hash": "sha1024"
+            }
+        }
+        self.assertTrueWithOptions(marking_definition)
diff --git a/stix2validator/test/v21/misc_tests.py b/stix2validator/test/v21/misc_tests.py
index e14aa26..5451675 100644
--- a/stix2validator/test/v21/misc_tests.py
+++ b/stix2validator/test/v21/misc_tests.py
@@ -68,7 +68,7 @@ def test_validate_file_warning(caplog):
     assert results.is_valid
 
     print_results(results)
-    assert re.search("Custom property .+ should have a type that starts with 'x_'", caplog.text)
+    assert re.search("Custom property .+ should ", caplog.text)
 
 
 def test_validate_file_invalid_brace(caplog):
@@ -111,7 +111,7 @@ def test_validate_string_warning(caplog):
     assert results.is_valid
 
     print_results(results)
-    assert re.search("Custom property .+ should have a type that starts with 'x_'", caplog.text)
+    assert re.search("Custom property .+ should ", caplog.text)
 
 
 def test_validate_string_invalid_timestamp(caplog):
diff --git a/stix2validator/test/v21/observed_data_tests.py b/stix2validator/test/v21/observed_data_tests.py
index 6a42fd2..52fd8c1 100644
--- a/stix2validator/test/v21/observed_data_tests.py
+++ b/stix2validator/test/v21/observed_data_tests.py
@@ -70,10 +70,9 @@ class ObservedDataTestCases(ValidatorTest):
                 }
             }
         }
-        results = validate_parsed_json(observed_data, self.options)
-        self.assertTrue(len(results.errors) == 1)
+        self.assertFalseWithOptions(observed_data, disabled='extensions-use')
 
-        self.check_ignore(observed_data, 'observable-dictionary-keys')
+        self.check_ignore(observed_data, 'observable-dictionary-keys,extensions-use')
 
     def test_dict_key_length(self):
         observed_data = {
@@ -87,7 +86,7 @@ class ObservedDataTestCases(ValidatorTest):
             },
         }
         # STIX 2.1 removed dictionary key minimum length
-        self.assertTrueWithOptions(observed_data)
+        self.assertTrueWithOptions(observed_data, disabled='extensions-use')
 
     def test_vocab_account_type(self):
         observed_data = {
@@ -264,12 +263,21 @@ class ObservedDataTestCases(ValidatorTest):
         observed_data['extensions']['foobar-ext'] = {
             "foo": "bar"
         }
-        self.assertFalseWithOptions(observed_data)
+        self.assertFalseWithOptions(observed_data, disabled='extensions-use')
+        self.assertFalseWithOptions(observed_data, strict_properties=True, disabled='extensions-use')
+
+        self.check_ignore(observed_data,
+                          'custom-prefix,custom-prefix-lax,extensions-use')
+
+    def test_observable_object_extensions2(self):
+        observed_data = copy.deepcopy(self.valid_object)
+        observed_data['extensions']['x-foo-bar-ext'] = {
+            "foo": "bar"
+        }
         self.assertFalseWithOptions(observed_data)
         self.assertFalseWithOptions(observed_data, strict_properties=True)
 
-        self.check_ignore(observed_data,
-                          'custom-prefix,custom-prefix-lax')
+        self.check_ignore(observed_data, 'extensions-use')
 
     def test_observable_object_extensions_prefix_lax(self):
         observed_data = copy.deepcopy(self.valid_object)
@@ -277,74 +285,117 @@ class ObservedDataTestCases(ValidatorTest):
             "foo": "bar"
         }
         self.assertFalseWithOptions(observed_data,
-                                    disabled='custom-prefix')
+                                    disabled='custom-prefix,extensions-use')
 
         del observed_data['extensions']['foobar-ext']
         observed_data['extensions']['x-foobar'] = {
             "foo": "bar"
         }
         self.assertFalseWithOptions(observed_data,
-                                    disabled='custom-prefix')
+                                    disabled='custom-prefix,extensions-use')
 
         del observed_data['extensions']['x-foobar']
         observed_data['extensions']['x-foobar-ext'] = {
             "foo": "bar"
         }
-        self.assertFalseWithOptions(observed_data)
+        self.assertFalseWithOptions(observed_data, disabled='extensions-use')
         self.check_ignore(observed_data,
-                          'custom-prefix')
+                          'custom-prefix,extensions-use')
         self.assertFalseWithOptions(observed_data,
-                                    disabled='custom-prefix-lax')
+                                    disabled='custom-prefix-lax,extensions-use')
 
     def test_observable_object_custom_properties(self):
         observed_data = copy.deepcopy(self.valid_object)
         observed_data['foo'] = "bar"
-        self.assertFalseWithOptions(observed_data)
+        self.assertFalseWithOptions(observed_data, disabled='extensions-use')
 
         self.check_ignore(observed_data,
-                          'custom-prefix,custom-prefix-lax')
+                          'custom-prefix,custom-prefix-lax,extensions-use')
         self.assertFalseWithOptions(observed_data,
-                                    disabled='custom-prefix-lax')
+                                    disabled='custom-prefix-lax,extensions-use')
 
     def test_observable_object_custom_properties_lax(self):
         observed_data = copy.deepcopy(self.valid_object)
         observed_data['foo'] = "bar"
         self.assertFalseWithOptions(observed_data,
-                                    disabled='custom-prefix')
+                                    disabled='custom-prefix,extensions-use')
 
         del observed_data['foo']
         observed_data['x_foo'] = "bar"
         self.check_ignore(observed_data,
-                          'custom-prefix')
+                          'custom-prefix,extensions-use')
         self.assertFalseWithOptions(observed_data,
-                                    disabled='custom-prefix-lax')
+                                    disabled='custom-prefix-lax,extensions-use')
 
     def test_observable_object_custom_properties_strict(self):
         observed_data = copy.deepcopy(self.valid_object)
         observed_data['x_x_foo'] = "bar"
+        self.assertFalseWithOptions(observed_data, strict_properties=True, disabled='extensions-use')
+
+    def test_observable_object_custom_property_without_extension(self):
+        observed_data = copy.deepcopy(self.valid_object)
+        observed_data['x_foo_something'] = "some value"
+
+        self.assertFalseWithOptions(observed_data)
         self.assertFalseWithOptions(observed_data, strict_properties=True)
+        self.assertFalseWithOptions(observed_data, strict_properties=True, disabled='extensions-use')
+        self.assertTrueWithOptions(observed_data, disabled='extensions-use')
 
-    def test_observable_object_extension_custom_properties(self):
+    def test_observable_object_custom_property_with_extension(self):
         observed_data = copy.deepcopy(self.valid_object)
-        observed_data['extensions']["ntfs-ext"]['foo'] = "bar"
+        observed_data['x_foo_something'] = "some value"
+        observed_data['extensions'] = {
+            "extension-definition--d83fce45-ef58-4c6c-a3f4-1fbc32e98c6e": {
+                "extension_type": "property-extension",
+                "rank": 5,
+                "toxicity": 8
+            }
+        }
+
+        self.assertFalseWithOptions(observed_data)
+        self.assertFalseWithOptions(observed_data, strict_properties=True)
+        self.assertFalseWithOptions(observed_data, strict_properties=True, disabled='extensions-use')
+
+        del observed_data['x_foo_something']
+        self.assertTrueWithOptions(observed_data, strict_properties=True)
+
+    def test_observable_object_custom_toplevel_property_with_extension(self):
+        observed_data = copy.deepcopy(self.valid_object)
+        observed_data['x_foo_something'] = "some value"
+        observed_data['rank'] = 5
+        observed_data['toxicity'] = 8
+        observed_data['extensions'] = {
+            "extension-definition--a932fcc6-e032-176c-826f-cb970a5a1fff": {
+                "extension_type": "toplevel-property-extension",
+            }
+        }
+
         self.assertFalseWithOptions(observed_data)
+        self.assertFalseWithOptions(observed_data, strict_properties=True)
+        self.assertFalseWithOptions(observed_data, strict_properties=True, disabled='extensions-use')
 
-        self.check_ignore(observed_data, 'custom-prefix,custom-prefix-lax')
+        del observed_data['x_foo_something']
+        self.assertFalseWithOptions(observed_data)
+        self.assertFalseWithOptions(observed_data, strict_properties=True)
+        self.assertFalseWithOptions(observed_data, strict_properties=True, disabled='extensions-use')
+        self.assertFalseWithOptions(observed_data, disabled='extensions-use')
+        self.assertTrueWithOptions(observed_data, disabled='extensions-use,custom-prefix,custom-prefix-lax')
 
-    def test_observable_object_extension_custom_properties_lax(self):
+    def test_observable_object_extension_custom_properties(self):
         observed_data = copy.deepcopy(self.valid_object)
         observed_data['extensions']["ntfs-ext"]['foo'] = "bar"
-        self.assertFalseWithOptions(observed_data,
-                                    disabled='custom-prefix')
+        self.assertFalseWithOptions(observed_data)
+        self.assertFalseWithOptions(observed_data, strict_properties=True)
+        self.assertFalseWithOptions(observed_data, strict_properties=True, disabled='extensions-use')
+        self.assertTrueWithOptions(observed_data, disabled='extensions-use,custom-prefix,custom-prefix-lax')
 
         del observed_data['extensions']["ntfs-ext"]['foo']
         observed_data['extensions']["ntfs-ext"]['x_foo'] = "bar"
-        self.check_ignore(observed_data, 'custom-prefix')
-        self.assertFalseWithOptions(observed_data,
-                                    disabled='custom-prefix-lax')
-        self.assertFalseWithOptions(observed_data,
-                                    disabled='custom-prefix',
-                                    strict_properties=True)
+        self.assertTrueWithOptions(observed_data, disabled='extensions-use,custom-prefix')
+
+        del observed_data['extensions']["ntfs-ext"]['x_foo']
+        observed_data['extensions']["ntfs-ext"]['x_org_foo'] = "bar"
+        self.assertTrueWithOptions(observed_data, disabled='extensions-use')
 
     def test_observable_object_embedded_custom_properties(self):
         observed_data = {
@@ -356,29 +407,10 @@ class ObservedDataTestCases(ValidatorTest):
             }
         }
         self.assertFalseWithOptions(observed_data)
-
-        self.check_ignore(observed_data, 'custom-prefix,custom-prefix-lax')
-
-    def test_observable_object_embedded_custom_properties_lax(self):
-        observed_data = {
-            "type": "x509-certificate",
-            "id": "x509-certificate--ff1e0780-358c-4808-a8c7-d0fca4ef6ef4",
-            "x509_v3_extensions": {
-              "issuer_alternative_name": "Example Corp",
-              "foo": "bar"
-            }
-        }
-        self.assertFalseWithOptions(observed_data,
-                                    disabled='custom-prefix')
-
-        del observed_data['x509_v3_extensions']['foo']
-        observed_data['x509_v3_extensions']['x_foo'] = "bar"
-        self.check_ignore(observed_data, 'custom-prefix')
-        self.assertFalseWithOptions(observed_data,
-                                    disabled='custom-prefix-lax')
-        self.assertFalseWithOptions(observed_data,
-                                    disabled='custom-prefix',
-                                    strict_properties=True)
+        self.assertFalseWithOptions(observed_data, strict_properties=True)
+        self.assertFalseWithOptions(observed_data, strict_properties=True, disabled='extensions-use')
+        self.assertFalseWithOptions(observed_data, disabled='extensions-use')
+        self.assertTrueWithOptions(observed_data, disabled='extensions-use,custom-prefix,custom-prefix-lax')
 
     def test_observable_object_embedded_dict_custom_properties(self):
         observed_data = {
@@ -395,9 +427,10 @@ class ObservedDataTestCases(ValidatorTest):
             ]
         }
         self.assertFalseWithOptions(observed_data)
-
-        self.check_ignore(observed_data,
-                          'custom-prefix,custom-prefix-lax')
+        self.assertFalseWithOptions(observed_data, strict_properties=True)
+        self.assertFalseWithOptions(observed_data, strict_properties=True, disabled='extensions-use')
+        self.assertFalseWithOptions(observed_data, disabled='extensions-use')
+        self.assertTrueWithOptions(observed_data, disabled='extensions-use,custom-prefix,custom-prefix-lax')
 
     def test_observable_object_embedded_dict_custom_properties_lax(self):
         observed_data = {
@@ -414,15 +447,15 @@ class ObservedDataTestCases(ValidatorTest):
             ]
         }
         self.assertFalseWithOptions(observed_data,
-                                    disabled='custom-prefix')
+                                    disabled='extensions-use,custom-prefix')
 
         del observed_data['values'][0]['foo']
         observed_data['values'][0]['x_foo'] = "bar"
-        self.check_ignore(observed_data, 'custom-prefix')
+        self.check_ignore(observed_data, 'extensions-use,custom-prefix')
         self.assertFalseWithOptions(observed_data,
-                                    disabled='custom-prefix-lax')
+                                    disabled='extensions-use,custom-prefix-lax')
         self.assertFalseWithOptions(observed_data,
-                                    disabled='custom-prefix',
+                                    disabled='extensions-use,custom-prefix',
                                     strict_properties=True)
 
     def test_observable_object_extension_embedded_custom_properties(self):
@@ -437,9 +470,17 @@ class ObservedDataTestCases(ValidatorTest):
               ]
         }
         self.assertFalseWithOptions(observed_data)
+        self.assertFalseWithOptions(observed_data, strict_properties=True)
+        self.assertFalseWithOptions(observed_data, strict_properties=True, disabled='extensions-use')
+        self.assertTrueWithOptions(observed_data, disabled='extensions-use,custom-prefix,custom-prefix-lax')
 
-        self.check_ignore(observed_data,
-                          'custom-prefix,custom-prefix-lax')
+        del observed_data['extensions']['ntfs-ext']['alternate_data_streams'][0]['foo']
+        observed_data['extensions']['ntfs-ext']['alternate_data_streams'][0]['x_foo'] = 'bar'
+        self.assertTrueWithOptions(observed_data, disabled='extensions-use,custom-prefix')
+
+        del observed_data['extensions']['ntfs-ext']['alternate_data_streams'][0]['x_foo']
+        observed_data['extensions']['ntfs-ext']['alternate_data_streams'][0]['x_org_foo'] = 'bar'
+        self.assertTrueWithOptions(observed_data, disabled='extensions-use')
 
     def test_observable_object_extension_embedded_custom_properties_lax(self):
         observed_data = copy.deepcopy(self.valid_object)
@@ -453,15 +494,15 @@ class ObservedDataTestCases(ValidatorTest):
               ]
         }
         self.assertFalseWithOptions(observed_data,
-                                    disabled='custom-prefix')
+                                    disabled='extensions-use,custom-prefix')
         del observed_data['extensions']['ntfs-ext']['alternate_data_streams'][0]['foo']
         observed_data['extensions']['ntfs-ext']['alternate_data_streams'][0]['x_foo'] = "bar"
         self.check_ignore(observed_data,
-                          'custom-prefix')
+                          'extensions-use,custom-prefix')
         self.assertFalseWithOptions(observed_data,
-                                    disabled='custom-prefix-lax')
+                                    disabled='extensions-use,custom-prefix-lax')
         self.assertFalseWithOptions(observed_data,
-                                    disabled='custom-prefix',
+                                    disabled='extensions-use,custom-prefix',
                                     strict_properties=True)
 
     def test_observable_object_extensions_string(self):
@@ -469,13 +510,15 @@ class ObservedDataTestCases(ValidatorTest):
         observed_data['extensions'] = 'example:Object-f39f745f-d36b-4dca-9a3e-16bb1c5516f0'
         self.assertFalseWithOptions(observed_data)
 
-    def test_observable_object_reserved_property(self):
+    def test_observable_object_reserved_type(self):
         observed_data = copy.deepcopy(self.valid_object)
         observed_data['type'] = 'action'
         self.assertFalseWithOptions(observed_data)
 
-        observed_data['type'] = 'file'
+    def test_observable_object_reserved_property(self):
+        observed_data = copy.deepcopy(self.valid_object)
         observed_data['action'] = True
+        print(observed_data)
         self.assertFalseWithOptions(observed_data)
 
     def test_windows_registry_key_truncated(self):
@@ -722,14 +765,22 @@ class ObservedDataTestCases(ValidatorTest):
         }
         self.assertFalseWithOptions(observed_data)
 
-    def test_additional_schemas_custom_extension(self):
+    def test_additional_schemas_custom_extension_old_invalid_method(self):
         observed_data = copy.deepcopy(self.valid_object)
         observed_data['extensions'] = {'x-example-com-foobar-ext': {
+            "foo_value": "something",
+        }}
+        self.assertFalseWithOptions(observed_data, schema_dir=self.custom_schemas)
+
+    def test_additional_schemas_custom_extension(self):
+        observed_data = copy.deepcopy(self.valid_object)
+        observed_data['extensions'] = {'extension-definition--d83fce45-ef58-4c6c-a3f4-1fbc32e98c6e': {
+            "extension_type": "property-extension",
             "bar_value": "something",
         }}
         self.assertFalseWithOptions(observed_data, schema_dir=self.custom_schemas)
 
-        observed_data['extensions']['x-example-com-foobar-ext']['foo_value'] = 'something else'
+        observed_data['extensions']['extension-definition--d83fce45-ef58-4c6c-a3f4-1fbc32e98c6e']['foo_value'] = 'something else'
         self.assertTrueWithOptions(observed_data, schema_dir=self.custom_schemas)
 
     def test_deprecated_objects_property(self):
diff --git a/stix2validator/test/v21/test_schemas/file.json b/stix2validator/test/v21/test_schemas/file.json
index 0cca60e..039e62f 100644
--- a/stix2validator/test/v21/test_schemas/file.json
+++ b/stix2validator/test/v21/test_schemas/file.json
@@ -2,9 +2,13 @@
     "properties": {
         "extensions": {
             "patternProperties": {
-                "^x-example-com-foobar-ext$": {
+                "^extension-definition--d83fce45-ef58-4c6c-a3f4-1fbc32e98c6e$": {
                     "type": "object",
                     "properties": {
+                        "extension_type": {
+                           "type": "string",
+                            "enum": ["property-extension"]
+                        },
                         "foo_value": {
                             "type": "string"
                         },
diff --git a/stix2validator/test/v21/test_schemas/x-new-observable.json b/stix2validator/test/v21/test_schemas/x-new-observable.json
index 4f2b36e..574b9a3 100644
--- a/stix2validator/test/v21/test_schemas/x-new-observable.json
+++ b/stix2validator/test/v21/test_schemas/x-new-observable.json
@@ -2,6 +2,19 @@
     "properties": {
         "foo": {
             "type": "string"
+        },
+        "extensions": {
+            "type": "object",
+            "properties": {
+                "extension-definition--3518f3da-8f95-4558-a1a7-706f759c34a1": {
+                    "type": "object",
+                    "properties": {
+                        "extension_type": {
+                            "enum": ["new-sco"]
+                        }
+                    }
+                }
+            }
         }
     }
 }
diff --git a/stix2validator/test/v21/test_schemas/x-new-type.json b/stix2validator/test/v21/test_schemas/x-new-type.json
index df609c7..08fa82f 100644
--- a/stix2validator/test/v21/test_schemas/x-new-type.json
+++ b/stix2validator/test/v21/test_schemas/x-new-type.json
@@ -5,6 +5,19 @@
         },
         "property2": {
             "type": "integer"
+        },
+        "extensions": {
+            "type": "object",
+            "properties": {
+                "extension-definition--ba73205e-96bb-40d3-8168-0056d862b229": {
+                    "type": "object",
+                    "properties": {
+                        "extension_type": {
+                            "enum": ["new-sdo"]
+                        }
+                    }
+                }
+            }
         }
     }
 }