Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Case Insensitive Filter #566

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions stix2/datastore/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down Expand Up @@ -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":
Expand All @@ -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 == "<":
Expand Down Expand Up @@ -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.
"""

Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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
45 changes: 45 additions & 0 deletions stix2/test/v21/test_datastore_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down