Compare commits

...

19 Commits

Author SHA1 Message Date
Willi Ballenthin
5a0d35e826 ida: function: extract function name
somehow we were extracting alternate names but not function names
2025-12-16 17:24:39 +01:00
Willi Ballenthin
28d107c0f3 loader: idalib: disable lumina
see #2742 in which Lumina names overwrote names provided by debug info
2025-12-16 16:58:45 +01:00
Willi Ballenthin
0d44fc5414 tests: idalib: better detect missing idapro package 2025-12-16 15:38:46 +01:00
Willi Ballenthin
654338dcc0 ci: add configuration for idalib tests 2025-12-16 13:11:21 +01:00
Moritz
34488b35fc Merge branch 'master' into idalib-tests 2025-12-15 15:29:29 +00:00
mr-tz
dc08843e2d address idalib-based test fails 2025-12-11 14:18:13 +00:00
Moritz
40b01f0998 Merge pull request #2787 from mandiant/dependabot/pip/msgspec-0.20.0
build(deps): bump msgspec from 0.19.0 to 0.20.0
2025-12-11 11:17:57 +01:00
Moritz
b96a3b6b23 Merge pull request #2786 from mandiant/dependabot/pip/black-25.12.0
build(deps-dev): bump black from 25.11.0 to 25.12.0
2025-12-11 11:17:33 +01:00
Moritz
43e5e60901 Merge pull request #2785 from mandiant/dependabot/pip/types-psutil-7.1.3.20251202
build(deps-dev): bump types-psutil from 7.0.0.20250218 to 7.1.3.20251202
2025-12-11 11:17:14 +01:00
Moritz
0f9f72dbd5 build(deps-dev): bump flake8-bugbear from 25.10.21 to 25.11.29 (#2784)
Bumps [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear) from 25.10.21 to 25.11.29.
- [Release notes](https://github.com/PyCQA/flake8-bugbear/releases)
- [Commits](https://github.com/PyCQA/flake8-bugbear/compare/25.10.21...25.11.29)

---
updated-dependencies:
- dependency-name: flake8-bugbear
  dependency-version: 25.11.29
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-11 11:16:51 +01:00
dependabot[bot]
fd9f584cc4 build(deps): bump msgspec from 0.19.0 to 0.20.0
Bumps [msgspec](https://github.com/jcrist/msgspec) from 0.19.0 to 0.20.0.
- [Release notes](https://github.com/jcrist/msgspec/releases)
- [Changelog](https://github.com/jcrist/msgspec/blob/main/docs/changelog.md)
- [Commits](https://github.com/jcrist/msgspec/compare/0.19.0...0.20.0)

---
updated-dependencies:
- dependency-name: msgspec
  dependency-version: 0.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 14:02:34 +00:00
dependabot[bot]
c3b785e217 build(deps-dev): bump black from 25.11.0 to 25.12.0
Bumps [black](https://github.com/psf/black) from 25.11.0 to 25.12.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/25.11.0...25.12.0)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 25.12.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 14:02:27 +00:00
dependabot[bot]
6ae17f7ef4 build(deps-dev): bump types-psutil from 7.0.0.20250218 to 7.1.3.20251202
Bumps [types-psutil](https://github.com/typeshed-internal/stub_uploader) from 7.0.0.20250218 to 7.1.3.20251202.
- [Commits](https://github.com/typeshed-internal/stub_uploader/commits)

---
updated-dependencies:
- dependency-name: types-psutil
  dependency-version: 7.1.3.20251202
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 14:02:22 +00:00
dependabot[bot]
13297ad324 build(deps-dev): bump flake8-bugbear from 25.10.21 to 25.11.29
Bumps [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear) from 25.10.21 to 25.11.29.
- [Release notes](https://github.com/PyCQA/flake8-bugbear/releases)
- [Commits](https://github.com/PyCQA/flake8-bugbear/compare/25.10.21...25.11.29)

---
updated-dependencies:
- dependency-name: flake8-bugbear
  dependency-version: 25.11.29
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 14:02:19 +00:00
Moritz
074f7c742c Merge branch 'master' into idalib-tests 2025-11-24 19:52:40 +01:00
Willi Ballenthin
cf463676b2 fixtures: remove dups 2025-11-03 12:47:12 +01:00
Willi Ballenthin
b5e5840a63 lints 2025-10-29 20:29:08 +01:00
Willi Ballenthin
f252b6bbd0 changelog 2025-10-29 20:23:12 +01:00
Willi Ballenthin
eda53ab3c1 tests: add feature tests for idalib 2025-10-29 20:20:57 +01:00
11 changed files with 262 additions and 44 deletions

View File

@@ -217,3 +217,52 @@ jobs:
exit_code=$(cat ../output.log | grep exit | awk '{print $NF}') exit_code=$(cat ../output.log | grep exit | awk '{print $NF}')
exit $exit_code exit $exit_code
idalib-tests:
name: IDA tests for ${{ matrix.python-version }}
runs-on: ubuntu-22.04
needs: [tests]
env:
IDA_LICENSE_ID: ${{ secrets.IDA_LICENSE_ID }}
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.13"]
ida:
- version: 9.0
slug: "release/9.0/ida-essential/ida-essential_90_x64linux.run"
- version: 9.2
slug: "release/9.2/ida-essential/ida-essential_92_x64linux.run"
steps:
- name: Checkout capa with submodules
# do only run if IDA_LICENSE_ID is available, have to do this in every step, see https://github.com/orgs/community/discussions/26726#discussioncomment-3253118
if: ${{ env.IDA_LICENSE_ID != 0 }}
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
submodules: recursive
- name: Set up Python ${{ matrix.python-version }}
if: ${{ env.IDA_LICENSE_ID != 0 }}
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
with:
python-version: ${{ matrix.python-version }}
- name: Setup uv
if: ${{ env.IDA_LICENSE_ID != 0 }}
uses: astral-sh/setup-uv@v6
- name: Install dependencies
if: ${{ env.IDA_LICENSE_ID != 0 }}
run: sudo apt-get install -y libyaml-dev
- name: Install capa
if: ${{ env.IDA_LICENSE_ID != 0 }}
run: |
pip install -r requirements.txt
pip install -e .[dev,scripts]
pip install idapro
- name: Install IDA ${{ matrix.ida.version }}
if: ${{ env.IDA_LICENSE_ID != 0 }}
run: |
uv run hcli --disable-updates ida install --download-id ${{ matrix.ida.slug }} --license-id ${{ secrets.IDA_LICENSE_ID }} --set-default --yes
env:
HCLI_API_KEY: ${{ secrets.HCLI_API_KEY }}
IDA_LICENSE_ID: ${{ secrets.IDA_LICENSE_ID }}
- name: Run tests
if: ${{ env.IDA_LICENSE_ID != 0 }}
run: pytest -v tests/test_idalib_features.py # explicitly refer to the idalib tests for performance. other tests run above.

View File

@@ -138,6 +138,7 @@ repos:
- "--ignore=tests/test_ghidra_features.py" - "--ignore=tests/test_ghidra_features.py"
- "--ignore=tests/test_ida_features.py" - "--ignore=tests/test_ida_features.py"
- "--ignore=tests/test_viv_features.py" - "--ignore=tests/test_viv_features.py"
- "--ignore=tests/test_idalib_features.py"
- "--ignore=tests/test_main.py" - "--ignore=tests/test_main.py"
- "--ignore=tests/test_scripts.py" - "--ignore=tests/test_scripts.py"
always_run: true always_run: true

View File

@@ -54,6 +54,7 @@ Additionally a Binary Ninja bug has been fixed. Released binaries now include AR
### New Features ### New Features
- ci: add support for arm64 binary releases - ci: add support for arm64 binary releases
- tests: run tests against IDA via idalib @williballenthin #2742
### Breaking Changes ### Breaking Changes

View File

@@ -18,6 +18,7 @@ import idaapi
import idautils import idautils
import capa.features.extractors.ida.helpers import capa.features.extractors.ida.helpers
from capa.features.file import FunctionName
from capa.features.common import Feature, Characteristic from capa.features.common import Feature, Characteristic
from capa.features.address import Address, AbsoluteVirtualAddress from capa.features.address import Address, AbsoluteVirtualAddress
from capa.features.extractors import loops from capa.features.extractors import loops
@@ -50,10 +51,35 @@ def extract_recursive_call(fh: FunctionHandle):
yield Characteristic("recursive call"), fh.address yield Characteristic("recursive call"), fh.address
def extract_function_name(fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]:
ea = fh.inner.start_ea
name = idaapi.get_name(ea)
yield FunctionName(name), fh.address
if name.startswith("_"):
# some linkers may prefix linked routines with a `_` to avoid name collisions.
# extract features for both the mangled and un-mangled representations.
# e.g. `_fwrite` -> `fwrite`
# see: https://stackoverflow.com/a/2628384/87207
yield FunctionName(name[1:]), fh.address
def extract_function_alternative_names(fh: FunctionHandle):
"""Get all alternative names for an address."""
for aname in capa.features.extractors.ida.helpers.get_function_alternative_names(fh.inner.start_ea):
yield FunctionName(aname), fh.address
def extract_features(fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]: def extract_features(fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]:
for func_handler in FUNCTION_HANDLERS: for func_handler in FUNCTION_HANDLERS:
for feature, addr in func_handler(fh): for feature, addr in func_handler(fh):
yield feature, addr yield feature, addr
FUNCTION_HANDLERS = (extract_function_calls_to, extract_function_loop, extract_recursive_call) FUNCTION_HANDLERS = (
extract_function_calls_to,
extract_function_loop,
extract_recursive_call,
extract_function_name,
extract_function_alternative_names,
)

View File

@@ -20,6 +20,7 @@ import idaapi
import ida_nalt import ida_nalt
import idautils import idautils
import ida_bytes import ida_bytes
import ida_funcs
import ida_segment import ida_segment
from capa.features.address import AbsoluteVirtualAddress from capa.features.address import AbsoluteVirtualAddress
@@ -436,3 +437,23 @@ def is_basic_block_return(bb: idaapi.BasicBlock) -> bool:
def has_sib(oper: idaapi.op_t) -> bool: def has_sib(oper: idaapi.op_t) -> bool:
# via: https://reverseengineering.stackexchange.com/a/14300 # via: https://reverseengineering.stackexchange.com/a/14300
return oper.specflag1 == 1 return oper.specflag1 == 1
def get_function_alternative_names(fva: int):
"""Get all alternative names for an address."""
# Check indented comment
cmt = ida_bytes.get_cmt(fva, False) # False = non-repeatable
if cmt:
for line in cmt.split("\n"):
if line.startswith("Alternative name is '") and line.endswith("'"):
name = line[len("Alternative name is '") : -1] # Extract name between quotes
yield name
# Check function comment
func_cmt = ida_funcs.get_func_cmt(idaapi.get_func(fva), False)
if func_cmt:
for line in func_cmt.split("\n"):
if line.startswith("Alternative name is '") and line.endswith("'"):
name = line[len("Alternative name is '") : -1]
yield name

View File

@@ -22,6 +22,7 @@ import idautils
import capa.features.extractors.helpers import capa.features.extractors.helpers
import capa.features.extractors.ida.helpers import capa.features.extractors.ida.helpers
from capa.features.file import FunctionName
from capa.features.insn import API, MAX_STRUCTURE_SIZE, Number, Offset, Mnemonic, OperandNumber, OperandOffset from capa.features.insn import API, MAX_STRUCTURE_SIZE, Number, Offset, Mnemonic, OperandNumber, OperandOffset
from capa.features.common import MAX_BYTES_FEATURE_SIZE, THUNK_CHAIN_DEPTH_DELTA, Bytes, String, Feature, Characteristic from capa.features.common import MAX_BYTES_FEATURE_SIZE, THUNK_CHAIN_DEPTH_DELTA, Bytes, String, Feature, Characteristic
from capa.features.address import Address, AbsoluteVirtualAddress from capa.features.address import Address, AbsoluteVirtualAddress
@@ -129,8 +130,8 @@ def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle)
# not a function (start) # not a function (start)
return return
if target_func.flags & idaapi.FUNC_LIB: name = idaapi.get_name(target_func.start_ea)
name = idaapi.get_name(target_func.start_ea) if target_func.flags & idaapi.FUNC_LIB or not name.startswith("sub_"):
yield API(name), ih.address yield API(name), ih.address
if name.startswith("_"): if name.startswith("_"):
# some linkers may prefix linked routines with a `_` to avoid name collisions. # some linkers may prefix linked routines with a `_` to avoid name collisions.
@@ -139,6 +140,10 @@ def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle)
# see: https://stackoverflow.com/a/2628384/87207 # see: https://stackoverflow.com/a/2628384/87207
yield API(name[1:]), ih.address yield API(name[1:]), ih.address
for altname in capa.features.extractors.ida.helpers.get_function_alternative_names(target_func.start_ea):
yield FunctionName(altname), ih.address
yield API(altname), ih.address
def extract_insn_number_features( def extract_insn_number_features(
fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle

View File

@@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import io
import os import os
import logging import logging
import datetime import datetime
@@ -23,24 +22,13 @@ from pathlib import Path
from rich.console import Console from rich.console import Console
from typing_extensions import assert_never from typing_extensions import assert_never
import capa.perf
import capa.rules import capa.rules
import capa.engine
import capa.helpers
import capa.version import capa.version
import capa.render.json
import capa.rules.cache
import capa.render.default
import capa.render.verbose
import capa.features.common import capa.features.common
import capa.features.freeze as frz import capa.features.freeze as frz
import capa.render.vverbose
import capa.features.extractors import capa.features.extractors
import capa.render.result_document
import capa.render.result_document as rdoc import capa.render.result_document as rdoc
import capa.features.extractors.common import capa.features.extractors.common
import capa.features.extractors.base_extractor
import capa.features.extractors.cape.extractor
from capa.rules import RuleSet from capa.rules import RuleSet
from capa.engine import MatchResults from capa.engine import MatchResults
from capa.exceptions import UnsupportedOSError, UnsupportedArchError, UnsupportedFormatError from capa.exceptions import UnsupportedOSError, UnsupportedArchError, UnsupportedFormatError
@@ -338,12 +326,23 @@ def get_extractor(
import capa.features.extractors.ida.extractor import capa.features.extractors.ida.extractor
logger.debug("idalib: opening database...") logger.debug("idalib: opening database...")
# idalib writes to stdout (ugh), so we have to capture that idapro.enable_console_messages(False)
# so as not to screw up structured output. with console.status("analyzing program...", spinner="dots"):
with capa.helpers.stdout_redirector(io.BytesIO()): # we set the primary and secondary Lumina servers to 0.0.0.0 to disable Lumina,
with console.status("analyzing program...", spinner="dots"): # which sometimes provides bad names, including overwriting names from debug info.
if idapro.open_database(str(input_path), run_auto_analysis=True): #
raise RuntimeError("failed to analyze input file") # return values from open_database:
# 0 - Success (database not packed)
# 1 - Success (database was packed)
# 2 - User cancelled or 32-64 bit conversion failed
# 4 - Database initialization failed
# -1 - Generic errors (database already open, auto-analysis failed, etc.)
# -2 - User cancelled operation
ret = idapro.open_database(
str(input_path), run_auto_analysis=True, args="-Olumina:host=0.0.0.0 -Osecondary_lumina:host=0.0.0.0"
)
if ret not in (0, 1):
raise RuntimeError("failed to analyze input file")
logger.debug("idalib: waiting for analysis...") logger.debug("idalib: waiting for analysis...")
ida_auto.auto_wait() ida_auto.auto_wait()

View File

@@ -127,7 +127,7 @@ dev = [
"pytest-sugar==1.1.1", "pytest-sugar==1.1.1",
"pytest-instafail==0.5.0", "pytest-instafail==0.5.0",
"flake8==7.3.0", "flake8==7.3.0",
"flake8-bugbear==25.10.21", "flake8-bugbear==25.11.29",
"flake8-encodings==0.5.1", "flake8-encodings==0.5.1",
"flake8-comprehensions==3.17.0", "flake8-comprehensions==3.17.0",
"flake8-logging-format==0.9.0", "flake8-logging-format==0.9.0",
@@ -138,7 +138,7 @@ dev = [
"flake8-use-pathlib==0.3.0", "flake8-use-pathlib==0.3.0",
"flake8-copyright==0.2.4", "flake8-copyright==0.2.4",
"ruff==0.14.7", "ruff==0.14.7",
"black==25.11.0", "black==25.12.0",
"isort==6.0.0", "isort==6.0.0",
"mypy==1.17.1", "mypy==1.17.1",
"mypy-protobuf==3.6.0", "mypy-protobuf==3.6.0",
@@ -148,7 +148,7 @@ dev = [
"types-backports==0.1.3", "types-backports==0.1.3",
"types-colorama==0.4.15.11", "types-colorama==0.4.15.11",
"types-PyYAML==6.0.8", "types-PyYAML==6.0.8",
"types-psutil==7.0.0.20250218", "types-psutil==7.1.3.20251202",
"types_requests==2.32.0.20240712", "types_requests==2.32.0.20240712",
"types-protobuf==6.32.1.20250918", "types-protobuf==6.32.1.20250918",
"deptry==0.23.0" "deptry==0.23.0"

View File

@@ -44,5 +44,5 @@ six==1.17.0
sortedcontainers==2.4.0 sortedcontainers==2.4.0
viv-utils==0.8.0 viv-utils==0.8.0
vivisect==1.2.1 vivisect==1.2.1
msgspec==0.19.0 msgspec==0.20.0
bump-my-version==1.2.4 bump-my-version==1.2.4

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import logging
import contextlib import contextlib
import collections import collections
from pathlib import Path from pathlib import Path
@@ -20,7 +20,6 @@ from functools import lru_cache
import pytest import pytest
import capa.main
import capa.features.file import capa.features.file
import capa.features.insn import capa.features.insn
import capa.features.common import capa.features.common
@@ -53,6 +52,7 @@ from capa.features.extractors.base_extractor import (
) )
from capa.features.extractors.dnfile.extractor import DnfileFeatureExtractor from capa.features.extractors.dnfile.extractor import DnfileFeatureExtractor
logger = logging.getLogger(__name__)
CD = Path(__file__).resolve().parent CD = Path(__file__).resolve().parent
DOTNET_DIR = CD / "data" / "dotnet" DOTNET_DIR = CD / "data" / "dotnet"
DNFILE_TESTFILES = DOTNET_DIR / "dnfile-testfiles" DNFILE_TESTFILES = DOTNET_DIR / "dnfile-testfiles"
@@ -200,6 +200,71 @@ def get_binja_extractor(path: Path):
return extractor return extractor
# we can't easily cache this because the extractor relies on global state (the opened database)
# which also has to be closed elsewhere. so, the idalib tests will just take a little bit to run.
def get_idalib_extractor(path: Path):
import capa.features.extractors.ida.idalib as idalib
if not idalib.has_idalib():
raise RuntimeError("cannot find IDA idalib module.")
if not idalib.load_idalib():
raise RuntimeError("failed to load IDA idalib module.")
import idapro
import ida_auto
import capa.features.extractors.ida.extractor
logger.debug("idalib: opening database...")
idapro.enable_console_messages(False)
# we set the primary and secondary Lumina servers to 0.0.0.0 to disable Lumina,
# which sometimes provides bad names, including overwriting names from debug info.
#
# return values from open_database:
# 0 - Success (database not packed)
# 1 - Success (database was packed)
# 2 - User cancelled or 32-64 bit conversion failed
# 4 - Database initialization failed
# -1 - Generic errors (database already open, auto-analysis failed, etc.)
# -2 - User cancelled operation
ret = idapro.open_database(
str(path), run_auto_analysis=True, args="-Olumina:host=0.0.0.0 -Osecondary_lumina:host=0.0.0.0"
)
if ret not in (0, 1):
raise RuntimeError("failed to analyze input file")
logger.debug("idalib: waiting for analysis...")
ida_auto.auto_wait()
logger.debug("idalib: opened database.")
extractor = capa.features.extractors.ida.extractor.IdaFeatureExtractor()
fixup_idalib(path, extractor)
return extractor
def fixup_idalib(path: Path, extractor):
"""
IDA fixups to overcome differences between backends
"""
import idaapi
import ida_funcs
def remove_library_id_flag(fva):
f = idaapi.get_func(fva)
f.flags &= ~ida_funcs.FUNC_LIB
ida_funcs.update_func(f)
if "kernel32-64" in path.name:
# remove (correct) library function id, so we can test x64 thunk
remove_library_id_flag(0x1800202B0)
if "al-khaser_x64" in path.name:
# remove (correct) library function id, so we can test x64 nested thunk
remove_library_id_flag(0x14004B4F0)
@lru_cache(maxsize=1) @lru_cache(maxsize=1)
def get_cape_extractor(path): def get_cape_extractor(path):
from capa.helpers import load_json_from_path from capa.helpers import load_json_from_path
@@ -894,20 +959,8 @@ FEATURE_PRESENCE_TESTS = sorted(
("mimikatz", "function=0x4556E5", capa.features.insn.API("advapi32.LsaQueryInformationPolicy"), False), ("mimikatz", "function=0x4556E5", capa.features.insn.API("advapi32.LsaQueryInformationPolicy"), False),
("mimikatz", "function=0x4556E5", capa.features.insn.API("LsaQueryInformationPolicy"), True), ("mimikatz", "function=0x4556E5", capa.features.insn.API("LsaQueryInformationPolicy"), True),
# insn/api: x64 # insn/api: x64
(
"kernel32-64",
"function=0x180001010",
capa.features.insn.API("RtlVirtualUnwind"),
True,
),
("kernel32-64", "function=0x180001010", capa.features.insn.API("RtlVirtualUnwind"), True), ("kernel32-64", "function=0x180001010", capa.features.insn.API("RtlVirtualUnwind"), True),
# insn/api: x64 thunk # insn/api: x64 thunk
(
"kernel32-64",
"function=0x1800202B0",
capa.features.insn.API("RtlCaptureContext"),
True,
),
("kernel32-64", "function=0x1800202B0", capa.features.insn.API("RtlCaptureContext"), True), ("kernel32-64", "function=0x1800202B0", capa.features.insn.API("RtlCaptureContext"), True),
# insn/api: x64 nested thunk # insn/api: x64 nested thunk
("al-khaser x64", "function=0x14004B4F0", capa.features.insn.API("__vcrt_GetModuleHandle"), True), ("al-khaser x64", "function=0x14004B4F0", capa.features.insn.API("__vcrt_GetModuleHandle"), True),
@@ -995,20 +1048,20 @@ FEATURE_PRESENCE_TESTS = sorted(
("pma16-01", "file", OS(OS_WINDOWS), True), ("pma16-01", "file", OS(OS_WINDOWS), True),
("pma16-01", "file", OS(OS_LINUX), False), ("pma16-01", "file", OS(OS_LINUX), False),
("mimikatz", "file", OS(OS_WINDOWS), True), ("mimikatz", "file", OS(OS_WINDOWS), True),
("pma16-01", "function=0x404356", OS(OS_WINDOWS), True), ("pma16-01", "function=0x401100", OS(OS_WINDOWS), True),
("pma16-01", "function=0x404356,bb=0x4043B9", OS(OS_WINDOWS), True), ("pma16-01", "function=0x401100,bb=0x401130", OS(OS_WINDOWS), True),
("mimikatz", "function=0x40105D", OS(OS_WINDOWS), True), ("mimikatz", "function=0x40105D", OS(OS_WINDOWS), True),
("pma16-01", "file", Arch(ARCH_I386), True), ("pma16-01", "file", Arch(ARCH_I386), True),
("pma16-01", "file", Arch(ARCH_AMD64), False), ("pma16-01", "file", Arch(ARCH_AMD64), False),
("mimikatz", "file", Arch(ARCH_I386), True), ("mimikatz", "file", Arch(ARCH_I386), True),
("pma16-01", "function=0x404356", Arch(ARCH_I386), True), ("pma16-01", "function=0x401100", Arch(ARCH_I386), True),
("pma16-01", "function=0x404356,bb=0x4043B9", Arch(ARCH_I386), True), ("pma16-01", "function=0x401100,bb=0x401130", Arch(ARCH_I386), True),
("mimikatz", "function=0x40105D", Arch(ARCH_I386), True), ("mimikatz", "function=0x40105D", Arch(ARCH_I386), True),
("pma16-01", "file", Format(FORMAT_PE), True), ("pma16-01", "file", Format(FORMAT_PE), True),
("pma16-01", "file", Format(FORMAT_ELF), False), ("pma16-01", "file", Format(FORMAT_ELF), False),
("mimikatz", "file", Format(FORMAT_PE), True), ("mimikatz", "file", Format(FORMAT_PE), True),
# format is also a global feature # format is also a global feature
("pma16-01", "function=0x404356", Format(FORMAT_PE), True), ("pma16-01", "function=0x401100", Format(FORMAT_PE), True),
("mimikatz", "function=0x456BB9", Format(FORMAT_PE), True), ("mimikatz", "function=0x456BB9", Format(FORMAT_PE), True),
# elf support # elf support
("7351f.elf", "file", OS(OS_LINUX), True), ("7351f.elf", "file", OS(OS_LINUX), True),

View File

@@ -0,0 +1,63 @@
# Copyright 2020 Google LLC
#
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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 logging
import pytest
import fixtures
import capa.features.extractors.ida.idalib
logger = logging.getLogger(__name__)
idalib_present = capa.features.extractors.ida.idalib.has_idalib()
if idalib_present:
try:
import idapro # noqa: F401 [imported but unused]
except ImportError:
idalib_present = False
@pytest.mark.skipif(idalib_present is False, reason="Skip idalib tests if the idalib Python API is not installed")
@fixtures.parametrize(
"sample,scope,feature,expected",
fixtures.FEATURE_PRESENCE_TESTS + fixtures.FEATURE_SYMTAB_FUNC_TESTS,
indirect=["sample", "scope"],
)
def test_idalib_features(sample, scope, feature, expected):
try:
fixtures.do_test_feature_presence(fixtures.get_idalib_extractor, sample, scope, feature, expected)
finally:
logger.debug("closing database...")
import idapro
idapro.close_database(save=False)
logger.debug("opened database.")
@pytest.mark.skipif(idalib_present is False, reason="Skip idalib tests if the idalib Python API is not installed")
@fixtures.parametrize(
"sample,scope,feature,expected",
fixtures.FEATURE_COUNT_TESTS,
indirect=["sample", "scope"],
)
def test_idalib_feature_counts(sample, scope, feature, expected):
try:
fixtures.do_test_feature_count(fixtures.get_idalib_extractor, sample, scope, feature, expected)
finally:
logger.debug("closing database...")
import idapro
idapro.close_database(save=False)
logger.debug("closed database.")