iflow-mcp_developermode-korea_reversecore-mcp 1.0.0__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.
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/METADATA +543 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/RECORD +79 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/WHEEL +5 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/top_level.txt +1 -0
- reversecore_mcp/__init__.py +9 -0
- reversecore_mcp/core/__init__.py +78 -0
- reversecore_mcp/core/audit.py +101 -0
- reversecore_mcp/core/binary_cache.py +138 -0
- reversecore_mcp/core/command_spec.py +357 -0
- reversecore_mcp/core/config.py +432 -0
- reversecore_mcp/core/container.py +288 -0
- reversecore_mcp/core/decorators.py +152 -0
- reversecore_mcp/core/error_formatting.py +93 -0
- reversecore_mcp/core/error_handling.py +142 -0
- reversecore_mcp/core/evidence.py +229 -0
- reversecore_mcp/core/exceptions.py +296 -0
- reversecore_mcp/core/execution.py +240 -0
- reversecore_mcp/core/ghidra.py +642 -0
- reversecore_mcp/core/ghidra_helper.py +481 -0
- reversecore_mcp/core/ghidra_manager.py +234 -0
- reversecore_mcp/core/json_utils.py +131 -0
- reversecore_mcp/core/loader.py +73 -0
- reversecore_mcp/core/logging_config.py +206 -0
- reversecore_mcp/core/memory.py +721 -0
- reversecore_mcp/core/metrics.py +198 -0
- reversecore_mcp/core/mitre_mapper.py +365 -0
- reversecore_mcp/core/plugin.py +45 -0
- reversecore_mcp/core/r2_helpers.py +404 -0
- reversecore_mcp/core/r2_pool.py +403 -0
- reversecore_mcp/core/report_generator.py +268 -0
- reversecore_mcp/core/resilience.py +252 -0
- reversecore_mcp/core/resource_manager.py +169 -0
- reversecore_mcp/core/result.py +132 -0
- reversecore_mcp/core/security.py +213 -0
- reversecore_mcp/core/validators.py +238 -0
- reversecore_mcp/dashboard/__init__.py +221 -0
- reversecore_mcp/prompts/__init__.py +56 -0
- reversecore_mcp/prompts/common.py +24 -0
- reversecore_mcp/prompts/game.py +280 -0
- reversecore_mcp/prompts/malware.py +1219 -0
- reversecore_mcp/prompts/report.py +150 -0
- reversecore_mcp/prompts/security.py +136 -0
- reversecore_mcp/resources.py +329 -0
- reversecore_mcp/server.py +727 -0
- reversecore_mcp/tools/__init__.py +49 -0
- reversecore_mcp/tools/analysis/__init__.py +74 -0
- reversecore_mcp/tools/analysis/capa_tools.py +215 -0
- reversecore_mcp/tools/analysis/die_tools.py +180 -0
- reversecore_mcp/tools/analysis/diff_tools.py +643 -0
- reversecore_mcp/tools/analysis/lief_tools.py +272 -0
- reversecore_mcp/tools/analysis/signature_tools.py +591 -0
- reversecore_mcp/tools/analysis/static_analysis.py +479 -0
- reversecore_mcp/tools/common/__init__.py +58 -0
- reversecore_mcp/tools/common/file_operations.py +352 -0
- reversecore_mcp/tools/common/memory_tools.py +516 -0
- reversecore_mcp/tools/common/patch_explainer.py +230 -0
- reversecore_mcp/tools/common/server_tools.py +115 -0
- reversecore_mcp/tools/ghidra/__init__.py +19 -0
- reversecore_mcp/tools/ghidra/decompilation.py +975 -0
- reversecore_mcp/tools/ghidra/ghidra_tools.py +1052 -0
- reversecore_mcp/tools/malware/__init__.py +61 -0
- reversecore_mcp/tools/malware/adaptive_vaccine.py +579 -0
- reversecore_mcp/tools/malware/dormant_detector.py +756 -0
- reversecore_mcp/tools/malware/ioc_tools.py +228 -0
- reversecore_mcp/tools/malware/vulnerability_hunter.py +519 -0
- reversecore_mcp/tools/malware/yara_tools.py +214 -0
- reversecore_mcp/tools/patch_explainer.py +19 -0
- reversecore_mcp/tools/radare2/__init__.py +13 -0
- reversecore_mcp/tools/radare2/r2_analysis.py +972 -0
- reversecore_mcp/tools/radare2/r2_session.py +376 -0
- reversecore_mcp/tools/radare2/radare2_mcp_tools.py +1183 -0
- reversecore_mcp/tools/report/__init__.py +4 -0
- reversecore_mcp/tools/report/email.py +82 -0
- reversecore_mcp/tools/report/report_mcp_tools.py +344 -0
- reversecore_mcp/tools/report/report_tools.py +1076 -0
- reversecore_mcp/tools/report/session.py +194 -0
- reversecore_mcp/tools/report_tools.py +11 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Safe subprocess execution with streaming and output limits.
|
|
3
|
+
|
|
4
|
+
This module provides functions to execute subprocess commands safely with:
|
|
5
|
+
- Streaming output to prevent OOM on large outputs
|
|
6
|
+
- Configurable output size limits
|
|
7
|
+
- Timeout handling
|
|
8
|
+
- Proper error handling and reporting
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import subprocess
|
|
13
|
+
import threading
|
|
14
|
+
from collections.abc import Coroutine
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from reversecore_mcp.core.logging_config import get_logger
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
from reversecore_mcp.core.exceptions import (
|
|
22
|
+
ExecutionTimeoutError,
|
|
23
|
+
ToolNotFoundError,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _BackgroundLoopRunner:
|
|
28
|
+
"""Run asyncio coroutines on a dedicated background event loop."""
|
|
29
|
+
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
self._loop = asyncio.new_event_loop()
|
|
32
|
+
self._thread = threading.Thread(
|
|
33
|
+
target=self._run_loop,
|
|
34
|
+
name="ReversecoreAsyncLoop",
|
|
35
|
+
daemon=True,
|
|
36
|
+
)
|
|
37
|
+
self._thread.start()
|
|
38
|
+
|
|
39
|
+
def _run_loop(self) -> None:
|
|
40
|
+
asyncio.set_event_loop(self._loop)
|
|
41
|
+
self._loop.run_forever()
|
|
42
|
+
|
|
43
|
+
def run(self, coro: Coroutine[Any, Any, tuple[str, int]]) -> tuple[str, int]:
|
|
44
|
+
future = asyncio.run_coroutine_threadsafe(coro, self._loop)
|
|
45
|
+
return future.result()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_BACKGROUND_LOOP_LOCK = threading.Lock()
|
|
49
|
+
_BACKGROUND_LOOP_RUNNER: _BackgroundLoopRunner | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _get_background_runner() -> _BackgroundLoopRunner:
|
|
53
|
+
global _BACKGROUND_LOOP_RUNNER
|
|
54
|
+
with _BACKGROUND_LOOP_LOCK:
|
|
55
|
+
if _BACKGROUND_LOOP_RUNNER is None:
|
|
56
|
+
_BACKGROUND_LOOP_RUNNER = _BackgroundLoopRunner()
|
|
57
|
+
return _BACKGROUND_LOOP_RUNNER
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def execute_subprocess_async(
|
|
61
|
+
cmd: list[str],
|
|
62
|
+
max_output_size: int = 10_000_000, # 10 MB default
|
|
63
|
+
timeout: int = 300, # 5 minutes default
|
|
64
|
+
encoding: str = "utf-8",
|
|
65
|
+
errors: str = "replace",
|
|
66
|
+
) -> tuple[str, int]:
|
|
67
|
+
"""
|
|
68
|
+
Execute a subprocess command asynchronously with streaming output and size limits.
|
|
69
|
+
|
|
70
|
+
This function uses asyncio.create_subprocess_exec to stream output in chunks,
|
|
71
|
+
preventing OOM issues when processing large files and avoiding CPU polling.
|
|
72
|
+
Output is truncated if it exceeds max_output_size.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
cmd: Command and arguments as a list (e.g., ["r2", "-q", "-c", "pdf @ main", "file.exe"])
|
|
76
|
+
max_output_size: Maximum output size in bytes (default: 10MB)
|
|
77
|
+
timeout: Maximum execution time in seconds (default: 300)
|
|
78
|
+
encoding: Text encoding for output (default: "utf-8")
|
|
79
|
+
errors: Error handling for encoding (default: "replace")
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Tuple of (output_text, bytes_read)
|
|
83
|
+
- output_text: The captured output (truncated if limit exceeded)
|
|
84
|
+
- bytes_read: Total bytes read (may exceed max_output_size if truncated)
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
ToolNotFoundError: If the command executable is not found
|
|
88
|
+
ExecutionTimeoutError: If the command exceeds the timeout
|
|
89
|
+
subprocess.CalledProcessError: If the command returns non-zero exit code
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
# Start the process with piped stdout/stderr
|
|
93
|
+
process = await asyncio.create_subprocess_exec(
|
|
94
|
+
*cmd,
|
|
95
|
+
stdout=asyncio.subprocess.PIPE,
|
|
96
|
+
stderr=asyncio.subprocess.PIPE,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Track PID for zombie cleanup in case of abnormal termination
|
|
100
|
+
from reversecore_mcp.core.resource_manager import resource_manager
|
|
101
|
+
resource_manager.track_pid(process.pid)
|
|
102
|
+
|
|
103
|
+
except FileNotFoundError:
|
|
104
|
+
# Extract command name from cmd list
|
|
105
|
+
tool_name = cmd[0] if cmd else "unknown"
|
|
106
|
+
raise ToolNotFoundError(tool_name)
|
|
107
|
+
|
|
108
|
+
# Read output in chunks
|
|
109
|
+
output_chunks = []
|
|
110
|
+
stderr_chunks = []
|
|
111
|
+
bytes_read = 0
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
# Read output in chunks with timeout checking
|
|
115
|
+
async def read_stream():
|
|
116
|
+
"""Read stdout in chunks until EOF or size limit."""
|
|
117
|
+
nonlocal bytes_read
|
|
118
|
+
chunk_size = 8192 # 8KB chunks
|
|
119
|
+
|
|
120
|
+
while True:
|
|
121
|
+
chunk = await process.stdout.read(chunk_size)
|
|
122
|
+
if not chunk:
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
# Decode chunk
|
|
126
|
+
decoded_chunk = chunk.decode(encoding, errors=errors)
|
|
127
|
+
chunk_bytes = len(chunk)
|
|
128
|
+
bytes_read += chunk_bytes
|
|
129
|
+
|
|
130
|
+
# Only append if we haven't exceeded the limit
|
|
131
|
+
if bytes_read <= max_output_size:
|
|
132
|
+
output_chunks.append(decoded_chunk)
|
|
133
|
+
|
|
134
|
+
# Wait for process to complete with timeout
|
|
135
|
+
# Wait for process to complete with timeout
|
|
136
|
+
try:
|
|
137
|
+
await asyncio.wait_for(read_stream(), timeout=timeout)
|
|
138
|
+
await asyncio.wait_for(process.wait(), timeout=1.0)
|
|
139
|
+
except asyncio.TimeoutError:
|
|
140
|
+
logger.warning(f"Command timed out after {timeout}s: {' '.join(cmd)}")
|
|
141
|
+
raise ExecutionTimeoutError(timeout)
|
|
142
|
+
finally:
|
|
143
|
+
# Critical: Ensure process is terminated to prevent zombies
|
|
144
|
+
if process.returncode is None:
|
|
145
|
+
try:
|
|
146
|
+
process.kill()
|
|
147
|
+
# Wait for process to die to reap the zombie
|
|
148
|
+
# We can't await indefinitely here, but usually kill is fast
|
|
149
|
+
try:
|
|
150
|
+
await asyncio.wait_for(process.wait(), timeout=2.0)
|
|
151
|
+
except asyncio.TimeoutError:
|
|
152
|
+
logger.error(f"Process {process.pid} refused to die after kill")
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.error(f"Failed to kill process {process.pid}: {e}")
|
|
155
|
+
|
|
156
|
+
# Read any remaining stderr
|
|
157
|
+
stderr_data = await process.stderr.read()
|
|
158
|
+
if stderr_data:
|
|
159
|
+
stderr_chunks.append(stderr_data.decode(encoding, errors=errors))
|
|
160
|
+
|
|
161
|
+
# Combine output chunks
|
|
162
|
+
output_text = "".join(output_chunks)
|
|
163
|
+
|
|
164
|
+
# Check if output was truncated
|
|
165
|
+
if bytes_read > max_output_size:
|
|
166
|
+
truncation_warning = (
|
|
167
|
+
f"\n\n[WARNING: Output truncated at {max_output_size} bytes. "
|
|
168
|
+
f"Total output size: {bytes_read} bytes]"
|
|
169
|
+
)
|
|
170
|
+
output_text += truncation_warning
|
|
171
|
+
|
|
172
|
+
# If process failed, raise CalledProcessError with stderr
|
|
173
|
+
if process.returncode != 0:
|
|
174
|
+
stderr_text = "".join(stderr_chunks)
|
|
175
|
+
raise subprocess.CalledProcessError(
|
|
176
|
+
process.returncode, cmd, output=output_text, stderr=stderr_text
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return output_text, bytes_read
|
|
180
|
+
|
|
181
|
+
except ImportError:
|
|
182
|
+
# Re-raise import errors (e.g. from missing dependencies)
|
|
183
|
+
raise
|
|
184
|
+
except Exception as e:
|
|
185
|
+
if not isinstance(e, (ToolNotFoundError, ExecutionTimeoutError, subprocess.CalledProcessError)):
|
|
186
|
+
logger.error(f"Command execution failed: {e}")
|
|
187
|
+
raise
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def execute_subprocess_streaming(
|
|
191
|
+
cmd: list[str],
|
|
192
|
+
max_output_size: int = 10_000_000, # 10 MB default
|
|
193
|
+
timeout: int = 300, # 5 minutes default
|
|
194
|
+
encoding: str = "utf-8",
|
|
195
|
+
errors: str = "replace",
|
|
196
|
+
) -> tuple[str, int]:
|
|
197
|
+
"""
|
|
198
|
+
Execute a subprocess command with streaming output and size limits.
|
|
199
|
+
|
|
200
|
+
This is a synchronous wrapper around execute_subprocess_async that provides
|
|
201
|
+
backward compatibility. It uses asyncio.run() to execute the async version.
|
|
202
|
+
|
|
203
|
+
This function uses asyncio to stream output in chunks, preventing
|
|
204
|
+
OOM issues when processing large files and avoiding CPU polling.
|
|
205
|
+
Output is truncated if it exceeds max_output_size.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
cmd: Command and arguments as a list (e.g., ["r2", "-q", "-c", "pdf @ main", "file.exe"])
|
|
209
|
+
max_output_size: Maximum output size in bytes (default: 10MB)
|
|
210
|
+
timeout: Maximum execution time in seconds (default: 300)
|
|
211
|
+
encoding: Text encoding for output (default: "utf-8")
|
|
212
|
+
errors: Error handling for encoding (default: "replace")
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Tuple of (output_text, bytes_read)
|
|
216
|
+
- output_text: The captured output (truncated if limit exceeded)
|
|
217
|
+
- bytes_read: Total bytes read (may exceed max_output_size if truncated)
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
ToolNotFoundError: If the command executable is not found
|
|
221
|
+
ExecutionTimeoutError: If the command exceeds the timeout
|
|
222
|
+
subprocess.CalledProcessError: If the command returns non-zero exit code
|
|
223
|
+
"""
|
|
224
|
+
coro = execute_subprocess_async(
|
|
225
|
+
cmd,
|
|
226
|
+
max_output_size=max_output_size,
|
|
227
|
+
timeout=timeout,
|
|
228
|
+
encoding=encoding,
|
|
229
|
+
errors=errors,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
running_loop = asyncio.get_running_loop()
|
|
234
|
+
except RuntimeError:
|
|
235
|
+
running_loop = None
|
|
236
|
+
|
|
237
|
+
if running_loop and running_loop.is_running():
|
|
238
|
+
return _get_background_runner().run(coro)
|
|
239
|
+
|
|
240
|
+
return asyncio.run(coro)
|