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,1166 @@
1
+ #!/usr/bin/env python3
2
+ """Tool management: discovery, validation, and cross-platform execution."""
3
+
4
+ import os
5
+ import sys
6
+ import shutil
7
+ import subprocess
8
+ import platform
9
+ import json
10
+ import logging
11
+ from abc import ABC, abstractmethod
12
+ from pathlib import Path
13
+ from typing import Dict, List, Optional, Union, Any, Tuple
14
+ from dataclasses import dataclass, field
15
+ from enum import Enum
16
+ import threading
17
+ import time
18
+ from contextlib import contextmanager
19
+
20
+ # Import ExecutionResult from execution_backend to avoid duplication
21
+ from .execution_backend import ExecutionResult, get_execution_backend_manager
22
+ from .tool_config import get_tool_config_manager, ToolConfig
23
+ from .execution_queue import get_execution_queue, TaskPriority
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ class ToolStatus(Enum):
28
+ """Tool availability status"""
29
+ AVAILABLE = "available"
30
+ MISSING = "missing"
31
+ INVALID = "invalid"
32
+ PERMISSION_DENIED = "permission_denied"
33
+ VERSION_MISMATCH = "version_mismatch"
34
+
35
+ @dataclass
36
+ class ToolInfo:
37
+ """Information about a tool"""
38
+ name: str
39
+ path: Optional[str] = None
40
+ version: Optional[str] = None
41
+ status: ToolStatus = ToolStatus.MISSING
42
+ aliases: List[str] = field(default_factory=list)
43
+ dependencies: List[str] = field(default_factory=list)
44
+ platforms: List[str] = field(default_factory=lambda: ["linux", "darwin", "windows"])
45
+ min_version: Optional[str] = None
46
+ max_version: Optional[str] = None
47
+ last_checked: Optional[float] = None
48
+ metadata: Dict[str, Any] = field(default_factory=dict)
49
+
50
+ @dataclass
51
+ class CategoryInfo:
52
+ """Information about the tool category"""
53
+ name: str
54
+ description: str
55
+ tools: List[str] = field(default_factory=list)
56
+
57
+ class SystemHealth(Enum):
58
+ """System health status"""
59
+ HEALTHY = "healthy"
60
+ DEGRADED = "degraded"
61
+ CRITICAL = "critical"
62
+
63
+ @dataclass
64
+ class SystemStatus:
65
+ """Overall system status"""
66
+ health: SystemHealth
67
+ total_tools: int
68
+ available_tools: int
69
+ missing_tools: int
70
+ required_missing: int
71
+ can_operate: bool
72
+ recommendations: List[str] = field(default_factory=list)
73
+
74
+ class ToolRegistry:
75
+ """Central registry for managing tool information"""
76
+
77
+ def __init__(self, config_path: Optional[str] = None):
78
+ self.tools: Dict[str, ToolInfo] = {}
79
+ self.config_path = config_path or self._get_default_config_path()
80
+ self._lock = threading.RLock()
81
+ self._load_config()
82
+
83
+ def _get_default_config_path(self) -> str:
84
+ """Get default configuration file path, preferring project-specific config"""
85
+ # First try project-specific configuration
86
+ project_config = Path('conf') / 'tools.json'
87
+ if project_config.exists():
88
+ return str(project_config)
89
+
90
+ # Fall back to user-specific configuration
91
+ config_dir = Path.home() / ".iotsploit"
92
+ config_dir.mkdir(exist_ok=True)
93
+ return str(config_dir / "tools.json")
94
+
95
+ def _load_config(self):
96
+ """Load tool configuration from file"""
97
+ if os.path.exists(self.config_path):
98
+ try:
99
+ with open(self.config_path, 'r') as f:
100
+ config = json.load(f)
101
+ for tool_name, tool_data in config.get('tools', {}).items():
102
+ # Convert status string back to enum if needed
103
+ if 'status' in tool_data and isinstance(tool_data['status'], str):
104
+ try:
105
+ tool_data['status'] = ToolStatus(tool_data['status'])
106
+ except ValueError:
107
+ # If status string is invalid, default to MISSING
108
+ tool_data['status'] = ToolStatus.MISSING
109
+ self.tools[tool_name] = ToolInfo(**tool_data)
110
+ logger.info(f"Loaded {len(self.tools)} tools from config")
111
+ except Exception as e:
112
+ logger.error(f"Failed to load tool config: {e}")
113
+ else:
114
+ self._create_default_config()
115
+
116
+ def _create_default_config(self):
117
+ """Create default tool configuration"""
118
+ default_tools = {
119
+ 'nmap': ToolInfo(
120
+ name='nmap',
121
+ aliases=['nmap'],
122
+ platforms=['linux', 'darwin', 'windows'],
123
+ min_version='7.0'
124
+ ),
125
+ 'adb': ToolInfo(
126
+ name='adb',
127
+ aliases=['adb'],
128
+ platforms=['linux', 'darwin', 'windows']
129
+ ),
130
+ 'openssl': ToolInfo(
131
+ name='openssl',
132
+ aliases=['openssl'],
133
+ platforms=['linux', 'darwin', 'windows']
134
+ ),
135
+ 'binwalk': ToolInfo(
136
+ name='binwalk',
137
+ aliases=['binwalk'],
138
+ platforms=['linux', 'darwin']
139
+ ),
140
+ 'john': ToolInfo(
141
+ name='john',
142
+ aliases=['john', 'john-the-ripper'],
143
+ platforms=['linux', 'darwin', 'windows']
144
+ ),
145
+ 'hashcat': ToolInfo(
146
+ name='hashcat',
147
+ aliases=['hashcat'],
148
+ platforms=['linux', 'darwin', 'windows']
149
+ )
150
+ }
151
+
152
+ for name, tool_info in default_tools.items():
153
+ self.tools[name] = tool_info
154
+
155
+ self.save_config()
156
+
157
+ def save_config(self):
158
+ """Save tool configuration to file"""
159
+ try:
160
+ config = {
161
+ 'tools': {
162
+ name: {
163
+ 'name': tool.name,
164
+ 'path': tool.path,
165
+ 'version': tool.version,
166
+ 'status': tool.status.value if hasattr(tool.status, 'value') else str(tool.status),
167
+ 'aliases': tool.aliases,
168
+ 'dependencies': tool.dependencies,
169
+ 'platforms': tool.platforms,
170
+ 'min_version': tool.min_version,
171
+ 'max_version': tool.max_version,
172
+ 'last_checked': tool.last_checked,
173
+ 'metadata': tool.metadata
174
+ }
175
+ for name, tool in self.tools.items()
176
+ }
177
+ }
178
+
179
+ with open(self.config_path, 'w') as f:
180
+ json.dump(config, f, indent=2)
181
+ logger.debug(f"Saved tool config to {self.config_path}")
182
+ except Exception as e:
183
+ logger.error(f"Failed to save tool config: {e}")
184
+ # Add more detailed error information
185
+ import traceback
186
+ logger.debug(f"Save config traceback: {traceback.format_exc()}")
187
+
188
+ def register_tool(self, tool_info: ToolInfo, save_config: bool = True):
189
+ """Register a new tool"""
190
+ with self._lock:
191
+ self.tools[tool_info.name] = tool_info
192
+ if save_config:
193
+ self.save_config()
194
+
195
+ def get_tool(self, name: str) -> Optional[ToolInfo]:
196
+ """Get tool information by name or alias"""
197
+ with self._lock:
198
+ # Direct name match
199
+ if name in self.tools:
200
+ return self.tools[name]
201
+
202
+ # Alias match
203
+ for tool in self.tools.values():
204
+ if name in tool.aliases:
205
+ return tool
206
+
207
+ return None
208
+
209
+ def list_tools(self) -> List[ToolInfo]:
210
+ """List all registered tools"""
211
+ with self._lock:
212
+ return list(self.tools.values())
213
+
214
+ class PathResolver:
215
+ """Resolves tool paths across different platforms"""
216
+
217
+ def __init__(self):
218
+ self.platform = platform.system().lower()
219
+ self.search_paths = self._get_search_paths()
220
+ self._path_cache: Dict[str, str] = {}
221
+
222
+ def _get_search_paths(self) -> List[str]:
223
+ """Get platform-specific search paths"""
224
+ paths = []
225
+
226
+ # System PATH
227
+ system_path = os.environ.get('PATH', '')
228
+ if system_path:
229
+ paths.extend(system_path.split(os.pathsep))
230
+
231
+ # Platform-specific paths
232
+ if self.platform == 'linux':
233
+ paths.extend([
234
+ '/usr/bin', '/usr/local/bin', '/bin', '/sbin',
235
+ '/usr/sbin', '/usr/local/sbin', '/opt/bin'
236
+ ])
237
+ elif self.platform == 'darwin': # macOS
238
+ paths.extend([
239
+ '/usr/bin', '/usr/local/bin', '/bin', '/sbin',
240
+ '/usr/sbin', '/opt/homebrew/bin', '/usr/local/sbin'
241
+ ])
242
+ elif self.platform == 'windows':
243
+ paths.extend([
244
+ 'C:\\Windows\\System32', 'C:\\Windows',
245
+ 'C:\\Program Files', 'C:\\Program Files (x86)'
246
+ ])
247
+
248
+ # User-specific paths
249
+ home = Path.home()
250
+ paths.extend([
251
+ str(home / 'bin'),
252
+ str(home / '.local' / 'bin'),
253
+ str(home / 'go' / 'bin')
254
+ ])
255
+
256
+ return [p for p in paths if p and os.path.exists(p)]
257
+
258
+ def resolve_tool_path(self, tool_name: str, aliases: List[str] = None) -> Optional[str]:
259
+ """Resolve the full path to a tool"""
260
+ # Check cache first
261
+ cache_key = f"{tool_name}:{','.join(aliases or [])}"
262
+ if cache_key in self._path_cache:
263
+ cached_path = self._path_cache[cache_key]
264
+ if os.path.exists(cached_path):
265
+ return cached_path
266
+ else:
267
+ # Remove invalid cache entry
268
+ del self._path_cache[cache_key]
269
+
270
+ # Try tool name and aliases
271
+ names_to_try = [tool_name] + (aliases or [])
272
+
273
+ for name in names_to_try:
274
+ # Try shutil.which first (respects PATH)
275
+ path = shutil.which(name)
276
+ if path and self._validate_executable(path):
277
+ self._path_cache[cache_key] = path
278
+ return path
279
+
280
+ # Manual search in known paths
281
+ for search_path in self.search_paths:
282
+ potential_path = os.path.join(search_path, name)
283
+
284
+ # Add .exe extension on Windows
285
+ if self.platform == 'windows' and not potential_path.endswith('.exe'):
286
+ potential_path += '.exe'
287
+
288
+ if os.path.exists(potential_path) and self._validate_executable(potential_path):
289
+ self._path_cache[cache_key] = potential_path
290
+ return potential_path
291
+
292
+ return None
293
+
294
+ def _validate_executable(self, path: str) -> bool:
295
+ """Validate that a path points to an executable file"""
296
+ try:
297
+ return os.path.isfile(path) and os.access(path, os.X_OK)
298
+ except (OSError, PermissionError):
299
+ return False
300
+
301
+ class ToolValidator:
302
+ """Validates tool availability and versions"""
303
+
304
+ def __init__(self, path_resolver: PathResolver):
305
+ self.path_resolver = path_resolver
306
+
307
+ def validate_tool(self, tool_info: ToolInfo) -> ToolInfo:
308
+ """Validate a tool and update its status"""
309
+ # Check if tool is supported on current platform
310
+ current_platform = platform.system().lower()
311
+ if current_platform not in tool_info.platforms:
312
+ tool_info.status = ToolStatus.INVALID
313
+ return tool_info
314
+
315
+ # Resolve tool path
316
+ if not tool_info.path:
317
+ tool_info.path = self.path_resolver.resolve_tool_path(
318
+ tool_info.name, tool_info.aliases
319
+ )
320
+
321
+ if not tool_info.path:
322
+ tool_info.status = ToolStatus.MISSING
323
+ return tool_info
324
+
325
+ # Validate executable permissions
326
+ if not os.access(tool_info.path, os.X_OK):
327
+ tool_info.status = ToolStatus.PERMISSION_DENIED
328
+ return tool_info
329
+
330
+ # Get and validate version
331
+ try:
332
+ version = self._get_tool_version(tool_info)
333
+ if version:
334
+ tool_info.version = version
335
+ if self._check_version_compatibility(tool_info):
336
+ tool_info.status = ToolStatus.AVAILABLE
337
+ else:
338
+ tool_info.status = ToolStatus.VERSION_MISMATCH
339
+ else:
340
+ tool_info.status = ToolStatus.AVAILABLE # Version check failed but tool exists
341
+ except Exception as e:
342
+ logger.warning(f"Version check failed for {tool_info.name}: {e}")
343
+ tool_info.status = ToolStatus.AVAILABLE # Assume available if version check fails
344
+
345
+ tool_info.last_checked = time.time()
346
+ return tool_info
347
+
348
+ def _get_tool_version(self, tool_info: ToolInfo) -> Optional[str]:
349
+ """Get tool version"""
350
+ version_commands = [
351
+ ['--version'],
352
+ ['-v'],
353
+ ['-V'],
354
+ ['version'],
355
+ ['--help'] # Some tools only show version in help
356
+ ]
357
+
358
+ for cmd_args in version_commands:
359
+ try:
360
+ result = subprocess.run(
361
+ [tool_info.path] + cmd_args,
362
+ capture_output=True,
363
+ text=True,
364
+ timeout=10
365
+ )
366
+
367
+ # Look for version in stdout or stderr
368
+ output = result.stdout + result.stderr
369
+ version = self._extract_version(output)
370
+ if version:
371
+ return version
372
+
373
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError):
374
+ continue
375
+
376
+ return None
377
+
378
+ def _extract_version(self, output: str) -> Optional[str]:
379
+ """Extract version number from command output"""
380
+ import re
381
+
382
+ # Common version patterns
383
+ patterns = [
384
+ r'version\s+(\d+\.\d+(?:\.\d+)?)',
385
+ r'v(\d+\.\d+(?:\.\d+)?)',
386
+ r'(\d+\.\d+(?:\.\d+)?)',
387
+ r'Version:\s*(\d+\.\d+(?:\.\d+)?)'
388
+ ]
389
+
390
+ for pattern in patterns:
391
+ match = re.search(pattern, output, re.IGNORECASE)
392
+ if match:
393
+ return match.group(1)
394
+
395
+ return None
396
+
397
+ def _check_version_compatibility(self, tool_info: ToolInfo) -> bool:
398
+ """Check if tool version meets requirements"""
399
+ if not tool_info.version:
400
+ return True # No version info, assume compatible
401
+
402
+ try:
403
+ from packaging import version
404
+ tool_version = version.parse(tool_info.version)
405
+
406
+ if tool_info.min_version:
407
+ min_ver = version.parse(tool_info.min_version)
408
+ if tool_version < min_ver:
409
+ return False
410
+
411
+ if tool_info.max_version:
412
+ max_ver = version.parse(tool_info.max_version)
413
+ if tool_version > max_ver:
414
+ return False
415
+
416
+ return True
417
+ except Exception:
418
+ # If version parsing fails, assume compatible
419
+ return True
420
+
421
+ class ToolExecutor:
422
+ """Executes tools with proper environment and error handling"""
423
+
424
+ def __init__(self, tool_registry: ToolRegistry, validator: ToolValidator):
425
+ self.registry = tool_registry
426
+ self.validator = validator
427
+ self.backend_manager = get_execution_backend_manager()
428
+
429
+ def execute_tool(self, tool_name: str, args: List[str],
430
+ working_dir: Optional[str] = None,
431
+ env: Optional[Dict[str, str]] = None,
432
+ timeout: Optional[int] = None,
433
+ backend: Optional[str] = None) -> ExecutionResult:
434
+ """Execute a tool with given arguments"""
435
+
436
+ # Get tool info
437
+ tool_info = self.registry.get_tool(tool_name)
438
+ if not tool_info:
439
+ raise ValueError(f"Tool '{tool_name}' not found in registry")
440
+
441
+ # Validate tool
442
+ tool_info = self.validator.validate_tool(tool_info)
443
+ if tool_info.status != ToolStatus.AVAILABLE:
444
+ raise RuntimeError(f"Tool '{tool_name}' is not available: {tool_info.status.value}")
445
+
446
+ logger.info(f"Executing: {tool_info.path} {' '.join(args)}")
447
+
448
+ # Execute using backend manager
449
+ return self.backend_manager.execute(
450
+ tool_path=tool_info.path,
451
+ args=args,
452
+ backend=backend,
453
+ working_dir=working_dir,
454
+ env=env,
455
+ timeout=timeout
456
+ )
457
+
458
+ class ToolManager:
459
+ """Main tool management interface"""
460
+
461
+ def __init__(self, config_path: Optional[str] = None):
462
+ self.registry = ToolRegistry(config_path)
463
+ self.path_resolver = PathResolver()
464
+ self.validator = ToolValidator(self.path_resolver)
465
+ self.executor = ToolExecutor(self.registry, self.validator)
466
+ self.config_manager = get_tool_config_manager()
467
+ self.backend_manager = get_execution_backend_manager()
468
+ self.queue_manager = get_execution_queue()
469
+ # Initialize tools from JSON configuration
470
+ self._initialize_tools()
471
+
472
+ def discover_tools(self) -> Dict[str, ToolStatus]:
473
+ """Discover and validate all registered tools"""
474
+ results = {}
475
+
476
+ for tool_name, tool_info in self.registry.tools.items():
477
+ validated_tool = self.validator.validate_tool(tool_info)
478
+ self.registry.tools[tool_name] = validated_tool
479
+ results[tool_name] = validated_tool.status
480
+
481
+ self.registry.save_config()
482
+ return results
483
+
484
+ def get_tool_status(self, tool_name: str) -> Optional[ToolStatus]:
485
+ """Get the status of a specific tool"""
486
+ tool_info = self.registry.get_tool(tool_name)
487
+ if not tool_info:
488
+ return None
489
+
490
+ # Refresh status if not checked recently
491
+ if not tool_info.last_checked or (time.time() - tool_info.last_checked) > 3600:
492
+ tool_info = self.validator.validate_tool(tool_info)
493
+ self.registry.tools[tool_info.name] = tool_info
494
+ self.registry.save_config()
495
+
496
+ return tool_info.status
497
+
498
+ def execute(self, tool_name: str, args: List[str], **kwargs) -> ExecutionResult:
499
+ """Execute a tool with arguments"""
500
+ return self.executor.execute_tool(tool_name, args, **kwargs)
501
+
502
+ def is_available(self, tool_name: str) -> bool:
503
+ """Check if a tool is available for use"""
504
+ status = self.get_tool_status(tool_name)
505
+ return status == ToolStatus.AVAILABLE
506
+
507
+ def list_available_tools(self) -> List[str]:
508
+ """List all available tools"""
509
+ available = []
510
+ for tool_name in self.registry.tools.keys():
511
+ if self.is_available(tool_name):
512
+ available.append(tool_name)
513
+ return available
514
+
515
+ def add_tool(self, name: str, path: Optional[str] = None,
516
+ aliases: List[str] = None, save_config: bool = True, **kwargs) -> bool:
517
+ """Add a new tool to the registry"""
518
+ tool_info = ToolInfo(
519
+ name=name,
520
+ path=path,
521
+ aliases=aliases or [],
522
+ **kwargs
523
+ )
524
+
525
+ # Validate the tool
526
+ validated_tool = self.validator.validate_tool(tool_info)
527
+ self.registry.register_tool(validated_tool, save_config=save_config)
528
+
529
+ return validated_tool.status == ToolStatus.AVAILABLE
530
+
531
+ # ========== Methods from ToolCategoryManager ==========
532
+
533
+ def _initialize_tools(self):
534
+ """Initialize all tools from JSON configuration"""
535
+ logger.info("Initializing tools from JSON config")
536
+
537
+ # Get tools from the main tools.json file
538
+ tool_configs = self.config_manager.get_tools_by_category("tools")
539
+
540
+ # If no tools found in "tools" category, try loading from the main config
541
+ if not tool_configs:
542
+ # Try to load from tools.json in conf directory first, then fallback
543
+ tools_file = Path('conf') / 'tools.json'
544
+ if not tools_file.exists():
545
+ tools_file = self.config_manager.config_dir / "tools.json"
546
+
547
+ if tools_file.exists():
548
+ with open(tools_file, 'r', encoding='utf-8') as f:
549
+ tools_data = json.load(f)
550
+
551
+ tools_added = 0
552
+ # Support tools field as dict or list
553
+ tools_section = tools_data.get('tools', [])
554
+ if isinstance(tools_section, dict):
555
+ items = tools_section.items()
556
+ else:
557
+ items = [(td.get('name', 'unknown'), td) for td in tools_section if isinstance(td, dict)]
558
+ for tool_name, tool_data in items:
559
+ try:
560
+ # Build tool_data dict and ensure it contains name
561
+ data = tool_data.copy() if isinstance(tool_data, dict) else {}
562
+ data.setdefault('name', tool_name)
563
+ tool_config = ToolConfig.from_dict(data)
564
+ tool_config.category = "tools"
565
+ # Add tool to manager (don't save config for each tool)
566
+ success = self.add_tool(
567
+ name=tool_config.name,
568
+ aliases=tool_config.aliases,
569
+ min_version=tool_config.min_version,
570
+ platforms=tool_config.platforms,
571
+ path=tool_config.path,
572
+ save_config=False
573
+ )
574
+ if success:
575
+ tools_added += 1
576
+ logger.debug(f"Added tool: {tool_config.name}")
577
+ else:
578
+ logger.warning(f"Failed to add tool: {tool_config.name}")
579
+ except Exception as e:
580
+ logger.error(f"Error adding tool {tool_name}: {e}")
581
+
582
+ # Save config once at the end
583
+ if tools_added > 0:
584
+ self.registry.save_config()
585
+ logger.info(f"Successfully initialized {tools_added} tools")
586
+ else:
587
+ # Load tools from category
588
+ tools_added = 0
589
+ for tool_config in tool_configs:
590
+ try:
591
+ success = self.add_tool(
592
+ name=tool_config.name,
593
+ aliases=tool_config.aliases,
594
+ min_version=tool_config.min_version,
595
+ platforms=tool_config.platforms,
596
+ path=tool_config.path,
597
+ save_config=False # Don't save for each tool
598
+ )
599
+
600
+ if success:
601
+ tools_added += 1
602
+ logger.debug(f"Added tool: {tool_config.name}")
603
+ else:
604
+ logger.warning(f"Failed to add tool: {tool_config.name}")
605
+
606
+ except Exception as e:
607
+ logger.error(f"Error adding tool {tool_config.name}: {e}")
608
+
609
+ # Save config once at the end
610
+ if tools_added > 0:
611
+ self.registry.save_config()
612
+ logger.info(f"Successfully initialized {tools_added} tools")
613
+
614
+ def get_category_info(self) -> CategoryInfo:
615
+ """Get category information from JSON config"""
616
+ # Try to get from tools.json in conf directory first, then fallback
617
+ tools_file = Path('conf') / 'tools.json'
618
+ if not tools_file.exists():
619
+ tools_file = self.config_manager.config_dir / "tools.json"
620
+
621
+ if tools_file.exists():
622
+ try:
623
+ with open(tools_file, 'r', encoding='utf-8') as f:
624
+ tools_data = json.load(f)
625
+
626
+ # Handle tools field, support dict or list format
627
+ tools_section = tools_data.get('tools', [])
628
+ all_tools = []
629
+
630
+ if isinstance(tools_section, dict):
631
+ # tools field is dict format: {"tool_name": {...}, ...}
632
+ all_tools = list(tools_section.keys())
633
+ elif isinstance(tools_section, list):
634
+ # tools field is list format: [{"name": "tool_name", ...}, ...]
635
+ all_tools = [tool.get('name', 'unknown') for tool in tools_section if isinstance(tool, dict)]
636
+
637
+ return CategoryInfo(
638
+ name=tools_data.get('name', 'IoTSploit Tools'),
639
+ description=tools_data.get('description', 'All tools for IoTSploit'),
640
+ tools=all_tools
641
+ )
642
+ except Exception as e:
643
+ logger.error(f"Error reading tools.json: {e}")
644
+
645
+ # Fallback
646
+ return CategoryInfo(
647
+ name="IoTSploit Tools",
648
+ description="All tools for IoTSploit security testing",
649
+ tools=[]
650
+ )
651
+
652
+ def get_available_tools(self) -> List[str]:
653
+ """Get list of available tools"""
654
+ category_info = self.get_category_info()
655
+ available = []
656
+
657
+ for tool_name in category_info.tools:
658
+ if self.is_available(tool_name):
659
+ available.append(tool_name)
660
+
661
+ return available
662
+
663
+ def get_missing_tools(self) -> List[str]:
664
+ """Get list of missing tools"""
665
+ category_info = self.get_category_info()
666
+ missing = []
667
+
668
+ for tool_name in category_info.tools:
669
+ if not self.is_available(tool_name):
670
+ missing.append(tool_name)
671
+
672
+ return missing
673
+
674
+ def get_required_tools(self) -> List[str]:
675
+ """Get list of required tools"""
676
+ category_info = self.get_category_info()
677
+ required = []
678
+
679
+ for tool_name in category_info.tools:
680
+ tool_config = self.get_tool_config(tool_name)
681
+ if tool_config and tool_config.required:
682
+ required.append(tool_name)
683
+
684
+ return required
685
+
686
+ def get_optional_tools(self) -> List[str]:
687
+ """Get list of optional tools"""
688
+ category_info = self.get_category_info()
689
+ optional = []
690
+
691
+ for tool_name in category_info.tools:
692
+ tool_config = self.get_tool_config(tool_name)
693
+ if tool_config and not tool_config.required:
694
+ optional.append(tool_name)
695
+
696
+ return optional
697
+
698
+ def validate_tools(self) -> Dict[str, Any]:
699
+ """Validate all tools"""
700
+ category_info = self.get_category_info()
701
+ required_tools = self.get_required_tools()
702
+
703
+ results = {
704
+ 'total_tools': len(category_info.tools),
705
+ 'available_tools': [],
706
+ 'missing_tools': [],
707
+ 'required_missing': [],
708
+ 'can_operate': True
709
+ }
710
+
711
+ for tool_name in category_info.tools:
712
+ if self.is_available(tool_name):
713
+ results['available_tools'].append(tool_name)
714
+ else:
715
+ results['missing_tools'].append(tool_name)
716
+ if tool_name in required_tools:
717
+ results['required_missing'].append(tool_name)
718
+
719
+ # Check if we can operate
720
+ results['can_operate'] = len(results['required_missing']) == 0
721
+
722
+ return results
723
+
724
+ def get_tool_config(self, tool_name: str) -> Optional[ToolConfig]:
725
+ """Get tool configuration from JSON"""
726
+ return self.config_manager.get_tool_config(tool_name)
727
+
728
+ def get_install_hints(self) -> Dict[str, str]:
729
+ """Get installation hints for missing tools"""
730
+ missing_tools = self.get_missing_tools()
731
+ hints = {}
732
+
733
+ for tool_name in missing_tools:
734
+ tool_config = self.get_tool_config(tool_name)
735
+ if tool_config and tool_config.install_hint:
736
+ hints[tool_name] = tool_config.install_hint
737
+
738
+ return hints
739
+
740
+ def reload_configurations(self):
741
+ """Reload all configurations from JSON files"""
742
+ self.config_manager.reload_configs()
743
+ self._initialize_tools()
744
+ logger.info("Reloaded all tool configurations from JSON files")
745
+
746
+ def get_config_stats(self) -> Dict[str, Any]:
747
+ """Get statistics about tool configurations"""
748
+ return self.config_manager.get_stats()
749
+
750
+ # Convenience methods for common operations
751
+ def flash_esp32(self, port: str, firmware_path: str, **kwargs) -> Dict[str, Any]:
752
+ """Convenience method for ESP32 flashing"""
753
+ if not self.is_available('esptool'):
754
+ return {"status": "error", "message": "esptool not available"}
755
+
756
+ args = [
757
+ '--chip', kwargs.get('chip', 'esp32s3'),
758
+ '--port', port,
759
+ '--baud', kwargs.get('baud', '460800'),
760
+ 'write_flash', kwargs.get('address', '0x10000'), firmware_path
761
+ ]
762
+
763
+ result = self.execute('esptool', args, timeout=300)
764
+ return {
765
+ "status": "success" if result.success else "error",
766
+ "message": "Flash completed" if result.success else result.stderr,
767
+ "execution_time": result.execution_time
768
+ }
769
+
770
+ def port_scan(self, target: str, ports: str = "1-1000",
771
+ scan_type: str = "syn") -> Dict[str, Any]:
772
+ """Convenience method for port scanning"""
773
+ if not self.is_available('nmap'):
774
+ return {"status": "error", "message": "nmap not available"}
775
+
776
+ scan_args = {
777
+ 'syn': ['-sS'],
778
+ 'tcp': ['-sT'],
779
+ 'udp': ['-sU'],
780
+ 'ping': ['-sn']
781
+ }
782
+
783
+ args = scan_args.get(scan_type, ['-sS'])
784
+ args.extend(['-p', ports, target])
785
+
786
+ result = self.execute('nmap', args, timeout=300)
787
+ return {
788
+ "status": "success" if result.success else "error",
789
+ "message": "Scan completed" if result.success else result.stderr,
790
+ "output": result.stdout,
791
+ "execution_time": result.execution_time
792
+ }
793
+
794
+ def extract_strings(self, file_path: str, min_length: int = 4) -> Dict[str, Any]:
795
+ """Convenience method for string extraction"""
796
+ if not self.is_available('strings'):
797
+ return {"status": "error", "message": "strings tool not available"}
798
+
799
+ args = [f'-n{min_length}', file_path]
800
+ result = self.execute('strings', args)
801
+
802
+ return {
803
+ "status": "success" if result.success else "error",
804
+ "message": "Strings extracted" if result.success else result.stderr,
805
+ "strings": result.stdout.split('\n') if result.success else [],
806
+ "execution_time": result.execution_time
807
+ }
808
+
809
+ def adb_devices(self) -> Dict[str, Any]:
810
+ """Convenience method for listing ADB devices"""
811
+ if not self.is_available('adb'):
812
+ return {"status": "error", "message": "adb not available"}
813
+
814
+ result = self.execute('adb', ['devices'])
815
+
816
+ devices = []
817
+ if result.success:
818
+ lines = result.stdout.split('\n')[1:] # Skip header
819
+ for line in lines:
820
+ line = line.strip()
821
+ if line and '\t' in line:
822
+ device_id, status = line.split('\t')
823
+ devices.append({'id': device_id, 'status': status})
824
+
825
+ return {
826
+ "status": "success" if result.success else "error",
827
+ "message": "Devices listed" if result.success else result.stderr,
828
+ "devices": devices,
829
+ "execution_time": result.execution_time
830
+ }
831
+
832
+ # ========== Methods from CentralizedToolManager ==========
833
+
834
+ def get_system_status(self) -> SystemStatus:
835
+ """Get comprehensive system status"""
836
+ validation = self.validate_tools()
837
+
838
+ # Determine health status
839
+ if validation['required_missing']:
840
+ health = SystemHealth.CRITICAL
841
+ elif validation['missing_tools']:
842
+ health = SystemHealth.DEGRADED
843
+ else:
844
+ health = SystemHealth.HEALTHY
845
+
846
+ # Generate recommendations
847
+ recommendations = []
848
+ if validation['required_missing']:
849
+ recommendations.append(f"Install {len(validation['required_missing'])} required tools")
850
+ if len(validation['missing_tools']) > len(validation['required_missing']):
851
+ optional_missing = len(validation['missing_tools']) - len(validation['required_missing'])
852
+ recommendations.append(f"Consider installing {optional_missing} optional tools")
853
+
854
+ return SystemStatus(
855
+ health=health,
856
+ total_tools=validation['total_tools'],
857
+ available_tools=len(validation['available_tools']),
858
+ missing_tools=len(validation['missing_tools']),
859
+ required_missing=len(validation['required_missing']),
860
+ can_operate=validation['can_operate'],
861
+ recommendations=recommendations
862
+ )
863
+
864
+ def get_health_report(self) -> Dict[str, Any]:
865
+ """Get detailed health report"""
866
+ status = self.get_system_status()
867
+ validation = self.validate_tools()
868
+
869
+ report = {
870
+ 'timestamp': self._get_timestamp(),
871
+ 'system_health': status.health.value,
872
+ 'can_operate': status.can_operate,
873
+ 'summary': {
874
+ 'total_tools': status.total_tools,
875
+ 'available_tools': status.available_tools,
876
+ 'missing_tools': status.missing_tools,
877
+ 'required_missing': status.required_missing
878
+ },
879
+ 'tools': {
880
+ 'available': validation['available_tools'],
881
+ 'missing': validation['missing_tools'],
882
+ 'required_missing': validation['required_missing']
883
+ },
884
+ 'recommendations': status.recommendations,
885
+ 'backend_status': self.backend_manager.get_status(),
886
+ 'queue_status': self.queue_manager.get_stats()
887
+ }
888
+
889
+ return report
890
+
891
+ def get_installation_guide(self) -> Dict[str, Any]:
892
+ """Get installation recommendations with hints"""
893
+ missing_tools = self.get_missing_tools()
894
+ install_hints = self.get_install_hints()
895
+ required_tools = self.get_required_tools()
896
+ optional_tools = self.get_optional_tools()
897
+
898
+ # Separate required and optional missing tools
899
+ required_missing = [t for t in missing_tools if t in required_tools]
900
+ optional_missing = [t for t in missing_tools if t in optional_tools]
901
+
902
+ guide = {
903
+ 'required_tools': {},
904
+ 'optional_tools': {},
905
+ 'priority_order': required_missing + optional_missing[:5] # Top 5 optional
906
+ }
907
+
908
+ for tool in required_missing:
909
+ guide['required_tools'][tool] = {
910
+ 'description': self._get_tool_description(tool),
911
+ 'install_hint': install_hints.get(tool, "No installation hint available"),
912
+ 'priority': 'HIGH'
913
+ }
914
+
915
+ for tool in optional_missing:
916
+ guide['optional_tools'][tool] = {
917
+ 'description': self._get_tool_description(tool),
918
+ 'install_hint': install_hints.get(tool, "No installation hint available"),
919
+ 'priority': 'MEDIUM'
920
+ }
921
+
922
+ return guide
923
+
924
+ def _get_tool_description(self, tool_name: str) -> str:
925
+ """Get tool description from config"""
926
+ tool_config = self.get_tool_config(tool_name)
927
+ return tool_config.description if tool_config else "No description available"
928
+
929
+ def _get_timestamp(self) -> str:
930
+ """Get current timestamp"""
931
+ from datetime import datetime
932
+ return datetime.now().isoformat()
933
+
934
+ def get_system_health(self, force_refresh: bool = False):
935
+ """Get system health with category status"""
936
+ status = self.get_system_status()
937
+ validation = self.validate_tools()
938
+
939
+ # Create a health object with category status
940
+ class HealthStatus:
941
+ def __init__(self, status, validation):
942
+ self.status = status.health.value
943
+ self.total_tools = status.total_tools
944
+ self.available_tools = status.available_tools
945
+ self.missing_tools = status.missing_tools
946
+ self.missing_critical_tools = status.required_missing
947
+ self.recommendations = status.recommendations
948
+ self.category_status = {
949
+ "tools": {
950
+ "total_tools": validation['total_tools'],
951
+ "available_tools": validation['available_tools'],
952
+ "missing_tools": validation['missing_tools'],
953
+ "can_operate": validation['can_operate']
954
+ }
955
+ }
956
+
957
+ return HealthStatus(status, validation)
958
+
959
+ def get_installation_recommendations(self) -> Dict[str, Dict[str, List[str]]]:
960
+ """Get installation recommendations by category"""
961
+ missing_tools = self.get_missing_tools()
962
+ required_tools = self.get_required_tools()
963
+
964
+ required_missing = [t for t in missing_tools if t in required_tools]
965
+ optional_missing = [t for t in missing_tools if t not in required_tools]
966
+
967
+ return {
968
+ "tools": {
969
+ "required": required_missing,
970
+ "optional": optional_missing
971
+ }
972
+ }
973
+
974
+ # Queue delegation methods
975
+ def submit_task(self, tool_name: str, args: List[str],
976
+ priority: Union[str, TaskPriority] = "normal", **kwargs) -> str:
977
+ """Submit a tool execution task to the queue"""
978
+ # Handle both string and enum priority inputs
979
+ if isinstance(priority, TaskPriority):
980
+ task_priority = priority
981
+ else:
982
+ # Convert priority string to enum
983
+ priority_map = {
984
+ 'critical': TaskPriority.CRITICAL,
985
+ 'high': TaskPriority.HIGH,
986
+ 'normal': TaskPriority.NORMAL,
987
+ 'low': TaskPriority.LOW,
988
+ 'background': TaskPriority.BACKGROUND
989
+ }
990
+ task_priority = priority_map.get(priority.lower(), TaskPriority.NORMAL)
991
+
992
+ # Get tool path
993
+ tool_info = self.registry.get_tool(tool_name)
994
+ if not tool_info:
995
+ raise ValueError(f"Tool '{tool_name}' not found")
996
+
997
+ return self.queue_manager.submit_task(
998
+ tool_name=tool_name,
999
+ tool_path=tool_info.path,
1000
+ args=args,
1001
+ priority=task_priority,
1002
+ **kwargs
1003
+ )
1004
+
1005
+ def get_task(self, task_id: str):
1006
+ """Get task by ID"""
1007
+ return self.queue_manager.get_task(task_id)
1008
+
1009
+ def wait_for_task(self, task_id: str, timeout: Optional[float] = None):
1010
+ """Wait for task completion"""
1011
+ return self.queue_manager.wait_for_task(task_id, timeout)
1012
+
1013
+ def get_queue_stats(self):
1014
+ """Get queue statistics"""
1015
+ return self.queue_manager.get_stats()
1016
+
1017
+ # Backend delegation methods
1018
+ def list_execution_backends(self) -> List[str]:
1019
+ """List available execution backends"""
1020
+ return self.backend_manager.list_available_backends()
1021
+
1022
+ def set_default_backend(self, backend_name: str):
1023
+ """Set default execution backend"""
1024
+ self.backend_manager.set_default_backend(backend_name)
1025
+
1026
+ # System management
1027
+ def initialize(self) -> bool:
1028
+ """Initialize the system"""
1029
+ try:
1030
+ logger.info("Initializing tool manager...")
1031
+
1032
+ # Discover tools
1033
+ discovery_results = self.discover_tools()
1034
+ logger.info(f"Discovered {len(discovery_results)} tools")
1035
+
1036
+ # Check system health
1037
+ status = self.get_system_status()
1038
+ logger.info(f"System health: {status.health.value}")
1039
+
1040
+ if status.health == SystemHealth.CRITICAL:
1041
+ logger.warning(f"System is in CRITICAL state - {status.required_missing} required tools missing")
1042
+
1043
+ return True
1044
+
1045
+ except Exception as e:
1046
+ logger.error(f"Failed to initialize system: {e}")
1047
+ return False
1048
+
1049
+ def cleanup(self):
1050
+ """Cleanup resources"""
1051
+ try:
1052
+ self.queue_manager.shutdown()
1053
+ logger.info("Tool manager cleaned up")
1054
+ except Exception as e:
1055
+ logger.error(f"Error during cleanup: {e}")
1056
+
1057
+ def shutdown(self, wait: bool = True):
1058
+ """Shutdown the system"""
1059
+ self.cleanup()
1060
+
1061
+ def __enter__(self):
1062
+ """Context manager entry"""
1063
+ self.initialize()
1064
+ return self
1065
+
1066
+ def __exit__(self, exc_type, exc_val, exc_tb):
1067
+ """Context manager exit"""
1068
+ self.cleanup()
1069
+
1070
+ # Convenience
1071
+ def get_tool_manager() -> ToolManager:
1072
+ """Get a singleton tool manager instance"""
1073
+ if not hasattr(get_tool_manager, '_instance'):
1074
+ get_tool_manager._instance = ToolManager()
1075
+ return get_tool_manager._instance
1076
+
1077
+ def execute_tool(tool_name: str, args: List[str], **kwargs) -> ExecutionResult:
1078
+ """Execute a tool using the default tool manager"""
1079
+ return get_tool_manager().execute(tool_name, args, **kwargs)
1080
+
1081
+ def is_tool_available(tool_name: str) -> bool:
1082
+ """Check if a tool is available"""
1083
+ return get_tool_manager().is_available(tool_name)
1084
+
1085
+ def print_system_report():
1086
+ """Print a comprehensive system report"""
1087
+ manager = get_tool_manager()
1088
+
1089
+ print("\n" + "="*60)
1090
+ print("šŸ”§ IOTSPLOIT TOOL MANAGEMENT SYSTEM REPORT")
1091
+ print("="*60)
1092
+
1093
+ # Get system status
1094
+ status = manager.get_system_status()
1095
+ health_icon = {
1096
+ "healthy": "āœ…",
1097
+ "degraded": "āš ļø",
1098
+ "critical": "āŒ"
1099
+ }.get(status.health.value, "ā“")
1100
+
1101
+ print(f"\nšŸ“Š System Health: {health_icon} {status.health.value.upper()}")
1102
+ print(f"šŸ”§ Total Tools: {status.total_tools}")
1103
+ print(f"āœ… Available: {status.available_tools}")
1104
+ print(f"āŒ Missing: {status.missing_tools}")
1105
+ print(f"šŸ”“ Required Missing: {status.required_missing}")
1106
+ print(f"šŸš€ Can Operate: {'Yes' if status.can_operate else 'No'}")
1107
+
1108
+ # Show recommendations
1109
+ if status.recommendations:
1110
+ print(f"\nšŸ’” Recommendations:")
1111
+ for rec in status.recommendations:
1112
+ print(f" • {rec}")
1113
+
1114
+ # Show queue stats
1115
+ try:
1116
+ queue_stats = manager.get_queue_stats()
1117
+ print(f"\nšŸ“‹ Queue Status:")
1118
+ print(f" • Total Tasks: {queue_stats.total_tasks}")
1119
+ print(f" • Running: {queue_stats.running_tasks}")
1120
+ print(f" • Pending: {queue_stats.pending_tasks}")
1121
+ print(f" • Completed: {queue_stats.completed_tasks}")
1122
+ print(f" • Failed: {queue_stats.failed_tasks}")
1123
+ if queue_stats.average_execution_time > 0:
1124
+ print(f" • Avg Execution Time: {queue_stats.average_execution_time:.2f}s")
1125
+ except Exception as e:
1126
+ print(f"\nšŸ“‹ Queue Status: Error - {e}")
1127
+
1128
+ # Show category info
1129
+ try:
1130
+ category_info = manager.get_category_info()
1131
+ print(f"\nšŸ“‚ Tool Category: {category_info.name}")
1132
+ print(f" Description: {category_info.description}")
1133
+ print(f" Tools: {len(category_info.tools)}")
1134
+
1135
+ # Show available vs missing
1136
+ available = manager.get_available_tools()
1137
+ missing = manager.get_missing_tools()
1138
+ required = manager.get_required_tools()
1139
+
1140
+ print(f"\nšŸ”§ Tool Breakdown:")
1141
+ print(f" āœ… Available: {len(available)}")
1142
+ if available[:5]: # Show first 5
1143
+ for tool in available[:5]:
1144
+ print(f" • {tool}")
1145
+ if len(available) > 5:
1146
+ print(f" ... and {len(available) - 5} more")
1147
+
1148
+ if missing:
1149
+ print(f" āŒ Missing: {len(missing)}")
1150
+ required_missing = [t for t in missing if t in required]
1151
+ optional_missing = [t for t in missing if t not in required]
1152
+
1153
+ if required_missing:
1154
+ print(f" šŸ”“ Required: {', '.join(required_missing[:3])}")
1155
+ if len(required_missing) > 3:
1156
+ print(f" ... and {len(required_missing) - 3} more")
1157
+
1158
+ if optional_missing:
1159
+ print(f" 🟔 Optional: {', '.join(optional_missing[:3])}")
1160
+ if len(optional_missing) > 3:
1161
+ print(f" ... and {len(optional_missing) - 3} more")
1162
+
1163
+ except Exception as e:
1164
+ print(f"\nšŸ“‚ Category Status: Error - {e}")
1165
+
1166
+ print("\n" + "="*60)