Merge pull request #1671 from yelhamer/rule-statement-building

This commit is contained in:
Willi Ballenthin
2023-08-01 22:15:03 +02:00
committed by GitHub
14 changed files with 482 additions and 113 deletions

View File

@@ -11,6 +11,7 @@
- Add support for flavor-based rule scopes @yelhamer
- Add ProcessesAddress and ThreadAddress #1612 @yelhamer
- Add dynamic capability extraction @yelhamer
- Add support for mixed-scopes rules @yelhamer
### Breaking Changes

View File

@@ -458,6 +458,13 @@ FORMAT_AUTO = "auto"
FORMAT_SC32 = "sc32"
FORMAT_SC64 = "sc64"
FORMAT_CAPE = "cape"
STATIC_FORMATS = (
FORMAT_SC32,
FORMAT_SC64,
FORMAT_PE,
FORMAT_ELF,
FORMAT_DOTNET,
)
DYNAMIC_FORMATS = (FORMAT_CAPE,)
FORMAT_FREEZE = "freeze"
FORMAT_RESULT = "result"

View File

@@ -20,6 +20,7 @@ import textwrap
import itertools
import contextlib
import collections
from enum import Enum
from typing import Any, Dict, List, Tuple, Callable, Optional
from pathlib import Path
@@ -78,6 +79,8 @@ from capa.features.common import (
FORMAT_DOTNET,
FORMAT_FREEZE,
FORMAT_RESULT,
STATIC_FORMATS,
DYNAMIC_FORMATS,
)
from capa.features.address import NO_ADDRESS, Address
from capa.features.extractors.base_extractor import (
@@ -113,6 +116,15 @@ E_UNSUPPORTED_IDA_VERSION = 19
logger = logging.getLogger("capa")
class ExecutionContext(str, Enum):
STATIC = "static"
DYNAMIC = "dynamic"
STATIC_CONTEXT = ExecutionContext.STATIC
DYNAMIC_CONTEXT = ExecutionContext.DYNAMIC
@contextlib.contextmanager
def timing(msg: str):
t0 = time.time()
@@ -823,6 +835,7 @@ def get_rules(
rule_paths: List[RulePath],
cache_dir=None,
on_load_rule: Callable[[RulePath, int, int], None] = on_load_rule_default,
analysis_context: Optional[ExecutionContext] = None,
) -> RuleSet:
"""
args:
@@ -861,7 +874,14 @@ def get_rules(
rules.append(rule)
logger.debug("loaded rule: '%s' with scope: %s", rule.name, rule.scopes)
ruleset = capa.rules.RuleSet(rules)
# filter rules according to the execution context
if analysis_context is STATIC_CONTEXT:
ruleset = capa.rules.RuleSet(rules, rules_filter_func=lambda rule: rule.scopes.static)
elif analysis_context is DYNAMIC_CONTEXT:
ruleset = capa.rules.RuleSet(rules, rules_filter_func=lambda rule: rule.scopes.dynamic)
else:
# default: load all rules
ruleset = capa.rules.RuleSet(rules)
capa.rules.cache.cache_ruleset(cache_dir, ruleset)
@@ -1382,7 +1402,15 @@ def main(argv: Optional[List[str]] = None):
else:
cache_dir = capa.rules.cache.get_default_cache_directory()
rules = get_rules(args.rules, cache_dir=cache_dir)
if format_ in STATIC_FORMATS:
analysis_context = STATIC_CONTEXT
elif format_ in DYNAMIC_FORMATS:
analysis_context = DYNAMIC_CONTEXT
else:
# freeze or result formats
analysis_context = None
rules = get_rules(args.rules, cache_dir=cache_dir, analysis_context=analysis_context)
logger.debug(
"successfully loaded %s rules",

View File

@@ -25,7 +25,7 @@ except ImportError:
# https://github.com/python/mypy/issues/1153
from backports.functools_lru_cache import lru_cache # type: ignore
from typing import Any, Set, Dict, List, Tuple, Union, Iterator
from typing import Any, Set, Dict, List, Tuple, Union, Iterator, Optional
from dataclasses import asdict, dataclass
import yaml
@@ -91,7 +91,6 @@ INSTRUCTION_SCOPE = Scope.INSTRUCTION.value
# used only to specify supported features per scope.
# not used to validate rules.
GLOBAL_SCOPE = "global"
DEV_SCOPE = "dev"
# these literals are used to check if the flavor
@@ -108,29 +107,56 @@ DYNAMIC_SCOPES = (
GLOBAL_SCOPE,
PROCESS_SCOPE,
THREAD_SCOPE,
DEV_SCOPE,
)
@dataclass
class Scopes:
static: str
dynamic: str
static: Optional[str] = None
dynamic: Optional[str] = None
def __contains__(self, scope: Union[Scope, str]) -> bool:
assert isinstance(scope, (Scope, str))
return (scope == self.static) or (scope == self.dynamic)
def __repr__(self) -> str:
if self.static and self.dynamic:
return f"static-scope: {self.static}, dyanamic-scope: {self.dynamic}"
elif self.static:
return f"static-scope: {self.static}"
elif self.dynamic:
return f"dynamic-scope: {self.dynamic}"
else:
raise ValueError("invalid rules class. at least one scope must be specified")
@classmethod
def from_dict(self, scopes: dict) -> "Scopes":
assert isinstance(scopes, dict)
# mark non-specified scopes as invalid
if "static" not in scopes:
scopes["static"] = None
if "dynamic" not in scopes:
scopes["dynamic"] = None
# check the syntax of the meta `scopes` field
if sorted(scopes) != ["dynamic", "static"]:
raise InvalidRule("scope flavors can be either static or dynamic")
if scopes["static"] not in STATIC_SCOPES:
if (not scopes["static"]) and (not scopes["dynamic"]):
raise InvalidRule("invalid scopes value. At least one scope must be specified")
# check that all the specified scopes are valid
if scopes["static"] not in (
*STATIC_SCOPES,
None,
):
raise InvalidRule(f"{scopes['static']} is not a valid static scope")
if scopes["dynamic"] not in DYNAMIC_SCOPES:
raise InvalidRule(f"{scopes['dynamic']} is not a valid dynamicscope")
return Scopes(scopes["static"], scopes["dynamic"])
if scopes["dynamic"] not in (
*DYNAMIC_SCOPES,
None,
):
raise InvalidRule(f"{scopes['dynamic']} is not a valid dynamic scope")
return Scopes(static=scopes["static"], dynamic=scopes["dynamic"])
SUPPORTED_FEATURES: Dict[str, Set] = {
@@ -205,12 +231,6 @@ SUPPORTED_FEATURES: Dict[str, Set] = {
capa.features.common.Class,
capa.features.common.Namespace,
},
DEV_SCOPE: {
# TODO(yelhamer): this is a temporary scope. remove it after support
# for the legacy scope keyword has been added (to rendering).
# https://github.com/mandiant/capa/pull/1580
capa.features.insn.API,
},
}
# global scope features are available in all other scopes
@@ -227,10 +247,6 @@ SUPPORTED_FEATURES[PROCESS_SCOPE].update(SUPPORTED_FEATURES[THREAD_SCOPE])
SUPPORTED_FEATURES[BASIC_BLOCK_SCOPE].update(SUPPORTED_FEATURES[INSTRUCTION_SCOPE])
# all basic block scope features are also function scope features
SUPPORTED_FEATURES[FUNCTION_SCOPE].update(SUPPORTED_FEATURES[BASIC_BLOCK_SCOPE])
# dynamic-dev scope contains all features
SUPPORTED_FEATURES[DEV_SCOPE].update(SUPPORTED_FEATURES[FILE_SCOPE])
SUPPORTED_FEATURES[DEV_SCOPE].update(SUPPORTED_FEATURES[FUNCTION_SCOPE])
SUPPORTED_FEATURES[DEV_SCOPE].update(SUPPORTED_FEATURES[PROCESS_SCOPE])
class InvalidRule(ValueError):
@@ -268,22 +284,29 @@ class InvalidRuleSet(ValueError):
return str(self)
def ensure_feature_valid_for_scope(scope: str, feature: Union[Feature, Statement]):
def ensure_feature_valid_for_scopes(scopes: Scopes, feature: Union[Feature, Statement]):
# construct a dict of all supported features
supported_features: Set = set()
if scopes.static:
supported_features.update(SUPPORTED_FEATURES[scopes.static])
if scopes.dynamic:
supported_features.update(SUPPORTED_FEATURES[scopes.dynamic])
# if the given feature is a characteristic,
# check that is a valid characteristic for the given scope.
if (
isinstance(feature, capa.features.common.Characteristic)
and isinstance(feature.value, str)
and capa.features.common.Characteristic(feature.value) not in SUPPORTED_FEATURES[scope]
and capa.features.common.Characteristic(feature.value) not in supported_features
):
raise InvalidRule(f"feature {feature} not supported for scope {scope}")
raise InvalidRule(f"feature {feature} not supported for scopes {scopes}")
if not isinstance(feature, capa.features.common.Characteristic):
# features of this scope that are not Characteristics will be Type instances.
# check that the given feature is one of these types.
types_for_scope = filter(lambda t: isinstance(t, type), SUPPORTED_FEATURES[scope])
types_for_scope = filter(lambda t: isinstance(t, type), supported_features)
if not isinstance(feature, tuple(types_for_scope)): # type: ignore
raise InvalidRule(f"feature {feature} not supported for scope {scope}")
raise InvalidRule(f"feature {feature} not supported for scopes {scopes}")
def parse_int(s: str) -> int:
@@ -491,71 +514,79 @@ def pop_statement_description_entry(d):
return description["description"]
def build_statements(d, scope: str):
def build_statements(d, scopes: Scopes):
if len(d.keys()) > 2:
raise InvalidRule("too many statements")
key = list(d.keys())[0]
description = pop_statement_description_entry(d[key])
if key == "and":
return ceng.And([build_statements(dd, scope) for dd in d[key]], description=description)
return ceng.And([build_statements(dd, scopes) for dd in d[key]], description=description)
elif key == "or":
return ceng.Or([build_statements(dd, scope) for dd in d[key]], description=description)
return ceng.Or([build_statements(dd, scopes) for dd in d[key]], description=description)
elif key == "not":
if len(d[key]) != 1:
raise InvalidRule("not statement must have exactly one child statement")
return ceng.Not(build_statements(d[key][0], scope), description=description)
return ceng.Not(build_statements(d[key][0], scopes), description=description)
elif key.endswith(" or more"):
count = int(key[: -len("or more")])
return ceng.Some(count, [build_statements(dd, scope) for dd in d[key]], description=description)
return ceng.Some(count, [build_statements(dd, scopes) for dd in d[key]], description=description)
elif key == "optional":
# `optional` is an alias for `0 or more`
# which is useful for documenting behaviors,
# like with `write file`, we might say that `WriteFile` is optionally found alongside `CreateFileA`.
return ceng.Some(0, [build_statements(dd, scope) for dd in d[key]], description=description)
return ceng.Some(0, [build_statements(dd, scopes) for dd in d[key]], description=description)
elif key == "process":
if scope != FILE_SCOPE:
if FILE_SCOPE not in scopes:
raise InvalidRule("process subscope supported only for file scope")
if len(d[key]) != 1:
raise InvalidRule("subscope must have exactly one child statement")
return ceng.Subscope(PROCESS_SCOPE, build_statements(d[key][0], PROCESS_SCOPE), description=description)
return ceng.Subscope(
PROCESS_SCOPE, build_statements(d[key][0], Scopes(dynamic=PROCESS_SCOPE)), description=description
)
elif key == "thread":
if scope not in (PROCESS_SCOPE, FILE_SCOPE):
if (PROCESS_SCOPE not in scopes) and (FILE_SCOPE not in scopes):
raise InvalidRule("thread subscope supported only for the process scope")
if len(d[key]) != 1:
raise InvalidRule("subscope must have exactly one child statement")
return ceng.Subscope(THREAD_SCOPE, build_statements(d[key][0], THREAD_SCOPE), description=description)
return ceng.Subscope(
THREAD_SCOPE, build_statements(d[key][0], Scopes(dynamic=THREAD_SCOPE)), description=description
)
elif key == "function":
if scope not in (FILE_SCOPE, DEV_SCOPE):
if FILE_SCOPE not in scopes:
raise InvalidRule("function subscope supported only for file scope")
if len(d[key]) != 1:
raise InvalidRule("subscope must have exactly one child statement")
return ceng.Subscope(FUNCTION_SCOPE, build_statements(d[key][0], FUNCTION_SCOPE), description=description)
return ceng.Subscope(
FUNCTION_SCOPE, build_statements(d[key][0], Scopes(static=FUNCTION_SCOPE)), description=description
)
elif key == "basic block":
if scope not in (FUNCTION_SCOPE, DEV_SCOPE):
if FUNCTION_SCOPE not in scopes:
raise InvalidRule("basic block subscope supported only for function scope")
if len(d[key]) != 1:
raise InvalidRule("subscope must have exactly one child statement")
return ceng.Subscope(BASIC_BLOCK_SCOPE, build_statements(d[key][0], BASIC_BLOCK_SCOPE), description=description)
return ceng.Subscope(
BASIC_BLOCK_SCOPE, build_statements(d[key][0], Scopes(static=BASIC_BLOCK_SCOPE)), description=description
)
elif key == "instruction":
if scope not in (FUNCTION_SCOPE, BASIC_BLOCK_SCOPE, DEV_SCOPE):
if all(s not in scopes for s in (FUNCTION_SCOPE, BASIC_BLOCK_SCOPE)):
raise InvalidRule("instruction subscope supported only for function and basic block scope")
if len(d[key]) == 1:
statements = build_statements(d[key][0], INSTRUCTION_SCOPE)
statements = build_statements(d[key][0], Scopes(static=INSTRUCTION_SCOPE))
else:
# for instruction subscopes, we support a shorthand in which the top level AND is implied.
# the following are equivalent:
@@ -569,7 +600,7 @@ def build_statements(d, scope: str):
# - arch: i386
# - mnemonic: cmp
#
statements = ceng.And([build_statements(dd, INSTRUCTION_SCOPE) for dd in d[key]])
statements = ceng.And([build_statements(dd, Scopes(static=INSTRUCTION_SCOPE)) for dd in d[key]])
return ceng.Subscope(INSTRUCTION_SCOPE, statements, description=description)
@@ -610,7 +641,7 @@ def build_statements(d, scope: str):
feature = Feature(arg)
else:
feature = Feature()
ensure_feature_valid_for_scope(scope, feature)
ensure_feature_valid_for_scopes(scopes, feature)
count = d[key]
if isinstance(count, int):
@@ -644,7 +675,7 @@ def build_statements(d, scope: str):
feature = capa.features.insn.OperandNumber(index, value, description=description)
except ValueError as e:
raise InvalidRule(str(e)) from e
ensure_feature_valid_for_scope(scope, feature)
ensure_feature_valid_for_scopes(scopes, feature)
return feature
elif key.startswith("operand[") and key.endswith("].offset"):
@@ -660,7 +691,7 @@ def build_statements(d, scope: str):
feature = capa.features.insn.OperandOffset(index, value, description=description)
except ValueError as e:
raise InvalidRule(str(e)) from e
ensure_feature_valid_for_scope(scope, feature)
ensure_feature_valid_for_scopes(scopes, feature)
return feature
elif (
@@ -680,7 +711,7 @@ def build_statements(d, scope: str):
feature = capa.features.insn.Property(value, access=access, description=description)
except ValueError as e:
raise InvalidRule(str(e)) from e
ensure_feature_valid_for_scope(scope, feature)
ensure_feature_valid_for_scopes(scopes, feature)
return feature
else:
@@ -690,7 +721,7 @@ def build_statements(d, scope: str):
feature = Feature(value, description=description)
except ValueError as e:
raise InvalidRule(str(e)) from e
ensure_feature_valid_for_scope(scope, feature)
ensure_feature_valid_for_scopes(scopes, feature)
return feature
@@ -773,13 +804,19 @@ class Rule:
# the name is a randomly generated, hopefully unique value.
# ideally, this won't every be rendered to a user.
name = self.name + "/" + uuid.uuid4().hex
if subscope.scope in STATIC_SCOPES:
scopes = Scopes(static=subscope.scope)
elif subscope.scope in DYNAMIC_SCOPES:
scopes = Scopes(dynamic=subscope.scope)
else:
raise InvalidRule(f"scope {subscope.scope} is not a valid subscope")
new_rule = Rule(
name,
Scopes(subscope.scope, DEV_SCOPE),
scopes,
subscope.child,
{
"name": name,
"scopes": asdict(Scopes(subscope.scope, DEV_SCOPE)),
"scopes": asdict(scopes),
# these derived rules are never meant to be inspected separately,
# they are dependencies for the parent rule,
# so mark it as such.
@@ -843,7 +880,16 @@ class Rule:
# this is probably the mode that rule authors will start with.
# each rule has two scopes, a static-flavor scope, and a
# dynamic-flavor one. which one is used depends on the analysis type.
scopes: Scopes = Scopes.from_dict(meta.get("scopes", {"static": "function", "dynamic": "dev"}))
if "scope" in meta:
raise InvalidRule("rule is in legacy mode (has scope meta field). please update to the new syntax.")
elif "scopes" in meta:
scopes_ = meta.get("scopes")
else:
raise InvalidRule("please specify at least one of this rule's (static/dynamic) scopes")
if not isinstance(scopes_, dict):
raise InvalidRule("the scopes field must contain a dictionary specifying the scopes")
scopes: Scopes = Scopes.from_dict(scopes_)
statements = d["rule"]["features"]
# the rule must start with a single logic node.
@@ -860,14 +906,7 @@ class Rule:
if not isinstance(meta.get("mbc", []), list):
raise InvalidRule("MBC mapping must be a list")
# TODO(yelhamer): once we've decided on the desired format for mixed-scope statements,
# we should go back and update this accordingly to either:
# - generate one englobing statement.
# - generate two respective statements and store them approriately
# https://github.com/mandiant/capa/pull/1580
statement = build_statements(statements[0], scopes.static)
_ = build_statements(statements[0], scopes.dynamic)
return cls(name, scopes, statement, meta, definition)
return cls(name, scopes, build_statements(statements[0], scopes), meta, definition)
@staticmethod
@lru_cache()
@@ -1173,7 +1212,11 @@ class RuleSet:
capa.engine.match(ruleset.file_rules, ...)
"""
def __init__(self, rules: List[Rule]):
def __init__(
self,
rules: List[Rule],
rules_filter_func=None,
):
super().__init__()
ensure_rules_are_unique(rules)
@@ -1190,6 +1233,11 @@ class RuleSet:
ensure_rule_dependencies_are_met(rules)
if rules_filter_func:
# this allows for filtering the ruleset based on
# the execution context (static or dynamic)
rules = list(filter(rules_filter_func, rules))
if len(rules) == 0:
raise InvalidRuleSet("no rules selected")

View File

@@ -19,7 +19,7 @@ EXPECTED = textwrap.dedent(
- user@domain.com
scopes:
static: function
dynamic: dev
dynamic: process
examples:
- foo1234
- bar5678
@@ -45,7 +45,7 @@ def test_rule_reformat_top_level_elements():
- user@domain.com
scopes:
static: function
dynamic: dev
dynamic: process
examples:
- foo1234
- bar5678
@@ -65,7 +65,7 @@ def test_rule_reformat_indentation():
- user@domain.com
scopes:
static: function
dynamic: dev
dynamic: process
examples:
- foo1234
- bar5678
@@ -91,7 +91,7 @@ def test_rule_reformat_order():
- bar5678
scopes:
static: function
dynamic: dev
dynamic: process
name: test rule
features:
- and:
@@ -117,7 +117,7 @@ def test_rule_reformat_meta_update():
- bar5678
scopes:
static: function
dynamic: dev
dynamic: process
name: AAAA
features:
- and:
@@ -143,7 +143,7 @@ def test_rule_reformat_string_description():
- user@domain.com
scopes:
static: function
dynamic: dev
dynamic: process
features:
- and:
- string: foo

View File

@@ -38,7 +38,7 @@ def test_main_single_rule(z9324d_extractor, tmpdir):
name: test rule
scopes:
static: file
dynamic: dev
dynamic: process
authors:
- test
features:
@@ -103,7 +103,7 @@ def test_ruleset():
name: file rule
scopes:
static: file
dynamic: dev
dynamic: process
features:
- characteristic: embedded pe
"""
@@ -117,7 +117,7 @@ def test_ruleset():
name: function rule
scopes:
static: function
dynamic: dev
dynamic: process
features:
- characteristic: tight loop
"""
@@ -131,7 +131,7 @@ def test_ruleset():
name: basic block rule
scopes:
static: basic block
dynamic: dev
dynamic: process
features:
- characteristic: nzxor
"""
@@ -170,7 +170,7 @@ def test_ruleset():
assert len(rules.file_rules) == 2
assert len(rules.function_rules) == 2
assert len(rules.basic_block_rules) == 1
assert len(rules.process_rules) == 1
assert len(rules.process_rules) == 4
assert len(rules.thread_rules) == 1
@@ -186,7 +186,7 @@ def test_match_across_scopes_file_function(z9324d_extractor):
name: install service
scopes:
static: function
dynamic: dev
dynamic: process
examples:
- 9324d1a8ae37a36ae560c37448c9705a:0x4073F0
features:
@@ -206,7 +206,7 @@ def test_match_across_scopes_file_function(z9324d_extractor):
name: .text section
scopes:
static: file
dynamic: dev
dynamic: process
examples:
- 9324d1a8ae37a36ae560c37448c9705a
features:
@@ -225,7 +225,7 @@ def test_match_across_scopes_file_function(z9324d_extractor):
name: .text section and install service
scopes:
static: file
dynamic: dev
dynamic: process
examples:
- 9324d1a8ae37a36ae560c37448c9705a
features:
@@ -255,7 +255,7 @@ def test_match_across_scopes(z9324d_extractor):
name: tight loop
scopes:
static: basic block
dynamic: dev
dynamic: process
examples:
- 9324d1a8ae37a36ae560c37448c9705a:0x403685
features:
@@ -273,7 +273,7 @@ def test_match_across_scopes(z9324d_extractor):
name: kill thread loop
scopes:
static: function
dynamic: dev
dynamic: process
examples:
- 9324d1a8ae37a36ae560c37448c9705a:0x403660
features:
@@ -293,7 +293,7 @@ def test_match_across_scopes(z9324d_extractor):
name: kill thread program
scopes:
static: file
dynamic: dev
dynamic: process
examples:
- 9324d1a8ae37a36ae560c37448c9705a
features:
@@ -322,7 +322,7 @@ def test_subscope_bb_rules(z9324d_extractor):
name: test rule
scopes:
static: function
dynamic: dev
dynamic: process
features:
- and:
- basic block:
@@ -348,7 +348,7 @@ def test_byte_matching(z9324d_extractor):
name: byte match test
scopes:
static: function
dynamic: dev
dynamic: process
features:
- and:
- bytes: ED 24 9E F4 52 A9 07 47 55 8E E1 AB 30 8E 23 61
@@ -373,7 +373,7 @@ def test_count_bb(z9324d_extractor):
namespace: test
scopes:
static: function
dynamic: dev
dynamic: process
features:
- and:
- count(basic blocks): 1 or more
@@ -399,7 +399,7 @@ def test_instruction_scope(z9324d_extractor):
namespace: test
scopes:
static: instruction
dynamic: dev
dynamic: process
features:
- and:
- mnemonic: push
@@ -429,7 +429,7 @@ def test_instruction_subscope(z9324d_extractor):
namespace: test
scopes:
static: function
dynamic: dev
dynamic: process
features:
- and:
- arch: i386

View File

@@ -43,6 +43,9 @@ def test_match_simple():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
namespace: testns1/testns2
features:
- number: 100
@@ -63,6 +66,9 @@ def test_match_range_exact():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- count(number(100)): 2
"""
@@ -87,7 +93,10 @@ def test_match_range_range():
"""
rule:
meta:
name: test rule
name: test rule
scopes:
static: function
dynamic: process
features:
- count(number(100)): (2, 3)
"""
@@ -117,6 +126,9 @@ def test_match_range_exact_zero():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- count(number(100)): 0
"""
@@ -142,7 +154,10 @@ def test_match_range_with_zero():
"""
rule:
meta:
name: test rule
name: test rule
scopes:
static: function
dynamic: process
features:
- count(number(100)): (0, 1)
"""
@@ -169,6 +184,9 @@ def test_match_adds_matched_rule_feature():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- number: 100
"""
@@ -187,6 +205,9 @@ def test_match_matched_rules():
rule:
meta:
name: test rule1
scopes:
static: function
dynamic: process
features:
- number: 100
"""
@@ -198,6 +219,9 @@ def test_match_matched_rules():
rule:
meta:
name: test rule2
scopes:
static: function
dynamic: process
features:
- match: test rule1
"""
@@ -232,6 +256,9 @@ def test_match_namespace():
rule:
meta:
name: CreateFile API
scopes:
static: function
dynamic: process
namespace: file/create/CreateFile
features:
- api: CreateFile
@@ -244,6 +271,9 @@ def test_match_namespace():
rule:
meta:
name: WriteFile API
scopes:
static: function
dynamic: process
namespace: file/write
features:
- api: WriteFile
@@ -256,6 +286,9 @@ def test_match_namespace():
rule:
meta:
name: file-create
scopes:
static: function
dynamic: process
features:
- match: file/create
"""
@@ -267,6 +300,9 @@ def test_match_namespace():
rule:
meta:
name: filesystem-any
scopes:
static: function
dynamic: process
features:
- match: file
"""
@@ -304,6 +340,9 @@ def test_match_substring():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- substring: abc
@@ -355,6 +394,9 @@ def test_match_regex():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- string: /.*bbbb.*/
@@ -367,6 +409,9 @@ def test_match_regex():
rule:
meta:
name: rule with implied wildcards
scopes:
static: function
dynamic: process
features:
- and:
- string: /bbbb/
@@ -379,6 +424,9 @@ def test_match_regex():
rule:
meta:
name: rule with anchor
scopes:
static: function
dynamic: process
features:
- and:
- string: /^bbbb/
@@ -425,6 +473,9 @@ def test_match_regex_ignorecase():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- string: /.*bbbb.*/i
@@ -448,6 +499,9 @@ def test_match_regex_complex():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- string: /.*HARDWARE\\Key\\key with spaces\\.*/i
@@ -471,6 +525,9 @@ def test_match_regex_values_always_string():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- string: /123/
@@ -500,6 +557,9 @@ def test_match_not():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
namespace: testns1/testns2
features:
- not:
@@ -518,6 +578,9 @@ def test_match_not_not():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
namespace: testns1/testns2
features:
- not:
@@ -537,6 +600,9 @@ def test_match_operand_number():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- operand[0].number: 0x10
@@ -564,6 +630,9 @@ def test_match_operand_offset():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- operand[0].offset: 0x10
@@ -591,6 +660,9 @@ def test_match_property_access():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- property/read: System.IO.FileInfo::Length
@@ -632,6 +704,9 @@ def test_match_os_any():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- and:

View File

@@ -25,7 +25,7 @@ def test_optimizer_order():
name: test rule
scopes:
static: function
dynamic: dev
dynamic: process
features:
- and:
- substring: "foo"

View File

@@ -52,7 +52,7 @@ def test_render_meta_attack():
name: test rule
scopes:
static: function
dynamic: dev
dynamic: process
authors:
- foo
att&ck:
@@ -90,7 +90,7 @@ def test_render_meta_mbc():
name: test rule
scopes:
static: function
dynamic: dev
dynamic: process
authors:
- foo
mbc:

View File

@@ -22,7 +22,7 @@ R1 = capa.rules.Rule.from_yaml(
- user@domain.com
scopes:
static: function
dynamic: dev
dynamic: process
examples:
- foo1234
- bar5678
@@ -44,7 +44,7 @@ R2 = capa.rules.Rule.from_yaml(
- user@domain.com
scopes:
static: function
dynamic: dev
dynamic: process
examples:
- foo1234
- bar5678

View File

@@ -56,7 +56,7 @@ def test_rule_yaml():
- user@domain.com
scopes:
static: function
dynamic: dev
dynamic: process
examples:
- foo1234
- bar5678
@@ -79,6 +79,9 @@ def test_rule_yaml_complex():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- and:
@@ -103,6 +106,9 @@ def test_rule_descriptions():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- description: and description
@@ -147,6 +153,9 @@ def test_invalid_rule_statement_descriptions():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- number: 1 = This is the number 1
@@ -163,6 +172,9 @@ def test_rule_yaml_not():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- number: 1
@@ -181,6 +193,9 @@ def test_rule_yaml_count():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- count(number(100)): 1
"""
@@ -197,6 +212,9 @@ def test_rule_yaml_count_range():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- count(number(100)): (1, 2)
"""
@@ -214,6 +232,9 @@ def test_rule_yaml_count_string():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- count(string(foo)): 2
"""
@@ -233,6 +254,9 @@ def test_invalid_rule_feature():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- foo: true
"""
@@ -248,7 +272,7 @@ def test_invalid_rule_feature():
name: test rule
scopes:
static: file
dynamic: dev
dynamic: process
features:
- characteristic: nzxor
"""
@@ -264,7 +288,7 @@ def test_invalid_rule_feature():
name: test rule
scopes:
static: function
dynamic: dev
dynamic: thread
features:
- characteristic: embedded pe
"""
@@ -280,28 +304,93 @@ def test_invalid_rule_feature():
name: test rule
scopes:
static: basic block
dynamic: dev
dynamic: thread
features:
- characteristic: embedded pe
"""
)
)
with pytest.raises(capa.rules.InvalidRule):
def test_multi_scope_rules_features():
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- api: write
- and:
- os: linux
- mnemonic: syscall
- number: 1 = write
"""
)
)
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- api: read
- and:
- os: linux
- mnemonic: syscall
- number: 0 = read
"""
)
)
def test_rules_flavor_filtering():
rules = [
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
name: static rule
scopes:
static: function
dynamic: process
features:
- mnemonic: xor
- api: CreateFileA
"""
)
)
),
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: dynamic rule
scopes:
dynamic: thread
features:
- api: CreateFileA
"""
)
),
]
static_rules = capa.rules.RuleSet(rules.copy(), rules_filter_func=lambda rule: rule.scopes.static)
dynamic_rules = capa.rules.RuleSet(rules, rules_filter_func=lambda rule: rule.scopes.dynamic)
# only static rule
assert len(static_rules) == 1
# only dynamic rule
assert len(dynamic_rules) == 1
def test_lib_rules():
@@ -313,6 +402,9 @@ def test_lib_rules():
rule:
meta:
name: a lib rule
scopes:
static: function
dynamic: process
lib: true
features:
- api: CreateFileA
@@ -325,6 +417,9 @@ def test_lib_rules():
rule:
meta:
name: a standard rule
scopes:
static: function
dynamic: process
lib: false
features:
- api: CreateFileW
@@ -348,7 +443,7 @@ def test_subscope_rules():
name: test function subscope
scopes:
static: file
dynamic: dev
dynamic: process
features:
- and:
- characteristic: embedded pe
@@ -407,7 +502,7 @@ def test_subscope_rules():
# the process rule scope has three rules:
# - the rule on which `test process subscope` depends,
assert len(rules.process_rules) == 2
assert len(rules.process_rules) == 3
# the thread rule scope has one rule:
# - the rule on which `test thread subscope` depends
@@ -424,6 +519,9 @@ def test_duplicate_rules():
rule:
meta:
name: rule-name
scopes:
static: function
dynamic: process
features:
- api: CreateFileA
"""
@@ -435,6 +533,9 @@ def test_duplicate_rules():
rule:
meta:
name: rule-name
scopes:
static: function
dynamic: process
features:
- api: CreateFileW
"""
@@ -454,6 +555,9 @@ def test_missing_dependency():
rule:
meta:
name: dependent rule
scopes:
static: function
dynamic: process
features:
- match: missing rule
"""
@@ -471,6 +575,9 @@ def test_invalid_rules():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- characteristic: number(1)
"""
@@ -484,6 +591,9 @@ def test_invalid_rules():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- characteristic: count(number(100))
"""
@@ -498,6 +608,9 @@ def test_invalid_rules():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
att&ck: Tactic::Technique::Subtechnique [Identifier]
features:
- number: 1
@@ -511,6 +624,9 @@ def test_invalid_rules():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
mbc: Objective::Behavior::Method [Identifier]
features:
- number: 1
@@ -585,6 +701,9 @@ def test_number_symbol():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- number: 1
@@ -612,6 +731,9 @@ def test_count_number_symbol():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- count(number(2 = symbol name)): 1
@@ -635,6 +757,9 @@ def test_invalid_number():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- number: "this is a string"
"""
@@ -648,6 +773,9 @@ def test_invalid_number():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- number: 2=
"""
@@ -661,6 +789,9 @@ def test_invalid_number():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- number: symbol name = 2
"""
@@ -674,6 +805,9 @@ def test_offset_symbol():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- offset: 1
@@ -698,6 +832,9 @@ def test_count_offset_symbol():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- count(offset(2 = symbol name)): 1
@@ -721,6 +858,9 @@ def test_invalid_offset():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- offset: "this is a string"
"""
@@ -734,6 +874,9 @@ def test_invalid_offset():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- offset: 2=
"""
@@ -747,6 +890,9 @@ def test_invalid_offset():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- offset: symbol name = 2
"""
@@ -762,6 +908,9 @@ def test_invalid_string_values_int():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- string: 123
"""
@@ -775,6 +924,9 @@ def test_invalid_string_values_int():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- string: 0x123
"""
@@ -788,6 +940,9 @@ def test_explicit_string_values_int():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- string: "123"
@@ -806,6 +961,9 @@ def test_string_values_special_characters():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- string: "hello\\r\\nworld"
@@ -825,6 +983,9 @@ def test_substring_feature():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- substring: abc
@@ -845,6 +1006,9 @@ def test_substring_description():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- substring: abc
@@ -865,6 +1029,9 @@ def test_filter_rules():
rule:
meta:
name: rule 1
scopes:
static: function
dynamic: process
authors:
- joe
features:
@@ -878,6 +1045,9 @@ def test_filter_rules():
rule:
meta:
name: rule 2
scopes:
static: function
dynamic: process
features:
- string: joe
"""
@@ -899,6 +1069,9 @@ def test_filter_rules_dependencies():
rule:
meta:
name: rule 1
scopes:
static: function
dynamic: process
features:
- match: rule 2
"""
@@ -910,6 +1083,9 @@ def test_filter_rules_dependencies():
rule:
meta:
name: rule 2
scopes:
static: function
dynamic: process
features:
- match: rule 3
"""
@@ -921,6 +1097,9 @@ def test_filter_rules_dependencies():
rule:
meta:
name: rule 3
scopes:
static: function
dynamic: process
features:
- api: CreateFile
"""
@@ -945,6 +1124,9 @@ def test_filter_rules_missing_dependency():
rule:
meta:
name: rule 1
scopes:
static: function
dynamic: process
authors:
- joe
features:
@@ -964,6 +1146,9 @@ def test_rules_namespace_dependencies():
rule:
meta:
name: rule 1
scopes:
static: function
dynamic: process
namespace: ns1/nsA
features:
- api: CreateFile
@@ -976,6 +1161,9 @@ def test_rules_namespace_dependencies():
rule:
meta:
name: rule 2
scopes:
static: function
dynamic: process
namespace: ns1/nsB
features:
- api: CreateFile
@@ -988,6 +1176,9 @@ def test_rules_namespace_dependencies():
rule:
meta:
name: rule 3
scopes:
static: function
dynamic: process
features:
- match: ns1/nsA
"""
@@ -999,6 +1190,9 @@ def test_rules_namespace_dependencies():
rule:
meta:
name: rule 4
scopes:
static: function
dynamic: process
features:
- match: ns1
"""
@@ -1025,7 +1219,7 @@ def test_function_name_features():
name: test rule
scopes:
static: file
dynamic: dev
dynamic: process
features:
- and:
- function-name: strcpy
@@ -1049,7 +1243,7 @@ def test_os_features():
name: test rule
scopes:
static: file
dynamic: dev
dynamic: process
features:
- and:
- os: windows
@@ -1069,7 +1263,7 @@ def test_format_features():
name: test rule
scopes:
static: file
dynamic: dev
dynamic: process
features:
- and:
- format: pe
@@ -1089,7 +1283,7 @@ def test_arch_features():
name: test rule
scopes:
static: file
dynamic: dev
dynamic: process
features:
- and:
- arch: amd64
@@ -1108,6 +1302,9 @@ def test_property_access():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- property/read: System.IO.FileInfo::Length
"""
@@ -1126,6 +1323,9 @@ def test_property_access_symbol():
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- property/read: System.IO.FileInfo::Length = some property
"""

