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.
pull/1/head
Michael Chisholm 2021-01-19 19:57:05 -05:00
parent f8c86f7352
commit fe2330af07
7 changed files with 417 additions and 121 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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