mirror of
https://github.com/mandiant/capa.git
synced 2025-12-08 13:50:38 -08:00
rules: scopes can now have subscope blocks with same scope (#2584)
This commit is contained in:
@@ -31,6 +31,7 @@
|
|||||||
- strings: add type hints and fix uncovered bugs @williballenthin #2555
|
- strings: add type hints and fix uncovered bugs @williballenthin #2555
|
||||||
- elffile: handle symbols without a name @williballenthin #2553
|
- elffile: handle symbols without a name @williballenthin #2553
|
||||||
- project: remove pytest-cov that wasn't used @williballenthin @2491
|
- project: remove pytest-cov that wasn't used @williballenthin @2491
|
||||||
|
- rules: scopes can now have subscope blocks with the same scope @williballenthin #2584
|
||||||
|
|
||||||
### capa Explorer Web
|
### capa Explorer Web
|
||||||
|
|
||||||
|
|||||||
@@ -597,6 +597,43 @@ def unique(sequence):
|
|||||||
return [x for x in sequence if not (x in seen or seen.add(x))] # type: ignore [func-returns-value]
|
return [x for x in sequence if not (x in seen or seen.add(x))] # type: ignore [func-returns-value]
|
||||||
|
|
||||||
|
|
||||||
|
STATIC_SCOPE_ORDER = [
|
||||||
|
Scope.FILE,
|
||||||
|
Scope.FUNCTION,
|
||||||
|
Scope.BASIC_BLOCK,
|
||||||
|
Scope.INSTRUCTION,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
DYNAMIC_SCOPE_ORDER = [
|
||||||
|
Scope.FILE,
|
||||||
|
Scope.PROCESS,
|
||||||
|
Scope.THREAD,
|
||||||
|
Scope.SPAN_OF_CALLS,
|
||||||
|
Scope.CALL,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def is_subscope_compatible(scope: Scope | None, subscope: Scope) -> bool:
|
||||||
|
if not scope:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if subscope in STATIC_SCOPE_ORDER:
|
||||||
|
try:
|
||||||
|
return STATIC_SCOPE_ORDER.index(subscope) >= STATIC_SCOPE_ORDER.index(scope)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif subscope in DYNAMIC_SCOPE_ORDER:
|
||||||
|
try:
|
||||||
|
return DYNAMIC_SCOPE_ORDER.index(subscope) >= DYNAMIC_SCOPE_ORDER.index(scope)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError("unexpected scope")
|
||||||
|
|
||||||
|
|
||||||
def build_statements(d, scopes: Scopes):
|
def build_statements(d, scopes: Scopes):
|
||||||
if len(d.keys()) > 2:
|
if len(d.keys()) > 2:
|
||||||
raise InvalidRule("too many statements")
|
raise InvalidRule("too many statements")
|
||||||
@@ -621,7 +658,7 @@ def build_statements(d, scopes: Scopes):
|
|||||||
return ceng.Some(0, unique(build_statements(dd, scopes) for dd in d[key]), description=description)
|
return ceng.Some(0, unique(build_statements(dd, scopes) for dd in d[key]), description=description)
|
||||||
|
|
||||||
elif key == "process":
|
elif key == "process":
|
||||||
if Scope.FILE not in scopes:
|
if not is_subscope_compatible(scopes.dynamic, Scope.PROCESS):
|
||||||
raise InvalidRule("`process` subscope supported only for `file` scope")
|
raise InvalidRule("`process` subscope supported only for `file` scope")
|
||||||
|
|
||||||
if len(d[key]) != 1:
|
if len(d[key]) != 1:
|
||||||
@@ -632,7 +669,7 @@ def build_statements(d, scopes: Scopes):
|
|||||||
)
|
)
|
||||||
|
|
||||||
elif key == "thread":
|
elif key == "thread":
|
||||||
if all(s not in scopes for s in (Scope.FILE, Scope.PROCESS)):
|
if not is_subscope_compatible(scopes.dynamic, Scope.THREAD):
|
||||||
raise InvalidRule("`thread` subscope supported only for the `process` scope")
|
raise InvalidRule("`thread` subscope supported only for the `process` scope")
|
||||||
|
|
||||||
if len(d[key]) != 1:
|
if len(d[key]) != 1:
|
||||||
@@ -643,7 +680,7 @@ def build_statements(d, scopes: Scopes):
|
|||||||
)
|
)
|
||||||
|
|
||||||
elif key == "span of calls":
|
elif key == "span of calls":
|
||||||
if all(s not in scopes for s in (Scope.FILE, Scope.PROCESS, Scope.THREAD, Scope.SPAN_OF_CALLS)):
|
if not is_subscope_compatible(scopes.dynamic, Scope.SPAN_OF_CALLS):
|
||||||
raise InvalidRule("`span of calls` subscope supported only for the `process` and `thread` scopes")
|
raise InvalidRule("`span of calls` subscope supported only for the `process` and `thread` scopes")
|
||||||
|
|
||||||
if len(d[key]) != 1:
|
if len(d[key]) != 1:
|
||||||
@@ -656,7 +693,7 @@ def build_statements(d, scopes: Scopes):
|
|||||||
)
|
)
|
||||||
|
|
||||||
elif key == "call":
|
elif key == "call":
|
||||||
if all(s not in scopes for s in (Scope.FILE, Scope.PROCESS, Scope.THREAD, Scope.SPAN_OF_CALLS, Scope.CALL)):
|
if not is_subscope_compatible(scopes.dynamic, Scope.CALL):
|
||||||
raise InvalidRule("`call` subscope supported only for the `process`, `thread`, and `call` scopes")
|
raise InvalidRule("`call` subscope supported only for the `process`, `thread`, and `call` scopes")
|
||||||
|
|
||||||
if len(d[key]) != 1:
|
if len(d[key]) != 1:
|
||||||
@@ -667,7 +704,7 @@ def build_statements(d, scopes: Scopes):
|
|||||||
)
|
)
|
||||||
|
|
||||||
elif key == "function":
|
elif key == "function":
|
||||||
if Scope.FILE not in scopes:
|
if not is_subscope_compatible(scopes.static, Scope.FUNCTION):
|
||||||
raise InvalidRule("`function` subscope supported only for `file` scope")
|
raise InvalidRule("`function` subscope supported only for `file` scope")
|
||||||
|
|
||||||
if len(d[key]) != 1:
|
if len(d[key]) != 1:
|
||||||
@@ -678,7 +715,7 @@ def build_statements(d, scopes: Scopes):
|
|||||||
)
|
)
|
||||||
|
|
||||||
elif key == "basic block":
|
elif key == "basic block":
|
||||||
if Scope.FUNCTION not in scopes:
|
if not is_subscope_compatible(scopes.static, Scope.BASIC_BLOCK):
|
||||||
raise InvalidRule("`basic block` subscope supported only for `function` scope")
|
raise InvalidRule("`basic block` subscope supported only for `function` scope")
|
||||||
|
|
||||||
if len(d[key]) != 1:
|
if len(d[key]) != 1:
|
||||||
@@ -689,7 +726,7 @@ def build_statements(d, scopes: Scopes):
|
|||||||
)
|
)
|
||||||
|
|
||||||
elif key == "instruction":
|
elif key == "instruction":
|
||||||
if all(s not in scopes for s in (Scope.FUNCTION, Scope.BASIC_BLOCK)):
|
if not is_subscope_compatible(scopes.static, Scope.INSTRUCTION):
|
||||||
raise InvalidRule("`instruction` subscope supported only for `function` and `basic block` scope")
|
raise InvalidRule("`instruction` subscope supported only for `function` and `basic block` scope")
|
||||||
|
|
||||||
if len(d[key]) == 1:
|
if len(d[key]) == 1:
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import capa.loader
|
|||||||
import capa.helpers
|
import capa.helpers
|
||||||
import capa.features.insn
|
import capa.features.insn
|
||||||
import capa.capabilities.common
|
import capa.capabilities.common
|
||||||
from capa.rules import Rule, Scope, RuleSet
|
from capa.rules import Rule, RuleSet
|
||||||
from capa.features.common import OS_AUTO, String, Feature, Substring
|
from capa.features.common import OS_AUTO, String, Feature, Substring
|
||||||
from capa.render.result_document import RuleMetadata
|
from capa.render.result_document import RuleMetadata
|
||||||
|
|
||||||
@@ -536,15 +536,8 @@ class RuleDependencyScopeMismatch(Lint):
|
|||||||
# Assume for now it is not.
|
# Assume for now it is not.
|
||||||
return True
|
return True
|
||||||
|
|
||||||
static_scope_order = [
|
assert child.scopes.static is not None
|
||||||
None,
|
return capa.rules.is_subscope_compatible(parent.scopes.static, child.scopes.static)
|
||||||
Scope.FILE,
|
|
||||||
Scope.FUNCTION,
|
|
||||||
Scope.BASIC_BLOCK,
|
|
||||||
Scope.INSTRUCTION,
|
|
||||||
]
|
|
||||||
|
|
||||||
return static_scope_order.index(child.scopes.static) >= static_scope_order.index(parent.scopes.static)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _is_dynamic_scope_compatible(parent: Rule, child: Rule) -> bool:
|
def _is_dynamic_scope_compatible(parent: Rule, child: Rule) -> bool:
|
||||||
@@ -563,16 +556,8 @@ class RuleDependencyScopeMismatch(Lint):
|
|||||||
# Assume for now it is not.
|
# Assume for now it is not.
|
||||||
return True
|
return True
|
||||||
|
|
||||||
dynamic_scope_order = [
|
assert child.scopes.dynamic is not None
|
||||||
None,
|
return capa.rules.is_subscope_compatible(parent.scopes.dynamic, child.scopes.dynamic)
|
||||||
Scope.FILE,
|
|
||||||
Scope.PROCESS,
|
|
||||||
Scope.THREAD,
|
|
||||||
Scope.SPAN_OF_CALLS,
|
|
||||||
Scope.CALL,
|
|
||||||
]
|
|
||||||
|
|
||||||
return dynamic_scope_order.index(child.scopes.dynamic) >= dynamic_scope_order.index(parent.scopes.dynamic)
|
|
||||||
|
|
||||||
|
|
||||||
class OptionalNotUnderAnd(Lint):
|
class OptionalNotUnderAnd(Lint):
|
||||||
|
|||||||
@@ -515,6 +515,36 @@ def test_meta_scope_keywords():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_subscope_same_as_scope():
|
||||||
|
static_scopes = sorted(
|
||||||
|
[e.value for e in capa.rules.STATIC_SCOPES if e not in (capa.rules.Scope.FILE, capa.rules.Scope.GLOBAL)]
|
||||||
|
)
|
||||||
|
dynamic_scopes = sorted(
|
||||||
|
[e.value for e in capa.rules.DYNAMIC_SCOPES if e not in (capa.rules.Scope.FILE, capa.rules.Scope.GLOBAL)]
|
||||||
|
)
|
||||||
|
|
||||||
|
for static_scope in static_scopes:
|
||||||
|
for dynamic_scope in dynamic_scopes:
|
||||||
|
_ = capa.rules.Rule.from_yaml(
|
||||||
|
textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
rule:
|
||||||
|
meta:
|
||||||
|
name: test rule
|
||||||
|
scopes:
|
||||||
|
static: {static_scope}
|
||||||
|
dynamic: {dynamic_scope}
|
||||||
|
features:
|
||||||
|
- or:
|
||||||
|
- {static_scope}:
|
||||||
|
- format: pe
|
||||||
|
- {dynamic_scope}:
|
||||||
|
- format: pe
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_lib_rules():
|
def test_lib_rules():
|
||||||
rules = capa.rules.RuleSet(
|
rules = capa.rules.RuleSet(
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user