mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-12 12:06:29 +03:00
fix: relocate binary analysis skill
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
"""Backend adapters — abstract interface, FakeAdapter for testing, Ghidra adapter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from binary_analysis.adapters.base import (
|
||||
AnalysisProfile,
|
||||
AnalysisResult,
|
||||
BackendAdapter,
|
||||
BinaryMetadata,
|
||||
CallEdge,
|
||||
ConcurrencyMode,
|
||||
DecompilationResult,
|
||||
)
|
||||
from binary_analysis.adapters.fake import FakeAdapter
|
||||
from binary_analysis.adapters.ghidra import GhidraAdapter
|
||||
|
||||
__all__ = [
|
||||
"AnalysisProfile",
|
||||
"AnalysisResult",
|
||||
"BackendAdapter",
|
||||
"BinaryMetadata",
|
||||
"CallEdge",
|
||||
"ConcurrencyMode",
|
||||
"DecompilationResult",
|
||||
"FakeAdapter",
|
||||
"GhidraAdapter",
|
||||
]
|
||||
@@ -0,0 +1,671 @@
|
||||
"""Abstract BackendAdapter interface.
|
||||
|
||||
Defines the typed behavioral contract that every backend must implement.
|
||||
Public commands never branch on backend names; they interact exclusively
|
||||
through this interface.
|
||||
|
||||
The interface is backend-neutral: all inputs and outputs use canonical
|
||||
domain entities. Backend-native objects never cross this boundary.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from binary_analysis.domain.entities import (
|
||||
Address,
|
||||
Binary,
|
||||
CallGraph,
|
||||
EntryPoint,
|
||||
Export,
|
||||
Function,
|
||||
Import,
|
||||
Instruction,
|
||||
Project,
|
||||
Reference,
|
||||
Section,
|
||||
String,
|
||||
Symbol,
|
||||
TriageResult,
|
||||
)
|
||||
|
||||
|
||||
class ConcurrencyMode(str, Enum):
|
||||
"""Declares how a backend handles concurrent access."""
|
||||
|
||||
PROJECT_SERIALIZED = "PROJECT_SERIALIZED"
|
||||
"""Only one operation per project at a time."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnalysisProfile:
|
||||
"""An analysis profile specification.
|
||||
|
||||
Attributes:
|
||||
name: Profile identifier (e.g., "standard", "quick", "deep").
|
||||
description: Human-readable description.
|
||||
analysers: List of analyser names included in this profile.
|
||||
"""
|
||||
|
||||
name: str
|
||||
description: str = ""
|
||||
analysers: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnalysisResult:
|
||||
"""Result of an analysis operation.
|
||||
|
||||
Attributes:
|
||||
success: Whether the analysis completed without critical errors.
|
||||
partial: Whether some analysers failed while others succeeded.
|
||||
completed_analysers: List of analyser names that completed.
|
||||
failed_analysers: List of analyser names that failed.
|
||||
diagnostics: List of diagnostic entries describing failures.
|
||||
"""
|
||||
|
||||
success: bool = True
|
||||
partial: bool = False
|
||||
completed_analysers: list[str] = field(default_factory=list)
|
||||
failed_analysers: list[str] = field(default_factory=list)
|
||||
diagnostics: list[dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DecompilationResult:
|
||||
"""Result of decompiling a function.
|
||||
|
||||
Attributes:
|
||||
pseudocode: The reconstructed pseudocode (never original source).
|
||||
address_map: Maps source line numbers (1-indexed) to canonical address objects.
|
||||
diagnostics: List of diagnostic entries.
|
||||
language: The source language of the decompilation output (e.g., "c").
|
||||
"""
|
||||
|
||||
pseudocode: str = ""
|
||||
address_map: dict[int, dict[str, Any]] = field(default_factory=dict)
|
||||
diagnostics: list[dict[str, Any]] = field(default_factory=list)
|
||||
language: str = "c"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CallEdge:
|
||||
"""A directed call edge between two functions.
|
||||
|
||||
Attributes:
|
||||
from_address: The caller function's entry address.
|
||||
to_address: The callee function's entry address.
|
||||
from_name: The caller function's name.
|
||||
to_name: The callee function's name.
|
||||
kind: The kind of call (direct, indirect, etc.).
|
||||
"""
|
||||
|
||||
from_address: Address | None = None
|
||||
to_address: Address | None = None
|
||||
from_name: str = ""
|
||||
to_name: str = ""
|
||||
kind: str = "direct"
|
||||
|
||||
|
||||
@dataclass
|
||||
class BinaryMetadata:
|
||||
"""Canonical metadata about a binary, backend-neutral.
|
||||
|
||||
This is a lightweight subset of the Binary entity focused on
|
||||
metadata that does not require full analysis.
|
||||
"""
|
||||
|
||||
format: str = ""
|
||||
architecture: str | None = None
|
||||
endianness: str | None = None
|
||||
size_bytes: int = 0
|
||||
entry_point: Address | None = None
|
||||
compiler: str | None = None
|
||||
source_language: str | None = None
|
||||
|
||||
|
||||
class BackendAdapter(ABC):
|
||||
"""Abstract interface for all backend adapters.
|
||||
|
||||
Every backend implementation must subclass this and implement
|
||||
all abstract methods. The adapter translates backend-specific
|
||||
data into canonical domain entities.
|
||||
|
||||
Concurrency is declared via the ``concurrency`` property.
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def concurrency(self) -> ConcurrencyMode:
|
||||
"""Declare how this backend handles concurrent access."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def initialize(self) -> None:
|
||||
"""Initialize the backend (start JVM, load libraries, etc.).
|
||||
|
||||
Must be safe to call multiple times (idempotent).
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def capabilities(self) -> dict[str, Any]:
|
||||
"""Return the backend's capabilities.
|
||||
|
||||
Returns:
|
||||
A dict describing supported formats, architectures, analyzers,
|
||||
and limitations.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def available_profiles(self) -> list[AnalysisProfile]:
|
||||
"""Return the list of available analysis profiles."""
|
||||
...
|
||||
|
||||
def validate_profile(self, profile_name: str) -> AnalysisProfile:
|
||||
"""Validate that a profile name is known.
|
||||
|
||||
Args:
|
||||
profile_name: The profile to validate.
|
||||
|
||||
Returns:
|
||||
The matching AnalysisProfile.
|
||||
|
||||
Raises:
|
||||
ValueError: If the profile is not available.
|
||||
"""
|
||||
profiles = self.available_profiles()
|
||||
for profile in profiles:
|
||||
if profile.name == profile_name:
|
||||
return profile
|
||||
available = [p.name for p in profiles]
|
||||
raise ValueError(
|
||||
f"Unknown analysis profile: {profile_name!r}. Available: {', '.join(available)}"
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def import_binary(self, path: str, project: Project) -> Binary:
|
||||
"""Import a binary into the backend.
|
||||
|
||||
Args:
|
||||
path: Path to the binary file on disk.
|
||||
project: The project this binary belongs to.
|
||||
|
||||
Returns:
|
||||
A canonical Binary entity with format, architecture, and
|
||||
SHA-256 populated.
|
||||
|
||||
Raises:
|
||||
Various backend-specific errors that are normalized to
|
||||
canonical error types by the caller.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def analyze(self, binary: Binary, profile: AnalysisProfile) -> AnalysisResult:
|
||||
"""Run analysis on an imported binary.
|
||||
|
||||
Args:
|
||||
binary: The canonical Binary entity to analyze.
|
||||
profile: The analysis profile to apply.
|
||||
|
||||
Returns:
|
||||
An AnalysisResult with completed/failed analysers and diagnostics.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_metadata(self, binary: Binary) -> BinaryMetadata:
|
||||
"""Return canonical metadata for a binary.
|
||||
|
||||
Does not require full analysis. Should return whatever info is
|
||||
available from the import step (format, architecture, etc.).
|
||||
|
||||
Args:
|
||||
binary: The binary to query.
|
||||
|
||||
Returns:
|
||||
Backend-neutral metadata.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_sections(self, binary: Binary) -> list[Section]:
|
||||
"""Return all sections in the binary.
|
||||
|
||||
Args:
|
||||
binary: The binary to query.
|
||||
|
||||
Returns:
|
||||
List of canonical Section entities.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_entrypoints(self, binary: Binary) -> list[EntryPoint]:
|
||||
"""Return all entry points in the binary.
|
||||
|
||||
Args:
|
||||
binary: The binary to query.
|
||||
|
||||
Returns:
|
||||
List of canonical EntryPoint entities.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_imports(self, binary: Binary) -> list[Import]:
|
||||
"""Return all imported symbols in the binary.
|
||||
|
||||
Args:
|
||||
binary: The binary to query.
|
||||
|
||||
Returns:
|
||||
List of canonical Import entities.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_exports(self, binary: Binary) -> list[Export]:
|
||||
"""Return all exported symbols in the binary.
|
||||
|
||||
Args:
|
||||
binary: The binary to query.
|
||||
|
||||
Returns:
|
||||
List of canonical Export entities.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_symbols(self, binary: Binary) -> list[Symbol]:
|
||||
"""Return all symbols in the binary.
|
||||
|
||||
Args:
|
||||
binary: The binary to query.
|
||||
|
||||
Returns:
|
||||
List of canonical Symbol entities.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_strings(
|
||||
self,
|
||||
binary: Binary,
|
||||
min_length: int = 4,
|
||||
contains: str | None = None,
|
||||
encoding_filter: str | None = None,
|
||||
) -> list[String]:
|
||||
"""Return all decoded strings in the binary.
|
||||
|
||||
Args:
|
||||
binary: The binary to query.
|
||||
min_length: Minimum string length to return (default 4).
|
||||
contains: Optional substring filter (case-sensitive).
|
||||
encoding_filter: Optional encoding filter (e.g., "ASCII", "UTF-16").
|
||||
|
||||
Returns:
|
||||
List of canonical String entities.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_functions(
|
||||
self,
|
||||
binary: Binary,
|
||||
exclude_external: bool = True,
|
||||
exclude_thunks: bool = True,
|
||||
) -> list[Function]:
|
||||
"""Return all functions in the binary.
|
||||
|
||||
Args:
|
||||
binary: The binary to query.
|
||||
exclude_external: If True, exclude externally defined functions.
|
||||
exclude_thunks: If True, exclude thunk functions.
|
||||
|
||||
Returns:
|
||||
List of canonical Function entities.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def decompile(self, binary: Binary, function: Function) -> DecompilationResult:
|
||||
"""Decompile a function to pseudocode.
|
||||
|
||||
Args:
|
||||
binary: The binary containing the function.
|
||||
function: The function to decompile.
|
||||
|
||||
Returns:
|
||||
Reconstructed pseudocode with address map and diagnostics.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def disassemble(
|
||||
self, binary: Binary, start_address: Address, end_address: Address
|
||||
) -> list[Instruction]:
|
||||
"""Disassemble instructions in an address range.
|
||||
|
||||
Args:
|
||||
binary: The binary to disassemble from.
|
||||
start_address: Start of the address range (inclusive).
|
||||
end_address: End of the address range (inclusive).
|
||||
|
||||
Returns:
|
||||
List of canonical Instruction entities.
|
||||
|
||||
Raises:
|
||||
ValueError: If the address range is entirely unmapped.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def read_bytes(self, binary: Binary, address: Address, length: int) -> tuple[bytes, int]:
|
||||
"""Read raw bytes from a binary at a given address.
|
||||
|
||||
Args:
|
||||
binary: The binary to read from.
|
||||
address: The starting address.
|
||||
length: The number of bytes to read.
|
||||
|
||||
Returns:
|
||||
A tuple of (bytes_read, actual_length). actual_length may be
|
||||
less than length if the read crosses a segment boundary.
|
||||
|
||||
Raises:
|
||||
ValueError: If the address is not mapped.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_xrefs(self, binary: Binary, address: Address) -> list[Reference]:
|
||||
"""Return cross-references to/from an address.
|
||||
|
||||
Args:
|
||||
binary: The binary to query.
|
||||
address: The address to find references for.
|
||||
|
||||
Returns:
|
||||
List of canonical Reference entities.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_callers(self, binary: Binary, function: Function) -> list[CallEdge]:
|
||||
"""Return functions that call the given function.
|
||||
|
||||
Args:
|
||||
binary: The binary to query.
|
||||
function: The target function.
|
||||
|
||||
Returns:
|
||||
List of CallEdge entities from callers to the target.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_callees(self, binary: Binary, function: Function) -> list[CallEdge]:
|
||||
"""Return functions called by the given function.
|
||||
|
||||
Args:
|
||||
binary: The binary to query.
|
||||
function: The target function.
|
||||
|
||||
Returns:
|
||||
List of CallEdge entities from the target to callees.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_callgraph(self, binary: Binary, function: Function, max_depth: int = 3) -> CallGraph:
|
||||
"""Build a call graph rooted at a function.
|
||||
|
||||
Args:
|
||||
binary: The binary to query.
|
||||
function: The root function.
|
||||
max_depth: Maximum depth to traverse (default 3, max 10).
|
||||
|
||||
Returns:
|
||||
A bounded CallGraph entity.
|
||||
"""
|
||||
...
|
||||
|
||||
def register_binary(self, binary: Binary, fixture_name: str) -> None: # noqa: B027
|
||||
"""Register a binary with a fixture name for fixture-based lookup.
|
||||
|
||||
This is a hook for fixture-based adapters (like FakeAdapter) that
|
||||
need to map Binary entities to pre-defined test fixture data. Real
|
||||
adapters (like GhidraAdapter) that use actual backend analysis
|
||||
should leave this as a no-op.
|
||||
|
||||
Args:
|
||||
binary: The canonical Binary entity to register.
|
||||
fixture_name: The name of the fixture dataset to associate.
|
||||
"""
|
||||
pass # Default no-op for real adapters
|
||||
|
||||
def run_triage(self, binary: Binary, profile: AnalysisProfile | None = None) -> TriageResult:
|
||||
"""Run the triage analysis pipeline on a binary.
|
||||
|
||||
Collects observations, evaluates heuristics, and identifies unknowns.
|
||||
Returns a TriageResult with structured findings. The default
|
||||
implementation uses the TriageEngine from the rules module.
|
||||
|
||||
Args:
|
||||
binary: The binary to triage.
|
||||
profile: Optional analysis profile for context.
|
||||
|
||||
Returns:
|
||||
A TriageResult with observations, heuristics, and unknowns.
|
||||
"""
|
||||
from binary_analysis.rules.engine import TriageEngine
|
||||
|
||||
engine = TriageEngine(self, binary)
|
||||
obs, heur, unk, diags = engine.run()
|
||||
partial = len(diags) > 0
|
||||
return TriageResult(
|
||||
observations=obs,
|
||||
heuristics=heur,
|
||||
unknowns=unk,
|
||||
engine_diagnostics=diags,
|
||||
partial=partial,
|
||||
)
|
||||
|
||||
def search(
|
||||
self,
|
||||
binary: Binary,
|
||||
query: str,
|
||||
search_type: str = "function",
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Search for entities matching a query string.
|
||||
|
||||
Searches across functions, strings, symbols, imports, and exports
|
||||
depending on the search type. Returns a list of result dicts with
|
||||
entity type, name, address, and relevance.
|
||||
|
||||
Args:
|
||||
binary: The binary to search within.
|
||||
query: The search query string.
|
||||
search_type: Type of entity to search ("function", "string", "symbol",
|
||||
"import", "export", "all"; default "function").
|
||||
|
||||
Returns:
|
||||
List of result dicts with keys: entity_type, name, address, and
|
||||
optional match_detail.
|
||||
|
||||
This is a concrete method with a default implementation that searches
|
||||
the basic fixtures. Backends may override for more sophisticated search.
|
||||
"""
|
||||
results: list[dict[str, Any]] = []
|
||||
query_lower = query.lower()
|
||||
|
||||
def _match(name: str) -> bool:
|
||||
"""Case-insensitive substring match."""
|
||||
return query_lower in name.lower()
|
||||
|
||||
if search_type in ("function", "all"):
|
||||
for fn in self.get_functions(binary, exclude_external=False, exclude_thunks=False):
|
||||
if _match(fn.name):
|
||||
results.append(
|
||||
{
|
||||
"entity_type": "function",
|
||||
"name": fn.name,
|
||||
"address": fn.address.to_dict() if fn.address else None,
|
||||
"match_detail": f"Function name matches '{query}'",
|
||||
"size_bytes": fn.size_bytes,
|
||||
}
|
||||
)
|
||||
|
||||
if search_type in ("string", "all"):
|
||||
for s in self.get_strings(binary):
|
||||
if _match(s.text):
|
||||
results.append(
|
||||
{
|
||||
"entity_type": "string",
|
||||
"name": s.text,
|
||||
"address": s.address.to_dict() if s.address else None,
|
||||
"match_detail": f"String contains '{query}'",
|
||||
"encoding": s.encoding,
|
||||
"length": s.length,
|
||||
}
|
||||
)
|
||||
|
||||
if search_type in ("symbol", "all"):
|
||||
for sym in self.get_symbols(binary):
|
||||
if _match(sym.name):
|
||||
results.append(
|
||||
{
|
||||
"entity_type": "symbol",
|
||||
"name": sym.name,
|
||||
"address": sym.address.to_dict() if sym.address else None,
|
||||
"match_detail": f"Symbol name matches '{query}'",
|
||||
"scope": sym.scope,
|
||||
}
|
||||
)
|
||||
|
||||
if search_type in ("import", "all"):
|
||||
for imp in self.get_imports(binary):
|
||||
if _match(imp.symbol) or _match(imp.module):
|
||||
results.append(
|
||||
{
|
||||
"entity_type": "import",
|
||||
"name": imp.symbol,
|
||||
"address": imp.address.to_dict() if imp.address else None,
|
||||
"match_detail": f"Import matches '{query}' in module '{imp.module}'",
|
||||
"module": imp.module,
|
||||
}
|
||||
)
|
||||
|
||||
if search_type in ("export", "all"):
|
||||
for exp in self.get_exports(binary):
|
||||
if _match(exp.name):
|
||||
results.append(
|
||||
{
|
||||
"entity_type": "export",
|
||||
"name": exp.name,
|
||||
"address": exp.address.to_dict() if exp.address else None,
|
||||
"match_detail": f"Export name matches '{query}'",
|
||||
"kind": exp.kind,
|
||||
}
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
def trace(
|
||||
self,
|
||||
binary: Binary,
|
||||
from_address: Address,
|
||||
to_address: Address,
|
||||
max_paths: int = 10,
|
||||
max_depth: int = 10,
|
||||
) -> tuple[list[list[dict[str, Any]]], bool]:
|
||||
"""Find bounded paths between two entities.
|
||||
|
||||
Traces call paths from a source address to a target address within
|
||||
the disclosed path count and depth limits.
|
||||
|
||||
Args:
|
||||
binary: The binary to trace within.
|
||||
from_address: The source entity address.
|
||||
to_address: The destination entity address.
|
||||
max_paths: Maximum number of paths to return (default 10).
|
||||
max_depth: Maximum path depth to explore (default 10).
|
||||
|
||||
Returns:
|
||||
A tuple of (paths, truncated) where paths is a list of paths,
|
||||
each path is a list of entity dicts with name, address, and
|
||||
depth, and truncated is True if paths were truncated at limits.
|
||||
|
||||
This is a concrete method with a default implementation that traces
|
||||
through the call graph. Backends may override for more sophisticated
|
||||
path finding.
|
||||
"""
|
||||
# Get all functions
|
||||
functions = self.get_functions(binary, exclude_external=False, exclude_thunks=False)
|
||||
|
||||
# Build an adjacency map: function address -> list of callee addresses
|
||||
adj: dict[str, list[str]] = {}
|
||||
addr_to_name: dict[str, str] = {}
|
||||
|
||||
for fn in functions:
|
||||
if fn.address is None:
|
||||
continue
|
||||
offset = fn.address.offset
|
||||
addr_to_name[offset] = fn.name
|
||||
callees = self.get_callees(binary, fn)
|
||||
targets = []
|
||||
for edge in callees:
|
||||
if edge.to_address is not None:
|
||||
targets.append(edge.to_address.offset)
|
||||
adj[offset] = targets
|
||||
|
||||
from_offset = from_address.offset
|
||||
to_offset = to_address.offset
|
||||
|
||||
paths: list[list[dict[str, Any]]] = []
|
||||
truncated = False
|
||||
|
||||
# BFS/DFS with depth limiting
|
||||
def _dfs(
|
||||
current: str, target: str, visited: set[str], current_path: list[str], depth: int
|
||||
) -> None:
|
||||
nonlocal truncated
|
||||
if len(paths) >= max_paths:
|
||||
truncated = True
|
||||
return
|
||||
if depth > max_depth:
|
||||
truncated = True
|
||||
return
|
||||
if current == target:
|
||||
# Build the path
|
||||
path_entities: list[dict[str, Any]] = []
|
||||
for d, addr in enumerate([*current_path, current]):
|
||||
path_entities.append(
|
||||
{
|
||||
"name": addr_to_name.get(addr, addr),
|
||||
"address": {
|
||||
"space": "ram",
|
||||
"offset": addr,
|
||||
"display": addr,
|
||||
},
|
||||
"depth": d,
|
||||
}
|
||||
)
|
||||
paths.append(path_entities)
|
||||
return
|
||||
if current in visited:
|
||||
return
|
||||
visited.add(current)
|
||||
for neighbor in adj.get(current, []):
|
||||
if neighbor not in visited:
|
||||
_dfs(neighbor, target, visited.copy(), [*current_path, current], depth + 1)
|
||||
|
||||
_dfs(from_offset, to_offset, set(), [], 1)
|
||||
|
||||
return paths, truncated
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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")
|
||||
Reference in New Issue
Block a user