Repository: oasis-open/cti-stix-validator

Issue:
Changes to Malware Analysis object
see https://github.com/oasis-tcs/cti-stix2/issues/213 and https://github.com/oasis-tcs/cti-stix2/issues/210

Discussion:

Patch:
diff --git a/docs/best-practices.rst b/docs/best-practices.rst
index d7a73d2..0bca34f 100644
--- a/docs/best-practices.rst
+++ b/docs/best-practices.rst
@@ -320,10 +320,6 @@ Optional Checks - STIX 2.1
 |        |                             |                                        | '<hash_type>', which is longer than 30 |
 |        |                             |                                        | characters.                            |
 +--------+-----------------------------+----------------------------------------+----------------------------------------+
-|  151   | os-execution-envs           | os_execution_envs entries are CPE      | Provided os execution environment      |
-|        |                             | 2.3 compliant                          | <os_env> is not CPE v2.3 compliant.    |
-|        |                             |                                        |                                        |
-+--------+-----------------------------+----------------------------------------+----------------------------------------+
 |   2    | approved-values             | all 2xx checks are run. Specifically:  |                                        |
 +--------+-----------------------------+----------------------------------------+----------------------------------------+
 |  201   | marking-definition-type     | marking definitions use a valid        | Marking definition 'definition_type'   |
@@ -391,10 +387,6 @@ Optional Checks - STIX 2.1
 |  223   | region                      | certain property values are from the   | '<property>' contains a value not in   |
 |        |                             | region vocabulary                      | the region-ov vocabulary               |
 +--------+-----------------------------+----------------------------------------+----------------------------------------+
-|  224   | course-of-action-type       | certain property values are from the   | '<property>' contains a value          |
-|        |                             | course-of-action-type vocabulary       | not in the course-of-action-type-ov    |
-|        |                             |                                        | vocabulary.                            |
-+--------+-----------------------------+----------------------------------------+----------------------------------------+
 |  225   | grouping-context            | certain property values are from the   | '<property>' contains a value not      |
 |        |                             | grouping-context vocabulary            | in the grouping-context-ov vocabulary  |
 +--------+-----------------------------+----------------------------------------+----------------------------------------+
@@ -414,6 +406,9 @@ Optional Checks - STIX 2.1
 |        |                             | processor-architecture vocabulary      | the                                    |
 |        |                             |                                        | processor-architecture-ov vocabulary   |
 +--------+-----------------------------+----------------------------------------+----------------------------------------+
+|  231   | malware-result              | certain property values are from the   | '<property>' contains a value not in   |
+|        |                             | malware-result vocabulary              | the malware-result-ov vocabulary       |
++--------+-----------------------------+----------------------------------------+----------------------------------------+
 |  241   | hash-algo                   | certain property values are from the   | Object '<identifier>' has a 'hashes'   |
 |        |                             | hash-algo vocabulary                   | dictionary with a hash of type         |
 |        |                             |                                        | '<hash_type>', which is not a value in |
diff --git a/stix2validator/schemas-2.1 b/stix2validator/schemas-2.1
index 5d0d5a2..f7c4fbf 160000
--- a/stix2validator/schemas-2.1
+++ b/stix2validator/schemas-2.1
@@ -1,1 +1,1 @@
-Subproject commit 5d0d5a2ee364215acb1f15db0016f3c1c17360bc
+Subproject commit f7c4fbfa2e4430fb44a4a6125ddf3c846e54001e
diff --git a/stix2validator/util.py b/stix2validator/util.py
index 29654cb..828790c 100644
--- a/stix2validator/util.py
+++ b/stix2validator/util.py
@@ -52,8 +52,6 @@ https://stix2-validator.readthedocs.io/en/latest/best-practices.html.
 |      |     format                  | follows the correct format             |
 | 150  | hash-length                 | keys in 'hashes'-type properties are   |
 |      |                             | not too long                           |
-| 151  | os-execution-envs           | os_execution_envs entries are CPE      |
-|      |                             | 2.3 compliant                          |
 |      |                             |                                        |
 |  2   | approved-values             | all 2xx checks are run                 |
 | 201  | marking-definition-type     | marking definitions use a valid        |
@@ -88,8 +86,6 @@ https://stix2-validator.readthedocs.io/en/latest/best-practices.html.
 |      |                             | tool_types vocabulary                  |
 | 223  | region                      | certain property values are from the   |
 |      |                             | region vocabulary                      |
-| 224  | course-of-action-tyoe       | certain property values are from the   |
-|      |                             | course-of-action vocabulary            |
 | 225  | grouping-context            | certain property values are from the   |
 |      |                             | grouping-context vocabulary            |
 | 226  | implementation-languages    | certain property values are from the   |
@@ -100,6 +96,8 @@ https://stix2-validator.readthedocs.io/en/latest/best-practices.html.
 |      |                             | malware-capabilities vocabulary        |
 | 230  | processor-architecture      | certain property values are from the   |
 |      |                             | processor-architecture vocabulary      |
+| 231  | malware-result              | certain property values are from the   |
+|      |                             | malware-result vocabulary              |
 | 241  | hash-algo                   | certain property values are from the   |
 |      |                             | hash-algo vocabulary                   |
 | 243  | windows-pebinary-type       | certain property values are from the   |
