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,78 @@
1
+ """
2
+ Core utilities for Reversecore_MCP.
3
+
4
+ This package contains security, execution, and exception handling utilities
5
+ used across all tool modules.
6
+ """
7
+
8
+ # Import decorators and helpers for public API
9
+ # Import dependency injection container
10
+ from reversecore_mcp.core.container import (
11
+ ServiceContainer,
12
+ container,
13
+ get_ghidra_service,
14
+ get_r2_pool,
15
+ get_resource_manager,
16
+ )
17
+ from reversecore_mcp.core.decorators import log_execution
18
+ from reversecore_mcp.core.error_formatting import format_error, get_validation_hint
19
+ from reversecore_mcp.core.exceptions import (
20
+ ExecutionTimeoutError,
21
+ OutputLimitExceededError,
22
+ ReversecoreError,
23
+ ToolNotFoundError,
24
+ ValidationError,
25
+ )
26
+ from reversecore_mcp.core.execution import (
27
+ execute_subprocess_async,
28
+ execute_subprocess_streaming,
29
+ )
30
+ from reversecore_mcp.core.logging_config import get_logger, setup_logging
31
+
32
+ # Import shared R2 helper functions
33
+ from reversecore_mcp.core.r2_helpers import (
34
+ build_r2_cmd,
35
+ calculate_dynamic_timeout,
36
+ escape_mermaid_chars,
37
+ execute_r2_command,
38
+ parse_json_output,
39
+ strip_address_prefixes,
40
+ )
41
+
42
+ # Import performance optimization modules
43
+ from reversecore_mcp.core.r2_pool import R2ConnectionPool, r2_pool
44
+ from reversecore_mcp.core.resource_manager import ResourceManager, resource_manager
45
+ from reversecore_mcp.core.security import validate_file_path
46
+
47
+ __all__ = [
48
+ "ReversecoreError",
49
+ "ToolNotFoundError",
50
+ "ExecutionTimeoutError",
51
+ "OutputLimitExceededError",
52
+ "ValidationError",
53
+ "execute_subprocess_streaming",
54
+ "execute_subprocess_async",
55
+ "validate_file_path",
56
+ "format_error",
57
+ "get_validation_hint",
58
+ "get_logger",
59
+ "setup_logging",
60
+ "log_execution",
61
+ "R2ConnectionPool",
62
+ "r2_pool",
63
+ "ResourceManager",
64
+ "resource_manager",
65
+ # R2 helper functions
66
+ "strip_address_prefixes",
67
+ "escape_mermaid_chars",
68
+ "build_r2_cmd",
69
+ "execute_r2_command",
70
+ "parse_json_output",
71
+ "calculate_dynamic_timeout",
72
+ # Dependency injection
73
+ "ServiceContainer",
74
+ "container",
75
+ "get_r2_pool",
76
+ "get_resource_manager",
77
+ "get_ghidra_service",
78
+ ]
@@ -0,0 +1,101 @@
1
+ """
2
+ Audit Logging Module
3
+
4
+ Provides a tamper-evident record of security-critical actions (uploads, patches, etc.).
5
+ Logs are written to a separate file (audit.json) to distinguish from operational logs.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import time
11
+ from datetime import datetime
12
+ from enum import Enum
13
+ from pathlib import Path
14
+ from typing import Any, Optional
15
+
16
+ from reversecore_mcp.core.config import get_config
17
+
18
+
19
+ class AuditAction(str, Enum):
20
+ """Types of actions that require auditing."""
21
+
22
+ FILE_UPLOAD = "FILE_UPLOAD"
23
+ BINARY_PATCH = "BINARY_PATCH"
24
+ FILE_DELETE = "FILE_DELETE"
25
+ CONFIG_CHANGE = "CONFIG_CHANGE"
26
+ AUTH_FAILURE = "AUTH_FAILURE"
27
+
28
+
29
+ class AuditLogger:
30
+ """Specialized logger for security audit trails."""
31
+
32
+ _instance = None
33
+
34
+ def __new__(cls):
35
+ if cls._instance is None:
36
+ cls._instance = super(AuditLogger, cls).__new__(cls)
37
+ cls._instance._initialized = False
38
+ return cls._instance
39
+
40
+ def __init__(self):
41
+ if self._initialized:
42
+ return
43
+
44
+ settings = get_config()
45
+ self.log_file = settings.workspace.parent / "audit.json" # Store outside workspace if possible, or root
46
+ # If workspace is root, maybe put in /var/log/reversecore if possible?
47
+ # For Docker, settings.workspace is /app/workspace. Parent is /app.
48
+ # Let's verify permission. If not, fallback to workspace.
49
+
50
+ # Prepare file handler
51
+ self._logger = logging.getLogger("reversecore_audit")
52
+ self._logger.setLevel(logging.INFO)
53
+ self._logger.propagate = False
54
+
55
+ try:
56
+ handler = logging.FileHandler(str(self.log_file), encoding="utf-8")
57
+ formatter = logging.Formatter("%(message)s")
58
+ handler.setFormatter(formatter)
59
+ self._logger.addHandler(handler)
60
+ except Exception:
61
+ # Fallback to standard log if separate file fails
62
+ self._logger = logging.getLogger("reversecore_mcp")
63
+ self._logger.warning("Failed to initialize separate audit log. Merging with app log.")
64
+
65
+ self._initialized = True
66
+
67
+ def log_event(
68
+ self,
69
+ action: AuditAction | str,
70
+ resource: str,
71
+ status: str,
72
+ user: str = "system",
73
+ ip: str = "local",
74
+ details: Optional[dict[str, Any]] = None
75
+ ) -> None:
76
+ """
77
+ Record a security event.
78
+
79
+ Args:
80
+ action: Type of action (e.g., FILE_UPLOAD)
81
+ resource: Target resource (filename, path)
82
+ status: SUCCESS or FAILURE
83
+ user: Username or ID
84
+ ip: Source IP
85
+ details: Additional context
86
+ """
87
+ event = {
88
+ "timestamp": datetime.utcnow().isoformat() + "Z",
89
+ "action": str(action),
90
+ "resource": resource,
91
+ "status": status,
92
+ "user": user,
93
+ "ip": ip,
94
+ "details": details or {}
95
+ }
96
+
97
+ self._logger.info(json.dumps(event))
98
+
99
+
100
+ # Global instance
101
+ audit_logger = AuditLogger()
@@ -0,0 +1,138 @@
1
+ """
2
+ Binary Metadata Cache
3
+
4
+ This module provides caching for binary analysis results.
5
+ It prevents redundant analysis of the same files.
6
+ """
7
+
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from reversecore_mcp.core.logging_config import get_logger
13
+ from reversecore_mcp.core.metrics import metrics_collector
14
+
15
+ logger = get_logger(__name__)
16
+
17
+ # Configuration constants
18
+ DEFAULT_CACHE_TTL_SECONDS = 60 # Default time-to-live for cache validation
19
+
20
+
21
+ class BinaryMetadataCache:
22
+ """
23
+ Caches metadata for analyzed binaries.
24
+
25
+ Keyed by file path and modification time (or hash).
26
+
27
+ Performance optimizations:
28
+ - TTL-based validation to reduce stat() calls
29
+ - Cached mtime stored with last check time
30
+ - Configurable cache lifetime (default: 60 seconds)
31
+ """
32
+
33
+ def __init__(self, ttl_seconds: int = DEFAULT_CACHE_TTL_SECONDS):
34
+ self._cache: dict[str, Any] = {}
35
+ # Store (mtime, last_check_time) tuple to reduce stat() calls
36
+ self._file_timestamps: dict[str, tuple[float, float]] = {}
37
+ self._ttl_seconds = ttl_seconds
38
+
39
+ def _get_cache_key(self, file_path: str) -> str:
40
+ """Generate a cache key based on file path."""
41
+ return str(Path(file_path).absolute())
42
+
43
+ def _is_valid(self, file_path: str) -> bool:
44
+ """
45
+ Check if cache entry is valid (file hasn't changed).
46
+
47
+ Uses TTL-based checking to avoid excessive stat() calls:
48
+ - If checked within TTL window, assume valid (fast path)
49
+ - Otherwise, verify mtime hasn't changed (slow path)
50
+ """
51
+ key = self._get_cache_key(file_path)
52
+ if key not in self._cache:
53
+ return False
54
+
55
+ if key not in self._file_timestamps:
56
+ return False
57
+
58
+ cached_mtime, last_check_time = self._file_timestamps[key]
59
+ current_time = time.time()
60
+
61
+ # Fast path: If within TTL window, trust the cache without stat()
62
+ # Using < instead of <= for strict TTL boundary to avoid edge case stat() calls
63
+ if current_time - last_check_time < self._ttl_seconds:
64
+ return True
65
+
66
+ # Slow path: TTL expired, need to check file modification time
67
+ try:
68
+ actual_mtime = Path(file_path).stat().st_mtime
69
+ is_valid = cached_mtime == actual_mtime
70
+
71
+ if is_valid:
72
+ # Update last check time to reset TTL window
73
+ self._file_timestamps[key] = (cached_mtime, current_time)
74
+ else:
75
+ # File changed, invalidate cache
76
+ if key in self._cache:
77
+ del self._cache[key]
78
+ if key in self._file_timestamps:
79
+ del self._file_timestamps[key]
80
+
81
+ return is_valid
82
+ except FileNotFoundError:
83
+ # File deleted, invalidate cache
84
+ if key in self._cache:
85
+ del self._cache[key]
86
+ if key in self._file_timestamps:
87
+ del self._file_timestamps[key]
88
+ return False
89
+
90
+ def get(self, file_path: str, key: str) -> Any | None:
91
+ """Get a specific metadata item for a file."""
92
+ cache_key = self._get_cache_key(file_path)
93
+ if self._is_valid(file_path):
94
+ val = self._cache[cache_key].get(key)
95
+ if val is not None:
96
+ metrics_collector.record_cache_hit("binary_cache")
97
+ return val
98
+
99
+ metrics_collector.record_cache_miss("binary_cache")
100
+ return None
101
+
102
+ def set(self, file_path: str, key: str, value: Any):
103
+ """Set a specific metadata item for a file."""
104
+ cache_key = self._get_cache_key(file_path)
105
+
106
+ # Initialize if needed
107
+ if cache_key not in self._cache:
108
+ self._cache[cache_key] = {}
109
+
110
+ # Update timestamp with current time as last check
111
+ try:
112
+ mtime = Path(file_path).stat().st_mtime
113
+ self._file_timestamps[cache_key] = (mtime, time.time())
114
+ except FileNotFoundError:
115
+ # For memory/stream analysis or temp files that no longer exist,
116
+ # use current time as mtime to enable caching
117
+ # Without this, cache always misses because _is_valid returns False
118
+ current_time = time.time()
119
+ self._file_timestamps[cache_key] = (current_time, current_time)
120
+
121
+ self._cache[cache_key][key] = value
122
+ logger.debug(f"Cached {key} for {file_path}")
123
+
124
+ def clear(self, file_path: str = None):
125
+ """Clear cache for a specific file or all files."""
126
+ if file_path:
127
+ key = self._get_cache_key(file_path)
128
+ if key in self._cache:
129
+ del self._cache[key]
130
+ if key in self._file_timestamps:
131
+ del self._file_timestamps[key]
132
+ else:
133
+ self._cache.clear()
134
+ self._file_timestamps.clear()
135
+
136
+
137
+ # Global instance with default TTL
138
+ binary_cache = BinaryMetadataCache(ttl_seconds=DEFAULT_CACHE_TTL_SECONDS)
@@ -0,0 +1,357 @@
1
+ """
2
+ Command specification and validation with regex patterns.
3
+
4
+ This module provides strict command validation using regular expressions
5
+ to prevent command injection attacks. It addresses the vulnerability where
6
+ commands like "pdf @ main; w hello" could bypass simple prefix matching.
7
+ """
8
+
9
+ import re
10
+ from dataclasses import dataclass
11
+ from typing import Literal, NewType
12
+
13
+ from reversecore_mcp.core.exceptions import ValidationError
14
+
15
+ CommandType = Literal["read", "write", "analyze", "system"]
16
+ ValidatedR2Command = NewType("ValidatedR2Command", str)
17
+
18
+
19
+ @dataclass
20
+ class CommandSpec:
21
+ """
22
+ Specification for a command with strict regex validation.
23
+
24
+ Attributes:
25
+ name: Human-readable command name
26
+ type: Command type (read, write, analyze, system)
27
+ regex: Compiled regex pattern for strict validation
28
+ description: Optional description of what the command does
29
+ """
30
+
31
+ name: str
32
+ type: CommandType
33
+ regex: re.Pattern
34
+ description: str = ""
35
+
36
+ def validate(self, cmd: str) -> bool:
37
+ """
38
+ Validate a command string against this spec's regex.
39
+
40
+ Args:
41
+ cmd: Command string to validate
42
+
43
+ Returns:
44
+ True if command matches, False otherwise
45
+ """
46
+ return self.regex.match(cmd.strip()) is not None
47
+
48
+
49
+ # Radare2 command specifications with strict regex patterns
50
+ R2_COMMAND_SPECS: list[CommandSpec] = [
51
+ # Disassembly commands
52
+ CommandSpec(
53
+ name="pdf",
54
+ type="read",
55
+ regex=re.compile(r"^pdf(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
56
+ description="Print disassembly function",
57
+ ),
58
+ CommandSpec(
59
+ name="pd",
60
+ type="read",
61
+ regex=re.compile(r"^pd(\s+\d+)?(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
62
+ description="Print disassembly",
63
+ ),
64
+ CommandSpec(
65
+ name="pdfj",
66
+ type="read",
67
+ regex=re.compile(r"^pdfj(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
68
+ description="Print disassembly function (JSON)",
69
+ ),
70
+ CommandSpec(
71
+ name="pdj",
72
+ type="read",
73
+ regex=re.compile(r"^pdj(\s+\d+)?(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
74
+ description="Print disassembly (JSON)",
75
+ ),
76
+ CommandSpec(
77
+ name="pdc",
78
+ type="read",
79
+ regex=re.compile(r"^pdc(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
80
+ description="Print C-like pseudo code",
81
+ ),
82
+ # Analysis commands
83
+ CommandSpec(
84
+ name="aaa",
85
+ type="analyze",
86
+ regex=re.compile(r"^aaa$"),
87
+ description="Analyze all referenced code",
88
+ ),
89
+ CommandSpec(
90
+ name="aa",
91
+ type="analyze",
92
+ regex=re.compile(r"^aa[a]?$"),
93
+ description="Analyze all",
94
+ ),
95
+ CommandSpec(
96
+ name="afl",
97
+ type="read",
98
+ regex=re.compile(r"^afl[j]?(\s*~.+)?$"),
99
+ description="Analyze functions list",
100
+ ),
101
+ CommandSpec(
102
+ name="aflj",
103
+ type="read",
104
+ regex=re.compile(r"^aflj(\s*~.+)?$"),
105
+ description="Analyze functions list (JSON)",
106
+ ),
107
+ CommandSpec(
108
+ name="af",
109
+ type="analyze",
110
+ regex=re.compile(r"^af(\s+@\s+[a-zA-Z0-9_.]+)?$"),
111
+ description="Analyze function",
112
+ ),
113
+ CommandSpec(
114
+ name="afi",
115
+ type="read",
116
+ regex=re.compile(r"^afi(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
117
+ description="Analyze function info",
118
+ ),
119
+ CommandSpec(
120
+ name="afv",
121
+ type="read",
122
+ regex=re.compile(r"^afv[j]?(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
123
+ description="Analyze function variables",
124
+ ),
125
+ # Graph commands
126
+ CommandSpec(
127
+ name="agfj",
128
+ type="read",
129
+ regex=re.compile(r"^agfj(\s+@\s+[a-zA-Z0-9_.]+)?$"),
130
+ description="Print function graph in JSON format",
131
+ ),
132
+ # ESIL emulation commands
133
+ CommandSpec(
134
+ name="aei",
135
+ type="analyze",
136
+ regex=re.compile(r"^aei$"),
137
+ description="Initialize ESIL VM",
138
+ ),
139
+ CommandSpec(
140
+ name="aeim",
141
+ type="analyze",
142
+ regex=re.compile(r"^aeim$"),
143
+ description="Initialize ESIL VM memory (stack)",
144
+ ),
145
+ CommandSpec(
146
+ name="aeip",
147
+ type="analyze",
148
+ regex=re.compile(r"^aeip$"),
149
+ description="Initialize ESIL VM program counter",
150
+ ),
151
+ CommandSpec(
152
+ name="aes",
153
+ type="analyze",
154
+ regex=re.compile(r"^aes(\s+\d+)?$"),
155
+ description="ESIL step execution",
156
+ ),
157
+ CommandSpec(
158
+ name="ar",
159
+ type="read",
160
+ regex=re.compile(r"^ar[j]?$"),
161
+ description="Show all register values",
162
+ ),
163
+ CommandSpec(
164
+ name="s",
165
+ type="analyze",
166
+ regex=re.compile(r"^s(\s+[a-zA-Z0-9_.]+)?$"),
167
+ description="Seek to address",
168
+ ),
169
+ # Information commands
170
+ CommandSpec(
171
+ name="i",
172
+ type="read",
173
+ regex=re.compile(r"^i[IiSszeEhj]?(\s*~.+)?$"),
174
+ description="File information",
175
+ ),
176
+ CommandSpec(
177
+ name="iI",
178
+ type="read",
179
+ regex=re.compile(r"^iI(\s*~.+)?$"),
180
+ description="Binary info",
181
+ ),
182
+ CommandSpec(
183
+ name="ii",
184
+ type="read",
185
+ regex=re.compile(r"^ii[j]?(\s*~.+)?$"),
186
+ description="Imports",
187
+ ),
188
+ CommandSpec(
189
+ name="iS",
190
+ type="read",
191
+ regex=re.compile(r"^iS[j]?(\s*~.+)?$"),
192
+ description="Sections",
193
+ ),
194
+ CommandSpec(
195
+ name="iz",
196
+ type="read",
197
+ regex=re.compile(r"^iz[j]?(\s*~.+)?$"),
198
+ description="Strings in data sections",
199
+ ),
200
+ CommandSpec(
201
+ name="izz",
202
+ type="read",
203
+ regex=re.compile(r"^izz[j]?(\s*~.+)?$"),
204
+ description="All strings",
205
+ ),
206
+ CommandSpec(
207
+ name="ie",
208
+ type="read",
209
+ regex=re.compile(r"^ie[j]?(\s*~.+)?$"),
210
+ description="Entry points",
211
+ ),
212
+ CommandSpec(
213
+ name="iE",
214
+ type="read",
215
+ regex=re.compile(r"^iE[j]?(\s*~.+)?$"),
216
+ description="Exports",
217
+ ),
218
+ # Hexdump commands
219
+ CommandSpec(
220
+ name="px",
221
+ type="read",
222
+ regex=re.compile(r"^px[wqd]?(\s+\d+)?(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
223
+ description="Print hexdump",
224
+ ),
225
+ CommandSpec(
226
+ name="pxw",
227
+ type="read",
228
+ regex=re.compile(r"^pxw(\s+\d+)?(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
229
+ description="Print hexdump (words)",
230
+ ),
231
+ CommandSpec(
232
+ name="pxq",
233
+ type="read",
234
+ regex=re.compile(r"^pxq(\s+\d+)?(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
235
+ description="Print hexdump (qwords)",
236
+ ),
237
+ CommandSpec(
238
+ name="p8",
239
+ type="read",
240
+ regex=re.compile(r"^p8(\s+\d+)?(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
241
+ description="Print raw bytes in hexadecimal",
242
+ ),
243
+ # Seek commands (read-only navigation)
244
+ CommandSpec(
245
+ name="s",
246
+ type="read",
247
+ regex=re.compile(r"^s(\s+[a-zA-Z0-9_.+\-]+)?(\s*~.+)?$"),
248
+ description="Seek to address",
249
+ ),
250
+ # Flag commands (read-only)
251
+ CommandSpec(
252
+ name="f",
253
+ type="read",
254
+ regex=re.compile(r"^f[sj]?(\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
255
+ description="Flags",
256
+ ),
257
+ CommandSpec(
258
+ name="fs",
259
+ type="read",
260
+ regex=re.compile(r"^fs(\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
261
+ description="Flag spaces",
262
+ ),
263
+ ]
264
+
265
+
266
+ # Dangerous patterns that should always be blocked
267
+ DANGEROUS_PATTERNS = [
268
+ re.compile(r";"), # SECURITY: Block ALL semicolons (prevents `pdf ~;!id` bypass)
269
+ re.compile(r"\|(?!})"), # Pipe to another command (but allow JSON `|` in output)
270
+ re.compile(r"`.*`"), # Backticks (command substitution)
271
+ re.compile(r"\$\(.*\)"), # Command substitution
272
+ re.compile(r"&&"), # Logical AND (command chaining)
273
+ re.compile(r"\|\|"), # Logical OR (command chaining)
274
+ re.compile(r"^w[oxa]?\s"), # Write commands
275
+ re.compile(r"^!"), # System commands
276
+ re.compile(r"^#!"), # Scripts
277
+ re.compile(r"~.*;"), # Tilde filter followed by semicolon (extra protection)
278
+ ]
279
+
280
+
281
+ def validate_r2_command(cmd: str, allow_write: bool = False) -> ValidatedR2Command:
282
+ """
283
+ Validate a radare2 command using strict regex patterns.
284
+
285
+ This function provides comprehensive validation to prevent command injection:
286
+ 1. Checks for dangerous patterns (semicolons, pipes, command substitution)
287
+ 2. Validates against known safe command patterns
288
+ 3. Blocks write commands unless explicitly allowed
289
+
290
+ Args:
291
+ cmd: Radare2 command string to validate
292
+ allow_write: If True, allow write commands (default: False)
293
+
294
+ Returns:
295
+ ValidatedR2Command: Command string marked as validated
296
+
297
+ Raises:
298
+ ValidationError: If command is invalid, dangerous, or not in allowlist
299
+
300
+ Example:
301
+ >>> validate_r2_command("pdf @ main")
302
+ ValidatedR2Command('pdf @ main')
303
+
304
+ >>> validate_r2_command("pdf @ main; w hello")
305
+ ValidationError: Dangerous command pattern detected
306
+ """
307
+ if not cmd or not cmd.strip():
308
+ raise ValidationError("Command string cannot be empty", details={"command": cmd})
309
+
310
+ cmd_stripped = cmd.strip()
311
+
312
+ # Check for dangerous patterns first
313
+ for pattern in DANGEROUS_PATTERNS:
314
+ if pattern.search(cmd_stripped):
315
+ raise ValidationError(
316
+ f"Dangerous command pattern detected: {pattern.pattern}. "
317
+ "Command chaining, pipes, and command substitution are not allowed.",
318
+ details={"command": cmd_stripped, "pattern": pattern.pattern},
319
+ )
320
+
321
+ # Try to match against command specifications
322
+ for spec in R2_COMMAND_SPECS:
323
+ if spec.validate(cmd_stripped):
324
+ # Check if write command when not allowed
325
+ if spec.type == "write" and not allow_write:
326
+ raise ValidationError(
327
+ f"Write commands are not allowed: {spec.name}",
328
+ details={"command": cmd_stripped, "command_type": spec.type},
329
+ )
330
+ return ValidatedR2Command(cmd_stripped)
331
+
332
+ # No match found
333
+ raise ValidationError(
334
+ f"Command not in allowlist: {cmd_stripped}. "
335
+ "Only read-only and analysis commands are allowed.",
336
+ details={
337
+ "command": cmd_stripped,
338
+ "allowed_commands": [spec.name for spec in R2_COMMAND_SPECS],
339
+ },
340
+ )
341
+
342
+
343
+ def is_safe_r2_command(cmd: str) -> bool:
344
+ """
345
+ Check if a radare2 command is safe (non-blocking validation).
346
+
347
+ Args:
348
+ cmd: Command string to check
349
+
350
+ Returns:
351
+ True if command is safe, False otherwise
352
+ """
353
+ try:
354
+ validate_r2_command(cmd, allow_write=False)
355
+ return True
356
+ except ValidationError:
357
+ return False