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,480 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import ida_typeinf
|
|
4
|
+
import ida_hexrays
|
|
5
|
+
import ida_nalt
|
|
6
|
+
import ida_bytes
|
|
7
|
+
import ida_frame
|
|
8
|
+
import ida_ida
|
|
9
|
+
import idaapi
|
|
10
|
+
|
|
11
|
+
from .rpc import tool
|
|
12
|
+
from .sync import idasync, ida_major
|
|
13
|
+
from .utils import (
|
|
14
|
+
normalize_list_input,
|
|
15
|
+
normalize_dict_list,
|
|
16
|
+
parse_address,
|
|
17
|
+
get_type_by_name,
|
|
18
|
+
parse_decls_ctypes,
|
|
19
|
+
my_modifier_t,
|
|
20
|
+
StructRead,
|
|
21
|
+
TypeEdit,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ============================================================================
|
|
26
|
+
# Type Declaration
|
|
27
|
+
# ============================================================================
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@tool
|
|
31
|
+
@idasync
|
|
32
|
+
def declare_type(
|
|
33
|
+
decls: Annotated[list[str] | str, "C type declarations"],
|
|
34
|
+
) -> list[dict]:
|
|
35
|
+
"""Declare types"""
|
|
36
|
+
decls = normalize_list_input(decls)
|
|
37
|
+
results = []
|
|
38
|
+
|
|
39
|
+
for decl in decls:
|
|
40
|
+
try:
|
|
41
|
+
flags = ida_typeinf.PT_SIL | ida_typeinf.PT_EMPTY | ida_typeinf.PT_TYP
|
|
42
|
+
errors, messages = parse_decls_ctypes(decl, flags)
|
|
43
|
+
|
|
44
|
+
pretty_messages = "\n".join(messages)
|
|
45
|
+
if errors > 0:
|
|
46
|
+
results.append(
|
|
47
|
+
{"decl": decl, "error": f"Failed to parse:\n{pretty_messages}"}
|
|
48
|
+
)
|
|
49
|
+
else:
|
|
50
|
+
results.append({"decl": decl, "ok": True})
|
|
51
|
+
except Exception as e:
|
|
52
|
+
results.append({"decl": decl, "error": str(e)})
|
|
53
|
+
|
|
54
|
+
return results
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ============================================================================
|
|
58
|
+
# Structure Operations
|
|
59
|
+
# ============================================================================
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@tool
|
|
63
|
+
@idasync
|
|
64
|
+
def read_struct(queries: list[StructRead] | StructRead) -> list[dict]:
|
|
65
|
+
"""Reads struct type definition and parses actual memory values at the
|
|
66
|
+
given address as instances of that struct type.
|
|
67
|
+
|
|
68
|
+
If struct name is not provided, attempts to auto-detect from address.
|
|
69
|
+
Auto-detection only works if IDA already has type information applied
|
|
70
|
+
at that address
|
|
71
|
+
|
|
72
|
+
Returns struct layout with actual memory values for each field.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
queries = normalize_dict_list(queries)
|
|
76
|
+
|
|
77
|
+
results = []
|
|
78
|
+
for query in queries:
|
|
79
|
+
addr_str = query.get("addr", "")
|
|
80
|
+
struct_name = query.get("struct", "")
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
# Parse address - this is required
|
|
84
|
+
if not addr_str:
|
|
85
|
+
results.append(
|
|
86
|
+
{
|
|
87
|
+
"addr": None,
|
|
88
|
+
"struct": struct_name,
|
|
89
|
+
"members": None,
|
|
90
|
+
"error": "Address is required for reading struct fields",
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
# Try to parse as address, then try name resolution
|
|
96
|
+
try:
|
|
97
|
+
addr = parse_address(addr_str)
|
|
98
|
+
except Exception:
|
|
99
|
+
addr = idaapi.get_name_ea(idaapi.BADADDR, addr_str)
|
|
100
|
+
if addr == idaapi.BADADDR:
|
|
101
|
+
results.append(
|
|
102
|
+
{
|
|
103
|
+
"addr": addr_str,
|
|
104
|
+
"struct": struct_name,
|
|
105
|
+
"members": None,
|
|
106
|
+
"error": f"Failed to resolve address: {addr_str}",
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
# Auto-detect struct type from address if not provided
|
|
112
|
+
if not struct_name:
|
|
113
|
+
tif_auto = ida_typeinf.tinfo_t()
|
|
114
|
+
if ida_nalt.get_tinfo(tif_auto, addr) and tif_auto.is_udt():
|
|
115
|
+
struct_name = tif_auto.get_type_name()
|
|
116
|
+
|
|
117
|
+
if not struct_name:
|
|
118
|
+
results.append(
|
|
119
|
+
{
|
|
120
|
+
"addr": addr_str,
|
|
121
|
+
"struct": None,
|
|
122
|
+
"members": None,
|
|
123
|
+
"error": "No struct specified and could not auto-detect from address",
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
tif = ida_typeinf.tinfo_t()
|
|
129
|
+
if not tif.get_named_type(None, struct_name):
|
|
130
|
+
results.append(
|
|
131
|
+
{
|
|
132
|
+
"addr": addr_str,
|
|
133
|
+
"struct": struct_name,
|
|
134
|
+
"members": None,
|
|
135
|
+
"error": f"Struct '{struct_name}' not found",
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
udt_data = ida_typeinf.udt_type_data_t()
|
|
141
|
+
if not tif.get_udt_details(udt_data):
|
|
142
|
+
results.append(
|
|
143
|
+
{
|
|
144
|
+
"addr": addr_str,
|
|
145
|
+
"struct": struct_name,
|
|
146
|
+
"members": None,
|
|
147
|
+
"error": "Failed to get struct details",
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
members = []
|
|
153
|
+
for member in udt_data:
|
|
154
|
+
offset = member.begin() // 8
|
|
155
|
+
member_type = member.type._print()
|
|
156
|
+
member_name = member.name
|
|
157
|
+
member_size = member.type.get_size()
|
|
158
|
+
|
|
159
|
+
# Read memory value at member address
|
|
160
|
+
member_addr = addr + offset
|
|
161
|
+
try:
|
|
162
|
+
if member.type.is_ptr():
|
|
163
|
+
is_64bit = (
|
|
164
|
+
ida_ida.inf_is_64bit()
|
|
165
|
+
if ida_major >= 9
|
|
166
|
+
else idaapi.get_inf_structure().is_64bit()
|
|
167
|
+
)
|
|
168
|
+
if is_64bit:
|
|
169
|
+
value = idaapi.get_qword(member_addr)
|
|
170
|
+
value_str = f"0x{value:016X}"
|
|
171
|
+
else:
|
|
172
|
+
value = idaapi.get_dword(member_addr)
|
|
173
|
+
value_str = f"0x{value:08X}"
|
|
174
|
+
elif member_size == 1:
|
|
175
|
+
value = idaapi.get_byte(member_addr)
|
|
176
|
+
value_str = f"0x{value:02X} ({value})"
|
|
177
|
+
elif member_size == 2:
|
|
178
|
+
value = idaapi.get_word(member_addr)
|
|
179
|
+
value_str = f"0x{value:04X} ({value})"
|
|
180
|
+
elif member_size == 4:
|
|
181
|
+
value = idaapi.get_dword(member_addr)
|
|
182
|
+
value_str = f"0x{value:08X} ({value})"
|
|
183
|
+
elif member_size == 8:
|
|
184
|
+
value = idaapi.get_qword(member_addr)
|
|
185
|
+
value_str = f"0x{value:016X} ({value})"
|
|
186
|
+
else:
|
|
187
|
+
bytes_data = []
|
|
188
|
+
for i in range(min(member_size, 16)):
|
|
189
|
+
try:
|
|
190
|
+
bytes_data.append(
|
|
191
|
+
f"{idaapi.get_byte(member_addr + i):02X}"
|
|
192
|
+
)
|
|
193
|
+
except Exception:
|
|
194
|
+
break
|
|
195
|
+
value_str = f"[{' '.join(bytes_data)}{'...' if member_size > 16 else ''}]"
|
|
196
|
+
except Exception:
|
|
197
|
+
value_str = "<failed to read>"
|
|
198
|
+
|
|
199
|
+
member_info = {
|
|
200
|
+
"offset": f"0x{offset:08X}",
|
|
201
|
+
"type": member_type,
|
|
202
|
+
"name": member_name,
|
|
203
|
+
"size": member_size,
|
|
204
|
+
"value": value_str,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
members.append(member_info)
|
|
208
|
+
|
|
209
|
+
results.append(
|
|
210
|
+
{"addr": addr_str, "struct": struct_name, "members": members}
|
|
211
|
+
)
|
|
212
|
+
except Exception as e:
|
|
213
|
+
results.append(
|
|
214
|
+
{
|
|
215
|
+
"addr": addr_str,
|
|
216
|
+
"struct": struct_name,
|
|
217
|
+
"members": None,
|
|
218
|
+
"error": str(e),
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return results
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@tool
|
|
226
|
+
@idasync
|
|
227
|
+
def search_structs(
|
|
228
|
+
filter: Annotated[
|
|
229
|
+
str, "Case-insensitive substring to search for in structure names"
|
|
230
|
+
],
|
|
231
|
+
) -> list[dict]:
|
|
232
|
+
"""Search structs"""
|
|
233
|
+
results = []
|
|
234
|
+
limit = ida_typeinf.get_ordinal_limit()
|
|
235
|
+
|
|
236
|
+
for ordinal in range(1, limit):
|
|
237
|
+
tif = ida_typeinf.tinfo_t()
|
|
238
|
+
if tif.get_numbered_type(None, ordinal):
|
|
239
|
+
type_name: str = tif.get_type_name()
|
|
240
|
+
if type_name and filter.lower() in type_name.lower():
|
|
241
|
+
if tif.is_udt():
|
|
242
|
+
udt_data = ida_typeinf.udt_type_data_t()
|
|
243
|
+
cardinality = 0
|
|
244
|
+
if tif.get_udt_details(udt_data):
|
|
245
|
+
cardinality = udt_data.size()
|
|
246
|
+
|
|
247
|
+
results.append(
|
|
248
|
+
{
|
|
249
|
+
"name": type_name,
|
|
250
|
+
"size": tif.get_size(),
|
|
251
|
+
"cardinality": cardinality,
|
|
252
|
+
"is_union": (
|
|
253
|
+
udt_data.is_union
|
|
254
|
+
if tif.get_udt_details(udt_data)
|
|
255
|
+
else False
|
|
256
|
+
),
|
|
257
|
+
"ordinal": ordinal,
|
|
258
|
+
}
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return results
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ============================================================================
|
|
265
|
+
# Type Inference & Application
|
|
266
|
+
# ============================================================================
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@tool
|
|
270
|
+
@idasync
|
|
271
|
+
def set_type(edits: list[TypeEdit] | TypeEdit) -> list[dict]:
|
|
272
|
+
"""Apply types (function/global/local/stack)"""
|
|
273
|
+
|
|
274
|
+
def parse_addr_type(s: str) -> dict:
|
|
275
|
+
# Support "addr:typename" format (auto-detects kind)
|
|
276
|
+
if ":" in s:
|
|
277
|
+
parts = s.split(":", 1)
|
|
278
|
+
return {"addr": parts[0].strip(), "ty": parts[1].strip()}
|
|
279
|
+
# Just typename without address (invalid)
|
|
280
|
+
return {"ty": s.strip()}
|
|
281
|
+
|
|
282
|
+
edits = normalize_dict_list(edits, parse_addr_type)
|
|
283
|
+
results = []
|
|
284
|
+
|
|
285
|
+
for edit in edits:
|
|
286
|
+
try:
|
|
287
|
+
# Auto-detect kind if not provided
|
|
288
|
+
kind = edit.get("kind")
|
|
289
|
+
if not kind:
|
|
290
|
+
if "signature" in edit:
|
|
291
|
+
kind = "function"
|
|
292
|
+
elif "variable" in edit:
|
|
293
|
+
kind = "local"
|
|
294
|
+
elif "addr" in edit:
|
|
295
|
+
# Check if address points to a function
|
|
296
|
+
try:
|
|
297
|
+
addr = parse_address(edit["addr"])
|
|
298
|
+
func = idaapi.get_func(addr)
|
|
299
|
+
if func and "name" in edit and "ty" in edit:
|
|
300
|
+
kind = "stack"
|
|
301
|
+
else:
|
|
302
|
+
kind = "global"
|
|
303
|
+
except Exception:
|
|
304
|
+
kind = "global"
|
|
305
|
+
else:
|
|
306
|
+
kind = "global"
|
|
307
|
+
|
|
308
|
+
if kind == "function":
|
|
309
|
+
func = idaapi.get_func(parse_address(edit["addr"]))
|
|
310
|
+
if not func:
|
|
311
|
+
results.append({"edit": edit, "error": "Function not found"})
|
|
312
|
+
continue
|
|
313
|
+
|
|
314
|
+
tif = ida_typeinf.tinfo_t(edit["signature"], None, ida_typeinf.PT_SIL)
|
|
315
|
+
if not tif.is_func():
|
|
316
|
+
results.append({"edit": edit, "error": "Not a function type"})
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
success = ida_typeinf.apply_tinfo(
|
|
320
|
+
func.start_ea, tif, ida_typeinf.PT_SIL
|
|
321
|
+
)
|
|
322
|
+
results.append(
|
|
323
|
+
{
|
|
324
|
+
"edit": edit,
|
|
325
|
+
"ok": success,
|
|
326
|
+
"error": None if success else "Failed to apply type",
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
elif kind == "global":
|
|
331
|
+
ea = idaapi.get_name_ea(idaapi.BADADDR, edit.get("name", ""))
|
|
332
|
+
if ea == idaapi.BADADDR:
|
|
333
|
+
ea = parse_address(edit["addr"])
|
|
334
|
+
|
|
335
|
+
tif = get_type_by_name(edit["ty"])
|
|
336
|
+
success = ida_typeinf.apply_tinfo(ea, tif, ida_typeinf.PT_SIL)
|
|
337
|
+
results.append(
|
|
338
|
+
{
|
|
339
|
+
"edit": edit,
|
|
340
|
+
"ok": success,
|
|
341
|
+
"error": None if success else "Failed to apply type",
|
|
342
|
+
}
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
elif kind == "local":
|
|
346
|
+
func = idaapi.get_func(parse_address(edit["addr"]))
|
|
347
|
+
if not func:
|
|
348
|
+
results.append({"edit": edit, "error": "Function not found"})
|
|
349
|
+
continue
|
|
350
|
+
|
|
351
|
+
new_tif = ida_typeinf.tinfo_t(edit["ty"], None, ida_typeinf.PT_SIL)
|
|
352
|
+
modifier = my_modifier_t(edit["variable"], new_tif)
|
|
353
|
+
success = ida_hexrays.modify_user_lvars(func.start_ea, modifier)
|
|
354
|
+
results.append(
|
|
355
|
+
{
|
|
356
|
+
"edit": edit,
|
|
357
|
+
"ok": success,
|
|
358
|
+
"error": None if success else "Failed to apply type",
|
|
359
|
+
}
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
elif kind == "stack":
|
|
363
|
+
func = idaapi.get_func(parse_address(edit["addr"]))
|
|
364
|
+
if not func:
|
|
365
|
+
results.append({"edit": edit, "error": "No function found"})
|
|
366
|
+
continue
|
|
367
|
+
|
|
368
|
+
frame_tif = ida_typeinf.tinfo_t()
|
|
369
|
+
if not ida_frame.get_func_frame(frame_tif, func):
|
|
370
|
+
results.append({"edit": edit, "error": "No frame"})
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
idx, udm = frame_tif.get_udm(edit["name"])
|
|
374
|
+
if not udm:
|
|
375
|
+
results.append({"edit": edit, "error": f"{edit['name']} not found"})
|
|
376
|
+
continue
|
|
377
|
+
|
|
378
|
+
tid = frame_tif.get_udm_tid(idx)
|
|
379
|
+
udm = ida_typeinf.udm_t()
|
|
380
|
+
frame_tif.get_udm_by_tid(udm, tid)
|
|
381
|
+
offset = udm.offset // 8
|
|
382
|
+
|
|
383
|
+
tif = get_type_by_name(edit["ty"])
|
|
384
|
+
success = ida_frame.set_frame_member_type(func, offset, tif)
|
|
385
|
+
results.append(
|
|
386
|
+
{
|
|
387
|
+
"edit": edit,
|
|
388
|
+
"ok": success,
|
|
389
|
+
"error": None if success else "Failed to set type",
|
|
390
|
+
}
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
else:
|
|
394
|
+
results.append({"edit": edit, "error": f"Unknown kind: {kind}"})
|
|
395
|
+
|
|
396
|
+
except Exception as e:
|
|
397
|
+
results.append({"edit": edit, "error": str(e)})
|
|
398
|
+
|
|
399
|
+
return results
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@tool
|
|
403
|
+
@idasync
|
|
404
|
+
def infer_types(
|
|
405
|
+
addrs: Annotated[list[str] | str, "Addresses to infer types for"],
|
|
406
|
+
) -> list[dict]:
|
|
407
|
+
"""Infer types"""
|
|
408
|
+
addrs = normalize_list_input(addrs)
|
|
409
|
+
results = []
|
|
410
|
+
|
|
411
|
+
for addr in addrs:
|
|
412
|
+
try:
|
|
413
|
+
ea = parse_address(addr)
|
|
414
|
+
tif = ida_typeinf.tinfo_t()
|
|
415
|
+
|
|
416
|
+
# Try Hex-Rays inference
|
|
417
|
+
if ida_hexrays.init_hexrays_plugin() and ida_hexrays.guess_tinfo(tif, ea):
|
|
418
|
+
results.append(
|
|
419
|
+
{
|
|
420
|
+
"addr": addr,
|
|
421
|
+
"inferred_type": str(tif),
|
|
422
|
+
"method": "hexrays",
|
|
423
|
+
"confidence": "high",
|
|
424
|
+
}
|
|
425
|
+
)
|
|
426
|
+
continue
|
|
427
|
+
|
|
428
|
+
# Try getting existing type info
|
|
429
|
+
if ida_nalt.get_tinfo(tif, ea):
|
|
430
|
+
results.append(
|
|
431
|
+
{
|
|
432
|
+
"addr": addr,
|
|
433
|
+
"inferred_type": str(tif),
|
|
434
|
+
"method": "existing",
|
|
435
|
+
"confidence": "high",
|
|
436
|
+
}
|
|
437
|
+
)
|
|
438
|
+
continue
|
|
439
|
+
|
|
440
|
+
# Try to guess from size
|
|
441
|
+
size = ida_bytes.get_item_size(ea)
|
|
442
|
+
if size > 0:
|
|
443
|
+
type_guess = {
|
|
444
|
+
1: "uint8_t",
|
|
445
|
+
2: "uint16_t",
|
|
446
|
+
4: "uint32_t",
|
|
447
|
+
8: "uint64_t",
|
|
448
|
+
}.get(size, f"uint8_t[{size}]")
|
|
449
|
+
|
|
450
|
+
results.append(
|
|
451
|
+
{
|
|
452
|
+
"addr": addr,
|
|
453
|
+
"inferred_type": type_guess,
|
|
454
|
+
"method": "size_based",
|
|
455
|
+
"confidence": "low",
|
|
456
|
+
}
|
|
457
|
+
)
|
|
458
|
+
continue
|
|
459
|
+
|
|
460
|
+
results.append(
|
|
461
|
+
{
|
|
462
|
+
"addr": addr,
|
|
463
|
+
"inferred_type": None,
|
|
464
|
+
"method": None,
|
|
465
|
+
"confidence": "none",
|
|
466
|
+
}
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
except Exception as e:
|
|
470
|
+
results.append(
|
|
471
|
+
{
|
|
472
|
+
"addr": addr,
|
|
473
|
+
"inferred_type": None,
|
|
474
|
+
"method": None,
|
|
475
|
+
"confidence": "none",
|
|
476
|
+
"error": str(e),
|
|
477
|
+
}
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
return results
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""IDA MCP API Key Authentication
|
|
2
|
+
|
|
3
|
+
Provides authentication middleware for MCP server with support for:
|
|
4
|
+
- Bearer token authentication (Authorization: Bearer <key>)
|
|
5
|
+
- X-API-Key header authentication
|
|
6
|
+
- Timing-attack resistant comparison
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import hmac
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Optional, Callable
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Paths that don't require authentication
|
|
16
|
+
AUTH_EXEMPT_PATHS = frozenset({
|
|
17
|
+
"/health",
|
|
18
|
+
"/config.html",
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def check_api_key(provided_key: Optional[str], expected_key: Optional[str]) -> bool:
|
|
23
|
+
"""Compare API keys using constant-time comparison to prevent timing attacks.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
provided_key: The key provided by the client
|
|
27
|
+
expected_key: The expected API key from configuration
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
True if keys match, False otherwise
|
|
31
|
+
"""
|
|
32
|
+
if not expected_key:
|
|
33
|
+
# No key configured = authentication disabled
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
if not provided_key:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
# Use hmac.compare_digest for constant-time comparison
|
|
40
|
+
return hmac.compare_digest(provided_key.encode("utf-8"), expected_key.encode("utf-8"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def extract_api_key_from_headers(headers: dict) -> Optional[str]:
|
|
44
|
+
"""Extract API key from request headers.
|
|
45
|
+
|
|
46
|
+
Supports two formats:
|
|
47
|
+
- Authorization: Bearer <key>
|
|
48
|
+
- X-API-Key: <key>
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
headers: Dictionary of HTTP headers (case-insensitive keys)
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
The extracted API key or None
|
|
55
|
+
"""
|
|
56
|
+
# Try Authorization header first (Bearer token)
|
|
57
|
+
auth_header = headers.get("Authorization") or headers.get("authorization")
|
|
58
|
+
if auth_header:
|
|
59
|
+
parts = auth_header.split(" ", 1)
|
|
60
|
+
if len(parts) == 2 and parts[0].lower() == "bearer":
|
|
61
|
+
return parts[1].strip()
|
|
62
|
+
|
|
63
|
+
# Try X-API-Key header
|
|
64
|
+
api_key = headers.get("X-API-Key") or headers.get("x-api-key")
|
|
65
|
+
if api_key:
|
|
66
|
+
return api_key.strip()
|
|
67
|
+
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def is_path_exempt(path: str) -> bool:
|
|
72
|
+
"""Check if a path is exempt from authentication.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
path: The request path (e.g., "/health", "/mcp")
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
True if the path doesn't require authentication
|
|
79
|
+
"""
|
|
80
|
+
# Remove query string if present
|
|
81
|
+
if "?" in path:
|
|
82
|
+
path = path.split("?", 1)[0]
|
|
83
|
+
|
|
84
|
+
return path in AUTH_EXEMPT_PATHS
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AuthMiddleware:
|
|
88
|
+
"""Authentication middleware for HTTP request handlers.
|
|
89
|
+
|
|
90
|
+
Usage:
|
|
91
|
+
auth = AuthMiddleware(api_key="secret")
|
|
92
|
+
|
|
93
|
+
# In request handler:
|
|
94
|
+
if not auth.authenticate(request):
|
|
95
|
+
return send_401_response()
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, api_key: Optional[str] = None, enabled: bool = False):
|
|
99
|
+
"""Initialize authentication middleware.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
api_key: The expected API key (None = no authentication)
|
|
103
|
+
enabled: Whether authentication is enabled
|
|
104
|
+
"""
|
|
105
|
+
self._api_key = api_key
|
|
106
|
+
self._enabled = enabled and api_key is not None
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def enabled(self) -> bool:
|
|
110
|
+
return self._enabled
|
|
111
|
+
|
|
112
|
+
def update_key(self, api_key: Optional[str], enabled: bool = True) -> None:
|
|
113
|
+
"""Update the API key configuration.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
api_key: New API key
|
|
117
|
+
enabled: Whether to enable authentication
|
|
118
|
+
"""
|
|
119
|
+
self._api_key = api_key
|
|
120
|
+
self._enabled = enabled and api_key is not None
|
|
121
|
+
|
|
122
|
+
def authenticate(self, path: str, headers: dict) -> bool:
|
|
123
|
+
"""Authenticate a request.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
path: Request path
|
|
127
|
+
headers: Request headers dictionary
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
True if authenticated, False if authentication failed
|
|
131
|
+
"""
|
|
132
|
+
# Skip if authentication is disabled
|
|
133
|
+
if not self._enabled:
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
# Check if path is exempt
|
|
137
|
+
if is_path_exempt(path):
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
# Extract and verify API key
|
|
141
|
+
provided_key = extract_api_key_from_headers(headers)
|
|
142
|
+
return check_api_key(provided_key, self._api_key)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def create_auth_check(api_key: Optional[str], enabled: bool = False) -> Callable[[str, dict], bool]:
|
|
146
|
+
"""Create a simple authentication check function.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
api_key: The expected API key
|
|
150
|
+
enabled: Whether authentication is enabled
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
A function that takes (path, headers) and returns True if authenticated
|
|
154
|
+
"""
|
|
155
|
+
middleware = AuthMiddleware(api_key, enabled)
|
|
156
|
+
return middleware.authenticate
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
__all__ = [
|
|
160
|
+
"check_api_key",
|
|
161
|
+
"extract_api_key_from_headers",
|
|
162
|
+
"is_path_exempt",
|
|
163
|
+
"AuthMiddleware",
|
|
164
|
+
"create_auth_check",
|
|
165
|
+
"AUTH_EXEMPT_PATHS",
|
|
166
|
+
]
|