From b2f0c1bc18544e26d7832f4651df7a7b7c7cb20b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Mu=C3=B1oz?= Date: Tue, 3 Mar 2026 01:23:08 +0100 Subject: [PATCH 1/3] binja: improve Binary Ninja API detection on macOS and Windows --- .../extractors/binja/find_binja_api.py | 70 +++++++++++++++---- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/capa/features/extractors/binja/find_binja_api.py b/capa/features/extractors/binja/find_binja_api.py index 0dee647df4..47dbbbe9b3 100644 --- a/capa/features/extractors/binja/find_binja_api.py +++ b/capa/features/extractors/binja/find_binja_api.py @@ -89,20 +89,53 @@ def get_binaryninja_path(desktop_entry: Path) -> Optional[Path]: return None +def _is_python_dir(p: Path) -> bool: + return (p / "binaryninja" / "__init__.py").is_file() + + +def _to_python_dir(p: Path) -> Optional[Path]: + """ + Accept either: + - root dir containing python/ (Linux desktop entry, macOS Resources) + - python dir itself (macOS/Windows direct detection) + Return python dir or None. + """ + if not p: + return None + if _is_python_dir(p): + return p + if _is_python_dir(p / "python"): + return p / "python" + return None def validate_binaryninja_path(binaryninja_path: Path) -> bool: - if not binaryninja_path: - return False + return _to_python_dir(binaryninja_path) is not None - module_path = binaryninja_path / "python" - if not module_path.is_dir(): - return False +def find_binaryninja_path_via_env() -> Optional[Path]: + env_install = os.environ.get("BN_INSTALL_DIR") + return Path(env_install) if env_install else None - if not (module_path / "binaryninja" / "__init__.py").is_file(): - return False - return True +def find_binaryninja_path_via_lastrun() -> Optional[Path]: + candidates: list[Path] = [] + + if sys.platform == "win32": + appdata = os.environ.get("APPDATA") or str(Path.home() / "AppData" / "Roaming") + candidates.append(Path(appdata) / "Binary Ninja" / "lastrun") + if sys.platform == "darwin": + candidates.append(Path.home() / "Library" / "Application Support" / "Binary Ninja" / "lastrun") + + # linux/other + candidates.append(Path.home() / ".binaryninja" / "lastrun") + + for lastrun in candidates: + try: + return Path(lastrun.read_text(encoding="utf-8").strip()) + except OSError: + continue + + return None def find_binaryninja() -> Optional[Path]: binaryninja_path = find_binaryninja_path_via_subprocess() @@ -111,10 +144,23 @@ def find_binaryninja() -> Optional[Path]: # ok logger.debug("detected OS: linux") elif sys.platform == "darwin": - logger.warning("unsupported platform to find Binary Ninja: %s", sys.platform) - return None + binaryninja_path = Path("/Applications/Binary Ninja.app/Contents/Resources") + if not validate_binaryninja_path(binaryninja_path): + logger.debug("failed to find Binary Ninja at default macOS path") + return None + return _to_python_dir(binaryninja_path) + elif sys.platform == "win32": - logger.warning("unsupported platform to find Binary Ninja: %s", sys.platform) + for candidate in ( + find_binaryninja_path_via_env(), + find_binaryninja_path_via_lastrun(), + Path("C:/Program Files/Vector35/BinaryNinja"), + ): + if candidate: + python_dir = _to_python_dir(candidate) + if python_dir: + return python_dir + logger.debug("failed to find Binary Ninja at default Windows path") return None else: logger.warning("unsupported platform to find Binary Ninja: %s", sys.platform) @@ -137,7 +183,7 @@ def find_binaryninja() -> Optional[Path]: logger.debug("found Binary Ninja installation: %s", binaryninja_path) - return binaryninja_path / "python" + return _to_python_dir(binaryninja_path) def is_binaryninja_installed() -> bool: From ddcdd467ac16593d74cd697686d7ec4cd369ec0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Mu=C3=B1oz?= Date: Tue, 3 Mar 2026 01:42:28 +0100 Subject: [PATCH 2/3] Update capa/features/extractors/binja/find_binja_api.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- capa/features/extractors/binja/find_binja_api.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/capa/features/extractors/binja/find_binja_api.py b/capa/features/extractors/binja/find_binja_api.py index 47dbbbe9b3..7099ee9d6a 100644 --- a/capa/features/extractors/binja/find_binja_api.py +++ b/capa/features/extractors/binja/find_binja_api.py @@ -131,7 +131,11 @@ def find_binaryninja_path_via_lastrun() -> Optional[Path]: for lastrun in candidates: try: - return Path(lastrun.read_text(encoding="utf-8").strip()) + path_str = lastrun.read_text(encoding="utf-8").strip() + if path_str: + path = Path(path_str) + if path.is_absolute() and not path_str.startswith("\\"): + return path except OSError: continue From e56e1ed71de15dee82c91bb0406d144551dfc832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Mu=C3=B1oz?= Date: Tue, 3 Mar 2026 02:03:49 +0100 Subject: [PATCH 3/3] binja: address review feedback --- .../extractors/binja/find_binja_api.py | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/capa/features/extractors/binja/find_binja_api.py b/capa/features/extractors/binja/find_binja_api.py index 7099ee9d6a..4dfa77bb92 100644 --- a/capa/features/extractors/binja/find_binja_api.py +++ b/capa/features/extractors/binja/find_binja_api.py @@ -93,7 +93,7 @@ def _is_python_dir(p: Path) -> bool: return (p / "binaryninja" / "__init__.py").is_file() -def _to_python_dir(p: Path) -> Optional[Path]: +def _to_python_dir(p: Optional[Path]) -> Optional[Path]: """ Accept either: - root dir containing python/ (Linux desktop entry, macOS Resources) @@ -113,7 +113,11 @@ def validate_binaryninja_path(binaryninja_path: Path) -> bool: def find_binaryninja_path_via_env() -> Optional[Path]: env_install = os.environ.get("BN_INSTALL_DIR") - return Path(env_install) if env_install else None + if env_install: + path = Path(env_install) + if path.is_absolute() and not str(env_install).startswith("\\\\"): + return path + return None def find_binaryninja_path_via_lastrun() -> Optional[Path]: @@ -122,20 +126,19 @@ def find_binaryninja_path_via_lastrun() -> Optional[Path]: if sys.platform == "win32": appdata = os.environ.get("APPDATA") or str(Path.home() / "AppData" / "Roaming") candidates.append(Path(appdata) / "Binary Ninja" / "lastrun") - - if sys.platform == "darwin": + elif sys.platform == "darwin": candidates.append(Path.home() / "Library" / "Application Support" / "Binary Ninja" / "lastrun") - - # linux/other - candidates.append(Path.home() / ".binaryninja" / "lastrun") + else: + # linux/other + candidates.append(Path.home() / ".binaryninja" / "lastrun") for lastrun in candidates: try: path_str = lastrun.read_text(encoding="utf-8").strip() - if path_str: - path = Path(path_str) - if path.is_absolute() and not path_str.startswith("\\"): - return path + if path_str: + path = Path(path_str) + if path.is_absolute() and not path_str.startswith("\\"): + return path except OSError: continue @@ -149,11 +152,11 @@ def find_binaryninja() -> Optional[Path]: logger.debug("detected OS: linux") elif sys.platform == "darwin": binaryninja_path = Path("/Applications/Binary Ninja.app/Contents/Resources") - if not validate_binaryninja_path(binaryninja_path): + python_dir = _to_python_dir(binaryninja_path) + if not python_dir: logger.debug("failed to find Binary Ninja at default macOS path") return None - return _to_python_dir(binaryninja_path) - + return python_dir elif sys.platform == "win32": for candidate in ( find_binaryninja_path_via_env(), @@ -185,8 +188,6 @@ def find_binaryninja() -> Optional[Path]: logger.debug("failed to validate Binary Ninja installation") return None - logger.debug("found Binary Ninja installation: %s", binaryninja_path) - return _to_python_dir(binaryninja_path)