Further ReferenceProperty refinements: make allow_custom=True

work when a whitelist of generic category types is used.
Disallow hybrid constraints (both generic and specific at the
same time).  Add more unit tests.
pull/1/head
Michael Chisholm 2020-06-19 18:48:38 -04:00
parent 387ce7e7cb
commit c7dd58ed89
4 changed files with 293 additions and 39 deletions

View File

@ -490,27 +490,44 @@ class HexProperty(Property):
class ReferenceProperty(Property):
_OBJECT_CATEGORIES = {"SDO", "SCO", "SRO"}
_WHITELIST, _BLACKLIST = range(2)
def __init__(self, valid_types=None, invalid_types=None, spec_version=DEFAULT_VERSION, **kwargs):
"""
references sometimes must be to a specific object type
"""
self.spec_version = spec_version
# These checks need to be done prior to the STIX object finishing construction
# and thus we can't use base.py's _check_mutually_exclusive_properties()
# in the typical location of _check_object_constraints() in sdo.py
if valid_types and invalid_types:
raise MutuallyExclusivePropertiesError(self.__class__, ['invalid_types', 'valid_types'])
elif valid_types is None and invalid_types is None:
raise MissingPropertiesError(self.__class__, ['invalid_types', 'valid_types'])
if (valid_types is not None and invalid_types is not None) or \
(valid_types is None and invalid_types is None):
raise ValueError(
"Exactly one of 'valid_types' and 'invalid_types' must be "
"given"
)
if valid_types and type(valid_types) is not list:
if valid_types and not isinstance(valid_types, list):
valid_types = [valid_types]
elif invalid_types and type(invalid_types) is not list:
elif invalid_types and not isinstance(invalid_types, list):
invalid_types = [invalid_types]
self.valid_types = valid_types
self.invalid_types = invalid_types
self.types = set(valid_types or invalid_types)
self.auth_type = self._WHITELIST if valid_types else self._BLACKLIST
# Handling both generic and non-generic types in the same constraint
# complicates life... let's keep it simple unless we really need the
# complexity.
self.generic_constraint = any(
t in self._OBJECT_CATEGORIES for t in self.types
)
if self.generic_constraint and any(
t not in self._OBJECT_CATEGORIES for t in self.types
):
raise ValueError(
"Generic type categories and specific types may not both be "
"given"
)
super(ReferenceProperty, self).__init__(**kwargs)
@ -523,26 +540,56 @@ class ReferenceProperty(Property):
obj_type = value[:value.index('--')]
if self.valid_types:
types = self.types
auth_type = self.auth_type
if allow_custom and auth_type == self._WHITELIST and \
self.generic_constraint:
# If allowing customization and using a whitelist, and if generic
# "category" types were given, we need to allow custom object types
# of those categories. Unless registered, it's impossible to know
# whether a given type is within a given category. So we take a
# permissive approach and allow any type which is not known to be
# in the wrong category. I.e. flip the whitelist set to a
# blacklist of a complementary set.
types = self._OBJECT_CATEGORIES - types
auth_type = self._BLACKLIST
if auth_type == self._WHITELIST:
# allow_custom is not applicable to "whitelist" style object type
# constraints, so we ignore it.
has_custom = False
ref_valid_types = enumerate_types(self.valid_types, self.spec_version)
if self.generic_constraint:
type_ok = _type_in_generic_set(
obj_type, types, self.spec_version
)
else:
type_ok = obj_type in types
if obj_type not in ref_valid_types:
raise ValueError("The type-specifying prefix '%s' for this property is not valid" % (obj_type))
if not type_ok:
raise ValueError(
"The type-specifying prefix '%s' for this property is not "
"valid" % obj_type
)
else:
# A type "blacklist" was used to describe legal object types.
# We must enforce the type blacklist regardless of allow_custom.
ref_invalid_types = enumerate_types(self.invalid_types, self.spec_version)
# A type "blacklist" was used to describe legal object types. We
# must enforce the type blacklist regardless of allow_custom.
if self.generic_constraint:
type_ok = not _type_in_generic_set(
obj_type, types, self.spec_version
)
else:
type_ok = obj_type not in types
if obj_type in ref_invalid_types:
raise ValueError("An invalid type-specifying prefix '%s' was specified for this property" % (obj_type))
if not type_ok:
raise ValueError(
"The type-specifying prefix '%s' for this property is not "
"valid" % obj_type
)
# allow_custom=True only allows references to custom objects which
# are not otherwise blacklisted. So we need to figure out whether
# are not otherwise blacklisted. We need to figure out whether
# the referenced object is custom or not. No good way to do that
# at present... just check if unregistered and for the "x-" type
# prefix, for now?
@ -562,28 +609,30 @@ class ReferenceProperty(Property):
return value, has_custom
def enumerate_types(types, spec_version):
def _type_in_generic_set(type_, type_set, spec_version):
"""
`types` is meant to be a list; it may contain specific object types and/or
the any of the words "SCO", "SDO", or "SRO"
Since "SCO", "SDO", and "SRO" are general types that encompass various specific object types,
once each of those words is being processed, that word will be removed from `return_types`,
so as not to mistakenly allow objects to be created of types "SCO", "SDO", or "SRO"
Determine if type_ is in the given set, with respect to the given STIX
version. This handles special generic category values "SDO", "SCO",
"SRO", so it's not a simple set containment check. The type_set is
implicitly "OR"d.
"""
return_types = types[:]
type_maps = STIX2_OBJ_MAPS[spec_version]
if "SDO" in types:
return_types.remove("SDO")
return_types += STIX2_OBJ_MAPS[spec_version]['objects'].keys()
if "SCO" in types:
return_types.remove("SCO")
return_types += STIX2_OBJ_MAPS[spec_version]['observables'].keys()
if "SRO" in types:
return_types.remove("SRO")
return_types += ['relationship', 'sighting']
result = False
for type_id in type_set:
if type_id == "SDO":
result = type_ in type_maps["objects"]
elif type_id == "SCO":
result = type_ in type_maps["observables"]
elif type_id == "SRO":
result = type_ in ["relationship", "sighting"]
else:
raise ValueError("Unrecognized generic type category: " + type_id)
return return_types
if result:
break
return result
SELECTOR_REGEX = re.compile(r"^([a-z0-9_-]{3,250}(\.(\[\d+\]|[a-z0-9_-]{1,250}))*|id)$")

