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.
Files changed (79) hide show
  1. iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/METADATA +543 -0
  2. iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/RECORD +79 -0
  3. iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/WHEEL +5 -0
  4. iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/entry_points.txt +2 -0
  5. iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/licenses/LICENSE +21 -0
  6. iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/top_level.txt +1 -0
  7. reversecore_mcp/__init__.py +9 -0
  8. reversecore_mcp/core/__init__.py +78 -0
  9. reversecore_mcp/core/audit.py +101 -0
  10. reversecore_mcp/core/binary_cache.py +138 -0
  11. reversecore_mcp/core/command_spec.py +357 -0
  12. reversecore_mcp/core/config.py +432 -0
  13. reversecore_mcp/core/container.py +288 -0
  14. reversecore_mcp/core/decorators.py +152 -0
  15. reversecore_mcp/core/error_formatting.py +93 -0
  16. reversecore_mcp/core/error_handling.py +142 -0
  17. reversecore_mcp/core/evidence.py +229 -0
  18. reversecore_mcp/core/exceptions.py +296 -0
  19. reversecore_mcp/core/execution.py +240 -0
  20. reversecore_mcp/core/ghidra.py +642 -0
  21. reversecore_mcp/core/ghidra_helper.py +481 -0
  22. reversecore_mcp/core/ghidra_manager.py +234 -0
  23. reversecore_mcp/core/json_utils.py +131 -0
  24. reversecore_mcp/core/loader.py +73 -0
  25. reversecore_mcp/core/logging_config.py +206 -0
  26. reversecore_mcp/core/memory.py +721 -0
  27. reversecore_mcp/core/metrics.py +198 -0
  28. reversecore_mcp/core/mitre_mapper.py +365 -0
  29. reversecore_mcp/core/plugin.py +45 -0
  30. reversecore_mcp/core/r2_helpers.py +404 -0
  31. reversecore_mcp/core/r2_pool.py +403 -0
  32. reversecore_mcp/core/report_generator.py +268 -0
  33. reversecore_mcp/core/resilience.py +252 -0
  34. reversecore_mcp/core/resource_manager.py +169 -0
  35. reversecore_mcp/core/result.py +132 -0
  36. reversecore_mcp/core/security.py +213 -0
  37. reversecore_mcp/core/validators.py +238 -0
  38. reversecore_mcp/dashboard/__init__.py +221 -0
  39. reversecore_mcp/prompts/__init__.py +56 -0
  40. reversecore_mcp/prompts/common.py +24 -0
  41. reversecore_mcp/prompts/game.py +280 -0
  42. reversecore_mcp/prompts/malware.py +1219 -0
  43. reversecore_mcp/prompts/report.py +150 -0
  44. reversecore_mcp/prompts/security.py +136 -0
  45. reversecore_mcp/resources.py +329 -0
  46. reversecore_mcp/server.py +727 -0
  47. reversecore_mcp/tools/__init__.py +49 -0
  48. reversecore_mcp/tools/analysis/__init__.py +74 -0
  49. reversecore_mcp/tools/analysis/capa_tools.py +215 -0
  50. reversecore_mcp/tools/analysis/die_tools.py +180 -0
  51. reversecore_mcp/tools/analysis/diff_tools.py +643 -0
  52. reversecore_mcp/tools/analysis/lief_tools.py +272 -0
  53. reversecore_mcp/tools/analysis/signature_tools.py +591 -0
  54. reversecore_mcp/tools/analysis/static_analysis.py +479 -0
  55. reversecore_mcp/tools/common/__init__.py +58 -0
  56. reversecore_mcp/tools/common/file_operations.py +352 -0
  57. reversecore_mcp/tools/common/memory_tools.py +516 -0
  58. reversecore_mcp/tools/common/patch_explainer.py +230 -0
  59. reversecore_mcp/tools/common/server_tools.py +115 -0
  60. reversecore_mcp/tools/ghidra/__init__.py +19 -0
  61. reversecore_mcp/tools/ghidra/decompilation.py +975 -0
  62. reversecore_mcp/tools/ghidra/ghidra_tools.py +1052 -0
  63. reversecore_mcp/tools/malware/__init__.py +61 -0
  64. reversecore_mcp/tools/malware/adaptive_vaccine.py +579 -0
  65. reversecore_mcp/tools/malware/dormant_detector.py +756 -0
  66. reversecore_mcp/tools/malware/ioc_tools.py +228 -0
  67. reversecore_mcp/tools/malware/vulnerability_hunter.py +519 -0
  68. reversecore_mcp/tools/malware/yara_tools.py +214 -0
  69. reversecore_mcp/tools/patch_explainer.py +19 -0
  70. reversecore_mcp/tools/radare2/__init__.py +13 -0
  71. reversecore_mcp/tools/radare2/r2_analysis.py +972 -0
  72. reversecore_mcp/tools/radare2/r2_session.py +376 -0
  73. reversecore_mcp/tools/radare2/radare2_mcp_tools.py +1183 -0
  74. reversecore_mcp/tools/report/__init__.py +4 -0
  75. reversecore_mcp/tools/report/email.py +82 -0
  76. reversecore_mcp/tools/report/report_mcp_tools.py +344 -0
  77. reversecore_mcp/tools/report/report_tools.py +1076 -0
  78. reversecore_mcp/tools/report/session.py +194 -0
  79. 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
+ )