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,728 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
# Disable pwntools terminal mode before any plugin imports to prevent
|
|
5
|
+
# "argument must be an int, or have a fileno() method" errors in non-TTY environments
|
|
6
|
+
os.environ.setdefault("PWNLIB_NOTERM", "1")
|
|
7
|
+
|
|
8
|
+
import glob
|
|
9
|
+
import importlib
|
|
10
|
+
from importlib import metadata as importlib_metadata
|
|
11
|
+
import importlib.util
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional, Callable, Any
|
|
14
|
+
|
|
15
|
+
import pluggy
|
|
16
|
+
|
|
17
|
+
from iotsploit_core.core.base_plugin import BasePlugin
|
|
18
|
+
from iotsploit_core.core.exploit_spec import ExploitPluginSpec
|
|
19
|
+
from iotsploit_core.domain.execution_plan import GroupStepSpec, PluginGroupSpec, PluginStepSpec
|
|
20
|
+
from iotsploit_core.domain.plugin import PluginMeta
|
|
21
|
+
from iotsploit_core.ports.plugin_repo import PluginGroupRepository, PluginMetaRepository
|
|
22
|
+
from iotsploit_core.ports.task_runner import TaskRunner
|
|
23
|
+
import logging
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
class ExploitPluginManager:
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
*,
|
|
31
|
+
plugin_repo: PluginMetaRepository,
|
|
32
|
+
group_repo: PluginGroupRepository,
|
|
33
|
+
task_runner: TaskRunner,
|
|
34
|
+
plugins_dir: str | Path | None = None,
|
|
35
|
+
context_factory: Optional[Callable[[], Any]] = None,
|
|
36
|
+
):
|
|
37
|
+
self._plugin_repo = plugin_repo
|
|
38
|
+
self._group_repo = group_repo
|
|
39
|
+
self._task_runner = task_runner
|
|
40
|
+
self._context_factory = context_factory
|
|
41
|
+
|
|
42
|
+
self.plugins = {} # plugin_name -> plugin instance (loaded on demand)
|
|
43
|
+
self.plugin_registry = {} # plugin_name -> {file_path, class_name}
|
|
44
|
+
self.pm = pluggy.PluginManager("exploit_mgr")
|
|
45
|
+
self.pm.add_hookspecs(ExploitPluginSpec)
|
|
46
|
+
self.plugins_dir = Path(plugins_dir) if plugins_dir is not None else self._default_plugins_dir()
|
|
47
|
+
self.load_plugins()
|
|
48
|
+
import atexit
|
|
49
|
+
atexit.register(self.cleanup_all_plugins)
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _default_plugins_dir() -> Path | None:
|
|
53
|
+
env = os.getenv("IOTSPLOIT_EXPLOIT_PLUGINS_DIR") or os.getenv("SAT_EXPLOIT_PLUGINS_DIR")
|
|
54
|
+
if env:
|
|
55
|
+
return Path(env)
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def _normalize_target_for_plugin(target):
|
|
60
|
+
"""
|
|
61
|
+
Normalize target into a dict snapshot when possible.
|
|
62
|
+
|
|
63
|
+
Rationale:
|
|
64
|
+
- Django web path currently passes a domain object (e.g., Vehicle).
|
|
65
|
+
- sudo-runner / CLI often pass dict already.
|
|
66
|
+
- Plugins should not need Django imports; dict mode is the stable boundary.
|
|
67
|
+
"""
|
|
68
|
+
if target is None:
|
|
69
|
+
return None
|
|
70
|
+
if isinstance(target, dict):
|
|
71
|
+
return target
|
|
72
|
+
# Domain targets implement get_info() to provide a serializable snapshot.
|
|
73
|
+
if hasattr(target, "get_info"):
|
|
74
|
+
try:
|
|
75
|
+
return target.get_info()
|
|
76
|
+
except Exception:
|
|
77
|
+
# Fall back to passing through if snapshot fails
|
|
78
|
+
return target
|
|
79
|
+
return target
|
|
80
|
+
|
|
81
|
+
def _ensure_context_injected(self, plugin_instance: BasePlugin) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Ensure plugin has backend context injected (idempotent).
|
|
84
|
+
|
|
85
|
+
This method:
|
|
86
|
+
1. Checks if context was already injected (via _iots_ctx_injected flag)
|
|
87
|
+
2. If not, builds context using context_factory and calls plugin.initialize()
|
|
88
|
+
3. Marks plugin as injected to avoid duplicate initialization
|
|
89
|
+
|
|
90
|
+
Note: context_factory is provided by the application layer (wiring),
|
|
91
|
+
keeping core free of platform/env dependencies.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
plugin_instance: Plugin instance to inject context into
|
|
95
|
+
"""
|
|
96
|
+
# Skip if already injected (idempotent)
|
|
97
|
+
if getattr(plugin_instance, "_iots_ctx_injected", False):
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Initialize plugin right before execution.
|
|
102
|
+
# Important: NEVER call initialize(None). Prefer initialize(ctx), fallback to initialize().
|
|
103
|
+
if hasattr(plugin_instance, "initialize"):
|
|
104
|
+
if self._context_factory:
|
|
105
|
+
ctx = self._context_factory()
|
|
106
|
+
try:
|
|
107
|
+
plugin_instance.initialize(ctx)
|
|
108
|
+
except TypeError:
|
|
109
|
+
# Legacy plugins may expose initialize() without params
|
|
110
|
+
try:
|
|
111
|
+
plugin_instance.initialize()
|
|
112
|
+
except TypeError:
|
|
113
|
+
logger.warning(
|
|
114
|
+
"[exploit_mgr] Plugin initialize() signature mismatch; "
|
|
115
|
+
"skipping initialization"
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
# No context factory (no backend injection). Still try legacy initialize().
|
|
119
|
+
try:
|
|
120
|
+
plugin_instance.initialize()
|
|
121
|
+
except TypeError:
|
|
122
|
+
logger.debug(
|
|
123
|
+
"[exploit_mgr] No context_factory and plugin initialize() requires args; "
|
|
124
|
+
"skipping initialization for %s",
|
|
125
|
+
plugin_instance.__class__.__name__,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Mark as injected
|
|
129
|
+
plugin_instance._iots_ctx_injected = True
|
|
130
|
+
logger.debug("[exploit_mgr] Injected backend context into plugin")
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
# Log error but don't fail execution (graceful degradation)
|
|
134
|
+
logger.warning(
|
|
135
|
+
"[exploit_mgr] Failed to inject backend context: %s. "
|
|
136
|
+
"Plugin may not have backend access.", str(e)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def _iter_entry_points(group: str):
|
|
141
|
+
entry_points = importlib_metadata.entry_points()
|
|
142
|
+
if hasattr(entry_points, "select"):
|
|
143
|
+
return list(entry_points.select(group=group))
|
|
144
|
+
return list(entry_points.get(group, []))
|
|
145
|
+
|
|
146
|
+
def _upsert_plugin_meta(self, plugin_class: type[BasePlugin], module_path: str) -> str | None:
|
|
147
|
+
plugin_instance = plugin_class()
|
|
148
|
+
plugin_info = plugin_instance.get_info() or {}
|
|
149
|
+
plugin_name = plugin_info.get("Name", plugin_class.__name__)
|
|
150
|
+
|
|
151
|
+
meta = PluginMeta(
|
|
152
|
+
name=plugin_name,
|
|
153
|
+
module_path=module_path,
|
|
154
|
+
enabled=True,
|
|
155
|
+
description=str(plugin_info.get("Description", "") or ""),
|
|
156
|
+
license=str(plugin_info.get("License", "") or ""),
|
|
157
|
+
author=str(plugin_info.get("Author", "") or ""),
|
|
158
|
+
parameters=plugin_info.get("Parameters", None),
|
|
159
|
+
)
|
|
160
|
+
self._plugin_repo.upsert(meta)
|
|
161
|
+
logger.debug("[exploit_mgr] Upserted plugin meta: %s", plugin_name)
|
|
162
|
+
return plugin_name
|
|
163
|
+
|
|
164
|
+
def _discover_filesystem_plugins(self, plugins_dir: str | Path | None = None) -> set[str]:
|
|
165
|
+
discovered_plugins: set[str] = set()
|
|
166
|
+
plugins_dir = Path(plugins_dir) if plugins_dir is not None else self.plugins_dir
|
|
167
|
+
|
|
168
|
+
if plugins_dir is None:
|
|
169
|
+
return discovered_plugins
|
|
170
|
+
if not plugins_dir.exists():
|
|
171
|
+
logger.debug("[exploit_mgr] Legacy plugin directory does not exist: %s", plugins_dir)
|
|
172
|
+
return discovered_plugins
|
|
173
|
+
|
|
174
|
+
base_dir = os.path.abspath(str(plugins_dir))
|
|
175
|
+
plugin_files = glob.glob(f"{base_dir}/**/*.py", recursive=True)
|
|
176
|
+
|
|
177
|
+
for plugin_file in plugin_files:
|
|
178
|
+
try:
|
|
179
|
+
if "__init__.py" in plugin_file:
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
abs_path = os.path.abspath(plugin_file)
|
|
183
|
+
temp_mod_name = f"exploit_autodiscover_{hash(abs_path)}"
|
|
184
|
+
spec = importlib.util.spec_from_file_location(temp_mod_name, abs_path)
|
|
185
|
+
module = importlib.util.module_from_spec(spec)
|
|
186
|
+
if spec is None or spec.loader is None:
|
|
187
|
+
logger.error("[exploit_mgr] Failed to create spec for %s", abs_path)
|
|
188
|
+
continue
|
|
189
|
+
spec.loader.exec_module(module)
|
|
190
|
+
|
|
191
|
+
plugin_class = None
|
|
192
|
+
for _, obj in module.__dict__.items():
|
|
193
|
+
if isinstance(obj, type) and issubclass(obj, BasePlugin) and obj != BasePlugin:
|
|
194
|
+
plugin_class = obj
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
if plugin_class is None:
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
plugin_name = self._upsert_plugin_meta(
|
|
201
|
+
plugin_class,
|
|
202
|
+
module_path=f"file://{abs_path}::{plugin_class.__name__}",
|
|
203
|
+
)
|
|
204
|
+
if plugin_name:
|
|
205
|
+
discovered_plugins.add(plugin_name)
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.error("[exploit_mgr] Error processing plugin file %s: %s", plugin_file, str(e))
|
|
208
|
+
|
|
209
|
+
return discovered_plugins
|
|
210
|
+
|
|
211
|
+
def _discover_entry_point_plugins(self) -> set[str]:
|
|
212
|
+
discovered_plugins: set[str] = set()
|
|
213
|
+
for entry_point in self._iter_entry_points("iotsploit.exploit_plugins"):
|
|
214
|
+
try:
|
|
215
|
+
plugin_class = entry_point.load()
|
|
216
|
+
if (
|
|
217
|
+
not isinstance(plugin_class, type)
|
|
218
|
+
or not issubclass(plugin_class, BasePlugin)
|
|
219
|
+
or plugin_class is BasePlugin
|
|
220
|
+
):
|
|
221
|
+
logger.warning(
|
|
222
|
+
"[exploit_mgr] Ignoring invalid exploit entry point %s -> %s",
|
|
223
|
+
entry_point.name,
|
|
224
|
+
entry_point.value,
|
|
225
|
+
)
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
plugin_name = self._upsert_plugin_meta(
|
|
229
|
+
plugin_class,
|
|
230
|
+
module_path=f"{plugin_class.__module__}.{plugin_class.__name__}",
|
|
231
|
+
)
|
|
232
|
+
if plugin_name:
|
|
233
|
+
discovered_plugins.add(plugin_name)
|
|
234
|
+
except Exception as e:
|
|
235
|
+
logger.error(
|
|
236
|
+
"[exploit_mgr] Error loading exploit entry point %s: %s",
|
|
237
|
+
entry_point.name,
|
|
238
|
+
str(e),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return discovered_plugins
|
|
242
|
+
|
|
243
|
+
def auto_discover_plugins(self, plugins_dir: str | Path | None = None) -> None:
|
|
244
|
+
"""
|
|
245
|
+
Automatically discover plugins from the legacy filesystem and packaged entry points.
|
|
246
|
+
Entry points run last so their dotted module paths take priority over legacy file paths.
|
|
247
|
+
"""
|
|
248
|
+
if plugins_dir is None:
|
|
249
|
+
plugins_dir = self.plugins_dir
|
|
250
|
+
logger.debug("[exploit_mgr] Starting plugin auto-discovery in %s", plugins_dir)
|
|
251
|
+
|
|
252
|
+
discovered_plugins = set()
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
discovered_plugins |= self._discover_filesystem_plugins(plugins_dir)
|
|
256
|
+
discovered_plugins |= self._discover_entry_point_plugins()
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
disabled = self._plugin_repo.disable_missing(discovered_plugins)
|
|
260
|
+
if disabled:
|
|
261
|
+
logger.debug("[exploit_mgr] Marked %d missing plugins as disabled", disabled)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
logger.error("[exploit_mgr] Error disabling missing plugins: %s", str(e))
|
|
264
|
+
|
|
265
|
+
logger.debug("[exploit_mgr] Plugin auto-discovery completed")
|
|
266
|
+
except Exception as e:
|
|
267
|
+
logger.error("[exploit_mgr] Error during plugin discovery: %s", str(e))
|
|
268
|
+
|
|
269
|
+
def load_plugins(self):
|
|
270
|
+
"""
|
|
271
|
+
Build registry from DB without importing modules. Instances load lazily on first use.
|
|
272
|
+
"""
|
|
273
|
+
self.auto_discover_plugins(self.plugins_dir)
|
|
274
|
+
self.plugin_registry.clear()
|
|
275
|
+
self.plugins.clear()
|
|
276
|
+
for plugin_db in self._plugin_repo.list_enabled():
|
|
277
|
+
try:
|
|
278
|
+
module_path_field = plugin_db.module_path or ""
|
|
279
|
+
file_path = None
|
|
280
|
+
class_name = None
|
|
281
|
+
|
|
282
|
+
# Preferred: file path encoding "file://<abs_path>::<ClassName>"
|
|
283
|
+
if module_path_field.startswith("file://") and "::" in module_path_field:
|
|
284
|
+
file_path = module_path_field[len("file://"):].split("::", 1)[0]
|
|
285
|
+
class_name = module_path_field.split("::", 1)[1]
|
|
286
|
+
else:
|
|
287
|
+
# Backward compatibility: dotted path "pkg.mod.Class"
|
|
288
|
+
parts = module_path_field.rsplit('.', 1)
|
|
289
|
+
if len(parts) == 2:
|
|
290
|
+
# Try resolve to file by searching sys.modules/importlib; we still store it for fallback load
|
|
291
|
+
file_path = module_path_field # mark as dotted path
|
|
292
|
+
class_name = parts[1]
|
|
293
|
+
else:
|
|
294
|
+
logger.warning(
|
|
295
|
+
"[exploit_mgr] Invalid module path format for plugin %s: %s",
|
|
296
|
+
plugin_db.name,
|
|
297
|
+
module_path_field,
|
|
298
|
+
)
|
|
299
|
+
continue
|
|
300
|
+
|
|
301
|
+
self.plugin_registry[plugin_db.name] = {
|
|
302
|
+
'file_or_module': file_path,
|
|
303
|
+
'class_name': class_name,
|
|
304
|
+
}
|
|
305
|
+
except Exception as e:
|
|
306
|
+
logger.error("[exploit_mgr] Failed to index plugin %s: %s", plugin_db.name, str(e))
|
|
307
|
+
|
|
308
|
+
def load_plugin_groups(self) -> list[PluginGroupSpec]:
|
|
309
|
+
return self._group_repo.list_enabled_groups()
|
|
310
|
+
|
|
311
|
+
def initialize(self):
|
|
312
|
+
self.pm.hook.initialize()
|
|
313
|
+
|
|
314
|
+
def exploit(self):
|
|
315
|
+
self.pm.hook.execute()
|
|
316
|
+
|
|
317
|
+
def _load_plugin_instance(self, plugin_name: str) -> Optional[BasePlugin]:
|
|
318
|
+
"""Load a plugin instance on demand using file-path import, then cache and register it."""
|
|
319
|
+
if plugin_name in self.plugins:
|
|
320
|
+
return self.plugins[plugin_name]
|
|
321
|
+
|
|
322
|
+
meta = self.plugin_registry.get(plugin_name)
|
|
323
|
+
if not meta:
|
|
324
|
+
logger.warning("[exploit_mgr] Plugin '%s' not found in registry or disabled.", plugin_name)
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
file_or_module = meta.get('file_or_module')
|
|
328
|
+
class_name = meta.get('class_name')
|
|
329
|
+
|
|
330
|
+
try:
|
|
331
|
+
plugin_class = None
|
|
332
|
+
if file_or_module and os.path.isabs(file_or_module):
|
|
333
|
+
# File path import
|
|
334
|
+
temp_mod_name = f"exploit_runtime_{hash(file_or_module)}"
|
|
335
|
+
spec = importlib.util.spec_from_file_location(temp_mod_name, file_or_module)
|
|
336
|
+
if spec is None or spec.loader is None:
|
|
337
|
+
raise ImportError(f"Failed to create spec for {file_or_module}")
|
|
338
|
+
module = importlib.util.module_from_spec(spec)
|
|
339
|
+
spec.loader.exec_module(module)
|
|
340
|
+
if hasattr(module, class_name):
|
|
341
|
+
plugin_class = getattr(module, class_name)
|
|
342
|
+
else:
|
|
343
|
+
# Backward compatibility: dotted import
|
|
344
|
+
module_name = file_or_module.rsplit('.', 1)[0]
|
|
345
|
+
module = importlib.import_module(module_name)
|
|
346
|
+
if hasattr(module, class_name):
|
|
347
|
+
plugin_class = getattr(module, class_name)
|
|
348
|
+
|
|
349
|
+
if not plugin_class or not issubclass(plugin_class, BasePlugin):
|
|
350
|
+
raise TypeError(f"Plugin class invalid or not a BasePlugin: {class_name}")
|
|
351
|
+
|
|
352
|
+
plugin_instance = plugin_class()
|
|
353
|
+
# Register to pluggy. Do NOT call initialize() here.
|
|
354
|
+
# Initialization (and backend injection) happens right before execution via _ensure_context_injected().
|
|
355
|
+
self.pm.register(plugin_instance)
|
|
356
|
+
self.plugins[plugin_name] = plugin_instance
|
|
357
|
+
logger.debug("[exploit_mgr] Lazily loaded and registered plugin: %s", plugin_name)
|
|
358
|
+
return plugin_instance
|
|
359
|
+
except Exception as e:
|
|
360
|
+
logger.error("[exploit_mgr] Failed to lazily load plugin %s: %s", plugin_name, str(e))
|
|
361
|
+
return None
|
|
362
|
+
|
|
363
|
+
async def execute_plugin_async(self, plugin_name, target=None, parameters=None):
|
|
364
|
+
"""Execute a plugin asynchronously if it supports it, otherwise fall back to sync execution"""
|
|
365
|
+
plugin_instance = self._load_plugin_instance(plugin_name)
|
|
366
|
+
if not plugin_instance:
|
|
367
|
+
logger.warning("[exploit_mgr] Plugin '%s' not found or disabled.", plugin_name)
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
# Normalize target for plugin boundary
|
|
371
|
+
target = self._normalize_target_for_plugin(target)
|
|
372
|
+
|
|
373
|
+
# Inject backend context before execution
|
|
374
|
+
self._ensure_context_injected(plugin_instance)
|
|
375
|
+
|
|
376
|
+
# Check if plugin supports async execution
|
|
377
|
+
if hasattr(plugin_instance, 'execute_async'):
|
|
378
|
+
result = await plugin_instance.execute_async(target, parameters)
|
|
379
|
+
else:
|
|
380
|
+
# Fall back to synchronous execution
|
|
381
|
+
result = plugin_instance.execute(target, parameters)
|
|
382
|
+
|
|
383
|
+
return result
|
|
384
|
+
|
|
385
|
+
def execute_plugin(self, plugin_name, target=None, parameters=None):
|
|
386
|
+
"""
|
|
387
|
+
Execute a plugin with the given parameters.
|
|
388
|
+
"""
|
|
389
|
+
return self._execute_plugin(plugin_name, target=target, parameters=parameters, force_async=None)
|
|
390
|
+
|
|
391
|
+
def _execute_plugin(self, plugin_name, target=None, parameters=None, *, force_async: bool | None = None):
|
|
392
|
+
plugin_instance = self._load_plugin_instance(plugin_name)
|
|
393
|
+
if not plugin_instance:
|
|
394
|
+
logger.warning("[exploit_mgr] Plugin '%s' not found or disabled.", plugin_name)
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
parameters = parameters or {}
|
|
399
|
+
# Normalize target for plugin boundary (dict snapshot preferred)
|
|
400
|
+
target = self._normalize_target_for_plugin(target)
|
|
401
|
+
|
|
402
|
+
# Determine if we should execute asynchronously
|
|
403
|
+
if force_async is None:
|
|
404
|
+
should_execute_async = (
|
|
405
|
+
hasattr(plugin_instance, 'execute_async') or # Plugin supports async
|
|
406
|
+
parameters.get('duration', 0) > 5 or # Long running task
|
|
407
|
+
parameters.get('stream', False) or # Streaming data
|
|
408
|
+
parameters.get('async', False) # Explicitly requested async
|
|
409
|
+
)
|
|
410
|
+
else:
|
|
411
|
+
should_execute_async = bool(force_async)
|
|
412
|
+
|
|
413
|
+
if should_execute_async:
|
|
414
|
+
logger.info("[exploit_mgr] Starting async execution for plugin: %s", plugin_name)
|
|
415
|
+
|
|
416
|
+
return self._task_runner.submit(
|
|
417
|
+
plugin_name,
|
|
418
|
+
target=target,
|
|
419
|
+
parameters=parameters,
|
|
420
|
+
context=None,
|
|
421
|
+
)
|
|
422
|
+
else:
|
|
423
|
+
# Execute synchronously
|
|
424
|
+
logger.info("[exploit_mgr] Executing plugin synchronously: %s", plugin_name)
|
|
425
|
+
|
|
426
|
+
# Inject backend context before execution
|
|
427
|
+
self._ensure_context_injected(plugin_instance)
|
|
428
|
+
|
|
429
|
+
result = plugin_instance.execute(target, parameters)
|
|
430
|
+
|
|
431
|
+
# Format the result
|
|
432
|
+
if hasattr(result, 'success'):
|
|
433
|
+
return {
|
|
434
|
+
'success': result.success,
|
|
435
|
+
'message': result.message if hasattr(result, 'message') else str(result),
|
|
436
|
+
'data': result.data if hasattr(result, 'data') else None
|
|
437
|
+
}
|
|
438
|
+
else:
|
|
439
|
+
return {
|
|
440
|
+
'message': str(result)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
except Exception as e:
|
|
444
|
+
logger.error("[exploit_mgr] Error executing plugin %s: %s", plugin_name, str(e))
|
|
445
|
+
raise ValueError(f"Error executing plugin {plugin_name}: {str(e)}")
|
|
446
|
+
|
|
447
|
+
def execute_selected_plugins(self, plugin_names, target=None):
|
|
448
|
+
results = {}
|
|
449
|
+
for plugin_name in plugin_names:
|
|
450
|
+
try:
|
|
451
|
+
result = self.execute_plugin(plugin_name, target)
|
|
452
|
+
results[plugin_name] = result
|
|
453
|
+
except ValueError as e:
|
|
454
|
+
logger.warning("[exploit_mgr] Error executing plugin '%s': %s", plugin_name, str(e))
|
|
455
|
+
return results
|
|
456
|
+
|
|
457
|
+
def list_plugins(self):
|
|
458
|
+
"""
|
|
459
|
+
Returns a list of all available plugins.
|
|
460
|
+
"""
|
|
461
|
+
# Prefer registry keys to avoid forcing loads
|
|
462
|
+
return list(self.plugin_registry.keys())
|
|
463
|
+
|
|
464
|
+
def list_plugin_info(self):
|
|
465
|
+
logger.debug("[exploit_mgr] Starting to list plugin information")
|
|
466
|
+
plugin_info = {}
|
|
467
|
+
|
|
468
|
+
# Use registry to avoid preloading all; load per plugin to get info
|
|
469
|
+
for plugin_name in self.plugin_registry.keys():
|
|
470
|
+
plugin_instance = self.plugins.get(plugin_name) or self._load_plugin_instance(plugin_name)
|
|
471
|
+
logger.debug("[exploit_mgr] Processing plugin: %s", plugin_name)
|
|
472
|
+
try:
|
|
473
|
+
if not plugin_instance:
|
|
474
|
+
plugin_info[plugin_name] = {"error": "failed to load plugin"}
|
|
475
|
+
continue
|
|
476
|
+
info = plugin_instance.get_info()
|
|
477
|
+
logger.debug("[exploit_mgr] Retrieved info for plugin %s: %s", plugin_name, info)
|
|
478
|
+
plugin_info[plugin_name] = info
|
|
479
|
+
|
|
480
|
+
except Exception as e:
|
|
481
|
+
logger.error("[exploit_mgr] Error processing plugin %s: %s", plugin_name, str(e))
|
|
482
|
+
plugin_info[plugin_name] = {"error": str(e)}
|
|
483
|
+
|
|
484
|
+
logger.debug("[exploit_mgr] Completed listing plugin information. Total plugins: %d", len(plugin_info))
|
|
485
|
+
return plugin_info
|
|
486
|
+
|
|
487
|
+
def get_plugin(self, plugin_name: str) -> Optional[BasePlugin]:
|
|
488
|
+
"""
|
|
489
|
+
Get a plugin instance by name.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
plugin_name (str): The name of the plugin to retrieve
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
Optional[BasePlugin]: The plugin instance if found, None otherwise
|
|
496
|
+
"""
|
|
497
|
+
plugin_instance = self.plugins.get(plugin_name) or self._load_plugin_instance(plugin_name)
|
|
498
|
+
if not plugin_instance:
|
|
499
|
+
logger.warning("[exploit_mgr] Plugin '%s' not found or disabled.", plugin_name)
|
|
500
|
+
return None
|
|
501
|
+
return plugin_instance
|
|
502
|
+
|
|
503
|
+
def get_plugin_info(self, plugin_name: str) -> Optional[dict]:
|
|
504
|
+
"""
|
|
505
|
+
Get plugin information by name.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
plugin_name (str): The name of the plugin to retrieve info for
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
Optional[dict]: The plugin info dictionary if found, None otherwise
|
|
512
|
+
"""
|
|
513
|
+
plugin_instance = self.plugins.get(plugin_name) or self._load_plugin_instance(plugin_name)
|
|
514
|
+
if not plugin_instance:
|
|
515
|
+
logger.warning("[exploit_mgr] Plugin '%s' not found or disabled.", plugin_name)
|
|
516
|
+
return None
|
|
517
|
+
|
|
518
|
+
try:
|
|
519
|
+
return plugin_instance.get_info()
|
|
520
|
+
except Exception as e:
|
|
521
|
+
logger.error("[exploit_mgr] Error getting info for plugin %s: %s", plugin_name, str(e))
|
|
522
|
+
return None
|
|
523
|
+
|
|
524
|
+
def execute_plugin_group(self, group_name, target=None, parameters=None, force_exec=True):
|
|
525
|
+
"""
|
|
526
|
+
Execute the named plugin‑group *including* its nested groups and plugins.
|
|
527
|
+
|
|
528
|
+
The actual ordered execution and failure‑handling logic lives inside
|
|
529
|
+
PluginGroup.execute(), which honours:
|
|
530
|
+
• PluginSequence.sequence / ignore_fail
|
|
531
|
+
• PluginGroupTree.sequence / ignore_fail / force_exec
|
|
532
|
+
"""
|
|
533
|
+
spec = self._group_repo.get_group(group_name)
|
|
534
|
+
if spec is None:
|
|
535
|
+
logger.error("[exploit_mgr] Plugin group '%s' does not exist", group_name)
|
|
536
|
+
return False
|
|
537
|
+
|
|
538
|
+
if not spec.enabled and not force_exec:
|
|
539
|
+
logger.warning("[exploit_mgr] Plugin group '%s' is disabled", group_name)
|
|
540
|
+
return False
|
|
541
|
+
|
|
542
|
+
logger.info("[exploit_mgr] Executing plugin group '%s'", group_name)
|
|
543
|
+
ok = self._execute_group_spec(spec, target=target, parameters=parameters, force_exec=force_exec, _stack=[])
|
|
544
|
+
|
|
545
|
+
if ok:
|
|
546
|
+
logger.info("[exploit_mgr] Plugin group '%s' executed successfully", group_name)
|
|
547
|
+
else:
|
|
548
|
+
logger.warning("[exploit_mgr] Plugin group '%s' finished with failures", group_name)
|
|
549
|
+
return ok
|
|
550
|
+
|
|
551
|
+
def _execute_group_spec(
|
|
552
|
+
self,
|
|
553
|
+
spec: PluginGroupSpec,
|
|
554
|
+
*,
|
|
555
|
+
target=None,
|
|
556
|
+
parameters=None,
|
|
557
|
+
force_exec: bool = True,
|
|
558
|
+
_stack: list[str],
|
|
559
|
+
) -> bool:
|
|
560
|
+
if spec.name in _stack:
|
|
561
|
+
logger.error(
|
|
562
|
+
"[exploit_mgr] Detected cyclic PluginGroup reference: %s",
|
|
563
|
+
" -> ".join(_stack + [spec.name]),
|
|
564
|
+
)
|
|
565
|
+
return False
|
|
566
|
+
|
|
567
|
+
if not spec.enabled and not force_exec:
|
|
568
|
+
return True
|
|
569
|
+
|
|
570
|
+
overall_ok = True
|
|
571
|
+
stack2 = _stack + [spec.name]
|
|
572
|
+
|
|
573
|
+
# Merge and execute all steps in order.
|
|
574
|
+
steps: list[tuple[int, str, object]] = []
|
|
575
|
+
for s in spec.group_steps:
|
|
576
|
+
steps.append((s.sequence, "group", s))
|
|
577
|
+
for s in spec.plugin_steps:
|
|
578
|
+
steps.append((s.sequence, "plugin", s))
|
|
579
|
+
steps.sort(key=lambda t: t[0])
|
|
580
|
+
|
|
581
|
+
for _, kind, s in steps:
|
|
582
|
+
if kind == "group":
|
|
583
|
+
s = s # type: ignore[assignment]
|
|
584
|
+
assert isinstance(s, GroupStepSpec)
|
|
585
|
+
child = self._group_repo.get_group(s.group_name)
|
|
586
|
+
if child is None:
|
|
587
|
+
logger.error(
|
|
588
|
+
"[exploit_mgr] Missing child PluginGroup '%s' referenced by '%s'",
|
|
589
|
+
s.group_name,
|
|
590
|
+
spec.name,
|
|
591
|
+
)
|
|
592
|
+
if not s.ignore_fail:
|
|
593
|
+
return False
|
|
594
|
+
overall_ok = False
|
|
595
|
+
continue
|
|
596
|
+
ok = self._execute_group_spec(
|
|
597
|
+
child,
|
|
598
|
+
target=target,
|
|
599
|
+
parameters=parameters,
|
|
600
|
+
force_exec=s.force_exec,
|
|
601
|
+
_stack=stack2,
|
|
602
|
+
)
|
|
603
|
+
if not ok:
|
|
604
|
+
if not s.ignore_fail:
|
|
605
|
+
return False
|
|
606
|
+
overall_ok = False
|
|
607
|
+
else:
|
|
608
|
+
assert isinstance(s, PluginStepSpec)
|
|
609
|
+
try:
|
|
610
|
+
result = self._execute_plugin(s.plugin_name, target=target, parameters=parameters, force_async=None)
|
|
611
|
+
if isinstance(result, dict) and result.get("execution_type") == "async":
|
|
612
|
+
ok = True
|
|
613
|
+
elif isinstance(result, dict):
|
|
614
|
+
ok = bool(result.get("success", True))
|
|
615
|
+
else:
|
|
616
|
+
ok = True if result is None else bool(result)
|
|
617
|
+
except Exception as e:
|
|
618
|
+
logger.error("[exploit_mgr] Error executing plugin %s: %s", s.plugin_name, str(e))
|
|
619
|
+
ok = False
|
|
620
|
+
|
|
621
|
+
if not ok:
|
|
622
|
+
if not s.ignore_fail:
|
|
623
|
+
return False
|
|
624
|
+
overall_ok = False
|
|
625
|
+
|
|
626
|
+
return overall_ok
|
|
627
|
+
|
|
628
|
+
def cleanup_all_plugins(self):
|
|
629
|
+
"""
|
|
630
|
+
Cleanup all registered plugins. This method is called automatically on program exit
|
|
631
|
+
and can also be called manually when needed.
|
|
632
|
+
"""
|
|
633
|
+
logger.debug("[exploit_mgr] Starting cleanup of all plugins")
|
|
634
|
+
try:
|
|
635
|
+
# Guard: if __init__ failed part-way, destructor may still run.
|
|
636
|
+
if not hasattr(self, "pm"):
|
|
637
|
+
return
|
|
638
|
+
# Log registered plugins before cleanup using class names instead of name attribute
|
|
639
|
+
registered_plugins = [p.__class__.__name__ for p in self.pm.get_plugins()]
|
|
640
|
+
logger.debug("[exploit_mgr] Registered plugins for cleanup: %s", registered_plugins)
|
|
641
|
+
|
|
642
|
+
# Call cleanup hook on all registered plugins
|
|
643
|
+
self.pm.hook.cleanup()
|
|
644
|
+
|
|
645
|
+
# Clear plugin references
|
|
646
|
+
if hasattr(self, "plugins"):
|
|
647
|
+
self.plugins.clear()
|
|
648
|
+
logger.debug("[exploit_mgr] Successfully cleaned up all plugins")
|
|
649
|
+
except Exception as e:
|
|
650
|
+
logger.error("[exploit_mgr] Error during plugin cleanup: %s", str(e))
|
|
651
|
+
|
|
652
|
+
def __del__(self):
|
|
653
|
+
"""
|
|
654
|
+
Destructor to ensure cleanup is called when the manager is destroyed
|
|
655
|
+
"""
|
|
656
|
+
try:
|
|
657
|
+
# Avoid noisy errors during interpreter shutdown / partial init.
|
|
658
|
+
if hasattr(self, "pm"):
|
|
659
|
+
self.cleanup_all_plugins()
|
|
660
|
+
except Exception as e:
|
|
661
|
+
logger.error("[exploit_mgr] Error during ExploitPluginManager cleanup: %s", str(e))
|
|
662
|
+
|
|
663
|
+
def reload_plugin(self, plugin_name):
|
|
664
|
+
"""
|
|
665
|
+
Reload a plugin after its code has been modified.
|
|
666
|
+
|
|
667
|
+
Args:
|
|
668
|
+
plugin_name (str): The name of the plugin to reload
|
|
669
|
+
|
|
670
|
+
Returns:
|
|
671
|
+
bool: True if the plugin was successfully reloaded, False otherwise
|
|
672
|
+
"""
|
|
673
|
+
try:
|
|
674
|
+
# Refresh registry from repo (also re-discovers plugins on disk)
|
|
675
|
+
self.load_plugins()
|
|
676
|
+
meta = self.plugin_registry.get(plugin_name)
|
|
677
|
+
if not meta:
|
|
678
|
+
logger.error("[exploit_mgr] Plugin %s not found in registry", plugin_name)
|
|
679
|
+
return False
|
|
680
|
+
|
|
681
|
+
file_or_module = meta.get("file_or_module")
|
|
682
|
+
class_name = meta.get("class_name")
|
|
683
|
+
|
|
684
|
+
# Reload the module
|
|
685
|
+
try:
|
|
686
|
+
# First, unregister the existing plugin
|
|
687
|
+
if plugin_name in self.plugins:
|
|
688
|
+
try:
|
|
689
|
+
# best-effort cleanup
|
|
690
|
+
old_inst = self.plugins[plugin_name]
|
|
691
|
+
if hasattr(old_inst, 'cleanup'):
|
|
692
|
+
old_inst.cleanup()
|
|
693
|
+
except Exception:
|
|
694
|
+
pass
|
|
695
|
+
self.pm.unregister(self.plugins[plugin_name])
|
|
696
|
+
del self.plugins[plugin_name]
|
|
697
|
+
|
|
698
|
+
# Reload via file-path or dotted import
|
|
699
|
+
plugin_class = None
|
|
700
|
+
if file_or_module and os.path.isabs(file_or_module):
|
|
701
|
+
temp_mod_name = f"exploit_runtime_{hash(file_or_module)}"
|
|
702
|
+
spec = importlib.util.spec_from_file_location(temp_mod_name, file_or_module)
|
|
703
|
+
if spec is None or spec.loader is None:
|
|
704
|
+
raise ImportError(f"Failed to create spec for {file_or_module}")
|
|
705
|
+
module = importlib.util.module_from_spec(spec)
|
|
706
|
+
importlib.invalidate_caches()
|
|
707
|
+
spec.loader.exec_module(module)
|
|
708
|
+
plugin_class = getattr(module, class_name)
|
|
709
|
+
else:
|
|
710
|
+
module_name = file_or_module.rsplit(".", 1)[0]
|
|
711
|
+
module = importlib.import_module(module_name)
|
|
712
|
+
importlib.reload(module)
|
|
713
|
+
plugin_class = getattr(module, class_name)
|
|
714
|
+
|
|
715
|
+
plugin_instance = plugin_class() # type: ignore[operator]
|
|
716
|
+
self.pm.register(plugin_instance)
|
|
717
|
+
# Do NOT call initialize() here.
|
|
718
|
+
# Initialization (and backend injection) happens right before execution via _ensure_context_injected().
|
|
719
|
+
self.plugins[plugin_name] = plugin_instance
|
|
720
|
+
|
|
721
|
+
logger.debug("[exploit_mgr] Successfully reloaded plugin: %s", plugin_name)
|
|
722
|
+
return True
|
|
723
|
+
except Exception as e:
|
|
724
|
+
logger.error("[exploit_mgr] Error reloading plugin %s: %s", plugin_name, str(e))
|
|
725
|
+
return False
|
|
726
|
+
except Exception as e:
|
|
727
|
+
logger.error("[exploit_mgr] Error in reload_plugin: %s", str(e))
|
|
728
|
+
return False
|