diff --git a/stix2validator/v20/enums.py b/stix2validator/v20/enums.py
index 5ac40a0..3f15629 100644
--- a/stix2validator/v20/enums.py
+++ b/stix2validator/v20/enums.py
@@ -186,8 +186,8 @@ WINDOWS_PEBINARY_TYPE_OV = [
 ]
 ACCOUNT_TYPE_OV = [
     "unix",
-    "windows local",
-    "windows domain",
+    "windows-local",
+    "windows-domain",
     "ldap",
     "tacacs",
     "radius",
@@ -195,8 +195,7 @@ ACCOUNT_TYPE_OV = [
     "openid",
     "facebook",
     "skype",
-    "twitter",
-    "kavi"
+    "twitter"
 ]
 
 
diff --git a/stix2validator/v20/musts.py b/stix2validator/v20/musts.py
index 60f9708..bcc2ffc 100644
--- a/stix2validator/v20/musts.py
+++ b/stix2validator/v20/musts.py
@@ -3,6 +3,7 @@
 
 import re
 
+from cpe import CPE
 from dateutil import parser
 from six import string_types
 from stix2patterns.v20.pattern import Pattern
@@ -387,6 +388,23 @@ def patterns(instance, options):
                                    "should start with 'x_'" % prop, instance['id'])
 
 
+@cyber_observable_check("2.0")
+def cpe_check(instance):
+    """Checks to see if provided cpe is a valid CPE v2.3 entry
+    """
+    for key, obj in instance['objects'].items():
+        if 'cpe' not in obj:
+            continue
+        try:
+            CPE(obj['cpe'], CPE.VERSION_2_3)
+        except NotImplementedError:
+            yield JSONError(
+                    "Provided CPE value of object '%s' ('%s') is not CPE v2.3"
+                    "compliant."
+                    % (key, obj['cpe']), instance['id'],
+            )
+
+
 def list_musts(options):
     """Construct the list of 'MUST' validators to be run by the validator.
     """
@@ -400,7 +418,8 @@ def list_musts(options):
         artifact_mime_type,
         character_set,
         software_language,
-        patterns
+        patterns,
+        cpe_check,
     ]
 
     return validator_list
diff --git a/stix2validator/v20/shoulds.py b/stix2validator/v20/shoulds.py
index 76b9168..7ba1a95 100644
--- a/stix2validator/v20/shoulds.py
+++ b/stix2validator/v20/shoulds.py
@@ -5,11 +5,11 @@ To add a new check:
     - define a new function
     - add the function to CHECKS
     - add the function to list_shoulds()
-- in utils.py:
+- in enums.py:
     - add the check code and name to CHECK_CODES
-- in scripts/stix2_validator:
+- in util.py:
     - add the check code and name to table
-- in README.rst:
+- in docs/best-practices:
     - add the check code and name to table
 """
 
@@ -281,8 +281,8 @@ def relationships_strict(instance):
 
     r_type = instance['relationship_type']
     try:
-        r_source = re.search(r"(.+)\-\-", instance['source_ref']).group(1)
-        r_target = re.search(r"(.+)\-\-", instance['target_ref']).group(1)
+        r_source = re.search(r"(.+)--", instance['source_ref']).group(1)
+        r_target = re.search(r"(.+)--", instance['target_ref']).group(1)
     except (AttributeError, TypeError):
         # Schemas already catch errors of these properties not being strings or
         # not containing the string '--'.
@@ -642,7 +642,7 @@ def custom_observable_properties_prefix_strict(instance):
                                         'custom-prefix')
 
         # Check object extensions' properties
-        if (type_ in enums.OBSERVABLE_EXTENSIONS and 'extensions' in obj):
+        if type_ in enums.OBSERVABLE_EXTENSIONS and 'extensions' in obj:
             for ext_key in obj['extensions']:
 
                 if ext_key in enums.OBSERVABLE_EXTENSIONS[type_]:
@@ -720,7 +720,7 @@ def custom_observable_properties_prefix_lax(instance):
                                         'custom-prefix-lax')
 
         # Check object extensions' properties
-        if (type_ in enums.OBSERVABLE_EXTENSIONS and 'extensions' in obj):
+        if type_ in enums.OBSERVABLE_EXTENSIONS and 'extensions' in obj:
             for ext_key in obj['extensions']:
 
                 if ext_key in enums.OBSERVABLE_EXTENSIONS[type_]:
@@ -771,7 +771,7 @@ def mime_type(instance):
     mime_pattern = re.compile(r'^(application|audio|font|image|message|model'
                               '|multipart|text|video)/[a-zA-Z0-9.+_-]+')
     for key, obj in instance['objects'].items():
-        if ('type' in obj and obj['type'] == 'file' and 'mime_type' in obj):
+        if 'type' in obj and obj['type'] == 'file' and 'mime_type' in obj:
             if enums.media_types():
                 if obj['mime_type'] not in enums.media_types():
                     yield JSONError("The 'mime_type' property of object '%s' "
@@ -857,7 +857,7 @@ def http_request_headers(instance):
     several common non-standard request fields listed elsewhere.
     """
     for key, obj in instance['objects'].items():
-        if ('type' in obj and obj['type'] == 'network-traffic'):
+        if 'type' in obj and obj['type'] == 'network-traffic':
             try:
                 headers = obj['extensions']['http-request-ext']['request_header']
             except KeyError:
@@ -878,7 +878,7 @@ def socket_options(instance):
     network-traffic objects are only valid socket options (SO_*).
     """
     for key, obj in instance['objects'].items():
-        if ('type' in obj and obj['type'] == 'network-traffic'):
+        if 'type' in obj and obj['type'] == 'network-traffic':
             try:
                 options = obj['extensions']['socket-ext']['options']
             except KeyError:
@@ -899,7 +899,7 @@ def pdf_doc_info(instance):
     Dictionary Keys.
     """
     for key, obj in instance['objects'].items():
