ida-pro-mcp-xjoker 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ida_pro_mcp/__init__.py +0 -0
- ida_pro_mcp/__main__.py +6 -0
- ida_pro_mcp/ida_mcp/__init__.py +68 -0
- ida_pro_mcp/ida_mcp/api_analysis.py +1296 -0
- ida_pro_mcp/ida_mcp/api_core.py +337 -0
- ida_pro_mcp/ida_mcp/api_debug.py +617 -0
- ida_pro_mcp/ida_mcp/api_memory.py +304 -0
- ida_pro_mcp/ida_mcp/api_modify.py +406 -0
- ida_pro_mcp/ida_mcp/api_python.py +179 -0
- ida_pro_mcp/ida_mcp/api_resources.py +295 -0
- ida_pro_mcp/ida_mcp/api_stack.py +167 -0
- ida_pro_mcp/ida_mcp/api_types.py +480 -0
- ida_pro_mcp/ida_mcp/auth.py +166 -0
- ida_pro_mcp/ida_mcp/cache.py +232 -0
- ida_pro_mcp/ida_mcp/config.py +228 -0
- ida_pro_mcp/ida_mcp/framework.py +547 -0
- ida_pro_mcp/ida_mcp/http.py +859 -0
- ida_pro_mcp/ida_mcp/port_utils.py +104 -0
- ida_pro_mcp/ida_mcp/rpc.py +187 -0
- ida_pro_mcp/ida_mcp/server_manager.py +339 -0
- ida_pro_mcp/ida_mcp/sync.py +233 -0
- ida_pro_mcp/ida_mcp/tests/__init__.py +14 -0
- ida_pro_mcp/ida_mcp/tests/test_api_analysis.py +336 -0
- ida_pro_mcp/ida_mcp/tests/test_api_core.py +237 -0
- ida_pro_mcp/ida_mcp/tests/test_api_memory.py +207 -0
- ida_pro_mcp/ida_mcp/tests/test_api_modify.py +123 -0
- ida_pro_mcp/ida_mcp/tests/test_api_resources.py +199 -0
- ida_pro_mcp/ida_mcp/tests/test_api_stack.py +77 -0
- ida_pro_mcp/ida_mcp/tests/test_api_types.py +249 -0
- ida_pro_mcp/ida_mcp/ui.py +357 -0
- ida_pro_mcp/ida_mcp/utils.py +1186 -0
- ida_pro_mcp/ida_mcp/zeromcp/__init__.py +5 -0
- ida_pro_mcp/ida_mcp/zeromcp/jsonrpc.py +384 -0
- ida_pro_mcp/ida_mcp/zeromcp/mcp.py +883 -0
- ida_pro_mcp/ida_mcp.py +186 -0
- ida_pro_mcp/idalib_server.py +354 -0
- ida_pro_mcp/idalib_session_manager.py +259 -0
- ida_pro_mcp/server.py +1060 -0
- ida_pro_mcp/test.py +170 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/METADATA +405 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/RECORD +45 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/WHEEL +5 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/entry_points.txt +4 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/licenses/LICENSE +21 -0
- ida_pro_mcp_xjoker-1.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import queue
|
|
3
|
+
import functools
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
import idaapi
|
|
8
|
+
import idc
|
|
9
|
+
from .rpc import McpToolError
|
|
10
|
+
from .zeromcp.jsonrpc import get_current_cancel_event, RequestCancelledError
|
|
11
|
+
|
|
12
|
+
# ============================================================================
|
|
13
|
+
# IDA Synchronization & Error Handling
|
|
14
|
+
# ============================================================================
|
|
15
|
+
|
|
16
|
+
ida_major, ida_minor = map(int, idaapi.get_kernel_version().split("."))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class IDAError(McpToolError):
|
|
20
|
+
def __init__(self, message: str):
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def message(self) -> str:
|
|
25
|
+
return self.args[0]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class IDASyncError(Exception):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CancelledError(RequestCancelledError):
|
|
33
|
+
"""Raised when a request is cancelled via notifications/cancelled."""
|
|
34
|
+
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
_TOOL_TIMEOUT_ENV = "IDA_MCP_TOOL_TIMEOUT_SEC"
|
|
40
|
+
_DEFAULT_TOOL_TIMEOUT_SEC = 15.0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _get_tool_timeout_seconds() -> float:
|
|
44
|
+
value = os.getenv(_TOOL_TIMEOUT_ENV, "").strip()
|
|
45
|
+
if value == "":
|
|
46
|
+
return _DEFAULT_TOOL_TIMEOUT_SEC
|
|
47
|
+
try:
|
|
48
|
+
return float(value)
|
|
49
|
+
except ValueError:
|
|
50
|
+
return _DEFAULT_TOOL_TIMEOUT_SEC
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ============================================================================
|
|
54
|
+
# Queue Object Pool - Reduces GC pressure and allocation overhead
|
|
55
|
+
# ============================================================================
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class QueuePool:
|
|
59
|
+
"""Object pool for Queue instances to reduce allocation overhead.
|
|
60
|
+
|
|
61
|
+
Reuses Queue objects instead of creating new ones for each IDA sync call.
|
|
62
|
+
Thread-safe implementation with automatic cleanup.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, max_size: int = 50):
|
|
66
|
+
self._pool: queue.Queue = queue.Queue(maxsize=max_size)
|
|
67
|
+
self._max_size = max_size
|
|
68
|
+
|
|
69
|
+
def acquire(self) -> queue.Queue:
|
|
70
|
+
"""Get a Queue from the pool or create a new one."""
|
|
71
|
+
try:
|
|
72
|
+
return self._pool.get_nowait()
|
|
73
|
+
except queue.Empty:
|
|
74
|
+
return queue.Queue()
|
|
75
|
+
|
|
76
|
+
def release(self, q: queue.Queue):
|
|
77
|
+
"""Return a Queue to the pool after clearing it."""
|
|
78
|
+
# Clear the queue before returning to pool
|
|
79
|
+
while not q.empty():
|
|
80
|
+
try:
|
|
81
|
+
q.get_nowait()
|
|
82
|
+
except queue.Empty:
|
|
83
|
+
break
|
|
84
|
+
# Try to return to pool
|
|
85
|
+
try:
|
|
86
|
+
self._pool.put_nowait(q)
|
|
87
|
+
except queue.Full:
|
|
88
|
+
# Pool full, let GC handle it
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
_queue_pool = QueuePool()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
call_stack = queue.LifoQueue()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _sync_wrapper(ff):
|
|
99
|
+
"""Call a function ff with a specific IDA safety_mode."""
|
|
100
|
+
|
|
101
|
+
res_container = _queue_pool.acquire()
|
|
102
|
+
|
|
103
|
+
def runned():
|
|
104
|
+
if not call_stack.empty():
|
|
105
|
+
last_func_name = call_stack.get()
|
|
106
|
+
error_str = f"Call stack is not empty while calling the function {ff.__name__} from {last_func_name}"
|
|
107
|
+
raise IDASyncError(error_str)
|
|
108
|
+
|
|
109
|
+
call_stack.put((ff.__name__))
|
|
110
|
+
try:
|
|
111
|
+
res_container.put(ff())
|
|
112
|
+
except Exception as x:
|
|
113
|
+
res_container.put(x)
|
|
114
|
+
finally:
|
|
115
|
+
call_stack.get()
|
|
116
|
+
|
|
117
|
+
idaapi.execute_sync(runned, idaapi.MFF_WRITE)
|
|
118
|
+
res = res_container.get()
|
|
119
|
+
_queue_pool.release(res_container)
|
|
120
|
+
if isinstance(res, Exception):
|
|
121
|
+
raise res
|
|
122
|
+
return res
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _normalize_timeout(value: object) -> float | None:
|
|
126
|
+
if value is None:
|
|
127
|
+
return None
|
|
128
|
+
try:
|
|
129
|
+
return float(value)
|
|
130
|
+
except (TypeError, ValueError):
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def sync_wrapper(ff, timeout_override: float | None = None):
|
|
135
|
+
"""Wrapper to enable batch mode during IDA synchronization."""
|
|
136
|
+
# Capture cancel event from thread-local before execute_sync
|
|
137
|
+
cancel_event = get_current_cancel_event()
|
|
138
|
+
|
|
139
|
+
def _run_with_batch(inner_ff):
|
|
140
|
+
def _wrapped():
|
|
141
|
+
old_batch = idc.batch(1)
|
|
142
|
+
try:
|
|
143
|
+
return inner_ff()
|
|
144
|
+
finally:
|
|
145
|
+
idc.batch(old_batch)
|
|
146
|
+
|
|
147
|
+
_wrapped.__name__ = inner_ff.__name__
|
|
148
|
+
return _wrapped
|
|
149
|
+
|
|
150
|
+
timeout = timeout_override
|
|
151
|
+
if timeout is None:
|
|
152
|
+
timeout = _get_tool_timeout_seconds()
|
|
153
|
+
if timeout > 0 or cancel_event is not None:
|
|
154
|
+
|
|
155
|
+
def timed_ff():
|
|
156
|
+
# Calculate deadline when execution starts on IDA main thread,
|
|
157
|
+
# not when the request was queued (avoids stale deadlines)
|
|
158
|
+
deadline = time.monotonic() + timeout if timeout > 0 else None
|
|
159
|
+
|
|
160
|
+
def profilefunc(frame, event, arg):
|
|
161
|
+
# Check cancellation first (higher priority)
|
|
162
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
163
|
+
raise CancelledError("Request was cancelled")
|
|
164
|
+
if deadline is not None and time.monotonic() >= deadline:
|
|
165
|
+
raise IDASyncError(f"Tool timed out after {timeout:.2f}s")
|
|
166
|
+
|
|
167
|
+
old_profile = sys.getprofile()
|
|
168
|
+
sys.setprofile(profilefunc)
|
|
169
|
+
try:
|
|
170
|
+
return ff()
|
|
171
|
+
finally:
|
|
172
|
+
sys.setprofile(old_profile)
|
|
173
|
+
|
|
174
|
+
timed_ff.__name__ = ff.__name__
|
|
175
|
+
return _sync_wrapper(_run_with_batch(timed_ff))
|
|
176
|
+
return _sync_wrapper(_run_with_batch(ff))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def idasync(f):
|
|
180
|
+
"""Run the function on the IDA main thread in write mode.
|
|
181
|
+
|
|
182
|
+
This is the unified decorator for all IDA synchronization.
|
|
183
|
+
Previously there were separate @idaread and @idawrite decorators,
|
|
184
|
+
but since read-only operations in IDA might actually require write
|
|
185
|
+
access (e.g., decompilation), we now use a single decorator.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
@functools.wraps(f)
|
|
189
|
+
def wrapper(*args, **kwargs):
|
|
190
|
+
ff = functools.partial(f, *args, **kwargs)
|
|
191
|
+
ff.__name__ = f.__name__
|
|
192
|
+
timeout_override = _normalize_timeout(
|
|
193
|
+
getattr(f, "__ida_mcp_timeout_sec__", None)
|
|
194
|
+
)
|
|
195
|
+
return sync_wrapper(ff, timeout_override)
|
|
196
|
+
|
|
197
|
+
return wrapper
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def tool_timeout(seconds: float):
|
|
201
|
+
"""Decorator to override per-tool timeout (seconds).
|
|
202
|
+
|
|
203
|
+
IMPORTANT: Must be applied BEFORE @idasync (i.e., listed AFTER it)
|
|
204
|
+
so the attribute exists when it captures the function in closure.
|
|
205
|
+
|
|
206
|
+
Correct order:
|
|
207
|
+
@tool
|
|
208
|
+
@idasync
|
|
209
|
+
@tool_timeout(90.0) # innermost
|
|
210
|
+
def my_func(...):
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
def decorator(func):
|
|
214
|
+
setattr(func, "__ida_mcp_timeout_sec__", seconds)
|
|
215
|
+
return func
|
|
216
|
+
|
|
217
|
+
return decorator
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def is_window_active():
|
|
221
|
+
"""Returns whether IDA is currently active."""
|
|
222
|
+
# Source: https://github.com/OALabs/hexcopy-ida/blob/8b0b2a3021d7dc9010c01821b65a80c47d491b61/hexcopy.py#L30
|
|
223
|
+
using_pyside6 = (ida_major > 9) or (ida_major == 9 and ida_minor >= 2)
|
|
224
|
+
|
|
225
|
+
if using_pyside6:
|
|
226
|
+
from PySide6 import QtWidgets
|
|
227
|
+
else:
|
|
228
|
+
from PyQt5 import QtWidgets
|
|
229
|
+
|
|
230
|
+
app = QtWidgets.QApplication.instance()
|
|
231
|
+
if app is None:
|
|
232
|
+
return False
|
|
233
|
+
return app.activeWindow() is not None
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""IDA Pro MCP Test Package.
|
|
2
|
+
|
|
3
|
+
This package contains test modules for each API module.
|
|
4
|
+
Tests are registered via the @test decorator from the framework module.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Import all test modules to register tests when the package is imported
|
|
8
|
+
from . import test_api_core
|
|
9
|
+
from . import test_api_analysis
|
|
10
|
+
from . import test_api_memory
|
|
11
|
+
from . import test_api_modify
|
|
12
|
+
from . import test_api_types
|
|
13
|
+
from . import test_api_stack
|
|
14
|
+
from . import test_api_resources
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""Tests for api_analysis API functions."""
|
|
2
|
+
|
|
3
|
+
# Import test framework from parent
|
|
4
|
+
from ..framework import (
|
|
5
|
+
test,
|
|
6
|
+
assert_has_keys,
|
|
7
|
+
assert_is_list,
|
|
8
|
+
get_any_function,
|
|
9
|
+
get_n_functions,
|
|
10
|
+
get_data_address,
|
|
11
|
+
get_unmapped_address,
|
|
12
|
+
get_functions_with_calls,
|
|
13
|
+
get_functions_with_callers,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Import functions under test
|
|
17
|
+
from ..api_analysis import (
|
|
18
|
+
decompile,
|
|
19
|
+
disasm,
|
|
20
|
+
xrefs_to,
|
|
21
|
+
xrefs_to_field,
|
|
22
|
+
callees,
|
|
23
|
+
find_bytes,
|
|
24
|
+
basic_blocks,
|
|
25
|
+
find,
|
|
26
|
+
export_funcs,
|
|
27
|
+
callgraph,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Import sync module for IDAError
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ============================================================================
|
|
34
|
+
# Tests for decompile
|
|
35
|
+
# ============================================================================
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@test()
|
|
39
|
+
def test_decompile_valid_function():
|
|
40
|
+
"""decompile returns code for a valid function"""
|
|
41
|
+
fn_addr = get_any_function()
|
|
42
|
+
if not fn_addr:
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
result = decompile(fn_addr)
|
|
46
|
+
assert_is_list(result, min_length=1)
|
|
47
|
+
# Should have code or error
|
|
48
|
+
r = result[0]
|
|
49
|
+
assert_has_keys(r, "addr")
|
|
50
|
+
# Either has code or has an error
|
|
51
|
+
assert r.get("code") is not None or r.get("error") is not None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@test()
|
|
55
|
+
def test_decompile_invalid_address():
|
|
56
|
+
"""decompile handles invalid address gracefully"""
|
|
57
|
+
result = decompile(get_unmapped_address())
|
|
58
|
+
assert_is_list(result, min_length=1)
|
|
59
|
+
# Should have an error
|
|
60
|
+
assert result[0].get("error") is not None or result[0].get("code") is None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@test()
|
|
64
|
+
def test_decompile_batch():
|
|
65
|
+
"""decompile can handle multiple addresses"""
|
|
66
|
+
addrs = get_n_functions(3)
|
|
67
|
+
if len(addrs) < 2:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
result = decompile(addrs)
|
|
71
|
+
assert len(result) == len(addrs)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ============================================================================
|
|
75
|
+
# Tests for disasm
|
|
76
|
+
# ============================================================================
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@test()
|
|
80
|
+
def test_disasm_valid_function():
|
|
81
|
+
"""disasm returns assembly for a valid function"""
|
|
82
|
+
fn_addr = get_any_function()
|
|
83
|
+
if not fn_addr:
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
result = disasm(fn_addr)
|
|
87
|
+
assert_is_list(result, min_length=1)
|
|
88
|
+
r = result[0]
|
|
89
|
+
assert_has_keys(r, "addr")
|
|
90
|
+
# Should have asm output or error
|
|
91
|
+
assert r.get("asm") is not None or r.get("error") is not None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@test()
|
|
95
|
+
def test_disasm_pagination():
|
|
96
|
+
"""disasm respects count parameter"""
|
|
97
|
+
fn_addr = get_any_function()
|
|
98
|
+
if not fn_addr:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
result = disasm(fn_addr, count=10)
|
|
102
|
+
assert_is_list(result, min_length=1)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@test()
|
|
106
|
+
def test_disasm_unmapped_address():
|
|
107
|
+
"""disasm handles unmapped address"""
|
|
108
|
+
result = disasm(get_unmapped_address())
|
|
109
|
+
assert_is_list(result, min_length=1)
|
|
110
|
+
# Should have error or empty asm
|
|
111
|
+
r = result[0]
|
|
112
|
+
assert r.get("error") is not None or r.get("asm") == "" or r.get("asm") is None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@test()
|
|
116
|
+
def test_disasm_data_segment():
|
|
117
|
+
"""disasm handles data segment addresses"""
|
|
118
|
+
data_addr = get_data_address()
|
|
119
|
+
if not data_addr:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
result = disasm(data_addr)
|
|
123
|
+
assert_is_list(result, min_length=1)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ============================================================================
|
|
127
|
+
# Tests for xrefs_to
|
|
128
|
+
# ============================================================================
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@test()
|
|
132
|
+
def test_xrefs_to():
|
|
133
|
+
"""xrefs_to returns cross-references for a function"""
|
|
134
|
+
fn_addrs = get_functions_with_callers()
|
|
135
|
+
if not fn_addrs:
|
|
136
|
+
# Fallback to any function
|
|
137
|
+
fn_addr = get_any_function()
|
|
138
|
+
if not fn_addr:
|
|
139
|
+
return
|
|
140
|
+
else:
|
|
141
|
+
fn_addr = fn_addrs[0]
|
|
142
|
+
|
|
143
|
+
result = xrefs_to(fn_addr)
|
|
144
|
+
assert_is_list(result, min_length=1)
|
|
145
|
+
r = result[0]
|
|
146
|
+
assert_has_keys(r, "addr", "xrefs", "error")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@test()
|
|
150
|
+
def test_xrefs_to_invalid():
|
|
151
|
+
"""xrefs_to handles invalid address"""
|
|
152
|
+
result = xrefs_to(get_unmapped_address())
|
|
153
|
+
assert_is_list(result, min_length=1)
|
|
154
|
+
# Should return empty xrefs or error
|
|
155
|
+
r = result[0]
|
|
156
|
+
assert_has_keys(r, "addr")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ============================================================================
|
|
160
|
+
# Tests for xrefs_to_field
|
|
161
|
+
# ============================================================================
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@test()
|
|
165
|
+
def test_xrefs_to_field_nonexistent_struct():
|
|
166
|
+
"""xrefs_to_field handles non-existent struct"""
|
|
167
|
+
result = xrefs_to_field({"struct": "NonExistentStruct", "field": "nonexistent"})
|
|
168
|
+
assert_is_list(result, min_length=1)
|
|
169
|
+
r = result[0]
|
|
170
|
+
assert r.get("error") is not None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@test()
|
|
174
|
+
def test_xrefs_to_field_batch():
|
|
175
|
+
"""xrefs_to_field handles batch queries"""
|
|
176
|
+
result = xrefs_to_field(
|
|
177
|
+
[
|
|
178
|
+
{"struct": "Struct1", "field": "field1"},
|
|
179
|
+
{"struct": "Struct2", "field": "field2"},
|
|
180
|
+
]
|
|
181
|
+
)
|
|
182
|
+
assert_is_list(result, min_length=2)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ============================================================================
|
|
186
|
+
# Tests for callees
|
|
187
|
+
# ============================================================================
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@test()
|
|
191
|
+
def test_callees():
|
|
192
|
+
"""callees returns functions called by a function"""
|
|
193
|
+
fn_addrs = get_functions_with_calls()
|
|
194
|
+
if not fn_addrs:
|
|
195
|
+
fn_addr = get_any_function()
|
|
196
|
+
if not fn_addr:
|
|
197
|
+
return
|
|
198
|
+
else:
|
|
199
|
+
fn_addr = fn_addrs[0]
|
|
200
|
+
|
|
201
|
+
result = callees(fn_addr)
|
|
202
|
+
assert_is_list(result, min_length=1)
|
|
203
|
+
r = result[0]
|
|
204
|
+
assert_has_keys(r, "addr", "callees", "error")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@test()
|
|
208
|
+
def test_callees_multiple():
|
|
209
|
+
"""callees handles multiple addresses"""
|
|
210
|
+
addrs = get_n_functions(3)
|
|
211
|
+
if len(addrs) < 2:
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
result = callees(addrs)
|
|
215
|
+
assert len(result) == len(addrs)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@test()
|
|
219
|
+
def test_callees_invalid_address():
|
|
220
|
+
"""callees handles invalid address"""
|
|
221
|
+
result = callees(get_unmapped_address())
|
|
222
|
+
assert_is_list(result, min_length=1)
|
|
223
|
+
r = result[0]
|
|
224
|
+
assert_has_keys(r, "addr")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ============================================================================
|
|
228
|
+
# Tests for find_bytes
|
|
229
|
+
# ============================================================================
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@test()
|
|
233
|
+
def test_find_bytes():
|
|
234
|
+
"""find_bytes can search for byte patterns"""
|
|
235
|
+
# Search for common bytes that should exist
|
|
236
|
+
result = find_bytes("00 00")
|
|
237
|
+
assert_is_list(result, min_length=1)
|
|
238
|
+
r = result[0]
|
|
239
|
+
assert_has_keys(r, "query", "matches", "error")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ============================================================================
|
|
243
|
+
# Tests for basic_blocks
|
|
244
|
+
# ============================================================================
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@test()
|
|
248
|
+
def test_basic_blocks():
|
|
249
|
+
"""basic_blocks returns blocks for a function"""
|
|
250
|
+
fn_addr = get_any_function()
|
|
251
|
+
if not fn_addr:
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
result = basic_blocks(fn_addr)
|
|
255
|
+
assert_is_list(result, min_length=1)
|
|
256
|
+
r = result[0]
|
|
257
|
+
assert_has_keys(r, "addr", "blocks", "error")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# ============================================================================
|
|
261
|
+
# Tests for find
|
|
262
|
+
# ============================================================================
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@test()
|
|
266
|
+
def test_find_string():
|
|
267
|
+
"""find can search for strings"""
|
|
268
|
+
# Most binaries have some strings
|
|
269
|
+
result = find("string", query="*")
|
|
270
|
+
assert_is_list(result, min_length=1)
|
|
271
|
+
r = result[0]
|
|
272
|
+
assert_has_keys(r, "query", "matches", "error")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@test()
|
|
276
|
+
def test_find_invalid_type():
|
|
277
|
+
"""find handles invalid search type"""
|
|
278
|
+
result = find("invalid_type", query="test")
|
|
279
|
+
assert_is_list(result, min_length=1)
|
|
280
|
+
r = result[0]
|
|
281
|
+
# Should have error for invalid type
|
|
282
|
+
assert r.get("error") is not None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ============================================================================
|
|
286
|
+
# Tests for export_funcs
|
|
287
|
+
# ============================================================================
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@test()
|
|
291
|
+
def test_export_funcs_json():
|
|
292
|
+
"""export_funcs returns JSON format"""
|
|
293
|
+
fn_addr = get_any_function()
|
|
294
|
+
if not fn_addr:
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
result = export_funcs(fn_addr, fmt="json")
|
|
298
|
+
assert_is_list(result, min_length=1)
|
|
299
|
+
r = result[0]
|
|
300
|
+
assert_has_keys(r, "addr")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@test()
|
|
304
|
+
def test_export_funcs_c_header():
|
|
305
|
+
"""export_funcs returns C header format"""
|
|
306
|
+
fn_addr = get_any_function()
|
|
307
|
+
if not fn_addr:
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
result = export_funcs(fn_addr, fmt="c_header")
|
|
311
|
+
assert_is_list(result, min_length=1)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@test()
|
|
315
|
+
def test_export_funcs_invalid_address():
|
|
316
|
+
"""export_funcs handles invalid address"""
|
|
317
|
+
result = export_funcs(get_unmapped_address())
|
|
318
|
+
assert_is_list(result, min_length=1)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# ============================================================================
|
|
322
|
+
# Tests for callgraph
|
|
323
|
+
# ============================================================================
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@test()
|
|
327
|
+
def test_callgraph():
|
|
328
|
+
"""callgraph returns call graph data"""
|
|
329
|
+
fn_addr = get_any_function()
|
|
330
|
+
if not fn_addr:
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
result = callgraph(fn_addr)
|
|
334
|
+
assert_is_list(result, min_length=1)
|
|
335
|
+
r = result[0]
|
|
336
|
+
assert_has_keys(r, "addr")
|