diff --git a/stix2/datastore/filters.py b/stix2/datastore/filters.py index 6d9273e1..3036fab9 100644 --- a/stix2/datastore/filters.py +++ b/stix2/datastore/filters.py @@ -6,7 +6,7 @@ import stix2.utils """Supported filter operations""" -FILTER_OPS = ['=', '!=', 'in', '>', '<', '>=', '<=', 'contains'] +FILTER_OPS = ['=', '!=', 'in', '>', '<', '>=', '<=', 'contains', 'iequals', 'icontains'] """Supported filter value types""" FILTER_VALUE_TYPES = ( @@ -88,6 +88,8 @@ def _check_property(self, stix_obj_property): if self.op == "=": return stix_obj_property == filter_value + elif self.op == "iequals": + return _casefold(filter_value) == _casefold(stix_obj_property) elif self.op == "!=": return stix_obj_property != filter_value elif self.op == "in": @@ -97,6 +99,11 @@ def _check_property(self, stix_obj_property): return filter_value in stix_obj_property.values() else: return filter_value in stix_obj_property + elif self.op == "icontains": + if isinstance(filter_value, dict): + return _casefold(filter_value) in _casefold(stix_obj_property).values() + else: + return _casefold(filter_value) in _casefold(stix_obj_property) elif self.op == ">": return stix_obj_property > filter_value elif self.op == "<": @@ -191,7 +198,7 @@ class FilterSet(object): """Internal STIX2 class to facilitate the grouping of Filters into sets. The primary motivation for this class came from the problem that Filters that had a dict as a value could not be added to a Python - set as dicts are not hashable. Thus this class provides set functionality + set as dicts are not hashable. Thus, this class provides set functionality but internally stores filters in a list. """ @@ -219,7 +226,7 @@ def add(self, filters=None): Operates like set, only adding unique stix2.Filters to the FilterSet Note: - method designed to be very accomodating (i.e. even accepting filters=None) + method designed to be very accommodating (i.e. even accepting filters=None) as it allows for blind calls (very useful in DataStore) Args: @@ -242,7 +249,7 @@ def remove(self, filters=None): """Remove a Filter, list of Filters, or FilterSet from the FilterSet. Note: - method designed to be very accomodating (i.e. even accepting filters=None) + method designed to be very accommodating (i.e. even accepting filters=None) as it allows for blind calls (very useful in DataStore) Args: @@ -259,3 +266,17 @@ def remove(self, filters=None): for f in filters: self._filters.remove(f) + + +def _casefold(input_value): + if not isinstance(input_value, FILTER_VALUE_TYPES): + input_value = stix2.utils._get_dict(input_value) + if hasattr(input_value, 'casefold'): + return input_value.casefold() + if isinstance(input_value, dict): + return {k: _casefold(v) for k, v in input_value.items()} + if isinstance(input_value, list): + return [_casefold(v) for v in input_value] + if isinstance(input_value, tuple): + return tuple(_casefold(v) for v in input_value) + return input_value diff --git a/stix2/test/v21/test_datastore_filters.py b/stix2/test/v21/test_datastore_filters.py index 0e531fdb..31bb2aaa 100644 --- a/stix2/test/v21/test_datastore_filters.py +++ b/stix2/test/v21/test_datastore_filters.py @@ -121,6 +121,15 @@ {"type": "file", "id": "file--42a7175a-42cc-508f-8fa7-23b330aff876", "name": "HAL 9000.exe", "spec_version": "2.1", "defanged": False}, ), Filter("labels", "contains", "heartbleed"), + Filter( + "objects", "iequals", + {"0": {"type": "FILE", "id": "FILE--42A7175A-42CC-508F-8FA7-23B330AFF876", "name": "HAL 9000.EXE", "spec_version": "2.1", "defanged": False}}, + ), + Filter( + "objects", "icontains", + {"type": "FILE", "id": "FILE--42A7175A-42CC-508F-8FA7-23B330AFF876", "name": "HAL 9000.EXE", "spec_version": "2.1", "defanged": False}, + ), + Filter("labels", "icontains", "HEARTBLEED"), ] # same as above objects but converted to real Python STIX2 objects @@ -343,6 +352,42 @@ def test_apply_common_filters15(): assert len(resp) == 1 +def test_apply_common_filters16(): + # Return any object that matches file object in "objects" + resp = list(apply_common_filters(stix_objs, [filters[17]])) + assert resp[0]["id"] == stix_objs[4]["id"] + assert len(resp) == 1 + # important additional check to make sure original File dict was + # not converted to File object. (this was a deep bug found) + assert isinstance(resp[0]["objects"]["0"], dict) + + resp = list(apply_common_filters(real_stix_objs, [filters[17]])) + assert resp[0].id == real_stix_objs[4].id + assert len(resp) == 1 + + +def test_apply_common_filters17(): + # Return any object that contains a case insensitive specific File Cyber Observable Object + resp = list(apply_common_filters(stix_objs, [filters[18]])) + assert resp[0]['id'] == stix_objs[4]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs, [filters[18]])) + assert resp[0].id == real_stix_objs[4].id + assert len(resp) == 1 + + +def test_apply_common_filters18(): + # Return any object that contains case insensitive 'heartbleed' in "labels" + resp = list(apply_common_filters(stix_objs, [filters[19]])) + assert resp[0]['id'] == stix_objs[3]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs, [filters[19]])) + assert resp[0].id == real_stix_objs[3].id + assert len(resp) == 1 + + def test_datetime_filter_behavior(): """if a filter is initialized with its value being a datetime object OR the STIX object property being filtered on is a datetime object, all