iotsploit-core 0.0.6__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.
- iotsploit_core/__init__.py +7 -0
- iotsploit_core/context.py +27 -0
- iotsploit_core/core/__init__.py +0 -0
- iotsploit_core/core/base_plugin.py +290 -0
- iotsploit_core/core/device_config.py +105 -0
- iotsploit_core/core/device_manager.py +942 -0
- iotsploit_core/core/device_registry.py +118 -0
- iotsploit_core/core/device_scanner.py +74 -0
- iotsploit_core/core/device_spec.py +105 -0
- iotsploit_core/core/device_store.py +86 -0
- iotsploit_core/core/execution_backend.py +464 -0
- iotsploit_core/core/execution_queue.py +456 -0
- iotsploit_core/core/exploit_manager.py +728 -0
- iotsploit_core/core/exploit_spec.py +73 -0
- iotsploit_core/core/stream_manager.py +157 -0
- iotsploit_core/core/tool_config.py +430 -0
- iotsploit_core/core/tool_manager.py +1166 -0
- iotsploit_core/core/tool_service.py +936 -0
- iotsploit_core/domain/__init__.py +3 -0
- iotsploit_core/domain/device.py +70 -0
- iotsploit_core/domain/execution_plan.py +29 -0
- iotsploit_core/domain/plugin.py +17 -0
- iotsploit_core/domain/stream.py +63 -0
- iotsploit_core/domain/target.py +243 -0
- iotsploit_core/platforms/__init__.py +19 -0
- iotsploit_core/platforms/consts.py +25 -0
- iotsploit_core/ports/__init__.py +6 -0
- iotsploit_core/ports/driver_state_repo.py +15 -0
- iotsploit_core/ports/plugin_repo.py +26 -0
- iotsploit_core/ports/stream_backend.py +28 -0
- iotsploit_core/ports/task_runner.py +22 -0
- iotsploit_core/ports/wifi_backend.py +148 -0
- iotsploit_core/py.typed +2 -0
- iotsploit_core/utils/__init__.py +53 -0
- iotsploit_core/utils/exceptions.py +152 -0
- iotsploit_core/utils/helpers.py +65 -0
- iotsploit_core/utils/iots_logger.py +164 -0
- iotsploit_core/utils/result.py +41 -0
- iotsploit_core-0.0.6.dist-info/METADATA +117 -0
- iotsploit_core-0.0.6.dist-info/RECORD +41 -0
- iotsploit_core-0.0.6.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Unified tool management and execution service for plugins."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from typing import Dict, List, Optional, Any, Union
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
from .tool_manager import get_tool_manager, ExecutionResult, ToolStatus
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
class ToolService:
|
|
16
|
+
"""
|
|
17
|
+
Tool Management Service
|
|
18
|
+
|
|
19
|
+
Provides a simple interface for plugins to manage and execute third-party tools
|
|
20
|
+
without worrying about path resolution, version checking, or cross-platform compatibility.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self.tool_manager = get_tool_manager() # Use singleton instead of creating new instance
|
|
25
|
+
self.logger = logging.getLogger("tool_service")
|
|
26
|
+
self._registered_tools = set()
|
|
27
|
+
|
|
28
|
+
def register_tool(self, tool_name: str, min_version: str = None,
|
|
29
|
+
aliases: List[str] = None, required: bool = True) -> bool:
|
|
30
|
+
"""
|
|
31
|
+
Register a tool for use by the calling plugin.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
tool_name: Name of the tool (e.g., 'esptool', 'nmap')
|
|
35
|
+
min_version: Minimum required version (optional)
|
|
36
|
+
aliases: Alternative names for the tool (optional)
|
|
37
|
+
required: Whether this tool is required for plugin operation
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
bool: True if tool is available, False otherwise
|
|
41
|
+
"""
|
|
42
|
+
self.logger.debug(f"Registering tool: {tool_name}")
|
|
43
|
+
|
|
44
|
+
# Add tool to registry if not already present
|
|
45
|
+
if not self.tool_manager.registry.get_tool(tool_name):
|
|
46
|
+
success = self.tool_manager.add_tool(
|
|
47
|
+
name=tool_name,
|
|
48
|
+
aliases=aliases or [],
|
|
49
|
+
min_version=min_version
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if not success and required:
|
|
53
|
+
self.logger.error(f"Required tool '{tool_name}' is not available")
|
|
54
|
+
return False
|
|
55
|
+
elif not success:
|
|
56
|
+
self.logger.warning(f"Optional tool '{tool_name}' is not available")
|
|
57
|
+
|
|
58
|
+
self._registered_tools.add(tool_name)
|
|
59
|
+
return self.tool_manager.is_available(tool_name)
|
|
60
|
+
|
|
61
|
+
def is_tool_available(self, tool_name: str) -> bool:
|
|
62
|
+
"""
|
|
63
|
+
Check if a tool is available for use.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
tool_name: Name of the tool to check
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
bool: True if tool is available, False otherwise
|
|
70
|
+
"""
|
|
71
|
+
return self.tool_manager.is_available(tool_name)
|
|
72
|
+
|
|
73
|
+
def get_tool_path(self, tool_name: str) -> Optional[str]:
|
|
74
|
+
"""
|
|
75
|
+
Get the full path to a tool.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
tool_name: Name of the tool
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
str: Full path to the tool, or None if not found
|
|
82
|
+
"""
|
|
83
|
+
tool_info = self.tool_manager.registry.get_tool(tool_name)
|
|
84
|
+
if tool_info and tool_info.path:
|
|
85
|
+
return tool_info.path
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
def get_tool_version(self, tool_name: str) -> Optional[str]:
|
|
89
|
+
"""
|
|
90
|
+
Get the version of a tool.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
tool_name: Name of the tool
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
str: Version string, or None if not available
|
|
97
|
+
"""
|
|
98
|
+
tool_info = self.tool_manager.registry.get_tool(tool_name)
|
|
99
|
+
if tool_info:
|
|
100
|
+
return tool_info.version
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
def execute_tool(self, tool_name: str, args: List[str],
|
|
104
|
+
working_dir: Optional[str] = None,
|
|
105
|
+
env: Optional[Dict[str, str]] = None,
|
|
106
|
+
timeout: Optional[int] = None,
|
|
107
|
+
check_availability: bool = True) -> ExecutionResult:
|
|
108
|
+
"""
|
|
109
|
+
Execute a tool with the given arguments.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
tool_name: Name of the tool to execute
|
|
113
|
+
args: List of command-line arguments
|
|
114
|
+
working_dir: Working directory for execution (optional)
|
|
115
|
+
env: Environment variables (optional)
|
|
116
|
+
timeout: Execution timeout in seconds (optional)
|
|
117
|
+
check_availability: Whether to check tool availability first
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
ExecutionResult: Result of the tool execution
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
RuntimeError: If tool is not available
|
|
124
|
+
ValueError: If tool is not registered
|
|
125
|
+
"""
|
|
126
|
+
if check_availability and not self.is_tool_available(tool_name):
|
|
127
|
+
raise RuntimeError(f"Tool '{tool_name}' is not available")
|
|
128
|
+
|
|
129
|
+
self.logger.info(f"Executing {tool_name} with args: {args}")
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
result = self.tool_manager.execute(
|
|
133
|
+
tool_name, args,
|
|
134
|
+
working_dir=working_dir,
|
|
135
|
+
env=env,
|
|
136
|
+
timeout=timeout
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if result.success:
|
|
140
|
+
self.logger.debug(f"{tool_name} executed successfully in {result.execution_time:.2f}s")
|
|
141
|
+
else:
|
|
142
|
+
self.logger.warning(f"{tool_name} failed with return code {result.return_code}")
|
|
143
|
+
if result.stderr:
|
|
144
|
+
self.logger.debug(f"stderr: {result.stderr}")
|
|
145
|
+
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
self.logger.error(f"Failed to execute {tool_name}: {e}")
|
|
150
|
+
raise
|
|
151
|
+
|
|
152
|
+
def execute_tool_simple(self, tool_name: str, args: List[str], **kwargs) -> bool:
|
|
153
|
+
"""
|
|
154
|
+
Simple tool execution that returns only success/failure.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
tool_name: Name of the tool to execute
|
|
158
|
+
args: List of command-line arguments
|
|
159
|
+
**kwargs: Additional arguments passed to execute_tool
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
bool: True if execution was successful, False otherwise
|
|
163
|
+
"""
|
|
164
|
+
try:
|
|
165
|
+
result = self.execute_tool(tool_name, args, **kwargs)
|
|
166
|
+
return result.success
|
|
167
|
+
except Exception as e:
|
|
168
|
+
self.logger.error(f"Tool execution failed: {e}")
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
def get_tool_output(self, tool_name: str, args: List[str], **kwargs) -> Optional[str]:
|
|
172
|
+
"""
|
|
173
|
+
Execute a tool and return its output.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
tool_name: Name of the tool to execute
|
|
177
|
+
args: List of command-line arguments
|
|
178
|
+
**kwargs: Additional arguments passed to execute_tool
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
str: Tool output (stdout), or None if execution failed
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
result = self.execute_tool(tool_name, args, **kwargs)
|
|
185
|
+
return result.stdout if result.success else None
|
|
186
|
+
except Exception as e:
|
|
187
|
+
self.logger.error(f"Tool execution failed: {e}")
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
def check_tool_requirements(self, required_tools: List[str],
|
|
191
|
+
optional_tools: List[str] = None) -> Dict[str, bool]:
|
|
192
|
+
"""
|
|
193
|
+
Check availability of multiple tools.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
required_tools: List of required tool names
|
|
197
|
+
optional_tools: List of optional tool names
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
dict: Tool name -> availability mapping
|
|
201
|
+
"""
|
|
202
|
+
results = {}
|
|
203
|
+
|
|
204
|
+
for tool in required_tools:
|
|
205
|
+
is_available = self.is_tool_available(tool)
|
|
206
|
+
results[tool] = is_available
|
|
207
|
+
if not is_available:
|
|
208
|
+
self.logger.error(f"Required tool '{tool}' is not available")
|
|
209
|
+
|
|
210
|
+
for tool in (optional_tools or []):
|
|
211
|
+
is_available = self.is_tool_available(tool)
|
|
212
|
+
results[tool] = is_available
|
|
213
|
+
if not is_available:
|
|
214
|
+
self.logger.warning(f"Optional tool '{tool}' is not available")
|
|
215
|
+
|
|
216
|
+
return results
|
|
217
|
+
|
|
218
|
+
def get_registered_tools(self) -> List[str]:
|
|
219
|
+
"""
|
|
220
|
+
Get list of tools registered by this service instance.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
list: Names of registered tools
|
|
224
|
+
"""
|
|
225
|
+
return list(self._registered_tools)
|
|
226
|
+
|
|
227
|
+
class BaseProgrammer:
|
|
228
|
+
"""Base class for all programmers"""
|
|
229
|
+
|
|
230
|
+
def __init__(self, tool_service: 'ToolService'):
|
|
231
|
+
self.tool_service = tool_service
|
|
232
|
+
self.logger = logging.getLogger(f"programmer.{self.__class__.__name__}")
|
|
233
|
+
|
|
234
|
+
def is_tool_available(self, tool_name: str) -> bool:
|
|
235
|
+
"""Check if required tool is available"""
|
|
236
|
+
return self.tool_service.is_tool_available(tool_name)
|
|
237
|
+
|
|
238
|
+
def execute_tool(self, tool_name: str, args: List[str], **kwargs):
|
|
239
|
+
"""Execute tool through the tool service"""
|
|
240
|
+
return self.tool_service.execute_tool(tool_name, args, **kwargs)
|
|
241
|
+
|
|
242
|
+
class ESP32Programmer(BaseProgrammer):
|
|
243
|
+
"""ESP32 device programmer using esptool"""
|
|
244
|
+
|
|
245
|
+
def __init__(self, tool_service: 'ToolService'):
|
|
246
|
+
super().__init__(tool_service)
|
|
247
|
+
self.tool_service.register_tool('esptool', required=False)
|
|
248
|
+
|
|
249
|
+
def flash_single(self, port: str, firmware_path: str, address: str = "0x10000",
|
|
250
|
+
chip: str = "esp32", baud: str = "460800") -> 'ExecutionResult':
|
|
251
|
+
"""
|
|
252
|
+
Flash single firmware file to ESP32 device.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
port: Serial port (e.g., '/dev/ttyUSB0')
|
|
256
|
+
firmware_path: Path to firmware file
|
|
257
|
+
address: Flash address (default: 0x10000)
|
|
258
|
+
chip: Chip type (default: esp32)
|
|
259
|
+
baud: Baud rate (default: 460800)
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
ExecutionResult: Result of the flash operation
|
|
263
|
+
"""
|
|
264
|
+
if not self.is_tool_available('esptool'):
|
|
265
|
+
raise RuntimeError("esptool is not available")
|
|
266
|
+
|
|
267
|
+
args = [
|
|
268
|
+
'--chip', chip,
|
|
269
|
+
'--port', port,
|
|
270
|
+
'--baud', baud,
|
|
271
|
+
'write_flash', address, firmware_path
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
return self.execute_tool('esptool', args, timeout=300)
|
|
275
|
+
|
|
276
|
+
def flash_multi(self, port: str, files: List[Dict[str, str]],
|
|
277
|
+
chip: str = "esp32s3", baud: str = "460800",
|
|
278
|
+
flash_mode: str = "dio", flash_freq: str = "80m",
|
|
279
|
+
flash_size: str = "2MB") -> 'ExecutionResult':
|
|
280
|
+
"""
|
|
281
|
+
Flash multiple files to ESP32 device (bootloader, partition table, app).
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
port: Serial port (e.g., '/dev/ttyUSB0')
|
|
285
|
+
files: List of dicts with 'address' and 'path' keys
|
|
286
|
+
chip: Chip type (default: esp32s3)
|
|
287
|
+
baud: Baud rate (default: 460800)
|
|
288
|
+
flash_mode: Flash mode (default: dio)
|
|
289
|
+
flash_freq: Flash frequency (default: 80m)
|
|
290
|
+
flash_size: Flash size (default: 2MB)
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
ExecutionResult: Result of the flash operation
|
|
294
|
+
"""
|
|
295
|
+
if not self.is_tool_available('esptool'):
|
|
296
|
+
raise RuntimeError("esptool is not available")
|
|
297
|
+
|
|
298
|
+
args = [
|
|
299
|
+
'--chip', chip,
|
|
300
|
+
'--port', port,
|
|
301
|
+
'--baud', baud,
|
|
302
|
+
'--before', 'default_reset',
|
|
303
|
+
'--after', 'hard_reset',
|
|
304
|
+
'write_flash',
|
|
305
|
+
'--flash_mode', flash_mode,
|
|
306
|
+
'--flash_freq', flash_freq,
|
|
307
|
+
'--flash_size', flash_size
|
|
308
|
+
]
|
|
309
|
+
|
|
310
|
+
# Add each file with its address
|
|
311
|
+
for file_info in files:
|
|
312
|
+
args.extend([file_info['address'], file_info['path']])
|
|
313
|
+
|
|
314
|
+
return self.execute_tool('esptool', args, timeout=300)
|
|
315
|
+
|
|
316
|
+
def erase_flash(self, port: str, chip: str = "esp32s3",
|
|
317
|
+
baud: str = "460800") -> 'ExecutionResult':
|
|
318
|
+
"""Erase ESP32 flash memory"""
|
|
319
|
+
if not self.is_tool_available('esptool'):
|
|
320
|
+
raise RuntimeError("esptool is not available")
|
|
321
|
+
|
|
322
|
+
args = [
|
|
323
|
+
'--chip', chip,
|
|
324
|
+
'--port', port,
|
|
325
|
+
'--baud', baud,
|
|
326
|
+
'erase_flash'
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
return self.execute_tool('esptool', args, timeout=120)
|
|
330
|
+
|
|
331
|
+
def get_chip_info(self, port: str, baud: str = "460800") -> 'ExecutionResult':
|
|
332
|
+
"""Get ESP32 chip information"""
|
|
333
|
+
if not self.is_tool_available('esptool'):
|
|
334
|
+
raise RuntimeError("esptool is not available")
|
|
335
|
+
|
|
336
|
+
args = [
|
|
337
|
+
'--port', port,
|
|
338
|
+
'--baud', baud,
|
|
339
|
+
'chip_id'
|
|
340
|
+
]
|
|
341
|
+
|
|
342
|
+
return self.execute_tool('esptool', args, timeout=30)
|
|
343
|
+
|
|
344
|
+
class STM32Programmer(BaseProgrammer):
|
|
345
|
+
"""STM32 device programmer using OpenOCD"""
|
|
346
|
+
|
|
347
|
+
def __init__(self, tool_service: 'ToolService'):
|
|
348
|
+
super().__init__(tool_service)
|
|
349
|
+
self.tool_service.register_tool('openocd', required=False)
|
|
350
|
+
|
|
351
|
+
def flash_firmware(self, firmware_path: str, interface: str = "stlink",
|
|
352
|
+
target: str = "stm32f4x") -> 'ExecutionResult':
|
|
353
|
+
"""
|
|
354
|
+
Flash firmware to STM32 device using OpenOCD.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
firmware_path: Path to firmware file
|
|
358
|
+
interface: Debug interface (default: stlink)
|
|
359
|
+
target: Target configuration (default: stm32f4x)
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
ExecutionResult: Result of the flash operation
|
|
363
|
+
"""
|
|
364
|
+
if not self.is_tool_available('openocd'):
|
|
365
|
+
raise RuntimeError("openocd is not available")
|
|
366
|
+
|
|
367
|
+
import tempfile
|
|
368
|
+
import os
|
|
369
|
+
|
|
370
|
+
# Create temporary script file
|
|
371
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.cfg', delete=False) as f:
|
|
372
|
+
script_path = f.name
|
|
373
|
+
f.write(f"""
|
|
374
|
+
source [find interface/{interface}.cfg]
|
|
375
|
+
transport select hla_swd
|
|
376
|
+
source [find target/{target}.cfg]
|
|
377
|
+
reset_config srst_only
|
|
378
|
+
init
|
|
379
|
+
reset halt
|
|
380
|
+
flash write_image erase {firmware_path} 0x08000000
|
|
381
|
+
reset run
|
|
382
|
+
exit
|
|
383
|
+
""")
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
args = ['-f', script_path]
|
|
387
|
+
result = self.execute_tool('openocd', args, timeout=300)
|
|
388
|
+
os.unlink(script_path)
|
|
389
|
+
return result
|
|
390
|
+
except Exception as e:
|
|
391
|
+
if os.path.exists(script_path):
|
|
392
|
+
os.unlink(script_path)
|
|
393
|
+
raise
|
|
394
|
+
|
|
395
|
+
class DFUProgrammer(BaseProgrammer):
|
|
396
|
+
"""DFU device programmer using dfu-util"""
|
|
397
|
+
|
|
398
|
+
def __init__(self, tool_service: 'ToolService'):
|
|
399
|
+
super().__init__(tool_service)
|
|
400
|
+
self.tool_service.register_tool('dfu-util', required=False)
|
|
401
|
+
|
|
402
|
+
def flash_firmware(self, firmware_path: str, vid: str, pid: str,
|
|
403
|
+
alt: Optional[str] = None) -> 'ExecutionResult':
|
|
404
|
+
"""
|
|
405
|
+
Flash firmware via DFU.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
firmware_path: Path to firmware file
|
|
409
|
+
vid: Vendor ID
|
|
410
|
+
pid: Product ID
|
|
411
|
+
alt: Alternative setting (optional)
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
ExecutionResult: Result of the flash operation
|
|
415
|
+
"""
|
|
416
|
+
if not self.is_tool_available('dfu-util'):
|
|
417
|
+
raise RuntimeError("dfu-util is not available")
|
|
418
|
+
|
|
419
|
+
args = ['-d', f"{vid}:{pid}", '-D', firmware_path]
|
|
420
|
+
|
|
421
|
+
if alt is not None:
|
|
422
|
+
args.extend(['-a', str(alt)])
|
|
423
|
+
|
|
424
|
+
return self.execute_tool('dfu-util', args, timeout=300)
|
|
425
|
+
|
|
426
|
+
class FPGAProgrammer(BaseProgrammer):
|
|
427
|
+
"""FPGA programmer using openFPGALoader"""
|
|
428
|
+
|
|
429
|
+
def __init__(self, tool_service: 'ToolService'):
|
|
430
|
+
super().__init__(tool_service)
|
|
431
|
+
self.tool_service.register_tool('openFPGALoader', required=False)
|
|
432
|
+
|
|
433
|
+
def is_tool_available(self, tool_name: str = 'openFPGALoader') -> bool:
|
|
434
|
+
"""Check if openFPGALoader is available, with fallback to direct detection"""
|
|
435
|
+
# First try the centralized tool service
|
|
436
|
+
if self.tool_service.is_tool_available(tool_name):
|
|
437
|
+
return True
|
|
438
|
+
|
|
439
|
+
# Fallback to direct detection if tool service fails
|
|
440
|
+
import shutil
|
|
441
|
+
return shutil.which(tool_name) is not None
|
|
442
|
+
|
|
443
|
+
def load_sram(self, bitstream_path: str, board: Optional[str] = None,
|
|
444
|
+
cable: Optional[str] = None) -> 'ExecutionResult':
|
|
445
|
+
"""
|
|
446
|
+
Load bitstream to FPGA SRAM (temporary).
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
bitstream_path: Path to bitstream file
|
|
450
|
+
board: Board type (optional)
|
|
451
|
+
cable: Cable type (optional)
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
ExecutionResult: Result of the load operation
|
|
455
|
+
"""
|
|
456
|
+
if not self.is_tool_available('openFPGALoader'):
|
|
457
|
+
raise RuntimeError("openFPGALoader is not available")
|
|
458
|
+
|
|
459
|
+
args = ['--bitstream', bitstream_path]
|
|
460
|
+
|
|
461
|
+
if board:
|
|
462
|
+
args.extend(['--board', board])
|
|
463
|
+
if cable:
|
|
464
|
+
args.extend(['--cable', cable])
|
|
465
|
+
|
|
466
|
+
try:
|
|
467
|
+
return self.execute_tool('openFPGALoader', args, timeout=300)
|
|
468
|
+
except RuntimeError:
|
|
469
|
+
# Fallback to direct execution if tool service fails
|
|
470
|
+
import shutil
|
|
471
|
+
import subprocess
|
|
472
|
+
import time
|
|
473
|
+
|
|
474
|
+
tool_path = shutil.which('openFPGALoader')
|
|
475
|
+
if not tool_path:
|
|
476
|
+
raise RuntimeError("openFPGALoader is not available")
|
|
477
|
+
|
|
478
|
+
command = [tool_path] + args
|
|
479
|
+
start_time = time.time()
|
|
480
|
+
|
|
481
|
+
result = subprocess.run(
|
|
482
|
+
command,
|
|
483
|
+
capture_output=True,
|
|
484
|
+
text=True,
|
|
485
|
+
timeout=300
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
execution_time = time.time() - start_time
|
|
489
|
+
|
|
490
|
+
return ExecutionResult(
|
|
491
|
+
success=result.returncode == 0,
|
|
492
|
+
return_code=result.returncode,
|
|
493
|
+
stdout=result.stdout,
|
|
494
|
+
stderr=result.stderr,
|
|
495
|
+
execution_time=execution_time,
|
|
496
|
+
command=' '.join(command),
|
|
497
|
+
tool_path=tool_path
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
def flash_bitstream(self, bitstream_path: str, board: Optional[str] = None,
|
|
501
|
+
cable: Optional[str] = None, external_flash: bool = False) -> 'ExecutionResult':
|
|
502
|
+
"""
|
|
503
|
+
Flash bitstream to FPGA configuration flash (permanent).
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
bitstream_path: Path to bitstream file
|
|
507
|
+
board: Board type (optional)
|
|
508
|
+
cable: Cable type (optional)
|
|
509
|
+
external_flash: Use external flash (optional)
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
ExecutionResult: Result of the flash operation
|
|
513
|
+
"""
|
|
514
|
+
if not self.is_tool_available('openFPGALoader'):
|
|
515
|
+
raise RuntimeError("openFPGALoader is not available")
|
|
516
|
+
|
|
517
|
+
args = ['--write-flash', '--bitstream', bitstream_path]
|
|
518
|
+
|
|
519
|
+
if board:
|
|
520
|
+
args.extend(['--board', board])
|
|
521
|
+
if cable:
|
|
522
|
+
args.extend(['--cable', cable])
|
|
523
|
+
if external_flash:
|
|
524
|
+
args.append('--external-flash')
|
|
525
|
+
|
|
526
|
+
try:
|
|
527
|
+
return self.execute_tool('openFPGALoader', args, timeout=300)
|
|
528
|
+
except RuntimeError:
|
|
529
|
+
# Fallback to direct execution if tool service fails
|
|
530
|
+
import shutil
|
|
531
|
+
import subprocess
|
|
532
|
+
import time
|
|
533
|
+
|
|
534
|
+
tool_path = shutil.which('openFPGALoader')
|
|
535
|
+
if not tool_path:
|
|
536
|
+
raise RuntimeError("openFPGALoader is not available")
|
|
537
|
+
|
|
538
|
+
command = [tool_path] + args
|
|
539
|
+
start_time = time.time()
|
|
540
|
+
|
|
541
|
+
result = subprocess.run(
|
|
542
|
+
command,
|
|
543
|
+
capture_output=True,
|
|
544
|
+
text=True,
|
|
545
|
+
timeout=300
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
execution_time = time.time() - start_time
|
|
549
|
+
|
|
550
|
+
return ExecutionResult(
|
|
551
|
+
success=result.returncode == 0,
|
|
552
|
+
return_code=result.returncode,
|
|
553
|
+
stdout=result.stdout,
|
|
554
|
+
stderr=result.stderr,
|
|
555
|
+
execution_time=execution_time,
|
|
556
|
+
command=' '.join(command),
|
|
557
|
+
tool_path=tool_path
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
class GreatFETProgrammer(BaseProgrammer):
|
|
561
|
+
"""GreatFET programmer using greatfet_firmware"""
|
|
562
|
+
|
|
563
|
+
def __init__(self, tool_service: 'ToolService'):
|
|
564
|
+
super().__init__(tool_service)
|
|
565
|
+
self.tool_service.register_tool('greatfet_firmware', required=False)
|
|
566
|
+
|
|
567
|
+
def flash_firmware(self, firmware_path: str, target: str = "spi",
|
|
568
|
+
serial: Optional[str] = None, board: Optional[str] = None) -> 'ExecutionResult':
|
|
569
|
+
"""
|
|
570
|
+
Flash firmware to GreatFET device.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
firmware_path: Path to firmware file
|
|
574
|
+
target: Target memory (sram or spi)
|
|
575
|
+
serial: Device serial number (optional)
|
|
576
|
+
board: Board type (optional)
|
|
577
|
+
|
|
578
|
+
Returns:
|
|
579
|
+
ExecutionResult: Result of the flash operation
|
|
580
|
+
"""
|
|
581
|
+
if not self.is_tool_available('greatfet_firmware'):
|
|
582
|
+
raise RuntimeError("greatfet_firmware is not available")
|
|
583
|
+
|
|
584
|
+
if target.lower() == 'sram':
|
|
585
|
+
args = ['-V', firmware_path]
|
|
586
|
+
else:
|
|
587
|
+
args = ['-w', firmware_path]
|
|
588
|
+
|
|
589
|
+
if serial:
|
|
590
|
+
args.extend(['-s', serial])
|
|
591
|
+
if board:
|
|
592
|
+
args.extend(['-b', board])
|
|
593
|
+
|
|
594
|
+
return self.execute_tool('greatfet_firmware', args, timeout=300)
|
|
595
|
+
|
|
596
|
+
class FirmwareToolService(ToolService):
|
|
597
|
+
"""Tool service specialized for firmware operations"""
|
|
598
|
+
|
|
599
|
+
def __init__(self):
|
|
600
|
+
super().__init__()
|
|
601
|
+
|
|
602
|
+
# Initialize firmware registry
|
|
603
|
+
from pathlib import Path
|
|
604
|
+
import json
|
|
605
|
+
# Use centralized configuration directory
|
|
606
|
+
self.conf_dir = Path('conf')
|
|
607
|
+
self.conf_dir.mkdir(exist_ok=True)
|
|
608
|
+
self.manifest_file = self.conf_dir / 'firmware_manifest.json'
|
|
609
|
+
self.manifests = self._load_manifests()
|
|
610
|
+
|
|
611
|
+
# Initialize programmers
|
|
612
|
+
self.esp32 = ESP32Programmer(self)
|
|
613
|
+
self.stm32 = STM32Programmer(self)
|
|
614
|
+
self.dfu = DFUProgrammer(self)
|
|
615
|
+
self.fpga = FPGAProgrammer(self)
|
|
616
|
+
self.greatfet = GreatFETProgrammer(self)
|
|
617
|
+
|
|
618
|
+
def _load_manifests(self) -> Dict:
|
|
619
|
+
"""Load firmware manifests from JSON file"""
|
|
620
|
+
if self.manifest_file.exists():
|
|
621
|
+
try:
|
|
622
|
+
with open(self.manifest_file, 'r') as f:
|
|
623
|
+
return json.load(f)
|
|
624
|
+
except json.JSONDecodeError:
|
|
625
|
+
self.logger.error("Error loading firmware manifest file")
|
|
626
|
+
return {}
|
|
627
|
+
return {}
|
|
628
|
+
|
|
629
|
+
def add_firmware(self, name: str, path: str, device_type: str, version: str,
|
|
630
|
+
flash_options: Optional[Dict[str, Any]] = None) -> bool:
|
|
631
|
+
"""Add firmware to registry"""
|
|
632
|
+
try:
|
|
633
|
+
from pathlib import Path
|
|
634
|
+
firmware_path = Path(path)
|
|
635
|
+
if not firmware_path.exists():
|
|
636
|
+
self.logger.error(f"Firmware file not found: {path}")
|
|
637
|
+
return False
|
|
638
|
+
|
|
639
|
+
firmware_info = {
|
|
640
|
+
"path": str(firmware_path),
|
|
641
|
+
"device_type": device_type,
|
|
642
|
+
"version": version
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if flash_options:
|
|
646
|
+
firmware_info["flash_options"] = flash_options
|
|
647
|
+
|
|
648
|
+
self.manifests[name] = firmware_info
|
|
649
|
+
self._save_manifests()
|
|
650
|
+
self.logger.info(f"Successfully added firmware: {name}")
|
|
651
|
+
return True
|
|
652
|
+
|
|
653
|
+
except Exception as e:
|
|
654
|
+
self.logger.error(f"Error adding firmware: {str(e)}")
|
|
655
|
+
return False
|
|
656
|
+
|
|
657
|
+
def get_firmware_info(self, name: str) -> Optional[Dict]:
|
|
658
|
+
"""Get information about specific firmware"""
|
|
659
|
+
return self.manifests.get(name)
|
|
660
|
+
|
|
661
|
+
def list_firmware(self) -> List[Dict]:
|
|
662
|
+
"""List all available firmware"""
|
|
663
|
+
return [{"name": name, **info} for name, info in self.manifests.items()]
|
|
664
|
+
|
|
665
|
+
def remove_firmware(self, name: str) -> bool:
|
|
666
|
+
"""Remove firmware from registry"""
|
|
667
|
+
try:
|
|
668
|
+
if name not in self.manifests:
|
|
669
|
+
self.logger.error(f"Firmware not found: {name}")
|
|
670
|
+
return False
|
|
671
|
+
|
|
672
|
+
del self.manifests[name]
|
|
673
|
+
self._save_manifests()
|
|
674
|
+
self.logger.info(f"Successfully removed firmware: {name}")
|
|
675
|
+
return True
|
|
676
|
+
|
|
677
|
+
except Exception as e:
|
|
678
|
+
self.logger.error(f"Error removing firmware: {str(e)}")
|
|
679
|
+
return False
|
|
680
|
+
|
|
681
|
+
def download_firmware(self, url: str, output_path: Optional[str] = None) -> Optional[str]:
|
|
682
|
+
"""Download firmware from URL"""
|
|
683
|
+
try:
|
|
684
|
+
import requests
|
|
685
|
+
self.logger.info(f"Downloading firmware from: {url}")
|
|
686
|
+
response = requests.get(url, stream=True)
|
|
687
|
+
response.raise_for_status()
|
|
688
|
+
|
|
689
|
+
if not output_path:
|
|
690
|
+
filename = url.split('/')[-1]
|
|
691
|
+
if not filename:
|
|
692
|
+
filename = "downloaded_firmware.bin"
|
|
693
|
+
output_path = str(self.conf_dir / filename)
|
|
694
|
+
|
|
695
|
+
with open(output_path, 'wb') as f:
|
|
696
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
697
|
+
f.write(chunk)
|
|
698
|
+
|
|
699
|
+
self.logger.info(f"Firmware downloaded to: {output_path}")
|
|
700
|
+
return output_path
|
|
701
|
+
|
|
702
|
+
except Exception as e:
|
|
703
|
+
self.logger.error(f"Error downloading firmware: {str(e)}")
|
|
704
|
+
return None
|
|
705
|
+
|
|
706
|
+
def flash_registered_firmware(self, name: str, options: Optional[Dict[str, Any]] = None) -> bool:
|
|
707
|
+
"""
|
|
708
|
+
Flash firmware from registry using appropriate programmer based on device type.
|
|
709
|
+
|
|
710
|
+
Args:
|
|
711
|
+
name: Name of registered firmware
|
|
712
|
+
options: Additional options to override defaults
|
|
713
|
+
|
|
714
|
+
Returns:
|
|
715
|
+
bool: True if successful, False otherwise
|
|
716
|
+
"""
|
|
717
|
+
try:
|
|
718
|
+
if name not in self.manifests:
|
|
719
|
+
self.logger.error(f"Firmware not found: {name}")
|
|
720
|
+
return False
|
|
721
|
+
|
|
722
|
+
firmware_info = self.manifests[name]
|
|
723
|
+
device_type = firmware_info.get('device_type', '').lower()
|
|
724
|
+
firmware_path = firmware_info['path']
|
|
725
|
+
|
|
726
|
+
# Merge options from manifest and provided options
|
|
727
|
+
flash_options = firmware_info.get('flash_options', {}).copy()
|
|
728
|
+
if options:
|
|
729
|
+
flash_options.update(options)
|
|
730
|
+
|
|
731
|
+
# Route to appropriate programmer based on device type
|
|
732
|
+
if device_type.startswith('esp32'):
|
|
733
|
+
if 'files' in flash_options:
|
|
734
|
+
# Multi-file ESP32 flash
|
|
735
|
+
result = self.esp32.flash_multi(
|
|
736
|
+
port=flash_options.get('port', '/dev/ttyUSB0'),
|
|
737
|
+
files=flash_options['files'],
|
|
738
|
+
chip=flash_options.get('chip', 'esp32s3'),
|
|
739
|
+
baud=flash_options.get('baud', '460800')
|
|
740
|
+
)
|
|
741
|
+
else:
|
|
742
|
+
# Single file ESP32 flash
|
|
743
|
+
result = self.esp32.flash_single(
|
|
744
|
+
port=flash_options.get('port', '/dev/ttyUSB0'),
|
|
745
|
+
firmware_path=firmware_path,
|
|
746
|
+
address=flash_options.get('address', '0x10000'),
|
|
747
|
+
chip=flash_options.get('chip', 'esp32'),
|
|
748
|
+
baud=flash_options.get('baud', '460800')
|
|
749
|
+
)
|
|
750
|
+
return result.success
|
|
751
|
+
|
|
752
|
+
elif device_type.startswith('stm32'):
|
|
753
|
+
result = self.stm32.flash_firmware(
|
|
754
|
+
firmware_path=firmware_path,
|
|
755
|
+
interface=flash_options.get('interface', 'stlink'),
|
|
756
|
+
target=flash_options.get('target', 'stm32f4x')
|
|
757
|
+
)
|
|
758
|
+
return result.success
|
|
759
|
+
|
|
760
|
+
elif device_type == 'dfu':
|
|
761
|
+
result = self.dfu.flash_firmware(
|
|
762
|
+
firmware_path=firmware_path,
|
|
763
|
+
vid=flash_options.get('vid'),
|
|
764
|
+
pid=flash_options.get('pid'),
|
|
765
|
+
alt=flash_options.get('alt')
|
|
766
|
+
)
|
|
767
|
+
return result.success
|
|
768
|
+
|
|
769
|
+
elif device_type.startswith('fpga'):
|
|
770
|
+
target = flash_options.get('target', 'sram').lower()
|
|
771
|
+
if target == 'sram':
|
|
772
|
+
result = self.fpga.load_sram(
|
|
773
|
+
bitstream_path=firmware_path,
|
|
774
|
+
board=flash_options.get('board'),
|
|
775
|
+
cable=flash_options.get('cable')
|
|
776
|
+
)
|
|
777
|
+
else:
|
|
778
|
+
result = self.fpga.flash_bitstream(
|
|
779
|
+
bitstream_path=firmware_path,
|
|
780
|
+
board=flash_options.get('board'),
|
|
781
|
+
cable=flash_options.get('cable'),
|
|
782
|
+
external_flash=flash_options.get('external_flash', False)
|
|
783
|
+
)
|
|
784
|
+
return result.success
|
|
785
|
+
|
|
786
|
+
elif device_type.startswith('greatfet'):
|
|
787
|
+
result = self.greatfet.flash_firmware(
|
|
788
|
+
firmware_path=firmware_path,
|
|
789
|
+
target=flash_options.get('target', 'spi'),
|
|
790
|
+
serial=flash_options.get('serial'),
|
|
791
|
+
board=flash_options.get('board')
|
|
792
|
+
)
|
|
793
|
+
return result.success
|
|
794
|
+
|
|
795
|
+
else:
|
|
796
|
+
self.logger.error(f"Unsupported device type: {device_type}")
|
|
797
|
+
return False
|
|
798
|
+
|
|
799
|
+
except Exception as e:
|
|
800
|
+
self.logger.error(f"Error flashing firmware: {str(e)}")
|
|
801
|
+
return False
|
|
802
|
+
|
|
803
|
+
def _save_manifests(self):
|
|
804
|
+
"""Save firmware manifests to JSON file"""
|
|
805
|
+
try:
|
|
806
|
+
import json
|
|
807
|
+
with open(self.manifest_file, 'w') as f:
|
|
808
|
+
json.dump(self.manifests, f, indent=2)
|
|
809
|
+
except Exception as e:
|
|
810
|
+
self.logger.error(f"Error saving firmware manifest: {str(e)}")
|
|
811
|
+
|
|
812
|
+
class NetworkToolService(ToolService):
|
|
813
|
+
"""Tool service specialized for network operations"""
|
|
814
|
+
|
|
815
|
+
def __init__(self):
|
|
816
|
+
super().__init__()
|
|
817
|
+
# Pre-register common network tools
|
|
818
|
+
self.register_tool('nmap', '7.0', required=False)
|
|
819
|
+
self.register_tool('masscan', required=False)
|
|
820
|
+
self.register_tool('curl', required=False)
|
|
821
|
+
self.register_tool('wget', required=False)
|
|
822
|
+
self.register_tool('ping', required=False)
|
|
823
|
+
self.register_tool('hydra', required=False)
|
|
824
|
+
|
|
825
|
+
def port_scan(self, target: str, ports: str = "1-1000",
|
|
826
|
+
scan_type: str = "syn") -> ExecutionResult:
|
|
827
|
+
"""
|
|
828
|
+
Perform port scan using nmap.
|
|
829
|
+
|
|
830
|
+
Args:
|
|
831
|
+
target: Target IP or hostname
|
|
832
|
+
ports: Port range (default: 1-1000)
|
|
833
|
+
scan_type: Scan type (syn, tcp, udp, ping)
|
|
834
|
+
|
|
835
|
+
Returns:
|
|
836
|
+
ExecutionResult: Result of the scan
|
|
837
|
+
"""
|
|
838
|
+
if not self.is_tool_available('nmap'):
|
|
839
|
+
raise RuntimeError("nmap is not available")
|
|
840
|
+
|
|
841
|
+
scan_args = {
|
|
842
|
+
'syn': ['-sS'],
|
|
843
|
+
'tcp': ['-sT'],
|
|
844
|
+
'udp': ['-sU'],
|
|
845
|
+
'ping': ['-sn']
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
args = scan_args.get(scan_type, ['-sS'])
|
|
849
|
+
args.extend(['-p', ports, target])
|
|
850
|
+
|
|
851
|
+
return self.execute_tool('nmap', args, timeout=300)
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
class BinaryToolService(ToolService):
|
|
856
|
+
"""Tool service specialized for binary analysis"""
|
|
857
|
+
|
|
858
|
+
def __init__(self):
|
|
859
|
+
super().__init__()
|
|
860
|
+
# Pre-register common binary analysis tools
|
|
861
|
+
self.register_tool('strings', required=False)
|
|
862
|
+
self.register_tool('binwalk', required=False)
|
|
863
|
+
self.register_tool('objdump', required=False)
|
|
864
|
+
self.register_tool('readelf', required=False)
|
|
865
|
+
self.register_tool('file', required=False)
|
|
866
|
+
|
|
867
|
+
def extract_strings(self, file_path: str, min_length: int = 4) -> ExecutionResult:
|
|
868
|
+
"""
|
|
869
|
+
Extract strings from binary file.
|
|
870
|
+
|
|
871
|
+
Args:
|
|
872
|
+
file_path: Path to binary file
|
|
873
|
+
min_length: Minimum string length
|
|
874
|
+
|
|
875
|
+
Returns:
|
|
876
|
+
ExecutionResult: Result containing extracted strings
|
|
877
|
+
"""
|
|
878
|
+
if not self.is_tool_available('strings'):
|
|
879
|
+
raise RuntimeError("strings tool is not available")
|
|
880
|
+
|
|
881
|
+
args = [f'-n{min_length}', file_path]
|
|
882
|
+
return self.execute_tool('strings', args)
|
|
883
|
+
|
|
884
|
+
def analyze_firmware(self, file_path: str, extract: bool = False) -> ExecutionResult:
|
|
885
|
+
"""
|
|
886
|
+
Analyze firmware file with binwalk.
|
|
887
|
+
|
|
888
|
+
Args:
|
|
889
|
+
file_path: Path to firmware file
|
|
890
|
+
extract: Whether to extract found files
|
|
891
|
+
|
|
892
|
+
Returns:
|
|
893
|
+
ExecutionResult: Result of the analysis
|
|
894
|
+
"""
|
|
895
|
+
if not self.is_tool_available('binwalk'):
|
|
896
|
+
raise RuntimeError("binwalk is not available")
|
|
897
|
+
|
|
898
|
+
args = []
|
|
899
|
+
if extract:
|
|
900
|
+
args.append('-e')
|
|
901
|
+
args.append(file_path)
|
|
902
|
+
|
|
903
|
+
return self.execute_tool('binwalk', args)
|
|
904
|
+
|
|
905
|
+
_tool_service_instance = None
|
|
906
|
+
_firmware_service_instance = None
|
|
907
|
+
_network_service_instance = None
|
|
908
|
+
_binary_service_instance = None
|
|
909
|
+
|
|
910
|
+
def get_tool_service() -> ToolService:
|
|
911
|
+
"""Get singleton ToolService instance"""
|
|
912
|
+
global _tool_service_instance
|
|
913
|
+
if _tool_service_instance is None:
|
|
914
|
+
_tool_service_instance = ToolService()
|
|
915
|
+
return _tool_service_instance
|
|
916
|
+
|
|
917
|
+
def get_firmware_service() -> FirmwareToolService:
|
|
918
|
+
"""Get singleton FirmwareToolService instance"""
|
|
919
|
+
global _firmware_service_instance
|
|
920
|
+
if _firmware_service_instance is None:
|
|
921
|
+
_firmware_service_instance = FirmwareToolService()
|
|
922
|
+
return _firmware_service_instance
|
|
923
|
+
|
|
924
|
+
def get_network_service() -> NetworkToolService:
|
|
925
|
+
"""Get singleton NetworkToolService instance"""
|
|
926
|
+
global _network_service_instance
|
|
927
|
+
if _network_service_instance is None:
|
|
928
|
+
_network_service_instance = NetworkToolService()
|
|
929
|
+
return _network_service_instance
|
|
930
|
+
|
|
931
|
+
def get_binary_service() -> BinaryToolService:
|
|
932
|
+
"""Get singleton BinaryToolService instance"""
|
|
933
|
+
global _binary_service_instance
|
|
934
|
+
if _binary_service_instance is None:
|
|
935
|
+
_binary_service_instance = BinaryToolService()
|
|
936
|
+
return _binary_service_instance
|