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.
master
Michael Chisholm 2020-02-07 18:17:12 -05:00
parent c96b74294a
commit 19707677c9
2 changed files with 257 additions and 18 deletions

View File

@ -58,6 +58,47 @@ def parse(data, allow_custom=False, version=None):
return obj 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): def dict_to_stix2(stix_dict, allow_custom=False, version=None):
"""convert dictionary to full python-stix2 object """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 version:
# If the version argument was passed, override other approaches. # If the version argument was passed, override other approaches.
v = 'v' + version.replace('.', '') 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: else:
# The spec says that SDO/SROs without spec_version will default to a v = _detect_spec_version(stix_dict)
# '2.0' representation.
v = 'v20'
OBJ_MAP = dict(STIX2_OBJ_MAPS[v]['objects'], **STIX2_OBJ_MAPS[v]['observables']) 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) 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 # get deep copy since we are going modify the dict and might
# modify the original dict as _get_dict() does not return new # modify the original dict as _get_dict() does not return new
# dict when passed a dict # 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. # If the version argument was passed, override other approaches.
v = 'v' + version.replace('.', '') v = 'v' + version.replace('.', '')
else: else:
# Use default version (latest) if no version was provided. v = _detect_spec_version(obj)
v = 'v' + stix2.DEFAULT_VERSION.replace('.', '')
if 'type' not in obj:
raise ParseError("Can't parse observable with no 'type' property: %s" % str(obj))
try: try:
OBJ_MAP_OBSERVABLE = STIX2_OBJ_MAPS[v]['observables'] OBJ_MAP_OBSERVABLE = STIX2_OBJ_MAPS[v]['observables']
obj_class = OBJ_MAP_OBSERVABLE[obj['type']] obj_class = OBJ_MAP_OBSERVABLE[obj['type']]

View File

@ -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