View File

@@ -22,9 +22,8 @@ def test_rule_scope_instruction():
name: test rule
scopes:
static: instruction
dynamic: dev
features:
- and:
- and:
- mnemonic: mov
- arch: i386
- os: windows
@@ -41,7 +40,6 @@ def test_rule_scope_instruction():
name: test rule
scopes:
static: instruction
dynamic: dev
features:
- characteristic: embedded pe
"""
@@ -60,7 +58,7 @@ def test_rule_subscope_instruction():
name: test rule
scopes:
static: function
dynamic: dev
dynamic: process
features:
- and:
- instruction:
@@ -91,7 +89,7 @@ def test_scope_instruction_implied_and():
name: test rule
scopes:
static: function
dynamic: dev
dynamic: process
features:
- and:
- instruction:
@@ -112,7 +110,7 @@ def test_scope_instruction_description():
name: test rule
scopes:
static: function
dynamic: dev
dynamic: process
features:
- and:
- instruction:
@@ -132,7 +130,7 @@ def test_scope_instruction_description():
name: test rule
scopes:
static: function
dynamic: dev
dynamic: process
features:
- and:
- instruction:

View File

@@ -109,7 +109,7 @@ def test_detect_duplicate_features(tmpdir):
name: Test Rule 0
scopes:
static: function
dynamic: dev
dynamic: process
features:
- and:
- number: 1
@@ -124,6 +124,9 @@ def test_detect_duplicate_features(tmpdir):
rule:
meta:
name: Test Rule 1
scopes:
static: function
dynamic: process
features:
- or:
- string: unique
@@ -143,6 +146,9 @@ def test_detect_duplicate_features(tmpdir):
rule:
meta:
name: Test Rule 2
scopes:
static: function
dynamic: process
features:
- and:
- string: "sites.ini"
@@ -157,6 +163,9 @@ def test_detect_duplicate_features(tmpdir):
rule:
meta:
name: Test Rule 3
scopes:
static: function
dynamic: process
features:
- or:
- not:
@@ -172,6 +181,9 @@ def test_detect_duplicate_features(tmpdir):
rule:
meta:
name: Test Rule 4
scopes:
static: function
dynamic: process
features:
- not:
- string: "expa"

View File

@@ -90,7 +90,7 @@ def test_null_feature_extractor():
name: xor loop
scopes:
static: basic block
dynamic: dev
dynamic: process
features:
- and:
- characteristic: tight loop