Update IDProperty and ReferenceProperty to support both stix 2.0

and 2.1 rules regarding identifiers.  Change relevant property
tests to specify which spec version to use, and modify tests
according to the specs.
master
Michael Chisholm 2019-06-12 20:19:47 -04:00
parent 5b6a0dc087
commit ed106f23ff
3 changed files with 91 additions and 43 deletions

View File

@ -16,22 +16,29 @@ from .core import STIX2_OBJ_MAPS, parse, parse_observable
from .exceptions import CustomContentError, DictionaryKeyError from .exceptions import CustomContentError, DictionaryKeyError
from .utils import _get_dict, get_class_hierarchy_names, parse_into_datetime from .utils import _get_dict, get_class_hierarchy_names, parse_into_datetime
# This uses the regular expression for a RFC 4122, Version 4 UUID. In the ERROR_INVALID_ID = (
# 8-4-4-4-12 hexadecimal representation, the first hex digit of the third "not a valid STIX identifier, must match <object-type>--<UUID>: {}"
# component must be a 4, and the first hex digit of the fourth component
# must be 8, 9, a, or b (10xx bit pattern).
ID_REGEX = re.compile(
r"^[a-z0-9][a-z0-9-]+[a-z0-9]--" # object type
"[0-9a-fA-F]{8}-"
"[0-9a-fA-F]{4}-"
"4[0-9a-fA-F]{3}-"
"[89abAB][0-9a-fA-F]{3}-"
"[0-9a-fA-F]{12}$",
) )
ERROR_INVALID_ID = (
"not a valid STIX identifier, must match <object-type>--<UUIDv4>" def _check_uuid(uuid_str, spec_version):
) """
Check whether the given UUID string is valid with respect to the given STIX
spec version. STIX 2.0 requires UUIDv4; 2.1 only requires the RFC 4122
variant.
:param uuid_str: A UUID as a string
:param spec_version: The STIX spec version
:return: True if the UUID is valid, False if not
:raises ValueError: If uuid_str is malformed
"""
uuid_obj = uuid.UUID(uuid_str)
ok = uuid_obj.variant == uuid.RFC_4122
if ok and spec_version == "2.0":
ok = uuid_obj.version == 4
return ok
class Property(object): class Property(object):
@ -185,15 +192,25 @@ class TypeProperty(Property):
class IDProperty(Property): class IDProperty(Property):
def __init__(self, type): def __init__(self, type, spec_version="2.1"):
self.required_prefix = type + "--" self.required_prefix = type + "--"
self.spec_version = spec_version
super(IDProperty, self).__init__() super(IDProperty, self).__init__()
def clean(self, value): def clean(self, value):
if not value.startswith(self.required_prefix): if not value.startswith(self.required_prefix):
raise ValueError("must start with '{}'.".format(self.required_prefix)) raise ValueError("must start with '{}'.".format(self.required_prefix))
if not ID_REGEX.match(value):
raise ValueError(ERROR_INVALID_ID) uuid_part = value[len(self.required_prefix):]
try:
result = _check_uuid(uuid_part, self.spec_version)
except ValueError:
# replace their ValueError with ours
raise ValueError(ERROR_INVALID_ID.format(value))
if not result:
raise ValueError(ERROR_INVALID_ID.format(value))
return value return value
def default(self): def default(self):
@ -366,22 +383,40 @@ class HexProperty(Property):
class ReferenceProperty(Property): class ReferenceProperty(Property):
def __init__(self, type=None, **kwargs): def __init__(self, type=None, spec_version="2.1", **kwargs):
""" """
references sometimes must be to a specific object type references sometimes must be to a specific object type
""" """
self.type = type self.required_prefix = type + "--" if type else None
self.spec_version = spec_version
super(ReferenceProperty, self).__init__(**kwargs) super(ReferenceProperty, self).__init__(**kwargs)
def clean(self, value): def clean(self, value):
if isinstance(value, _STIXBase): if isinstance(value, _STIXBase):
value = value.id value = value.id
value = str(value) value = str(value)
if self.type:
if not value.startswith(self.type): if self.required_prefix:
raise ValueError("must start with '{}'.".format(self.type)) if not value.startswith(self.required_prefix):
if not ID_REGEX.match(value): raise ValueError(
raise ValueError(ERROR_INVALID_ID) "must start with '{}'.".format(self.required_prefix),
)
try:
if self.required_prefix:
uuid_part = value[len(self.required_prefix):]
else:
idx = value.index("--")
uuid_part = value[idx+2:]
result = _check_uuid(uuid_part, self.spec_version)
except ValueError:
# replace their ValueError with ours
raise ValueError(ERROR_INVALID_ID.format(value))
if not result:
raise ValueError(ERROR_INVALID_ID.format(value))
return value return value