-        if ('type' in obj and obj['type'] == 'file'):
+        if 'type' in obj and obj['type'] == 'file':
             try:
                 did = obj['extensions']['pdf-ext']['document_info_dict']
             except KeyError:
@@ -947,7 +947,7 @@ def hash_length(instance):
                 pass
             else:
                 for h in hashes:
-                    if (len(h) > 30):
+                    if len(h) > 30:
                         yield JSONError("Object '%s' has a 'hashes' dictionary"
                                         " with a hash of type '%s', which is "
                                         "longer than 30 characters."
@@ -962,7 +962,7 @@ def hash_length(instance):
                     if 'hashes' not in datastream:
                         continue
                     for h in datastream['hashes']:
-                        if (len(h) > 30):
+                        if len(h) > 30:
                             yield JSONError("Object '%s' has an NTFS extension"
                                             " with an alternate data stream that has a"
                                             " 'hashes' dictionary with a hash of type "
@@ -976,7 +976,7 @@ def hash_length(instance):
                 pass
             else:
                 for h in head_hashes:
-                    if (len(h) > 30):
+                    if len(h) > 30:
                         yield JSONError("Object '%s' has a Windows PE Binary "
                                         "File extension with a file header hash of "
                                         "'%s', which is longer than 30 "
@@ -989,7 +989,7 @@ def hash_length(instance):
                 pass
             else:
                 for h in hashes:
-                    if (len(h) > 30):
+                    if len(h) > 30:
                         yield JSONError("Object '%s' has a Windows PE Binary "
                                         "File extension with an optional header that "
                                         "has a hash of '%s', which is longer "
@@ -1005,7 +1005,7 @@ def hash_length(instance):
                     if 'hashes' not in s:
                         continue
                     for h in s['hashes']:
-                        if (len(h) > 30):
+                        if len(h) > 30:
                             yield JSONError("Object '%s' has a Windows PE "
                                             "Binary File extension with a section that"
                                             " has a hash of '%s', which is "
@@ -1019,7 +1019,7 @@ def hash_length(instance):
                 pass
             else:
                 for h in hashes:
-                    if (len(h) > 30):
+                    if len(h) > 30:
                         yield JSONError("Object '%s' has a 'hashes' dictionary"
                                         " with a hash of type '%s', which is "
                                         "longer than 30 characters."
@@ -1032,7 +1032,7 @@ def extref_hashes(instance):
             if 'url' in extref and 'hashes' not in extref:
                 src = extref['source_name'] if 'source_name' in extref else ''
                 return JSONError("External reference '%s' has a URL but no hash."
-                                 % (src), instance['id'], 'extref-hashes')
+                                 % src, instance['id'], 'extref-hashes')
 
 
 def enforce_relationship_refs(instance):
diff --git a/stix2validator/v21/enums.py b/stix2validator/v21/enums.py
index 5fe7172..3e04308 100644
--- a/stix2validator/v21/enums.py
+++ b/stix2validator/v21/enums.py
@@ -26,12 +26,6 @@ ATTACK_RESOURCE_LEVEL_OV = [
     "organization",
     "government",
 ]
-COURSE_OF_ACTION_TYPE_OV = [
-    "textual:text/plain",
-    "textual:text/html",
-    "textual:text/md",
-    "textual:pdf",
-]
 GROUPING_CONTEXT_OV = [
     "suspicious-activity",
     "malware-analysis",
@@ -43,7 +37,7 @@ IDENTITY_CLASS_OV = [
     "system",
     "organization",
     "class",
-    "unspecified",
+    "unknown",
 ]
 IMPLEMENTATION_LANGUAGES_OV = [
     "applescript",
@@ -81,20 +75,27 @@ INDUSTRY_SECTOR_OV = [
     "agriculture",
     "aerospace",
     "automotive",
+    "chemical",
+    "commercial",
     "communications",
     "construction",
-    "defence",
+    "defense",
     "education",
     "energy",
     "entertainment",
     "financial-services",
-    "government-national",
-    "government-regional",
+    "government",
+    "emergency-services",
     "government-local",
+    "government-national",
     "government-public-services",
+    "government-regional",
     "healthcare",
     "hospitality-leisure",
     "infrastructure",
+    "dams",
+    "nuclear",
+    "water",
     "insurance",
     "manufacturing",
     "mining",
@@ -117,9 +118,9 @@ INFRASTRUCTURE_TYPE_OV = [
     "phishing",
     "reconnaissance",
     "staging",
-    "undefined",
+    "unknown",
 ]
-MALWARE_AV_RESULT_OV = [
+MALWARE_RESULT_OV = [
     "malicious",
     "suspicious",
     "benign",
@@ -309,9 +310,7 @@ WINDOWS_PEBINARY_TYPE_OV = [
 ]
 ACCOUNT_TYPE_OV = [
     "unix",
-    "windows local",
     "windows-local",
-    "windows domain",
     "windows-domain",
     "ldap",
     "tacacs",
@@ -344,9 +343,6 @@ ATTACK_RESOURCE_LEVEL_USES = {
     "intrusion-set": ["resource_level"],
     "threat-actor": ["resource_level"],
 }
-COURSE_OF_ACTION_TYPE_USES = {
-    "course-of-action": ["action_type"],
-}
 GROUPING_CONTEXT_USES = {
     "grouping": ["context"],
 }
@@ -379,6 +375,9 @@ INDUSTRY_SECTOR_USES = {
 MALWARE_CAPABILITIES_USES = {
     "malware": ["capabilities"],
 }
+MALWARE_RESULT_USES = {
+    "malware-analysis": ["result"],
+}
 REGION_USES = {
     "location": ["region"],
 }
@@ -527,11 +526,7 @@ PROPERTIES = {
         'granular_markings',
         'name',
         'description',
-        'action_type',
-        'os_execution_envs',
-        'action_bin',
-        'action_type',
-        'action_reference',
+        'action'
     ],
     "grouping": [
         'type',
@@ -691,7 +686,7 @@ PROPERTIES = {
         'kill_chain_phases',
         'first_seen',
         'last_seen',
-        'os_execution_envs',
+        'operating_system_refs',
         'architecture_execution_envs',
         'implementation_languages',
         'capabilities',
@@ -713,6 +708,9 @@ PROPERTIES = {
         'granular_markings',
         'product',
         'version',
+        'host_vm_ref',
+        'operating_system_ref',
+        'installed_software_ref',
         'configuration_version',
         'modules',
         'analysis_engine_version',
@@ -720,11 +718,10 @@ PROPERTIES = {
         'submitted',
         'analysis_started',
         'analysis_ended',
-        'av_result',
-        'host_vm_ref',
-        'operating_system_ref',
-        'installed_software_refs',
+        'result_name',
+        'result',
         'analysis_sco_refs',
+        'sample_ref',
     ],
     "note": [
         'type',
@@ -1163,6 +1160,7 @@ OBSERVABLE_PROPERTIES = {
         'extensions',
         'name',
         'cpe',
+        'swid',
         'languages',
         'vendor',
         'version',
@@ -1803,7 +1801,7 @@ RELATIONSHIPS = {
     },
     'malware-analysis': {
         'characterizes': 'malware',
-        'av-analysis-of': 'malware',
+        'analysis-of': 'malware',
         'static-analysis-of': 'malware',
         'dynamic-analysis-of': 'malware',
     },
@@ -2000,9 +1998,6 @@ VOCAB_PROPERTIES = {
     "artifact": [
         'hashes'
     ],
-    "course-of-action": [
-        'action_type',
-    ],
     "file": [
         "hashes",
     ],
@@ -2033,6 +2028,9 @@ VOCAB_PROPERTIES = {
         'implementation_languages',
         'capabilities',
     ],
+    "malware-analysis": [
+        'result',
+    ],
     "ntfs-ext": [
         'hashes',
     ],
@@ -2066,13 +2064,6 @@ VOCAB_PROPERTIES = {
 }
 
 DEPRECATED_PROPERTIES = {
-    'domain-name': ['resolves_to_refs'],
-    'ipv4-addr': [
-        'resolves_to_refs',
-        'belongs_to_refs'],
-    'ipv6-addr': [
-        'resolves_to_refs'
-        'belongs_to_refs'],
     'observed-data': ['objects'],
 }
 
@@ -2090,7 +2081,6 @@ CHECK_CODES = {
     '143': 'malware-analysis-product',
     '149': 'windows-process-priority-format',
     '150': 'hash-length',
-    '151': 'os-execution-envs',
     '2': 'approved-values',
     '201': 'marking-definition-type',
     '202': 'relationship-types',
@@ -2108,12 +2098,12 @@ CHECK_CODES = {
     '221': 'threat-actor-sophistication',
     '222': 'tool-types',
     '223': 'region',
-    '224': 'course-of-action-type',
     '225': 'grouping-context',
     '226': 'implementation-languages',
     '227': 'infrastructure-types',
     '228': 'malware-capabilities',
     '230': 'processor-architecture',
+    '231': 'malware-result',
     '241': 'hash-algo',
     '243': 'windows-pebinary-type',
     '244': 'account-type',
diff --git a/stix2validator/v21/musts.py b/stix2validator/v21/musts.py
index 647d212..643966a 100644
--- a/stix2validator/v21/musts.py
+++ b/stix2validator/v21/musts.py
@@ -4,6 +4,7 @@ import operator
 import re
 import uuid
 
+from cpe import CPE
 from dateutil import parser
 from six import string_types
 from stix2patterns.v21.pattern import Pattern
@@ -408,15 +409,11 @@ def patterns(instance, options):
     """Ensure that the syntax of the pattern of an indicator is valid, and that
     objects and properties referenced by the pattern are valid.
     """
-    if instance['type'] != 'indicator' or 'pattern' not in instance:
-        return
-
-    if instance.get('pattern_type', '') != 'stix':
+    if (instance['type'] != 'indicator' or instance.get('pattern_type', '') != 'stix' or
+            isinstance(instance.get('pattern', ''), string_types) is False):
         return
 
     pattern = instance['pattern']
-    if not isinstance(pattern, string_types):
-        return  # This error already caught by schemas
     if 'pattern_version' in instance:
         pattern_version = instance['pattern_version']
     elif 'spec_version' in instance:
@@ -478,6 +475,20 @@ def patterns(instance, options):
                                    "should start with 'x_'" % prop, instance['id'])
 
 
+def cpe_check(instance):
+    """Checks to see if provided cpe is a valid CPE v2.3 entry
+    """
+    if 'cpe' not in instance:
+        return
+    try:
+        CPE(instance['cpe'], CPE.VERSION_2_3)
+    except NotImplementedError:
+        yield JSONError(
+                "Provided CPE value '%s' is not CPE v2.3 compliant." %
+                instance['cpe'], instance['id'],
+        )
+
+
 def language_contents(instance):
     """Ensure keys in Language Content's 'contents' dictionary are valid
     language codes, and that the keys in the sub-dictionaries match the rules
@@ -547,9 +558,10 @@ def list_musts(options):
         language,
         software_language,
         patterns,
+        cpe_check,
         language_contents,
         uuid_version_check,
-        process
+        process,
     ]
 
     return validator_list
diff --git a/stix2validator/v21/shoulds.py b/stix2validator/v21/shoulds.py
index 0b20b26..c6dafc8 100644
--- a/stix2validator/v21/shoulds.py
+++ b/stix2validator/v21/shoulds.py
@@ -5,8 +5,10 @@ To add a new check:
     - define a new function
     - add the function to CHECKS
     - add the function to list_shoulds()
-- in utils.py:
+- in enums.py:
     - add the check code and name to CHECK_CODES
+- in util.py:
+    - add the check code and name to table
 - in docs/best-practices:
     - add the check code and name to table
 """
@@ -16,7 +18,6 @@ from itertools import chain
 import re
 import uuid
 
-from cpe import CPE
 from six import string_types
 from stix2patterns.v21.pattern import Pattern
 
@@ -165,20 +166,6 @@ def uuid_check(instance):
                         % instance['id'], instance['id'], 'uuid-check')
 
 
-def os_execution_envs_check(instance):
-    """Checks to see if provided os execution env is a valid CPE v2.3 entry
-    """
-    if 'os_execution_envs' not in instance:
-        return
-    for os_env in instance['os_execution_envs']:
-        try:
-            CPE(os_env, CPE.VERSION_2_3)
-        except NotImplementedError:
-            yield JSONError("Provided os execution environment %s is not"
-                            " CPE v2.3 compliant." % os_env, instance['id'],
-                            'os-execution-envs')
-
-
 def open_vocab_values(instance):
     """Ensure that the values of all properties which use open vocabularies are
     in lowercase and use hyphens instead of spaces or underscores as word
@@ -269,11 +256,6 @@ def vocab_attack_resource_level(instance):
                        'attack-resource-level')
 
 
-def vocab_course_of_action_type(instance):
-    return check_vocab(instance, "COURSE_OF_ACTION_TYPE",
-                       'course-of-action-type')
-
-
 def vocab_grouping_context(instance):
     return check_vocab(instance, "GROUPING_CONTEXT",
                        'grouping-context')
@@ -309,6 +291,11 @@ def vocab_malware_types(instance):
                        'malware-types')
 
 
+def vocab_malware_result(instance):
+    return check_vocab(instance, "MALWARE_RESULT",
+                       'malware-result')
+
+
 def vocab_malware_capabilities(instance):
     return check_vocab(instance, "MALWARE_CAPABILITIES",
                        'malware-capabilities')
@@ -383,8 +370,8 @@ def relationships_strict(instance):
 
     r_type = instance['relationship_type']
     try:
-        r_source = re.search(r"(.+)\-\-", instance['source_ref']).group(1)
-        r_target = re.search(r"(.+)\-\-", instance['target_ref']).group(1)
+        r_source = re.search(r"(.+)--", instance['source_ref']).group(1)
+        r_target = re.search(r"(.+)--", instance['target_ref']).group(1)
     except (AttributeError, TypeError):
         # Schemas already catch errors of these properties not being strings or
         # not containing the string '--'.
@@ -699,7 +686,7 @@ def custom_observable_properties_prefix_strict(instance):
                                     'custom-prefix')
 
     # Check object extensions' properties
-    if (type_ in enums.OBSERVABLE_EXTENSIONS and 'extensions' in instance):
+    if type_ in enums.OBSERVABLE_EXTENSIONS and 'extensions' in instance:
         for ext_key in instance['extensions']:
 
             if ext_key in enums.OBSERVABLE_EXTENSIONS[type_]:
@@ -776,7 +763,7 @@ def custom_observable_properties_prefix_lax(instance):
                                     'custom-prefix-lax')
 
     # Check object extensions' properties
-    if (type_ in enums.OBSERVABLE_EXTENSIONS and 'extensions' in instance):
+    if type_ in enums.OBSERVABLE_EXTENSIONS and 'extensions' in instance:
         for ext_key in instance['extensions']:
 
             if ext_key in enums.OBSERVABLE_EXTENSIONS[type_]:
@@ -825,7 +812,7 @@ def mime_type(instance):
     """
     mime_pattern = re.compile(r'^(application|audio|font|image|message|model'
                               '|multipart|text|video)/[a-zA-Z0-9.+_-]+')
-    if ('type' in instance and instance['type'] == 'file' and 'mime_type' in instance):
+    if 'type' in instance and instance['type'] == 'file' and 'mime_type' in instance:
         if enums.media_types():
             if instance['mime_type'] not in enums.media_types():
                 yield JSONError("The 'mime_type' property of object '%s' "
@@ -908,7 +895,7 @@ def http_request_headers(instance):
     not differentiate between request and response headers, and leaves out
     several common non-standard request fields listed elsewhere.
     """
-    if ('type' in instance and instance['type'] == 'network-traffic'):
+    if 'type' in instance and instance['type'] == 'network-traffic':
         try:
             headers = instance['extensions']['http-request-ext']['request_header']
         except KeyError:
@@ -928,7 +915,7 @@ def socket_options(instance):
     """Ensure the keys of the 'options' property of the socket-ext extension of
     network-traffic objects are only valid socket options (SO_*).
     """
-    if ('type' in instance and instance['type'] == 'network-traffic'):
+    if 'type' in instance and instance['type'] == 'network-traffic':
         try:
             options = instance['extensions']['socket-ext']['options']
         except KeyError:
@@ -948,7 +935,7 @@ def pdf_doc_info(instance):
     extension of file objects are only valid PDF Document Information
     Dictionary Keys.
     """
-    if ('type' in instance and instance['type'] == 'file'):
+    if 'type' in instance and instance['type'] == 'file':
         try:
             did = instance['extensions']['pdf-ext']['document_info_dict']
         except KeyError:
@@ -1019,7 +1006,7 @@ def hash_length(instance):
             return
         else:
             for h in hashes:
-                if (len(h) > 30):
+                if len(h) > 30:
                     return JSONError("Object '%s' has a 'hashes' dictionary"
                                      " with a hash of type '%s', which is "
                                      "longer than 30 characters."
@@ -1034,7 +1021,7 @@ def hash_length(instance):
                 if 'hashes' not in datastream:
                     return
                 for h in datastream['hashes']:
-                    if (len(h) > 30):
+                    if len(h) > 30:
                         return JSONError("Object '%s' has an NTFS extension"
                                          " with an alternate data stream that has a"
                                          " 'hashes' dictionary with a hash of type "
@@ -1048,7 +1035,7 @@ def hash_length(instance):
             return
         else:
             for h in head_hashes:
-                if (len(h) > 30):
+                if len(h) > 30:
                     return JSONError("Object '%s' has a Windows PE Binary "
                                      "File extension with a file header hash of "
                                      "'%s', which is longer than 30 "
@@ -1061,7 +1048,7 @@ def hash_length(instance):
             return
         else:
             for h in hashes:
-                if (len(h) > 30):
+                if len(h) > 30:
                     return JSONError("Object '%s' has a Windows PE Binary "
                                      "File extension with an optional header that "
                                      "has a hash of '%s', which is longer "
@@ -1077,7 +1064,7 @@ def hash_length(instance):
                 if 'hashes' not in s:
                     return
                 for h in s['hashes']:
-                    if (len(h) > 30):
+                    if len(h) > 30:
                         return JSONError("Object '%s' has a Windows PE "
                                          "Binary File extension with a section that"
                                          " has a hash of '%s', which is "
@@ -1091,7 +1078,7 @@ def hash_length(instance):
             return
         else:
             for h in hashes:
-                if (len(h) > 30):
+                if len(h) > 30:
                     return JSONError("Object '%s' has a 'hashes' dictionary"
                                      " with a hash of type '%s', which is "
                                      "longer than 30 characters."
@@ -1104,7 +1091,7 @@ def extref_hashes(instance):
             if 'url' in extref and 'hashes' not in extref:
                 src = extref['source_name'] if 'source_name' in extref else ''
                 return JSONError("External reference '%s' has a URL but no hash."
-                                 % (src), instance['id'], 'extref-hashes')
+                                 % src, instance['id'], 'extref-hashes')
 
 
 def duplicate_ids(instance):
@@ -1142,15 +1129,15 @@ def types_strict(instance):
                                 "specification."
                                 % (key, obj['type']), instance['id'])
 
-    if instance['type'] == 'indicator' and 'pattern' in instance:
+    if (instance['type'] == 'indicator' and instance.get('pattern_type', '') == 'stix' and
+            isinstance(instance.get('pattern', ''), string_types)):
         pattern = instance['pattern']
-        if isinstance(pattern, string_types):
-            p = Pattern(pattern)
-            inspection = p.inspect().comparisons
-            for objtype in inspection:
-                if objtype not in enums.OBSERVABLE_TYPES:
-                    yield PatternError("'%s' is not a valid stix observable type"
-                                       % objtype, instance['id'])
+        p = Pattern(pattern)
+        inspection = p.inspect().comparisons
+        for objtype in inspection:
+            if objtype not in enums.OBSERVABLE_TYPES:
+                yield PatternError("'%s' is not a valid stix observable type"
+                                   % objtype, instance['id'])
 
 
 def properties_strict(instance):
@@ -1175,19 +1162,19 @@ def properties_strict(instance):
             for error in properties_strict_helper(instance, instance['id']):
                 yield error
 
-    if instance['type'] == 'indicator' and 'pattern' in instance:
+    if (instance['type'] == 'indicator' and instance.get('pattern_type', '') == 'stix' and
+            isinstance(instance.get('pattern', ''), string_types)):
         pattern = instance['pattern']
-        if isinstance(pattern, string_types):
-            p = Pattern(pattern)
-            inspection = p.inspect().comparisons
-            for objtype, expression_list in inspection.items():
-                for exp in expression_list:
-                    path = exp[0]
-                    # Get the property name without list index, dictionary key, or referenced object property
-                    prop = path[0]
-                    if objtype in enums.OBSERVABLE_PROPERTIES and prop not in enums.OBSERVABLE_PROPERTIES[objtype]:
-                        yield PatternError("'%s' is not a valid property for '%s' objects"
-                                           % (prop, objtype), instance['id'])
+        p = Pattern(pattern)
+        inspection = p.inspect().comparisons
+        for objtype, expression_list in inspection.items():
+            for exp in expression_list:
+                path = exp[0]
+                # Get the property name without list index, dictionary key, or referenced object property
+                prop = path[0]
+                if objtype in enums.OBSERVABLE_PROPERTIES and prop not in enums.OBSERVABLE_PROPERTIES[objtype]:
+                    yield PatternError("'%s' is not a valid property for '%s' objects"
+                                       % (prop, objtype), instance['id'])
 
 
 def properties_strict_helper(obj, obj_id):
@@ -1252,17 +1239,16 @@ CHECKS = {
         malware_analysis_product,
         windows_process_priority_format,
         hash_length,
-        os_execution_envs_check,
         vocab_marking_definition,
         relationships_strict,
         duplicate_ids,
         vocab_attack_motivation,
         vocab_attack_resource_level,
-        vocab_course_of_action_type,
         vocab_grouping_context,
         vocab_implementation_languages,
         vocab_infrastructure_types,
         vocab_malware_capabilities,
+        vocab_malware_result,
         vocab_processor_architecture,
         vocab_identity_class,
         vocab_indicator_types,
@@ -1304,7 +1290,6 @@ CHECKS = {
         malware_analysis_product,
         windows_process_priority_format,
         hash_length,
-        os_execution_envs_check,
     ],
     'custom-prefix': custom_prefix_strict,
     'custom-prefix-lax': custom_prefix_lax,
@@ -1316,18 +1301,17 @@ CHECKS = {
     'malware-analysis-product': malware_analysis_product,
     'windows-process-priority-format': windows_process_priority_format,
     'hash-length': hash_length,
-    "os-execution-envs": os_execution_envs_check,
     'approved-values': [
         vocab_marking_definition,
         relationships_strict,
         duplicate_ids,
         vocab_attack_motivation,
         vocab_attack_resource_level,
-        vocab_course_of_action_type,
         vocab_grouping_context,
         vocab_implementation_languages,
         vocab_infrastructure_types,
         vocab_malware_capabilities,
+        vocab_malware_result,
         vocab_processor_architecture,
         vocab_identity_class,
         vocab_indicator_types,
@@ -1358,11 +1342,11 @@ CHECKS = {
     'all-vocabs': [
         vocab_attack_motivation,
         vocab_attack_resource_level,
-        vocab_course_of_action_type,
         vocab_grouping_context,
         vocab_implementation_languages,
         vocab_infrastructure_types,
         vocab_malware_capabilities,
+        vocab_malware_result,
         vocab_processor_architecture,
         vocab_identity_class,
         vocab_indicator_types,
@@ -1381,10 +1365,10 @@ CHECKS = {
     ],
     'attack-motivation': vocab_attack_motivation,
     'attack-resource-level': vocab_attack_resource_level,
-    'course-of-action-type': vocab_course_of_action_type,
     'grouping-context': vocab_grouping_context,
     'implementation-languages': vocab_implementation_languages,
     'infrastructure-types': vocab_infrastructure_types,
+    'malware-result': vocab_malware_result,
     'malware-capabilities': vocab_malware_capabilities,
     'processor-architecture': vocab_processor_architecture,
     'identity-class': vocab_identity_class,
@@ -1469,8 +1453,6 @@ def list_shoulds(options):
                     validator_list.append(CHECKS['windows-process-priority-format'])
                 if 'hash-length' not in options.disabled:
                     validator_list.append(CHECKS['hash-length'])
-                if 'os-execution-envs' not in options.disabled:
-                    validator_list.append(CHECKS['os-execution-envs'])
 
             if 'approved-values' not in options.disabled:
                 if 'marking-definition-type' not in options.disabled:


Test patch:
diff --git a/stix2validator/test/v20/observed_data_tests.py b/stix2validator/test/v20/observed_data_tests.py
index ff989b8..39dfce3 100644
--- a/stix2validator/test/v20/observed_data_tests.py
+++ b/stix2validator/test/v20/observed_data_tests.py
@@ -670,6 +670,18 @@ class ObservedDataTestCases(ValidatorTest):
         observed_data['objects']['2']['languages'][0] = 'eng'
         self.assertTrueWithOptions(observed_data)
 
+    def test_software_cpe(self):
+        observed_data = copy.deepcopy(self.valid_observed_data)
+        observed_data['objects']['2'] = {
+            "type": "software",
+            "name": "word",
+            "cpe": "invalid",
+        }
+        self.assertFalseWithOptions(observed_data)
+
+        observed_data['objects']['2']['cpe'] = 'cpe:2.3:a:microsoft:word:2000:*:*:*:*:*:*:*'
+        self.assertTrueWithOptions(observed_data)
+
     def test_email_address_invalid_value(self):
         observed_data = copy.deepcopy(self.valid_observed_data)
         observed_data['objects']['2'] = {
diff --git a/stix2validator/test/v21/coa_tests.py b/stix2validator/test/v21/coa_tests.py
index 3b0928c..9061759 100644
--- a/stix2validator/test/v21/coa_tests.py
+++ b/stix2validator/test/v21/coa_tests.py
@@ -1,8 +1,7 @@
-import copy
 import json
 
 from . import ValidatorTest
-from ... import validate_parsed_json, validate_string
+from ... import validate_string
 
 VALID_COURSE_OF_ACTION = u"""
 {
@@ -13,44 +12,14 @@ VALID_COURSE_OF_ACTION = u"""
     "created": "2016-04-06T20:03:48.000Z",
     "modified": "2016-04-06T20:03:48.000Z",
     "name": "mitigation-poison-ivy-firewall",
-    "description": "Recommended steps to respond to the Poison Ivy malware",
-    "action_type": "textual:text/plain",
-    "action_reference":
-    { "source_name": "internet",
-    "url": "hxxps://www.stopthebad.com/poisonivyresponse.asa"
-    }
+    "description": "Recommended steps to respond to the Poison Ivy malware"
 }
 """
 
 
-class MalwareTestCases(ValidatorTest):
+class CoATestCases(ValidatorTest):
     valid_course_of_action = json.loads(VALID_COURSE_OF_ACTION)
 
     def test_wellformed_coa(self):
         results = validate_string(VALID_COURSE_OF_ACTION, self.options)
         self.assertTrue(results.is_valid)
-
-    def test_invalid_action_type(self):
-        coa = copy.deepcopy(self.valid_course_of_action)
-        coa['action_type'] = "invalid"
-        self.assertFalseWithOptions(coa)
-
-        self.check_ignore(coa, 'course-of-action-type')
-
-    def test_missing_action(self):
-        coa = copy.deepcopy(self.valid_course_of_action)
-        del coa['action_reference']
-        results = validate_parsed_json(coa, self.options)
-        self.assertTrue(results.is_valid)
-
-    def test_invalid_action_bin_and_reference(self):
-        coa = copy.deepcopy(self.valid_course_of_action)
-        coa['action_bin'] = "SGVsbG8gV29ybGQ="
-        self.assertFalseWithOptions(coa)
-
-    def test_invalid_os_execution_envs(self):
-        coa = copy.deepcopy(self.valid_course_of_action)
-        coa['os_execution_envs'] = ["f==00"]
-        self.assertFalseWithOptions(coa)
-
-        self.check_ignore(coa, 'os-execution-envs')
diff --git a/stix2validator/test/v21/indicator_tests.py b/stix2validator/test/v21/indicator_tests.py
index 1381e96..157d0b8 100644
--- a/stix2validator/test/v21/indicator_tests.py
+++ b/stix2validator/test/v21/indicator_tests.py
@@ -250,8 +250,9 @@ class IndicatorTestCases(ValidatorTest):
         indicator["pattern_type"] = "snort"
         indicator["pattern_version"] = "2.9.15"
 
-        results = validate_parsed_json(indicator, self.options)
-        self.assertEqual(results.is_valid, True)
+        self.assertTrueWithOptions(indicator)
+        self.assertTrueWithOptions(indicator, strict_types=True)
+        self.assertTrueWithOptions(indicator, strict_properties=True)
 
     def test_indicator_different_pattern_type_not_in_enum(self):
         pattern = ("signature example-signature {"
diff --git a/stix2validator/test/v21/malware_analysis_tests.py b/stix2validator/test/v21/malware_analysis_tests.py
index 0a2d982..0bceb35 100644
--- a/stix2validator/test/v21/malware_analysis_tests.py
+++ b/stix2validator/test/v21/malware_analysis_tests.py
@@ -7,12 +7,13 @@ from ... import validate_parsed_json, validate_string
 VALID_MALWARE = u"""
 {
     "type": "malware-analysis",
-    "id": "malware-analysis--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
-    "product" : "test-product",
     "spec_version" : "2.1",
-    "av_result": "malicious",
+    "id": "malware-analysis--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
     "created": "2016-04-06T20:03:48.000Z",
-    "modified": "2016-04-06T20:03:48.000Z"
+    "modified": "2016-04-06T20:03:48.000Z",
+    "product" : "test-product",
+    "version": "1.3.1",
+    "result": "malicious"
 }
 """
 
@@ -38,3 +39,10 @@ class MalwareTestCases(ValidatorTest):
         self.assertFalseWithOptions(malware)
         malware['host_vm_ref'] = "software--a4186d48-a4de-4146-af88-6cec55e65c69"
         self.assertTrueWithOptions(malware)
+
+    def test_result(self):
+        malware = copy.deepcopy(self.valid_malware)
+        malware['result'] = "bad"
+        self.assertFalseWithOptions(malware)
+        malware['result'] = "malicious"
+        self.assertTrueWithOptions(malware)
diff --git a/stix2validator/test/v21/observed_data_tests.py b/stix2validator/test/v21/observed_data_tests.py
index 2ed74e1..9272983 100644
--- a/stix2validator/test/v21/observed_data_tests.py
+++ b/stix2validator/test/v21/observed_data_tests.py
@@ -599,6 +599,18 @@ class ObservedDataTestCases(ValidatorTest):
         observed_data['languages'][0] = 'eng'
         self.assertTrueWithOptions(observed_data)
 
+    def test_software_cpe(self):
+        observed_data = {
+            "type": "software",
+            "id": "software--ff1e0780-358c-5808-a8c7-d0fca4ef6ef4",
+            "name": "word",
+            "cpe": "invalid",
+        }
+        self.assertFalseWithOptions(observed_data)
+
+        observed_data['cpe'] = 'cpe:2.3:a:microsoft:word:2000:*:*:*:*:*:*:*'
+        self.assertTrueWithOptions(observed_data)
+
     def test_email_address_invalid_value(self):
         observed_data = {
             "type": "email-addr",
@@ -764,7 +776,7 @@ class ObservedDataTestCases(ValidatorTest):
         observed_data['last_observed'] = "2016-05-07T20:06:37.000Z"
         self.assertTrueWithOptions(observed_data)
 
-    def test_domain_name_deprecated_property(self):
+    def test_domain_name_not_deprecated_property(self):
         observed_data = {
             "type": "domain-name",
             "spec_version": "2.1",
@@ -772,8 +784,4 @@ class ObservedDataTestCases(ValidatorTest):
             "value": "example.com",
             "resolves_to_refs": ["ipv4-addr--efcd5e80-570d-4131-b213-62cb18eaa6a8"]
         }
-        self.assertFalseWithOptions(observed_data)
-        self.check_ignore(observed_data, 'deprecated-properties')
-
-        del observed_data['resolves_to_refs']
         self.assertTrueWithOptions(observed_data)