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,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Security utilities for input validation and sanitization.
|
|
3
|
+
|
|
4
|
+
This module provides functions to validate and sanitize user inputs before
|
|
5
|
+
they are used in subprocess calls, preventing command injection and
|
|
6
|
+
unauthorized file access.
|
|
7
|
+
|
|
8
|
+
Note: For radare2 command validation, use the improved regex-based validation
|
|
9
|
+
in command_spec.py which provides stronger security guarantees.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import threading
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from functools import lru_cache
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from reversecore_mcp.core.config import get_config
|
|
18
|
+
from reversecore_mcp.core.exceptions import ValidationError
|
|
19
|
+
|
|
20
|
+
# Configuration constants
|
|
21
|
+
PATH_VALIDATION_CACHE_SIZE = 256 # Number of path resolutions to cache
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class WorkspaceConfig:
|
|
26
|
+
"""Immutable configuration for workspace-aware file validation."""
|
|
27
|
+
|
|
28
|
+
workspace: Path
|
|
29
|
+
read_only_dirs: tuple[Path, ...]
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_env(cls) -> "WorkspaceConfig":
|
|
33
|
+
"""Create a workspace configuration from the cached Config instance."""
|
|
34
|
+
config = get_config()
|
|
35
|
+
return cls(workspace=config.workspace, read_only_dirs=config.read_only_dirs)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Lazy-initialized workspace configuration with thread safety
|
|
39
|
+
# This avoids initialization errors when the module is imported before
|
|
40
|
+
# environment variables are set (common in test fixtures)
|
|
41
|
+
_WORKSPACE_CONFIG: WorkspaceConfig | None = None
|
|
42
|
+
_WORKSPACE_CONFIG_LOCK = threading.Lock()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_workspace_config() -> WorkspaceConfig:
|
|
46
|
+
"""
|
|
47
|
+
Get the workspace configuration, initializing it lazily on first access.
|
|
48
|
+
|
|
49
|
+
This lazy initialization pattern allows tests to set up environment
|
|
50
|
+
variables or mock configurations before the first access.
|
|
51
|
+
Thread-safe via double-checked locking pattern.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
WorkspaceConfig instance
|
|
55
|
+
"""
|
|
56
|
+
global _WORKSPACE_CONFIG
|
|
57
|
+
if _WORKSPACE_CONFIG is None:
|
|
58
|
+
with _WORKSPACE_CONFIG_LOCK:
|
|
59
|
+
if _WORKSPACE_CONFIG is None:
|
|
60
|
+
_WORKSPACE_CONFIG = WorkspaceConfig.from_env()
|
|
61
|
+
return _WORKSPACE_CONFIG
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def refresh_workspace_config() -> WorkspaceConfig:
|
|
65
|
+
"""Recompute the default workspace configuration (mainly for tests)."""
|
|
66
|
+
global _WORKSPACE_CONFIG
|
|
67
|
+
with _WORKSPACE_CONFIG_LOCK:
|
|
68
|
+
_WORKSPACE_CONFIG = WorkspaceConfig.from_env()
|
|
69
|
+
# Clear the path resolution cache when config changes
|
|
70
|
+
_resolve_path_cached.cache_clear()
|
|
71
|
+
return _WORKSPACE_CONFIG
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def reset_workspace_config() -> None:
|
|
75
|
+
"""
|
|
76
|
+
Reset the workspace configuration to uninitialized state.
|
|
77
|
+
|
|
78
|
+
This is useful for tests that need to change environment variables
|
|
79
|
+
and have the configuration re-read on next access.
|
|
80
|
+
"""
|
|
81
|
+
global _WORKSPACE_CONFIG
|
|
82
|
+
with _WORKSPACE_CONFIG_LOCK:
|
|
83
|
+
_WORKSPACE_CONFIG = None
|
|
84
|
+
_resolve_path_cached.cache_clear()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@lru_cache(maxsize=PATH_VALIDATION_CACHE_SIZE)
|
|
88
|
+
def _resolve_path_cached(path_str: str) -> tuple[Path, bool, str]:
|
|
89
|
+
"""
|
|
90
|
+
Cached path resolution to avoid repeated filesystem calls.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Tuple of (resolved_path, is_file, error_message)
|
|
94
|
+
If resolution fails, returns (original_path, False, error_message)
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
file_path = Path(path_str)
|
|
98
|
+
abs_path = file_path.resolve(strict=True)
|
|
99
|
+
is_file = abs_path.is_file()
|
|
100
|
+
return (abs_path, is_file, "")
|
|
101
|
+
except (OSError, RuntimeError) as e:
|
|
102
|
+
return (Path(path_str), False, str(e))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def validate_file_path(
|
|
106
|
+
path: str,
|
|
107
|
+
read_only: bool = False,
|
|
108
|
+
config: WorkspaceConfig | None = None,
|
|
109
|
+
) -> Path:
|
|
110
|
+
"""
|
|
111
|
+
Validate and normalize a file path.
|
|
112
|
+
|
|
113
|
+
This function ensures that:
|
|
114
|
+
1. The path exists and points to a file (not a directory)
|
|
115
|
+
2. The path is within the allowed workspace directory (REVERSECORE_WORKSPACE)
|
|
116
|
+
or within allowed read-only directories (if read_only=True)
|
|
117
|
+
3. The path is resolved to an absolute path
|
|
118
|
+
|
|
119
|
+
The workspace directory is determined by an immutable WorkspaceConfig that
|
|
120
|
+
is loaded once from environment variables (REVERSECORE_WORKSPACE and
|
|
121
|
+
REVERSECORE_READ_DIRS).
|
|
122
|
+
|
|
123
|
+
Performance: Uses LRU cache for path resolution to avoid repeated
|
|
124
|
+
filesystem calls for frequently accessed files.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
path: The file path to validate
|
|
128
|
+
read_only: If True, also allow files from configured read-only directories
|
|
129
|
+
config: Optional WorkspaceConfig override (useful for tests)
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
The normalized absolute file path as a Path instance
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ValueError: If the path is invalid, doesn't exist, or is outside
|
|
136
|
+
the allowed directories
|
|
137
|
+
"""
|
|
138
|
+
active_config = config or get_workspace_config()
|
|
139
|
+
|
|
140
|
+
# Handle relative paths: resolve them relative to workspace directory
|
|
141
|
+
# This allows users to specify just the filename (e.g., "sample.exe")
|
|
142
|
+
# instead of the full path ("/app/workspace/sample.exe")
|
|
143
|
+
file_path = Path(path)
|
|
144
|
+
|
|
145
|
+
# Defense against AI mistakes: if a host-side absolute path is passed
|
|
146
|
+
# (e.g., "/Users/john/Reversecore_Workspace/sample.exe"), extract just
|
|
147
|
+
# the filename and try to find it in the workspace directory.
|
|
148
|
+
# This handles cases where AI ignores the prompt instructions.
|
|
149
|
+
if file_path.is_absolute() and not str(file_path).startswith(str(active_config.workspace)):
|
|
150
|
+
# Path is absolute but not in workspace - likely a host path
|
|
151
|
+
# Extract filename and try workspace
|
|
152
|
+
filename_only = file_path.name
|
|
153
|
+
workspace_path = active_config.workspace / filename_only
|
|
154
|
+
if workspace_path.exists():
|
|
155
|
+
path = str(workspace_path)
|
|
156
|
+
# If not found, continue with original path (will error with helpful message)
|
|
157
|
+
|
|
158
|
+
if not file_path.is_absolute():
|
|
159
|
+
# Try workspace-relative path first
|
|
160
|
+
workspace_path = active_config.workspace / path
|
|
161
|
+
if workspace_path.exists():
|
|
162
|
+
path = str(workspace_path)
|
|
163
|
+
|
|
164
|
+
# Use cached path resolution to avoid repeated filesystem calls
|
|
165
|
+
abs_path, is_file, error = _resolve_path_cached(path)
|
|
166
|
+
|
|
167
|
+
if error:
|
|
168
|
+
raise ValidationError(
|
|
169
|
+
f"Invalid file path: {path}. Error: {error}",
|
|
170
|
+
details={
|
|
171
|
+
"path": path,
|
|
172
|
+
"error": error,
|
|
173
|
+
"hint": "Ensure the file is in the workspace directory",
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Check that it's a file, not a directory
|
|
178
|
+
if not is_file:
|
|
179
|
+
raise ValidationError(
|
|
180
|
+
f"Path does not point to a file: {abs_path}",
|
|
181
|
+
details={"path": str(abs_path)},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def _is_relative_to(base: Path) -> bool:
|
|
185
|
+
try:
|
|
186
|
+
abs_path.relative_to(base)
|
|
187
|
+
return True
|
|
188
|
+
except ValueError:
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
is_in_workspace = _is_relative_to(active_config.workspace)
|
|
192
|
+
if is_in_workspace and not read_only:
|
|
193
|
+
return abs_path
|
|
194
|
+
|
|
195
|
+
is_in_read_dir = False
|
|
196
|
+
if read_only and not is_in_workspace:
|
|
197
|
+
for read_dir in active_config.read_only_dirs:
|
|
198
|
+
if _is_relative_to(read_dir):
|
|
199
|
+
is_in_read_dir = True
|
|
200
|
+
break
|
|
201
|
+
|
|
202
|
+
if not (is_in_workspace or is_in_read_dir):
|
|
203
|
+
allowed_dirs = [str(active_config.workspace)]
|
|
204
|
+
if read_only:
|
|
205
|
+
allowed_dirs.extend(str(d) for d in active_config.read_only_dirs)
|
|
206
|
+
raise ValidationError(
|
|
207
|
+
f"File path is outside allowed directories: {abs_path}. "
|
|
208
|
+
f"Allowed directories: {allowed_dirs}. "
|
|
209
|
+
f"Set REVERSECORE_WORKSPACE or REVERSECORE_READ_DIRS environment variables to change allowed paths.",
|
|
210
|
+
details={"path": str(abs_path), "allowed_directories": allowed_dirs},
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return abs_path
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Input validators for tool-specific parameters.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from reversecore_mcp.core.config import get_config
|
|
9
|
+
from reversecore_mcp.core.exceptions import ValidationError
|
|
10
|
+
|
|
11
|
+
# Pre-compile address validation pattern for performance
|
|
12
|
+
# Allow alphanumeric, dots, underscores, and C++ symbol chars (:, <, >, ~, *, &, [, ], (, ), -, ,, @)
|
|
13
|
+
_ADDRESS_PATTERN = re.compile(r"^[a-zA-Z0-9_:.<>~*&\[\]()\-,@]+$")
|
|
14
|
+
|
|
15
|
+
# OPTIMIZATION: Pre-compile pattern for hex prefix removal
|
|
16
|
+
_HEX_PREFIX_PATTERN = re.compile(r"^0[xX]")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def validate_address_format(address: str, param_name: str = "address") -> None:
|
|
20
|
+
"""
|
|
21
|
+
Validate address format to prevent shell injection.
|
|
22
|
+
|
|
23
|
+
Ensures the address contains only safe characters: alphanumeric, dots,
|
|
24
|
+
underscores, and optional '0x' prefix.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
address: The address string to validate (e.g., 'main', '0x401000', 'sym.decrypt')
|
|
28
|
+
param_name: Name of the parameter for error messages (default: 'address')
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
ValidationError: If address format is invalid
|
|
32
|
+
"""
|
|
33
|
+
# OPTIMIZATION: Use pre-compiled regex pattern instead of replace
|
|
34
|
+
clean_address = _HEX_PREFIX_PATTERN.sub("", address)
|
|
35
|
+
|
|
36
|
+
if not _ADDRESS_PATTERN.match(clean_address):
|
|
37
|
+
raise ValidationError(
|
|
38
|
+
f"{param_name} must contain only safe characters (alphanumeric, dots, underscores, "
|
|
39
|
+
"and C++ symbol characters like :, <, >)"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def validate_tool_parameters(tool_name: str, params: dict[str, Any]) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Validate tool-specific parameters.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
tool_name: Name of the tool
|
|
49
|
+
params: Parameters to validate
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
ValidationError: If parameters are invalid
|
|
53
|
+
"""
|
|
54
|
+
validators = {
|
|
55
|
+
"run_strings": _validate_strings_params,
|
|
56
|
+
"run_radare2": _validate_radare2_params,
|
|
57
|
+
"disassemble_with_capstone": _validate_capstone_params,
|
|
58
|
+
"run_yara": _validate_yara_params,
|
|
59
|
+
"generate_function_graph": _validate_cfg_params,
|
|
60
|
+
"emulate_machine_code": _validate_emulation_params,
|
|
61
|
+
"get_pseudo_code": _validate_pseudo_code_params,
|
|
62
|
+
"generate_signature": _validate_signature_params,
|
|
63
|
+
"extract_rtti_info": _validate_rtti_params,
|
|
64
|
+
"smart_decompile": _validate_decompile_params,
|
|
65
|
+
"generate_yara_rule": _validate_yara_generation_params,
|
|
66
|
+
"diff_binaries": _validate_diff_binaries_params,
|
|
67
|
+
"match_libraries": _validate_match_libraries_params,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if tool_name in validators:
|
|
71
|
+
validators[tool_name](params)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _validate_strings_params(params: dict[str, Any]) -> None:
|
|
75
|
+
"""Validate run_strings parameters."""
|
|
76
|
+
min_length = params.get("min_length", 4)
|
|
77
|
+
if not isinstance(min_length, int) or min_length < 1:
|
|
78
|
+
raise ValidationError("min_length must be a positive integer")
|
|
79
|
+
|
|
80
|
+
max_output_size = params.get("max_output_size", get_config().max_output_size)
|
|
81
|
+
if not isinstance(max_output_size, int) or max_output_size < 1:
|
|
82
|
+
raise ValidationError("max_output_size must be a positive integer")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _validate_radare2_params(params: dict[str, Any]) -> None:
|
|
86
|
+
"""Validate run_radare2 parameters."""
|
|
87
|
+
if "r2_command" not in params:
|
|
88
|
+
raise ValidationError("r2_command is required")
|
|
89
|
+
|
|
90
|
+
if not isinstance(params["r2_command"], str):
|
|
91
|
+
raise ValidationError("r2_command must be a string")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _validate_capstone_params(params: dict[str, Any]) -> None:
|
|
95
|
+
"""Validate disassemble_with_capstone parameters."""
|
|
96
|
+
offset = params.get("offset", 0)
|
|
97
|
+
if not isinstance(offset, int) or offset < 0:
|
|
98
|
+
raise ValidationError("offset must be a non-negative integer")
|
|
99
|
+
|
|
100
|
+
size = params.get("size", 1024)
|
|
101
|
+
if not isinstance(size, int) or size < 1:
|
|
102
|
+
raise ValidationError("size must be a positive integer")
|
|
103
|
+
|
|
104
|
+
# Note: Architecture validation is done by the tool function itself
|
|
105
|
+
# to provide more detailed error messages
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _validate_yara_params(params: dict[str, Any]) -> None:
|
|
109
|
+
"""Validate run_yara parameters."""
|
|
110
|
+
if "rule_file" not in params:
|
|
111
|
+
raise ValidationError("rule_file is required")
|
|
112
|
+
|
|
113
|
+
timeout = params.get("timeout", 300)
|
|
114
|
+
if not isinstance(timeout, int) or timeout < 1:
|
|
115
|
+
raise ValidationError("timeout must be a positive integer")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _validate_cfg_params(params: dict[str, Any]) -> None:
|
|
119
|
+
"""Validate generate_function_graph parameters."""
|
|
120
|
+
if "function_address" in params:
|
|
121
|
+
if not isinstance(params["function_address"], str):
|
|
122
|
+
raise ValidationError("function_address must be a string")
|
|
123
|
+
|
|
124
|
+
output_format = params.get("format", "mermaid")
|
|
125
|
+
allowed_formats = ["json", "mermaid", "dot"]
|
|
126
|
+
if output_format not in allowed_formats:
|
|
127
|
+
raise ValidationError(
|
|
128
|
+
f"Invalid format '{output_format}'. Allowed: {', '.join(allowed_formats)}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _validate_emulation_params(params: dict[str, Any]) -> None:
|
|
133
|
+
"""Validate emulate_machine_code parameters."""
|
|
134
|
+
if "start_address" in params:
|
|
135
|
+
if not isinstance(params["start_address"], str):
|
|
136
|
+
raise ValidationError("start_address must be a string")
|
|
137
|
+
|
|
138
|
+
instructions = params.get("instructions", 50)
|
|
139
|
+
if not isinstance(instructions, int):
|
|
140
|
+
raise ValidationError("instructions must be an integer")
|
|
141
|
+
|
|
142
|
+
# Critical: Prevent infinite loops and CPU exhaustion
|
|
143
|
+
if instructions < 1:
|
|
144
|
+
raise ValidationError("instructions must be at least 1")
|
|
145
|
+
|
|
146
|
+
max_instructions = get_config().max_emulation_instructions
|
|
147
|
+
if instructions > max_instructions:
|
|
148
|
+
raise ValidationError(
|
|
149
|
+
f"instructions cannot exceed {max_instructions} (safety limit to prevent CPU exhaustion)"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _validate_pseudo_code_params(params: dict[str, Any]) -> None:
|
|
154
|
+
"""Validate get_pseudo_code parameters."""
|
|
155
|
+
if "address" in params:
|
|
156
|
+
if not isinstance(params["address"], str):
|
|
157
|
+
raise ValidationError("address must be a string")
|
|
158
|
+
|
|
159
|
+
timeout = params.get("timeout", 300)
|
|
160
|
+
if not isinstance(timeout, int) or timeout < 1:
|
|
161
|
+
raise ValidationError("timeout must be a positive integer")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _validate_signature_params(params: dict[str, Any]) -> None:
|
|
165
|
+
"""Validate generate_signature parameters."""
|
|
166
|
+
if "address" not in params:
|
|
167
|
+
raise ValidationError("address is required")
|
|
168
|
+
|
|
169
|
+
if not isinstance(params["address"], str):
|
|
170
|
+
raise ValidationError("address must be a string")
|
|
171
|
+
|
|
172
|
+
length = params.get("length", 32)
|
|
173
|
+
if not isinstance(length, int) or length < 1 or length > 1024:
|
|
174
|
+
raise ValidationError("length must be between 1 and 1024 bytes")
|
|
175
|
+
|
|
176
|
+
timeout = params.get("timeout", 300)
|
|
177
|
+
if not isinstance(timeout, int) or timeout < 1:
|
|
178
|
+
raise ValidationError("timeout must be a positive integer")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _validate_rtti_params(params: dict[str, Any]) -> None:
|
|
182
|
+
"""Validate extract_rtti_info parameters."""
|
|
183
|
+
timeout = params.get("timeout", 300)
|
|
184
|
+
if not isinstance(timeout, int) or timeout < 1:
|
|
185
|
+
raise ValidationError("timeout must be a positive integer")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _validate_decompile_params(params: dict[str, Any]) -> None:
|
|
189
|
+
"""Validate smart_decompile parameters."""
|
|
190
|
+
if "function_address" in params:
|
|
191
|
+
if not isinstance(params["function_address"], str):
|
|
192
|
+
raise ValidationError("function_address must be a string")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _validate_yara_generation_params(params: dict[str, Any]) -> None:
|
|
196
|
+
"""Validate generate_yara_rule parameters."""
|
|
197
|
+
if "function_address" in params:
|
|
198
|
+
if not isinstance(params["function_address"], str):
|
|
199
|
+
raise ValidationError("function_address must be a string")
|
|
200
|
+
|
|
201
|
+
if "byte_length" in params:
|
|
202
|
+
byte_length = params["byte_length"]
|
|
203
|
+
if not isinstance(byte_length, int):
|
|
204
|
+
raise ValidationError("byte_length must be a positive integer")
|
|
205
|
+
if byte_length < 1:
|
|
206
|
+
raise ValidationError("byte_length must be a positive integer")
|
|
207
|
+
if byte_length > 1024:
|
|
208
|
+
raise ValidationError("byte_length cannot exceed 1024")
|
|
209
|
+
|
|
210
|
+
if "rule_name" in params:
|
|
211
|
+
if not isinstance(params["rule_name"], str):
|
|
212
|
+
raise ValidationError("rule_name must be a string")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _validate_diff_binaries_params(params: dict[str, Any]) -> None:
|
|
216
|
+
"""Validate diff_binaries parameters."""
|
|
217
|
+
if "function_name" in params and params["function_name"] is not None:
|
|
218
|
+
if not isinstance(params["function_name"], str):
|
|
219
|
+
raise ValidationError("function_name must be a string")
|
|
220
|
+
|
|
221
|
+
max_output_size = params.get("max_output_size", 10_000_000)
|
|
222
|
+
if not isinstance(max_output_size, int) or max_output_size < 1:
|
|
223
|
+
raise ValidationError("max_output_size must be a positive integer")
|
|
224
|
+
|
|
225
|
+
timeout = params.get("timeout", 300)
|
|
226
|
+
if not isinstance(timeout, int) or timeout < 1:
|
|
227
|
+
raise ValidationError("timeout must be a positive integer")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _validate_match_libraries_params(params: dict[str, Any]) -> None:
|
|
231
|
+
"""Validate match_libraries parameters."""
|
|
232
|
+
max_output_size = params.get("max_output_size", 10_000_000)
|
|
233
|
+
if not isinstance(max_output_size, int) or max_output_size < 1:
|
|
234
|
+
raise ValidationError("max_output_size must be a positive integer")
|
|
235
|
+
|
|
236
|
+
timeout = params.get("timeout", 300)
|
|
237
|
+
if not isinstance(timeout, int) or timeout < 1:
|
|
238
|
+
raise ValidationError("timeout must be a positive integer")
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Web Dashboard for Reversecore MCP.
|
|
3
|
+
|
|
4
|
+
Provides a visual interface for binary analysis using FastAPI + Jinja2.
|
|
5
|
+
|
|
6
|
+
SECURITY NOTES:
|
|
7
|
+
- All user-provided data (filenames, binary strings) is auto-escaped by Jinja2
|
|
8
|
+
- Path traversal protection via validate_file_path()
|
|
9
|
+
- CSRF tokens required for state-changing operations
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import html
|
|
13
|
+
import secrets
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from fastapi import APIRouter, Request
|
|
17
|
+
from fastapi.responses import HTMLResponse
|
|
18
|
+
from fastapi.staticfiles import StaticFiles
|
|
19
|
+
from fastapi.templating import Jinja2Templates
|
|
20
|
+
|
|
21
|
+
# Setup paths
|
|
22
|
+
DASHBOARD_DIR = Path(__file__).parent
|
|
23
|
+
TEMPLATES_DIR = DASHBOARD_DIR / "templates"
|
|
24
|
+
STATIC_DIR = DASHBOARD_DIR / "static"
|
|
25
|
+
|
|
26
|
+
# Create router
|
|
27
|
+
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
|
28
|
+
|
|
29
|
+
# Setup templates with auto-escaping enabled (default)
|
|
30
|
+
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
|
31
|
+
|
|
32
|
+
# CSRF token storage (in production, use Redis or database)
|
|
33
|
+
_csrf_tokens: dict[str, str] = {}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _generate_csrf_token(session_id: str) -> str:
|
|
37
|
+
"""Generate a CSRF token for a session."""
|
|
38
|
+
token = secrets.token_urlsafe(32)
|
|
39
|
+
_csrf_tokens[session_id] = token
|
|
40
|
+
return token
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _verify_csrf_token(session_id: str, token: str) -> bool:
|
|
44
|
+
"""Verify a CSRF token."""
|
|
45
|
+
expected = _csrf_tokens.get(session_id)
|
|
46
|
+
return expected is not None and secrets.compare_digest(expected, token)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _sanitize_for_display(text: str, max_length: int = 1000) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Sanitize binary-extracted text for safe display.
|
|
52
|
+
|
|
53
|
+
This is a defense-in-depth measure on top of Jinja2's auto-escaping.
|
|
54
|
+
"""
|
|
55
|
+
if not isinstance(text, str):
|
|
56
|
+
text = str(text)
|
|
57
|
+
# Truncate long strings
|
|
58
|
+
if len(text) > max_length:
|
|
59
|
+
text = text[:max_length] + "... [truncated]"
|
|
60
|
+
# HTML escape (Jinja2 does this, but we double-check for safety)
|
|
61
|
+
return html.escape(text)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_router() -> APIRouter:
|
|
65
|
+
"""Get the dashboard router."""
|
|
66
|
+
return router
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_static_files() -> StaticFiles:
|
|
70
|
+
"""Get static files mount."""
|
|
71
|
+
return StaticFiles(directory=str(STATIC_DIR))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@router.get("/", response_class=HTMLResponse)
|
|
75
|
+
async def dashboard_index(request: Request):
|
|
76
|
+
"""Dashboard overview page."""
|
|
77
|
+
from reversecore_mcp.core.config import get_config
|
|
78
|
+
|
|
79
|
+
settings = get_config()
|
|
80
|
+
workspace = settings.workspace
|
|
81
|
+
|
|
82
|
+
# Get list of files in workspace
|
|
83
|
+
files = []
|
|
84
|
+
if workspace.exists():
|
|
85
|
+
for f in workspace.iterdir():
|
|
86
|
+
if f.is_file() and not f.name.startswith("."):
|
|
87
|
+
stat = f.stat()
|
|
88
|
+
# Sanitize filename for display (defense in depth)
|
|
89
|
+
files.append(
|
|
90
|
+
{
|
|
91
|
+
"name": _sanitize_for_display(f.name, 255),
|
|
92
|
+
"name_raw": f.name, # For URL construction
|
|
93
|
+
"size": stat.st_size,
|
|
94
|
+
"modified": stat.st_mtime,
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Sort by modified time (newest first)
|
|
99
|
+
files.sort(key=lambda x: x["modified"], reverse=True)
|
|
100
|
+
|
|
101
|
+
return templates.TemplateResponse(
|
|
102
|
+
"index.html",
|
|
103
|
+
{
|
|
104
|
+
"request": request,
|
|
105
|
+
"files": files,
|
|
106
|
+
"workspace": str(workspace),
|
|
107
|
+
"file_count": len(files),
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@router.get("/analysis/{filename}", response_class=HTMLResponse)
|
|
113
|
+
async def dashboard_analysis(request: Request, filename: str):
|
|
114
|
+
"""Analysis page for a specific file."""
|
|
115
|
+
from reversecore_mcp.core.config import get_config
|
|
116
|
+
from reversecore_mcp.core.security import validate_file_path
|
|
117
|
+
|
|
118
|
+
settings = get_config()
|
|
119
|
+
file_path = settings.workspace / filename
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
validated_path = validate_file_path(str(file_path))
|
|
123
|
+
except Exception as e:
|
|
124
|
+
return templates.TemplateResponse(
|
|
125
|
+
"error.html",
|
|
126
|
+
{"request": request, "error": _sanitize_for_display(str(e))},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Get basic file info
|
|
130
|
+
file_info = {
|
|
131
|
+
"name": _sanitize_for_display(validated_path.name, 255),
|
|
132
|
+
"path": str(validated_path),
|
|
133
|
+
"size": validated_path.stat().st_size,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Try to get functions list
|
|
137
|
+
functions = []
|
|
138
|
+
disasm = ""
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
from reversecore_mcp.tools.radare2.r2_session import R2Session
|
|
142
|
+
|
|
143
|
+
session = R2Session(str(validated_path))
|
|
144
|
+
session.analyze(level=1)
|
|
145
|
+
|
|
146
|
+
# Get functions
|
|
147
|
+
funcs_json = session.cmdj("aflj") or []
|
|
148
|
+
for func in funcs_json[:50]: # Limit to 50
|
|
149
|
+
# SECURITY: Sanitize function names from binary
|
|
150
|
+
functions.append(
|
|
151
|
+
{
|
|
152
|
+
"name": _sanitize_for_display(func.get("name", "unknown"), 100),
|
|
153
|
+
"offset": hex(func.get("offset", 0)),
|
|
154
|
+
"size": func.get("size", 0),
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Get entry point disassembly
|
|
159
|
+
raw_disasm = session.cmd("pdf @ entry0") or "No disassembly available"
|
|
160
|
+
# SECURITY: Sanitize disassembly output
|
|
161
|
+
disasm = _sanitize_for_display(raw_disasm, 50000)
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
disasm = f"Error: {_sanitize_for_display(str(e))}"
|
|
165
|
+
|
|
166
|
+
return templates.TemplateResponse(
|
|
167
|
+
"analysis.html",
|
|
168
|
+
{
|
|
169
|
+
"request": request,
|
|
170
|
+
"file": file_info,
|
|
171
|
+
"functions": functions,
|
|
172
|
+
"disasm": disasm,
|
|
173
|
+
},
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@router.get("/iocs/{filename}", response_class=HTMLResponse)
|
|
178
|
+
async def dashboard_iocs(request: Request, filename: str):
|
|
179
|
+
"""IOC extraction page."""
|
|
180
|
+
from reversecore_mcp.core.config import get_config
|
|
181
|
+
from reversecore_mcp.core.security import validate_file_path
|
|
182
|
+
|
|
183
|
+
settings = get_config()
|
|
184
|
+
file_path = settings.workspace / filename
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
validated_path = validate_file_path(str(file_path))
|
|
188
|
+
except Exception as e:
|
|
189
|
+
return templates.TemplateResponse(
|
|
190
|
+
"error.html",
|
|
191
|
+
{"request": request, "error": _sanitize_for_display(str(e))},
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Extract IOCs
|
|
195
|
+
iocs: dict = {"urls": [], "ips": [], "emails": [], "strings": []}
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
from reversecore_mcp.tools.malware.ioc_tools import extract_iocs
|
|
199
|
+
|
|
200
|
+
result = await extract_iocs(str(validated_path))
|
|
201
|
+
if result.status == "success" and isinstance(result.data, dict):
|
|
202
|
+
raw_iocs = result.data
|
|
203
|
+
# SECURITY: Sanitize all IOC values extracted from binary
|
|
204
|
+
iocs["urls"] = [_sanitize_for_display(u, 500) for u in raw_iocs.get("urls", [])]
|
|
205
|
+
iocs["ips"] = [_sanitize_for_display(ip, 50) for ip in raw_iocs.get("ips", [])]
|
|
206
|
+
iocs["emails"] = [_sanitize_for_display(e, 100) for e in raw_iocs.get("emails", [])]
|
|
207
|
+
iocs["strings"] = [
|
|
208
|
+
_sanitize_for_display(s, 200) for s in raw_iocs.get("strings", [])[:100]
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
iocs["error"] = _sanitize_for_display(str(e))
|
|
213
|
+
|
|
214
|
+
return templates.TemplateResponse(
|
|
215
|
+
"iocs.html",
|
|
216
|
+
{
|
|
217
|
+
"request": request,
|
|
218
|
+
"filename": _sanitize_for_display(filename, 255),
|
|
219
|
+
"iocs": iocs,
|
|
220
|
+
},
|
|
221
|
+
)
|