View File

@ -5,7 +5,7 @@ import pytest
import stix2 import stix2
from stix2.exceptions import AtLeastOnePropertyError, DictionaryKeyError from stix2.exceptions import AtLeastOnePropertyError, DictionaryKeyError
from stix2.properties import ( from stix2.properties import (
ERROR_INVALID_ID, BinaryProperty, BooleanProperty, DictionaryProperty, BinaryProperty, BooleanProperty, DictionaryProperty,
EmbeddedObjectProperty, EnumProperty, ExtensionsProperty, FloatProperty, EmbeddedObjectProperty, EnumProperty, ExtensionsProperty, FloatProperty,
HashesProperty, HexProperty, IDProperty, IntegerProperty, ListProperty, HashesProperty, HexProperty, IDProperty, IntegerProperty, ListProperty,
Property, ReferenceProperty, STIXObjectProperty, StringProperty, Property, ReferenceProperty, STIXObjectProperty, StringProperty,
@ -89,7 +89,7 @@ def test_type_property():
assert prop.clean(prop.default()) assert prop.clean(prop.default())
ID_PROP = IDProperty('my-type') ID_PROP = IDProperty('my-type', spec_version="2.0")
MY_ID = 'my-type--232c9d3f-49fc-4440-bb01-607f638778e7' MY_ID = 'my-type--232c9d3f-49fc-4440-bb01-607f638778e7'
@ -127,7 +127,7 @@ CONSTANT_IDS.extend(constants.RELATIONSHIP_IDS)
@pytest.mark.parametrize("value", CONSTANT_IDS) @pytest.mark.parametrize("value", CONSTANT_IDS)
def test_id_property_valid_for_type(value): def test_id_property_valid_for_type(value):
type = value.split('--', 1)[0] type = value.split('--', 1)[0]
assert IDProperty(type=type).clean(value) == value assert IDProperty(type=type, spec_version="2.0").clean(value) == value
def test_id_property_wrong_type(): def test_id_property_wrong_type():
@ -147,9 +147,8 @@ def test_id_property_wrong_type():
], ],
) )
def test_id_property_not_a_valid_hex_uuid(value): def test_id_property_not_a_valid_hex_uuid(value):
with pytest.raises(ValueError) as excinfo: with pytest.raises(ValueError):
ID_PROP.clean(value) ID_PROP.clean(value)
assert str(excinfo.value) == ERROR_INVALID_ID
def test_id_property_default(): def test_id_property_default():
@ -275,7 +274,7 @@ def test_boolean_property_invalid(value):
def test_reference_property(): def test_reference_property():
ref_prop = ReferenceProperty() ref_prop = ReferenceProperty(spec_version="2.0")
assert ref_prop.clean("my-type--00000000-0000-4000-8000-000000000000") assert ref_prop.clean("my-type--00000000-0000-4000-8000-000000000000")
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -286,6 +285,16 @@ def test_reference_property():
ref_prop.clean("my-type--00000000-0000-0000-0000-000000000000") ref_prop.clean("my-type--00000000-0000-0000-0000-000000000000")
def test_reference_property_specific_type():
ref_prop = ReferenceProperty("my-type", spec_version="2.0")
with pytest.raises(ValueError):
ref_prop.clean("not-my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf")
assert ref_prop.clean("my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf") == \
"my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf"
@pytest.mark.parametrize( @pytest.mark.parametrize(
"value", [ "value", [
'2017-01-01T12:34:56Z', '2017-01-01T12:34:56Z',

View File

@ -1,11 +1,9 @@
import uuid
import pytest import pytest
import stix2 import stix2
from stix2.exceptions import AtLeastOnePropertyError, DictionaryKeyError from stix2.exceptions import AtLeastOnePropertyError, DictionaryKeyError
from stix2.properties import ( from stix2.properties import (
ERROR_INVALID_ID, BinaryProperty, BooleanProperty, DictionaryProperty, BinaryProperty, BooleanProperty, DictionaryProperty,
EmbeddedObjectProperty, EnumProperty, ExtensionsProperty, FloatProperty, EmbeddedObjectProperty, EnumProperty, ExtensionsProperty, FloatProperty,
HashesProperty, HexProperty, IDProperty, IntegerProperty, ListProperty, HashesProperty, HexProperty, IDProperty, IntegerProperty, ListProperty,
Property, ReferenceProperty, StringProperty, TimestampProperty, Property, ReferenceProperty, StringProperty, TimestampProperty,
@ -89,7 +87,7 @@ def test_type_property():
assert prop.clean(prop.default()) assert prop.clean(prop.default())
ID_PROP = IDProperty('my-type') ID_PROP = IDProperty('my-type', spec_version="2.1")
MY_ID = 'my-type--232c9d3f-49fc-4440-bb01-607f638778e7' MY_ID = 'my-type--232c9d3f-49fc-4440-bb01-607f638778e7'
@ -127,7 +125,7 @@ CONSTANT_IDS.extend(constants.RELATIONSHIP_IDS)
@pytest.mark.parametrize("value", CONSTANT_IDS) @pytest.mark.parametrize("value", CONSTANT_IDS)
def test_id_property_valid_for_type(value): def test_id_property_valid_for_type(value):
type = value.split('--', 1)[0] type = value.split('--', 1)[0]
assert IDProperty(type=type).clean(value) == value assert IDProperty(type=type, spec_version="2.1").clean(value) == value
def test_id_property_wrong_type(): def test_id_property_wrong_type():
@ -139,17 +137,13 @@ def test_id_property_wrong_type():
@pytest.mark.parametrize( @pytest.mark.parametrize(
"value", [ "value", [
'my-type--foo', 'my-type--foo',
# Not a v4 UUID # Not a RFC 4122 UUID
'my-type--00000000-0000-0000-0000-000000000000', 'my-type--00000000-0000-0000-0000-000000000000',
'my-type--' + str(uuid.uuid1()),
'my-type--' + str(uuid.uuid3(uuid.NAMESPACE_DNS, "example.org")),
'my-type--' + str(uuid.uuid5(uuid.NAMESPACE_DNS, "example.org")),
], ],
) )
def test_id_property_not_a_valid_hex_uuid(value): def test_id_property_not_a_valid_hex_uuid(value):
with pytest.raises(ValueError) as excinfo: with pytest.raises(ValueError):
ID_PROP.clean(value) ID_PROP.clean(value)
assert str(excinfo.value) == ERROR_INVALID_ID
def test_id_property_default(): def test_id_property_default():
@ -275,17 +269,27 @@ def test_boolean_property_invalid(value):
def test_reference_property(): def test_reference_property():
ref_prop = ReferenceProperty() ref_prop = ReferenceProperty(spec_version="2.1")
assert ref_prop.clean("my-type--00000000-0000-4000-8000-000000000000") assert ref_prop.clean("my-type--00000000-0000-4000-8000-000000000000")
with pytest.raises(ValueError): with pytest.raises(ValueError):
ref_prop.clean("foo") ref_prop.clean("foo")
# This is not a valid V4 UUID # This is not a valid RFC 4122 UUID
with pytest.raises(ValueError): with pytest.raises(ValueError):
ref_prop.clean("my-type--00000000-0000-0000-0000-000000000000") ref_prop.clean("my-type--00000000-0000-0000-0000-000000000000")
def test_reference_property_specific_type():
ref_prop = ReferenceProperty("my-type", spec_version="2.1")
with pytest.raises(ValueError):
ref_prop.clean("not-my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf")
assert ref_prop.clean("my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf") == \
"my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf"
@pytest.mark.parametrize( @pytest.mark.parametrize(
"value", [ "value", [
'2017-01-01T12:34:56Z', '2017-01-01T12:34:56Z',