mirror of
https://github.com/mandiant/capa.git
synced 2025-12-22 23:26:21 -08:00
Merge remote-tracking branch 'parentrepo/dynamic-feature-extraction' into sync-1657
This commit is contained in:
@@ -50,7 +50,9 @@ def test_render_meta_attack():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
authors:
|
||||
- foo
|
||||
att&ck:
|
||||
@@ -86,7 +88,9 @@ def test_render_meta_mbc():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
authors:
|
||||
- foo
|
||||
mbc:
|
||||
Submodule tests/data updated: bfcf387b5b...f4e21c6037
@@ -38,7 +38,7 @@ from capa.features.common import (
|
||||
FeatureAccess,
|
||||
)
|
||||
from capa.features.address import Address
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle
|
||||
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, ThreadHandle, ProcessHandle, FunctionHandle
|
||||
from capa.features.extractors.dnfile.extractor import DnfileFeatureExtractor
|
||||
|
||||
CD = Path(__file__).resolve().parent
|
||||
@@ -180,6 +180,20 @@ def get_binja_extractor(path: Path):
|
||||
return extractor
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_cape_extractor(path):
|
||||
import gzip
|
||||
import json
|
||||
|
||||
from capa.features.extractors.cape.extractor import CapeExtractor
|
||||
|
||||
with gzip.open(path, "r") as compressed_report:
|
||||
report_json = compressed_report.read()
|
||||
report = json.loads(report_json)
|
||||
|
||||
return CapeExtractor.from_report(report)
|
||||
|
||||
|
||||
def extract_global_features(extractor):
|
||||
features = collections.defaultdict(set)
|
||||
for feature, va in extractor.extract_global_features():
|
||||
@@ -195,6 +209,23 @@ def extract_file_features(extractor):
|
||||
return features
|
||||
|
||||
|
||||
def extract_process_features(extractor, ph):
|
||||
features = collections.defaultdict(set)
|
||||
for thread in extractor.get_threads(ph):
|
||||
for feature, va in extractor.extract_thread_features(ph, thread):
|
||||
features[feature].add(va)
|
||||
for feature, va in extractor.extract_process_features(ph):
|
||||
features[feature].add(va)
|
||||
return features
|
||||
|
||||
|
||||
def extract_thread_features(extractor, ph, th):
|
||||
features = collections.defaultdict(set)
|
||||
for feature, va in extractor.extract_thread_features(ph, th):
|
||||
features[feature].add(va)
|
||||
return features
|
||||
|
||||
|
||||
# f may not be hashable (e.g. ida func_t) so cannot @lru_cache this
|
||||
def extract_function_features(extractor, fh):
|
||||
features = collections.defaultdict(set)
|
||||
@@ -305,7 +336,11 @@ def get_data_path_by_name(name) -> Path:
|
||||
elif name.startswith("294b8d"):
|
||||
return CD / "data" / "294b8db1f2702b60fb2e42fdc50c2cee6a5046112da9a5703a548a4fa50477bc.elf_"
|
||||
elif name.startswith("2bf18d"):
|
||||
return CD / "data" / "2bf18d0403677378adad9001b1243211.elf_"
|
||||
return os.path.join(CD, "data", "2bf18d0403677378adad9001b1243211.elf_")
|
||||
elif name.startswith("0000a657"):
|
||||
return os.path.join(
|
||||
CD, "data", "dynamic", "cape", "0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json.gz"
|
||||
)
|
||||
elif name.startswith("ea2876"):
|
||||
return CD / "data" / "ea2876e9175410b6f6719f80ee44b9553960758c7d0f7bed73c0fe9a78d8e669.dll_"
|
||||
else:
|
||||
@@ -383,6 +418,20 @@ def sample(request):
|
||||
return resolve_sample(request.param)
|
||||
|
||||
|
||||
def get_process(extractor, ppid: int, pid: int) -> ProcessHandle:
|
||||
for ph in extractor.get_processes():
|
||||
if ph.address.ppid == ppid and ph.address.pid == pid:
|
||||
return ph
|
||||
raise ValueError("process not found")
|
||||
|
||||
|
||||
def get_thread(extractor, ph: ProcessHandle, tid: int) -> ThreadHandle:
|
||||
for th in extractor.get_threads(ph):
|
||||
if th.address.tid == tid:
|
||||
return th
|
||||
raise ValueError("thread not found")
|
||||
|
||||
|
||||
def get_function(extractor, fva: int) -> FunctionHandle:
|
||||
for fh in extractor.get_functions():
|
||||
if isinstance(extractor, DnfileFeatureExtractor):
|
||||
@@ -490,6 +539,40 @@ def resolve_scope(scope):
|
||||
|
||||
inner_function.__name__ = scope
|
||||
return inner_function
|
||||
elif "thread=" in scope:
|
||||
# like `process=(pid:ppid),thread=1002`
|
||||
assert "process=" in scope
|
||||
pspec, _, tspec = scope.partition(",")
|
||||
pspec = pspec.partition("=")[2][1:-1].split(":")
|
||||
assert len(pspec) == 2
|
||||
pid, ppid = map(int, pspec)
|
||||
tid = int(tspec.partition("=")[2])
|
||||
|
||||
def inner_thread(extractor):
|
||||
ph = get_process(extractor, ppid, pid)
|
||||
th = get_thread(extractor, ph, tid)
|
||||
features = extract_thread_features(extractor, ph, th)
|
||||
for k, vs in extract_global_features(extractor).items():
|
||||
features[k].update(vs)
|
||||
return features
|
||||
|
||||
inner_thread.__name__ = scope
|
||||
return inner_thread
|
||||
elif "process=" in scope:
|
||||
# like `process=(pid:ppid)`
|
||||
pspec = scope.partition("=")[2][1:-1].split(":")
|
||||
assert len(pspec) == 2
|
||||
pid, ppid = map(int, pspec)
|
||||
|
||||
def inner_process(extractor):
|
||||
ph = get_process(extractor, ppid, pid)
|
||||
features = extract_process_features(extractor, ph)
|
||||
for k, vs in extract_global_features(extractor).items():
|
||||
features[k].update(vs)
|
||||
return features
|
||||
|
||||
inner_process.__name__ = scope
|
||||
return inner_process
|
||||
else:
|
||||
raise ValueError("unexpected scope fixture")
|
||||
|
||||
@@ -515,6 +598,80 @@ def parametrize(params, values, **kwargs):
|
||||
return pytest.mark.parametrize(params, values, ids=ids, **kwargs)
|
||||
|
||||
|
||||
DYNAMIC_FEATURE_PRESENCE_TESTS = sorted(
|
||||
[
|
||||
# file/string
|
||||
("0000a657", "file", capa.features.common.String("T_Ba?.BcRJa"), True),
|
||||
("0000a657", "file", capa.features.common.String("GetNamedPipeClientSessionId"), True),
|
||||
("0000a657", "file", capa.features.common.String("nope"), False),
|
||||
# file/sections
|
||||
("0000a657", "file", capa.features.file.Section(".rdata"), True),
|
||||
("0000a657", "file", capa.features.file.Section(".nope"), False),
|
||||
# file/imports
|
||||
("0000a657", "file", capa.features.file.Import("NdrSimpleTypeUnmarshall"), True),
|
||||
("0000a657", "file", capa.features.file.Import("Nope"), False),
|
||||
# file/exports
|
||||
("0000a657", "file", capa.features.file.Export("Nope"), False),
|
||||
# process/environment variables
|
||||
(
|
||||
"0000a657",
|
||||
"process=(1180:3052)",
|
||||
capa.features.common.String("C:\\Users\\comp\\AppData\\Roaming\\Microsoft\\Jxoqwnx\\jxoqwn.exe"),
|
||||
True,
|
||||
),
|
||||
("0000a657", "process=(1180:3052)", capa.features.common.String("nope"), False),
|
||||
# thread/api calls
|
||||
("0000a657", "process=(2852:3052),thread=2804", capa.features.insn.API("NtQueryValueKey"), True),
|
||||
("0000a657", "process=(2852:3052),thread=2804", capa.features.insn.API("GetActiveWindow"), False),
|
||||
# thread/number call argument
|
||||
("0000a657", "process=(2852:3052),thread=2804", capa.features.insn.Number(0x000000EC), True),
|
||||
("0000a657", "process=(2852:3052),thread=2804", capa.features.insn.Number(110173), False),
|
||||
# thread/string call argument
|
||||
("0000a657", "process=(2852:3052),thread=2804", capa.features.common.String("SetThreadUILanguage"), True),
|
||||
("0000a657", "process=(2852:3052),thread=2804", capa.features.common.String("nope"), False),
|
||||
],
|
||||
# order tests by (file, item)
|
||||
# so that our LRU cache is most effective.
|
||||
key=lambda t: (t[0], t[1]),
|
||||
)
|
||||
|
||||
DYNAMIC_FEATURE_COUNT_TESTS = sorted(
|
||||
[
|
||||
# file/string
|
||||
("0000a657", "file", capa.features.common.String("T_Ba?.BcRJa"), 1),
|
||||
("0000a657", "file", capa.features.common.String("GetNamedPipeClientSessionId"), 1),
|
||||
("0000a657", "file", capa.features.common.String("nope"), 0),
|
||||
# file/sections
|
||||
("0000a657", "file", capa.features.file.Section(".rdata"), 1),
|
||||
("0000a657", "file", capa.features.file.Section(".nope"), 0),
|
||||
# file/imports
|
||||
("0000a657", "file", capa.features.file.Import("NdrSimpleTypeUnmarshall"), 1),
|
||||
("0000a657", "file", capa.features.file.Import("Nope"), 0),
|
||||
# file/exports
|
||||
("0000a657", "file", capa.features.file.Export("Nope"), 0),
|
||||
# process/environment variables
|
||||
(
|
||||
"0000a657",
|
||||
"process=(1180:3052)",
|
||||
capa.features.common.String("C:\\Users\\comp\\AppData\\Roaming\\Microsoft\\Jxoqwnx\\jxoqwn.exe"),
|
||||
2,
|
||||
),
|
||||
("0000a657", "process=(1180:3052)", capa.features.common.String("nope"), 0),
|
||||
# thread/api calls
|
||||
("0000a657", "process=(2852:3052),thread=2804", capa.features.insn.API("NtQueryValueKey"), 7),
|
||||
("0000a657", "process=(2852:3052),thread=2804", capa.features.insn.API("GetActiveWindow"), 0),
|
||||
# thread/number call argument
|
||||
("0000a657", "process=(2852:3052),thread=2804", capa.features.insn.Number(0x000000EC), 1),
|
||||
("0000a657", "process=(2852:3052),thread=2804", capa.features.insn.Number(110173), 0),
|
||||
# thread/string call argument
|
||||
("0000a657", "process=(2852:3052),thread=2804", capa.features.common.String("SetThreadUILanguage"), 1),
|
||||
("0000a657", "process=(2852:3052),thread=2804", capa.features.common.String("nope"), 0),
|
||||
],
|
||||
# order tests by (file, item)
|
||||
# so that our LRU cache is most effective.
|
||||
key=lambda t: (t[0], t[1]),
|
||||
)
|
||||
|
||||
FEATURE_PRESENCE_TESTS = sorted(
|
||||
[
|
||||
# file/characteristic("embedded pe")
|
||||
|
||||
27
tests/test_cape_features.py
Normal file
27
tests/test_cape_features.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Copyright (C) 2020 Mandiant, Inc. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
||||
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and limitations under the License.
|
||||
import fixtures
|
||||
from fixtures import scope, sample
|
||||
|
||||
|
||||
@fixtures.parametrize(
|
||||
"sample,scope,feature,expected",
|
||||
fixtures.DYNAMIC_FEATURE_PRESENCE_TESTS,
|
||||
indirect=["sample", "scope"],
|
||||
)
|
||||
def test_cape_features(sample, scope, feature, expected):
|
||||
fixtures.do_test_feature_presence(fixtures.get_cape_extractor, sample, scope, feature, expected)
|
||||
|
||||
|
||||
@fixtures.parametrize(
|
||||
"sample,scope,feature,expected",
|
||||
fixtures.DYNAMIC_FEATURE_COUNT_TESTS,
|
||||
indirect=["sample", "scope"],
|
||||
)
|
||||
def test_cape_feature_counts(sample, scope, feature, expected):
|
||||
fixtures.do_test_feature_count(fixtures.get_cape_extractor, sample, scope, feature, expected)
|
||||
@@ -17,7 +17,9 @@ EXPECTED = textwrap.dedent(
|
||||
name: test rule
|
||||
authors:
|
||||
- user@domain.com
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
examples:
|
||||
- foo1234
|
||||
- bar5678
|
||||
@@ -41,7 +43,9 @@ def test_rule_reformat_top_level_elements():
|
||||
name: test rule
|
||||
authors:
|
||||
- user@domain.com
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
examples:
|
||||
- foo1234
|
||||
- bar5678
|
||||
@@ -59,7 +63,9 @@ def test_rule_reformat_indentation():
|
||||
name: test rule
|
||||
authors:
|
||||
- user@domain.com
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
examples:
|
||||
- foo1234
|
||||
- bar5678
|
||||
@@ -83,7 +89,9 @@ def test_rule_reformat_order():
|
||||
examples:
|
||||
- foo1234
|
||||
- bar5678
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
name: test rule
|
||||
features:
|
||||
- and:
|
||||
@@ -107,7 +115,9 @@ def test_rule_reformat_meta_update():
|
||||
examples:
|
||||
- foo1234
|
||||
- bar5678
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
name: AAAA
|
||||
features:
|
||||
- and:
|
||||
@@ -131,7 +141,9 @@ def test_rule_reformat_string_description():
|
||||
name: test rule
|
||||
authors:
|
||||
- user@domain.com
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
features:
|
||||
- and:
|
||||
- string: foo
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import json
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
import fixtures
|
||||
|
||||
import capa.main
|
||||
@@ -17,6 +18,7 @@ import capa.engine
|
||||
import capa.features
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="relies on the legeacy ruleset. scopes keyword hasn't been added there")
|
||||
def test_main(z9324d_extractor):
|
||||
# tests rules can be loaded successfully and all output modes
|
||||
path = z9324d_extractor.path
|
||||
@@ -34,7 +36,9 @@ def test_main_single_rule(z9324d_extractor, tmpdir):
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: file
|
||||
scopes:
|
||||
static: file
|
||||
dynamic: dev
|
||||
authors:
|
||||
- test
|
||||
features:
|
||||
@@ -76,6 +80,7 @@ def test_main_non_ascii_filename_nonexistent(tmpdir, caplog):
|
||||
assert NON_ASCII_FILENAME in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="relies on the legeacy ruleset. scopes keyword hasn't been added there")
|
||||
def test_main_shellcode(z499c2_extractor):
|
||||
path = z499c2_extractor.path
|
||||
assert capa.main.main([path, "-vv", "-f", "sc32"]) == 0
|
||||
@@ -95,7 +100,9 @@ def test_ruleset():
|
||||
rule:
|
||||
meta:
|
||||
name: file rule
|
||||
scope: file
|
||||
scopes:
|
||||
static: file
|
||||
dynamic: dev
|
||||
features:
|
||||
- characteristic: embedded pe
|
||||
"""
|
||||
@@ -107,7 +114,9 @@ def test_ruleset():
|
||||
rule:
|
||||
meta:
|
||||
name: function rule
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
features:
|
||||
- characteristic: tight loop
|
||||
"""
|
||||
@@ -119,17 +128,49 @@ def test_ruleset():
|
||||
rule:
|
||||
meta:
|
||||
name: basic block rule
|
||||
scope: basic block
|
||||
scopes:
|
||||
static: basic block
|
||||
dynamic: dev
|
||||
features:
|
||||
- characteristic: nzxor
|
||||
"""
|
||||
)
|
||||
),
|
||||
capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: process rule
|
||||
scopes:
|
||||
static: file
|
||||
dynamic: process
|
||||
features:
|
||||
- string: "explorer.exe"
|
||||
"""
|
||||
)
|
||||
),
|
||||
capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: thread rule
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: thread
|
||||
features:
|
||||
- api: RegDeleteKey
|
||||
"""
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
assert len(rules.file_rules) == 1
|
||||
assert len(rules.function_rules) == 1
|
||||
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.thread_rules) == 1
|
||||
|
||||
|
||||
def test_match_across_scopes_file_function(z9324d_extractor):
|
||||
@@ -142,7 +183,9 @@ def test_match_across_scopes_file_function(z9324d_extractor):
|
||||
rule:
|
||||
meta:
|
||||
name: install service
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
examples:
|
||||
- 9324d1a8ae37a36ae560c37448c9705a:0x4073F0
|
||||
features:
|
||||
@@ -160,7 +203,9 @@ def test_match_across_scopes_file_function(z9324d_extractor):
|
||||
rule:
|
||||
meta:
|
||||
name: .text section
|
||||
scope: file
|
||||
scopes:
|
||||
static: file
|
||||
dynamic: dev
|
||||
examples:
|
||||
- 9324d1a8ae37a36ae560c37448c9705a
|
||||
features:
|
||||
@@ -177,7 +222,9 @@ def test_match_across_scopes_file_function(z9324d_extractor):
|
||||
rule:
|
||||
meta:
|
||||
name: .text section and install service
|
||||
scope: file
|
||||
scopes:
|
||||
static: file
|
||||
dynamic: dev
|
||||
examples:
|
||||
- 9324d1a8ae37a36ae560c37448c9705a
|
||||
features:
|
||||
@@ -205,7 +252,9 @@ def test_match_across_scopes(z9324d_extractor):
|
||||
rule:
|
||||
meta:
|
||||
name: tight loop
|
||||
scope: basic block
|
||||
scopes:
|
||||
static: basic block
|
||||
dynamic: dev
|
||||
examples:
|
||||
- 9324d1a8ae37a36ae560c37448c9705a:0x403685
|
||||
features:
|
||||
@@ -221,7 +270,9 @@ def test_match_across_scopes(z9324d_extractor):
|
||||
rule:
|
||||
meta:
|
||||
name: kill thread loop
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
examples:
|
||||
- 9324d1a8ae37a36ae560c37448c9705a:0x403660
|
||||
features:
|
||||
@@ -239,7 +290,9 @@ def test_match_across_scopes(z9324d_extractor):
|
||||
rule:
|
||||
meta:
|
||||
name: kill thread program
|
||||
scope: file
|
||||
scopes:
|
||||
static: file
|
||||
dynamic: dev
|
||||
examples:
|
||||
- 9324d1a8ae37a36ae560c37448c9705a
|
||||
features:
|
||||
@@ -266,7 +319,9 @@ def test_subscope_bb_rules(z9324d_extractor):
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
features:
|
||||
- and:
|
||||
- basic block:
|
||||
@@ -290,7 +345,9 @@ def test_byte_matching(z9324d_extractor):
|
||||
rule:
|
||||
meta:
|
||||
name: byte match test
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
features:
|
||||
- and:
|
||||
- bytes: ED 24 9E F4 52 A9 07 47 55 8E E1 AB 30 8E 23 61
|
||||
@@ -313,7 +370,9 @@ def test_count_bb(z9324d_extractor):
|
||||
meta:
|
||||
name: count bb
|
||||
namespace: test
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
features:
|
||||
- and:
|
||||
- count(basic blocks): 1 or more
|
||||
@@ -337,7 +396,9 @@ def test_instruction_scope(z9324d_extractor):
|
||||
meta:
|
||||
name: push 1000
|
||||
namespace: test
|
||||
scope: instruction
|
||||
scopes:
|
||||
static: instruction
|
||||
dynamic: dev
|
||||
features:
|
||||
- and:
|
||||
- mnemonic: push
|
||||
@@ -365,7 +426,9 @@ def test_instruction_subscope(z9324d_extractor):
|
||||
meta:
|
||||
name: push 1000 on i386
|
||||
namespace: test
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
features:
|
||||
- and:
|
||||
- arch: i386
|
||||
@@ -382,6 +445,7 @@ def test_instruction_subscope(z9324d_extractor):
|
||||
assert 0x406F60 in {result[0] for result in capabilities["push 1000 on i386"]}
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="relies on the legeacy ruleset. scopes keyword hasn't been added there")
|
||||
def test_fix262(pma16_01_extractor, capsys):
|
||||
path = pma16_01_extractor.path
|
||||
assert capa.main.main([path, "-vv", "-t", "send HTTP request", "-q"]) == 0
|
||||
@@ -391,6 +455,7 @@ def test_fix262(pma16_01_extractor, capsys):
|
||||
assert "www.practicalmalwareanalysis.com" not in std.out
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="relies on the legeacy ruleset. scopes keyword hasn't been added there")
|
||||
def test_not_render_rules_also_matched(z9324d_extractor, capsys):
|
||||
# rules that are also matched by other rules should not get rendered by default.
|
||||
# this cuts down on the amount of output while giving approx the same detail.
|
||||
@@ -417,6 +482,7 @@ def test_not_render_rules_also_matched(z9324d_extractor, capsys):
|
||||
assert "create TCP socket" in std.out
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="relies on the legeacy ruleset. scopes keyword hasn't been added there")
|
||||
def test_json_meta(capsys):
|
||||
path = str(fixtures.get_data_path_by_name("pma01-01"))
|
||||
assert capa.main.main([path, "-j"]) == 0
|
||||
@@ -432,6 +498,7 @@ def test_json_meta(capsys):
|
||||
assert {"address": ["absolute", 0x10001179]} in info["matched_basic_blocks"]
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="relies on the legeacy ruleset. scopes keyword hasn't been added there")
|
||||
def test_main_dotnet(_1c444_dotnetfile_extractor):
|
||||
# tests successful execution and all output modes
|
||||
path = _1c444_dotnetfile_extractor.path
|
||||
@@ -442,6 +509,7 @@ def test_main_dotnet(_1c444_dotnetfile_extractor):
|
||||
assert capa.main.main([path]) == 0
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="relies on the legeacy ruleset. scopes keyword hasn't been added there")
|
||||
def test_main_dotnet2(_692f_dotnetfile_extractor):
|
||||
# tests successful execution and one rendering
|
||||
# above covers all output modes
|
||||
@@ -449,18 +517,21 @@ def test_main_dotnet2(_692f_dotnetfile_extractor):
|
||||
assert capa.main.main([path, "-vv"]) == 0
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="relies on the legeacy ruleset. scopes keyword hasn't been added there")
|
||||
def test_main_dotnet3(_0953c_dotnetfile_extractor):
|
||||
# tests successful execution and one rendering
|
||||
path = _0953c_dotnetfile_extractor.path
|
||||
assert capa.main.main([path, "-vv"]) == 0
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="relies on the legeacy ruleset. scopes keyword hasn't been added there")
|
||||
def test_main_dotnet4(_039a6_dotnetfile_extractor):
|
||||
# tests successful execution and one rendering
|
||||
path = _039a6_dotnetfile_extractor.path
|
||||
assert capa.main.main([path, "-vv"]) == 0
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="ResultDocument hasn't been updated yet")
|
||||
def test_main_rd():
|
||||
path = str(fixtures.get_data_path_by_name("pma01-01-rd"))
|
||||
assert capa.main.main([path, "-vv"]) == 0
|
||||
|
||||
@@ -23,7 +23,9 @@ def test_optimizer_order():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
features:
|
||||
- and:
|
||||
- substring: "foo"
|
||||
|
||||
@@ -20,7 +20,9 @@ R1 = capa.rules.Rule.from_yaml(
|
||||
name: test rule
|
||||
authors:
|
||||
- user@domain.com
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
examples:
|
||||
- foo1234
|
||||
- bar5678
|
||||
@@ -40,7 +42,9 @@ R2 = capa.rules.Rule.from_yaml(
|
||||
name: test rule 2
|
||||
authors:
|
||||
- user@domain.com
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
examples:
|
||||
- foo1234
|
||||
- bar5678
|
||||
|
||||
@@ -39,7 +39,9 @@ ADDR4 = capa.features.address.AbsoluteVirtualAddress(0x401004)
|
||||
|
||||
|
||||
def test_rule_ctor():
|
||||
r = capa.rules.Rule("test rule", capa.rules.FUNCTION_SCOPE, Or([Number(1)]), {})
|
||||
r = capa.rules.Rule(
|
||||
"test rule", capa.rules.Scopes(capa.rules.FUNCTION_SCOPE, capa.rules.FILE_SCOPE), Or([Number(1)]), {}
|
||||
)
|
||||
assert bool(r.evaluate({Number(0): {ADDR1}})) is False
|
||||
assert bool(r.evaluate({Number(1): {ADDR2}})) is True
|
||||
|
||||
@@ -52,7 +54,9 @@ def test_rule_yaml():
|
||||
name: test rule
|
||||
authors:
|
||||
- user@domain.com
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
examples:
|
||||
- foo1234
|
||||
- bar5678
|
||||
@@ -242,7 +246,9 @@ def test_invalid_rule_feature():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: file
|
||||
scopes:
|
||||
static: file
|
||||
dynamic: dev
|
||||
features:
|
||||
- characteristic: nzxor
|
||||
"""
|
||||
@@ -256,7 +262,9 @@ def test_invalid_rule_feature():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
features:
|
||||
- characteristic: embedded pe
|
||||
"""
|
||||
@@ -270,13 +278,31 @@ def test_invalid_rule_feature():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: basic block
|
||||
scopes:
|
||||
static: basic block
|
||||
dynamic: dev
|
||||
features:
|
||||
- characteristic: embedded pe
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
with pytest.raises(capa.rules.InvalidRule):
|
||||
capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: process
|
||||
features:
|
||||
- mnemonic: xor
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_lib_rules():
|
||||
rules = capa.rules.RuleSet(
|
||||
@@ -319,8 +345,10 @@ def test_subscope_rules():
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: file
|
||||
name: test function subscope
|
||||
scopes:
|
||||
static: file
|
||||
dynamic: dev
|
||||
features:
|
||||
- and:
|
||||
- characteristic: embedded pe
|
||||
@@ -330,17 +358,61 @@ def test_subscope_rules():
|
||||
- characteristic: loop
|
||||
"""
|
||||
)
|
||||
)
|
||||
),
|
||||
capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test process subscope
|
||||
scopes:
|
||||
static: file
|
||||
dynamic: file
|
||||
features:
|
||||
- and:
|
||||
- import: WININET.dll.HttpOpenRequestW
|
||||
- process:
|
||||
- and:
|
||||
- substring: "http://"
|
||||
"""
|
||||
)
|
||||
),
|
||||
capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test thread subscope
|
||||
scopes:
|
||||
static: file
|
||||
dynamic: process
|
||||
features:
|
||||
- and:
|
||||
- string: "explorer.exe"
|
||||
- thread:
|
||||
- api: HttpOpenRequestW
|
||||
"""
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
# the file rule scope will have one rules:
|
||||
# - `test rule`
|
||||
assert len(rules.file_rules) == 1
|
||||
# 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 function rule scope have one rule:
|
||||
# - the rule on which `test rule` depends
|
||||
# the function rule scope have two rule:
|
||||
# - the rule on which `test function subscope` depends
|
||||
assert len(rules.function_rules) == 1
|
||||
|
||||
# the process rule scope has three rules:
|
||||
# - the rule on which `test process subscope` depends,
|
||||
assert len(rules.process_rules) == 2
|
||||
|
||||
# the thread rule scope has one rule:
|
||||
# - the rule on which `test thread subscope` depends
|
||||
assert len(rules.thread_rules) == 1
|
||||
|
||||
|
||||
def test_duplicate_rules():
|
||||
with pytest.raises(capa.rules.InvalidRule):
|
||||
@@ -445,6 +517,66 @@ def test_invalid_rules():
|
||||
"""
|
||||
)
|
||||
)
|
||||
with pytest.raises(capa.rules.InvalidRule):
|
||||
_ = capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scopes:
|
||||
static: basic block
|
||||
behavior: process
|
||||
features:
|
||||
- number: 1
|
||||
"""
|
||||
)
|
||||
)
|
||||
with pytest.raises(capa.rules.InvalidRule):
|
||||
_ = capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scopes:
|
||||
legacy: basic block
|
||||
dynamic: process
|
||||
features:
|
||||
- number: 1
|
||||
"""
|
||||
)
|
||||
)
|
||||
with pytest.raises(capa.rules.InvalidRule):
|
||||
_ = capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scopes:
|
||||
static: process
|
||||
dynamic: process
|
||||
features:
|
||||
- number: 1
|
||||
"""
|
||||
)
|
||||
)
|
||||
with pytest.raises(capa.rules.InvalidRule):
|
||||
_ = capa.rules.Rule.from_yaml(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scopes:
|
||||
static: basic block
|
||||
dynamic: function
|
||||
features:
|
||||
- number: 1
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_number_symbol():
|
||||
@@ -891,7 +1023,9 @@ def test_function_name_features():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: file
|
||||
scopes:
|
||||
static: file
|
||||
dynamic: dev
|
||||
features:
|
||||
- and:
|
||||
- function-name: strcpy
|
||||
@@ -913,7 +1047,9 @@ def test_os_features():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: file
|
||||
scopes:
|
||||
static: file
|
||||
dynamic: dev
|
||||
features:
|
||||
- and:
|
||||
- os: windows
|
||||
@@ -931,7 +1067,9 @@ def test_format_features():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: file
|
||||
scopes:
|
||||
static: file
|
||||
dynamic: dev
|
||||
features:
|
||||
- and:
|
||||
- format: pe
|
||||
@@ -949,7 +1087,9 @@ def test_arch_features():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: file
|
||||
scopes:
|
||||
static: file
|
||||
dynamic: dev
|
||||
features:
|
||||
- and:
|
||||
- arch: amd64
|
||||
|
||||
@@ -20,7 +20,9 @@ def test_rule_scope_instruction():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: instruction
|
||||
scopes:
|
||||
static: instruction
|
||||
dynamic: dev
|
||||
features:
|
||||
- and:
|
||||
- mnemonic: mov
|
||||
@@ -37,7 +39,9 @@ def test_rule_scope_instruction():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: instruction
|
||||
scopes:
|
||||
static: instruction
|
||||
dynamic: dev
|
||||
features:
|
||||
- characteristic: embedded pe
|
||||
"""
|
||||
@@ -54,7 +58,9 @@ def test_rule_subscope_instruction():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
features:
|
||||
- and:
|
||||
- instruction:
|
||||
@@ -83,7 +89,9 @@ def test_scope_instruction_implied_and():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
features:
|
||||
- and:
|
||||
- instruction:
|
||||
@@ -102,7 +110,9 @@ def test_scope_instruction_description():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
features:
|
||||
- and:
|
||||
- instruction:
|
||||
@@ -120,7 +130,9 @@ def test_scope_instruction_description():
|
||||
rule:
|
||||
meta:
|
||||
name: test rule
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
features:
|
||||
- and:
|
||||
- instruction:
|
||||
|
||||
@@ -38,14 +38,22 @@ def get_rule_path():
|
||||
@pytest.mark.parametrize(
|
||||
"script,args",
|
||||
[
|
||||
pytest.param("capa2yara.py", [get_rules_path()]),
|
||||
pytest.param("capafmt.py", [get_rule_path()]),
|
||||
pytest.param("capa2yara.py", [get_rules_path()], marks=pytest.mark.xfail(reason="relies on legacy ruleset")),
|
||||
pytest.param(
|
||||
"capafmt.py", [get_rule_path()], marks=pytest.mark.xfail(reason="rendering hasn't been added yet")
|
||||
),
|
||||
# not testing lint.py as it runs regularly anyway
|
||||
pytest.param("match-function-id.py", [get_file_path()]),
|
||||
pytest.param("show-capabilities-by-function.py", [get_file_path()]),
|
||||
pytest.param(
|
||||
"show-capabilities-by-function.py",
|
||||
[get_file_path()],
|
||||
marks=pytest.mark.xfail(reason="rendering hasn't been added yet"),
|
||||
),
|
||||
pytest.param("show-features.py", [get_file_path()]),
|
||||
pytest.param("show-features.py", ["-F", "0x407970", get_file_path()]),
|
||||
pytest.param("capa_as_library.py", [get_file_path()]),
|
||||
pytest.param(
|
||||
"capa_as_library.py", [get_file_path()], marks=pytest.mark.xfail(reason="relies on legacy ruleset")
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_scripts(script, args):
|
||||
@@ -54,6 +62,7 @@ def test_scripts(script, args):
|
||||
assert p.returncode == 0
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="relies on legacy ruleset")
|
||||
def test_bulk_process(tmp_path):
|
||||
# create test directory to recursively analyze
|
||||
t = tmp_path / "test"
|
||||
@@ -74,6 +83,7 @@ def run_program(script_path, args):
|
||||
return subprocess.run(args, stdout=subprocess.PIPE)
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="rendering hasn't been added yet")
|
||||
def test_proto_conversion(tmp_path):
|
||||
t = tmp_path / "proto-test"
|
||||
t.mkdir()
|
||||
@@ -97,7 +107,9 @@ def test_detect_duplicate_features(tmpdir):
|
||||
rule:
|
||||
meta:
|
||||
name: Test Rule 0
|
||||
scope: function
|
||||
scopes:
|
||||
static: function
|
||||
dynamic: dev
|
||||
features:
|
||||
- and:
|
||||
- number: 1
|
||||
|
||||
@@ -24,7 +24,7 @@ import capa.features.extractors.base_extractor
|
||||
from capa.features.address import Address, AbsoluteVirtualAddress
|
||||
from capa.features.extractors.base_extractor import BBHandle, FunctionHandle
|
||||
|
||||
EXTRACTOR = capa.features.extractors.null.NullFeatureExtractor(
|
||||
EXTRACTOR = capa.features.extractors.null.NullStaticFeatureExtractor(
|
||||
base_address=AbsoluteVirtualAddress(0x401000),
|
||||
global_features=[],
|
||||
file_features=[
|
||||
@@ -83,7 +83,9 @@ def test_null_feature_extractor():
|
||||
rule:
|
||||
meta:
|
||||
name: xor loop
|
||||
scope: basic block
|
||||
scopes:
|
||||
static: basic block
|
||||
dynamic: dev
|
||||
features:
|
||||
- and:
|
||||
- characteristic: tight loop
|
||||
@@ -119,8 +121,8 @@ def compare_extractors(a, b):
|
||||
|
||||
|
||||
def test_freeze_str_roundtrip():
|
||||
load = capa.features.freeze.loads
|
||||
dump = capa.features.freeze.dumps
|
||||
load = capa.features.freeze.loads_static
|
||||
dump = capa.features.freeze.dumps_static
|
||||
reanimated = load(dump(EXTRACTOR))
|
||||
compare_extractors(EXTRACTOR, reanimated)
|
||||
|
||||
Reference in New Issue
Block a user