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 | 54 | ||||
| -rw-r--r-- | tests/analysis/scan/examples.py | 70 | ||||
| -rw-r--r-- | tests/analysis/scan/functions.py | 239 | ||||
| -rw-r--r-- | tests/analysis/scan/fuzzing.py | 289 | ||||
| -rw-r--r-- | tests/analysis/scan/grammar.py | 484 | ||||
| -rw-r--r-- | tests/analysis/scan/matches.py | 64 | ||||
| -rw-r--r-- | tests/analysis/scan/pyapi.py | 297 | ||||
| -rw-r--r-- | tests/analysis/scan/scanning_hex.py | 716 | ||||
| -rw-r--r-- | tests/analysis/scan/scanning_str.py | 194 | ||||
| -rw-r--r-- | tests/analysis/scan/sets.py | 118 | 
11 files changed, 2623 insertions, 0 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..507b7e2 --- /dev/null +++ b/tests/analysis/scan/common.py @@ -0,0 +1,54 @@ + +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')) + +        return scanner, ctx + + +    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/functions.py b/tests/analysis/scan/functions.py new file mode 100644 index 0000000..6aca957 --- /dev/null +++ b/tests/analysis/scan/functions.py @@ -0,0 +1,239 @@ + +from common import RostTestClass +from pychrysalide.analysis.contents import MemoryContent + + +class TestRostFunctions(RostTestClass): +    """TestCases for the core functions of ROST.""" + +    # Core +    # ==== + +    def testSetCounter(self): +        """Count quantities and set sizes.""" + +        rule = ''' +rule test { + +   condition: +      count("ABC") == 3 +      and count("AB", "C") == count("ABC") + +} +''' + +        self.check_rule_success(rule) + + +        cnt = MemoryContent(b'\x01\x02\x02\x03\x03\x03') + +        rule = ''' +rule test { + +    bytes: +        $int_01 = "\x01" +        $int_02 = "\x02" +        $int_3 = "\x03" + +    condition: +        count($int_0*, $int_3) == #int_* + +} +''' + +        self.check_rule_success(rule, cnt) + + +    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) + + +    def testMaxCommon(self): +        """Count the largest quantity of same items in a set.""" + +        cnt = MemoryContent(b'') + +        cases = [ +            [ '1', 1 ], +            [ '1, 2, 3', 1 ], +            [ '1, 2, 1, 3, 1', 3 ], +            [ '1, "a", 2, 3, "a"', 2 ], +        ] + +        for c, q in cases: + +            rule = ''' +rule test { + +   condition: +      maxcommon(%s) == %u + +} +''' % (c, q) + +            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 testMathOperations(self): +        """Perform math operations with core functions.""" + +        rule = ''' +rule test { + +   condition: +      math.to_string(123) == "123" +         and math.to_string(291, 16) == "0x123" +         and math.to_string(-83, 8) == "-0123" +         and math.to_string(123, 2) == "0b1111011" + +} +''' + +        self.check_rule_success(rule) + + +    def testStringOperations(self): +        """Perform string operations with core functions.""" + +        rule = ''' +rule test { + +   condition: +      string.lower("ABCd") == "abcd" and string.lower("123abc") == "123abc" + +} +''' + +        self.check_rule_success(rule) + + +        rule = ''' +rule test { + +   condition: +      string.upper("abcD") == "ABCD" and string.upper("123ABC") == "123ABC" + +} +''' + +        self.check_rule_success(rule) + + +        rule = ''' +rule test { + +   condition: +      string.to_int("123") == 123 +         and string.to_int("123", 16) == 291 +         and string.to_int("0x123") == 291 +         and string.to_int("-0123") == -83 + +} +''' + +        self.check_rule_success(rule) + + +        rule = r''' +rule test { + +   condition: +      "A\x00B\x00C\x00D\x00" endswith string.wide("CD") +          and "A\x00B\x00C\x00D\x00" contains string.wide("BC") + +} +''' + +        self.check_rule_success(rule) + + +    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/fuzzing.py b/tests/analysis/scan/fuzzing.py new file mode 100644 index 0000000..1b9b25b --- /dev/null +++ b/tests/analysis/scan/fuzzing.py @@ -0,0 +1,289 @@ + +from common import RostTestClass +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 pychrysalide.analysis.scan.patterns.backends import BitapBackend + + +class TestRostFuzzingFixes(RostTestClass): +    """TestCases to remember all the fixes for crashes identified by fuzzing.""" + +    def testEmptyPatternListWithContent(self): +        """Check no backend is run if there is no pattern to look for.""" + +        content = MemoryContent(b'\n') + +        rule = ''' +''' + +        backends = [ +            AcismBackend, # This one was segfaulting +            BitapBackend, +        ] + +        for b in backends: + +            options = ScanOptions() +            options.backend_for_data = b + +            scanner = ContentScanner(rule) +            ctx = scanner.analyze(options, content) + +            self.assertIsNotNone(ctx) + + +    def testMandatoryCondition(self): +        """Ensure a condition section exists in a rule.""" + +        rule = ''' +rule test { + +} +''' + +        with self.assertRaisesRegex(ValueError, 'Unable to create content scanner'): + +            scanner = ContentScanner(rule) + + +    def testNonExistingPattern(self): +        """Avoid to count the matches of a non-existing pattern.""" + +        rule = ''' +rule test { + +   condition: +      #badid + +} +''' + +        with self.assertRaisesRegex(ValueError, 'Unable to create content scanner'): + +            scanner = ContentScanner(rule) + + +    def testNamespacesWithoutReductionCode(self): +        """Clean the code for ROST namespaces.""" + +        rule = ''' +rule test { + +   condition: +      console + +} +''' + +        self.check_rule_failure(rule) + + +    def testCallOnNonCallable(self): +        """Reject calls on non callable expressions softly.""" + +        rule = ''' +rule test { + +   condition: +      console.log().log() + +} +''' + +        self.check_rule_failure(rule) + + +    def testSelfReferencingRule(self): +        """Reject any rule referencing itself as match condition.""" + +        rule = ''' +rule test { + +   condition: +      test + +} +''' + +        self.check_rule_failure(rule) + + +    def testSelfReferencingRule(self): +        """Expect only one argument for the not operator, even in debug mode.""" + +        rule = ''' +rule test { + +   condition: +      not(0) + +} +''' + +        self.check_rule_success(rule) + + +    def testNoCommon(self): +        """Handle the case where no common item is found from an empty set.""" + +        rule = ''' +rule test { + +   bytes: +      $a = "a" + +   condition: +      maxcommon($a) == 0 + +} +''' + +        self.check_rule_success(rule) + + +    def testAAsAcharacter(self): +        """Consider the 'a' character as a valid lowercase character.""" + +        rule = ''' +rule test { + +   bytes: +      $a = "0000a0I0" nocase + +   condition: +      $a + +} +''' + +        self.check_rule_failure(rule) + + +    def testAAsAcharacter(self): +        """Do not expect initialized trackers when there is no real defined search pattern.""" + +        rule = ''' +rule test { + +   bytes: +      $a = {[0]} + +   condition: +      $a + +} +''' + +        with self.assertRaisesRegex(ValueError, 'Unable to create content scanner'): + +            scanner = ContentScanner(rule) + + +    def testAllocations(self): +        """Handle big alloctions for strings in conditions with regular expressions.""" + +        rule = ''' +rule test { + +   condition: +      "%s" == "%s" + +} +''' % ("0" * (256 * 2 + 8), "0" * (256 * 2 + 8)) + +        self.check_rule_success(rule) + + +    def testFileFinalAccess(self): +        """Ensure patterns found at the edges of scanned content do not crash the scanner.""" + +        cnt = MemoryContent(bytes([ 0 for i in range(16) ])) + +        rule = ''' +rule test { + +   bytes: +      $a = { 00 00 00 00 00 00 00 00 } + +   condition: +      $a + +} +''' + +        self.check_rule_success(rule, cnt) + + +    def testValidHexRangeMerge(self): +        """Merge valid hexadecimal ranges.""" + +        rule = ''' +rule test { + +   bytes: +      $a = { [0] ?? } + +   condition: +      $a + +} +''' + +        with self.assertRaisesRegex(ValueError, 'Unable to create content scanner'): + +            scanner = ContentScanner(rule) + + +        rule = ''' +rule test { + +   bytes: +      $a = { [2] ?? } + +   condition: +      $a + +} +''' + +        self.check_rule_failure(rule) + + +    def testSmallBase64(self): +        """Handle small base64 encodings which may produce few patterns.""" + +        rule = ''' +rule test { + +   bytes: +      $a = "0" base64 + +   condition: +      $a + +} +''' + +        self.check_rule_failure(rule) + + +    def testCountIndex(self): +        """Ban pattern count indexes from the grammer.""" + +        rule = ''' +rule test { + +   bytes: +      $a = "1" + +   condition: +      #*[0] + +} +''' + +        with self.assertRaisesRegex(ValueError, 'Unable to create content scanner'): + +            scanner = ContentScanner(rule) diff --git a/tests/analysis/scan/grammar.py b/tests/analysis/scan/grammar.py new file mode 100644 index 0000000..14f67fa --- /dev/null +++ b/tests/analysis/scan/grammar.py @@ -0,0 +1,484 @@ + +import json + +from common import RostTestClass +from pychrysalide.analysis.contents import MemoryContent + + +class TestRostGrammar(RostTestClass): +    """TestCases for the ROST grammar.""" + +    def testRelationalExpressions(self): +        """Build expressions with relational comparisons.""" + +        cases = [ + +            # 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 ], + +        ] + +        for op1, kwd, op2, expected in cases: + +            rule = ''' +rule test { + +   condition: +      %s %s %s + +} +''' % (op1, kwd, op2) + +            if expected: +                self.check_rule_success(rule) +            else: +                self.check_rule_failure(rule) + + +    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 ], +        ] + +        for cond, expected in cases: + +            rule = ''' +rule test { + +   condition: +      %s + +} +''' % (cond) + +            if expected: +                self.check_rule_success(rule) +            else: +                self.check_rule_failure(rule) + + +    def testArithmeticOperations(self): +        """Evaluate some arithmetic operations.""" + +        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', + +            # Legacy +            '1 + 4 * 3 + 2 == 15', +            '(1 + 4) * 3 + 2 == 17', +            '1 + 4 * (3 + 2) == 21', +            '(1 + 4) * (3 + 2) == 25', + +        ] + +        for c in cases: + +            rule = ''' +rule test { + +   condition: +      %s + +} +''' % (c) + +            self.check_rule_success(rule) + + +    def testBasicStringsOperations(self): +        """Build expressions with basic strings operations.""" + +        cases = [ + +            # 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 ], + +            # 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 ], + +        ] + +        for op1, kwd, op2, expected in cases: + +            rule = ''' +rule test { + +   condition: +      "%s" %s "%s" + +} +''' % (op1, kwd, op2) + +            if expected: +                self.check_rule_success(rule) +            else: +                self.check_rule_failure(rule) + + +    def testSizeUnits(self): +        """Evaluate size units.""" + +        cases = [ +            '1KB == 1024', +            '2MB == 2 * 1024 * 1024', +            '4Kb == (4 * 1024)', +            '1KB <= 1024 and 1024 < 1MB', +        ] + +        for c in cases: + +            rule = ''' +rule test { + +   condition: +      %s + +} +''' % (c) + +            self.check_rule_success(rule) + + +    def testPrivateRules(self): +        """Ensure private rules remain silent.""" + +        for private in [ True, False ]: +            for state in [ True, False ]: + +                rule = ''' +%srule silent { + +   condition: +      %s + +} + +rule test { + +   condition: +      silent + +} +''' % ('private ' if private else '', 'true' if state else 'false') + +                scanner, ctx = self._validate_rule_result(rule, self._empty_content, state) + +                data = scanner.convert_to_json(ctx) +                jdata = json.loads(data) + +                # Exemple : +                # +                # [{'bytes_patterns': [], 'matched': True, 'name': 'test'}, +                #  {'bytes_patterns': [], 'matched': True, 'name': 'silent'}] + +                found = len([ j['name'] for j in jdata if j['name'] == 'silent' ]) > 0 + +                self.assertTrue(private ^ found) + + +    def testGlobalRules(self): +        """Take global rules into account.""" + +        for glob_state in [ True, False ]: +            for state in [ True, False ]: + +                rule = ''' +%srule silent { + +   condition: +      %s + +} + +rule test { + +   condition: +      true + +} +''' % ('global ' if glob_state else '', 'true' if state else 'false') + +                expected = not(glob_state) or state + +                if expected: +                    self.check_rule_success(rule) +                else: +                    self.check_rule_failure(rule) + + +    def testMatchCount(self): +        """Ensure match count provides expected values.""" + +        cnt = MemoryContent(b'\x01\x02\x02\x03\x03\x03') + +        rule = ''' +rule test { + +    bytes: +        $int_01 = "\x01" +        $int_02 = "\x02" +        $int_03 = "\x03" + +    condition: +        #int_01 == count($int_01) and #int_01 == 1 +        and #int_02 == count($int_02) and #int_02 == 2 +        and #int_03 == count($int_03) and #int_03 == 3 +        and #int_0* == count($int_0*) and #int_0* == 6 + +} +''' + +        self.check_rule_success(rule, cnt) + + +    def testBackingUpHandlers(self): +        """Ensure handlers for backing up removals do not limit the grammar.""" + +        cnt = MemoryContent(b'AB12') + +        # Uncompleted token in rule definition: '?? ?? ' + +        rule = ''' +rule test { + +   bytes: +      $a = { ?? ?? } + +   condition: +      #a == 3 + +} +''' + +        self.check_rule_success(rule, content=cnt) + +        # Uncompleted token in rule definition: '?? ' + +        rule = ''' +rule test { + +   bytes: +      $a = { ?? 4? } + +   condition: +      #a == 1 + +} +''' + +        self.check_rule_success(rule, content=cnt) + +        # Uncompleted token in rule definition: '?? ?' + +        rule = ''' +rule test { + +   bytes: +      $a = { ?? ?2 } + +   condition: +      #a == 2 + +} +''' + +        self.check_rule_success(rule, content=cnt) + +        # Uncompleted token in rule definition: '?? ' + +        rule = ''' +rule test { + +   bytes: +      $a = { ?? 42 } + +   condition: +      #a == 1 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        # Uncompleted token in rule definition: '?1 ?' + +        rule = ''' +rule test { + +   bytes: +      $a = { ?1 ?? } + +   condition: +      #a == 2 + +} +''' + +        self.check_rule_success(rule, content=cnt) + +        # Uncompleted token in rule definition: '?1 4? ' + +        rule = ''' +rule test { + +   bytes: +      $a = { ?1 4? } + +   condition: +      #a == 1 + +} +''' + +        self.check_rule_success(rule, content=cnt) + +        # Uncompleted token in rule definition: '?1 ?2 ' + +        rule = ''' +rule test { + +   bytes: +      $a = { ?1 ?2 } + +   condition: +      #a == 2 + +} +''' + +        self.check_rule_success(rule, content=cnt) + +        # Uncompleted token in rule definition: '?1 4' + +        rule = ''' +rule test { + +   bytes: +      $a = { ?1 42 } + +   condition: +      #a == 1 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        # Uncompleted token in rule definition: '41 ' + +        rule = ''' +rule test { + +   bytes: +      $a = { 41 ?? } + +   condition: +      #a == 1 + +} +''' + +        self.check_rule_success(rule, content=cnt) + +        # Uncompleted token in rule definition: '41 4' + +        rule = ''' +rule test { + +   bytes: +      $a = { 41 4? } + +   condition: +      #a == 1 + +} +''' + +        self.check_rule_success(rule, content=cnt) + +        # Uncompleted token in rule definition: '41 ' + +        rule = ''' +rule test { + +   bytes: +      $a = { 41 ?2 } + +   condition: +      #a == 1 + +} +''' + +        self.check_rule_success(rule, content=cnt) + +        # Uncompleted token in rule definition: '41 42 ' + +        rule = ''' +rule test { + +   bytes: +      $a = { 41 42 } + +   condition: +      #a == 1 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + + + +# TODO : test     <haystack> matches <regex> + + + diff --git a/tests/analysis/scan/matches.py b/tests/analysis/scan/matches.py new file mode 100644 index 0000000..efcae4f --- /dev/null +++ b/tests/analysis/scan/matches.py @@ -0,0 +1,64 @@ + +from common import RostTestClass +from pychrysalide.analysis.contents import MemoryContent + + +class TestRostMatchs(RostTestClass): +    """TestCases for the ROST pattern matching engine.""" + +    def testCountMatches(self): +        """Count matched patterns.""" + +        cnt = MemoryContent(b'aaa aaa bbb aaa') + +        rule = ''' +rule test { + +   bytes: +      $a = "aaa" +      $b = "bbb" + +   condition: +      #a == 3 and #b < 2 + +} +''' + +        self.check_rule_success(rule, cnt) + + +    def testCountSameMatches(self): +        """Count matches of similar patterns.""" + +        cnt = MemoryContent(b'ABCDabcdABCDabcd') + +        rule = ''' +rule test { + +   bytes: +      $a = "\x61\x62\x63\x64" +      $b = "\x61\x62\x63\x64" + +   condition: +      #a == 2 and #b == 2 + +} +''' + +        self.check_rule_success(rule, cnt) + + +        rule = ''' +rule test { + +   bytes: +      $a = "\x61\x62\x63\x64" +      $b = "\x61\x62\x63" + +   condition: +      #a == 2 and #b == 2 + +} +''' + +        self.check_rule_success(rule, cnt) diff --git a/tests/analysis/scan/pyapi.py b/tests/analysis/scan/pyapi.py new file mode 100644 index 0000000..7a697b3 --- /dev/null +++ b/tests/analysis/scan/pyapi.py @@ -0,0 +1,297 @@ + +import binascii +import struct + +from chrysacase import ChrysalideTestCase +from gi._constants import TYPE_INVALID +from pychrysalide.analysis.scan import ScanExpression +from pychrysalide.analysis.scan import ScanOptions +from pychrysalide.analysis.scan import find_token_modifiers_for_name +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.ScanReductionState.REDUCED) +                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 testBytePatternModifiers(self): +        """Validate the bytes produced by modifiers.""" + +        mod = find_token_modifiers_for_name('plain') +        self.assertIsNotNone(mod) + +        source = b'ABC' +        transformed = mod.transform(source) + +        self.assertEqual(source, transformed[0]) + + +        mod = find_token_modifiers_for_name('hex') +        self.assertIsNotNone(mod) + +        source = b'ABC' +        transformed = mod.transform(source) + +        self.assertEqual(binascii.hexlify(source), transformed[0]) + + +        mod = find_token_modifiers_for_name('rev') +        self.assertIsNotNone(mod) + +        source = b'ABC' +        transformed = mod.transform(source) + +        self.assertEqual(source[::-1], transformed[0]) + + +        mod = find_token_modifiers_for_name('lower') +        self.assertIsNotNone(mod) + +        source = b'AbC' +        transformed = mod.transform(source) + +        self.assertEqual(source.lower(), transformed[0]) + + +        mod = find_token_modifiers_for_name('upper') +        self.assertIsNotNone(mod) + +        source = b'AbC' +        transformed = mod.transform(source) + +        self.assertEqual(source.upper(), transformed[0]) + + +        mod = find_token_modifiers_for_name('wide') +        self.assertIsNotNone(mod) + +        source = b'ABC' +        transformed = mod.transform(source) + +        self.assertEqual(source.decode('ascii'), transformed[0].decode('utf-16-le')) + + +        mod = find_token_modifiers_for_name('base64') +        self.assertIsNotNone(mod) + +        source = b'ABC' +        transformed = mod.transform(source) + +        self.assertEqual(len(transformed), 3) +        self.assertEqual(transformed[0], b'QUJD') +        self.assertEqual(transformed[1], b'FCQ') +        self.assertEqual(transformed[2], b'BQk') + + +    def testClassicalAPIHashing(self): +        """Reproduce classical API Hashing results.""" + +        def b2i(t): +            return struct.unpack('<I', t)[0] + + +        # Example: +        #  - PlugX (2020) - https://vms.drweb.fr/virus/?i=21512304 + +        mod = find_token_modifiers_for_name('crc32') +        self.assertIsNotNone(mod) + +        source = b'GetCurrentProcess\x00' +        transformed = mod.transform(source) + +        self.assertEqual(b2i(transformed[0]), 0x3690e66) + + +        # Example: +        #  - GuLoader (2020) - https://www.crowdstrike.com/blog/guloader-malware-analysis/ + +        mod = find_token_modifiers_for_name('djb2') +        self.assertIsNotNone(mod) + +        source = b'GetProcAddress' +        transformed = mod.transform(source) + +        self.assertEqual(b2i(transformed[0]), 0xcf31bb1f) + + +    def testCustomAPIHashing(self): +        """Reproduce custom API Hashing results.""" + +        def b2i(t): +            return struct.unpack('<I', t)[0] + + +        # Example: +        # Underminer Exploit Kit (2019) - https://jsac.jpcert.or.jp/archive/2019/pdf/JSAC2019_1_koike-nakajima_jp.pdf + +        mod = find_token_modifiers_for_name('add1505-shl5') +        self.assertIsNotNone(mod) + +        source = b'LoadLibraryA' +        transformed = mod.transform(source) + +        self.assertEqual(b2i(transformed[0]), 0x5fbff0fb) + + +        # Example: +        # Enigma Stealer (2023) https://www.trendmicro.com/es_mx/research/23/b/enigma-stealer-targets-cryptocurrency-industry-with-fake-jobs.html + +        mod = find_token_modifiers_for_name('enigma-murmur') +        self.assertIsNotNone(mod) + +        source = b'CreateMutexW' +        transformed = mod.transform(source) + +        self.assertEqual(b2i(transformed[0]), 0xfd43765a) + + +        # Examples: +        #  - ShadowHammer (2019) - https://blog.f-secure.com/analysis-shadowhammer-asus-attack-first-stage-payload/ +        #  - ShadowHammer (2019) - https://securelist.com/operation-shadowhammer-a-high-profile-supply-chain-attack/90380/ + +        mod = find_token_modifiers_for_name('imul21-add') +        self.assertIsNotNone(mod) + +        source = b'VirtualAlloc' +        transformed = mod.transform(source) + +        self.assertEqual(b2i(transformed[0]), 0xdf894b12) + + +        # Examples: +        #  - Bottle Exploit Kit (2019) - https://nao-sec.org/2019/12/say-hello-to-bottle-exploit-kit.html +        #  - ShadowHammer (2019) - https://securelist.com/operation-shadowhammer-a-high-profile-supply-chain-attack/90380/ + +        mod = find_token_modifiers_for_name('imul83-add') +        self.assertIsNotNone(mod) + +        source = b'GetProcAddress' +        transformed = mod.transform(source) + +        self.assertEqual(b2i(transformed[0]), 0x9ab9b854) + + +        # Examples: +        #  - ?? (2021) - https://www.threatspike.com/blogs/reflective-dll-injection +        #  - Mustang Panda (2022) - https://blog.talosintelligence.com/mustang-panda-targets-europe/ + +        mod = find_token_modifiers_for_name('ror13') +        self.assertIsNotNone(mod) + +        source = b'GetProcAddress' +        transformed = mod.transform(source) + +        self.assertEqual(b2i(transformed[0]), 0x7c0dfcaa) + +        source = b'VirtualAlloc' +        transformed = mod.transform(source) + +        self.assertEqual(b2i(transformed[0]), 0x91afca54) + + +        # Example: +        #  - Energetic Bear (2019) - https://insights.sei.cmu.edu/blog/api-hashing-tool-imagine-that/ + +        mod = find_token_modifiers_for_name('sll1-add-hash32') +        self.assertIsNotNone(mod) + +        source = b'LoadLibraryA' +        transformed = mod.transform(source) + +        self.assertEqual(b2i(transformed[0]), 0x000d5786) + + +        # Example: +        #  - SideWinder/WarHawk (2022) - https://www.zscaler.com/blogs/security-research/warhawk-new-backdoor-arsenal-sidewinder-apt-group + +        mod = find_token_modifiers_for_name('sub42') +        self.assertIsNotNone(mod) + +        source = b'LoadLibraryA' +        transformed = mod.transform(source) + +        self.assertEqual(transformed[0], b'\x8e\xb1\xa3\xa6\x8e\xab\xa4\xb4\xa3\xb4\xbb\x83') + + +        # Example: +        #  - TrickBot (2021) - https://medium.com/walmartglobaltech/trickbot-crews-new-cobaltstrike-loader-32c72b78e81c + +        mod = find_token_modifiers_for_name('sub-index1') +        self.assertIsNotNone(mod) + +        source = b'raw.githubusercontent.com' +        transformed = mod.transform(source) + +        self.assertEqual(transformed[0], b'\x73\x63\x7a\x32\x6c\x6f\x7b\x70\x7e\x6c\x80\x7f\x72\x80\x72\x7f\x7f\x86\x78\x82\x89\x44\x7a\x87\x86') + + +    def testBytePatternModifiersAPI(self): +        """Validate the API for pattern modifiers.""" + +        mod = find_token_modifiers_for_name('plain') +        self.assertIsNotNone(mod) + +        source = [ b'ABC', b'01234' ] +        transformed = mod.transform(source) + +        self.assertEqual(len(source), len(transformed)) +        self.assertEqual(source[0], transformed[0]) +        self.assertEqual(source[1], transformed[1]) + + +        mod = find_token_modifiers_for_name('xor') +        self.assertIsNotNone(mod) + +        source = [ b'ABC' ] +        transformed = mod.transform(source, 0x20) + +        self.assertEqual(transformed[0], b'abc') diff --git a/tests/analysis/scan/scanning_hex.py b/tests/analysis/scan/scanning_hex.py new file mode 100644 index 0000000..4b0fda4 --- /dev/null +++ b/tests/analysis/scan/scanning_hex.py @@ -0,0 +1,716 @@ + +from common import RostTestClass +from pychrysalide.analysis.contents import MemoryContent + + +class TestRostScanningBinary(RostTestClass): +    """TestCases for the bytes section syntax (binary).""" + +    def testLonelyPatterns(self): +        """Evaluate the most simple patterns.""" + +        cnt = MemoryContent(b'Abcdef') + +        rule = ''' +rule test { + +   bytes: +      $a = { 41 } + +   condition: +      #a == 1 and @a[0] == 0 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'Abcdef') + +        rule = ''' +rule test { + +   bytes: +      $a = { 62 } + +   condition: +      #a == 1 and @a[0] == 1 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'Abcdef') + +        rule = ''' +rule test { + +   bytes: +      $a = { 66 } + +   condition: +      #a == 1 and @a[0] == 5 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'Abcdef') + +        rule = ''' +rule test { + +   bytes: +      $a = { ?1 } + +   condition: +      #a == 1 and @a[0] == 0 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'Abcdef') + +        rule = ''' +rule test { + +   bytes: +      $a = { ?2 } + +   condition: +      #a == 1 and @a[0] == 1 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'Abcdef') + +        rule = ''' +rule test { + +   bytes: +      $a = { ?6 } + +   condition: +      #a == 1 and @a[0] == 5 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +    def ___testLonelyPatternsNot(self): +        """Evaluate the most simple patterns (not version).""" + +        cnt = MemoryContent(b'Abcdef') + +        rule = ''' +rule test { + +   bytes: +      $a = { ~41 } + +   condition: +      #a == 5 and @a[0] == 1 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'Abcdef') + +        rule = ''' +rule test { + +   bytes: +      $a = { ~62 } + +   condition: +      #a == 5 and @a[0] == 0 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'Abcdef') + +        rule = ''' +rule test { + +   bytes: +      $a = { ~66 } + +   condition: +      #a == 5 and @a[4] == 4 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'Abcdef') + +        rule = ''' +rule test { + +   bytes: +      $a = { ~?1 } + +   condition: +      #a == 5 and @a[0] == 1 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'Abcdef') + +        rule = ''' +rule test { + +   bytes: +      $a = { ~?2 } + +   condition: +      #a == 5 and @a[0] == 0 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'Abcdef') + +        rule = ''' +rule test { + +   bytes: +      $a = { ~?6 } + +   condition: +      #a == 5 and @a[4] == 4 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +    def testSimpleHexPattern(self): +        """Test a simple hex pattern.""" + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { 41 62 63 } + +   condition: +      #a == 1 and @a[0] == 4 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { 2d 41 62 63 } + +   condition: +      #a == 1 and @a[0] == 3 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +    def testSimpleMaskedHexPattern(self): +        """Test a simple masked hex pattern.""" + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { ?1 6? ?3 } + +   condition: +      #a == 1 and @a[0] == 4 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +    def testHexPatternWithPlainAndMasked(self): +        """Test hex patterns with plain and masked bytes.""" + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { 41 6? ?3 } + +   condition: +      #a == 1 and @a[0] == 4 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { 4? 62 ?3 } + +   condition: +      #a == 1 and @a[0] == 4 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { 4? ?2 63 } + +   condition: +      #a == 1 and @a[0] == 4 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { 4? ?2 ?3 } + +   condition: +      #a == 1 and @a[0] == 4 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { 2d 4? ?2 63 } + +   condition: +      #a == 1 and @a[0] == 3 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { 2d 4? 62 ?3 2d } + +   condition: +      #a == 1 and @a[0] == 3 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { 2? 41 6? 63 ?d } + +   condition: +      #a == 1 and @a[0] == 3 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +    def testHexPatternWithPlainAndHoles(self): +        """Test hex patterns with plain bytes and holes.""" + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { 33 ?? 41 ?? 63 ?? 34 } + +   condition: +      #a == 1 and @a[0] == 2 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { ?? 33 ?? 41 ?? 63 ?? 34 ?? } + +   condition: +      #a == 1 and @a[0] == 1 and !a[0] == 9 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { ?? 33 [1-5] 63 ?? 34 ?? } + +   condition: +      #a == 1 and @a[0] == 1 and !a[0] == 9 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { [3-4] 41 ?? 63 ?? 34 ?? } + +   condition: +      #a == 1 and @a[0] == 1 and !a[0] == 9 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { ?? 33 ?? 41 ?? 63 [3-] } + +   condition: +      #a == 1 and @a[0] == 1 and !a[0] == 9 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +    def testHexPatternWithMaskedAndHoles(self): +        """Test hex patterns with masked bytes and holes.""" + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { ?3 ?? 4? ?? 6? ?? ?4 } + +   condition: +      #a == 1 and @a[0] == 2 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { ?? ?3 ?? 4? ?? 6? ?? ?4 ?? } + +   condition: +      #a == 1 and @a[0] == 1 and !a[0] == 9 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { ?? ?3 [1-5] ?3 ?? ?4 ?? } + +   condition: +      #a == 1 and @a[0] == 1 and !a[0] == 9 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { [3-4] ?1 ?? ?3 ?? ?4 ?? } + +   condition: +      #a == 1 and @a[0] == 1 and !a[0] == 9 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { ?? 3? ?? 4? ?? 6? [3-] } + +   condition: +      #a == 1 and @a[0] == 1 and !a[0] == 9 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +    def testPipedPlainHexPatterns(self): +        """Look for several patterns at once with piped definition.""" + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { 41 62 ( 63 | 64 | 65 ) } + +   condition: +      #a == 1 and @a[0] == 4 and !a[0] == 3 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { ( 41 | f2 | f3 ) 62 63 } + +   condition: +      #a == 1 and @a[0] == 4 and !a[0] == 3 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { 41 ( 61 | 62 | 63 ) 63 } + +   condition: +      #a == 1 and @a[0] == 4 and !a[0] == 3 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { ( 41 62 63 | 42 62 63 | 43 62 63 ) } + +   condition: +      #a == 1 and @a[0] == 4 and !a[0] == 3 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +    def testPipedMaskedHexPatterns(self): +        """Look for several patterns at once with piped and masked definition.""" + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { 4? 6? ( ?3 | ?4 | ?5 ) } + +   condition: +      #a == 1 and @a[0] == 4 and !a[0] == 3 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { ( ?1 | ?2 | ?3 ) 6? 6? } + +   condition: +      #a == 1 and @a[0] == 4 and !a[0] == 3 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { 4? ( ?1 | ?2 | ?3 ) 6? } + +   condition: +      #a == 1 and @a[0] == 4 and !a[0] == 3 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +    def testDuplicatedResultsFiltering(self): +        """Filter duplicated results.""" + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = { ( 4? ?2 ?3 | 4? 6? 6? | ?3 6? ?3 ) } + +   condition: +      #a == 1 and @a[0] == 4 and !a[0] == 3 + +} +''' + +        self.check_rule_success(rule, content=cnt) diff --git a/tests/analysis/scan/scanning_str.py b/tests/analysis/scan/scanning_str.py new file mode 100644 index 0000000..75427a7 --- /dev/null +++ b/tests/analysis/scan/scanning_str.py @@ -0,0 +1,194 @@ + +from common import RostTestClass +from pychrysalide.analysis.contents import MemoryContent + + +class TestRostScanningStrings(RostTestClass): +    """TestCases for the bytes section syntax (strings).""" + +    def testSimpleStringPattern(self): +        """Test a simple string pattern.""" + +        cnt = MemoryContent(b'123-Abc-456') + +        rule = ''' +rule test { + +   bytes: +      $a = "Abc" + +   condition: +      #a == 1 and @a[0] == 4 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +    def testEscapedStringPattern(self): +        """Test escaped string patterns.""" + +        cnt = MemoryContent(b'\a\b\t\n\v\f\r' + bytes([ 0x1b ]) + b'"\\\xff') + +        rule = r''' +rule test { + +   bytes: +      $a = "\a\b\t\n\v\f\r\e\"\\\xff" + +   condition: +      #a == 1 and @a[0] == 0 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'\a\b\t\n--123--\v\f\r' + bytes([ 0x1b ]) + b'"\\\xff') + +        rule = r''' +rule test { + +   bytes: +      $a = "\a\b\t\n--123--\v\f\r\e\"\\\xff" + +   condition: +      #a == 1 and @a[0] == 0 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +    def testStringModifiers(self): +        """Check string modifiers output.""" + +        cnt = MemoryContent(b'--414243--') + +        rule = ''' +rule test { + +   bytes: +      $a = "ABC" hex + +   condition: +      #a == 1 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'--ABC--') + +        rule = ''' +rule test { + +   bytes: +      $a = "ABC" plain + +   condition: +      #a == 1 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'--CBA--') + +        rule = ''' +rule test { + +   bytes: +      $a = "ABC" rev + +   condition: +      #a == 1 + +} +''' + + +    def testStringPatternFullword(self): +        """Test a fullword string pattern.""" + +        cnt = MemoryContent(b'ABCDEF 123 ') + +        rule = ''' +rule test { + +   bytes: +      $a = "DEF" fullword +      $b = "123" fullword + +   condition: +      #a == 0 and #b == 1 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'DEF 123 ') + +        rule = ''' +rule test { + +   bytes: +      $a = "DEF" fullword +      $b = "123" fullword + +   condition: +      #a == 1 and #b == 1 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +        cnt = MemoryContent(b'\tDEF 123 ') + +        rule = ''' +rule test { + +   bytes: +      $a = "DEF" fullword +      $b = "123" fullword + +   condition: +      #a == 1 and #b == 1 + +} +''' + +        self.check_rule_success(rule, content=cnt) + + +    def testStringPatternCase(self): +        """Test a string pattern with case care.""" + +        cnt = MemoryContent(b'abc123-Abc123Def456GHI...z0z1z2z3z4z5z6z7z8z9') + +        rule = ''' +rule test { + +   bytes: +      $a = "Abc" nocase +      $b = "ABC123DEF456GHI" nocase +      $z = "z0z1z2z3z4z5z6z7z8z9" nocase + +   condition: +      #a == 2 and #b == 1 and #z == 1 + +} +''' + +        self.check_rule_success(rule, content=cnt) 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" +  | 
