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"]: