xsoar-cli 0.0.3__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.
Potentially problematic release.
This version of xsoar-cli might be problematic. Click here for more details.
- xsoar_cli/__about__.py +4 -0
- xsoar_cli/__init__.py +1 -0
- xsoar_cli/case/README.md +31 -0
- xsoar_cli/case/__init__.py +0 -0
- xsoar_cli/case/commands.py +86 -0
- xsoar_cli/cli.py +41 -0
- xsoar_cli/config/README.md +12 -0
- xsoar_cli/config/__init__.py +0 -0
- xsoar_cli/config/commands.py +99 -0
- xsoar_cli/graph/README.md +17 -0
- xsoar_cli/graph/__init__.py +0 -0
- xsoar_cli/graph/commands.py +32 -0
- xsoar_cli/manifest/README.md +23 -0
- xsoar_cli/manifest/__init__.py +0 -0
- xsoar_cli/manifest/commands.py +200 -0
- xsoar_cli/pack/README.md +7 -0
- xsoar_cli/pack/__init__.py +0 -0
- xsoar_cli/pack/commands.py +55 -0
- xsoar_cli/playbook/README.md +19 -0
- xsoar_cli/playbook/__init__.py +0 -0
- xsoar_cli/playbook/commands.py +67 -0
- xsoar_cli/plugins/README.md +433 -0
- xsoar_cli/plugins/__init__.py +68 -0
- xsoar_cli/plugins/commands.py +296 -0
- xsoar_cli/plugins/manager.py +399 -0
- xsoar_cli/utilities.py +94 -0
- xsoar_cli-0.0.3.dist-info/METADATA +128 -0
- xsoar_cli-0.0.3.dist-info/RECORD +31 -0
- xsoar_cli-0.0.3.dist-info/WHEEL +4 -0
- xsoar_cli-0.0.3.dist-info/entry_points.txt +2 -0
- xsoar_cli-0.0.3.dist-info/licenses/LICENSE.txt +9 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plugin management commands for XSOAR CLI
|
|
3
|
+
|
|
4
|
+
This module provides CLI commands for managing plugins, including
|
|
5
|
+
listing, loading, reloading, and creating example plugins.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from .manager import PluginManager
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.group(help="Manage XSOAR CLI plugins")
|
|
18
|
+
def plugins():
|
|
19
|
+
"""Plugin management commands."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.command(help="List all available and loaded plugins")
|
|
23
|
+
@click.option("--verbose", "-v", is_flag=True, help="Show detailed information")
|
|
24
|
+
@click.pass_context
|
|
25
|
+
def list_plugins(ctx: click.Context, verbose: bool):
|
|
26
|
+
"""List all plugins in the plugins directory."""
|
|
27
|
+
plugin_manager = PluginManager()
|
|
28
|
+
|
|
29
|
+
# Discover all plugins
|
|
30
|
+
discovered = plugin_manager.discover_plugins()
|
|
31
|
+
|
|
32
|
+
# Load all plugins to get their info
|
|
33
|
+
plugin_manager.load_all_plugins(ignore_errors=True)
|
|
34
|
+
|
|
35
|
+
loaded_info = plugin_manager.get_plugin_info()
|
|
36
|
+
failed_info = plugin_manager.get_failed_plugins()
|
|
37
|
+
|
|
38
|
+
if not discovered:
|
|
39
|
+
click.echo(f"No plugins found in {plugin_manager.plugins_dir}")
|
|
40
|
+
click.echo("Run 'xsoar-cli plugins create-example' to create an example plugin.")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
click.echo(f"Plugins directory: {plugin_manager.plugins_dir}")
|
|
44
|
+
click.echo(f"Discovered {len(discovered)} plugin files\n")
|
|
45
|
+
|
|
46
|
+
# Show loaded plugins
|
|
47
|
+
if loaded_info:
|
|
48
|
+
click.echo("✅ Loaded Plugins:")
|
|
49
|
+
for plugin_name, info in loaded_info.items():
|
|
50
|
+
if verbose:
|
|
51
|
+
click.echo(f" • {plugin_name}")
|
|
52
|
+
click.echo(f" Name: {info['name']}")
|
|
53
|
+
click.echo(f" Version: {info['version']}")
|
|
54
|
+
click.echo(f" Description: {info['description']}")
|
|
55
|
+
else:
|
|
56
|
+
click.echo(f" • {plugin_name} (v{info['version']})")
|
|
57
|
+
click.echo()
|
|
58
|
+
|
|
59
|
+
# Show failed plugins
|
|
60
|
+
if failed_info:
|
|
61
|
+
click.echo("❌ Failed Plugins:")
|
|
62
|
+
for plugin_name, error in failed_info.items():
|
|
63
|
+
if verbose:
|
|
64
|
+
click.echo(f" • {plugin_name}: {error}")
|
|
65
|
+
else:
|
|
66
|
+
click.echo(f" • {plugin_name}")
|
|
67
|
+
click.echo()
|
|
68
|
+
|
|
69
|
+
# Show unloaded plugins (discovered but not loaded and not failed)
|
|
70
|
+
unloaded = set(discovered) - set(loaded_info.keys()) - set(failed_info.keys())
|
|
71
|
+
if unloaded:
|
|
72
|
+
click.echo("⚠️ Unloaded Plugins:")
|
|
73
|
+
for plugin_name in unloaded:
|
|
74
|
+
click.echo(f" • {plugin_name}")
|
|
75
|
+
|
|
76
|
+
# Show command conflicts
|
|
77
|
+
conflicts = plugin_manager.get_command_conflicts()
|
|
78
|
+
if conflicts:
|
|
79
|
+
click.echo()
|
|
80
|
+
click.echo("⚠️ Command Conflicts:")
|
|
81
|
+
for conflict in conflicts:
|
|
82
|
+
click.echo(f" • Plugin '{conflict['plugin_name']}' command '{conflict['command_name']}' conflicts with core command")
|
|
83
|
+
click.echo(f" Plugin version: {conflict['plugin_version']}")
|
|
84
|
+
click.echo(" Solution: Rename the command in your plugin or use a command group")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@click.command(help="Reload a specific plugin")
|
|
88
|
+
@click.argument("plugin_name", type=str)
|
|
89
|
+
def reload(plugin_name: str):
|
|
90
|
+
"""Reload a specific plugin."""
|
|
91
|
+
plugin_manager = PluginManager()
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
click.echo(f"Reloading plugin: {plugin_name}...")
|
|
95
|
+
plugin = plugin_manager.reload_plugin(plugin_name)
|
|
96
|
+
|
|
97
|
+
if plugin:
|
|
98
|
+
click.echo(f"✅ Successfully reloaded plugin: {plugin_name}")
|
|
99
|
+
click.echo(f" Name: {plugin.name}")
|
|
100
|
+
click.echo(f" Version: {plugin.version}")
|
|
101
|
+
else:
|
|
102
|
+
click.echo(f"❌ Failed to reload plugin: {plugin_name}")
|
|
103
|
+
|
|
104
|
+
except Exception as e:
|
|
105
|
+
click.echo(f"❌ Error reloading plugin {plugin_name}: {e}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@click.command(help="Create an example plugin file")
|
|
109
|
+
@click.option("--force", is_flag=True, help="Overwrite existing example plugin")
|
|
110
|
+
def create_example(force: bool):
|
|
111
|
+
"""Create an example plugin in the plugins directory."""
|
|
112
|
+
plugin_manager = PluginManager()
|
|
113
|
+
|
|
114
|
+
example_file = plugin_manager.plugins_dir / "example_plugin.py"
|
|
115
|
+
|
|
116
|
+
if example_file.exists() and not force:
|
|
117
|
+
click.echo(f"Example plugin already exists at: {example_file}")
|
|
118
|
+
click.echo("Use --force to overwrite it.")
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
plugin_manager.create_example_plugin()
|
|
122
|
+
click.echo(f"✅ Created example plugin at: {example_file}")
|
|
123
|
+
click.echo("\nTo test the example plugin:")
|
|
124
|
+
click.echo(" xsoar-cli example hello --name YourName")
|
|
125
|
+
click.echo(" xsoar-cli example info")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@click.command(help="Show information about a specific plugin")
|
|
129
|
+
@click.argument("plugin_name", type=str)
|
|
130
|
+
def info(plugin_name: str):
|
|
131
|
+
"""Show detailed information about a plugin."""
|
|
132
|
+
plugin_manager = PluginManager()
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
plugin = plugin_manager.load_plugin(plugin_name)
|
|
136
|
+
|
|
137
|
+
if plugin:
|
|
138
|
+
click.echo("Plugin Information:")
|
|
139
|
+
click.echo(f" File: {plugin_manager.plugins_dir / plugin_name}.py")
|
|
140
|
+
click.echo(f" Name: {plugin.name}")
|
|
141
|
+
click.echo(f" Version: {plugin.version}")
|
|
142
|
+
click.echo(f" Description: {plugin.description or 'No description provided'}")
|
|
143
|
+
|
|
144
|
+
# Try to get command info
|
|
145
|
+
try:
|
|
146
|
+
command = plugin.get_command()
|
|
147
|
+
click.echo(f" Command: {command.name}")
|
|
148
|
+
if hasattr(command, "commands"):
|
|
149
|
+
subcommands = list(command.commands.keys())
|
|
150
|
+
if subcommands:
|
|
151
|
+
click.echo(f" Subcommands: {', '.join(subcommands)}")
|
|
152
|
+
except Exception as e:
|
|
153
|
+
click.echo(f" Command: Error loading command ({e})")
|
|
154
|
+
else:
|
|
155
|
+
click.echo(f"❌ Plugin not found or failed to load: {plugin_name}")
|
|
156
|
+
|
|
157
|
+
except Exception as e:
|
|
158
|
+
click.echo(f"❌ Error loading plugin {plugin_name}: {e}")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@click.command(help="Validate all plugins")
|
|
162
|
+
def validate():
|
|
163
|
+
"""Validate all plugins in the plugins directory."""
|
|
164
|
+
plugin_manager = PluginManager()
|
|
165
|
+
|
|
166
|
+
discovered = plugin_manager.discover_plugins()
|
|
167
|
+
|
|
168
|
+
if not discovered:
|
|
169
|
+
click.echo(f"No plugins found in {plugin_manager.plugins_dir}")
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
click.echo(f"Validating {len(discovered)} plugins...\n")
|
|
173
|
+
|
|
174
|
+
all_valid = True
|
|
175
|
+
|
|
176
|
+
for plugin_name in discovered:
|
|
177
|
+
try:
|
|
178
|
+
plugin = plugin_manager.load_plugin(plugin_name)
|
|
179
|
+
if plugin:
|
|
180
|
+
# Test that the plugin can provide a command
|
|
181
|
+
command = plugin.get_command()
|
|
182
|
+
if not isinstance(command, (click.Command, click.Group)):
|
|
183
|
+
raise ValueError("get_command() must return a Click Command or Group")
|
|
184
|
+
|
|
185
|
+
click.echo(f"✅ {plugin_name}: Valid")
|
|
186
|
+
else:
|
|
187
|
+
click.echo(f"❌ {plugin_name}: Failed to load")
|
|
188
|
+
all_valid = False
|
|
189
|
+
|
|
190
|
+
except Exception as e:
|
|
191
|
+
click.echo(f"❌ {plugin_name}: {e}")
|
|
192
|
+
all_valid = False
|
|
193
|
+
|
|
194
|
+
# Check for command conflicts by attempting registration
|
|
195
|
+
try:
|
|
196
|
+
from xsoar_cli.cli import cli
|
|
197
|
+
|
|
198
|
+
temp_plugin_manager = PluginManager()
|
|
199
|
+
temp_plugin_manager.load_all_plugins(ignore_errors=True)
|
|
200
|
+
temp_plugin_manager.register_plugin_commands(cli)
|
|
201
|
+
|
|
202
|
+
conflicts = temp_plugin_manager.get_command_conflicts()
|
|
203
|
+
if conflicts:
|
|
204
|
+
click.echo("\n⚠️ Command Conflicts Detected:")
|
|
205
|
+
for conflict in conflicts:
|
|
206
|
+
click.echo(f" • Plugin '{conflict['plugin_name']}' command '{conflict['command_name']}' conflicts with core command")
|
|
207
|
+
click.echo(" Solution: Rename the command or use a command group")
|
|
208
|
+
all_valid = False
|
|
209
|
+
except Exception as e:
|
|
210
|
+
click.echo(f"\n⚠️ Could not check for command conflicts: {e}")
|
|
211
|
+
|
|
212
|
+
click.echo()
|
|
213
|
+
if all_valid:
|
|
214
|
+
click.echo("🎉 All plugins are valid!")
|
|
215
|
+
else:
|
|
216
|
+
click.echo("⚠️ Some plugins have validation errors.")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@click.command(help="Open the plugins directory")
|
|
220
|
+
def open_dir():
|
|
221
|
+
"""Open the plugins directory in the system file manager."""
|
|
222
|
+
plugin_manager = PluginManager()
|
|
223
|
+
plugins_dir = plugin_manager.plugins_dir
|
|
224
|
+
|
|
225
|
+
click.echo(f"Plugins directory: {plugins_dir}")
|
|
226
|
+
|
|
227
|
+
# Try to open the directory
|
|
228
|
+
import subprocess
|
|
229
|
+
import sys
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
if sys.platform == "win32":
|
|
233
|
+
subprocess.run(["explorer", str(plugins_dir)], check=True)
|
|
234
|
+
elif sys.platform == "darwin":
|
|
235
|
+
subprocess.run(["open", str(plugins_dir)], check=True)
|
|
236
|
+
else:
|
|
237
|
+
subprocess.run(["xdg-open", str(plugins_dir)], check=True)
|
|
238
|
+
|
|
239
|
+
click.echo("Opened plugins directory in file manager.")
|
|
240
|
+
|
|
241
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
242
|
+
click.echo("Could not open directory automatically.")
|
|
243
|
+
click.echo(f"Please navigate to: {plugins_dir}")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# Add all commands to the plugins group
|
|
247
|
+
@click.command(help="Check for command conflicts with core CLI")
|
|
248
|
+
def check_conflicts():
|
|
249
|
+
"""Check for command conflicts between plugins and core CLI."""
|
|
250
|
+
plugin_manager = PluginManager()
|
|
251
|
+
|
|
252
|
+
# Load all plugins
|
|
253
|
+
plugin_manager.load_all_plugins(ignore_errors=True)
|
|
254
|
+
|
|
255
|
+
# Check conflicts by attempting registration with a temporary CLI group
|
|
256
|
+
import click
|
|
257
|
+
|
|
258
|
+
temp_cli = click.Group()
|
|
259
|
+
|
|
260
|
+
# Add core commands to temp CLI to simulate real conflicts
|
|
261
|
+
core_commands = ["case", "config", "graph", "manifest", "pack", "playbook", "plugins"]
|
|
262
|
+
for cmd_name in core_commands:
|
|
263
|
+
temp_cli.add_command(click.Command(cmd_name, callback=lambda: None))
|
|
264
|
+
|
|
265
|
+
# Attempt to register plugin commands
|
|
266
|
+
plugin_manager.register_plugin_commands(temp_cli)
|
|
267
|
+
|
|
268
|
+
conflicts = plugin_manager.get_command_conflicts()
|
|
269
|
+
|
|
270
|
+
if not conflicts:
|
|
271
|
+
click.echo("✅ No command conflicts detected!")
|
|
272
|
+
click.echo("All plugin commands have unique names.")
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
click.echo(f"⚠️ Found {len(conflicts)} command conflict(s):")
|
|
276
|
+
click.echo()
|
|
277
|
+
|
|
278
|
+
for conflict in conflicts:
|
|
279
|
+
click.echo(f"🔸 Plugin: {conflict['plugin_name']} (v{conflict['plugin_version']})")
|
|
280
|
+
click.echo(f" Command: '{conflict['command_name']}'")
|
|
281
|
+
click.echo(" Conflicts with: Core CLI command")
|
|
282
|
+
click.echo()
|
|
283
|
+
|
|
284
|
+
click.echo("💡 Solutions:")
|
|
285
|
+
click.echo(" • Rename the conflicting command in your plugin")
|
|
286
|
+
click.echo(" • Use a command group to namespace your commands")
|
|
287
|
+
click.echo(" • Example: Instead of 'case', use 'mycase' or 'myplugin case'")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
plugins.add_command(list_plugins, name="list")
|
|
291
|
+
plugins.add_command(reload)
|
|
292
|
+
plugins.add_command(create_example, name="create-example")
|
|
293
|
+
plugins.add_command(info)
|
|
294
|
+
plugins.add_command(validate)
|
|
295
|
+
plugins.add_command(check_conflicts, name="check-conflicts")
|
|
296
|
+
plugins.add_command(open_dir, name="open")
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plugin Manager for XSOAR CLI
|
|
3
|
+
|
|
4
|
+
This module handles the discovery, loading, and management of plugins
|
|
5
|
+
for the xsoar-cli application from a local directory.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib.util
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Optional, Type
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from . import PluginLoadError, PluginRegistrationError, XSOARPlugin
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PluginManager:
|
|
22
|
+
"""
|
|
23
|
+
Manages the discovery, loading, and registration of XSOAR CLI plugins
|
|
24
|
+
from the ~/.local/xsoar-cli/plugins directory.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, plugins_dir: Optional[Path] = None):
|
|
28
|
+
"""
|
|
29
|
+
Initialize the plugin manager.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
plugins_dir: Custom plugins directory. If None, uses ~/.local/xsoar-cli/plugins
|
|
33
|
+
"""
|
|
34
|
+
if plugins_dir is None:
|
|
35
|
+
self.plugins_dir = Path.home() / ".local" / "xsoar-cli" / "plugins"
|
|
36
|
+
else:
|
|
37
|
+
self.plugins_dir = plugins_dir
|
|
38
|
+
|
|
39
|
+
self.loaded_plugins: Dict[str, XSOARPlugin] = {}
|
|
40
|
+
self.failed_plugins: Dict[str, Exception] = {}
|
|
41
|
+
self.command_conflicts: List[Dict[str, str]] = []
|
|
42
|
+
|
|
43
|
+
# Ensure plugins directory exists
|
|
44
|
+
self.plugins_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
|
|
46
|
+
# Add plugins directory to Python path if not already there
|
|
47
|
+
plugins_dir_str = str(self.plugins_dir)
|
|
48
|
+
if plugins_dir_str not in sys.path:
|
|
49
|
+
sys.path.insert(0, plugins_dir_str)
|
|
50
|
+
|
|
51
|
+
def discover_plugins(self) -> List[str]:
|
|
52
|
+
"""
|
|
53
|
+
Discover available plugins by scanning the plugins directory for Python files.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of plugin module names found
|
|
57
|
+
"""
|
|
58
|
+
plugin_names = []
|
|
59
|
+
|
|
60
|
+
if not self.plugins_dir.exists():
|
|
61
|
+
logger.info(f"Plugins directory does not exist: {self.plugins_dir}")
|
|
62
|
+
return plugin_names
|
|
63
|
+
|
|
64
|
+
for file_path in self.plugins_dir.glob("*.py"):
|
|
65
|
+
if file_path.name.startswith("__"):
|
|
66
|
+
continue # Skip __init__.py, __pycache__, etc.
|
|
67
|
+
|
|
68
|
+
module_name = file_path.stem
|
|
69
|
+
plugin_names.append(module_name)
|
|
70
|
+
|
|
71
|
+
logger.info(f"Discovered {len(plugin_names)} plugin files: {plugin_names}")
|
|
72
|
+
return plugin_names
|
|
73
|
+
|
|
74
|
+
def _load_module_from_file(self, module_name: str, file_path: Path):
|
|
75
|
+
"""
|
|
76
|
+
Load a Python module from a file path.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
module_name: Name to give the module
|
|
80
|
+
file_path: Path to the Python file
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The loaded module
|
|
84
|
+
"""
|
|
85
|
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
|
86
|
+
if spec is None or spec.loader is None:
|
|
87
|
+
raise PluginLoadError(f"Could not load module spec for {file_path}")
|
|
88
|
+
|
|
89
|
+
module = importlib.util.module_from_spec(spec)
|
|
90
|
+
|
|
91
|
+
# Inject XSOARPlugin class into the module's namespace
|
|
92
|
+
# This allows plugins to use XSOARPlugin without complex imports
|
|
93
|
+
from . import XSOARPlugin
|
|
94
|
+
|
|
95
|
+
module.XSOARPlugin = XSOARPlugin
|
|
96
|
+
|
|
97
|
+
sys.modules[module_name] = module
|
|
98
|
+
spec.loader.exec_module(module)
|
|
99
|
+
return module
|
|
100
|
+
|
|
101
|
+
def _find_plugin_classes(self, module) -> List[Type[XSOARPlugin]]:
|
|
102
|
+
"""
|
|
103
|
+
Find all XSOARPlugin classes in a module.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
module: The module to search
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
List of plugin classes found
|
|
110
|
+
"""
|
|
111
|
+
plugin_classes = []
|
|
112
|
+
|
|
113
|
+
for attr_name in dir(module):
|
|
114
|
+
attr = getattr(module, attr_name)
|
|
115
|
+
|
|
116
|
+
# Check if it's a class that inherits from XSOARPlugin
|
|
117
|
+
if isinstance(attr, type) and issubclass(attr, XSOARPlugin) and attr is not XSOARPlugin:
|
|
118
|
+
plugin_classes.append(attr)
|
|
119
|
+
|
|
120
|
+
return plugin_classes
|
|
121
|
+
|
|
122
|
+
def load_plugin(self, plugin_name: str) -> Optional[XSOARPlugin]:
|
|
123
|
+
"""
|
|
124
|
+
Load a single plugin by name.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
plugin_name: Name of the plugin module to load
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
The loaded plugin instance, or None if loading failed
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
PluginLoadError: If the plugin fails to load
|
|
134
|
+
"""
|
|
135
|
+
if plugin_name in self.loaded_plugins:
|
|
136
|
+
return self.loaded_plugins[plugin_name]
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
file_path = self.plugins_dir / f"{plugin_name}.py"
|
|
140
|
+
if not file_path.exists():
|
|
141
|
+
raise PluginLoadError(f"Plugin file not found: {file_path}")
|
|
142
|
+
|
|
143
|
+
# Load the module
|
|
144
|
+
module = self._load_module_from_file(plugin_name, file_path)
|
|
145
|
+
|
|
146
|
+
# Find plugin classes in the module
|
|
147
|
+
plugin_classes = self._find_plugin_classes(module)
|
|
148
|
+
|
|
149
|
+
if not plugin_classes:
|
|
150
|
+
raise PluginLoadError(
|
|
151
|
+
f"No XSOARPlugin classes found in {plugin_name}.py",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if len(plugin_classes) > 1:
|
|
155
|
+
logger.warning(
|
|
156
|
+
f"Multiple plugin classes found in {plugin_name}.py, using the first one",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Instantiate the first plugin class found
|
|
160
|
+
plugin_class = plugin_classes[0]
|
|
161
|
+
plugin_instance = plugin_class()
|
|
162
|
+
|
|
163
|
+
# Initialize the plugin
|
|
164
|
+
try:
|
|
165
|
+
plugin_instance.initialize()
|
|
166
|
+
except Exception as e:
|
|
167
|
+
raise PluginLoadError(f"Plugin '{plugin_name}' initialization failed: {e}")
|
|
168
|
+
|
|
169
|
+
# Store the loaded plugin
|
|
170
|
+
self.loaded_plugins[plugin_name] = plugin_instance
|
|
171
|
+
logger.info(f"Successfully loaded plugin: {plugin_name}")
|
|
172
|
+
|
|
173
|
+
return plugin_instance
|
|
174
|
+
|
|
175
|
+
except Exception as e:
|
|
176
|
+
self.failed_plugins[plugin_name] = e
|
|
177
|
+
logger.error(f"Failed to load plugin '{plugin_name}': {e}")
|
|
178
|
+
raise PluginLoadError(f"Failed to load plugin '{plugin_name}': {e}")
|
|
179
|
+
|
|
180
|
+
def load_all_plugins(self, ignore_errors: bool = True) -> Dict[str, XSOARPlugin]:
|
|
181
|
+
"""
|
|
182
|
+
Load all discovered plugins.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
ignore_errors: If True, continue loading other plugins when one fails
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Dictionary of successfully loaded plugins
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
PluginLoadError: If ignore_errors is False and any plugin fails to load
|
|
192
|
+
"""
|
|
193
|
+
discovered_plugins = self.discover_plugins()
|
|
194
|
+
|
|
195
|
+
for plugin_name in discovered_plugins:
|
|
196
|
+
try:
|
|
197
|
+
self.load_plugin(plugin_name)
|
|
198
|
+
except PluginLoadError as e:
|
|
199
|
+
if not ignore_errors:
|
|
200
|
+
raise
|
|
201
|
+
logger.warning(f"Skipping failed plugin: {e}")
|
|
202
|
+
|
|
203
|
+
return self.loaded_plugins.copy()
|
|
204
|
+
|
|
205
|
+
def register_plugin_commands(self, cli_group: click.Group) -> None:
|
|
206
|
+
"""
|
|
207
|
+
Register all loaded plugin commands with the CLI group.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
cli_group: The Click group to register commands with
|
|
211
|
+
|
|
212
|
+
Raises:
|
|
213
|
+
PluginRegistrationError: If a plugin command fails to register
|
|
214
|
+
"""
|
|
215
|
+
conflicts = []
|
|
216
|
+
|
|
217
|
+
for plugin_name, plugin in self.loaded_plugins.items():
|
|
218
|
+
try:
|
|
219
|
+
command = plugin.get_command()
|
|
220
|
+
if not isinstance(command, (click.Command, click.Group)):
|
|
221
|
+
raise PluginRegistrationError(
|
|
222
|
+
f"Plugin '{plugin_name}' get_command() must return a Click Command or Group",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Ensure the command name doesn't conflict with existing commands
|
|
226
|
+
if command.name in cli_group.commands:
|
|
227
|
+
conflict_info = {
|
|
228
|
+
"plugin_name": plugin_name,
|
|
229
|
+
"command_name": command.name,
|
|
230
|
+
"plugin_version": plugin.version,
|
|
231
|
+
}
|
|
232
|
+
conflicts.append(conflict_info)
|
|
233
|
+
logger.warning(
|
|
234
|
+
f"Plugin '{plugin_name}' command '{command.name}' conflicts with existing command",
|
|
235
|
+
)
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
cli_group.add_command(command)
|
|
239
|
+
logger.info(f"Registered command '{command.name}' from plugin '{plugin_name}'")
|
|
240
|
+
|
|
241
|
+
except Exception as e:
|
|
242
|
+
error_msg = f"Failed to register plugin '{plugin_name}': {e}"
|
|
243
|
+
logger.error(error_msg)
|
|
244
|
+
raise PluginRegistrationError(error_msg)
|
|
245
|
+
|
|
246
|
+
# Store conflicts for later reporting
|
|
247
|
+
self.command_conflicts = conflicts
|
|
248
|
+
|
|
249
|
+
def unload_plugin(self, plugin_name: str) -> None:
|
|
250
|
+
"""
|
|
251
|
+
Unload a plugin and call its cleanup method.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
plugin_name: Name of the plugin to unload
|
|
255
|
+
"""
|
|
256
|
+
if plugin_name in self.loaded_plugins:
|
|
257
|
+
plugin = self.loaded_plugins[plugin_name]
|
|
258
|
+
try:
|
|
259
|
+
plugin.cleanup()
|
|
260
|
+
except Exception as e:
|
|
261
|
+
logger.warning(f"Plugin '{plugin_name}' cleanup failed: {e}")
|
|
262
|
+
del self.loaded_plugins[plugin_name]
|
|
263
|
+
logger.info(f"Unloaded plugin: {plugin_name}")
|
|
264
|
+
|
|
265
|
+
def unload_all_plugins(self) -> None:
|
|
266
|
+
"""Unload all plugins and call their cleanup methods."""
|
|
267
|
+
plugin_names = list(self.loaded_plugins.keys())
|
|
268
|
+
for plugin_name in plugin_names:
|
|
269
|
+
self.unload_plugin(plugin_name)
|
|
270
|
+
|
|
271
|
+
def get_plugin_info(self) -> Dict[str, Dict[str, str]]:
|
|
272
|
+
"""
|
|
273
|
+
Get information about all loaded plugins.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Dictionary with plugin information
|
|
277
|
+
"""
|
|
278
|
+
info = {}
|
|
279
|
+
for plugin_name, plugin in self.loaded_plugins.items():
|
|
280
|
+
info[plugin_name] = {
|
|
281
|
+
"name": plugin.name,
|
|
282
|
+
"version": plugin.version,
|
|
283
|
+
"description": plugin.description or "No description provided",
|
|
284
|
+
}
|
|
285
|
+
return info
|
|
286
|
+
|
|
287
|
+
def get_failed_plugins(self) -> Dict[str, str]:
|
|
288
|
+
"""
|
|
289
|
+
Get information about plugins that failed to load.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Dictionary with plugin names and error messages
|
|
293
|
+
"""
|
|
294
|
+
return {name: str(error) for name, error in self.failed_plugins.items()}
|
|
295
|
+
|
|
296
|
+
def get_command_conflicts(self) -> List[Dict[str, str]]:
|
|
297
|
+
"""
|
|
298
|
+
Get information about command conflicts.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
List of dictionaries with conflict information
|
|
302
|
+
"""
|
|
303
|
+
return self.command_conflicts.copy()
|
|
304
|
+
|
|
305
|
+
def reload_plugin(self, plugin_name: str) -> Optional[XSOARPlugin]:
|
|
306
|
+
"""
|
|
307
|
+
Reload a plugin by unloading and loading it again.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
plugin_name: Name of the plugin to reload
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
The reloaded plugin instance, or None if reloading failed
|
|
314
|
+
"""
|
|
315
|
+
# Unload if already loaded
|
|
316
|
+
if plugin_name in self.loaded_plugins:
|
|
317
|
+
self.unload_plugin(plugin_name)
|
|
318
|
+
|
|
319
|
+
# Remove from failed plugins if it was there
|
|
320
|
+
if plugin_name in self.failed_plugins:
|
|
321
|
+
del self.failed_plugins[plugin_name]
|
|
322
|
+
|
|
323
|
+
# Remove from sys.modules to force reload
|
|
324
|
+
if plugin_name in sys.modules:
|
|
325
|
+
del sys.modules[plugin_name]
|
|
326
|
+
|
|
327
|
+
# Load again
|
|
328
|
+
return self.load_plugin(plugin_name)
|
|
329
|
+
|
|
330
|
+
def create_example_plugin(self) -> None:
|
|
331
|
+
"""
|
|
332
|
+
Create an example plugin file in the plugins directory.
|
|
333
|
+
"""
|
|
334
|
+
example_content = '''"""
|
|
335
|
+
Example XSOAR CLI Plugin
|
|
336
|
+
|
|
337
|
+
This is an example plugin that demonstrates how to create custom commands
|
|
338
|
+
for the xsoar-cli application.
|
|
339
|
+
|
|
340
|
+
The XSOARPlugin class is automatically injected into the plugin namespace,
|
|
341
|
+
so you don't need to import it.
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
import click
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class ExamplePlugin(XSOARPlugin):
|
|
348
|
+
"""Example plugin implementation."""
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def name(self) -> str:
|
|
352
|
+
return "example"
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def version(self) -> str:
|
|
356
|
+
return "1.0.0"
|
|
357
|
+
|
|
358
|
+
@property
|
|
359
|
+
def description(self) -> str:
|
|
360
|
+
return "An example plugin demonstrating the plugin system"
|
|
361
|
+
|
|
362
|
+
def get_command(self) -> click.Command:
|
|
363
|
+
"""Return the Click command for this plugin."""
|
|
364
|
+
|
|
365
|
+
@click.group(help="Example plugin commands")
|
|
366
|
+
def example():
|
|
367
|
+
pass
|
|
368
|
+
|
|
369
|
+
@click.command(help="Say hello")
|
|
370
|
+
@click.option("--name", default="World", help="Name to greet")
|
|
371
|
+
def hello(name: str):
|
|
372
|
+
click.echo(f"Hello, {name}! This is from the example plugin.")
|
|
373
|
+
|
|
374
|
+
@click.command(help="Show plugin info")
|
|
375
|
+
def info():
|
|
376
|
+
click.echo(f"Plugin: {self.name}")
|
|
377
|
+
click.echo(f"Version: {self.version}")
|
|
378
|
+
click.echo(f"Description: {self.description}")
|
|
379
|
+
|
|
380
|
+
example.add_command(hello)
|
|
381
|
+
example.add_command(info)
|
|
382
|
+
|
|
383
|
+
return example
|
|
384
|
+
|
|
385
|
+
def initialize(self):
|
|
386
|
+
"""Initialize the plugin."""
|
|
387
|
+
click.echo("Example plugin initialized!")
|
|
388
|
+
|
|
389
|
+
def cleanup(self):
|
|
390
|
+
"""Cleanup plugin resources."""
|
|
391
|
+
pass
|
|
392
|
+
'''
|
|
393
|
+
|
|
394
|
+
example_file = self.plugins_dir / "example_plugin.py"
|
|
395
|
+
if not example_file.exists():
|
|
396
|
+
example_file.write_text(example_content)
|
|
397
|
+
logger.info(f"Created example plugin at: {example_file}")
|
|
398
|
+
else:
|
|
399
|
+
logger.info(f"Example plugin already exists at: {example_file}")
|