Files
magnus919_agent-skills/binary-analysis/scripts/binary_analysis/adapters/fake.py
T

1662 lines
62 KiB
Python

"""FakeAdapter — a fully controllable in-memory backend adapter for testing.
Implements the BackendAdapter interface with configurable responses:
- Normal data returns for all structural query types
- Import failures (exit code 10), analysis crashes (exit code 11),
and backend failures (exit code 13)
- Slow operations and timeout simulation
- Unmapped addresses, partial mapping, and truncation
- Custom binary fixtures with deterministic address layouts
"""
from __future__ import annotations
import os
import time
from typing import Any, ClassVar
from uuid import uuid4
from binary_analysis.adapters.base import (
AnalysisProfile,
AnalysisResult,
BackendAdapter,
BinaryMetadata,
CallEdge,
ConcurrencyMode,
DecompilationResult,
)
from binary_analysis.domain.entities import (
Address,
Binary,
CallGraph,
EntryPoint,
Export,
Function,
Import,
Instruction,
Project,
Reference,
Section,
String,
Symbol,
)
from binary_analysis.domain.enums import (
Confidence,
Endianness,
FunctionNameSource,
ImportResolution,
ReferenceKind,
)
class FakeAdapter(BackendAdapter):
"""In-memory backend adapter with fully controllable behaviour.
Usage::
adapter = FakeAdapter()
adapter.set_fixture("test-bin", FakeAdapter.pe_fixture())
adapter.configure_import_failure("test-bin", "Simulated import failure")
adapter.configure_slow_operation("analyze", 5.0) # 5-second delay
adapter.configure_unmapped_range(0x5000, 0x6000)
"""
# ------------------------------------------------------------------
# Configuration constants and helpers
# ------------------------------------------------------------------
DEFAULT_PROFILES: ClassVar[list[AnalysisProfile]] = [
AnalysisProfile(
name="standard",
description="Standard analysis: functions, sections, strings, symbols, imports/exports",
analysers=[
"functions",
"sections",
"strings",
"symbols",
"imports",
"exports",
"entrypoints",
],
),
AnalysisProfile(
name="quick",
description="Quick analysis: functions and sections only",
analysers=["functions", "sections"],
),
AnalysisProfile(
name="deep",
description="Deep analysis: full decompilation and callgraph",
analysers=[
"functions",
"sections",
"strings",
"symbols",
"imports",
"exports",
"entrypoints",
"decompiler",
"callgraph",
"xrefs",
],
),
]
@property
def concurrency(self) -> ConcurrencyMode:
return ConcurrencyMode.PROJECT_SERIALIZED
# ------------------------------------------------------------------
# Fixture helpers — pre-built deterministic data sets
# ------------------------------------------------------------------
@staticmethod
def pe_fixture() -> dict[str, Any]:
"""Return a PE fixture with known section/function/import/export layouts.
Represents a minimal x86 PE executable with:
- .text, .rdata, .data sections
- Three functions: main (0x401000), check_password (0x401200), print_message (0x401400)
- Imports from kernel32.dll and msvcrt.dll
- Exports: start entrypoint
"""
return FakeAdapter._build_fixture(
fmt="PE",
arch="x86",
endianness=Endianness.LITTLE,
sections=[
{
"name": ".text",
"address": Address(
space="ram", offset="0x401000", display="0x401000", file_offset=1024
),
"virtual_size": 8192,
"raw_size": 7168,
"flags": ["r", "x"],
"entropy": 5.92,
},
{
"name": ".rdata",
"address": Address(
space="ram", offset="0x403000", display="0x403000", file_offset=8192
),
"virtual_size": 4096,
"raw_size": 2048,
"flags": ["r"],
"entropy": 3.14,
},
{
"name": ".data",
"address": Address(
space="ram", offset="0x404000", display="0x404000", file_offset=12288
),
"virtual_size": 8192,
"raw_size": 512,
"flags": ["r", "w"],
"entropy": 1.87,
},
],
entrypoints=[
{
"address": Address(
space="ram", offset="0x401000", display="0x401000", file_offset=1024
),
"kind": "program",
"confidence": Confidence.HIGH,
"name": "_start",
},
],
imports=[
{
"module": "kernel32.dll",
"symbol": "GetProcAddress",
"address": Address(space="ram", offset="0x403100", display="0x403100"),
"resolution": ImportResolution.RESOLVED,
"ordinal": None,
},
{
"module": "kernel32.dll",
"symbol": "LoadLibraryA",
"address": Address(space="ram", offset="0x403108", display="0x403108"),
"resolution": ImportResolution.RESOLVED,
"ordinal": None,
},
{
"module": "kernel32.dll",
"symbol": "VirtualAlloc",
"address": Address(space="ram", offset="0x403110", display="0x403110"),
"resolution": ImportResolution.RESOLVED,
"ordinal": None,
},
{
"module": "msvcrt.dll",
"symbol": "printf",
"address": Address(space="ram", offset="0x403118", display="0x403118"),
"resolution": ImportResolution.RESOLVED,
"ordinal": None,
},
{
"module": "msvcrt.dll",
"symbol": "scanf",
"address": Address(space="ram", offset="0x403120", display="0x403120"),
"resolution": ImportResolution.RESOLVED,
"ordinal": None,
},
],
exports=[
{
"name": "_start",
"address": Address(space="ram", offset="0x401000", display="0x401000"),
"ordinal": 1,
"forwarder": None,
"kind": "function",
},
],
symbols=[
{
"name": "main",
"address": Address(space="ram", offset="0x401000", display="0x401000"),
"source": FunctionNameSource.ORIGINAL,
"scope": "global",
},
{
"name": "check_password",
"address": Address(space="ram", offset="0x401200", display="0x401200"),
"source": FunctionNameSource.ORIGINAL,
"scope": "global",
},
{
"name": "print_message",
"address": Address(space="ram", offset="0x401400", display="0x401400"),
"source": FunctionNameSource.ORIGINAL,
"scope": "global",
},
{
"name": "printf",
"address": Address(space="ram", offset="0x403118", display="0x403118"),
"source": FunctionNameSource.IMPORTED,
"scope": "global",
},
],
strings=[
{
"text": "Enter password: ",
"encoding": "ASCII",
"address": Address(space="ram", offset="0x403200", display="0x403200"),
"length": 17,
},
{
"text": "Access granted!",
"encoding": "ASCII",
"address": Address(space="ram", offset="0x403220", display="0x403220"),
"length": 15,
},
{
"text": "Access denied!",
"encoding": "ASCII",
"address": Address(space="ram", offset="0x403240", display="0x403240"),
"length": 14,
},
{
"text": "kernel32.dll",
"encoding": "ASCII",
"address": Address(space="ram", offset="0x403260", display="0x403260"),
"length": 13,
},
{
"text": "msvcrt.dll",
"encoding": "ASCII",
"address": Address(space="ram", offset="0x403270", display="0x403270"),
"length": 10,
},
],
functions=[
{
"name": "main",
"address": Address(space="ram", offset="0x401000", display="0x401000"),
"size_bytes": 512,
"confidence": Confidence.HIGH,
"name_source": FunctionNameSource.ORIGINAL,
"is_external": False,
"is_thunk": False,
"signature": "int main(int argc, char **argv)",
"basic_block_count": 12,
"instruction_count": 87,
"cyclomatic_complexity": 5,
},
{
"name": "check_password",
"address": Address(space="ram", offset="0x401200", display="0x401200"),
"size_bytes": 256,
"confidence": Confidence.HIGH,
"name_source": FunctionNameSource.ORIGINAL,
"is_external": False,
"is_thunk": False,
"signature": "int check_password(const char *input)",
"basic_block_count": 5,
"instruction_count": 34,
"cyclomatic_complexity": 3,
},
{
"name": "print_message",
"address": Address(space="ram", offset="0x401400", display="0x401400"),
"size_bytes": 128,
"confidence": Confidence.HIGH,
"name_source": FunctionNameSource.ORIGINAL,
"is_external": False,
"is_thunk": False,
"signature": "void print_message(const char *msg)",
"basic_block_count": 3,
"instruction_count": 18,
"cyclomatic_complexity": 2,
},
{
"name": "printf",
"address": Address(space="ram", offset="0x403118", display="0x403118"),
"size_bytes": 8,
"confidence": Confidence.HIGH,
"name_source": FunctionNameSource.IMPORTED,
"is_external": True,
"is_thunk": False,
"signature": None,
},
],
)
@staticmethod
def elf_fixture() -> dict[str, Any]:
"""Return an ELF fixture with known section/function/import/export layouts.
Represents a minimal x86-64 ELF executable with:
- .text, .rodata, .data, .bss sections
- Four functions: _start (0x401000), main (0x401100),
compute_hash (0x401300), parse_input (0x401500)
- Imports from libc.so.6
- Exports: main, compute_hash
"""
return FakeAdapter._build_fixture(
fmt="ELF",
arch="x86-64",
endianness=Endianness.LITTLE,
sections=[
{
"name": ".text",
"address": Address(
space="ram", offset="0x401000", display="0x401000", file_offset=4096
),
"virtual_size": 16384,
"raw_size": 12288,
"flags": ["r", "x"],
"entropy": 6.12,
},
{
"name": ".rodata",
"address": Address(
space="ram", offset="0x405000", display="0x405000", file_offset=16384
),
"virtual_size": 4096,
"raw_size": 1024,
"flags": ["r"],
"entropy": 2.87,
},
{
"name": ".data",
"address": Address(
space="ram", offset="0x406000", display="0x406000", file_offset=20480
),
"virtual_size": 4096,
"raw_size": 256,
"flags": ["r", "w"],
"entropy": 1.45,
},
{
"name": ".bss",
"address": Address(space="ram", offset="0x407000", display="0x407000"),
"virtual_size": 8192,
"raw_size": 0,
"flags": ["r", "w"],
"entropy": 0.0,
},
],
entrypoints=[
{
"address": Address(
space="ram", offset="0x401000", display="0x401000", file_offset=4096
),
"kind": "program",
"confidence": Confidence.HIGH,
"name": "_start",
},
],
imports=[
{
"module": "libc.so.6",
"symbol": "printf",
"address": Address(space="ram", offset="0x405100", display="0x405100"),
"resolution": ImportResolution.RESOLVED,
"ordinal": None,
},
{
"module": "libc.so.6",
"symbol": "fgets",
"address": Address(space="ram", offset="0x405108", display="0x405108"),
"resolution": ImportResolution.RESOLVED,
"ordinal": None,
},
{
"module": "libc.so.6",
"symbol": "malloc",
"address": Address(space="ram", offset="0x405110", display="0x405110"),
"resolution": ImportResolution.RESOLVED,
"ordinal": None,
},
{
"module": "libc.so.6",
"symbol": "free",
"address": Address(space="ram", offset="0x405118", display="0x405118"),
"resolution": ImportResolution.RESOLVED,
"ordinal": None,
},
{
"module": "libc.so.6",
"symbol": "strcmp",
"address": Address(space="ram", offset="0x405120", display="0x405120"),
"resolution": ImportResolution.RESOLVED,
"ordinal": None,
},
],
exports=[
{
"name": "main",
"address": Address(space="ram", offset="0x401100", display="0x401100"),
"ordinal": None,
"forwarder": None,
"kind": "function",
},
{
"name": "compute_hash",
"address": Address(space="ram", offset="0x401300", display="0x401300"),
"ordinal": None,
"forwarder": None,
"kind": "function",
},
],
symbols=[
{
"name": "_start",
"address": Address(space="ram", offset="0x401000", display="0x401000"),
"source": FunctionNameSource.ORIGINAL,
"scope": "global",
},
{
"name": "main",
"address": Address(space="ram", offset="0x401100", display="0x401100"),
"source": FunctionNameSource.ORIGINAL,
"scope": "global",
},
{
"name": "compute_hash",
"address": Address(space="ram", offset="0x401300", display="0x401300"),
"source": FunctionNameSource.ORIGINAL,
"scope": "global",
},
{
"name": "parse_input",
"address": Address(space="ram", offset="0x401500", display="0x401500"),
"source": FunctionNameSource.ORIGINAL,
"scope": "local",
},
],
strings=[
{
"text": "Enter input: ",
"encoding": "ASCII",
"address": Address(space="ram", offset="0x405200", display="0x405200"),
"length": 14,
},
{
"text": "Hash: 0x",
"encoding": "ASCII",
"address": Address(space="ram", offset="0x405210", display="0x405210"),
"length": 8,
},
{
"text": "Invalid input",
"encoding": "ASCII",
"address": Address(space="ram", offset="0x405220", display="0x405220"),
"length": 13,
},
{
"text": "libc.so.6",
"encoding": "ASCII",
"address": Address(space="ram", offset="0x405230", display="0x405230"),
"length": 9,
},
],
functions=[
{
"name": "_start",
"address": Address(space="ram", offset="0x401000", display="0x401000"),
"size_bytes": 64,
"confidence": Confidence.HIGH,
"name_source": FunctionNameSource.BACKEND_GENERATED,
"is_external": False,
"is_thunk": False,
"signature": "void _start()",
"basic_block_count": 2,
"instruction_count": 6,
"cyclomatic_complexity": 1,
},
{
"name": "main",
"address": Address(space="ram", offset="0x401100", display="0x401100"),
"size_bytes": 384,
"confidence": Confidence.HIGH,
"name_source": FunctionNameSource.ORIGINAL,
"is_external": False,
"is_thunk": False,
"signature": "int main(int argc, char **argv)",
"basic_block_count": 10,
"instruction_count": 72,
"cyclomatic_complexity": 4,
},
{
"name": "compute_hash",
"address": Address(space="ram", offset="0x401300", display="0x401300"),
"size_bytes": 256,
"confidence": Confidence.HIGH,
"name_source": FunctionNameSource.ORIGINAL,
"is_external": False,
"is_thunk": False,
"signature": "uint32_t compute_hash(const char *data)",
"basic_block_count": 6,
"instruction_count": 41,
"cyclomatic_complexity": 3,
},
{
"name": "parse_input",
"address": Address(space="ram", offset="0x401500", display="0x401500"),
"size_bytes": 192,
"confidence": Confidence.HIGH,
"name_source": FunctionNameSource.ORIGINAL,
"is_external": False,
"is_thunk": False,
"signature": "int parse_input(const char *buf, size_t len)",
"basic_block_count": 4,
"instruction_count": 28,
"cyclomatic_complexity": 2,
},
],
)
@staticmethod
def macho_fixture() -> dict[str, Any]:
"""Return a Mach-O fixture with known section/function/import/export layouts.
Represents a minimal arm64 macOS binary with:
- __TEXT (__text, __cstring, __const), __DATA (__data, __bss),
__LINKEDIT sections
- Three functions: _main (0x100003f80), _validate_input (0x100003fc0),
_do_work (0x100004000)
- Imports from libSystem.B.dylib
- Exports: _main
"""
return FakeAdapter._build_fixture(
fmt="Mach-O",
arch="arm64",
endianness=Endianness.LITTLE,
sections=[
{
"name": "__text",
"address": Address(
space="ram", offset="0x100003f80", display="0x100003f80", file_offset=0
),
"virtual_size": 4096,
"raw_size": 2048,
"flags": ["r", "x"],
"entropy": 5.71,
},
{
"name": "__cstring",
"address": Address(
space="ram", offset="0x100004f80", display="0x100004f80", file_offset=4096
),
"virtual_size": 1024,
"raw_size": 512,
"flags": ["r"],
"entropy": 3.02,
},
{
"name": "__const",
"address": Address(
space="ram", offset="0x100005380", display="0x100005380", file_offset=5120
),
"virtual_size": 1024,
"raw_size": 256,
"flags": ["r"],
"entropy": 1.92,
},
{
"name": "__data",
"address": Address(
space="ram", offset="0x100005780", display="0x100005780", file_offset=6144
),
"virtual_size": 1024,
"raw_size": 128,
"flags": ["r", "w"],
"entropy": 1.12,
},
{
"name": "__bss",
"address": Address(space="ram", offset="0x100005b80", display="0x100005b80"),
"virtual_size": 4096,
"raw_size": 0,
"flags": ["r", "w"],
"entropy": 0.0,
},
{
"name": "__linkedit",
"address": Address(
space="ram", offset="0x100006b80", display="0x100006b80", file_offset=7168
),
"virtual_size": 2048,
"raw_size": 1024,
"flags": ["r"],
"entropy": 4.33,
},
],
entrypoints=[
{
"address": Address(
space="ram", offset="0x100003f80", display="0x100003f80", file_offset=0
),
"kind": "program",
"confidence": Confidence.HIGH,
"name": "_main",
},
],
imports=[
{
"module": "libSystem.B.dylib",
"symbol": "_printf",
"address": Address(space="ram", offset="0x100005400", display="0x100005400"),
"resolution": ImportResolution.RESOLVED,
"ordinal": None,
},
{
"module": "libSystem.B.dylib",
"symbol": "_malloc",
"address": Address(space="ram", offset="0x100005408", display="0x100005408"),
"resolution": ImportResolution.RESOLVED,
"ordinal": None,
},
{
"module": "libSystem.B.dylib",
"symbol": "_free",
"address": Address(space="ram", offset="0x100005410", display="0x100005410"),
"resolution": ImportResolution.RESOLVED,
"ordinal": None,
},
{
"module": "libSystem.B.dylib",
"symbol": "_dispatch_async",
"address": Address(space="ram", offset="0x100005418", display="0x100005418"),
"resolution": ImportResolution.RESOLVED,
"ordinal": None,
},
],
exports=[
{
"name": "_main",
"address": Address(space="ram", offset="0x100003f80", display="0x100003f80"),
"ordinal": None,
"forwarder": None,
"kind": "function",
},
{
"name": "_validate_input",
"address": Address(space="ram", offset="0x100003fc0", display="0x100003fc0"),
"ordinal": None,
"forwarder": None,
"kind": "function",
},
],
symbols=[
{
"name": "_main",
"address": Address(space="ram", offset="0x100003f80", display="0x100003f80"),
"source": FunctionNameSource.ORIGINAL,
"scope": "global",
},
{
"name": "_validate_input",
"address": Address(space="ram", offset="0x100003fc0", display="0x100003fc0"),
"source": FunctionNameSource.ORIGINAL,
"scope": "global",
},
{
"name": "_do_work",
"address": Address(space="ram", offset="0x100004000", display="0x100004000"),
"source": FunctionNameSource.ORIGINAL,
"scope": "local",
},
],
strings=[
{
"text": "Hello, World!",
"encoding": "ASCII",
"address": Address(space="ram", offset="0x100004f80", display="0x100004f80"),
"length": 13,
},
{
"text": "Processing...",
"encoding": "ASCII",
"address": Address(space="ram", offset="0x100004f90", display="0x100004f90"),
"length": 14,
},
{
"text": "Done.",
"encoding": "ASCII",
"address": Address(space="ram", offset="0x100004fa0", display="0x100004fa0"),
"length": 5,
},
{
"text": "libSystem.B.dylib",
"encoding": "ASCII",
"address": Address(space="ram", offset="0x100004fb0", display="0x100004fb0"),
"length": 18,
},
],
functions=[
{
"name": "_main",
"address": Address(space="ram", offset="0x100003f80", display="0x100003f80"),
"size_bytes": 64,
"confidence": Confidence.HIGH,
"name_source": FunctionNameSource.ORIGINAL,
"is_external": False,
"is_thunk": False,
"signature": "int main(int argc, char **argv)",
"basic_block_count": 3,
"instruction_count": 12,
"cyclomatic_complexity": 2,
},
{
"name": "_validate_input",
"address": Address(space="ram", offset="0x100003fc0", display="0x100003fc0"),
"size_bytes": 64,
"confidence": Confidence.HIGH,
"name_source": FunctionNameSource.ORIGINAL,
"is_external": False,
"is_thunk": False,
"signature": "bool validate_input(const char *data)",
"basic_block_count": 2,
"instruction_count": 8,
"cyclomatic_complexity": 2,
},
{
"name": "_do_work",
"address": Address(space="ram", offset="0x100004000", display="0x100004000"),
"size_bytes": 128,
"confidence": Confidence.HIGH,
"name_source": FunctionNameSource.ORIGINAL,
"is_external": False,
"is_thunk": False,
"signature": "void do_work(size_t count)",
"basic_block_count": 4,
"instruction_count": 21,
"cyclomatic_complexity": 2,
},
],
)
@staticmethod
def _build_fixture(
fmt: str,
arch: str,
endianness: Endianness,
sections: list[dict[str, Any]],
entrypoints: list[dict[str, Any]],
imports: list[dict[str, Any]],
exports: list[dict[str, Any]],
symbols: list[dict[str, Any]],
strings: list[dict[str, Any]],
functions: list[dict[str, Any]],
) -> dict[str, Any]:
"""Build a fixture data dict from structured inputs.
Returns a dict with keys matching the FakeAdapter's internal fixture storage.
"""
# Build Section entities
section_entities = []
for s in sections:
section_entities.append(
Section(
name=s["name"],
address=s.get("address"),
virtual_size=s.get("virtual_size", 0),
raw_size=s.get("raw_size", 0),
flags=s.get("flags", []),
entropy=s.get("entropy"),
)
)
# Build EntryPoint entities
entrypoint_entities = []
for ep in entrypoints:
entrypoint_entities.append(
EntryPoint(
address=ep.get("address"),
kind=ep.get("kind", "unknown"),
confidence=ep.get("confidence", Confidence.UNKNOWN),
name=ep.get("name"),
)
)
# Build Import entities
import_entities = []
for imp in imports:
import_entities.append(
Import(
module=imp.get("module", ""),
symbol=imp.get("symbol", ""),
address=imp.get("address"),
resolution=imp.get("resolution", ImportResolution.UNRESOLVED),
ordinal=imp.get("ordinal"),
)
)
# Build Export entities
export_entities = []
for exp in exports:
export_entities.append(
Export(
name=exp.get("name", ""),
address=exp.get("address"),
ordinal=exp.get("ordinal"),
forwarder=exp.get("forwarder"),
kind=exp.get("kind", "function"),
)
)
# Build Symbol entities
symbol_entities = []
for sym in symbols:
symbol_entities.append(
Symbol(
name=sym.get("name", ""),
address=sym.get("address"),
source=sym.get("source", FunctionNameSource.UNKNOWN),
scope=sym.get("scope", "unknown"),
)
)
# Build String entities
string_entities = []
for st in strings:
string_entities.append(
String(
text=st.get("text", ""),
encoding=st.get("encoding", "ASCII"),
address=st.get("address"),
length=st.get("length", len(st.get("text", ""))),
)
)
# Build Function entities
function_entities = []
for fn in functions:
function_entities.append(
Function(
name=fn.get("name", ""),
address=fn.get("address"),
size_bytes=fn.get("size_bytes", 0),
confidence=fn.get("confidence", Confidence.UNKNOWN),
name_source=fn.get("name_source", FunctionNameSource.UNKNOWN),
is_external=fn.get("is_external", False),
is_thunk=fn.get("is_thunk", False),
signature=fn.get("signature"),
basic_block_count=fn.get("basic_block_count"),
instruction_count=fn.get("instruction_count"),
cyclomatic_complexity=fn.get("cyclomatic_complexity"),
)
)
return {
"format": fmt,
"architecture": arch,
"endianness": endianness,
"sections": section_entities,
"entrypoints": entrypoint_entities,
"imports": import_entities,
"exports": export_entities,
"symbols": symbol_entities,
"strings": string_entities,
"functions": function_entities,
}
# ------------------------------------------------------------------
# FakeAdapter implementation
# ------------------------------------------------------------------
def __init__(self) -> None:
self._initialized: bool = False
self._fixtures: dict[str, dict[str, Any]] = {}
self._binaries: dict[str, dict[str, Any]] = {}
# Failure configuration
self._import_failures: dict[str, str] = {}
self._analysis_failure: str | None = None
self._backend_failures: dict[str, str] = {}
# Slow operation configuration
self._slow_operations: dict[str, float] = {}
# Address mapping configuration
self._unmapped_ranges: list[tuple[int, int]] = []
self._partially_mapped_ranges: list[tuple[int, int, int]] = []
self._truncation_points: dict[int, int] = {} # start_addr -> max_bytes
# Override data
self._override_sections: dict[str, list[Section]] = {}
self._override_functions: dict[str, list[Function]] = {}
self._override_strings: dict[str, list[String]] = {}
# Read BINARY_FAKE_* environment variables for black-box CLI testing
self._read_env_config()
# ------------------------------------------------------------------
# Environment variable configuration
# ------------------------------------------------------------------
def _read_env_config(self) -> None:
"""Read BINARY_FAKE_* environment variables and apply failure/injection modes.
This enables black-box CLI testing without modifying CLI command modules.
All supported env vars are read once during __init__ and converted to
FakeAdapter configuration via the standard configure_* API.
Supported env vars:
- BINARY_FAKE_IMPORT_FAILURE : str — error message; triggers ImportFailedError
- BINARY_FAKE_ANALYSIS_FAILURE : str — error message; triggers AnalysisFailedError
- BINARY_FAKE_BACKEND_FAILURE : str — "method:message" or just "message";
triggers BackendFailureError
- BINARY_FAKE_SLOW_IMPORT_MS : int — milliseconds of delay before import
- BINARY_FAKE_SLOW_ANALYZE_MS : int — milliseconds of delay before analyze
- BINARY_FAKE_SLOW_DECOMPILE_MS : int — milliseconds of delay before decompile
- BINARY_FAKE_UNMAPPED_RANGES : str — "start:end,..." hex ranges to mark unmapped
- BINARY_FAKE_TRUNCATION : str — "addr:max_bytes,..." hex pairs for byte truncation
"""
# --- Import failure ---
import_failure = os.environ.get("BINARY_FAKE_IMPORT_FAILURE", "")
if import_failure:
# Empty-string key matches any path ("" in "anything" is True)
self.configure_import_failure("", import_failure)
# --- Analysis failure ---
analysis_failure = os.environ.get("BINARY_FAKE_ANALYSIS_FAILURE", "")
if analysis_failure:
self.configure_analysis_failure(analysis_failure)
# --- Backend failure (format: "method:message" or just "message") ---
backend_failure = os.environ.get("BINARY_FAKE_BACKEND_FAILURE", "")
if backend_failure:
if ":" in backend_failure:
method, msg = backend_failure.split(":", 1)
self.configure_backend_failure(method.strip(), msg.strip())
else:
self.configure_backend_failure("get_functions", backend_failure)
# --- Slow operations (milliseconds → seconds) ---
for env_name, operation in [
("BINARY_FAKE_SLOW_IMPORT_MS", "import"),
("BINARY_FAKE_SLOW_ANALYZE_MS", "analyze"),
("BINARY_FAKE_SLOW_DECOMPILE_MS", "decompile"),
]:
value = os.environ.get(env_name, "")
if value:
try:
delay = float(value) / 1000.0
if delay > 0:
self.configure_slow_operation(operation, delay)
except ValueError:
pass # Ignore non-numeric values
# --- Unmapped ranges (format: "0xSTART:0xEND,...") ---
unmapped = os.environ.get("BINARY_FAKE_UNMAPPED_RANGES", "")
if unmapped:
self._parse_range_list(unmapped, self.configure_unmapped_range)
# --- Truncation (format: "0xADDR:MAX_BYTES,...") ---
truncation = os.environ.get("BINARY_FAKE_TRUNCATION", "")
if truncation:
self._parse_pair_list(truncation, self.configure_truncation)
@staticmethod
def _parse_range_list(raw: str, configure: Any) -> None:
"""Parse a comma-separated list of 'start:end' hex ranges.
Args:
raw: Comma-separated hex range spec (e.g., "0x5000:0x6000,0x7000:0x7100").
configure: Callable(start: int, end: int) to apply each parsed range.
"""
for item in raw.split(","):
item = item.strip()
if ":" in item:
try:
start_str, end_str = item.split(":", 1)
start = int(start_str.strip(), 16)
end = int(end_str.strip(), 16)
configure(start, end)
except (ValueError, IndexError):
pass
@staticmethod
def _parse_pair_list(raw: str, configure: Any) -> None:
"""Parse a comma-separated list of 'addr:value' hex:int pairs.
Args:
raw: Comma-separated hex pair spec (e.g., "0x401000:8,0x402000:4").
configure: Callable(addr: int, value: int) to apply each parsed pair.
"""
for item in raw.split(","):
item = item.strip()
if ":" in item:
try:
addr_str, val_str = item.split(":", 1)
addr = int(addr_str.strip(), 16)
val = int(val_str.strip())
configure(addr, val)
except (ValueError, IndexError):
pass
# ------------------------------------------------------------------
# Configuration API
# ------------------------------------------------------------------
def set_fixture(self, name: str, fixture: dict[str, Any]) -> None:
"""Register a named fixture in the adapter."""
self._fixtures[name] = fixture
def configure_import_failure(self, binary_name: str, message: str) -> None:
"""Configure an import failure for a specific binary."""
self._import_failures[binary_name] = message
def configure_analysis_failure(self, message: str) -> None:
"""Configure the next analysis to fail completely."""
self._analysis_failure = message
def configure_backend_failure(self, method_name: str, message: str) -> None:
"""Configure a backend failure for a specific method (e.g., 'get_functions')."""
self._backend_failures[method_name] = message
def configure_slow_operation(self, operation: str, delay_seconds: float) -> None:
"""Make a specific operation slow (simulate delay)."""
self._slow_operations[operation] = delay_seconds
def configure_unmapped_range(self, start: int, end: int) -> None:
"""Mark an address range as unmapped.
Args:
start: Start offset (integer).
end: End offset (integer, exclusive).
"""
self._unmapped_ranges.append((start, end))
def configure_partial_mapping(self, start: int, end: int, mapped_end: int) -> None:
"""Mark a range as partially mapped — from start to mapped_end only.
Args:
start: Start offset.
end: Intended end offset.
mapped_end: Actual end of mapped data (must be < end).
"""
self._partially_mapped_ranges.append((start, end, mapped_end))
def configure_truncation(self, start_addr: int, max_bytes: int) -> None:
"""Configure truncation at a given address.
Args:
start_addr: Starting address offset.
max_bytes: Maximum bytes that can be read from this address.
"""
self._truncation_points[start_addr] = max_bytes
def clear_configuration(self) -> None:
"""Reset all failure, slow, and mapping configuration."""
self._import_failures.clear()
self._analysis_failure = None
self._backend_failures.clear()
self._slow_operations.clear()
self._unmapped_ranges.clear()
self._partially_mapped_ranges.clear()
self._truncation_points.clear()
# ------------------------------------------------------------------
# BackendAdapter implementation
# ------------------------------------------------------------------
def register_binary(self, binary: Binary, fixture_name: str) -> None:
"""Register a binary with a fixture name for fixture-based lookup.
Populates the internal _binaries mapping so that get_* methods
(which call _get_binary_fixture) can find the right fixture data.
Args:
binary: The canonical Binary entity to register.
fixture_name: The name of the fixture dataset to associate.
"""
self._binaries[str(binary.id)] = {
"binary": binary,
"fixture_name": fixture_name,
}
def initialize(self) -> None:
self._initialized = True
def capabilities(self) -> dict[str, Any]:
return {
"adapter": "fake",
"adapter_version": "0.1.0",
"backend": "FakeAdapter",
"backend_version": "0.1.0",
"supported_formats": ["PE", "ELF", "Mach-O"],
"supported_architectures": ["x86", "x86-64", "arm64"],
"concurrency": "PROJECT_SERIALIZED",
"max_depth": 10,
}
def available_profiles(self) -> list[AnalysisProfile]:
return list(self.DEFAULT_PROFILES)
def import_binary(self, path: str, project: Project) -> Binary:
self._check_slow("import")
# Check for import failure
# Determine the fixture name from path or project
for name, msg in self._import_failures.items():
if name in path or name == project.name:
from binary_analysis.domain.errors import ImportFailedError
raise ImportFailedError(msg, binary_path=path)
# Determine format from fixture data
fixture = self._resolve_fixture(path, project)
binary = Binary(
id=uuid4(),
sha256="a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
path=path,
format=fixture.get("format", "unknown"),
import_mode="copy",
size_bytes=16384,
architecture=fixture.get("architecture"),
endianness=fixture.get("endianness"),
)
# Store the binary
self.register_binary(binary, self._resolve_fixture_name(path, project))
return binary
def analyze(self, binary: Binary, profile: AnalysisProfile) -> AnalysisResult:
self._check_slow("analyze")
if self._analysis_failure is not None:
from binary_analysis.domain.errors import AnalysisFailedError
msg = self._analysis_failure
self._analysis_failure = None
raise AnalysisFailedError(msg)
# Build result based on profile
fixture = self._get_binary_fixture(binary)
available = set(fixture.keys())
requested = set(profile.analysers)
completed = []
failed = []
diagnostics = []
for analyser in requested:
if analyser in available and fixture.get(analyser):
completed.append(analyser)
else:
failed.append(analyser)
diagnostics.append(
{
"severity": "WARNING",
"category": analyser,
"message": f"Analyser '{analyser}' not available in fixture",
"recoverable": True,
}
)
return AnalysisResult(
success=len(failed) == 0 or len(completed) > 0,
partial=len(failed) > 0 and len(completed) > 0,
completed_analysers=completed,
failed_analysers=failed,
diagnostics=diagnostics,
)
def get_metadata(self, binary: Binary) -> BinaryMetadata:
self._check_slow("get_metadata")
self._check_backend_failure("get_metadata")
fixture = self._get_binary_fixture(binary)
endian_val = fixture.get("endianness")
return BinaryMetadata(
format=fixture.get("format", "unknown"),
architecture=fixture.get("architecture"),
endianness=endian_val.value if endian_val is not None else None,
size_bytes=16384,
entry_point=self._first_entrypoint(fixture),
)
def get_sections(self, binary: Binary) -> list[Section]:
self._check_slow("get_sections")
self._check_backend_failure("get_sections")
if str(binary.id) in self._override_sections:
return list(self._override_sections[str(binary.id)])
fixture = self._get_binary_fixture(binary)
return list(fixture.get("sections", []))
def get_entrypoints(self, binary: Binary) -> list[EntryPoint]:
self._check_backend_failure("get_entrypoints")
fixture = self._get_binary_fixture(binary)
return list(fixture.get("entrypoints", []))
def get_imports(self, binary: Binary) -> list[Import]:
self._check_backend_failure("get_imports")
fixture = self._get_binary_fixture(binary)
return list(fixture.get("imports", []))
def get_exports(self, binary: Binary) -> list[Export]:
self._check_backend_failure("get_exports")
fixture = self._get_binary_fixture(binary)
return list(fixture.get("exports", []))
def get_symbols(self, binary: Binary) -> list[Symbol]:
self._check_backend_failure("get_symbols")
fixture = self._get_binary_fixture(binary)
return list(fixture.get("symbols", []))
def get_strings(
self,
binary: Binary,
min_length: int = 4,
contains: str | None = None,
encoding_filter: str | None = None,
) -> list[String]:
self._check_slow("get_strings")
self._check_backend_failure("get_strings")
fixture = self._get_binary_fixture(binary)
strings = fixture.get("strings", [])
result = []
for s in strings:
if s.length < min_length:
continue
if contains is not None and contains not in s.text:
continue
if encoding_filter is not None and s.encoding != encoding_filter:
continue
result.append(s)
return result
def get_functions(
self,
binary: Binary,
exclude_external: bool = True,
exclude_thunks: bool = True,
) -> list[Function]:
self._check_backend_failure("get_functions")
if str(binary.id) in self._override_functions:
functions = list(self._override_functions[str(binary.id)])
else:
fixture = self._get_binary_fixture(binary)
functions = list(fixture.get("functions", []))
result = []
for fn in functions:
if exclude_external and fn.is_external:
continue
if exclude_thunks and fn.is_thunk:
continue
result.append(fn)
return result
def decompile(self, binary: Binary, function: Function) -> DecompilationResult:
self._check_slow("decompile")
self._check_backend_failure("decompile")
func_name = function.name
fn_address = function.address.offset if function.address else "0x0"
pseudocode = (
f"// Reconstructed pseudocode for {func_name} @ {fn_address}\n"
f"// Generated by FakeAdapter\n"
f"\n"
f"{'int' if function.signature and 'int' in function.signature else 'void'} "
f"{func_name}(void) {{\n"
f" // Function body ({function.size_bytes} bytes)\n"
f" // ... (simulated decompilation)\n"
f" return;\n"
f"}}\n"
)
address_map: dict[int, dict[str, Any]] = {}
if function.address:
for i in range(1, pseudocode.count("\n") + 1):
address_map[i] = function.address.to_dict()
return DecompilationResult(
pseudocode=pseudocode,
address_map=address_map,
diagnostics=[],
language="c",
)
def disassemble(
self, binary: Binary, start_address: Address, end_address: Address
) -> list[Instruction]:
self._check_backend_failure("disassemble")
# Check if the range is unmapped
if self._is_unmapped(start_address) and self._is_unmapped(end_address):
raise ValueError(
f"Address range {start_address.offset}..{end_address.offset} is unmapped"
)
fixture = self._get_binary_fixture(binary)
# Generate synthetic instructions for the range
instructions = self._generate_instructions(start_address, end_address, fixture)
return instructions
def read_bytes(self, binary: Binary, address: Address, length: int) -> tuple[bytes, int]:
self._check_backend_failure("read_bytes")
if length <= 0:
raise ValueError("Length must be positive")
start_int = self._addr_to_int(address)
# Check if unmapped
if self._is_unmapped(address):
raise ValueError(f"Address {address.offset} is unmapped")
# Apply truncation
actual_length = length
if start_int in self._truncation_points:
max_bytes = self._truncation_points[start_int]
actual_length = min(length, max_bytes)
# Generate deterministic bytes based on address
data = bytes((start_int + i) % 256 for i in range(actual_length))
return (data, actual_length)
def get_xrefs(self, binary: Binary, address: Address) -> list[Reference]:
self._check_backend_failure("get_xrefs")
fixture = self._get_binary_fixture(binary)
functions = fixture.get("functions", [])
refs = []
for fn in functions:
if fn.address is None:
continue
if fn.address.offset == address.offset:
# References FROM this function to others
for target in functions:
if target.address is None or target is fn:
continue
refs.append(
Reference(
from_addr=fn.address,
to_addr=target.address,
kind=ReferenceKind.CALL,
confidence=Confidence.HIGH,
)
)
elif fn.address.offset != address.offset:
# If another function's address matches, add a reference TO it
pass
return refs
def get_callers(self, binary: Binary, function: Function) -> list[CallEdge]:
self._check_backend_failure("get_callers")
fixture = self._get_binary_fixture(binary)
functions = fixture.get("functions", [])
# Find functions that "call" this one — in the fake, each function
# calls the next one in the list (for deterministic graph)
callers = []
for i, fn in enumerate(functions):
if fn.address is None or function.address is None:
continue
# In our fake model, each function calls the next one
if i + 1 < len(functions) and functions[i + 1].address == function.address:
callers.append(
CallEdge(
from_address=fn.address,
to_address=function.address,
from_name=fn.name,
to_name=function.name,
kind="direct",
)
)
return callers
def get_callees(self, binary: Binary, function: Function) -> list[CallEdge]:
self._check_backend_failure("get_callees")
fixture = self._get_binary_fixture(binary)
functions = fixture.get("functions", [])
callees = []
for i, fn in enumerate(functions):
if fn.address is None or function.address is None:
continue
if fn.address == function.address and i + 1 < len(functions):
callee = functions[i + 1]
callees.append(
CallEdge(
from_address=function.address,
to_address=callee.address,
from_name=function.name,
to_name=callee.name,
kind="direct",
)
)
return callees
def get_callgraph(self, binary: Binary, function: Function, max_depth: int = 3) -> CallGraph:
self._check_backend_failure("get_callgraph")
self._check_slow("get_callgraph")
fixture = self._get_binary_fixture(binary)
functions = fixture.get("functions", [])
# Build a linear chain: each function calls the next
nodes: list[dict[str, Any]] = []
edges: list[dict[str, Any]] = []
# Find the root index
root_idx = None
for i, fn in enumerate(functions):
if fn.address and function.address and fn.address == function.address:
root_idx = i
break
if root_idx is None:
if function.address:
nodes.append(
{
"name": function.name,
"address": function.address.to_dict(),
"depth": 0,
}
)
return CallGraph(
root_address=function.address,
nodes=nodes,
edges=edges,
max_depth=max_depth,
total_nodes=len(nodes),
total_edges=len(edges),
truncated=False,
)
visited: set[int] = set()
truncated = False
for depth in range(min(max_depth + 1, len(functions))):
idx = root_idx + depth
if idx >= len(functions):
break
fn = functions[idx]
if fn.address is None:
continue
visited.add(idx)
nodes.append(
{
"name": fn.name,
"address": fn.address.to_dict(),
"depth": depth,
}
)
if idx + 1 < len(functions) and depth < max_depth:
next_fn = functions[idx + 1]
if next_fn.address:
edges.append(
{
"from": fn.address.to_dict(),
"to": next_fn.address.to_dict(),
"kind": "CALL",
}
)
return CallGraph(
root_address=function.address,
nodes=nodes,
edges=edges,
max_depth=max_depth,
total_nodes=len(nodes),
total_edges=len(edges),
truncated=truncated,
)
# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------
def _check_slow(self, operation: str) -> None:
"""Simulate a slow operation if configured."""
if operation in self._slow_operations:
delay = self._slow_operations[operation]
if delay > 0:
time.sleep(delay)
def _check_backend_failure(self, method_name: str) -> None:
"""Check if a backend failure is configured for this method."""
if method_name in self._backend_failures:
from binary_analysis.domain.errors import BackendFailureError
msg = self._backend_failures[method_name]
raise BackendFailureError(msg, original_error="Simulated backend failure")
def _resolve_fixture_name(self, path: str, project: Project) -> str:
"""Determine which fixture to use based on the binary path."""
for name in self._fixtures:
if name in path or name == project.name:
return name
# Default: return the first fixture
if self._fixtures:
return next(iter(self._fixtures))
return "pe-default"
def _resolve_fixture(self, path: str, project: Project) -> dict[str, Any]:
"""Resolve the fixture data for a given binary path."""
name = self._resolve_fixture_name(path, project)
if name in self._fixtures:
return self._fixtures[name]
# Return a minimal default fixture
return {
"format": "PE",
"architecture": "x86",
"endianness": Endianness.LITTLE,
"sections": [],
"entrypoints": [],
"imports": [],
"exports": [],
"symbols": [],
"strings": [],
"functions": [],
}
def _get_binary_fixture(self, binary: Binary) -> dict[str, Any]:
"""Get the fixture data associated with a binary."""
key = str(binary.id)
if key in self._binaries:
fixture_name = self._binaries[key].get("fixture_name", "")
if fixture_name in self._fixtures:
return self._fixtures[fixture_name]
return {
"format": binary.format or "PE",
"architecture": binary.architecture or "x86",
"endianness": binary.endianness or Endianness.LITTLE,
"sections": [],
"entrypoints": [],
"imports": [],
"exports": [],
"symbols": [],
"strings": [],
"functions": [],
}
def _first_entrypoint(self, fixture: dict[str, Any]) -> Address | None:
"""Return the first entrypoint's address, or None."""
entrypoints: list[EntryPoint] = fixture.get("entrypoints", [])
if entrypoints:
return entrypoints[0].address
return None
@staticmethod
def _addr_to_int(addr: Address) -> int:
"""Convert an Address offset string to an integer."""
if addr.offset.startswith("0x"):
return int(addr.offset, 16)
return int(addr.offset, 16)
def _is_unmapped(self, addr: Address) -> bool:
"""Check if an address falls within any unmapped range."""
addr_int = self._addr_to_int(addr)
return any(start <= addr_int < end for start, end in self._unmapped_ranges)
def _generate_instructions(
self,
start_address: Address,
end_address: Address,
fixture: dict[str, Any],
) -> list[Instruction]:
"""Generate synthetic instructions for an address range."""
start_int = self._addr_to_int(start_address)
end_int = self._addr_to_int(end_address)
instructions: list[Instruction] = []
offset = start_int
idx = 0
# Simple x86-like instruction templates
templates = [
("push", "rbp"),
("mov", "rbp, rsp"),
("sub", "rsp, 0x20"),
("mov", "eax, 0x0"),
("call", "0x401100"),
("test", "eax, eax"),
("je", "0x401050"),
("lea", "rdi, [rip+0x1f4]"),
("call", "0x401200"),
("add", "rsp, 0x20"),
("pop", "rbp"),
("ret", ""),
]
while offset <= end_int and len(instructions) < 1000:
template = templates[idx % len(templates)]
inst_size = 1 + len(template[0]) % 5 # 1-5 bytes
instr = Instruction(
mnemonic=template[0],
operands=template[1],
bytes_hex=format(offset % 256, "02x"),
address=Address(
space="ram",
offset=f"0x{offset:x}",
display=f"0x{offset:x}",
),
size_bytes=inst_size,
)
instructions.append(instr)
offset += inst_size
idx += 1
return instructions