summaryrefslogtreecommitdiff
path: root/tests/analysis/scan
diff options
context:
space:
mode:
authorCyrille Bagard <nocbos@gmail.com>2023-08-06 16:54:57 (GMT)
committerCyrille Bagard <nocbos@gmail.com>2023-08-06 16:54:57 (GMT)
commit4fcc35a52ccb025b6d803d85e017931cd2452960 (patch)
treee95920f16c273e41f9cae1ea2f02571c221a514e /tests/analysis/scan
parent74d062d4ec55d7ac3914bbf64b8b6c5ab52227df (diff)
Extend the ROST grammar with a first batch of new features.
Diffstat (limited to 'tests/analysis/scan')
-rw-r--r--tests/analysis/scan/booleans.py98
-rw-r--r--tests/analysis/scan/common.py52
-rw-r--r--tests/analysis/scan/examples.py70
-rw-r--r--tests/analysis/scan/expr.py169
-rw-r--r--tests/analysis/scan/exprs.py122
-rw-r--r--tests/analysis/scan/func.py16
-rw-r--r--tests/analysis/scan/functions.py104
-rw-r--r--tests/analysis/scan/grammar.py327
-rw-r--r--tests/analysis/scan/matches.py27
-rw-r--r--tests/analysis/scan/options.py16
-rw-r--r--tests/analysis/scan/pyapi.py58
-rw-r--r--tests/analysis/scan/sets.py118
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"
+