fix: relocate binary analysis skill

This commit is contained in:
username
2026-07-31 13:25:08 -04:00
parent 0d4a3652e4
commit f66ed48b8c
107 changed files with 17 additions and 2 deletions
@@ -0,0 +1,16 @@
"""Ghidra backend adapter — PyGhidra bridge.
Provides the GhidraAdapter that bridges the canonical domain model to
PyGhidra/Ghidra. The adapter module contains the GhidraAdapter class and
the bridge module handles JVM startup and Ghidra API translation.
Exports:
GhidraAdapter: Backend adapter implementing the BackendAdapter interface
with PROJECT_SERIALIZED concurrency and capability detection.
"""
from __future__ import annotations
from binary_analysis.adapters.ghidra.adapter import GhidraAdapter
__all__ = ["GhidraAdapter"]
@@ -0,0 +1,409 @@
"""GhidraAdapter — bridges the canonical domain model to PyGhidra/Ghidra.
Implements the BackendAdapter interface using PyGhidra for JVM interaction
and Ghidra API calls. This is a skeleton implementation at this stage;
full analysis methods are deferred to subsequent features.
Key characteristics:
- PROJECT_SERIALIZED concurrency: only one operation per project at a time
- Error normalization: Ghidra/Java exceptions mapped to canonical error types
- Capability detection: reports available formats, analyzers, and limitations
- Idempotent initialization: safe to call initialize() multiple times
"""
from __future__ import annotations
import logging
from typing import Any, ClassVar
from binary_analysis.adapters.base import (
AnalysisProfile,
AnalysisResult,
BackendAdapter,
BinaryMetadata,
CallEdge,
ConcurrencyMode,
DecompilationResult,
)
from binary_analysis.adapters.ghidra.bridge import (
ensure_initialized,
get_ghidra_version,
is_pyghidra_available,
)
from binary_analysis.domain.entities import (
Address,
Binary,
CallGraph,
EntryPoint,
Export,
Function,
Import,
Instruction,
Project,
Reference,
Section,
String,
Symbol,
)
logger = logging.getLogger("binary_analysis.adapters.ghidra.adapter")
class GhidraAdapter(BackendAdapter):
"""Ghidra backend adapter via PyGhidra.
Concurrency: PROJECT_SERIALIZED.
Skeleton implementation — structural queries, decompile, disassemble,
and analysis methods raise NotImplementedError until fully implemented
in subsequent features. initialize(), capabilities(), and
available_profiles() are functional with capability detection.
"""
# ------------------------------------------------------------------
# Built-in analysis profiles
# ------------------------------------------------------------------
DEFAULT_PROFILES: ClassVar[list[AnalysisProfile]] = [
AnalysisProfile(
name="standard",
description=(
"Standard analysis: auto-analysis with function discovery, "
"reference analysis, decompiler parameter ID, and data type propagation"
),
analysers=[
"function_start",
"function_id",
"references",
"data_type_propagation",
"decompiler_parameter_id",
"stack_analysis",
],
),
AnalysisProfile(
name="quick",
description=("Quick analysis: function discovery and basic reference analysis only"),
analysers=[
"function_start",
"function_id",
"references",
],
),
AnalysisProfile(
name="deep",
description=(
"Deep analysis: full auto-analysis plus decompiler, callgraph, "
"and cross-reference analysis"
),
analysers=[
"function_start",
"function_id",
"references",
"data_type_propagation",
"decompiler_parameter_id",
"stack_analysis",
"decompiler",
"callgraph",
"xrefs",
"string_analysis",
"constant_propagation",
],
),
]
# ------------------------------------------------------------------
# Supported formats (reported by Ghidra)
# ------------------------------------------------------------------
_SUPPORTED_FORMATS: tuple[str, ...] = (
"PE",
"ELF",
"Mach-O",
"COFF",
"NES",
"RAW",
"MIPS",
"Intel Hex",
"Motorola SREC",
"DOS MZ",
)
_SUPPORTED_ARCHITECTURES: tuple[str, ...] = (
"x86",
"x86-64",
"ARM",
"ARM-64",
"MIPS",
"MIPS-64",
"PowerPC",
"PowerPC-64",
"SPARC",
"6502",
"Z80",
"Java Bytecode",
"Dalvik",
)
# ------------------------------------------------------------------
# Properties
# ------------------------------------------------------------------
@property
def concurrency(self) -> ConcurrencyMode:
"""Ghidra requires project-level serialization.
Only one operation per Ghidra project at a time. This is because
Ghidra's ProgramDB is not thread-safe and Ghidra projects lock
at the program level.
"""
return ConcurrencyMode.PROJECT_SERIALIZED
# ------------------------------------------------------------------
# Lifecycle
# ------------------------------------------------------------------
def initialize(self) -> None:
"""Initialize the Ghidra backend.
Starts the JVM and initializes Ghidra in headless mode.
Safe to call multiple times (idempotent).
Raises:
RuntimeError: If PyGhidra is not available or JVM startup fails.
"""
if not is_pyghidra_available():
raise RuntimeError(
"PyGhidra is not available. Run 'binary doctor' to diagnose "
"or 'binary bootstrap --apply' to install dependencies."
)
ensure_initialized()
logger.info("GhidraAdapter initialized")
# ------------------------------------------------------------------
# Capabilities
# ------------------------------------------------------------------
def capabilities(self) -> dict[str, Any]:
"""Return the Ghidra backend's capabilities.
Reports:
- Supported binary formats
- Supported architectures
- Available analyzers (by profile)
- Backend version
- Concurrency model
- PyGhidra status
- JVM status
Returns:
A dict describing capabilities, formats, and limitations.
"""
version = get_ghidra_version()
jvm_ready = ensure_initialized()
return {
"backend": "Ghidra",
"backend_version": version or "unknown",
"adapter": "GhidraAdapter",
"adapter_version": "0.1.0",
"concurrency": self.concurrency.value,
"pyghidra_available": is_pyghidra_available(),
"jvm_initialized": jvm_ready,
"formats": list(self._SUPPORTED_FORMATS),
"architectures": list(self._SUPPORTED_ARCHITECTURES),
"profiles": [
{
"name": p.name,
"description": p.description,
"analyser_count": len(p.analysers),
}
for p in self.DEFAULT_PROFILES
],
"limitations": [
"Skeleton implementation — structural queries and analysis "
"methods deferred to subsequent features",
"Single-project concurrency (PROJECT_SERIALIZED)",
"Headless mode only — no GUI interaction",
],
}
def available_profiles(self) -> list[AnalysisProfile]:
"""Return the list of available analysis profiles.
Returns:
List of built-in Ghidra analysis profiles.
"""
return list(self.DEFAULT_PROFILES)
# ------------------------------------------------------------------
# Import
# ------------------------------------------------------------------
def import_binary(self, path: str, project: Project) -> Binary:
"""Import a binary into Ghidra. SKELETON — deferred.
Raises:
NotImplementedError: Full implementation deferred.
"""
raise NotImplementedError("Ghidra binary import is deferred to subsequent features")
# ------------------------------------------------------------------
# Analysis
# ------------------------------------------------------------------
def analyze(self, binary: Binary, profile: AnalysisProfile) -> AnalysisResult:
"""Run analysis on an imported binary. SKELETON — deferred.
Raises:
NotImplementedError: Full implementation deferred.
"""
raise NotImplementedError("Ghidra analysis is deferred to subsequent features")
# ------------------------------------------------------------------
# Metadata
# ------------------------------------------------------------------
def get_metadata(self, binary: Binary) -> BinaryMetadata:
"""Return canonical metadata. SKELETON — deferred.
Raises:
NotImplementedError: Full implementation deferred.
"""
raise NotImplementedError("Ghidra metadata query is deferred to subsequent features")
# ------------------------------------------------------------------
# Structural queries
# ------------------------------------------------------------------
def get_sections(self, binary: Binary) -> list[Section]:
"""Return all sections in the binary. SKELETON — deferred.
Raises:
NotImplementedError: Full implementation deferred.
"""
raise NotImplementedError("Ghidra section query is deferred to subsequent features")
def get_entrypoints(self, binary: Binary) -> list[EntryPoint]:
"""Return all entry points. SKELETON — deferred.
Raises:
NotImplementedError: Full implementation deferred.
"""
raise NotImplementedError("Ghidra entrypoints query is deferred to subsequent features")
def get_imports(self, binary: Binary) -> list[Import]:
"""Return all imported symbols. SKELETON — deferred.
Raises:
NotImplementedError: Full implementation deferred.
"""
raise NotImplementedError("Ghidra imports query is deferred to subsequent features")
def get_exports(self, binary: Binary) -> list[Export]:
"""Return all exported symbols. SKELETON — deferred.
Raises:
NotImplementedError: Full implementation deferred.
"""
raise NotImplementedError("Ghidra exports query is deferred to subsequent features")
def get_symbols(self, binary: Binary) -> list[Symbol]:
"""Return all symbols. SKELETON — deferred.
Raises:
NotImplementedError: Full implementation deferred.
"""
raise NotImplementedError("Ghidra symbols query is deferred to subsequent features")
def get_strings(
self,
binary: Binary,
min_length: int = 4,
contains: str | None = None,
encoding_filter: str | None = None,
) -> list[String]:
"""Return decoded strings. SKELETON — deferred.
Raises:
NotImplementedError: Full implementation deferred.
"""
raise NotImplementedError("Ghidra strings query is deferred to subsequent features")
def get_functions(
self,
binary: Binary,
exclude_external: bool = True,
exclude_thunks: bool = True,
) -> list[Function]:
"""Return all functions. SKELETON — deferred.
Raises:
NotImplementedError: Full implementation deferred.
"""
raise NotImplementedError("Ghidra functions query is deferred to subsequent features")
# ------------------------------------------------------------------
# Focused analysis
# ------------------------------------------------------------------
def decompile(self, binary: Binary, function: Function) -> DecompilationResult:
"""Decompile a function. SKELETON — deferred.
Raises:
NotImplementedError: Full implementation deferred.
"""
raise NotImplementedError("Ghidra decompile is deferred to subsequent features")
def disassemble(
self, binary: Binary, start_address: Address, end_address: Address
) -> list[Instruction]:
"""Disassemble instructions in an address range. SKELETON — deferred.
Raises:
NotImplementedError: Full implementation deferred.
"""
raise NotImplementedError("Ghidra disassembly is deferred to subsequent features")
def read_bytes(self, binary: Binary, address: Address, length: int) -> tuple[bytes, int]:
"""Read raw bytes. SKELETON — deferred.
Raises:
NotImplementedError: Full implementation deferred.
"""
raise NotImplementedError("Ghidra byte reading is deferred to subsequent features")
# ------------------------------------------------------------------
# References
# ------------------------------------------------------------------
def get_xrefs(self, binary: Binary, address: Address) -> list[Reference]:
"""Return cross-references. SKELETON — deferred.
Raises:
NotImplementedError: Full implementation deferred.
"""
raise NotImplementedError("Ghidra xrefs query is deferred to subsequent features")
def get_callers(self, binary: Binary, function: Function) -> list[CallEdge]:
"""Return functions that call the given function. SKELETON — deferred.
Raises:
NotImplementedError: Full implementation deferred.
"""
raise NotImplementedError("Ghidra callers query is deferred to subsequent features")
def get_callees(self, binary: Binary, function: Function) -> list[CallEdge]:
"""Return functions called by the given function. SKELETON — deferred.
Raises:
NotImplementedError: Full implementation deferred.
"""
raise NotImplementedError("Ghidra callees query is deferred to subsequent features")
def get_callgraph(self, binary: Binary, function: Function, max_depth: int = 3) -> CallGraph:
"""Build a call graph. SKELETON — deferred.
Raises:
NotImplementedError: Full implementation deferred.
"""
raise NotImplementedError("Ghidra callgraph is deferred to subsequent features")
@@ -0,0 +1,286 @@
"""PyGhidra bridge layer — JVM startup and Ghidra API translation.
Provides safe, idempotent initialization of the Ghidra headless environment
and utilities for translating Ghidra exceptions to canonical error types.
This module is the only place in the codebase that imports PyGhidra.
All other modules interact with Ghidra through the adapter boundary.
"""
from __future__ import annotations
import logging
import os
from typing import Any
from binary_analysis.domain.enums import ExitCode
from binary_analysis.domain.errors import (
AnalysisFailedError,
BackendFailureError,
ImportFailedError,
OperationTimeoutError,
UnsupportedFormatError,
)
logger = logging.getLogger("binary_analysis.adapters.ghidra.bridge")
# ---------------------------------------------------------------------------
# State tracking
# ---------------------------------------------------------------------------
_initialized: bool = False
_pyghidra_available: bool | None = None
_ghidra_version: str | None = None
def is_pyghidra_available() -> bool:
"""Check whether PyGhidra can be imported.
Returns:
True if PyGhidra is importable and JAVA_HOME/GHIDRA_INSTALL_DIR
are configured.
"""
global _pyghidra_available
if _pyghidra_available is not None:
return _pyghidra_available
# Check environment variables
java_home = os.environ.get("JAVA_HOME")
ghidra_install = os.environ.get("GHIDRA_INSTALL_DIR")
if not java_home or not ghidra_install:
logger.debug("PyGhidra not available: JAVA_HOME and/or GHIDRA_INSTALL_DIR not set")
_pyghidra_available = False
return False
try:
import pyghidra # noqa: F401
_pyghidra_available = True
return True
except ImportError:
logger.debug("PyGhidra not available: import failed")
_pyghidra_available = False
return False
def get_ghidra_version() -> str | None:
"""Return the Ghidra version string if available.
The version is read from the Ghidra application.properties file
or set during initialization.
"""
global _ghidra_version
if _ghidra_version is not None:
return _ghidra_version
ghidra_install = os.environ.get("GHIDRA_INSTALL_DIR", "")
props_path = os.path.join(ghidra_install, "Ghidra", "application.properties")
if os.path.isfile(props_path):
try:
with open(props_path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line.startswith("application.version="):
_ghidra_version = line.split("=", 1)[1].strip()
return _ghidra_version
except OSError:
logger.debug("Could not read Ghidra application.properties")
return None
def start_jvm(headless: bool = True) -> None:
"""Start the JVM and initialize Ghidra in headless mode.
This is the safe entry point for PyGhidra initialization. It handles:
- Verifying JAVA_HOME and GHIDRA_INSTALL_DIR
- Starting the JVM with appropriate memory settings
- Initializing Ghidra in headless mode
Args:
headless: If True, initialize Ghidra in headless mode (no GUI).
Raises:
RuntimeError: If PyGhidra is not available or JVM startup fails.
"""
global _initialized
if _initialized:
return
if not is_pyghidra_available():
raise RuntimeError(
"PyGhidra is not available. Ensure JAVA_HOME and GHIDRA_INSTALL_DIR "
"are set, and PyGhidra is installed."
)
try:
import pyghidra
pyghidra.start()
_initialized = True
_ghidra_version = get_ghidra_version()
logger.info("Ghidra JVM started successfully (version: %s)", _ghidra_version)
except Exception as e:
logger.error("Failed to start Ghidra JVM: %s", e)
raise RuntimeError(f"Failed to start Ghidra JVM: {e}") from e
def ensure_initialized() -> bool:
"""Ensure PyGhidra is initialized, starting the JVM if necessary.
Returns:
True if initialization succeeded or was already done,
False if PyGhidra is not available.
"""
global _initialized
if _initialized:
return True
try:
start_jvm(headless=True)
return True
except RuntimeError:
return False
def is_initialized() -> bool:
"""Return whether the Ghidra JVM has been started."""
return _initialized
# ---------------------------------------------------------------------------
# Ghidra error normalization
# ---------------------------------------------------------------------------
# Mapping of Ghidra exception class names to canonical error factories.
# Each entry is (exception_class_name_prefix, error_factory).
_GHIDRA_ERROR_MAP: list[tuple[str, Any]] = []
def _build_error_map() -> list[tuple[str, Any]]:
"""Build the Ghidra error-to-canonical mapping lazily."""
if _GHIDRA_ERROR_MAP:
return _GHIDRA_ERROR_MAP
_GHIDRA_ERROR_MAP.extend(
[
(
"CancelledException",
lambda msg, orig: OperationTimeoutError(f"Operation cancelled: {msg}"),
),
(
"TimeoutException",
lambda msg, orig: OperationTimeoutError(f"Operation timed out: {msg}"),
),
(
"UnsupportedLanguageException",
lambda msg, orig: UnsupportedFormatError(f"Unsupported language or format: {msg}"),
),
(
"DomainFileException",
lambda msg, orig: ImportFailedError(f"Domain file error: {msg}"),
),
(
"PortableExecutableException",
lambda msg, orig: ImportFailedError(f"PE import error: {msg}"),
),
(
"ELFException",
lambda msg, orig: ImportFailedError(f"ELF import error: {msg}"),
),
(
"MachException",
lambda msg, orig: ImportFailedError(f"Mach-O import error: {msg}"),
),
(
"AssertException",
lambda msg, orig: AnalysisFailedError(f"Ghidra assertion failed: {msg}"),
),
(
"IOException",
lambda msg, orig: BackendFailureError(
f"Ghidra I/O error: {msg}", original_error=str(orig)
),
),
(
"RuntimeException",
lambda msg, orig: BackendFailureError(
f"Ghidra runtime error: {msg}", original_error=str(orig)
),
),
]
)
return _GHIDRA_ERROR_MAP
def normalize_error(error: Exception) -> Any:
"""Map a Ghidra or Java exception to a canonical error type.
Uses class name matching against known Ghidra error types. Falls back
to BackendFailureError for unrecognized exceptions.
Args:
error: The exception raised by Ghidra/PyGhidra/JVM.
Returns:
A BinaryAnalysisError subclass instance with the appropriate
exit code and message.
"""
error_map = _build_error_map()
error_name = type(error).__name__
error_msg = str(error)
for prefix, factory in error_map:
if prefix in error_name:
return factory(error_msg, error)
# Fallback: generic backend failure
return BackendFailureError(
f"Unexpected Ghidra error ({error_name}): {error_msg}",
original_error=error_msg,
)
def map_exit_code_to_error(ghidra_exception: Exception) -> ExitCode:
"""Map a Ghidra exception to the appropriate canonical exit code.
Args:
ghidra_exception: The Ghidra/Java exception.
Returns:
The canonical ExitCode for this error class.
"""
error = normalize_error(ghidra_exception)
return ExitCode(error.exit_code)
# ---------------------------------------------------------------------------
# Ghidra API translation utilities (skeleton)
# ---------------------------------------------------------------------------
def translate_program_to_binary(program: Any) -> dict[str, Any]:
"""Translate a Ghidra Program object to a canonical binary dict.
Skeleton only — returns minimal metadata. Full translation deferred
to subsequent features.
Args:
program: A Ghidra Program object.
Returns:
A dict with basic binary identity fields.
"""
raise NotImplementedError("Full Ghidra API translation is deferred to subsequent features")
def translate_function_manager(program: Any) -> list[dict[str, Any]]:
"""Translate Ghidra's FunctionManager data to canonical function dicts.
Skeleton only — deferred to subsequent features.
"""
raise NotImplementedError("Full Ghidra API translation is deferred to subsequent features")