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.
Files changed (41) hide show
  1. iotsploit_core/__init__.py +7 -0
  2. iotsploit_core/context.py +27 -0
  3. iotsploit_core/core/__init__.py +0 -0
  4. iotsploit_core/core/base_plugin.py +290 -0
  5. iotsploit_core/core/device_config.py +105 -0
  6. iotsploit_core/core/device_manager.py +942 -0
  7. iotsploit_core/core/device_registry.py +118 -0
  8. iotsploit_core/core/device_scanner.py +74 -0
  9. iotsploit_core/core/device_spec.py +105 -0
  10. iotsploit_core/core/device_store.py +86 -0
  11. iotsploit_core/core/execution_backend.py +464 -0
  12. iotsploit_core/core/execution_queue.py +456 -0
  13. iotsploit_core/core/exploit_manager.py +728 -0
  14. iotsploit_core/core/exploit_spec.py +73 -0
  15. iotsploit_core/core/stream_manager.py +157 -0
  16. iotsploit_core/core/tool_config.py +430 -0
  17. iotsploit_core/core/tool_manager.py +1166 -0
  18. iotsploit_core/core/tool_service.py +936 -0
  19. iotsploit_core/domain/__init__.py +3 -0
  20. iotsploit_core/domain/device.py +70 -0
  21. iotsploit_core/domain/execution_plan.py +29 -0
  22. iotsploit_core/domain/plugin.py +17 -0
  23. iotsploit_core/domain/stream.py +63 -0
  24. iotsploit_core/domain/target.py +243 -0
  25. iotsploit_core/platforms/__init__.py +19 -0
  26. iotsploit_core/platforms/consts.py +25 -0
  27. iotsploit_core/ports/__init__.py +6 -0
  28. iotsploit_core/ports/driver_state_repo.py +15 -0
  29. iotsploit_core/ports/plugin_repo.py +26 -0
  30. iotsploit_core/ports/stream_backend.py +28 -0
  31. iotsploit_core/ports/task_runner.py +22 -0
  32. iotsploit_core/ports/wifi_backend.py +148 -0
  33. iotsploit_core/py.typed +2 -0
  34. iotsploit_core/utils/__init__.py +53 -0
  35. iotsploit_core/utils/exceptions.py +152 -0
  36. iotsploit_core/utils/helpers.py +65 -0
  37. iotsploit_core/utils/iots_logger.py +164 -0
  38. iotsploit_core/utils/result.py +41 -0
  39. iotsploit_core-0.0.6.dist-info/METADATA +117 -0
  40. iotsploit_core-0.0.6.dist-info/RECORD +41 -0
  41. 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