From fe2330af076ee9546d1879a22a88e2c0f67c6f63 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Tue, 19 Jan 2021 19:57:05 -0500 Subject: [PATCH] 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.