View File

@ -111,6 +111,37 @@ def test_reference_property_whitelist_type():
assert result == ("my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False)
def test_reference_property_whitelist_generic_type():
ref_prop = ReferenceProperty(
valid_types=["SCO", "SRO"], spec_version="2.0"
)
result = ref_prop.clean("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False)
assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False)
result = ref_prop.clean("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True)
assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False)
result = ref_prop.clean(
"sighting--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False
)
assert result == ("sighting--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False)
result = ref_prop.clean(
"some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True
)
assert result == ("some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True)
with pytest.raises(ValueError):
ref_prop.clean("some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False)
with pytest.raises(ValueError):
ref_prop.clean("identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False)
with pytest.raises(ValueError):
ref_prop.clean("identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True)
def test_reference_property_blacklist_type():
ref_prop = ReferenceProperty(invalid_types="identity", spec_version="2.0")
result = ref_prop.clean(
@ -144,6 +175,60 @@ def test_reference_property_blacklist_type():
)
def test_reference_property_blacklist_generic_type():
ref_prop = ReferenceProperty(
invalid_types=["SDO", "SRO"], spec_version="2.0"
)
result = ref_prop.clean(
"file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False,
)
assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False)
result = ref_prop.clean(
"file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True,
)
assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False)
result = ref_prop.clean(
"some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True,
)
assert result == ("some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True)
with pytest.raises(ValueError):
ref_prop.clean(
"identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True,
)
with pytest.raises(ValueError):
ref_prop.clean(
"identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False,
)
with pytest.raises(ValueError):
ref_prop.clean(
"relationship--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True,
)
with pytest.raises(ValueError):
ref_prop.clean(
"relationship--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False,
)
with pytest.raises(CustomContentError):
ref_prop.clean(
"some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False,
)
def test_reference_property_hybrid_constraint_type():
with pytest.raises(ValueError):
ReferenceProperty(valid_types=["a", "SCO"], spec_version="2.0")
with pytest.raises(ValueError):
ReferenceProperty(invalid_types=["a", "SCO"], spec_version="2.0")
@pytest.mark.parametrize(
"d", [
{'description': 'something'},

View File

@ -84,3 +84,38 @@ def test_malware_analysis_constraint():
stix2.v21.MalwareAnalysis(
product="Acme Malware Analyzer",
)
def test_malware_analysis_custom_sco_refs():
ma = stix2.v21.MalwareAnalysis(
product="super scanner",
analysis_sco_refs=[
"file--6e8c78cf-4bcc-4729-9265-86a97bfc91ba",
"some-object--f6bfc147-e844-4578-ae01-847979890239"
],
allow_custom=True,
)
assert "some-object--f6bfc147-e844-4578-ae01-847979890239" in \
ma["analysis_sco_refs"]
assert ma.has_custom
with pytest.raises(stix2.exceptions.InvalidValueError):
stix2.v21.MalwareAnalysis(
product="super scanner",
analysis_sco_refs=[
"file--6e8c78cf-4bcc-4729-9265-86a97bfc91ba",
"some-object--f6bfc147-e844-4578-ae01-847979890239"
]
)
with pytest.raises(stix2.exceptions.InvalidValueError):
stix2.v21.MalwareAnalysis(
product="super scanner",
analysis_sco_refs=[
"file--6e8c78cf-4bcc-4729-9265-86a97bfc91ba",
# standard object type; wrong category (not SCO)
"identity--56977a19-49ef-49d7-b259-f733fa4b7bbc"
],
allow_custom=True,
)

View File

@ -132,6 +132,37 @@ def test_reference_property_whitelist_type():
assert result == ("my-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False)
def test_reference_property_whitelist_generic_type():
ref_prop = ReferenceProperty(
valid_types=["SCO", "SRO"], spec_version="2.1"
)
result = ref_prop.clean("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False)
assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False)
result = ref_prop.clean("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True)
assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False)
result = ref_prop.clean(
"sighting--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False
)
assert result == ("sighting--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False)
result = ref_prop.clean(
"some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True
)
assert result == ("some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True)
with pytest.raises(ValueError):
ref_prop.clean("some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False)
with pytest.raises(ValueError):
ref_prop.clean("identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False)
with pytest.raises(ValueError):
ref_prop.clean("identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True)
def test_reference_property_blacklist_type():
ref_prop = ReferenceProperty(invalid_types="identity", spec_version="2.1")
result = ref_prop.clean(
@ -165,6 +196,60 @@ def test_reference_property_blacklist_type():
)
def test_reference_property_blacklist_generic_type():
ref_prop = ReferenceProperty(
invalid_types=["SDO", "SRO"], spec_version="2.1"
)
result = ref_prop.clean(
"file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False,
)
assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False)
result = ref_prop.clean(
"file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True,
)
assert result == ("file--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False)
result = ref_prop.clean(
"some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True,
)
assert result == ("some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True)
with pytest.raises(ValueError):
ref_prop.clean(
"identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True,
)
with pytest.raises(ValueError):
ref_prop.clean(
"identity--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False,
)
with pytest.raises(ValueError):
ref_prop.clean(
"relationship--8a8e8758-f92c-4058-ba38-f061cd42a0cf", True,
)
with pytest.raises(ValueError):
ref_prop.clean(
"relationship--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False,
)
with pytest.raises(CustomContentError):
ref_prop.clean(
"some-type--8a8e8758-f92c-4058-ba38-f061cd42a0cf", False,
)
def test_reference_property_hybrid_constraint_type():
with pytest.raises(ValueError):
ReferenceProperty(valid_types=["a", "SCO"], spec_version="2.1")
with pytest.raises(ValueError):
ReferenceProperty(invalid_types=["a", "SCO"], spec_version="2.1")
@pytest.mark.parametrize(
"d", [
{'description': 'something'},