From fa6cff8a3442685eeef456e045cc16fa8cdf5b19 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Wed, 6 Jan 2021 18:59:48 -0500 Subject: [PATCH 01/20] WIP adding is_sdo() et al functions to this library. On hold while I address circular import problems. --- stix2/utils.py | 184 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 174 insertions(+), 10 deletions(-) diff --git a/stix2/utils.py b/stix2/utils.py index 22efcc2..02e6f39 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -9,6 +9,11 @@ import pytz import six import stix2 +try: + import stix2.parsing as mappings +except ImportError: + import stix2.core as mappings + # Sentinel value for properties that should be set to the current time. # We can't use the standard 'default' approach, since if there are multiple @@ -313,18 +318,177 @@ def get_type_from_id(stix_id): return stix_id.split('--', 1)[0] -def is_marking(obj_or_id): - """Determines whether the given object or object ID is/is for a marking - definition. +def _stix_type_of(value): + """ + Get a STIX type from the given value: if a STIX ID is passed, the type + prefix is extracted; if string which is not a STIX ID is passed, it is + assumed to be a STIX type and is returned; otherwise it is assumed to be a + mapping with a "type" property, and the value of that property is returned. - :param obj_or_id: A STIX object or object ID as a string. - :return: True if a marking definition, False otherwise. + :param value: A mapping with a "type" property, or a STIX ID or type + as a string + :return: A STIX type + """ + if isinstance(value, str): + if "--" in value: + type_ = get_type_from_id(value) + else: + type_ = value + else: + type_ = value["type"] + + return type_ + + +def _get_stix2_class_maps(stix_version): + """ + Get the stix2 class mappings for the given STIX version. + + :param stix_version: A STIX version as a string + :return: The class mappings. This will be a dict mapping from some general + category name, e.g. "object" to another mapping from STIX type + to a stix2 class. + """ + stix_vid = "v" + stix_version.replace(".", "") + cls_maps = mappings.STIX2_OBJ_MAPS[stix_vid] + + return cls_maps + + +def is_sdo(value, stix_version=stix2.DEFAULT_VERSION): + """ + Determine whether the given object, type, or ID is/is for an SDO. + + :param value: A mapping with a "type" property, or a STIX ID or type + as a string + :param stix_version: A STIX version as a string + :return: True if the type of the given value is an SDO type; False + if not """ - if isinstance(obj_or_id, (stix2.base._STIXBase, dict)): - result = obj_or_id["type"] == "marking-definition" - else: - # it's a string ID - result = obj_or_id.startswith("marking-definition--") + # Eventually this needs to be moved into the stix2 library (and maybe + # improved?); see cti-python-stix2 github issue #450. + + cls_maps = _get_stix2_class_maps(stix_version) + type_ = _stix_type_of(value) + result = type_ in cls_maps["objects"] and type_ not in { + "relationship", "sighting", "marking-definition", "bundle", + "language-content" + } + + return result + + +def is_sco(value, stix_version=stix2.DEFAULT_VERSION): + """ + Determine whether the given object, type, or ID is/is for an SCO. + + :param value: A mapping with a "type" property, or a STIX ID or type + as a string + :param stix_version: A STIX version as a string + :return: True if the type of the given value is an SCO type; False + if not + """ + cls_maps = _get_stix2_class_maps(stix_version) + type_ = _stix_type_of(value) + result = type_ in cls_maps["observables"] + + return result + + +def is_sro(value, stix_version=stix2.DEFAULT_VERSION): + """ + Determine whether the given object, type, or ID is/is for an SCO. + + :param value: A mapping with a "type" property, or a STIX ID or type + as a string + :param stix_version: A STIX version as a string + :return: True if the type of the given value is an SRO type; False + if not + """ + + # No STIX version dependence here yet... + type_ = _stix_type_of(value) + result = type_ in ("sighting", "relationship") + + return result + + +def is_object(value, stix_version=stix2.DEFAULT_VERSION): + """ + Determine whether an object, type, or ID is/is for any STIX object. This + includes all SDOs, SCOs, meta-objects, and bundle. + + :param value: A mapping with a "type" property, or a STIX ID or type + as a string + :param stix_version: A STIX version as a string + :return: True if the type of the given value is a valid STIX type with + respect to the given STIX version; False if not + """ + cls_maps = _get_stix2_class_maps(stix_version) + type_ = _stix_type_of(value) + result = type_ in cls_maps["observables"] or type_ in cls_maps["objects"] + + return result + + +def is_marking(value, stix_version=stix2.DEFAULT_VERSION): + """Determines whether the given value is/is for a marking definition. + + :param value: A STIX object, object ID, or type as a string. + :param stix_version: A STIX version as a string + :return: True if the value is/is for a marking definition, False otherwise. + """ + + # No STIX version dependence here yet... + type_ = _stix_type_of(value) + result = type_ == "marking-definition" + + return result + + +class STIXTypeClass(enum.Enum): + """ + Represents different classes of STIX type. + """ + SDO = 0 + SCO = 1 + SRO = 2 + + +def is_stix_type(value, stix_version=stix2.DEFAULT_VERSION, *types): + """ + Determine whether the type of the given value satisfies the given + constraints. 'types' must contain STIX types as strings, and/or the + STIXTypeClass enum values. STIX types imply an exact match constraint; + STIXTypeClass enum values imply a more general constraint, that the object + or type be in that class of STIX type. These constraints are implicitly + OR'd together. + + :param value: A mapping with a "type" property, or a STIX ID or type + as a string + :param stix_version: A STIX version as a string + :param types: A sequence of STIX type strings or STIXTypeClass enum values + :return: True if the object or type satisfies the constraints; False if not + """ + + for type_ in types: + if type_ is STIXTypeClass.SDO: + result = is_sdo(value, stix_version) + elif type_ is STIXTypeClass.SCO: + result = is_sco(value, stix_version) + elif type_ is STIXTypeClass.SRO: + result = is_sro(value, stix_version) + else: + # Assume a string STIX type is given instead of a class enum, + # and just check for exact match. + obj_type = _stix_type_of(value) + result = is_object(obj_type, stix_version) and obj_type == type_ + + if result: + break + + else: + result = False return result From 51937232db4f9173a9e4309d15b3e7c7cbfa749f Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Mon, 11 Jan 2021 20:35:41 -0500 Subject: [PATCH 02/20] Fix to an import statement which was necessary due to the circular import refactoring. I think I just forgot to include this in the previous commit... --- stix2/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stix2/utils.py b/stix2/utils.py index 02e6f39..94921a7 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -10,7 +10,7 @@ import six import stix2 try: - import stix2.parsing as mappings + import stix2.registry as mappings except ImportError: import stix2.core as mappings From 0f2ce0ac729c7faa0a6a637178dbf5df6e308d8c Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Mon, 11 Jan 2021 20:38:31 -0500 Subject: [PATCH 03/20] Add unit tests for the is_*() utility type checking functions. --- stix2/test/test_utils_type_checks.py | 287 +++++++++++++++++++++++++++ stix2/test/v21/test_utils.py | 37 ++++ 2 files changed, 324 insertions(+) create mode 100644 stix2/test/test_utils_type_checks.py diff --git a/stix2/test/test_utils_type_checks.py b/stix2/test/test_utils_type_checks.py new file mode 100644 index 0000000..31d99eb --- /dev/null +++ b/stix2/test/test_utils_type_checks.py @@ -0,0 +1,287 @@ +import pytest +import stix2.utils + + +### +### Tests using types/behaviors common to STIX 2.0 and 2.1. +### + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +@pytest.mark.parametrize( + "type_", [ + "attack-pattern", + "campaign", + "course-of-action", + "identity", + "indicator", + "intrusion-set", + "malware", + "observed-data", + "report", + "threat-actor", + "tool", + "vulnerability" + ] +) +def test_is_sdo(type_, stix_version): + assert stix2.utils.is_sdo(type_, stix_version) + + id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert stix2.utils.is_sdo(id_, stix_version) + + d = { + "type": type_ + } + assert stix2.utils.is_sdo(d, stix_version) + + assert stix2.utils.is_stix_type( + type_, stix_version, stix2.utils.STIXTypeClass.SDO + ) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +@pytest.mark.parametrize( + "type_", [ + "relationship", + "sighting", + "marking-definition", + "bundle", + "language-content", + "ipv4-addr", + "foo" + ] +) +def test_is_not_sdo(type_, stix_version): + assert not stix2.utils.is_sdo(type_, stix_version) + + id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert not stix2.utils.is_sdo(id_, stix_version) + + d = { + "type": type_ + } + assert not stix2.utils.is_sdo(d, stix_version) + + assert not stix2.utils.is_stix_type( + type_, stix_version, stix2.utils.STIXTypeClass.SDO + ) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +@pytest.mark.parametrize( + "type_", [ + "artifact", + "autonomous-system", + "directory", + "domain-name", + "email-addr", + "email-message", + "file", + "ipv4-addr", + "ipv6-addr", + "mac-addr", + "mutex", + "network-traffic", + "process", + "software", + "url", + "user-account", + "windows-registry-key", + "x509-certificate" + ] +) +def test_is_sco(type_, stix_version): + assert stix2.utils.is_sco(type_, stix_version) + + id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert stix2.utils.is_sco(id_, stix_version) + + d = { + "type": type_ + } + assert stix2.utils.is_sco(d, stix_version) + + assert stix2.utils.is_stix_type( + type_, stix_version, stix2.utils.STIXTypeClass.SCO + ) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +@pytest.mark.parametrize( + "type_", [ + "identity", + "sighting", + "marking-definition", + "bundle", + "language-content", + "foo" + ] +) +def test_is_not_sco(type_, stix_version): + assert not stix2.utils.is_sco(type_, stix_version) + + id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert not stix2.utils.is_sco(id_, stix_version) + + d = { + "type": type_ + } + assert not stix2.utils.is_sco(d, stix_version) + + assert not stix2.utils.is_stix_type( + type_, stix_version, stix2.utils.STIXTypeClass.SCO + ) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +@pytest.mark.parametrize( + "type_", [ + "relationship", + "sighting" + ] +) +def test_is_sro(type_, stix_version): + assert stix2.utils.is_sro(type_, stix_version) + + id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert stix2.utils.is_sro(id_, stix_version) + + d = { + "type": type_ + } + assert stix2.utils.is_sro(d, stix_version) + + assert stix2.utils.is_stix_type( + type_, stix_version, stix2.utils.STIXTypeClass.SRO + ) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +@pytest.mark.parametrize( + "type_", [ + "identity", + "marking-definition", + "bundle", + "language-content", + "ipv4-addr", + "foo" + ] +) +def test_is_not_sro(type_, stix_version): + assert not stix2.utils.is_sro(type_, stix_version) + + id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert not stix2.utils.is_sro(id_, stix_version) + + d = { + "type": type_ + } + assert not stix2.utils.is_sro(d, stix_version) + + assert not stix2.utils.is_stix_type( + type_, stix_version, stix2.utils.STIXTypeClass.SRO + ) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +def test_is_marking(stix_version): + assert stix2.utils.is_marking("marking-definition", stix_version) + + id_ = "marking-definition--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert stix2.utils.is_marking(id_, stix_version) + + d = { + "type": "marking-definition" + } + assert stix2.utils.is_marking(d, stix_version) + + assert stix2.utils.is_stix_type( + "marking-definition", stix_version, "marking-definition" + ) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +@pytest.mark.parametrize( + "type_", [ + "identity", + "bundle", + "language-content", + "ipv4-addr", + "foo" + ] +) +def test_is_not_marking(type_, stix_version): + assert not stix2.utils.is_marking(type_, stix_version) + + id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert not stix2.utils.is_marking(id_, stix_version) + + d = { + "type": type_ + } + assert not stix2.utils.is_marking(d, stix_version) + + assert not stix2.utils.is_stix_type( + type_, stix_version, "marking-definition" + ) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +@pytest.mark.parametrize( + "type_", [ + "identity", + "relationship", + "sighting", + "marking-definition", + "bundle", + "ipv4-addr" + ] +) +def test_is_object(type_, stix_version): + assert stix2.utils.is_object(type_, stix_version) + + id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert stix2.utils.is_object(id_, stix_version) + + d = { + "type": type_ + } + assert stix2.utils.is_object(d, stix_version) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +def test_is_not_object(stix_version): + assert not stix2.utils.is_object("foo", stix_version) + + id_ = "foo--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert not stix2.utils.is_object(id_, stix_version) + + d = { + "type": "foo" + } + assert not stix2.utils.is_object(d, stix_version) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +def test_is_stix_type(stix_version): + + assert not stix2.utils.is_stix_type( + "foo", stix_version, stix2.utils.STIXTypeClass.SDO, "foo" + ) + + assert stix2.utils.is_stix_type( + "bundle", stix_version, "foo", "bundle" + ) + + assert stix2.utils.is_stix_type( + "identity", stix_version, + stix2.utils.STIXTypeClass.SDO, + stix2.utils.STIXTypeClass.SRO + ) + + assert stix2.utils.is_stix_type( + "software", stix_version, + stix2.utils.STIXTypeClass.SDO, + stix2.utils.STIXTypeClass.SCO + ) diff --git a/stix2/test/v21/test_utils.py b/stix2/test/v21/test_utils.py index 41bc087..940d1db 100644 --- a/stix2/test/v21/test_utils.py +++ b/stix2/test/v21/test_utils.py @@ -241,3 +241,40 @@ def test_find_property_index(object, tuple_to_find, expected_index): ) def test_iterate_over_values(dict_value, tuple_to_find, expected_index): assert stix2.serialization._find_property_in_seq(dict_value.values(), *tuple_to_find) == expected_index + + +# Only 2.1-specific types/behaviors tested here. +@pytest.mark.parametrize( + "type_", [ + "grouping", + "infrastructure", + "location", + "malware-analysis", + "note", + "opinion" + ] +) +def test_is_sdo(type_): + assert stix2.utils.is_sdo(type_, "2.1") + + id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert stix2.utils.is_sdo(id_, "2.1") + + d = { + "type": type_ + } + assert stix2.utils.is_sdo(d, "2.1") + + assert stix2.utils.is_stix_type( + type_, "2.1", stix2.utils.STIXTypeClass.SDO + ) + + +def test_type_checks_language_content(): + assert stix2.utils.is_object("language-content", "2.1") + assert not stix2.utils.is_sdo("language-content", "2.1") + assert not stix2.utils.is_sco("language-content", "2.1") + assert not stix2.utils.is_sro("language-content", "2.1") + assert stix2.utils.is_stix_type( + "language-content", "2.1", "language-content" + ) From 24307626b0e929b0520110b7bf3e1407d666ae7a Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Tue, 12 Jan 2021 16:30:26 -0500 Subject: [PATCH 04/20] Move get_stix2_class_maps() from .utils to .registry (since it's really just a simple accessor into the class maps table), and change other code to use it, in places where it was simple and made sense. --- stix2/registration.py | 27 +++++++++++---------------- stix2/registry.py | 15 +++++++++++++++ stix2/utils.py | 21 +++------------------ 3 files changed, 29 insertions(+), 34 deletions(-) diff --git a/stix2/registration.py b/stix2/registration.py index bc07995..e3aa5e5 100644 --- a/stix2/registration.py +++ b/stix2/registration.py @@ -68,13 +68,11 @@ def _register_marking(new_marking, version=DEFAULT_VERSION): if not re.match(PREFIX_21_REGEX, prop_name): raise ValueError("Property name '%s' must begin with an alpha character." % prop_name) - if version: - v = 'v' + version.replace('.', '') - else: - # Use default version (latest) if no version was provided. - v = 'v' + DEFAULT_VERSION.replace('.', '') + class_maps = registry.get_stix2_class_maps( + version or DEFAULT_VERSION + ) - OBJ_MAP_MARKING = registry.STIX2_OBJ_MAPS[v]['markings'] + OBJ_MAP_MARKING = class_maps['markings'] if mark_type in OBJ_MAP_MARKING.keys(): raise DuplicateRegistrationError("STIX Marking", mark_type) OBJ_MAP_MARKING[mark_type] = new_marking @@ -130,13 +128,11 @@ def _register_observable(new_observable, version=DEFAULT_VERSION): "is not a ListProperty containing ReferenceProperty." % prop_name, ) - if version: - v = 'v' + version.replace('.', '') - else: - # Use default version (latest) if no version was provided. - v = 'v' + DEFAULT_VERSION.replace('.', '') + class_maps = registry.get_stix2_class_maps( + version or DEFAULT_VERSION + ) - OBJ_MAP_OBSERVABLE = registry.STIX2_OBJ_MAPS[v]['observables'] + OBJ_MAP_OBSERVABLE = class_maps['observables'] if new_observable._type in OBJ_MAP_OBSERVABLE.keys(): raise DuplicateRegistrationError("Cyber Observable", new_observable._type) OBJ_MAP_OBSERVABLE[new_observable._type] = new_observable @@ -182,8 +178,6 @@ def _register_observable_extension( if not re.match(PREFIX_21_REGEX, prop_name): raise ValueError("Property name '%s' must begin with an alpha character." % prop_name) - v = 'v' + version.replace('.', '') - try: observable_type = observable._type except AttributeError: @@ -192,8 +186,9 @@ def _register_observable_extension( "created with the @CustomObservable decorator.", ) - OBJ_MAP_OBSERVABLE = registry.STIX2_OBJ_MAPS[v]['observables'] - EXT_MAP = registry.STIX2_OBJ_MAPS[v]['observable-extensions'] + class_maps = registry.get_stix2_class_maps(version) + OBJ_MAP_OBSERVABLE = class_maps['observables'] + EXT_MAP = class_maps['observable-extensions'] try: if ext_type in EXT_MAP[observable_type].keys(): diff --git a/stix2/registry.py b/stix2/registry.py index 6cb6cd8..692fe3e 100644 --- a/stix2/registry.py +++ b/stix2/registry.py @@ -26,3 +26,18 @@ def _collect_stix2_mappings(): elif re.match(r'^stix2\.v2[0-9]\.common$', name) and is_pkg is False: mod = importlib.import_module(name, str(top_level_module.__name__)) STIX2_OBJ_MAPS[ver]['markings'] = mod.OBJ_MAP_MARKING + + +def get_stix2_class_maps(stix_version): + """ + Get the stix2 class mappings for the given STIX version. + + :param stix_version: A STIX version as a string + :return: The class mappings. This will be a dict mapping from some general + category name, e.g. "object" to another mapping from STIX type + to a stix2 class. + """ + stix_vid = "v" + stix_version.replace(".", "") + cls_maps = STIX2_OBJ_MAPS[stix_vid] + + return cls_maps diff --git a/stix2/utils.py b/stix2/utils.py index 94921a7..76fbea0 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -340,21 +340,6 @@ def _stix_type_of(value): return type_ -def _get_stix2_class_maps(stix_version): - """ - Get the stix2 class mappings for the given STIX version. - - :param stix_version: A STIX version as a string - :return: The class mappings. This will be a dict mapping from some general - category name, e.g. "object" to another mapping from STIX type - to a stix2 class. - """ - stix_vid = "v" + stix_version.replace(".", "") - cls_maps = mappings.STIX2_OBJ_MAPS[stix_vid] - - return cls_maps - - def is_sdo(value, stix_version=stix2.DEFAULT_VERSION): """ Determine whether the given object, type, or ID is/is for an SDO. @@ -369,7 +354,7 @@ def is_sdo(value, stix_version=stix2.DEFAULT_VERSION): # Eventually this needs to be moved into the stix2 library (and maybe # improved?); see cti-python-stix2 github issue #450. - cls_maps = _get_stix2_class_maps(stix_version) + cls_maps = mappings.get_stix2_class_maps(stix_version) type_ = _stix_type_of(value) result = type_ in cls_maps["objects"] and type_ not in { "relationship", "sighting", "marking-definition", "bundle", @@ -389,7 +374,7 @@ def is_sco(value, stix_version=stix2.DEFAULT_VERSION): :return: True if the type of the given value is an SCO type; False if not """ - cls_maps = _get_stix2_class_maps(stix_version) + cls_maps = mappings.get_stix2_class_maps(stix_version) type_ = _stix_type_of(value) result = type_ in cls_maps["observables"] @@ -425,7 +410,7 @@ def is_object(value, stix_version=stix2.DEFAULT_VERSION): :return: True if the type of the given value is a valid STIX type with respect to the given STIX version; False if not """ - cls_maps = _get_stix2_class_maps(stix_version) + cls_maps = mappings.get_stix2_class_maps(stix_version) type_ = _stix_type_of(value) result = type_ in cls_maps["observables"] or type_ in cls_maps["objects"] From 188f704b28460d50fb29e5a7e8fd4cec2855fde7 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Tue, 12 Jan 2021 21:17:34 -0500 Subject: [PATCH 05/20] Remove a "this needs to be moved into the stix2 library" comment, since this *is* in the stix2 library! --- stix2/utils.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/stix2/utils.py b/stix2/utils.py index 76fbea0..34915d9 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -350,10 +350,6 @@ def is_sdo(value, stix_version=stix2.DEFAULT_VERSION): :return: True if the type of the given value is an SDO type; False if not """ - - # Eventually this needs to be moved into the stix2 library (and maybe - # improved?); see cti-python-stix2 github issue #450. - cls_maps = mappings.get_stix2_class_maps(stix_version) type_ = _stix_type_of(value) result = type_ in cls_maps["objects"] and type_ not in { From f88fba6751e1d3899a84fbe28670c6d4551bd4bf Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Wed, 13 Jan 2021 21:09:40 -0500 Subject: [PATCH 06/20] Change the stix2 class map structure to be keyed at the top level with STIX versions in the same format as is used everywhere else in the API: "X.Y", as opposed to the "vXY" format used by the version-specific python packages. This eliminates all of the awkward conversion from public API format to "vXX" format. Also a little bit of code rearranging in the registration module to ensure that some STIX 2.1-specific checks are done whether version 2.1 is given explicitly or is defaulted to. In the same module I also added a missing import of stix2.properties, since my IDE was claiming it could not find a function from that module. --- stix2/parsing.py | 33 ++++++++++---------------- stix2/properties.py | 8 +++---- stix2/registration.py | 25 ++++++++++--------- stix2/registry.py | 21 +++++++++++++--- stix2/test/test_spec_version_detect.py | 26 ++++++++++---------- stix2/test/v20/test_custom.py | 12 ++++------ stix2/test/v20/test_parsing.py | 5 ++-- stix2/test/v21/test_custom.py | 11 ++++----- stix2/test/v21/test_parsing.py | 10 ++++---- stix2/versioning.py | 8 +++---- 10 files changed, 77 insertions(+), 82 deletions(-) diff --git a/stix2/parsing.py b/stix2/parsing.py index d9b6516..fa2df71 100644 --- a/stix2/parsing.py +++ b/stix2/parsing.py @@ -49,8 +49,7 @@ def _detect_spec_version(stix_dict): :param stix_dict: A dict with some STIX content. Must at least have a "type" property. - :return: A string in "vXX" format, where "XX" indicates the spec version, - e.g. "v20", "v21", etc. + :return: A STIX version in "X.Y" format """ obj_type = stix_dict["type"] @@ -58,16 +57,16 @@ def _detect_spec_version(stix_dict): if 'spec_version' in stix_dict: # For STIX 2.0, applies to bundles only. # For STIX 2.1+, applies to SCOs, SDOs, SROs, and markings only. - v = 'v' + stix_dict['spec_version'].replace('.', '') + v = stix_dict['spec_version'] elif "id" not in stix_dict: # Only 2.0 SCOs don't have ID properties - v = "v20" + v = "2.0" elif obj_type == 'bundle': # Bundle without a spec_version property: must be 2.1. But to # future-proof, use max version over all contained SCOs, with 2.1 # minimum. v = max( - "v21", + "2.1", max( _detect_spec_version(obj) for obj in stix_dict["objects"] ), @@ -75,10 +74,10 @@ def _detect_spec_version(stix_dict): elif obj_type in registry.STIX2_OBJ_MAPS["v21"]["observables"]: # Non-bundle object with an ID and without spec_version. Could be a # 2.1 SCO or 2.0 SDO/SRO/marking. Check for 2.1 SCO... - v = "v21" + v = "2.1" else: # Not a 2.1 SCO; must be a 2.0 object. - v = "v20" + v = "2.0" return v @@ -115,15 +114,12 @@ def dict_to_stix2(stix_dict, allow_custom=False, version=None): if 'type' not in stix_dict: raise ParseError("Can't parse object with no 'type' property: %s" % str(stix_dict)) - if version: - # If the version argument was passed, override other approaches. - v = 'v' + version.replace('.', '') - else: - v = _detect_spec_version(stix_dict) + if not version: + version = _detect_spec_version(stix_dict) OBJ_MAP = dict( - registry.STIX2_OBJ_MAPS[v]['objects'], - **registry.STIX2_OBJ_MAPS[v]['observables'] + registry.STIX2_OBJ_MAPS[version]['objects'], + **registry.STIX2_OBJ_MAPS[version]['observables'] ) try: @@ -168,14 +164,11 @@ def parse_observable(data, _valid_refs=None, allow_custom=False, version=None): obj['_valid_refs'] = _valid_refs or [] - if version: - # If the version argument was passed, override other approaches. - v = 'v' + version.replace('.', '') - else: - v = _detect_spec_version(obj) + if not version: + version = _detect_spec_version(obj) try: - OBJ_MAP_OBSERVABLE = registry.STIX2_OBJ_MAPS[v]['observables'] + OBJ_MAP_OBSERVABLE = registry.STIX2_OBJ_MAPS[version]['observables'] obj_class = OBJ_MAP_OBSERVABLE[obj['type']] except KeyError: if allow_custom: diff --git a/stix2/properties.py b/stix2/properties.py index ba31d78..bf7fc8c 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -503,14 +503,14 @@ class ReferenceProperty(Property): possible_prefix = value[:value.index('--')] if self.valid_types: - ref_valid_types = enumerate_types(self.valid_types, 'v' + self.spec_version.replace(".", "")) + ref_valid_types = enumerate_types(self.valid_types, self.spec_version) if possible_prefix in ref_valid_types: required_prefix = possible_prefix else: raise ValueError("The type-specifying prefix '%s' for this property is not valid" % (possible_prefix)) elif self.invalid_types: - ref_invalid_types = enumerate_types(self.invalid_types, 'v' + self.spec_version.replace(".", "")) + ref_invalid_types = enumerate_types(self.invalid_types, self.spec_version) if possible_prefix not in ref_invalid_types: required_prefix = possible_prefix @@ -655,9 +655,7 @@ class ExtensionsProperty(DictionaryProperty): except ValueError: raise ValueError("The extensions property must contain a dictionary") - v = 'v' + self.spec_version.replace('.', '') - - specific_type_map = STIX2_OBJ_MAPS[v]['observable-extensions'].get(self.enclosing_type, {}) + specific_type_map = STIX2_OBJ_MAPS[self.spec_version]['observable-extensions'].get(self.enclosing_type, {}) for key, subvalue in dictified.items(): if key in specific_type_map: cls = specific_type_map[key] diff --git a/stix2/registration.py b/stix2/registration.py index e3aa5e5..65d2714 100644 --- a/stix2/registration.py +++ b/stix2/registration.py @@ -31,18 +31,15 @@ def _register_object(new_type, version=DEFAULT_VERSION): properties = new_type._properties + if not version: + version = DEFAULT_VERSION + if version == "2.1": for prop_name, prop in properties.items(): if not re.match(PREFIX_21_REGEX, prop_name): raise ValueError("Property name '%s' must begin with an alpha character" % prop_name) - if version: - v = 'v' + version.replace('.', '') - else: - # Use default version (latest) if no version was provided. - v = 'v' + DEFAULT_VERSION.replace('.', '') - - OBJ_MAP = registry.STIX2_OBJ_MAPS[v]['objects'] + OBJ_MAP = registry.STIX2_OBJ_MAPS[version]['objects'] if new_type._type in OBJ_MAP.keys(): raise DuplicateRegistrationError("STIX Object", new_type._type) OBJ_MAP[new_type._type] = new_type @@ -61,6 +58,9 @@ def _register_marking(new_marking, version=DEFAULT_VERSION): mark_type = new_marking._type properties = new_marking._properties + if not version: + version = DEFAULT_VERSION + _validate_type(mark_type, version) if version == "2.1": @@ -68,9 +68,7 @@ def _register_marking(new_marking, version=DEFAULT_VERSION): if not re.match(PREFIX_21_REGEX, prop_name): raise ValueError("Property name '%s' must begin with an alpha character." % prop_name) - class_maps = registry.get_stix2_class_maps( - version or DEFAULT_VERSION - ) + class_maps = registry.get_stix2_class_maps(version) OBJ_MAP_MARKING = class_maps['markings'] if mark_type in OBJ_MAP_MARKING.keys(): @@ -89,6 +87,9 @@ def _register_observable(new_observable, version=DEFAULT_VERSION): """ properties = new_observable._properties + if not version: + version = stix2.DEFAULT_VERSION + if version == "2.0": # If using STIX2.0, check properties ending in "_ref/s" are ObjectReferenceProperties for prop_name, prop in properties.items(): @@ -128,9 +129,7 @@ def _register_observable(new_observable, version=DEFAULT_VERSION): "is not a ListProperty containing ReferenceProperty." % prop_name, ) - class_maps = registry.get_stix2_class_maps( - version or DEFAULT_VERSION - ) + class_maps = registry.get_stix2_class_maps(version) OBJ_MAP_OBSERVABLE = class_maps['observables'] if new_observable._type in OBJ_MAP_OBSERVABLE.keys(): diff --git a/stix2/registry.py b/stix2/registry.py index 692fe3e..3b45e48 100644 --- a/stix2/registry.py +++ b/stix2/registry.py @@ -7,6 +7,20 @@ import re STIX2_OBJ_MAPS = {} +def _stix_vid_to_version(stix_vid): + """ + Convert a python package name representing a STIX version in the form "vXX" + to the dotted style used in the public APIs of this library, "X.X". + + :param stix_vid: A package name in the form "vXX" + :return: A STIX version in dotted style + """ + assert len(stix_vid) >= 3 + + stix_version = stix_vid[1] + "." + stix_vid[2:] + return stix_version + + def _collect_stix2_mappings(): """Navigate the package once and retrieve all object mapping dicts for each v2X package. Includes OBJ_MAP, OBJ_MAP_OBSERVABLE, EXT_MAP.""" @@ -16,14 +30,16 @@ def _collect_stix2_mappings(): prefix = str(top_level_module.__name__) + '.' for module_loader, name, is_pkg in pkgutil.walk_packages(path=path, prefix=prefix): - ver = name.split('.')[1] + stix_vid = name.split('.')[1] if re.match(r'^stix2\.v2[0-9]$', name) and is_pkg: + ver = _stix_vid_to_version(stix_vid) mod = importlib.import_module(name, str(top_level_module.__name__)) STIX2_OBJ_MAPS[ver] = {} STIX2_OBJ_MAPS[ver]['objects'] = mod.OBJ_MAP STIX2_OBJ_MAPS[ver]['observables'] = mod.OBJ_MAP_OBSERVABLE STIX2_OBJ_MAPS[ver]['observable-extensions'] = mod.EXT_MAP elif re.match(r'^stix2\.v2[0-9]\.common$', name) and is_pkg is False: + ver = _stix_vid_to_version(stix_vid) mod = importlib.import_module(name, str(top_level_module.__name__)) STIX2_OBJ_MAPS[ver]['markings'] = mod.OBJ_MAP_MARKING @@ -37,7 +53,6 @@ def get_stix2_class_maps(stix_version): category name, e.g. "object" to another mapping from STIX type to a stix2 class. """ - stix_vid = "v" + stix_version.replace(".", "") - cls_maps = STIX2_OBJ_MAPS[stix_vid] + cls_maps = STIX2_OBJ_MAPS[stix_version] return cls_maps diff --git a/stix2/test/test_spec_version_detect.py b/stix2/test/test_spec_version_detect.py index 7039024..9196e16 100644 --- a/stix2/test/test_spec_version_detect.py +++ b/stix2/test/test_spec_version_detect.py @@ -17,7 +17,7 @@ from stix2.parsing import _detect_spec_version "name": "alice", "identity_class": "individual", }, - "v20", + "2.0", ), ( { @@ -29,14 +29,14 @@ from stix2.parsing import _detect_spec_version "target_ref": "identity--ba18dde2-56d3-4a34-aa0b-fc56f5be568f", "relationship_type": "targets", }, - "v20", + "2.0", ), ( { "type": "file", "name": "notes.txt", }, - "v20", + "2.0", ), ( { @@ -48,7 +48,7 @@ from stix2.parsing import _detect_spec_version "statement": "Copyright (c) ACME Corp.", }, }, - "v20", + "2.0", ), ( { @@ -75,7 +75,7 @@ from stix2.parsing import _detect_spec_version }, ], }, - "v20", + "2.0", ), # STIX 2.1 examples ( @@ -87,7 +87,7 @@ from stix2.parsing import _detect_spec_version "modified": "2001-07-01T09:33:17.000Z", "name": "alice", }, - "v21", + "2.1", ), ( { @@ -100,7 +100,7 @@ from stix2.parsing import _detect_spec_version "target_ref": "identity--ba18dde2-56d3-4a34-aa0b-fc56f5be568f", "relationship_type": "targets", }, - "v21", + "2.1", ), ( { @@ -109,7 +109,7 @@ from stix2.parsing import _detect_spec_version "spec_version": "2.1", "name": "notes.txt", }, - "v21", + "2.1", ), ( { @@ -117,7 +117,7 @@ from stix2.parsing import _detect_spec_version "id": "file--5eef3404-6a94-4db3-9a1a-5684cbea0dfe", "name": "notes.txt", }, - "v21", + "2.1", ), ( { @@ -131,7 +131,7 @@ from stix2.parsing import _detect_spec_version "tlp": "green", }, }, - "v21", + "2.1", ), ( { @@ -153,7 +153,7 @@ from stix2.parsing import _detect_spec_version }, ], }, - "v21", + "2.1", ), # Mixed spec examples ( @@ -180,7 +180,7 @@ from stix2.parsing import _detect_spec_version }, ], }, - "v21", + "2.1", ), ( { @@ -202,7 +202,7 @@ from stix2.parsing import _detect_spec_version }, ], }, - "v21", + "2.1", ), ], ) diff --git a/stix2/test/v20/test_custom.py b/stix2/test/v20/test_custom.py index 6ce4a62..a83bf24 100644 --- a/stix2/test/v20/test_custom.py +++ b/stix2/test/v20/test_custom.py @@ -1044,9 +1044,8 @@ def test_register_custom_object_with_version(): } cust_obj_1 = stix2.parsing.dict_to_stix2(custom_obj_1, version='2.0') - v = 'v20' - assert cust_obj_1.type in stix2.registry.STIX2_OBJ_MAPS[v]['objects'] + assert cust_obj_1.type in stix2.registry.STIX2_OBJ_MAPS['2.0']['objects'] # spec_version is not in STIX 2.0, and is required in 2.1, so this # suffices as a test for a STIX 2.0 object. assert "spec_version" not in cust_obj_1 @@ -1076,9 +1075,8 @@ class NewObservable2(object): def test_register_observable_with_version(): custom_obs = NewObservable2(property1="Test Observable") - v = 'v20' - assert custom_obs.type in stix2.registry.STIX2_OBJ_MAPS[v]['observables'] + assert custom_obs.type in stix2.registry.STIX2_OBJ_MAPS['2.0']['observables'] def test_register_duplicate_observable_with_version(): @@ -1101,10 +1099,9 @@ def test_register_marking_with_version(): ) class NewObj2(): pass - v = 'v20' no = NewObj2(property1='something') - assert no._type in stix2.registry.STIX2_OBJ_MAPS[v]['markings'] + assert no._type in stix2.registry.STIX2_OBJ_MAPS['2.0']['markings'] def test_register_observable_extension_with_version(): @@ -1116,10 +1113,9 @@ def test_register_observable_extension_with_version(): class SomeCustomExtension2: pass - v = 'v20' example = SomeCustomExtension2(keys='test123') - assert example._type in stix2.registry.STIX2_OBJ_MAPS[v]['observable-extensions']['user-account'] + assert example._type in stix2.registry.STIX2_OBJ_MAPS['2.0']['observable-extensions']['user-account'] def test_register_duplicate_observable_extension(): diff --git a/stix2/test/v20/test_parsing.py b/stix2/test/v20/test_parsing.py index 01c6607..6317e5a 100644 --- a/stix2/test/v20/test_parsing.py +++ b/stix2/test/v20/test_parsing.py @@ -73,7 +73,6 @@ def test_register_marking_with_version(): _properties = OrderedDict() registration._register_marking(NewMarking1, version='2.0') - v = 'v20' - assert NewMarking1._type in registry.STIX2_OBJ_MAPS[v]['markings'] - assert v in str(registry.STIX2_OBJ_MAPS[v]['markings'][NewMarking1._type]) + assert NewMarking1._type in registry.STIX2_OBJ_MAPS['2.0']['markings'] + assert 'v20' in str(registry.STIX2_OBJ_MAPS['2.0']['markings'][NewMarking1._type]) diff --git a/stix2/test/v21/test_custom.py b/stix2/test/v21/test_custom.py index f9cb574..36e3548 100644 --- a/stix2/test/v21/test_custom.py +++ b/stix2/test/v21/test_custom.py @@ -1265,9 +1265,8 @@ def test_register_custom_object_with_version(): } cust_obj_1 = stix2.parsing.dict_to_stix2(custom_obj_1, version='2.1') - v = 'v21' - assert cust_obj_1.type in stix2.registry.STIX2_OBJ_MAPS[v]['objects'] + assert cust_obj_1.type in stix2.registry.STIX2_OBJ_MAPS['2.1']['objects'] assert cust_obj_1.spec_version == "2.1" @@ -1295,9 +1294,8 @@ class NewObservable3(object): def test_register_observable(): custom_obs = NewObservable3(property1="Test Observable") - v = 'v21' - assert custom_obs.type in stix2.registry.STIX2_OBJ_MAPS[v]['observables'] + assert custom_obs.type in stix2.registry.STIX2_OBJ_MAPS['2.1']['observables'] def test_register_duplicate_observable(): @@ -1323,10 +1321,9 @@ def test_register_observable_custom_extension(): pass example = NewExtension2(property1="Hi there") - v = 'v21' - assert 'domain-name' in stix2.registry.STIX2_OBJ_MAPS[v]['observables'] - assert example._type in stix2.registry.STIX2_OBJ_MAPS[v]['observable-extensions']['domain-name'] + assert 'domain-name' in stix2.registry.STIX2_OBJ_MAPS['2.1']['observables'] + assert example._type in stix2.registry.STIX2_OBJ_MAPS['2.1']['observable-extensions']['domain-name'] def test_register_duplicate_observable_extension(): diff --git a/stix2/test/v21/test_parsing.py b/stix2/test/v21/test_parsing.py index a68d9fe..f23eb7d 100644 --- a/stix2/test/v21/test_parsing.py +++ b/stix2/test/v21/test_parsing.py @@ -78,10 +78,9 @@ def test_register_marking_with_version(): _properties = OrderedDict() registration._register_marking(NewMarking1, version='2.1') - v = 'v21' - assert NewMarking1._type in registry.STIX2_OBJ_MAPS[v]['markings'] - assert v in str(registry.STIX2_OBJ_MAPS[v]['markings'][NewMarking1._type]) + assert NewMarking1._type in registry.STIX2_OBJ_MAPS['2.1']['markings'] + assert 'v21' in str(registry.STIX2_OBJ_MAPS['2.1']['markings'][NewMarking1._type]) @pytest.mark.xfail(reason="The default version is not 2.1", condition=DEFAULT_VERSION != "2.1") @@ -92,7 +91,6 @@ def test_register_marking_with_no_version(): _properties = OrderedDict() registration._register_marking(NewMarking2) - v = 'v21' - assert NewMarking2._type in registry.STIX2_OBJ_MAPS[v]['markings'] - assert v in str(registry.STIX2_OBJ_MAPS[v]['markings'][NewMarking2._type]) + assert NewMarking2._type in registry.STIX2_OBJ_MAPS['2.1']['markings'] + assert 'v21' in str(registry.STIX2_OBJ_MAPS['2.1']['markings'][NewMarking2._type]) diff --git a/stix2/versioning.py b/stix2/versioning.py index e66f394..d24a179 100644 --- a/stix2/versioning.py +++ b/stix2/versioning.py @@ -75,7 +75,7 @@ def _is_versionable(data): is_versionable = False is_21 = False - stix_vid = None + stix_version = None if isinstance(data, Mapping): @@ -87,8 +87,8 @@ def _is_versionable(data): # (is_21 means 2.1 or later; try not to be 2.1-specific) is_21 = True elif isinstance(data, dict): - stix_vid = stix2.parsing._detect_spec_version(data) - is_21 = stix_vid != "v20" + stix_version = stix2.parsing._detect_spec_version(data) + is_21 = stix_version != "2.0" # Then, determine versionability. @@ -110,7 +110,7 @@ def _is_versionable(data): # registered class, and from that get a more complete picture of its # properties. elif isinstance(data, dict): - class_maps = stix2.registry.STIX2_OBJ_MAPS[stix_vid] + class_maps = stix2.registry.STIX2_OBJ_MAPS[stix_version] obj_type = data["type"] if obj_type in class_maps["objects"]: From db1d0b736bee5c02f4a5c683d46e72b2a8a8a1f7 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Wed, 13 Jan 2021 21:46:32 -0500 Subject: [PATCH 07/20] Remove registry.get_stix2_class_maps(), since now that the class map structure is keyed by normal "X.Y" style versions, the convenience that function provided is no longer necessary. So it no longer makes sense to have the function (at least, not for that reason). Change users of that function to use the STIX2_OBJ_MAPS structure directly. --- stix2/registration.py | 13 ++++--------- stix2/registry.py | 14 -------------- stix2/utils.py | 6 +++--- 3 files changed, 7 insertions(+), 26 deletions(-) diff --git a/stix2/registration.py b/stix2/registration.py index 65d2714..6d426f2 100644 --- a/stix2/registration.py +++ b/stix2/registration.py @@ -68,9 +68,7 @@ def _register_marking(new_marking, version=DEFAULT_VERSION): if not re.match(PREFIX_21_REGEX, prop_name): raise ValueError("Property name '%s' must begin with an alpha character." % prop_name) - class_maps = registry.get_stix2_class_maps(version) - - OBJ_MAP_MARKING = class_maps['markings'] + OBJ_MAP_MARKING = registry.STIX2_OBJ_MAPS[version]['markings'] if mark_type in OBJ_MAP_MARKING.keys(): raise DuplicateRegistrationError("STIX Marking", mark_type) OBJ_MAP_MARKING[mark_type] = new_marking @@ -129,9 +127,7 @@ def _register_observable(new_observable, version=DEFAULT_VERSION): "is not a ListProperty containing ReferenceProperty." % prop_name, ) - class_maps = registry.get_stix2_class_maps(version) - - OBJ_MAP_OBSERVABLE = class_maps['observables'] + OBJ_MAP_OBSERVABLE = registry.STIX2_OBJ_MAPS[version]['observables'] if new_observable._type in OBJ_MAP_OBSERVABLE.keys(): raise DuplicateRegistrationError("Cyber Observable", new_observable._type) OBJ_MAP_OBSERVABLE[new_observable._type] = new_observable @@ -185,9 +181,8 @@ def _register_observable_extension( "created with the @CustomObservable decorator.", ) - class_maps = registry.get_stix2_class_maps(version) - OBJ_MAP_OBSERVABLE = class_maps['observables'] - EXT_MAP = class_maps['observable-extensions'] + OBJ_MAP_OBSERVABLE = registry.STIX2_OBJ_MAPS[version]['observables'] + EXT_MAP = registry.STIX2_OBJ_MAPS[version]['observable-extensions'] try: if ext_type in EXT_MAP[observable_type].keys(): diff --git a/stix2/registry.py b/stix2/registry.py index 3b45e48..3849913 100644 --- a/stix2/registry.py +++ b/stix2/registry.py @@ -42,17 +42,3 @@ def _collect_stix2_mappings(): ver = _stix_vid_to_version(stix_vid) mod = importlib.import_module(name, str(top_level_module.__name__)) STIX2_OBJ_MAPS[ver]['markings'] = mod.OBJ_MAP_MARKING - - -def get_stix2_class_maps(stix_version): - """ - Get the stix2 class mappings for the given STIX version. - - :param stix_version: A STIX version as a string - :return: The class mappings. This will be a dict mapping from some general - category name, e.g. "object" to another mapping from STIX type - to a stix2 class. - """ - cls_maps = STIX2_OBJ_MAPS[stix_version] - - return cls_maps diff --git a/stix2/utils.py b/stix2/utils.py index 34915d9..c44dbe1 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -350,7 +350,7 @@ def is_sdo(value, stix_version=stix2.DEFAULT_VERSION): :return: True if the type of the given value is an SDO type; False if not """ - cls_maps = mappings.get_stix2_class_maps(stix_version) + cls_maps = mappings.STIX2_OBJ_MAPS[stix_version] type_ = _stix_type_of(value) result = type_ in cls_maps["objects"] and type_ not in { "relationship", "sighting", "marking-definition", "bundle", @@ -370,7 +370,7 @@ def is_sco(value, stix_version=stix2.DEFAULT_VERSION): :return: True if the type of the given value is an SCO type; False if not """ - cls_maps = mappings.get_stix2_class_maps(stix_version) + cls_maps = mappings.STIX2_OBJ_MAPS[stix_version] type_ = _stix_type_of(value) result = type_ in cls_maps["observables"] @@ -406,7 +406,7 @@ def is_object(value, stix_version=stix2.DEFAULT_VERSION): :return: True if the type of the given value is a valid STIX type with respect to the given STIX version; False if not """ - cls_maps = mappings.get_stix2_class_maps(stix_version) + cls_maps = mappings.STIX2_OBJ_MAPS[stix_version] type_ = _stix_type_of(value) result = type_ in cls_maps["observables"] or type_ in cls_maps["objects"] From bf284d0a0b80b60add62f8d1d0db687f5b19a635 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Thu, 14 Jan 2021 20:55:38 -0500 Subject: [PATCH 08/20] Fix silly docstring copy-paste typo --- stix2/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stix2/utils.py b/stix2/utils.py index c44dbe1..fdbbf42 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -379,7 +379,7 @@ def is_sco(value, stix_version=stix2.DEFAULT_VERSION): def is_sro(value, stix_version=stix2.DEFAULT_VERSION): """ - Determine whether the given object, type, or ID is/is for an SCO. + Determine whether the given object, type, or ID is/is for an SRO. :param value: A mapping with a "type" property, or a STIX ID or type as a string From f8c86f73525821fd214a34e564c230c3ae0f74ce Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Fri, 15 Jan 2021 19:12:14 -0500 Subject: [PATCH 09/20] Fixups after a rebase. There were several conflict resolutions and I probably forgot some stuff... --- stix2/parsing.py | 2 +- stix2/registration.py | 2 +- stix2/utils.py | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/stix2/parsing.py b/stix2/parsing.py index fa2df71..850e8d7 100644 --- a/stix2/parsing.py +++ b/stix2/parsing.py @@ -71,7 +71,7 @@ def _detect_spec_version(stix_dict): _detect_spec_version(obj) for obj in stix_dict["objects"] ), ) - elif obj_type in registry.STIX2_OBJ_MAPS["v21"]["observables"]: + elif obj_type in registry.STIX2_OBJ_MAPS["2.1"]["observables"]: # Non-bundle object with an ID and without spec_version. Could be a # 2.1 SCO or 2.0 SDO/SRO/marking. Check for 2.1 SCO... v = "2.1" diff --git a/stix2/registration.py b/stix2/registration.py index 6d426f2..4ec019a 100644 --- a/stix2/registration.py +++ b/stix2/registration.py @@ -86,7 +86,7 @@ def _register_observable(new_observable, version=DEFAULT_VERSION): properties = new_observable._properties if not version: - version = stix2.DEFAULT_VERSION + version = DEFAULT_VERSION if version == "2.0": # If using STIX2.0, check properties ending in "_ref/s" are ObjectReferenceProperties diff --git a/stix2/utils.py b/stix2/utils.py index fdbbf42..8cb6291 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -8,7 +8,7 @@ import re import pytz import six -import stix2 +import stix2.version try: import stix2.registry as mappings except ImportError: @@ -340,7 +340,7 @@ def _stix_type_of(value): return type_ -def is_sdo(value, stix_version=stix2.DEFAULT_VERSION): +def is_sdo(value, stix_version=stix2.version.DEFAULT_VERSION): """ Determine whether the given object, type, or ID is/is for an SDO. @@ -360,7 +360,7 @@ def is_sdo(value, stix_version=stix2.DEFAULT_VERSION): return result -def is_sco(value, stix_version=stix2.DEFAULT_VERSION): +def is_sco(value, stix_version=stix2.version.DEFAULT_VERSION): """ Determine whether the given object, type, or ID is/is for an SCO. @@ -377,7 +377,7 @@ def is_sco(value, stix_version=stix2.DEFAULT_VERSION): return result -def is_sro(value, stix_version=stix2.DEFAULT_VERSION): +def is_sro(value, stix_version=stix2.version.DEFAULT_VERSION): """ Determine whether the given object, type, or ID is/is for an SRO. @@ -395,7 +395,7 @@ def is_sro(value, stix_version=stix2.DEFAULT_VERSION): return result -def is_object(value, stix_version=stix2.DEFAULT_VERSION): +def is_object(value, stix_version=stix2.version.DEFAULT_VERSION): """ Determine whether an object, type, or ID is/is for any STIX object. This includes all SDOs, SCOs, meta-objects, and bundle. @@ -413,7 +413,7 @@ def is_object(value, stix_version=stix2.DEFAULT_VERSION): return result -def is_marking(value, stix_version=stix2.DEFAULT_VERSION): +def is_marking(value, stix_version=stix2.version.DEFAULT_VERSION): """Determines whether the given value is/is for a marking definition. :param value: A STIX object, object ID, or type as a string. @@ -437,7 +437,7 @@ class STIXTypeClass(enum.Enum): SRO = 2 -def is_stix_type(value, stix_version=stix2.DEFAULT_VERSION, *types): +def is_stix_type(value, stix_version=stix2.version.DEFAULT_VERSION, *types): """ Determine whether the type of the given value satisfies the given constraints. 'types' must contain STIX types as strings, and/or the From fe2330af076ee9546d1879a22a88e2c0f67c6f63 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Tue, 19 Jan 2021 19:57:05 -0500 Subject: [PATCH 10/20] Improve is_sdo() et al utility functions with respect to dict/mapping values: do a simple verification of the value's STIX version, not just its type. Added a lot more unit tests to test behavior on dicts. To make the implementation work, I had to move the detect_spec_version() function out of the parsing module and into utils. So that required small changes at all its previous call sites. --- stix2/parsing.py | 46 +------ stix2/test/test_spec_version_detect.py | 4 +- stix2/test/test_utils_type_checks.py | 25 ---- stix2/test/v20/test_utils.py | 143 +++++++++++++++++++++ stix2/test/v21/test_utils.py | 151 +++++++++++++++++++--- stix2/utils.py | 165 ++++++++++++++++++++----- stix2/versioning.py | 4 +- 7 files changed, 417 insertions(+), 121 deletions(-) diff --git a/stix2/parsing.py b/stix2/parsing.py index 850e8d7..87085d7 100644 --- a/stix2/parsing.py +++ b/stix2/parsing.py @@ -4,7 +4,7 @@ import copy from . import registry from .exceptions import ParseError -from .utils import _get_dict +from .utils import _get_dict, detect_spec_version def parse(data, allow_custom=False, version=None): @@ -42,46 +42,6 @@ def parse(data, allow_custom=False, version=None): return obj -def _detect_spec_version(stix_dict): - """ - Given a dict representing a STIX object, try to detect what spec version - it is likely to comply with. - - :param stix_dict: A dict with some STIX content. Must at least have a - "type" property. - :return: A STIX version in "X.Y" format - """ - - obj_type = stix_dict["type"] - - if 'spec_version' in stix_dict: - # For STIX 2.0, applies to bundles only. - # For STIX 2.1+, applies to SCOs, SDOs, SROs, and markings only. - v = stix_dict['spec_version'] - elif "id" not in stix_dict: - # Only 2.0 SCOs don't have ID properties - v = "2.0" - elif obj_type == 'bundle': - # Bundle without a spec_version property: must be 2.1. But to - # future-proof, use max version over all contained SCOs, with 2.1 - # minimum. - v = max( - "2.1", - max( - _detect_spec_version(obj) for obj in stix_dict["objects"] - ), - ) - elif obj_type in registry.STIX2_OBJ_MAPS["2.1"]["observables"]: - # Non-bundle object with an ID and without spec_version. Could be a - # 2.1 SCO or 2.0 SDO/SRO/marking. Check for 2.1 SCO... - v = "2.1" - else: - # Not a 2.1 SCO; must be a 2.0 object. - v = "2.0" - - return v - - def dict_to_stix2(stix_dict, allow_custom=False, version=None): """convert dictionary to full python-stix2 object @@ -115,7 +75,7 @@ def dict_to_stix2(stix_dict, allow_custom=False, version=None): raise ParseError("Can't parse object with no 'type' property: %s" % str(stix_dict)) if not version: - version = _detect_spec_version(stix_dict) + version = detect_spec_version(stix_dict) OBJ_MAP = dict( registry.STIX2_OBJ_MAPS[version]['objects'], @@ -165,7 +125,7 @@ def parse_observable(data, _valid_refs=None, allow_custom=False, version=None): obj['_valid_refs'] = _valid_refs or [] if not version: - version = _detect_spec_version(obj) + version = detect_spec_version(obj) try: OBJ_MAP_OBSERVABLE = registry.STIX2_OBJ_MAPS[version]['observables'] diff --git a/stix2/test/test_spec_version_detect.py b/stix2/test/test_spec_version_detect.py index 9196e16..570cc8e 100644 --- a/stix2/test/test_spec_version_detect.py +++ b/stix2/test/test_spec_version_detect.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import pytest -from stix2.parsing import _detect_spec_version +from stix2.utils import detect_spec_version @pytest.mark.parametrize( @@ -207,6 +207,6 @@ from stix2.parsing import _detect_spec_version ], ) def test_spec_version_detect(obj_dict, expected_ver): - detected_ver = _detect_spec_version(obj_dict) + detected_ver = detect_spec_version(obj_dict) assert detected_ver == expected_ver diff --git a/stix2/test/test_utils_type_checks.py b/stix2/test/test_utils_type_checks.py index 31d99eb..7cf23b9 100644 --- a/stix2/test/test_utils_type_checks.py +++ b/stix2/test/test_utils_type_checks.py @@ -30,11 +30,6 @@ def test_is_sdo(type_, stix_version): id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" assert stix2.utils.is_sdo(id_, stix_version) - d = { - "type": type_ - } - assert stix2.utils.is_sdo(d, stix_version) - assert stix2.utils.is_stix_type( type_, stix_version, stix2.utils.STIXTypeClass.SDO ) @@ -97,11 +92,6 @@ def test_is_sco(type_, stix_version): id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" assert stix2.utils.is_sco(id_, stix_version) - d = { - "type": type_ - } - assert stix2.utils.is_sco(d, stix_version) - assert stix2.utils.is_stix_type( type_, stix_version, stix2.utils.STIXTypeClass.SCO ) @@ -147,11 +137,6 @@ def test_is_sro(type_, stix_version): id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" assert stix2.utils.is_sro(id_, stix_version) - d = { - "type": type_ - } - assert stix2.utils.is_sro(d, stix_version) - assert stix2.utils.is_stix_type( type_, stix_version, stix2.utils.STIXTypeClass.SRO ) @@ -191,11 +176,6 @@ def test_is_marking(stix_version): id_ = "marking-definition--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" assert stix2.utils.is_marking(id_, stix_version) - d = { - "type": "marking-definition" - } - assert stix2.utils.is_marking(d, stix_version) - assert stix2.utils.is_stix_type( "marking-definition", stix_version, "marking-definition" ) @@ -244,11 +224,6 @@ def test_is_object(type_, stix_version): id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" assert stix2.utils.is_object(id_, stix_version) - d = { - "type": type_ - } - assert stix2.utils.is_object(d, stix_version) - @pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) def test_is_not_object(stix_version): diff --git a/stix2/test/v20/test_utils.py b/stix2/test/v20/test_utils.py index de66332..b7c91e3 100644 --- a/stix2/test/v20/test_utils.py +++ b/stix2/test/v20/test_utils.py @@ -237,3 +237,146 @@ def test_find_property_index(object, tuple_to_find, expected_index): ) def test_iterate_over_values(dict_value, tuple_to_find, expected_index): assert stix2.serialization._find_property_in_seq(dict_value.values(), *tuple_to_find) == expected_index + + +@pytest.mark.parametrize( + "type_", [ + "attack-pattern", + "campaign", + "course-of-action", + "identity", + "indicator", + "intrusion-set", + "malware", + "observed-data", + "report", + "threat-actor", + "tool", + "vulnerability" + ] +) +def test_is_sdo_dict(type_): + d = { + "type": type_ + } + assert stix2.utils.is_sdo(d, "2.0") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "software", "spec_version": "2.1"}, + {"type": "software"}, + {"type": "identity", "spec_version": "2.1"}, + {"type": "marking-definition", "spec_version": "2.1"}, + {"type": "marking-definition"}, + {"type": "bundle", "spec_version": "2.1"}, + {"type": "bundle"}, + {"type": "language-content", "spec_version": "2.1"}, + {"type": "language-content"}, + {"type": "relationship", "spec_version": "2.1"}, + {"type": "relationship"}, + {"type": "foo", "spec_version": "2.1"}, + {"type": "foo"}, + ] +) +def test_is_not_sdo_dict(dict_): + assert not stix2.utils.is_sdo(dict_, "2.0") + + +def test_is_sco_dict(): + d = { + "type": "file" + } + + assert stix2.utils.is_sco(d, "2.0") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "identity"}, + {"type": "identity", "spec_version": "2.1"}, + {"type": "software", "spec_version": "2.1"}, + {"type": "marking-definition", "spec_version": "2.1"}, + {"type": "marking-definition"}, + {"type": "bundle", "spec_version": "2.1"}, + {"type": "bundle"}, + {"type": "language-content", "spec_version": "2.1"}, + {"type": "language-content"}, + {"type": "relationship", "spec_version": "2.1"}, + {"type": "relationship"}, + {"type": "foo", "spec_version": "2.1"}, + {"type": "foo"}, + ] +) +def test_is_not_sco_dict(dict_): + assert not stix2.utils.is_sco(dict_, "2.0") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "relationship"}, + {"type": "sighting"}, + ] +) +def test_is_sro_dict(dict_): + assert stix2.utils.is_sro(dict_, "2.0") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "identity", "spec_version": "2.1"}, + {"type": "identity"}, + {"type": "software", "spec_version": "2.1"}, + {"type": "software"}, + {"type": "marking-definition", "spec_version": "2.1"}, + {"type": "marking-definition"}, + {"type": "bundle", "spec_version": "2.1"}, + {"type": "bundle"}, + {"type": "language-content", "spec_version": "2.1"}, + {"type": "language-content"}, + {"type": "relationship", "spec_version": "2.1"}, + {"type": "sighting", "spec_version": "2.1"}, + {"type": "foo", "spec_version": "2.1"}, + {"type": "foo"}, + ] +) +def test_is_not_sro_dict(dict_): + assert not stix2.utils.is_sro(dict_, "2.0") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "identity"}, + {"type": "software"}, + {"type": "marking-definition"}, + { + "type": "bundle", + "id": "bundle--8f431680-6278-4767-ba43-5edb682d7086", + "spec_version": "2.0", + "objects": [ + {"type": "identity"}, + {"type": "software"}, + {"type": "marking-definition"}, + ] + }, + ] +) +def test_is_object_dict(dict_): + assert stix2.utils.is_object(dict_, "2.0") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "identity", "spec_version": "2.1"}, + {"type": "software", "spec_version": "2.1"}, + {"type": "marking-definition", "spec_version": "2.1"}, + {"type": "bundle", "spec_version": "2.1"}, + {"type": "language-content", "spec_version": "2.1"}, + {"type": "relationship", "spec_version": "2.1"}, + {"type": "sighting", "spec_version": "2.1"}, + {"type": "foo", "spec_version": "2.1"}, + {"type": "foo"}, + ] +) +def test_is_not_object_dict(dict_): + assert not stix2.utils.is_object(dict_, "2.0") diff --git a/stix2/test/v21/test_utils.py b/stix2/test/v21/test_utils.py index 940d1db..71debda 100644 --- a/stix2/test/v21/test_utils.py +++ b/stix2/test/v21/test_utils.py @@ -243,9 +243,22 @@ def test_iterate_over_values(dict_value, tuple_to_find, expected_index): assert stix2.serialization._find_property_in_seq(dict_value.values(), *tuple_to_find) == expected_index -# Only 2.1-specific types/behaviors tested here. @pytest.mark.parametrize( "type_", [ + "attack-pattern", + "campaign", + "course-of-action", + "identity", + "indicator", + "intrusion-set", + "malware", + "observed-data", + "report", + "threat-actor", + "tool", + "vulnerability", + + # New in 2.1 "grouping", "infrastructure", "location", @@ -254,27 +267,127 @@ def test_iterate_over_values(dict_value, tuple_to_find, expected_index): "opinion" ] ) -def test_is_sdo(type_): - assert stix2.utils.is_sdo(type_, "2.1") - - id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" - assert stix2.utils.is_sdo(id_, "2.1") - +def test_is_sdo_dict(type_): d = { - "type": type_ + "type": type_, + "spec_version": "2.1" } assert stix2.utils.is_sdo(d, "2.1") - assert stix2.utils.is_stix_type( - type_, "2.1", stix2.utils.STIXTypeClass.SDO - ) + +@pytest.mark.parametrize( + "dict_", [ + {"type": "software", "spec_version": "2.1"}, + {"type": "software"}, + {"type": "identity"}, + {"type": "marking-definition", "spec_version": "2.1"}, + {"type": "marking-definition"}, + {"type": "bundle", "spec_version": "2.1"}, + {"type": "bundle"}, + {"type": "language-content", "spec_version": "2.1"}, + {"type": "language-content"}, + {"type": "relationship", "spec_version": "2.1"}, + {"type": "relationship"}, + {"type": "foo", "spec_version": "2.1"}, + ] +) +def test_is_not_sdo_dict(dict_): + assert not stix2.utils.is_sdo(dict_, "2.1") -def test_type_checks_language_content(): - assert stix2.utils.is_object("language-content", "2.1") - assert not stix2.utils.is_sdo("language-content", "2.1") - assert not stix2.utils.is_sco("language-content", "2.1") - assert not stix2.utils.is_sro("language-content", "2.1") - assert stix2.utils.is_stix_type( - "language-content", "2.1", "language-content" - ) +def test_is_sco_dict(): + d = { + "type": "file", + "spec_version": "2.1" + } + + assert stix2.utils.is_sco(d, "2.1") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "identity"}, + {"type": "identity", "spec_version": "2.1"}, + {"type": "software"}, + {"type": "marking-definition", "spec_version": "2.1"}, + {"type": "marking-definition"}, + {"type": "bundle", "spec_version": "2.1"}, + {"type": "bundle"}, + {"type": "language-content", "spec_version": "2.1"}, + {"type": "language-content"}, + {"type": "relationship", "spec_version": "2.1"}, + {"type": "relationship"}, + {"type": "foo", "spec_version": "2.1"}, + ] +) +def test_is_not_sco_dict(dict_): + assert not stix2.utils.is_sco(dict_, "2.1") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "relationship", "spec_version": "2.1"}, + {"type": "sighting", "spec_version": "2.1"}, + ] +) +def test_is_sro_dict(dict_): + assert stix2.utils.is_sro(dict_, "2.1") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "identity", "spec_version": "2.1"}, + {"type": "identity"}, + {"type": "software", "spec_version": "2.1"}, + {"type": "software"}, + {"type": "marking-definition", "spec_version": "2.1"}, + {"type": "marking-definition"}, + {"type": "bundle", "spec_version": "2.1"}, + {"type": "bundle"}, + {"type": "language-content", "spec_version": "2.1"}, + {"type": "language-content"}, + {"type": "relationship"}, + {"type": "sighting"}, + {"type": "foo", "spec_version": "2.1"}, + ] +) +def test_is_not_sro_dict(dict_): + assert not stix2.utils.is_sro(dict_, "2.1") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "identity", "spec_version": "2.1"}, + {"type": "software", "spec_version": "2.1"}, + {"type": "marking-definition", "spec_version": "2.1"}, + {"type": "language-content", "spec_version": "2.1"}, + { + "type": "bundle", + "id": "bundle--8f431680-6278-4767-ba43-5edb682d7086", + "objects": [ + {"type": "identity", "spec_version": "2.1"}, + {"type": "software", "spec_version": "2.1"}, + {"type": "marking-definition", "spec_version": "2.1"}, + {"type": "language-content", "spec_version": "2.1"}, + ] + }, + ] +) +def test_is_object_dict(dict_): + assert stix2.utils.is_object(dict_, "2.1") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "identity"}, + {"type": "software"}, + {"type": "marking-definition"}, + {"type": "bundle"}, + {"type": "language-content"}, + {"type": "relationship"}, + {"type": "sighting"}, + {"type": "foo"}, + ] +) +def test_is_not_object_dict(dict_): + assert not stix2.utils.is_object(dict_, "2.1") diff --git a/stix2/utils.py b/stix2/utils.py index 8cb6291..a7c2b73 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -1,5 +1,6 @@ """Utility functions and classes for the STIX2 library.""" +import collections.abc import datetime as dt import enum import json @@ -318,6 +319,46 @@ def get_type_from_id(stix_id): return stix_id.split('--', 1)[0] +def detect_spec_version(stix_dict): + """ + Given a dict representing a STIX object, try to detect what spec version + it is likely to comply with. + + :param stix_dict: A dict with some STIX content. Must at least have a + "type" property. + :return: A STIX version in "X.Y" format + """ + + obj_type = stix_dict["type"] + + if 'spec_version' in stix_dict: + # For STIX 2.0, applies to bundles only. + # For STIX 2.1+, applies to SCOs, SDOs, SROs, and markings only. + v = stix_dict['spec_version'] + elif "id" not in stix_dict: + # Only 2.0 SCOs don't have ID properties + v = "2.0" + elif obj_type == 'bundle': + # Bundle without a spec_version property: must be 2.1. But to + # future-proof, use max version over all contained SCOs, with 2.1 + # minimum. + v = max( + "2.1", + max( + detect_spec_version(obj) for obj in stix_dict["objects"] + ), + ) + elif obj_type in mappings.STIX2_OBJ_MAPS["2.1"]["observables"]: + # Non-bundle object with an ID and without spec_version. Could be a + # 2.1 SCO or 2.0 SDO/SRO/marking. Check for 2.1 SCO... + v = "2.1" + else: + # Not a 2.1 SCO; must be a 2.0 object. + v = "2.0" + + return v + + def _stix_type_of(value): """ Get a STIX type from the given value: if a STIX ID is passed, the type @@ -342,55 +383,93 @@ def _stix_type_of(value): def is_sdo(value, stix_version=stix2.version.DEFAULT_VERSION): """ - Determine whether the given object, type, or ID is/is for an SDO. + Determine whether the given object, type, or ID is/is for an SDO of the + given STIX version. If value is a type or ID, this just checks whether + the type was registered as an SDO in the given STIX version. If a mapping, + *simple* STIX version inference is additionally done on the value, and the + result is checked against stix_version. It does not attempt to fully + validate the value. :param value: A mapping with a "type" property, or a STIX ID or type as a string :param stix_version: A STIX version as a string - :return: True if the type of the given value is an SDO type; False - if not + :return: True if the type of the given value is an SDO type of the given + version; False if not """ - cls_maps = mappings.STIX2_OBJ_MAPS[stix_version] - type_ = _stix_type_of(value) - result = type_ in cls_maps["objects"] and type_ not in { - "relationship", "sighting", "marking-definition", "bundle", - "language-content" - } + + result = True + if isinstance(value, collections.abc.Mapping): + value_stix_version = detect_spec_version(value) + if value_stix_version != stix_version: + result = False + + if result: + cls_maps = mappings.STIX2_OBJ_MAPS[stix_version] + type_ = _stix_type_of(value) + result = type_ in cls_maps["objects"] and type_ not in { + "relationship", "sighting", "marking-definition", "bundle", + "language-content" + } return result def is_sco(value, stix_version=stix2.version.DEFAULT_VERSION): """ - Determine whether the given object, type, or ID is/is for an SCO. + Determine whether the given object, type, or ID is/is for an SCO of the + given STIX version. If value is a type or ID, this just checks whether + the type was registered as an SCO in the given STIX version. If a mapping, + *simple* STIX version inference is additionally done on the value, and the + result is checked against stix_version. It does not attempt to fully + validate the value. :param value: A mapping with a "type" property, or a STIX ID or type as a string :param stix_version: A STIX version as a string - :return: True if the type of the given value is an SCO type; False - if not + :return: True if the type of the given value is an SCO type of the given + version; False if not """ - cls_maps = mappings.STIX2_OBJ_MAPS[stix_version] - type_ = _stix_type_of(value) - result = type_ in cls_maps["observables"] + + result = True + if isinstance(value, collections.abc.Mapping): + value_stix_version = detect_spec_version(value) + if value_stix_version != stix_version: + result = False + + if result: + cls_maps = mappings.STIX2_OBJ_MAPS[stix_version] + type_ = _stix_type_of(value) + result = type_ in cls_maps["observables"] return result def is_sro(value, stix_version=stix2.version.DEFAULT_VERSION): """ - Determine whether the given object, type, or ID is/is for an SRO. + Determine whether the given object, type, or ID is/is for an SRO of the + given STIX version. If value is a type or ID, this just checks whether + the type was registered as an SRO in the given STIX version. If a mapping, + *simple* STIX version inference is additionally done on the value, and the + result is checked against stix_version. It does not attempt to fully + validate the value. :param value: A mapping with a "type" property, or a STIX ID or type as a string :param stix_version: A STIX version as a string - :return: True if the type of the given value is an SRO type; False - if not + :return: True if the type of the given value is an SRO type of the given + version; False if not """ - # No STIX version dependence here yet... - type_ = _stix_type_of(value) - result = type_ in ("sighting", "relationship") + result = True + if isinstance(value, collections.abc.Mapping): + value_stix_version = detect_spec_version(value) + if value_stix_version != stix_version: + result = False + + if result: + # No need to check registration in this case + type_ = _stix_type_of(value) + result = type_ in ("sighting", "relationship") return result @@ -398,7 +477,11 @@ def is_sro(value, stix_version=stix2.version.DEFAULT_VERSION): def is_object(value, stix_version=stix2.version.DEFAULT_VERSION): """ Determine whether an object, type, or ID is/is for any STIX object. This - includes all SDOs, SCOs, meta-objects, and bundle. + includes all SDOs, SCOs, meta-objects, and bundle. If value is a type or + ID, this just checks whether the type was registered in the given STIX + version. If a mapping, *simple* STIX version inference is additionally + done on the value, and the result is checked against stix_version. It does + not attempt to fully validate the value. :param value: A mapping with a "type" property, or a STIX ID or type as a string @@ -406,24 +489,46 @@ def is_object(value, stix_version=stix2.version.DEFAULT_VERSION): :return: True if the type of the given value is a valid STIX type with respect to the given STIX version; False if not """ - cls_maps = mappings.STIX2_OBJ_MAPS[stix_version] - type_ = _stix_type_of(value) - result = type_ in cls_maps["observables"] or type_ in cls_maps["objects"] + + result = True + if isinstance(value, collections.abc.Mapping): + value_stix_version = detect_spec_version(value) + if value_stix_version != stix_version: + result = False + + if result: + cls_maps = mappings.STIX2_OBJ_MAPS[stix_version] + type_ = _stix_type_of(value) + result = type_ in cls_maps["observables"] \ + or type_ in cls_maps["objects"] return result def is_marking(value, stix_version=stix2.version.DEFAULT_VERSION): - """Determines whether the given value is/is for a marking definition. + """ + Determine whether the given object, type, or ID is/is for an marking + definition of the given STIX version. If value is a type or ID, this just + checks whether the type was registered as an SDO in the given STIX version. + If a mapping, *simple* STIX version inference is additionally done on the + value, and the result is checked against stix_version. It does not attempt + to fully validate the value. :param value: A STIX object, object ID, or type as a string. :param stix_version: A STIX version as a string :return: True if the value is/is for a marking definition, False otherwise. """ - # No STIX version dependence here yet... - type_ = _stix_type_of(value) - result = type_ == "marking-definition" + result = True + if isinstance(value, collections.abc.Mapping): + value_stix_version = detect_spec_version(value) + if value_stix_version != stix_version: + result = False + + if result: + # No need to check registration in this case + type_ = _stix_type_of(value) + result = type_ == "marking-definition" return result @@ -464,7 +569,7 @@ def is_stix_type(value, stix_version=stix2.version.DEFAULT_VERSION, *types): # Assume a string STIX type is given instead of a class enum, # and just check for exact match. obj_type = _stix_type_of(value) - result = is_object(obj_type, stix_version) and obj_type == type_ + result = obj_type == type_ and is_object(value, stix_version) if result: break diff --git a/stix2/versioning.py b/stix2/versioning.py index d24a179..d9f7605 100644 --- a/stix2/versioning.py +++ b/stix2/versioning.py @@ -10,7 +10,7 @@ from six.moves.collections_abc import Mapping import stix2.base import stix2.registry -from stix2.utils import get_timestamp, parse_into_datetime +from stix2.utils import get_timestamp, parse_into_datetime, detect_spec_version import stix2.v20 from .exceptions import ( @@ -87,7 +87,7 @@ def _is_versionable(data): # (is_21 means 2.1 or later; try not to be 2.1-specific) is_21 = True elif isinstance(data, dict): - stix_version = stix2.parsing._detect_spec_version(data) + stix_version = detect_spec_version(data) is_21 = stix_version != "2.0" # Then, determine versionability. From 473e7d0068b016cdaf98cb6b6a9c711ff9c85451 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Tue, 19 Jan 2021 23:08:55 -0500 Subject: [PATCH 11/20] Change versioning module to use some of the is_* utility functions. Changed some ">= 2.1" stix version semantics to be "== 2.1", because we don't have any version >= 2.1, so they are currently equivalent, and the is_*() functions don't support STIX version ranges. They only support exact versions. We can look at this again if a newer STIX version ever emerges. Also added a class_for_type() function to the registry module, which was useful for the versioning module changes described above. I thought that function would be helpful in the parsing module, to simplify code there, so I changed that module a bit to use it. --- stix2/parsing.py | 20 ++++++++------------ stix2/registry.py | 36 ++++++++++++++++++++++++++++++++++++ stix2/versioning.py | 41 ++++++++++++++++++++++++----------------- 3 files changed, 68 insertions(+), 29 deletions(-) diff --git a/stix2/parsing.py b/stix2/parsing.py index 87085d7..e13ff67 100644 --- a/stix2/parsing.py +++ b/stix2/parsing.py @@ -77,19 +77,16 @@ def dict_to_stix2(stix_dict, allow_custom=False, version=None): if not version: version = detect_spec_version(stix_dict) - OBJ_MAP = dict( - registry.STIX2_OBJ_MAPS[version]['objects'], - **registry.STIX2_OBJ_MAPS[version]['observables'] - ) + obj_type = stix_dict["type"] + obj_class = registry.class_for_type(obj_type, version, "objects") \ + or registry.class_for_type(obj_type, version, "observables") - try: - obj_class = OBJ_MAP[stix_dict['type']] - except KeyError: + if not obj_class: if allow_custom: # flag allows for unknown custom objects too, but will not # be parsed into STIX object, returned as is return stix_dict - raise ParseError("Can't parse unknown object type '%s'! For custom types, use the CustomObject decorator." % stix_dict['type']) + raise ParseError("Can't parse unknown object type '%s'! For custom types, use the CustomObject decorator." % obj_type) return obj_class(allow_custom=allow_custom, **stix_dict) @@ -127,10 +124,9 @@ def parse_observable(data, _valid_refs=None, allow_custom=False, version=None): if not version: version = detect_spec_version(obj) - try: - OBJ_MAP_OBSERVABLE = registry.STIX2_OBJ_MAPS[version]['observables'] - obj_class = OBJ_MAP_OBSERVABLE[obj['type']] - except KeyError: + obj_type = obj["type"] + obj_class = registry.class_for_type(obj_type, version, "observables") + if not obj_class: if allow_custom: # flag allows for unknown custom objects too, but will not # be parsed into STIX observable object, just returned as is diff --git a/stix2/registry.py b/stix2/registry.py index 3849913..2825e68 100644 --- a/stix2/registry.py +++ b/stix2/registry.py @@ -42,3 +42,39 @@ def _collect_stix2_mappings(): ver = _stix_vid_to_version(stix_vid) mod = importlib.import_module(name, str(top_level_module.__name__)) STIX2_OBJ_MAPS[ver]['markings'] = mod.OBJ_MAP_MARKING + + +def class_for_type(stix_type, stix_version, category=None): + """ + Get the registered class which implements a particular STIX type for a + particular STIX version. + + :param stix_type: A STIX type as a string + :param stix_version: A STIX version as a string, e.g. "2.1" + :param category: An optional "category" value, which is just used directly + as a second key after the STIX version, and depends on how the types + are internally categorized. This would be useful if the same STIX type + is used to mean two different things within the same STIX version. So + it's unlikely to be necessary. Pass None to just search all the + categories and return the first class found. + :return: A registered python class which implements the given STIX type, or + None if one is not found. + """ + cls = None + + cat_map = STIX2_OBJ_MAPS.get(stix_version) + if cat_map: + if category: + class_map = cat_map.get(category) + if class_map: + cls = class_map.get(stix_type) + else: + cls = cat_map["objects"].get(stix_type) \ + or cat_map["observables"].get(stix_type) \ + or cat_map["markings"].get(stix_type) + + # Left "observable-extensions" out; it has a different + # substructure. A version->category->type lookup would result + # in another map, not a class. So it doesn't fit the pattern. + + return cls diff --git a/stix2/versioning.py b/stix2/versioning.py index d9f7605..7e1336a 100644 --- a/stix2/versioning.py +++ b/stix2/versioning.py @@ -10,7 +10,10 @@ from six.moves.collections_abc import Mapping import stix2.base import stix2.registry -from stix2.utils import get_timestamp, parse_into_datetime, detect_spec_version +from stix2.utils import ( + get_timestamp, parse_into_datetime, detect_spec_version, + is_sdo, is_sro, is_sco +) import stix2.v20 from .exceptions import ( @@ -74,7 +77,6 @@ def _is_versionable(data): """ is_versionable = False - is_21 = False stix_version = None if isinstance(data, Mapping): @@ -82,13 +84,12 @@ def _is_versionable(data): # First, determine spec version. It's easy for our stix2 objects; more # work for dicts. is_21 = False - if isinstance(data, stix2.base._STIXBase) and \ - not isinstance(data, stix2.v20._STIXBase20): - # (is_21 means 2.1 or later; try not to be 2.1-specific) - is_21 = True + if isinstance(data, stix2.v20._STIXBase20): + stix_version = "2.0" + elif isinstance(data, stix2.v21._STIXBase21): + stix_version = "2.1" elif isinstance(data, dict): stix_version = detect_spec_version(data) - is_21 = stix_version != "2.0" # Then, determine versionability. @@ -110,22 +111,23 @@ def _is_versionable(data): # registered class, and from that get a more complete picture of its # properties. elif isinstance(data, dict): - class_maps = stix2.registry.STIX2_OBJ_MAPS[stix_version] obj_type = data["type"] - if obj_type in class_maps["objects"]: + if is_sdo(obj_type, stix_version) or is_sro(obj_type, stix_version): # Should we bother checking properties for SDOs/SROs? # They were designed to be versionable. is_versionable = True - elif obj_type in class_maps["observables"]: + elif is_sco(obj_type, stix_version): # but do check SCOs - cls = class_maps["observables"][obj_type] + cls = stix2.registry.class_for_type( + obj_type, stix_version, "observables" + ) is_versionable = _VERSIONING_PROPERTIES.issubset( cls._properties, ) - return is_versionable, is_21 + return is_versionable, stix_version def new_version(data, allow_custom=None, **kwargs): @@ -144,7 +146,7 @@ def new_version(data, allow_custom=None, **kwargs): :return: The new object. """ - is_versionable, is_21 = _is_versionable(data) + is_versionable, stix_version = _is_versionable(data) if not is_versionable: raise ValueError( @@ -165,10 +167,13 @@ def new_version(data, allow_custom=None, **kwargs): # probably were). That would imply an ID change, which is not allowed # across versions. sco_locked_props = [] - if is_21 and isinstance(data, stix2.base._Observable): + if is_sco(data, "2.1"): + cls = stix2.registry.class_for_type( + data["type"], stix_version, "observables" + ) uuid_ = uuid.UUID(data["id"][-36:]) if uuid_.variant == uuid.RFC_4122 and uuid_.version == 5: - sco_locked_props = data._id_contributing_properties + sco_locked_props = cls._id_contributing_properties unchangable_properties = set() for prop in itertools.chain(STIX_UNMOD_PROPERTIES, sco_locked_props): @@ -179,7 +184,7 @@ def new_version(data, allow_custom=None, **kwargs): # Different versioning precision rules in STIX 2.0 vs 2.1, so we need # to know which rules to apply. - precision_constraint = "min" if is_21 else "exact" + precision_constraint = "min" if stix_version == "2.1" else "exact" cls = type(data) if 'modified' not in kwargs: @@ -189,7 +194,9 @@ def new_version(data, allow_custom=None, **kwargs): ) new_modified = get_timestamp() - new_modified = _fudge_modified(old_modified, new_modified, is_21) + new_modified = _fudge_modified( + old_modified, new_modified, stix_version == "2.1" + ) kwargs['modified'] = new_modified From 5aadf1ae91e90d1896a351d0aa6c169e5dc1c7bc Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Wed, 20 Jan 2021 19:16:22 -0500 Subject: [PATCH 12/20] Add some unit tests for attempting to change ID contributing properties of a 2.1 SCO with UUIDv5 ID, when creating a new version. --- stix2/test/v21/test_versioning.py | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/stix2/test/v21/test_versioning.py b/stix2/test/v21/test_versioning.py index f10877f..efcf28f 100644 --- a/stix2/test/v21/test_versioning.py +++ b/stix2/test/v21/test_versioning.py @@ -346,6 +346,38 @@ def test_version_sco_with_custom(): assert revoked_obj.revoked +def test_version_sco_id_contributing_properties(): + file_sco_obj = stix2.v21.File( + name="data.txt", + created="1973-11-23T02:31:37Z", + modified="1991-05-13T19:24:57Z", + revoked=False, + allow_custom=True, + ) + + with pytest.raises(stix2.exceptions.UnmodifiablePropertyError) as e: + stix2.versioning.new_version(file_sco_obj, name="foo.dat") + + assert e.value.unchangable_properties == {"name"} + + +def test_version_sco_id_contributing_properties_dict(): + file_sco_dict = { + "type": "file", + "id": "file--c27c572c-2e17-5ce1-817e-67bb97629a56", + "spec_version": "2.1", + "name": "data.txt", + "created": "1973-11-23T02:31:37Z", + "modified": "1991-05-13T19:24:57Z", + "revoked": False + } + + with pytest.raises(stix2.exceptions.UnmodifiablePropertyError) as e: + stix2.versioning.new_version(file_sco_dict, name="foo.dat") + + assert e.value.unchangable_properties == {"name"} + + def test_version_disable_custom(): m = stix2.v21.Malware( name="foo", description="Steals your identity!", is_family=False, From 92a478b39b25f6cd6c9cbd14c597aa75325be1bb Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Wed, 20 Jan 2021 19:42:06 -0500 Subject: [PATCH 13/20] A minor revision to stix2.versioning: it's silly to look up a class in the registry when you have an instance of one of those classes. Because in that case, you can just get the class of the instance and not deal with the registry at all. --- stix2/versioning.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/stix2/versioning.py b/stix2/versioning.py index 7e1336a..336ea4a 100644 --- a/stix2/versioning.py +++ b/stix2/versioning.py @@ -168,11 +168,15 @@ def new_version(data, allow_custom=None, **kwargs): # across versions. sco_locked_props = [] if is_sco(data, "2.1"): - cls = stix2.registry.class_for_type( - data["type"], stix_version, "observables" - ) uuid_ = uuid.UUID(data["id"][-36:]) if uuid_.variant == uuid.RFC_4122 and uuid_.version == 5: + if isinstance(data, stix2.base._Observable): + cls = data.__class__ + else: + cls = stix2.registry.class_for_type( + data["type"], stix_version, "observables" + ) + sco_locked_props = cls._id_contributing_properties unchangable_properties = set() From 38067a6ec77fde13400c7dc737f7b63f6ae9813d Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Wed, 20 Jan 2021 20:49:01 -0500 Subject: [PATCH 14/20] pre-commit stylistic fixes --- stix2/registry.py | 6 +-- stix2/test/test_utils_type_checks.py | 70 ++++++++++++++-------------- stix2/test/v20/test_utils.py | 22 ++++----- stix2/test/v21/test_utils.py | 22 ++++----- stix2/test/v21/test_versioning.py | 2 +- stix2/utils.py | 5 +- stix2/versioning.py | 11 ++--- 7 files changed, 69 insertions(+), 69 deletions(-) diff --git a/stix2/registry.py b/stix2/registry.py index 2825e68..3dcc3a5 100644 --- a/stix2/registry.py +++ b/stix2/registry.py @@ -73,8 +73,8 @@ def class_for_type(stix_type, stix_version, category=None): or cat_map["observables"].get(stix_type) \ or cat_map["markings"].get(stix_type) - # Left "observable-extensions" out; it has a different - # substructure. A version->category->type lookup would result - # in another map, not a class. So it doesn't fit the pattern. + # Left "observable-extensions" out; it has a different + # substructure. A version->category->type lookup would result + # in another map, not a class. So it doesn't fit the pattern. return cls diff --git a/stix2/test/test_utils_type_checks.py b/stix2/test/test_utils_type_checks.py index 7cf23b9..2144653 100644 --- a/stix2/test/test_utils_type_checks.py +++ b/stix2/test/test_utils_type_checks.py @@ -1,9 +1,9 @@ import pytest + import stix2.utils - ### -### Tests using types/behaviors common to STIX 2.0 and 2.1. +# Tests using types/behaviors common to STIX 2.0 and 2.1. ### @@ -21,8 +21,8 @@ import stix2.utils "report", "threat-actor", "tool", - "vulnerability" - ] + "vulnerability", + ], ) def test_is_sdo(type_, stix_version): assert stix2.utils.is_sdo(type_, stix_version) @@ -31,7 +31,7 @@ def test_is_sdo(type_, stix_version): assert stix2.utils.is_sdo(id_, stix_version) assert stix2.utils.is_stix_type( - type_, stix_version, stix2.utils.STIXTypeClass.SDO + type_, stix_version, stix2.utils.STIXTypeClass.SDO, ) @@ -44,8 +44,8 @@ def test_is_sdo(type_, stix_version): "bundle", "language-content", "ipv4-addr", - "foo" - ] + "foo", + ], ) def test_is_not_sdo(type_, stix_version): assert not stix2.utils.is_sdo(type_, stix_version) @@ -54,12 +54,12 @@ def test_is_not_sdo(type_, stix_version): assert not stix2.utils.is_sdo(id_, stix_version) d = { - "type": type_ + "type": type_, } assert not stix2.utils.is_sdo(d, stix_version) assert not stix2.utils.is_stix_type( - type_, stix_version, stix2.utils.STIXTypeClass.SDO + type_, stix_version, stix2.utils.STIXTypeClass.SDO, ) @@ -83,8 +83,8 @@ def test_is_not_sdo(type_, stix_version): "url", "user-account", "windows-registry-key", - "x509-certificate" - ] + "x509-certificate", + ], ) def test_is_sco(type_, stix_version): assert stix2.utils.is_sco(type_, stix_version) @@ -93,7 +93,7 @@ def test_is_sco(type_, stix_version): assert stix2.utils.is_sco(id_, stix_version) assert stix2.utils.is_stix_type( - type_, stix_version, stix2.utils.STIXTypeClass.SCO + type_, stix_version, stix2.utils.STIXTypeClass.SCO, ) @@ -105,8 +105,8 @@ def test_is_sco(type_, stix_version): "marking-definition", "bundle", "language-content", - "foo" - ] + "foo", + ], ) def test_is_not_sco(type_, stix_version): assert not stix2.utils.is_sco(type_, stix_version) @@ -115,12 +115,12 @@ def test_is_not_sco(type_, stix_version): assert not stix2.utils.is_sco(id_, stix_version) d = { - "type": type_ + "type": type_, } assert not stix2.utils.is_sco(d, stix_version) assert not stix2.utils.is_stix_type( - type_, stix_version, stix2.utils.STIXTypeClass.SCO + type_, stix_version, stix2.utils.STIXTypeClass.SCO, ) @@ -128,8 +128,8 @@ def test_is_not_sco(type_, stix_version): @pytest.mark.parametrize( "type_", [ "relationship", - "sighting" - ] + "sighting", + ], ) def test_is_sro(type_, stix_version): assert stix2.utils.is_sro(type_, stix_version) @@ -138,7 +138,7 @@ def test_is_sro(type_, stix_version): assert stix2.utils.is_sro(id_, stix_version) assert stix2.utils.is_stix_type( - type_, stix_version, stix2.utils.STIXTypeClass.SRO + type_, stix_version, stix2.utils.STIXTypeClass.SRO, ) @@ -150,8 +150,8 @@ def test_is_sro(type_, stix_version): "bundle", "language-content", "ipv4-addr", - "foo" - ] + "foo", + ], ) def test_is_not_sro(type_, stix_version): assert not stix2.utils.is_sro(type_, stix_version) @@ -160,12 +160,12 @@ def test_is_not_sro(type_, stix_version): assert not stix2.utils.is_sro(id_, stix_version) d = { - "type": type_ + "type": type_, } assert not stix2.utils.is_sro(d, stix_version) assert not stix2.utils.is_stix_type( - type_, stix_version, stix2.utils.STIXTypeClass.SRO + type_, stix_version, stix2.utils.STIXTypeClass.SRO, ) @@ -177,7 +177,7 @@ def test_is_marking(stix_version): assert stix2.utils.is_marking(id_, stix_version) assert stix2.utils.is_stix_type( - "marking-definition", stix_version, "marking-definition" + "marking-definition", stix_version, "marking-definition", ) @@ -188,8 +188,8 @@ def test_is_marking(stix_version): "bundle", "language-content", "ipv4-addr", - "foo" - ] + "foo", + ], ) def test_is_not_marking(type_, stix_version): assert not stix2.utils.is_marking(type_, stix_version) @@ -198,12 +198,12 @@ def test_is_not_marking(type_, stix_version): assert not stix2.utils.is_marking(id_, stix_version) d = { - "type": type_ + "type": type_, } assert not stix2.utils.is_marking(d, stix_version) assert not stix2.utils.is_stix_type( - type_, stix_version, "marking-definition" + type_, stix_version, "marking-definition", ) @@ -215,8 +215,8 @@ def test_is_not_marking(type_, stix_version): "sighting", "marking-definition", "bundle", - "ipv4-addr" - ] + "ipv4-addr", + ], ) def test_is_object(type_, stix_version): assert stix2.utils.is_object(type_, stix_version) @@ -233,7 +233,7 @@ def test_is_not_object(stix_version): assert not stix2.utils.is_object(id_, stix_version) d = { - "type": "foo" + "type": "foo", } assert not stix2.utils.is_object(d, stix_version) @@ -242,21 +242,21 @@ def test_is_not_object(stix_version): def test_is_stix_type(stix_version): assert not stix2.utils.is_stix_type( - "foo", stix_version, stix2.utils.STIXTypeClass.SDO, "foo" + "foo", stix_version, stix2.utils.STIXTypeClass.SDO, "foo", ) assert stix2.utils.is_stix_type( - "bundle", stix_version, "foo", "bundle" + "bundle", stix_version, "foo", "bundle", ) assert stix2.utils.is_stix_type( "identity", stix_version, stix2.utils.STIXTypeClass.SDO, - stix2.utils.STIXTypeClass.SRO + stix2.utils.STIXTypeClass.SRO, ) assert stix2.utils.is_stix_type( "software", stix_version, stix2.utils.STIXTypeClass.SDO, - stix2.utils.STIXTypeClass.SCO + stix2.utils.STIXTypeClass.SCO, ) diff --git a/stix2/test/v20/test_utils.py b/stix2/test/v20/test_utils.py index b7c91e3..0443933 100644 --- a/stix2/test/v20/test_utils.py +++ b/stix2/test/v20/test_utils.py @@ -252,12 +252,12 @@ def test_iterate_over_values(dict_value, tuple_to_find, expected_index): "report", "threat-actor", "tool", - "vulnerability" - ] + "vulnerability", + ], ) def test_is_sdo_dict(type_): d = { - "type": type_ + "type": type_, } assert stix2.utils.is_sdo(d, "2.0") @@ -277,7 +277,7 @@ def test_is_sdo_dict(type_): {"type": "relationship"}, {"type": "foo", "spec_version": "2.1"}, {"type": "foo"}, - ] + ], ) def test_is_not_sdo_dict(dict_): assert not stix2.utils.is_sdo(dict_, "2.0") @@ -285,7 +285,7 @@ def test_is_not_sdo_dict(dict_): def test_is_sco_dict(): d = { - "type": "file" + "type": "file", } assert stix2.utils.is_sco(d, "2.0") @@ -306,7 +306,7 @@ def test_is_sco_dict(): {"type": "relationship"}, {"type": "foo", "spec_version": "2.1"}, {"type": "foo"}, - ] + ], ) def test_is_not_sco_dict(dict_): assert not stix2.utils.is_sco(dict_, "2.0") @@ -316,7 +316,7 @@ def test_is_not_sco_dict(dict_): "dict_", [ {"type": "relationship"}, {"type": "sighting"}, - ] + ], ) def test_is_sro_dict(dict_): assert stix2.utils.is_sro(dict_, "2.0") @@ -338,7 +338,7 @@ def test_is_sro_dict(dict_): {"type": "sighting", "spec_version": "2.1"}, {"type": "foo", "spec_version": "2.1"}, {"type": "foo"}, - ] + ], ) def test_is_not_sro_dict(dict_): assert not stix2.utils.is_sro(dict_, "2.0") @@ -357,9 +357,9 @@ def test_is_not_sro_dict(dict_): {"type": "identity"}, {"type": "software"}, {"type": "marking-definition"}, - ] + ], }, - ] + ], ) def test_is_object_dict(dict_): assert stix2.utils.is_object(dict_, "2.0") @@ -376,7 +376,7 @@ def test_is_object_dict(dict_): {"type": "sighting", "spec_version": "2.1"}, {"type": "foo", "spec_version": "2.1"}, {"type": "foo"}, - ] + ], ) def test_is_not_object_dict(dict_): assert not stix2.utils.is_object(dict_, "2.0") diff --git a/stix2/test/v21/test_utils.py b/stix2/test/v21/test_utils.py index 71debda..6d108d4 100644 --- a/stix2/test/v21/test_utils.py +++ b/stix2/test/v21/test_utils.py @@ -264,13 +264,13 @@ def test_iterate_over_values(dict_value, tuple_to_find, expected_index): "location", "malware-analysis", "note", - "opinion" - ] + "opinion", + ], ) def test_is_sdo_dict(type_): d = { "type": type_, - "spec_version": "2.1" + "spec_version": "2.1", } assert stix2.utils.is_sdo(d, "2.1") @@ -289,7 +289,7 @@ def test_is_sdo_dict(type_): {"type": "relationship", "spec_version": "2.1"}, {"type": "relationship"}, {"type": "foo", "spec_version": "2.1"}, - ] + ], ) def test_is_not_sdo_dict(dict_): assert not stix2.utils.is_sdo(dict_, "2.1") @@ -298,7 +298,7 @@ def test_is_not_sdo_dict(dict_): def test_is_sco_dict(): d = { "type": "file", - "spec_version": "2.1" + "spec_version": "2.1", } assert stix2.utils.is_sco(d, "2.1") @@ -318,7 +318,7 @@ def test_is_sco_dict(): {"type": "relationship", "spec_version": "2.1"}, {"type": "relationship"}, {"type": "foo", "spec_version": "2.1"}, - ] + ], ) def test_is_not_sco_dict(dict_): assert not stix2.utils.is_sco(dict_, "2.1") @@ -328,7 +328,7 @@ def test_is_not_sco_dict(dict_): "dict_", [ {"type": "relationship", "spec_version": "2.1"}, {"type": "sighting", "spec_version": "2.1"}, - ] + ], ) def test_is_sro_dict(dict_): assert stix2.utils.is_sro(dict_, "2.1") @@ -349,7 +349,7 @@ def test_is_sro_dict(dict_): {"type": "relationship"}, {"type": "sighting"}, {"type": "foo", "spec_version": "2.1"}, - ] + ], ) def test_is_not_sro_dict(dict_): assert not stix2.utils.is_sro(dict_, "2.1") @@ -369,9 +369,9 @@ def test_is_not_sro_dict(dict_): {"type": "software", "spec_version": "2.1"}, {"type": "marking-definition", "spec_version": "2.1"}, {"type": "language-content", "spec_version": "2.1"}, - ] + ], }, - ] + ], ) def test_is_object_dict(dict_): assert stix2.utils.is_object(dict_, "2.1") @@ -387,7 +387,7 @@ def test_is_object_dict(dict_): {"type": "relationship"}, {"type": "sighting"}, {"type": "foo"}, - ] + ], ) def test_is_not_object_dict(dict_): assert not stix2.utils.is_object(dict_, "2.1") diff --git a/stix2/test/v21/test_versioning.py b/stix2/test/v21/test_versioning.py index efcf28f..051fb2e 100644 --- a/stix2/test/v21/test_versioning.py +++ b/stix2/test/v21/test_versioning.py @@ -369,7 +369,7 @@ def test_version_sco_id_contributing_properties_dict(): "name": "data.txt", "created": "1973-11-23T02:31:37Z", "modified": "1991-05-13T19:24:57Z", - "revoked": False + "revoked": False, } with pytest.raises(stix2.exceptions.UnmodifiablePropertyError) as e: diff --git a/stix2/utils.py b/stix2/utils.py index a7c2b73..ea1fb40 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -10,6 +10,7 @@ import pytz import six import stix2.version + try: import stix2.registry as mappings except ImportError: @@ -408,7 +409,7 @@ def is_sdo(value, stix_version=stix2.version.DEFAULT_VERSION): type_ = _stix_type_of(value) result = type_ in cls_maps["objects"] and type_ not in { "relationship", "sighting", "marking-definition", "bundle", - "language-content" + "language-content", } return result @@ -500,7 +501,7 @@ def is_object(value, stix_version=stix2.version.DEFAULT_VERSION): cls_maps = mappings.STIX2_OBJ_MAPS[stix_version] type_ = _stix_type_of(value) result = type_ in cls_maps["observables"] \ - or type_ in cls_maps["objects"] + or type_ in cls_maps["objects"] return result diff --git a/stix2/versioning.py b/stix2/versioning.py index 336ea4a..7c9709f 100644 --- a/stix2/versioning.py +++ b/stix2/versioning.py @@ -11,8 +11,8 @@ from six.moves.collections_abc import Mapping import stix2.base import stix2.registry from stix2.utils import ( - get_timestamp, parse_into_datetime, detect_spec_version, - is_sdo, is_sro, is_sco + detect_spec_version, get_timestamp, is_sco, is_sdo, is_sro, + parse_into_datetime, ) import stix2.v20 @@ -83,7 +83,6 @@ def _is_versionable(data): # First, determine spec version. It's easy for our stix2 objects; more # work for dicts. - is_21 = False if isinstance(data, stix2.v20._STIXBase20): stix_version = "2.0" elif isinstance(data, stix2.v21._STIXBase21): @@ -121,7 +120,7 @@ def _is_versionable(data): elif is_sco(obj_type, stix_version): # but do check SCOs cls = stix2.registry.class_for_type( - obj_type, stix_version, "observables" + obj_type, stix_version, "observables", ) is_versionable = _VERSIONING_PROPERTIES.issubset( cls._properties, @@ -174,7 +173,7 @@ def new_version(data, allow_custom=None, **kwargs): cls = data.__class__ else: cls = stix2.registry.class_for_type( - data["type"], stix_version, "observables" + data["type"], stix_version, "observables", ) sco_locked_props = cls._id_contributing_properties @@ -199,7 +198,7 @@ def new_version(data, allow_custom=None, **kwargs): new_modified = get_timestamp() new_modified = _fudge_modified( - old_modified, new_modified, stix_version == "2.1" + old_modified, new_modified, stix_version == "2.1", ) kwargs['modified'] = new_modified From 404fcd04caf730475bfbef3e18205ea2a36692d5 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Wed, 20 Jan 2021 20:58:13 -0500 Subject: [PATCH 15/20] Remove some ugly python2 compatibilty code from stix2.versioning module, since we no longer support python2. --- stix2/versioning.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/stix2/versioning.py b/stix2/versioning.py index 7c9709f..fc8bc7d 100644 --- a/stix2/versioning.py +++ b/stix2/versioning.py @@ -5,8 +5,7 @@ import datetime as dt import itertools import uuid -import six -from six.moves.collections_abc import Mapping +from collections.abc import Mapping import stix2.base import stix2.registry @@ -92,18 +91,9 @@ def _is_versionable(data): # Then, determine versionability. - if six.PY2: - # dumb python2 compatibility: map.keys() returns a list, not a set! - # six.viewkeys() compatibility function uses dict.viewkeys() on - # python2, which is not a Mapping mixin method, so that doesn't - # work either (for our stix2 objects). - keys = set(data) - else: - keys = data.keys() - # This should be sufficient for STIX objects; maybe we get lucky with # dicts here but probably not. - if keys >= _VERSIONING_PROPERTIES: + if data.keys() >= _VERSIONING_PROPERTIES: is_versionable = True # Tougher to handle dicts. We need to consider STIX version, map to a From f9b9e0d2d7b9684c6ed9932ea922995aca5d775b Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Wed, 20 Jan 2021 20:59:10 -0500 Subject: [PATCH 16/20] pre-commit stylistic fixes --- stix2/versioning.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stix2/versioning.py b/stix2/versioning.py index fc8bc7d..01affe9 100644 --- a/stix2/versioning.py +++ b/stix2/versioning.py @@ -1,12 +1,11 @@ """STIX2 core versioning methods.""" +from collections.abc import Mapping import copy import datetime as dt import itertools import uuid -from collections.abc import Mapping - import stix2.base import stix2.registry from stix2.utils import ( From eead72aabc1b93d1a63b608a5a9451fa3ff17740 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Wed, 27 Jan 2021 22:25:41 -0500 Subject: [PATCH 17/20] Fix docstring typo in is_marking(). Made a minor docstring update to is_sro() as well, so it doesn't talk as if you can register custom SROs. That didn't actually make sense. --- stix2/utils.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/stix2/utils.py b/stix2/utils.py index ea1fb40..fa3b25d 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -449,10 +449,10 @@ def is_sro(value, stix_version=stix2.version.DEFAULT_VERSION): """ Determine whether the given object, type, or ID is/is for an SRO of the given STIX version. If value is a type or ID, this just checks whether - the type was registered as an SRO in the given STIX version. If a mapping, - *simple* STIX version inference is additionally done on the value, and the - result is checked against stix_version. It does not attempt to fully - validate the value. + the type is an SRO in the given STIX version. If a mapping, *simple* STIX + version inference is additionally done on the value, and the result is + checked against stix_version. It does not attempt to fully validate the + value. :param value: A mapping with a "type" property, or a STIX ID or type as a string @@ -510,10 +510,10 @@ def is_marking(value, stix_version=stix2.version.DEFAULT_VERSION): """ Determine whether the given object, type, or ID is/is for an marking definition of the given STIX version. If value is a type or ID, this just - checks whether the type was registered as an SDO in the given STIX version. - If a mapping, *simple* STIX version inference is additionally done on the - value, and the result is checked against stix_version. It does not attempt - to fully validate the value. + checks whether the type is "marking-definition". If a mapping, *simple* + STIX version inference is additionally done on the value, and the result + is checked against stix_version. It does not attempt to fully validate the + value. :param value: A STIX object, object ID, or type as a string. :param stix_version: A STIX version as a string From 83abf78af54ff9da947755b963fbd6f9f5fa9860 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Thu, 28 Jan 2021 19:21:57 -0500 Subject: [PATCH 18/20] Remove old compatibility code regarding importing the old stix2.core module. --- stix2/utils.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/stix2/utils.py b/stix2/utils.py index fa3b25d..b7351a0 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -10,11 +10,7 @@ import pytz import six import stix2.version - -try: - import stix2.registry as mappings -except ImportError: - import stix2.core as mappings +import stix2.registry as mappings # Sentinel value for properties that should be set to the current time. From 98b0b2ed415746f362efd725f900a61fd9d2880d Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Thu, 28 Jan 2021 19:32:27 -0500 Subject: [PATCH 19/20] pre-commit stylistic fix --- stix2/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stix2/utils.py b/stix2/utils.py index b7351a0..1262b4b 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -9,9 +9,8 @@ import re import pytz import six -import stix2.version import stix2.registry as mappings - +import stix2.version # Sentinel value for properties that should be set to the current time. # We can't use the standard 'default' approach, since if there are multiple From 3878788da4e4c7cd0a7f32c1e732a4a1d2b648a1 Mon Sep 17 00:00:00 2001 From: Chris Lenk Date: Thu, 28 Jan 2021 22:54:52 -0500 Subject: [PATCH 20/20] Update is_sro docstring --- stix2/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stix2/utils.py b/stix2/utils.py index 1262b4b..3e272f8 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -444,7 +444,7 @@ def is_sro(value, stix_version=stix2.version.DEFAULT_VERSION): """ Determine whether the given object, type, or ID is/is for an SRO of the given STIX version. If value is a type or ID, this just checks whether - the type is an SRO in the given STIX version. If a mapping, *simple* STIX + the type is "sighting" or "relationship". If a mapping, *simple* STIX version inference is additionally done on the value, and the result is checked against stix_version. It does not attempt to fully validate the value.