diff --git a/capa/rules/__init__.py b/capa/rules/__init__.py index 571ca611..4aa2cfa8 100644 --- a/capa/rules/__init__.py +++ b/capa/rules/__init__.py @@ -124,7 +124,7 @@ class Scopes: def __repr__(self) -> str: if self.static and self.dynamic: - return f"static-scope: {self.static}, dyanamic-scope: {self.dynamic}" + return f"static-scope: {self.static}, dynamic-scope: {self.dynamic}" elif self.static: return f"static-scope: {self.static}" elif self.dynamic: @@ -1267,6 +1267,7 @@ class RuleSet: self.file_rules = self._get_rules_for_scope(rules, FILE_SCOPE) self.process_rules = self._get_rules_for_scope(rules, PROCESS_SCOPE) self.thread_rules = self._get_rules_for_scope(rules, THREAD_SCOPE) + self.call_rules = self._get_rules_for_scope(rules, CALL_SCOPE) self.function_rules = self._get_rules_for_scope(rules, FUNCTION_SCOPE) self.basic_block_rules = self._get_rules_for_scope(rules, BASIC_BLOCK_SCOPE) self.instruction_rules = self._get_rules_for_scope(rules, INSTRUCTION_SCOPE) @@ -1279,6 +1280,7 @@ class RuleSet: self.process_rules ) (self._easy_thread_rules_by_feature, self._hard_thread_rules) = self._index_rules_by_feature(self.thread_rules) + (self._easy_call_rules_by_feature, self._hard_call_rules) = self._index_rules_by_feature(self.call_rules) (self._easy_function_rules_by_feature, self._hard_function_rules) = self._index_rules_by_feature( self.function_rules ) @@ -1533,6 +1535,9 @@ class RuleSet: elif scope == Scope.THREAD: easy_rules_by_feature = self._easy_thread_rules_by_feature hard_rule_names = self._hard_thread_rules + elif scope == Scope.CALL: + easy_rules_by_feature = self._easy_call_rules_by_feature + hard_rule_names = self._hard_call_rules elif scope == Scope.FUNCTION: easy_rules_by_feature = self._easy_function_rules_by_feature hard_rule_names = self._hard_function_rules diff --git a/tests/test_main.py b/tests/test_main.py index 51daa691..da592dc4 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -165,13 +165,55 @@ def test_ruleset(): """ ) ), + capa.rules.Rule.from_yaml( + textwrap.dedent( + """ + rule: + meta: + name: test call subscope + scopes: + static: basic block + dynamic: thread + features: + - and: + - string: "explorer.exe" + - call: + - api: HttpOpenRequestW + """ + ) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent( + """ + rule: + meta: + name: test rule + scopes: + static: instruction + dynamic: call + features: + - and: + - or: + - api: socket + - and: + - os: linux + - mnemonic: syscall + - number: 41 = socket() + - number: 6 = IPPROTO_TCP + - number: 1 = SOCK_STREAM + - number: 2 = AF_INET + """ + ) + ), ] ) assert len(rules.file_rules) == 2 assert len(rules.function_rules) == 2 - assert len(rules.basic_block_rules) == 1 + assert len(rules.basic_block_rules) == 2 + assert len(rules.instruction_rules) == 1 assert len(rules.process_rules) == 4 - assert len(rules.thread_rules) == 1 + assert len(rules.thread_rules) == 2 + assert len(rules.call_rules) == 2 def test_match_across_scopes_file_function(z9324d_extractor): diff --git a/tests/test_rules.py b/tests/test_rules.py index b730d198..6b9372bb 100644 --- a/tests/test_rules.py +++ b/tests/test_rules.py @@ -353,6 +353,30 @@ def test_multi_scope_rules_features(): ) ) + _ = capa.rules.Rule.from_yaml( + textwrap.dedent( + """ + rule: + meta: + name: test rule + scopes: + static: instruction + dynamic: call + features: + - and: + - or: + - api: socket + - and: + - os: linux + - mnemonic: syscall + - number: 41 = socket() + - number: 6 = IPPROTO_TCP + - number: 1 = SOCK_STREAM + - number: 2 = AF_INET + """ + ) + ) + def test_rules_flavor_filtering(): rules = [ @@ -489,12 +513,30 @@ def test_subscope_rules(): """ ) ), + capa.rules.Rule.from_yaml( + textwrap.dedent( + """ + rule: + meta: + name: test call subscope + scopes: + static: basic block + dynamic: thread + features: + - and: + - string: "explorer.exe" + - call: + - api: HttpOpenRequestW + """ + ) + ), ] ) - # the file rule scope will have two rules: - # - `test function subscope` and `test process subscope` - # plus the dynamic flavor of all rules - # assert len(rules.file_rules) == 4 + # the file rule scope will have four rules: + # - `test function subscope`, `test process subscope` and + # `test thread subscope` for the static scope + # - and `test process subscope` for both scopes + assert len(rules.file_rules) == 3 # the function rule scope have two rule: # - the rule on which `test function subscope` depends @@ -504,9 +546,14 @@ def test_subscope_rules(): # - the rule on which `test process subscope` depends, assert len(rules.process_rules) == 3 - # the thread rule scope has one rule: + # the thread rule scope has two rule: # - the rule on which `test thread subscope` depends - assert len(rules.thread_rules) == 1 + # - the `test call subscope` rule + assert len(rules.thread_rules) == 2 + + # the call rule scope has one rule: + # - the rule on which `test call subcsope` depends + assert len(rules.call_rules) == 1 def test_duplicate_rules(): diff --git a/tests/unsupported_capa_rules.txt b/tests/unsupported_capa_rules.txt new file mode 100644 index 00000000..e69de29b diff --git a/tests/unsupported_capa_rules.yml b/tests/unsupported_capa_rules.yml new file mode 100644 index 00000000..e69de29b