From 058c1fefd2ddb9e2c01d16b104c1848729c8353b Mon Sep 17 00:00:00 2001 From: Colton Gabertan <66766340+colton-gabertan@users.noreply.github.com> Date: Mon, 21 Aug 2023 11:16:13 -0700 Subject: [PATCH] ghidra: unit tests (#1727) * restore from corrupted .git * lint repo * temp: remove lint failing rule * implement dereferencing, clean up extractors * implement proper dereferencing routines as applicable * fix nzxor implementation, remediate ghidra analysis issues * lint repo * Assert typing, lint repo * avoid extracting pointers in bytes extraction * attempt to recover submodule * implement GhidraFeatureExtractor & ghidra_main() * lint repo * document examples, clean-up & testing * lint repo * properly map import dict * properly map fake addresses * fix fake addr mapping * properly map externs * re-align consistency with other backends * lint repo * fix dereferencing routine * clean up helpers * fix format string * disable progress bar to exit gracefully * enable pbar in headless runtime mode * implement fixture test script * implement ghidra unit test script * refactor repo for breaking Ghidrathon change * bump ghidrathon CI version, run unit test in CI * change CI config * fix wget line for ghidrathon * fix unzip paths * fix ghidra import issue * disable pytest faulthandler module * fix ghidra state variables * use toAddr * restructure for consistency * Bump Ghidrathon version for CI, fix pytest ghidra runtime detection --- .github/workflows/tests.yml | 6 +- capa/features/extractors/ghidra/basicblock.py | 8 +- capa/features/extractors/ghidra/extractor.py | 8 +- capa/features/extractors/ghidra/file.py | 15 ++-- capa/features/extractors/ghidra/function.py | 9 +- capa/features/extractors/ghidra/global_.py | 7 +- capa/features/extractors/ghidra/helpers.py | 17 ++-- capa/features/extractors/ghidra/insn.py | 10 ++- tests/fixtures.py | 12 ++- tests/test_ghidra_features.py | 89 +++++++++++++++++++ 10 files changed, 133 insertions(+), 48 deletions(-) create mode 100644 tests/test_ghidra_features.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e5265340..6b0f7dd0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -153,7 +153,7 @@ jobs: ghidra-version: ["10.3"] public-version: ["PUBLIC_20230510"] # for ghidra releases jep-version: ["4.1.1"] - ghidrathon-version: ["2.1.0"] + ghidrathon-version: ["3.0.0"] steps: - name: Checkout capa with submodules uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 @@ -194,7 +194,5 @@ jobs: - name: Install capa run: pip install -e .[dev] - name: Run tests - run: | # runs main.py for now... - .github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC/support/analyzeHeadless .github/ghidra/project ghidra_test -Import ./tests/data/'Practical Malware Analysis Lab 01-01.dll_' - .github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC/support/analyzeHeadless .github/ghidra/project ghidra_test -process 'Practical Malware Analysis Lab 01-01.dll_' -ScriptPath ./capa -PostScript main.py + run: .github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC/support/analyzeHeadless .github/ghidra/project ghidra_test -Import ./tests/data/'mimikatz.exe_' -ScriptPath ./tests/ -PostScript test_ghidra_features.py diff --git a/capa/features/extractors/ghidra/basicblock.py b/capa/features/extractors/ghidra/basicblock.py index f27ad6ed..7811ba0c 100644 --- a/capa/features/extractors/ghidra/basicblock.py +++ b/capa/features/extractors/ghidra/basicblock.py @@ -20,9 +20,6 @@ from capa.features.basicblock import BasicBlock from capa.features.extractors.helpers import MIN_STACKSTRING_LEN from capa.features.extractors.base_extractor import BBHandle, FunctionHandle -currentProgram = currentProgram() # type: ignore # noqa: F821 -listing = currentProgram.getListing() # type: ignore # noqa: F821 - def get_printable_len(op: ghidra.program.model.scalar.Scalar) -> int: """Return string length if all operand bytes are ascii or utf16-le printable""" @@ -79,7 +76,7 @@ def bb_contains_stackstring(bb: ghidra.program.model.block.CodeBlock) -> bool: true if basic block contains enough moves of constant bytes to the stack """ count = 0 - for insn in listing.getInstructions(bb, True): + for insn in currentProgram().getListing().getInstructions(bb, True): # type: ignore [name-defined] # noqa: F821 if is_mov_imm_to_stack(insn): count += get_printable_len(insn.getScalar(1)) if count > MIN_STACKSTRING_LEN: @@ -91,7 +88,8 @@ def _bb_has_tight_loop(bb: ghidra.program.model.block.CodeBlock): """ parse tight loops, true if last instruction in basic block branches to bb start """ - last_insn = listing.getInstructions(bb, False).next() # Reverse Ordered, first InstructionDB + # Reverse Ordered, first InstructionDB + last_insn = currentProgram().getListing().getInstructions(bb, False).next() # type: ignore [name-defined] # noqa: F821 if last_insn.getFlowType().isJump(): return last_insn.getAddress(0) == bb.getMinAddress() diff --git a/capa/features/extractors/ghidra/extractor.py b/capa/features/extractors/ghidra/extractor.py index 83a6716a..36c4a994 100644 --- a/capa/features/extractors/ghidra/extractor.py +++ b/capa/features/extractors/ghidra/extractor.py @@ -16,9 +16,6 @@ from capa.features.common import Feature from capa.features.address import Address, AbsoluteVirtualAddress from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle, FeatureExtractor -currentProgram = currentProgram() # type: ignore # noqa: F821 -currentAddress = currentAddress() # type: ignore # noqa: F821 - class GhidraFeatureExtractor(FeatureExtractor): def __init__(self): @@ -29,7 +26,7 @@ class GhidraFeatureExtractor(FeatureExtractor): self.global_features.extend(capa.features.extractors.ghidra.global_.extract_arch()) def get_base_address(self): - return AbsoluteVirtualAddress(currentProgram.getImageBase().getOffset()) # type: ignore [name-defined] # noqa: F821 + return AbsoluteVirtualAddress(currentProgram().getImageBase().getOffset()) # type: ignore [name-defined] # noqa: F821 def extract_global_features(self): yield from self.global_features @@ -44,8 +41,7 @@ class GhidraFeatureExtractor(FeatureExtractor): @staticmethod def get_function(addr: int) -> FunctionHandle: - get_addr = currentAddress.getAddress(hex(addr)) # type: ignore [name-defined] # noqa: F821 - func = getFunctionContaining(get_addr) # type: ignore [name-defined] # noqa: F821 + func = getFunctionContaining(toAddr(addr)) # type: ignore [name-defined] # noqa: F821 return FunctionHandle(address=AbsoluteVirtualAddress(func.getAddress().getOffset()), inner=func) def extract_function_features(self, fh: FunctionHandle) -> Iterator[Tuple[Feature, Address]]: diff --git a/capa/features/extractors/ghidra/file.py b/capa/features/extractors/ghidra/file.py index f3da41f9..4de925da 100644 --- a/capa/features/extractors/ghidra/file.py +++ b/capa/features/extractors/ghidra/file.py @@ -19,7 +19,6 @@ from capa.features.file import Export, Import, Section, FunctionName from capa.features.common import FORMAT_PE, FORMAT_ELF, Format, String, Feature, Characteristic from capa.features.address import NO_ADDRESS, Address, FileOffsetAddress, AbsoluteVirtualAddress -currentProgram = currentProgram() # type: ignore # noqa: F821 MAX_OFFSET_PE_AFTER_MZ = 0x200 @@ -46,7 +45,7 @@ def check_segment_for_pe() -> Iterator[Tuple[int, int]]: for off in capa.features.extractors.ghidra.helpers.find_byte_sequence(mzx): todo.append((off, mzx, pex, i)) - seg_max = currentProgram.getMaxAddress() # type: ignore [name-defined] # noqa: F821 + seg_max = currentProgram().getMaxAddress() # type: ignore [name-defined] # noqa: F821 while len(todo): off, mzx, pex, i = todo.pop() @@ -81,7 +80,7 @@ def extract_file_embedded_pe() -> Iterator[Tuple[Feature, Address]]: def extract_file_export_names() -> Iterator[Tuple[Feature, Address]]: """extract function exports""" - st = currentProgram.getSymbolTable() # type: ignore [name-defined] # noqa: F821 + st = currentProgram().getSymbolTable() # type: ignore [name-defined] # noqa: F821 for addr in st.getExternalEntryPointIterator(): yield Export(st.getPrimarySymbol(addr).getName()), AbsoluteVirtualAddress(addr.getOffset()) @@ -98,7 +97,7 @@ def extract_file_import_names() -> Iterator[Tuple[Feature, Address]]: - importname """ - for f in currentProgram.getFunctionManager().getExternalFunctions(): # type: ignore [name-defined] # noqa: F821 + for f in currentProgram().getFunctionManager().getExternalFunctions(): # type: ignore [name-defined] # noqa: F821 for r in f.getSymbol().getReferences(): if r.getReferenceType().isData(): addr = r.getFromAddress().getOffset() # gets pointer to fake external addr @@ -114,14 +113,14 @@ def extract_file_import_names() -> Iterator[Tuple[Feature, Address]]: def extract_file_section_names() -> Iterator[Tuple[Feature, Address]]: """extract section names""" - for block in currentProgram.getMemory().getBlocks(): # type: ignore [name-defined] # noqa: F821 + for block in currentProgram().getMemory().getBlocks(): # type: ignore [name-defined] # noqa: F821 yield Section(block.getName()), AbsoluteVirtualAddress(block.getStart().getOffset()) def extract_file_strings() -> Iterator[Tuple[Feature, Address]]: """extract ASCII and UTF-16 LE strings""" - for block in currentProgram.getMemory().getBlocks(): # type: ignore [name-defined] # noqa: F821 + for block in currentProgram().getMemory().getBlocks(): # type: ignore [name-defined] # noqa: F821 if block.isInitialized(): p_bytes = capa.features.extractors.ghidra.helpers.get_block_bytes(block) @@ -139,7 +138,7 @@ def extract_file_function_names() -> Iterator[Tuple[Feature, Address]]: extract the names of statically-linked library functions. """ - for sym in currentProgram.getSymbolTable().getAllSymbols(True): # type: ignore [name-defined] # noqa: F821 + for sym in currentProgram().getSymbolTable().getAllSymbols(True): # type: ignore [name-defined] # noqa: F821 # .isExternal() misses more than this config for the function symbols if sym.getSymbolType() == SymbolType.FUNCTION and sym.getSource() == SourceType.ANALYSIS and sym.isGlobal(): name = sym.getName() # starts to resolve names based on Ghidra's FidDB @@ -156,7 +155,7 @@ def extract_file_function_names() -> Iterator[Tuple[Feature, Address]]: def extract_file_format() -> Iterator[Tuple[Feature, Address]]: - ef = currentProgram.getExecutableFormat() # type: ignore [name-defined] # noqa: F821 + ef = currentProgram().getExecutableFormat() # type: ignore [name-defined] # noqa: F821 if "PE" in ef: yield Format(FORMAT_PE), NO_ADDRESS elif "ELF" in ef: diff --git a/capa/features/extractors/ghidra/function.py b/capa/features/extractors/ghidra/function.py index 3f9c956c..d31ba86a 100644 --- a/capa/features/extractors/ghidra/function.py +++ b/capa/features/extractors/ghidra/function.py @@ -16,9 +16,6 @@ from capa.features.address import Address, AbsoluteVirtualAddress from capa.features.extractors import loops from capa.features.extractors.base_extractor import FunctionHandle -currentProgram = currentProgram() # type: ignore # noqa: F821 -monitor = monitor() # type: ignore # noqa: F821 - def extract_function_calls_to(fh: FunctionHandle): """extract callers to a function""" @@ -32,8 +29,8 @@ def extract_function_loop(fh: FunctionHandle): f: ghidra.program.database.function.FunctionDB = fh.inner edges = [] - for block in SimpleBlockIterator(BasicBlockModel(currentProgram), f.getBody(), monitor): # type: ignore [name-defined] # noqa: F821 - dests = block.getDestinations(monitor) # type: ignore [name-defined] # noqa: F821 + for block in SimpleBlockIterator(BasicBlockModel(currentProgram()), f.getBody(), monitor()): # type: ignore [name-defined] # noqa: F821 + dests = block.getDestinations(monitor()) # type: ignore [name-defined] # noqa: F821 s_addrs = block.getStartAddresses() while dests.hasNext(): # For loop throws Python TypeError @@ -47,7 +44,7 @@ def extract_function_loop(fh: FunctionHandle): def extract_recursive_call(fh: FunctionHandle): f: ghidra.program.database.function.FunctionDB = fh.inner - for func in f.getCalledFunctions(monitor): # type: ignore [name-defined] # noqa: F821 + for func in f.getCalledFunctions(monitor()): # type: ignore [name-defined] # noqa: F821 if func.getEntryPoint().getOffset() == f.getEntryPoint().getOffset(): yield Characteristic("recursive call"), AbsoluteVirtualAddress(f.getEntryPoint().getOffset()) diff --git a/capa/features/extractors/ghidra/global_.py b/capa/features/extractors/ghidra/global_.py index 59797dec..b2e3093a 100644 --- a/capa/features/extractors/ghidra/global_.py +++ b/capa/features/extractors/ghidra/global_.py @@ -15,17 +15,16 @@ from capa.features.common import OS, ARCH_I386, ARCH_AMD64, OS_WINDOWS, Arch, Fe from capa.features.address import NO_ADDRESS, Address logger = logging.getLogger(__name__) -currentProgram = currentProgram() # type: ignore # noqa: F821 def extract_os() -> Iterator[Tuple[Feature, Address]]: - format_name: str = currentProgram.getExecutableFormat() # type: ignore [name-defined] # noqa: F821 + format_name: str = currentProgram().getExecutableFormat() # type: ignore [name-defined] # noqa: F821 if "PE" in format_name: yield OS(OS_WINDOWS), NO_ADDRESS elif "ELF" in format_name: - program_memory = currentProgram.getMemory() # type: ignore [name-defined] # noqa: F821 + program_memory = currentProgram().getMemory() # type: ignore [name-defined] # noqa: F821 fbytes_list = program_memory.getAllFileBytes() fbytes = fbytes_list[0] @@ -58,7 +57,7 @@ def extract_os() -> Iterator[Tuple[Feature, Address]]: def extract_arch() -> Iterator[Tuple[Feature, Address]]: - lang_id = currentProgram.getMetadata().get("Language ID") # type: ignore [name-defined] # noqa: F821 + lang_id = currentProgram().getMetadata().get("Language ID") # type: ignore [name-defined] # noqa: F821 if "x86" in lang_id and "64" in lang_id: yield Arch(ARCH_AMD64), NO_ADDRESS diff --git a/capa/features/extractors/ghidra/helpers.py b/capa/features/extractors/ghidra/helpers.py index 6f520b09..9cf70485 100644 --- a/capa/features/extractors/ghidra/helpers.py +++ b/capa/features/extractors/ghidra/helpers.py @@ -17,9 +17,6 @@ import capa.features.extractors.helpers from capa.features.address import AbsoluteVirtualAddress from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle -monitor = monitor() # type: ignore # noqa: F821 -currentProgram = currentProgram() # type: ignore # noqa: F821 - def fix_byte(b: int) -> bytes: """Transform signed ints from Java into bytes for Python @@ -38,7 +35,7 @@ def find_byte_sequence(seq: bytes) -> Iterator[int]: """ seqstr = "".join([f"\\x{b:02x}" for b in seq]) # .add(1) to avoid false positives on regular PE files - eas = findBytes(currentProgram.getMinAddress().add(1), seqstr, 1, 1) # type: ignore [name-defined] # noqa: F821 + eas = findBytes(currentProgram().getMinAddress().add(1), seqstr, 1, 1) # type: ignore [name-defined] # noqa: F821 yield from eas @@ -80,7 +77,7 @@ def get_block_bytes(block: ghidra.program.model.mem.MemoryBlock) -> bytes: def get_function_symbols() -> Iterator[FunctionHandle]: """yield all non-external function symbols""" - for fhandle in currentProgram.getFunctionManager().getFunctionsNoStubs(True): # type: ignore [name-defined] # noqa: F821 + for fhandle in currentProgram().getFunctionManager().getFunctionsNoStubs(True): # type: ignore [name-defined] # noqa: F821 yield FunctionHandle(address=AbsoluteVirtualAddress(fhandle.getEntryPoint().getOffset()), inner=fhandle) @@ -88,7 +85,7 @@ def get_function_blocks(fh: FunctionHandle) -> Iterator[BBHandle]: """yield BBHandle for each bb in a given function""" func: ghidra.program.database.function.FunctionDB = fh.inner - for bb in SimpleBlockIterator(BasicBlockModel(currentProgram), func.getBody(), monitor): # type: ignore [name-defined] # noqa: F821 + for bb in SimpleBlockIterator(BasicBlockModel(currentProgram()), func.getBody(), monitor()): # type: ignore [name-defined] # noqa: F821 yield BBHandle(address=AbsoluteVirtualAddress(bb.getMinAddress().getOffset()), inner=bb) @@ -107,7 +104,7 @@ def get_file_imports() -> Dict[int, List[str]]: import_dict: Dict[int, List[str]] = {} - for f in currentProgram.getFunctionManager().getExternalFunctions(): # type: ignore [name-defined] # noqa: F821 + for f in currentProgram().getFunctionManager().getExternalFunctions(): # type: ignore [name-defined] # noqa: F821 for r in f.getSymbol().getReferences(): if r.getReferenceType().isData(): addr = r.getFromAddress().getOffset() # gets pointer to fake external addr @@ -138,7 +135,7 @@ def get_file_externs() -> Dict[int, List[str]]: extern_dict: Dict[int, List[str]] = {} - for sym in currentProgram.getSymbolTable().getAllSymbols(True): # type: ignore [name-defined] # noqa: F821 + for sym in currentProgram().getSymbolTable().getAllSymbols(True): # type: ignore [name-defined] # noqa: F821 # .isExternal() misses more than this config for the function symbols if sym.getSymbolType() == SymbolType.FUNCTION and sym.getSource() == SourceType.ANALYSIS and sym.isGlobal(): name = sym.getName() # starts to resolve names based on Ghidra's FidDB @@ -176,7 +173,7 @@ def map_fake_import_addrs() -> Dict[int, List[int]]: """ fake_dict: Dict[int, List[int]] = {} - for f in currentProgram.getFunctionManager().getExternalFunctions(): # type: ignore [name-defined] # noqa: F821 + for f in currentProgram().getFunctionManager().getExternalFunctions(): # type: ignore [name-defined] # noqa: F821 for r in f.getSymbol().getReferences(): if r.getReferenceType().isData(): fake_dict.setdefault(f.getEntryPoint().getOffset(), []).append(r.getFromAddress().getOffset()) @@ -200,7 +197,7 @@ def get_external_locs() -> List[int]: - 0x000b34EC -> External Location """ locs = [] - for fh in currentProgram.getFunctionManager().getExternalFunctions(): # type: ignore [name-defined] # noqa: F821 + for fh in currentProgram().getFunctionManager().getExternalFunctions(): # type: ignore [name-defined] # noqa: F821 external_loc = fh.getExternalLocation().getAddress() if external_loc: locs.append(external_loc) diff --git a/capa/features/extractors/ghidra/insn.py b/capa/features/extractors/ghidra/insn.py index 9c77286f..bf733472 100644 --- a/capa/features/extractors/ghidra/insn.py +++ b/capa/features/extractors/ghidra/insn.py @@ -21,8 +21,6 @@ from capa.features.extractors.base_extractor import BBHandle, InsnHandle, Functi # security cookie checks may perform non-zeroing XORs, these are expected within a certain # byte range within the first and returning basic blocks, this helps to reduce FP features SECURITY_COOKIE_BYTES_DELTA = 0x40 -currentProgram = currentProgram() # type: ignore # noqa: F821 -monitor = monitor() # type: ignore # noqa: F821 # significantly cut down on runtime by caching api info imports = capa.features.extractors.ghidra.helpers.get_file_imports() @@ -375,13 +373,17 @@ def check_nzxor_security_cookie_delta( Check the last bb of the function containing the insn """ - model = SimpleBlockModel(currentProgram) # type: ignore [name-defined] # noqa: F821 + model = SimpleBlockModel(currentProgram()) # type: ignore [name-defined] # noqa: F821 insn_addr = insn.getAddress() func_asv = fh.getBody() first_addr = func_asv.getMinAddress() last_addr = func_asv.getMaxAddress() - if model.getFirstCodeBlockContaining(first_addr, monitor) == model.getFirstCodeBlockContaining(last_addr, monitor): # type: ignore [name-defined] # noqa: F821 + if model.getFirstCodeBlockContaining( + first_addr, monitor() # type: ignore [name-defined] # noqa: F821 + ) == model.getFirstCodeBlockContaining( + last_addr, monitor() # type: ignore [name-defined] # noqa: F821 + ): if insn_addr < first_addr.add(SECURITY_COOKIE_BYTES_DELTA): return True else: diff --git a/tests/fixtures.py b/tests/fixtures.py index 0dd6ea59..d2339f1e 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -180,6 +180,16 @@ def get_binja_extractor(path: Path): return extractor +@lru_cache(maxsize=1) +def get_ghidra_extractor(path: Path): + import capa.features.extractors.ghidra.extractor + + extractor = capa.features.extractors.ghidra.extractor.GhidraFeatureExtractor() + setattr(extractor, "path", path.as_posix()) + + return extractor + + def extract_global_features(extractor): features = collections.defaultdict(set) for feature, va in extractor.extract_global_features(): @@ -357,7 +367,7 @@ def get_sample_md5_by_name(name): elif name.startswith("3b13b"): # file name is SHA256 hash return "56a6ffe6a02941028cc8235204eef31d" - elif name == "7351f.elf": + elif name.startswith("7351f"): return "7351f8a40c5450557b24622417fc478d" elif name.startswith("79abd"): return "79abd17391adc6251ecdc58d13d76baf" diff --git a/tests/test_ghidra_features.py b/tests/test_ghidra_features.py new file mode 100644 index 00000000..cbd05782 --- /dev/null +++ b/tests/test_ghidra_features.py @@ -0,0 +1,89 @@ +# Copyright (C) 2023 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. +""" +Must invoke this script from within the Ghidra Runtime Enviornment +""" +import sys +import logging + +import pytest +import fixtures + +logger = logging.getLogger("test_ghidra_features") + +ghidra_present: bool = False +try: + import ghidra # noqa: F401 + + ghidra_present = True +except ImportError: + pass + + +def standardize_posix_str(psx_str): + """fixture test passes the PosixPath to the test data + + params: psx_str - PosixPath() to the test data + return: string that matches test-id sample name + """ + + if "Practical Malware Analysis Lab" in str(psx_str): + # /'Practical Malware Analysis Lab 16-01.exe_' -> 'pma16-01' + wanted_str = "pma" + str(psx_str).split("/")[-1][len("Practical Malware Analysis Lab ") : -5] + else: + # /mimikatz.exe_ -> mimikatz + wanted_str = str(psx_str).split("/")[-1][:-5] + + if "_" in wanted_str: + # al-khaser_x86 -> al-khaser x86 + wanted_str = wanted_str.replace("_", " ") + + return wanted_str + + +def check_input_file(wanted): + """check that test is running on the loaded sample + + params: wanted - PosixPath() passed from test arg + """ + + import capa.ghidra.helpers as ghidra_helpers + + found = ghidra_helpers.get_file_md5() + sample_name = standardize_posix_str(wanted) + + if not found.startswith(fixtures.get_sample_md5_by_name(sample_name)): + raise RuntimeError(f"please run the tests against sample with MD5: `{found}`") + + +@pytest.mark.skipif(ghidra_present is False, reason="Ghidra tests must be ran within Ghidra") +@fixtures.parametrize("sample,scope,feature,expected", fixtures.FEATURE_PRESENCE_TESTS, indirect=["sample", "scope"]) +def test_ghidra_features(sample, scope, feature, expected): + try: + check_input_file(sample) + except RuntimeError: + pytest.skip(reason="Test must be ran against sample loaded in Ghidra") + + fixtures.do_test_feature_presence(fixtures.get_ghidra_extractor, sample, scope, feature, expected) + + +@pytest.mark.skipif(ghidra_present is False, reason="Ghidra tests must be ran within Ghidra") +@fixtures.parametrize("sample,scope,feature,expected", fixtures.FEATURE_COUNT_TESTS, indirect=["sample", "scope"]) +def test_ghidra_feature_counts(sample, scope, feature, expected): + try: + check_input_file(sample) + except RuntimeError: + pytest.skip(reason="Test must be ran against sample loaded in Ghidra") + + fixtures.do_test_feature_count(fixtures.get_ghidra_extractor, sample, scope, feature, expected) + + +if __name__ == "__main__": + # No support for faulthandler module in Ghidrathon, see: + # https://github.com/mandiant/Ghidrathon/issues/70 + sys.exit(pytest.main(["--pyargs", "-p no:faulthandler", "test_ghidra_features"]))