From 9168dc5c29001f8d5a8f48eae60dfb9fac5e6d1e Mon Sep 17 00:00:00 2001 From: Michael Weiser Date: Thu, 17 Mar 2022 08:30:19 +0000 Subject: [PATCH] expressions: Ignore non-iterability of None in membership tests Python makes a distinction between something being contained in something else (or not) and something not being able to be contained in nothing (i.e. None) and raises a 'NoneType not iterable' exception in the latter case. We can see no application of this distinction in our use-case as of now. In fact it causes problems when writing tests against object properties which are None. Therefore we handle that case in our containment tests and interpret it as something (obviously) not being contained in nothing. Closes #176. --- peekaboo/ruleset/expressions.py | 15 +++++++++------ tests/test.py | 11 +++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/peekaboo/ruleset/expressions.py b/peekaboo/ruleset/expressions.py index a47ab157..a73c5f47 100644 --- a/peekaboo/ruleset/expressions.py +++ b/peekaboo/ruleset/expressions.py @@ -448,16 +448,19 @@ def __init__(self, tokens): } @staticmethod - def in_(a, b): + def in_(op1, op2): """ Literally implement membership test. Make it a static method so we can do identity checks. Do not use operator.contains because it needs - operands swapped. """ - return a in b + operands swapped. Also, there's no foreseeable use of the distinction + that something cannot be present in nothing in our application. So + prevent 'NoneType not iterable' exceptions by checking that op2 is not + None. """ + return op2 is not None and op1 in op2 @staticmethod - def not_in(a, b): - """ Naively implement non-membership test. """ - return a not in b + def not_in(op1, op2): + """ Implement non-membership test. """ + return op2 is not None and op1 not in op2 @staticmethod def and_(op1, op2): diff --git a/tests/test.py b/tests/test.py index 3df3fefc..9de811a4 100755 --- a/tests/test.py +++ b/tests/test.py @@ -1297,6 +1297,7 @@ def test_rule_expression_filetools(self): == "AppleDouble encoded Macintosh file" -> ignore expression.1 : sample.file_extension in {"doc", "docx"} and filereport.type_by_content != /application\/.*word/ -> bad + expression.2: "text" in filereport.type_by_name -> ignore ''' sample_kwargs = { @@ -1324,6 +1325,13 @@ def test_rule_expression_filetools(self): result = rule.evaluate(sample) self.assertEqual(result.result, Result.ignored) + # expression.2 should not raise a 'NoneType not iterable' exception due + # to type_by_name being None (and it should not match). This happens if + # the file name is empty, for example. + sample = Sample(b'dummy') + result = rule.evaluate(sample) + self.assertEqual(result.result, Result.unknown) + @asynctest async def test_rule_expression_knowntools(self): """ Test generic rule on knowntoolsreport. """ @@ -1931,6 +1939,9 @@ def test_basic_expressions(self): ["'foo' == 'bar'", False], ["'foo' == 'foo'", True], ["'foo' in 'bar'", False], + # we swallow None being non-iterable + ["'foo' in None", False], + ["'foo' not in None", False], # re.search() for "match anywhere in operand" ["'foo' in 'foobar'", True], ["/foo/ in 'afoobar'", True],