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,259 @@
|
|
|
1
|
+
"""IDALib Session Manager - Multi-binary management for headless MCP server
|
|
2
|
+
|
|
3
|
+
This module provides session management for multiple IDA databases in idalib mode.
|
|
4
|
+
Each session represents an opened binary with its own IDA database instance.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import uuid
|
|
8
|
+
import threading
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, Optional, Any
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
import idapro
|
|
16
|
+
import ida_auto
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class IDASession:
|
|
23
|
+
"""Represents a single IDA database session"""
|
|
24
|
+
|
|
25
|
+
session_id: str
|
|
26
|
+
input_path: Path
|
|
27
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
28
|
+
last_accessed: datetime = field(default_factory=datetime.now)
|
|
29
|
+
is_analyzing: bool = False
|
|
30
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict:
|
|
33
|
+
"""Convert session to dictionary format"""
|
|
34
|
+
return {
|
|
35
|
+
"session_id": self.session_id,
|
|
36
|
+
"input_path": str(self.input_path),
|
|
37
|
+
"filename": self.input_path.name,
|
|
38
|
+
"created_at": self.created_at.isoformat(),
|
|
39
|
+
"last_accessed": self.last_accessed.isoformat(),
|
|
40
|
+
"is_analyzing": self.is_analyzing,
|
|
41
|
+
"metadata": self.metadata,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class IDASessionManager:
|
|
46
|
+
"""Manages multiple IDA database sessions for idalib mode"""
|
|
47
|
+
|
|
48
|
+
def __init__(self):
|
|
49
|
+
self._sessions: Dict[str, IDASession] = {}
|
|
50
|
+
self._current_session_id: Optional[str] = None
|
|
51
|
+
self._lock = threading.RLock()
|
|
52
|
+
logger.info("IDASessionManager initialized")
|
|
53
|
+
|
|
54
|
+
def open_binary(
|
|
55
|
+
self,
|
|
56
|
+
input_path: Path | str,
|
|
57
|
+
run_auto_analysis: bool = True,
|
|
58
|
+
session_id: Optional[str] = None,
|
|
59
|
+
) -> str:
|
|
60
|
+
"""Open a binary file and create a new session
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
input_path: Path to the binary file
|
|
64
|
+
run_auto_analysis: Whether to run auto-analysis
|
|
65
|
+
session_id: Optional custom session ID (auto-generated if not provided)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Session ID for the opened binary
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
FileNotFoundError: If the input file doesn't exist
|
|
72
|
+
RuntimeError: If failed to open the database
|
|
73
|
+
"""
|
|
74
|
+
input_path = Path(input_path)
|
|
75
|
+
|
|
76
|
+
if not input_path.exists():
|
|
77
|
+
raise FileNotFoundError(f"Input file not found: {input_path}")
|
|
78
|
+
|
|
79
|
+
with self._lock:
|
|
80
|
+
# Check if this file is already open
|
|
81
|
+
for sid, session in self._sessions.items():
|
|
82
|
+
if session.input_path.resolve() == input_path.resolve():
|
|
83
|
+
logger.info(f"Binary already open in session: {sid}")
|
|
84
|
+
self._current_session_id = sid
|
|
85
|
+
session.last_accessed = datetime.now()
|
|
86
|
+
return sid
|
|
87
|
+
|
|
88
|
+
# Close current database if any (Do we need to close the database first?)
|
|
89
|
+
if self._current_session_id is not None:
|
|
90
|
+
logger.debug("Closing current database before opening new one")
|
|
91
|
+
idapro.close_database()
|
|
92
|
+
|
|
93
|
+
# Generate session ID
|
|
94
|
+
if session_id is None:
|
|
95
|
+
session_id = str(uuid.uuid4())[:8]
|
|
96
|
+
|
|
97
|
+
# Open the database
|
|
98
|
+
logger.info(f"Opening database: {input_path} (session: {session_id})")
|
|
99
|
+
|
|
100
|
+
if idapro.open_database(
|
|
101
|
+
str(input_path), run_auto_analysis=run_auto_analysis
|
|
102
|
+
):
|
|
103
|
+
raise RuntimeError(f"Failed to open database: {input_path}")
|
|
104
|
+
|
|
105
|
+
# Create session object
|
|
106
|
+
session = IDASession(
|
|
107
|
+
session_id=session_id,
|
|
108
|
+
input_path=input_path,
|
|
109
|
+
is_analyzing=run_auto_analysis,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
self._sessions[session_id] = session
|
|
113
|
+
self._current_session_id = session_id
|
|
114
|
+
|
|
115
|
+
# Wait for analysis if requested
|
|
116
|
+
if run_auto_analysis:
|
|
117
|
+
logger.debug(
|
|
118
|
+
f"Waiting for auto-analysis to complete (session: {session_id})"
|
|
119
|
+
)
|
|
120
|
+
ida_auto.auto_wait()
|
|
121
|
+
session.is_analyzing = False
|
|
122
|
+
logger.info(f"Auto-analysis completed (session: {session_id})")
|
|
123
|
+
|
|
124
|
+
logger.info(f"Session created: {session_id} for {input_path.name}")
|
|
125
|
+
return session_id
|
|
126
|
+
|
|
127
|
+
def close_session(self, session_id: str) -> bool:
|
|
128
|
+
"""Close a specific session and its database
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
session_id: Session ID to close
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
True if closed successfully, False if session not found
|
|
135
|
+
"""
|
|
136
|
+
with self._lock:
|
|
137
|
+
if session_id not in self._sessions:
|
|
138
|
+
logger.warning(f"Session not found: {session_id}")
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
session = self._sessions[session_id]
|
|
142
|
+
logger.info(f"Closing session: {session_id} ({session.input_path.name})")
|
|
143
|
+
|
|
144
|
+
# If this is the current session, close the database
|
|
145
|
+
if self._current_session_id == session_id:
|
|
146
|
+
idapro.close_database()
|
|
147
|
+
self._current_session_id = None
|
|
148
|
+
|
|
149
|
+
# Remove session
|
|
150
|
+
del self._sessions[session_id]
|
|
151
|
+
logger.info(f"Session closed: {session_id}")
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
def switch_session(self, session_id: str) -> bool:
|
|
155
|
+
"""Switch to a different session
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
session_id: Session ID to switch to
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
True if switched successfully
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
ValueError: If session not found
|
|
165
|
+
"""
|
|
166
|
+
with self._lock:
|
|
167
|
+
if session_id not in self._sessions:
|
|
168
|
+
raise ValueError(f"Session not found: {session_id}")
|
|
169
|
+
|
|
170
|
+
if self._current_session_id == session_id:
|
|
171
|
+
logger.debug(f"Already on session: {session_id}")
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
session = self._sessions[session_id]
|
|
175
|
+
|
|
176
|
+
# Close current database
|
|
177
|
+
if self._current_session_id is not None:
|
|
178
|
+
logger.debug(f"Closing current session: {self._current_session_id}")
|
|
179
|
+
idapro.close_database()
|
|
180
|
+
|
|
181
|
+
# Open the target session's database
|
|
182
|
+
logger.info(
|
|
183
|
+
f"Switching to session: {session_id} ({session.input_path.name})"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if idapro.open_database(str(session.input_path), run_auto_analysis=False):
|
|
187
|
+
raise RuntimeError(f"Failed to switch to session: {session_id}")
|
|
188
|
+
|
|
189
|
+
self._current_session_id = session_id
|
|
190
|
+
session.last_accessed = datetime.now()
|
|
191
|
+
|
|
192
|
+
logger.info(f"Switched to session: {session_id}")
|
|
193
|
+
return True
|
|
194
|
+
|
|
195
|
+
def get_current_session(self) -> Optional[IDASession]:
|
|
196
|
+
"""Get the current active session
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Current session or None if no active session
|
|
200
|
+
"""
|
|
201
|
+
with self._lock:
|
|
202
|
+
if self._current_session_id is None:
|
|
203
|
+
return None
|
|
204
|
+
return self._sessions.get(self._current_session_id)
|
|
205
|
+
|
|
206
|
+
def list_sessions(self) -> list[dict]:
|
|
207
|
+
"""List all open sessions
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
List of session dictionaries with metadata
|
|
211
|
+
"""
|
|
212
|
+
with self._lock:
|
|
213
|
+
return [
|
|
214
|
+
{
|
|
215
|
+
**session.to_dict(),
|
|
216
|
+
"is_current": session.session_id == self._current_session_id,
|
|
217
|
+
}
|
|
218
|
+
for session in self._sessions.values()
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
def get_session(self, session_id: str) -> Optional[IDASession]:
|
|
222
|
+
"""Get a specific session by ID
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
session_id: Session ID to retrieve
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Session object or None if not found
|
|
229
|
+
"""
|
|
230
|
+
with self._lock:
|
|
231
|
+
return self._sessions.get(session_id)
|
|
232
|
+
|
|
233
|
+
def close_all_sessions(self):
|
|
234
|
+
"""Close all sessions and databases"""
|
|
235
|
+
with self._lock:
|
|
236
|
+
logger.info(f"Closing all {len(self._sessions)} sessions")
|
|
237
|
+
|
|
238
|
+
if self._current_session_id is not None:
|
|
239
|
+
idapro.close_database()
|
|
240
|
+
self._current_session_id = None
|
|
241
|
+
|
|
242
|
+
self._sessions.clear()
|
|
243
|
+
logger.info("All sessions closed")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# Global session manager instance
|
|
247
|
+
_session_manager: Optional[IDASessionManager] = None
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def get_session_manager() -> IDASessionManager:
|
|
251
|
+
"""Get the global session manager instance
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Global IDASessionManager instance
|
|
255
|
+
"""
|
|
256
|
+
global _session_manager
|
|
257
|
+
if _session_manager is None:
|
|
258
|
+
_session_manager = IDASessionManager()
|
|
259
|
+
return _session_manager
|