diff options
Diffstat (limited to 'tests/analysis/scan')
-rw-r--r-- | tests/analysis/scan/booleans.py | 98 | ||||
-rw-r--r-- | tests/analysis/scan/common.py | 52 | ||||
-rw-r--r-- | tests/analysis/scan/examples.py | 70 | ||||
-rw-r--r-- | tests/analysis/scan/expr.py | 169 | ||||
-rw-r--r-- | tests/analysis/scan/exprs.py | 122 | ||||
-rw-r--r-- | tests/analysis/scan/func.py | 16 | ||||
-rw-r--r-- | tests/analysis/scan/functions.py | 104 | ||||
-rw-r--r-- | tests/analysis/scan/grammar.py | 327 | ||||
-rw-r--r-- | tests/analysis/scan/matches.py | 27 | ||||
-rw-r--r-- | tests/analysis/scan/options.py | 16 | ||||
-rw-r--r-- | tests/analysis/scan/pyapi.py | 58 | ||||
-rw-r--r-- | tests/analysis/scan/sets.py | 118 |
12 files changed, 643 insertions, 534 deletions
diff --git a/tests/analysis/scan/booleans.py b/tests/analysis/scan/booleans.py new file mode 100644 index 0000000..aa3c1a3 --- /dev/null +++ b/tests/analysis/scan/booleans.py @@ -0,0 +1,98 @@ + +from common import RostTestClass + + +class TestRostBooleans(RostTestClass): + """TestCases for booleans and ROST.""" + + def testFinalCondition(self): + """Validate the final condition.""" + + rule = ''' +rule test { + + condition: + false + +} +''' + + self.check_rule_failure(rule) + + + rule = ''' +rule test { + + condition: + true + +} +''' + + self.check_rule_success(rule) + + + def testBasicBooleanOperations(self): + """Evaluate basic boolean operations.""" + + rule = ''' +rule test { + + condition: + true and false + +} +''' + + self.check_rule_failure(rule) + + + rule = ''' +rule test { + + condition: + true or false + +} +''' + + self.check_rule_success(rule) + + + def testImplicitCast(self): + """Imply implicit casts to booleans.""" + + rule = ''' +rule test { + + condition: + true and 0 + +} +''' + + self.check_rule_failure(rule) + + + rule = ''' +rule test { + + condition: + 1 or false + +} +''' + + self.check_rule_success(rule) + + + rule = ''' +rule test { + + condition: + 1 or () + +} +''' + + self.check_rule_success(rule) diff --git a/tests/analysis/scan/common.py b/tests/analysis/scan/common.py new file mode 100644 index 0000000..3b52e38 --- /dev/null +++ b/tests/analysis/scan/common.py @@ -0,0 +1,52 @@ + +from chrysacase import ChrysalideTestCase +from pychrysalide.analysis.contents import MemoryContent +from pychrysalide.analysis.scan import ContentScanner +from pychrysalide.analysis.scan import ScanOptions +from pychrysalide.analysis.scan.patterns.backends import AcismBackend + + +class RostTestClass(ChrysalideTestCase): + """TestCase for analysis.scan.ScanExpression.""" + + @classmethod + def setUpClass(cls): + + super(RostTestClass, cls).setUpClass() + + cls._options = ScanOptions() + cls._options.backend_for_data = AcismBackend + + cls._empty_content = MemoryContent(b'') + + + def _validate_rule_result(self, rule, content, expected): + """Check for scan success or failure.""" + + scanner = ContentScanner(rule) + ctx = scanner.analyze(self._options, content) + + self.assertIsNotNone(ctx) + + if expected: + self.assertTrue(ctx.has_match_for_rule('test')) + else: + self.assertFalse(ctx.has_match_for_rule('test')) + + + def check_rule_success(self, rule, content = None): + """Check for scan success.""" + + if content is None: + content = self._empty_content + + self._validate_rule_result(rule, content, True) + + + def check_rule_failure(self, rule, content = None): + """Check for scan failure.""" + + if content is None: + content = self._empty_content + + self._validate_rule_result(rule, content, False) diff --git a/tests/analysis/scan/examples.py b/tests/analysis/scan/examples.py new file mode 100644 index 0000000..74b5094 --- /dev/null +++ b/tests/analysis/scan/examples.py @@ -0,0 +1,70 @@ + +from common import RostTestClass +from pychrysalide.analysis.contents import MemoryContent + + +class TestRostExamples(RostTestClass): + """TestCases for the examples provides in the ROST documentation.""" + + def testComments(self): + """Ensure comments do not bother rule definitions.""" + + rule = ''' +/* + Multi-line header... +*/ + +rule test { // comment + + /* + * Some context + */ + + condition: /* List of condition(s) */ + true // Dummy condition + +} +''' + + self.check_rule_success(rule) + + + def testArithmeticPrecedence(self): + """Take care of arithmetic operators precedence.""" + + rule = ''' +rule test { // MulFirst + + condition: + 1 + 4 * (3 + 2) == 21 + and + (1 + 4) * (3 + 2) == 25 + +} +''' + + self.check_rule_success(rule) + + + def testUintCast(self): + """Process nested integer values from binary content.""" + + cnt = MemoryContent(b'\x4d\x5a\x00\x00' + b'\x50\x45\x00\x00' + 52 * b'\x00' + b'\x04\x00\x00\x00') + + rule = ''' +rule test { // IsPE + + condition: + + // MZ signature at offset 0 and ... + + uint16(0) == 0x5a4d and + + // ... PE signature at offset stored in the MZ header at offset 0x3c + + uint32(uint32(0x3c)) == 0x00004550 + +} +''' + + self.check_rule_success(rule, cnt) diff --git a/tests/analysis/scan/expr.py b/tests/analysis/scan/expr.py deleted file mode 100644 index dbe8c55..0000000 --- a/tests/analysis/scan/expr.py +++ /dev/null @@ -1,169 +0,0 @@ - - -from chrysacase import ChrysalideTestCase -from pychrysalide.analysis.scan import ScanExpression -from pychrysalide.glibext import ComparableItem - - -class TestScanExpression(ChrysalideTestCase): - """TestCase for analysis.scan.ScanExpression.""" - - - def testDirectInstances(self): - """Reject direct instances.""" - - with self.assertRaisesRegex(RuntimeError, 'pychrysalide.analysis.scan.ScanExpression is an abstract class'): - - e = ScanExpression() - - - def testBooleanComparison(self): - """Compare custom scan expressions.""" - - class StrLenExpr(ScanExpression): - - def __init__(self, value): - super().__init__(ScanExpression.ExprValueType.STRING) - self._value = value - - def _cmp_rich(self, other, op): - - if op == ComparableItem.RichCmpOperation.EQ: - return len(self._value) == len(other._value) - - - e0 = StrLenExpr('00000000000') - - e1 = StrLenExpr('00000000000') - - e2 = StrLenExpr('000000000000000000000000000') - - self.assertTrue(e0 == e1) - - # !? - # Python teste e0 != e1 (non implémenté), puis e1 != e0 (pareil) et en déduit une différence ! - # self.assertFalse(e0 != e1) - - self.assertFalse(e0 == e2) - - # TypeError: '<' not supported between instances of 'StrLenExpr' and 'StrLenExpr' - with self.assertRaisesRegex(TypeError, '\'<\' not supported between instances'): - self.assertTrue(e0 < e1) - - - - - - - # def testTypeSubclassing(self): - # """Verify the data type subclassing is working.""" - - # class MyType(DataType): - - # def __init__(self, num): - # super(MyType, self).__init__() - # self._num = num - - # def _to_string(self, include): - # return '%x' % self._num - - # def _dup(self): - # return MyType(self._num) - - # tp = MyType(0x123) - - # self.assertEqual(str(tp), '123') - - # tp2 = tp.dup() - - # self.assertEqual(str(tp), str(tp2)) - - - # def testTypeDefaultProperties(self): - # """Check for default values of some type properties.""" - - # class MyPropType(DataType): - # pass - - # tp = MyPropType() - - # self.assertTrue(tp.handle_namespaces) - - # self.assertFalse(tp.is_pointer) - - # self.assertFalse(tp.is_reference) - - # class MyPropType2(DataType): - - # def _handle_namespaces(self): - # return True - - # def _is_pointer(self): - # return 123 < 1234 - - # def _is_reference(self): - # return False - - # tp2 = MyPropType2() - - # self.assertTrue(tp.handle_namespaces) - - # self.assertTrue(tp2.is_pointer) - - # self.assertFalse(tp2.is_reference) - - - # def testTypeNamespaces(self): - # """Test the type namespace property.""" - - # class MyNSType(DataType): - - # def __init__(self, name): - # super(MyNSType, self).__init__() - # self._name = name - - # def _to_string(self, include): - # return self._name - - # tp = MyNSType('TP') - # ns = MyNSType('NS') - - # self.assertIsNone(tp.namespace) - - # tp.namespace = (ns, '.') - - # self.assertEqual(str(tp), 'NS.TP') - - # self.assertEqual(tp.namespace, (ns, '.')) - - - # def testTypeHash(self): - # """Hash a user-defined type.""" - - # class MyUserType(DataType): - - # def __init__(self, name): - # super(MyUserType, self).__init__() - # self._name = name - - # def _hash(self): - # return hash(self._name) - - # tp = MyUserType('random') - - # self.assertEqual(tp.hash, hash('random') & ((1 << 32) - 1)) - - # class MyOutOfRangeUserType(DataType): - - # hard_coded_hash = -8752470794866657507 - - # def __init__(self, name): - # super(MyOutOfRangeUserType, self).__init__() - # self._name = name - - # def _hash(self): - # return self.hard_coded_hash - - # tp = MyOutOfRangeUserType('out-of-range') - - # self.assertEqual(tp.hash, MyOutOfRangeUserType.hard_coded_hash & ((1 << 32) - 1)) diff --git a/tests/analysis/scan/exprs.py b/tests/analysis/scan/exprs.py deleted file mode 100644 index c89dc59..0000000 --- a/tests/analysis/scan/exprs.py +++ /dev/null @@ -1,122 +0,0 @@ - -from chrysacase import ChrysalideTestCase -from pychrysalide.analysis.contents import MemoryContent -from pychrysalide.analysis.scan import ContentScanner -from pychrysalide.analysis.scan import ScanOptions -from pychrysalide.analysis.scan.patterns.backends import AcismBackend - - -class TestScanExpressions(ChrysalideTestCase): - """TestCase for analysis.scan.exprs.*.""" - - @classmethod - def setUpClass(cls): - - super(TestScanExpressions, cls).setUpClass() - - cls._options = ScanOptions() - cls._options.backend_for_data = AcismBackend - - - def testBasicStringOperations(self): - """Evaluate basic string operations.""" - - cnt = MemoryContent(b'empty') - - rule = ''' -rule test { - - condition: - "123abc456" contains "abc" - -} -''' - - scanner = ContentScanner(rule) - ctx = scanner.analyze(self._options, cnt) - - self.assertTrue(ctx.has_match_for_rule('test')) - - rule = ''' -rule test { - - condition: - "123\t456" contains "\t" - -} -''' - - scanner = ContentScanner(rule) - ctx = scanner.analyze(self._options, cnt) - - self.assertTrue(ctx.has_match_for_rule('test')) - - rule = ''' -rule test { - - condition: - "123-456" startswith "1" - -} -''' - - scanner = ContentScanner(rule) - ctx = scanner.analyze(self._options, cnt) - - self.assertTrue(ctx.has_match_for_rule('test')) - - rule = ''' -rule test { - - condition: - "123-456" startswith "1234" - -} -''' - - scanner = ContentScanner(rule) - ctx = scanner.analyze(self._options, cnt) - - self.assertFalse(ctx.has_match_for_rule('test')) - - rule = ''' -rule test { - - condition: - "123-456" endswith "6" - -} -''' - - scanner = ContentScanner(rule) - ctx = scanner.analyze(self._options, cnt) - - self.assertTrue(ctx.has_match_for_rule('test')) - - rule = ''' -rule test { - - condition: - "123-456" endswith "3456" - -} -''' - - scanner = ContentScanner(rule) - ctx = scanner.analyze(self._options, cnt) - - self.assertFalse(ctx.has_match_for_rule('test')) - - rule = ''' -rule test { - - condition: - "ABCD" iequals "AbCd" - -} -''' - - scanner = ContentScanner(rule) - ctx = scanner.analyze(self._options, cnt) - - self.assertTrue(ctx.has_match_for_rule('test')) diff --git a/tests/analysis/scan/func.py b/tests/analysis/scan/func.py deleted file mode 100644 index bd7d0ce..0000000 --- a/tests/analysis/scan/func.py +++ /dev/null @@ -1,16 +0,0 @@ - - -from chrysacase import ChrysalideTestCase -from pychrysalide.analysis.scan import ScanFunction - - -class TestScanFunction(ChrysalideTestCase): - """TestCase for analysis.scan.ScanFunction.""" - - - def testDirectInstances(self): - """Reject direct instances.""" - - with self.assertRaisesRegex(RuntimeError, 'pychrysalide.analysis.scan.ScanFunction is an abstract class'): - - f = ScanFunction('name') diff --git a/tests/analysis/scan/functions.py b/tests/analysis/scan/functions.py new file mode 100644 index 0000000..8553018 --- /dev/null +++ b/tests/analysis/scan/functions.py @@ -0,0 +1,104 @@ + +from common import RostTestClass +from pychrysalide.analysis.contents import MemoryContent + + +class TestRostFunctions(RostTestClass): + """TestCases for the core functions of ROST.""" + + # Core + # ==== + + def testDatasize(self): + """Handle the size of the provided data.""" + + cnt = MemoryContent(b'\x01\x02\x03\x04') + + cases = [ + 'datasize == 4', + 'uint16(0) == 0x201 and uint16(datasize - 2) == 0x0403', + ] + + for c in cases: + + rule = ''' +rule test { + + condition: + %s + +} +''' % c + + self.check_rule_success(rule, cnt) + + + # Modules + # ======= + + def testConsole(self): + """Ensure logging always returns true.""" + + rule = ''' +rule test { + + condition: + console.log() + +} +''' + + self.check_rule_success(rule) + + + def testMagic(self): + """Scan text content with the Magic module.""" + + cnt = MemoryContent(b'aaaa') + + cases = [ + [ 'type', 'ASCII text, with no line terminators' ], + [ 'mime_encoding', 'us-ascii' ], + [ 'mime_type', 'text/plain' ], + ] + + for target, expected in cases: + + rule = ''' +rule test { + + condition: + magic.%s() == "%s" + +} +''' % (target, expected) + + self.check_rule_success(rule, cnt) + + + def testTime(self): + """Check current time.""" + + # Cf. https://www.epochconverter.com/ + + rule = ''' +rule test { + + condition: + time.make(2023, 8, 5, 22, 8, 41) == 0x64cec869 + +} +''' + + self.check_rule_success(rule) + + rule = ''' +rule test { + + condition: + time.now() >= 0x64cec874 and time.now() <= time.now() + +} +''' + + self.check_rule_success(rule) diff --git a/tests/analysis/scan/grammar.py b/tests/analysis/scan/grammar.py index 5a2e1d5..8b18f81 100644 --- a/tests/analysis/scan/grammar.py +++ b/tests/analysis/scan/grammar.py @@ -1,286 +1,191 @@ -from chrysacase import ChrysalideTestCase -from pychrysalide.analysis.contents import MemoryContent -from pychrysalide.analysis.scan import ContentScanner -from pychrysalide.analysis.scan import ScanOptions -from pychrysalide.analysis.scan.patterns.backends import AcismBackend +from common import RostTestClass -class TestRostGrammar(ChrysalideTestCase): - """TestCase for analysis.scan.ScanExpression.""" +class TestRostGrammar(RostTestClass): + """TestCases for the ROST grammar.""" - @classmethod - def setUpClass(cls): + def testRelationalExpressions(self): + """Build expressions with relational comparisons.""" - super(TestRostGrammar, cls).setUpClass() + cases = [ - cls._options = ScanOptions() - cls._options.backend_for_data = AcismBackend + # Regular + [ '-1', '<=', '2', True ], + [ '-1', '<=', '2', True ], + [ '"aaa"', '==', '"aaa"', True ], + [ '"aaa"', '<', '"aaaa"', True ], + [ '""', '<', '"aaaa"', True ], + # Cast + [ 'false', '==', '0', True ], + [ 'false', '==', '1', False ], + [ 'true', '!=', '0', True ], + [ '1', '==', 'true', True ], + [ 'false', '==', '()', True ], + [ 'true', '==', '(0,)', True ], - def testComments(self): - """Ensure comments do not bother rule definitions.""" + ] - cnt = MemoryContent(b'no_real_content') + for op1, kwd, op2, expected in cases: - rule = ''' -/* - Multi-line header... -*/ - -rule test { // comment - - /* - * Some context - */ - - condition: /* List of condition(s) */ - true // Dummy condition - -} -''' - - scanner = ContentScanner(rule) - ctx = scanner.analyze(self._options, cnt) - - self.assertTrue(not(ctx is None) and ctx.has_match_for_rule('test')) - - - def testUintCast(self): - """Process nested integer values from binary content.""" - - cnt = MemoryContent(b'\x4d\x5a\x00\x00' + b'\x50\x45\x00\x00' + 52 * b'\x00' + b'\x04\x00\x00\x00') - - rule = ''' -rule IsPE { - - condition: - - // MZ signature at offset 0 and ... - - uint16(0) == 0x5a4d and - - // ... PE signature at offset stored in the MZ header at offset 0x3c - - uint32(uint32(0x3c)) == 0x00004550 - -} -''' - - scanner = ContentScanner(rule) - ctx = scanner.analyze(self._options, cnt) - - self.assertTrue(not(ctx is None) and ctx.has_match_for_rule('IsPE')) - - - def testBasicBooleanConditions(self): - """Evaluate basic boolean conditions.""" - - cnt = MemoryContent(b'0123') - - rule = ''' -rule test { - - condition: - true and false - -} -''' - - scanner = ContentScanner(rule) - - ctx = scanner.analyze(self._options, cnt) - - self.assertTrue(not(ctx is None)) - self.assertFalse(ctx.has_match_for_rule('test')) - - rule = ''' + rule = ''' rule test { condition: - true or false + %s %s %s } -''' +''' % (op1, kwd, op2) - scanner = ContentScanner(rule) + if expected: + self.check_rule_success(rule) + else: + self.check_rule_failure(rule) - ctx = scanner.analyze(self._options, cnt) - self.assertTrue(not(ctx is None) and ctx.has_match_for_rule('test')) + def testLogicalOperations(self): + """Evaluate some logical operations.""" + cases = [ + [ 'true and false', False ], + [ 'false or false', False ], + [ 'true and true or false', True ], + [ 'false or true and false', False ], + [ '1 or false', True ], + ] - def testArithmeticOpeations(self): - """Compute some arithmetic operations.""" + for cond, expected in cases: - cnt = MemoryContent(b'0123') - - rule = ''' + rule = ''' rule test { condition: - 1 + 4 * 3 + 2 == 15 + %s } -''' - - scanner = ContentScanner(rule) - - ctx = scanner.analyze(self._options, cnt) +''' % (cond) - self.assertTrue(not(ctx is None) and ctx.has_match_for_rule('test')) + if expected: + self.check_rule_success(rule) + else: + self.check_rule_failure(rule) - rule = ''' -rule test { + def testArithmeticOperations(self): + """Evaluate some arithmetic operations.""" - condition: - (1 + 4) * 3 + 2 == 17 + cases = [ -} -''' + # Clever + '1 + 2 == 3', + '10 + -3 == 7', + '-3 + 10 == 7', + '-10 - 1 < 0', + '-10 - 1 == -11', + '(-10 - 1) == -11', + '(-1 - -10) == 9', + '-2 * -3 == 6', + '-2 * 3 == -6', - scanner = ContentScanner(rule) + # Legacy + '1 + 4 * 3 + 2 == 15', + '(1 + 4) * 3 + 2 == 17', + '1 + 4 * (3 + 2) == 21', + '(1 + 4) * (3 + 2) == 25', - ctx = scanner.analyze(self._options, cnt) + ] - self.assertTrue(not(ctx is None) and ctx.has_match_for_rule('test')) + for c in cases: - - rule = ''' + rule = ''' rule test { condition: - 1 + 4 * (3 + 2) == 21 + %s } -''' +''' % (c) - scanner = ContentScanner(rule) + self.check_rule_success(rule) - ctx = scanner.analyze(self._options, cnt) - self.assertTrue(not(ctx is None) and ctx.has_match_for_rule('test')) + def testBasicStringsOperations(self): + """Build expressions with basic strings operations.""" + cases = [ - rule = ''' -rule test { + # Clever + [ '123---456', 'contains', '---', True ], + [ '123---456', 'contains', 'xxx', False ], + [ '---123---456', 'startswith', '---', True ], + [ '---123---456', 'startswith', 'xxx', False ], + [ '123---456---', 'endswith', '---', True ], + [ '123---456---', 'endswith', 'xxx', False ], + [ 'AAA---BBB', 'icontains', 'aaa', True ], + [ 'AAA---BBB', 'icontains', 'xxx', False ], + [ 'AAA---BBB', 'istartswith', 'aAa', True ], + [ 'AAA---BBB', 'istartswith', 'xxx', False ], + [ 'AAA---BBB', 'iendswith', 'bBb', True ], + [ 'AAA---BBB', 'iendswith', 'xxx', False ], + [ 'AzertY', 'iequals', 'AZERTY', True ], + [ 'AzertY', 'iequals', 'AZERTY-', False ], - condition: - (1 + 4) * (3 + 2) == 25 - -} -''' + # Legacy + [ '123\t456', 'contains', '\t', True ], + [ '123-456', 'startswith', '1', True ], + [ '123-456', 'startswith', '1234', False ], + [ '123-456', 'endswith', '6', True ], + [ '123-456', 'endswith', '3456', False ], - scanner = ContentScanner(rule) + ] - ctx = scanner.analyze(self._options, cnt) + for op1, kwd, op2, expected in cases: - self.assertTrue(not(ctx is None) and ctx.has_match_for_rule('test')) - - - def testSizeUnits(self): - """Evaluate size units.""" - - cnt = MemoryContent(b'0123') - - - rule = ''' -rule test { - - condition: - 1KB == 1024 - -} -''' - - scanner = ContentScanner(rule) - - ctx = scanner.analyze(self._options, cnt) - - self.assertTrue(not(ctx is None) and ctx.has_match_for_rule('test')) - - - rule = ''' + rule = ''' rule test { condition: - 2MB == 2 * 1024 * 1024 + "%s" %s "%s" } -''' +''' % (op1, kwd, op2) - scanner = ContentScanner(rule) + if expected: + self.check_rule_success(rule) + else: + self.check_rule_failure(rule) - ctx = scanner.analyze(self._options, cnt) - - self.assertTrue(not(ctx is None) and ctx.has_match_for_rule('test')) - - - rule = ''' -rule test { - - condition: - 4Kb == (4 * 1024) - -} -''' - - scanner = ContentScanner(rule) - - ctx = scanner.analyze(self._options, cnt) - - self.assertTrue(not(ctx is None) and ctx.has_match_for_rule('test')) - - - rule = ''' -rule test { - - condition: - 1KB <= 1024 and 1024 < 1MB - -} -''' - - scanner = ContentScanner(rule) - - ctx = scanner.analyze(self._options, cnt) - - self.assertTrue(not(ctx is None) and ctx.has_match_for_rule('test')) + def testSizeUnits(self): + """Evaluate size units.""" - def testNamespace(self): - """Resolve main functions with the root scan namesapce.""" + cases = [ + '1KB == 1024', + '2MB == 2 * 1024 * 1024', + '4Kb == (4 * 1024)', + '1KB <= 1024 and 1024 < 1MB', + ] - cnt = MemoryContent(b'\x01\x02\x03\x04') + for c in cases: - rule = ''' + rule = ''' rule test { condition: - datasize == 4 + %s } -''' +''' % (c) - scanner = ContentScanner(rule) + self.check_rule_success(rule) - ctx = scanner.analyze(self._options, cnt) - self.assertTrue(not(ctx is None) and ctx.has_match_for_rule('test')) - rule = ''' -rule test { - condition: - uint16(0) == 0x201 and uint16(datasize - 2) == 0x0403 -} -''' +# TODO : test <haystack> matches <regex> - scanner = ContentScanner(rule) - ctx = scanner.analyze(self._options, cnt) - self.assertTrue(not(ctx is None) and ctx.has_match_for_rule('test')) diff --git a/tests/analysis/scan/matches.py b/tests/analysis/scan/matches.py new file mode 100644 index 0000000..0d7556e --- /dev/null +++ b/tests/analysis/scan/matches.py @@ -0,0 +1,27 @@ + +from common import RostTestClass +from pychrysalide.analysis.contents import MemoryContent + + +class TestRostMatchs(RostTestClass): + """TestCases for the ROST pattern matching engine.""" + + def testCountMatches(self): + """Count matches patterns.""" + + cnt = MemoryContent(b'aaa aaa bbb aaa') + + rule = ''' +rule test { + + strings: + $a = "aaa" + $b = "bbb" + + condition: + #a == 3 and #b < 2 + +} +''' + + self.check_rule_success(rule, cnt) diff --git a/tests/analysis/scan/options.py b/tests/analysis/scan/options.py deleted file mode 100644 index 7617b3a..0000000 --- a/tests/analysis/scan/options.py +++ /dev/null @@ -1,16 +0,0 @@ - - -from chrysacase import ChrysalideTestCase -from gi._constants import TYPE_INVALID -from pychrysalide.analysis.scan import ScanOptions - - -class TestScanOptions(ChrysalideTestCase): - """TestCase for analysis.scan.ScanOptions.""" - - def testEmptyOptions(self): - """Check default scan options.""" - - ops = ScanOptions() - - self.assertEqual(ops.backend_for_data, TYPE_INVALID) diff --git a/tests/analysis/scan/pyapi.py b/tests/analysis/scan/pyapi.py new file mode 100644 index 0000000..1bba44e --- /dev/null +++ b/tests/analysis/scan/pyapi.py @@ -0,0 +1,58 @@ + +from chrysacase import ChrysalideTestCase +from gi._constants import TYPE_INVALID +from pychrysalide.analysis.scan import ScanExpression +from pychrysalide.analysis.scan import ScanOptions +from pychrysalide.glibext import ComparableItem + + +class TestRostPythonAPI(ChrysalideTestCase): + """TestCase for the ROST Python API.""" + + def testEmptyOptions(self): + """Check default scan options.""" + + ops = ScanOptions() + + self.assertEqual(ops.backend_for_data, TYPE_INVALID) + + + def testDirectInstancesOfExpression(self): + """Reject direct instances of ROST expressions.""" + + with self.assertRaisesRegex(RuntimeError, 'pychrysalide.analysis.scan.ScanExpression is an abstract class'): + + e = ScanExpression() + + def testBooleanComparison(self): + """Compare custom scan expressions.""" + + class StrLenExpr(ScanExpression): + + def __init__(self, value): + super().__init__(ScanExpression.ExprValueType.STRING) + self._value = value + + def _cmp_rich(self, other, op): + + if op == ComparableItem.RichCmpOperation.EQ: + return len(self._value) == len(other._value) + + + e0 = StrLenExpr('00000000000') + + e1 = StrLenExpr('00000000000') + + e2 = StrLenExpr('000000000000000000000000000') + + self.assertTrue(e0 == e1) + + # !? + # Python teste e0 != e1 (non implémenté), puis e1 != e0 (pareil) et en déduit une différence ! + # self.assertFalse(e0 != e1) + + self.assertFalse(e0 == e2) + + # TypeError: '<' not supported between instances of 'StrLenExpr' and 'StrLenExpr' + with self.assertRaisesRegex(TypeError, '\'<\' not supported between instances'): + self.assertTrue(e0 < e1) diff --git a/tests/analysis/scan/sets.py b/tests/analysis/scan/sets.py new file mode 100644 index 0000000..1d10fbf --- /dev/null +++ b/tests/analysis/scan/sets.py @@ -0,0 +1,118 @@ + +from common import RostTestClass + + +class TestRostSets(RostTestClass): + """TestCases for sets support in ROST.""" + + def testSetsAsBooleans(self): + """Convert sets to boolean.""" + + rule = ''' +rule test { + + condition: + () + +} +''' + + self.check_rule_failure(rule) + + + rule = ''' +rule test { + + condition: + (1, ) + +} +''' + + self.check_rule_success(rule) + + + rule = ''' +rule test { + + condition: + ("aaa", true, 123) + +} +''' + + self.check_rule_success(rule) + + + def testStringAsArray(self): + """Handle strings as arrays.""" + + rule = ''' +rule test { + + condition: + count("aaa") + +} +''' + + self.check_rule_success(rule) + + + rule = ''' +rule test { + + condition: + count("aaa") == 3 + +} +''' + + self.check_rule_success(rule) + + + def testSetsIntersections(self): + """Perform sets intersections.""" + + rule = ''' +rule test { + + condition: + ("aaa", "bbb") in ("AAA", "BBB", "aaa") + +} +''' + + self.check_rule_success(rule) + + + rule = ''' +rule test { + + condition: + ("aaa", "bbb") in ("123", ) + +} +''' + + self.check_rule_failure(rule) + + + + + + + + + + + + + + # TODO : + + # test : intersection(a, a) == a + + # test : "123" in "0123456789" + # test : "123" in "012987" + |