Skip to content

Commit edf7f89

Browse files
tests: migrate BinExport2 count tests to data-driven fixtures
Extend parse_feature to handle operand[N].number and operand[N].offset via functools.partial so count(operand[1].offset(0x0)) can be expressed in fixture JSON. Move the two BE2_INTEL count tests into binexport.json, remove the old FEATURE_COUNT_TESTS_BE2_INTEL list and do_test_feature_count helper. Also fix rebase issues: wrong import in get_viv_extractor (capa.main → capa.loader), missing import in get_ghidra_extractor, missing CD constant in test_scripts.py, and import ordering. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 44fe97c commit edf7f89

5 files changed

Lines changed: 24 additions & 39 deletions

File tree

capa/rules/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import struct
2222
import logging
2323
import binascii
24+
import functools
2425
import collections
2526
from enum import Enum
2627
from typing import Any, Union, Callable, Iterator, Optional, cast
@@ -444,6 +445,12 @@ def parse_feature(key: str):
444445
return capa.features.common.Namespace
445446
elif key == "property":
446447
return capa.features.insn.Property
448+
elif key.startswith("operand[") and key.endswith("].number"):
449+
index = int(key[len("operand[") : -len("].number")])
450+
return functools.partial(capa.features.insn.OperandNumber, index)
451+
elif key.startswith("operand[") and key.endswith("].offset"):
452+
index = int(key[len("operand[") : -len("].offset")])
453+
return functools.partial(capa.features.insn.OperandOffset, index)
447454
else:
448455
raise InvalidRule(f"unexpected statement: {key}")
449456

tests/fixtures.py

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525

2626
import capa.rules
2727
import capa.engine as ceng
28-
import capa.loader
2928
import capa.render.result_document
3029
from capa.features.common import OS_AUTO, FORMAT_AUTO, Feature
3130
from capa.features.address import Address
@@ -647,29 +646,6 @@ def parametrize(params, values, **kwargs):
647646
return pytest.mark.parametrize(params, values, ids=ids, **kwargs)
648647

649648

650-
FEATURE_COUNT_TESTS_BE2_INTEL = [
651-
(
652-
"mimikatz",
653-
"function=0x40105d,bb=0x401125,insn=0x401125",
654-
capa.features.insn.Offset(0),
655-
1,
656-
),
657-
(
658-
"mimikatz",
659-
"function=0x40105d,bb=0x401125,insn=0x401125",
660-
capa.features.insn.OperandOffset(1, 0),
661-
1,
662-
),
663-
]
664-
665-
666-
def do_test_feature_count(get_extractor, sample, scope, feature, expected):
667-
extractor = get_extractor(sample)
668-
features = scope(extractor)
669-
assert features.get(feature, set()) != set(), f"{feature} should be found in {scope.__name__}"
670-
assert len(features[feature]) == expected, f"{feature} should be found {expected} times in {scope.__name__}"
671-
672-
673649
def get_result_doc(path: Path):
674650
return capa.render.result_document.ResultDocument.from_file(path)
675651

@@ -727,7 +703,7 @@ def dynamic_a0000a6_rd():
727703
# as well as some fixtures below
728704
@functools.lru_cache(maxsize=1)
729705
def get_viv_extractor(path: Path):
730-
import capa.main
706+
import capa.loader
731707
import capa.features.extractors.viv.extractor
732708

733709
sigpaths = [
@@ -856,6 +832,7 @@ def get_ghidra_extractor(path: Path):
856832
if not pyghidra.started():
857833
pyghidra.start()
858834

835+
import capa.loader
859836
import capa.features.extractors.ghidra.context
860837

861838
if path in GHIDRA_CACHE:

tests/fixtures/features/binexport.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,18 @@
10551055
"location": "function=0x401000",
10561056
"feature": "count(basic blocks): 3",
10571057
"explanation": "Ghidra: 3 basic blocks in function"
1058+
},
1059+
{
1060+
"file": "mimikatz.ghidra.be2",
1061+
"location": "function=0x40105d,bb=0x401125,insn=0x401125",
1062+
"feature": "count(offset(0x0)): 1",
1063+
"explanation": "MOV [EDI], CX matches OFFSET_ZERO_PATTERNS, must yield Offset(0) exactly once"
1064+
},
1065+
{
1066+
"file": "mimikatz.ghidra.be2",
1067+
"location": "function=0x40105d,bb=0x401125,insn=0x401125",
1068+
"feature": "count(operand[1].offset(0x0)): 1",
1069+
"explanation": "MOV [EDI], CX matches OFFSET_ZERO_PATTERNS, must yield OperandOffset(1, 0) exactly once"
10581070
}
10591071
]
10601072
}

tests/test_binexport_features.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,3 @@
2323
def test_binexport_features(feature_fixture):
2424
extractor = fixtures.get_binexport_extractor(feature_fixture.sample_path)
2525
fixtures.run_feature_fixture(extractor, feature_fixture)
26-
27-
28-
@fixtures.parametrize(
29-
"sample,scope,feature,expected",
30-
fixtures.FEATURE_COUNT_TESTS_BE2_INTEL,
31-
indirect=["sample", "scope"],
32-
)
33-
def test_binexport_feature_counts_intel(sample, scope, feature, expected):
34-
sample = sample.parent / "binexport2" / (sample.name + ".ghidra.BinExport")
35-
assert sample.exists()
36-
fixtures.do_test_feature_count(
37-
fixtures.get_binexport_extractor, sample, scope, feature, expected
38-
)

tests/test_scripts.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
from pathlib import Path
2222

2323
import pytest
24+
import fixtures
2425

2526
import capa.rules
26-
import fixtures
2727

2828
logger = logging.getLogger(__name__)
2929

30+
CD = Path(__file__).resolve().parent
31+
3032

3133
def get_script_path(s: str):
3234
return str(fixtures.CD / ".." / "scripts" / s)

0 commit comments

Comments
 (0)