From 19707677c9a99db3e4cf9b88679311917988a77f Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Fri, 7 Feb 2020 18:17:12 -0500 Subject: [PATCH] Fix STIX version detection from dicts. In particular, 2.1 SCOs without the spec_version property ought to be correctly detected as 2.1 now. --- stix2/core.py | 65 +++++--- stix2/test/test_spec_version_detect.py | 210 +++++++++++++++++++++++++ 2 files changed, 257 insertions(+), 18 deletions(-) create mode 100644 stix2/test/test_spec_version_detect.py diff --git a/stix2/core.py b/stix2/core.py index aa9044d..ae9e1e4 100644 --- a/stix2/core.py +++ b/stix2/core.py @@ -58,6 +58,47 @@ 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 string in "vXX" format, where "XX" indicates the spec version, + e.g. "v20", "v21", etc. + """ + + 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 = 'v' + stix_dict['spec_version'].replace('.', '') + elif "id" not in stix_dict: + # Only 2.0 SCOs don't have ID properties + v = "v20" + 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", + max( + _detect_spec_version(obj) for obj in stix_dict["objects"] + ) + ) + elif obj_type in 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" + else: + # Not a 2.1 SCO; must be a 2.0 object. + v = "v20" + + return v + + def dict_to_stix2(stix_dict, allow_custom=False, version=None): """convert dictionary to full python-stix2 object @@ -93,21 +134,8 @@ def dict_to_stix2(stix_dict, allow_custom=False, version=None): if version: # If the version argument was passed, override other approaches. v = 'v' + version.replace('.', '') - elif 'spec_version' in stix_dict: - # For STIX 2.0, applies to bundles only. - # For STIX 2.1+, applies to SDOs, SROs, and markings only. - v = 'v' + stix_dict['spec_version'].replace('.', '') - elif stix_dict['type'] == 'bundle': - # bundles without spec_version are ambiguous. - if any('spec_version' in x for x in stix_dict['objects']): - # Only on 2.1 we are allowed to have 'spec_version' in SDOs/SROs. - v = 'v21' - else: - v = 'v' + stix2.DEFAULT_VERSION.replace('.', '') else: - # The spec says that SDO/SROs without spec_version will default to a - # '2.0' representation. - v = 'v20' + v = _detect_spec_version(stix_dict) OBJ_MAP = dict(STIX2_OBJ_MAPS[v]['objects'], **STIX2_OBJ_MAPS[v]['observables']) @@ -142,6 +170,10 @@ def parse_observable(data, _valid_refs=None, allow_custom=False, version=None): """ obj = _get_dict(data) + + if 'type' not in obj: + raise ParseError("Can't parse observable with no 'type' property: %s" % str(obj)) + # get deep copy since we are going modify the dict and might # modify the original dict as _get_dict() does not return new # dict when passed a dict @@ -153,11 +185,8 @@ def parse_observable(data, _valid_refs=None, allow_custom=False, version=None): # If the version argument was passed, override other approaches. v = 'v' + version.replace('.', '') else: - # Use default version (latest) if no version was provided. - v = 'v' + stix2.DEFAULT_VERSION.replace('.', '') + v = _detect_spec_version(obj) - if 'type' not in obj: - raise ParseError("Can't parse observable with no 'type' property: %s" % str(obj)) try: OBJ_MAP_OBSERVABLE = STIX2_OBJ_MAPS[v]['observables'] obj_class = OBJ_MAP_OBSERVABLE[obj['type']] diff --git a/stix2/test/test_spec_version_detect.py b/stix2/test/test_spec_version_detect.py new file mode 100644 index 0000000..a5d3ee5 --- /dev/null +++ b/stix2/test/test_spec_version_detect.py @@ -0,0 +1,210 @@ +from __future__ import unicode_literals + +import pytest + +from stix2.core import _detect_spec_version + + +@pytest.mark.parametrize("obj_dict, expected_ver", [ + # STIX 2.0 examples + ( + { + "type": "identity", + "id": "identity--d7f72e8d-657a-43ec-9324-b3ec67a97486", + "created": "1972-05-21T05:33:09.000Z", + "modified": "1973-05-28T02:10:54.000Z", + "name": "alice", + "identity_class": "individual" + }, + "v20" + ), + ( + { + "type": "relationship", + "id": "relationship--63b0f1b7-925e-4795-ac9b-61fb9f235f1a", + "created": "1981-08-11T13:48:19.000Z", + "modified": "2000-02-16T15:33:15.000Z", + "source_ref": "attack-pattern--9391504a-ef29-4a41-a257-5634d9edc391", + "target_ref": "identity--ba18dde2-56d3-4a34-aa0b-fc56f5be568f", + "relationship_type": "targets" + }, + "v20" + ), + ( + { + "type": "file", + "name": "notes.txt" + }, + "v20" + ), + ( + { + "type": "marking-definition", + "id": "marking-definition--2a13090f-a493-4b70-85fe-fa021d91dcd2", + "created": "1998-03-27T19:44:53.000Z", + "definition_type": "statement", + "definition": { + "statement": "Copyright (c) ACME Corp." + } + }, + "v20" + ), + ( + { + "type": "bundle", + "id": "bundle--8379cb02-8131-47c8-8a7c-9a1f0e0986b1", + "spec_version": "2.0", + "objects": [ + { + "type": "identity", + "id": "identity--d7f72e8d-657a-43ec-9324-b3ec67a97486", + "created": "1972-05-21T05:33:09.000Z", + "modified": "1973-05-28T02:10:54.000Z", + "name": "alice", + "identity_class": "individual" + }, + { + "type": "marking-definition", + "id": "marking-definition--2a13090f-a493-4b70-85fe-fa021d91dcd2", + "created": "1998-03-27T19:44:53.000Z", + "definition_type": "statement", + "definition": { + "statement": "Copyright (c) ACME Corp." + } + } + ] + }, + "v20" + ), + # STIX 2.1 examples + ( + { + "type": "identity", + "id": "identity--22299b4c-bc38-4485-ad7d-8222f01c58c7", + "spec_version": "2.1", + "created": "1995-07-24T04:07:48.000Z", + "modified": "2001-07-01T09:33:17.000Z", + "name": "alice" + }, + "v21" + ), + ( + { + "type": "relationship", + "id": "relationship--0eec232d-e1ea-4f85-8e78-0de6ae9d09f0", + "spec_version": "2.1", + "created": "1975-04-05T10:47:22.000Z", + "modified": "1983-04-25T20:56:00.000Z", + "source_ref": "attack-pattern--9391504a-ef29-4a41-a257-5634d9edc391", + "target_ref": "identity--ba18dde2-56d3-4a34-aa0b-fc56f5be568f", + "relationship_type": "targets" + }, + "v21" + ), + ( + { + "type": "file", + "id": "file--5eef3404-6a94-4db3-9a1a-5684cbea0dfe", + "spec_version": "2.1", + "name": "notes.txt" + }, + "v21" + ), + ( + { + "type": "file", + "id": "file--5eef3404-6a94-4db3-9a1a-5684cbea0dfe", + "name": "notes.txt" + }, + "v21" + ), + ( + { + "type": "marking-definition", + "spec_version": "2.1", + "id": "marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da", + "created": "2017-01-20T00:00:00.000Z", + "definition_type": "tlp", + "name": "TLP:GREEN", + "definition": { + "tlp": "green" + } + }, + "v21" + ), + ( + { + "type": "bundle", + "id": "bundle--d5787acd-1ffd-4630-ada3-6857698f6287", + "objects": [ + { + "type": "identity", + "id": "identity--22299b4c-bc38-4485-ad7d-8222f01c58c7", + "spec_version": "2.1", + "created": "1995-07-24T04:07:48.000Z", + "modified": "2001-07-01T09:33:17.000Z", + "name": "alice" + }, + { + "type": "file", + "id": "file--5eef3404-6a94-4db3-9a1a-5684cbea0dfe", + "name": "notes.txt" + }, + ] + }, + "v21" + ), + # Mixed spec examples + ( + { + "type": "bundle", + "id": "bundle--e1a01e29-3432-401a-ab9f-c1082b056605", + "objects": [ + { + "type": "identity", + "id": "identity--d7f72e8d-657a-43ec-9324-b3ec67a97486", + "created": "1972-05-21T05:33:09.000Z", + "modified": "1973-05-28T02:10:54.000Z", + "name": "alice", + "identity_class": "individual" + }, + { + "type": "relationship", + "id": "relationship--63b0f1b7-925e-4795-ac9b-61fb9f235f1a", + "created": "1981-08-11T13:48:19.000Z", + "modified": "2000-02-16T15:33:15.000Z", + "source_ref": "attack-pattern--9391504a-ef29-4a41-a257-5634d9edc391", + "target_ref": "identity--ba18dde2-56d3-4a34-aa0b-fc56f5be568f", + "relationship_type": "targets" + }, + ] + }, + "v21" + ), + ( + { + "type": "bundle", + "id": "bundle--eecad3d9-bb9a-4263-93f6-1c0ccc984574", + "objects": [ + { + "type": "identity", + "id": "identity--d7f72e8d-657a-43ec-9324-b3ec67a97486", + "created": "1972-05-21T05:33:09.000Z", + "modified": "1973-05-28T02:10:54.000Z", + "name": "alice", + "identity_class": "individual" + }, + { + "type": "file", + "id": "file--5eef3404-6a94-4db3-9a1a-5684cbea0dfe", + "name": "notes.txt" + }, + ] + }, + "v21" + ) +]) +def test_spec_version_detect(obj_dict, expected_ver): + detected_ver = _detect_spec_version(obj_dict) + + assert detected_ver == expected_ver