iflow-mcp-m507_ai-soc-agent 1.0.0__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 (85) hide show
  1. iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/METADATA +410 -0
  2. iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/RECORD +85 -0
  3. iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/WHEEL +5 -0
  4. iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/entry_points.txt +2 -0
  5. iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/licenses/LICENSE +21 -0
  6. iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/top_level.txt +1 -0
  7. src/__init__.py +8 -0
  8. src/ai_controller/README.md +139 -0
  9. src/ai_controller/__init__.py +12 -0
  10. src/ai_controller/agent_executor.py +596 -0
  11. src/ai_controller/cli/__init__.py +2 -0
  12. src/ai_controller/cli/main.py +243 -0
  13. src/ai_controller/session_manager.py +409 -0
  14. src/ai_controller/web/__init__.py +2 -0
  15. src/ai_controller/web/server.py +1181 -0
  16. src/ai_controller/web/static/css/README.md +102 -0
  17. src/api/__init__.py +13 -0
  18. src/api/case_management.py +271 -0
  19. src/api/edr.py +187 -0
  20. src/api/kb.py +136 -0
  21. src/api/siem.py +308 -0
  22. src/core/__init__.py +10 -0
  23. src/core/config.py +242 -0
  24. src/core/config_storage.py +684 -0
  25. src/core/dto.py +50 -0
  26. src/core/errors.py +36 -0
  27. src/core/logging.py +128 -0
  28. src/integrations/__init__.py +8 -0
  29. src/integrations/case_management/__init__.py +5 -0
  30. src/integrations/case_management/iris/__init__.py +11 -0
  31. src/integrations/case_management/iris/iris_client.py +885 -0
  32. src/integrations/case_management/iris/iris_http.py +274 -0
  33. src/integrations/case_management/iris/iris_mapper.py +263 -0
  34. src/integrations/case_management/iris/iris_models.py +128 -0
  35. src/integrations/case_management/thehive/__init__.py +8 -0
  36. src/integrations/case_management/thehive/thehive_client.py +193 -0
  37. src/integrations/case_management/thehive/thehive_http.py +147 -0
  38. src/integrations/case_management/thehive/thehive_mapper.py +190 -0
  39. src/integrations/case_management/thehive/thehive_models.py +125 -0
  40. src/integrations/cti/__init__.py +6 -0
  41. src/integrations/cti/local_tip/__init__.py +10 -0
  42. src/integrations/cti/local_tip/local_tip_client.py +90 -0
  43. src/integrations/cti/local_tip/local_tip_http.py +110 -0
  44. src/integrations/cti/opencti/__init__.py +10 -0
  45. src/integrations/cti/opencti/opencti_client.py +101 -0
  46. src/integrations/cti/opencti/opencti_http.py +418 -0
  47. src/integrations/edr/__init__.py +6 -0
  48. src/integrations/edr/elastic_defend/__init__.py +6 -0
  49. src/integrations/edr/elastic_defend/elastic_defend_client.py +351 -0
  50. src/integrations/edr/elastic_defend/elastic_defend_http.py +162 -0
  51. src/integrations/eng/__init__.py +10 -0
  52. src/integrations/eng/clickup/__init__.py +8 -0
  53. src/integrations/eng/clickup/clickup_client.py +513 -0
  54. src/integrations/eng/clickup/clickup_http.py +156 -0
  55. src/integrations/eng/github/__init__.py +8 -0
  56. src/integrations/eng/github/github_client.py +169 -0
  57. src/integrations/eng/github/github_http.py +158 -0
  58. src/integrations/eng/trello/__init__.py +8 -0
  59. src/integrations/eng/trello/trello_client.py +207 -0
  60. src/integrations/eng/trello/trello_http.py +162 -0
  61. src/integrations/kb/__init__.py +12 -0
  62. src/integrations/kb/fs_kb_client.py +313 -0
  63. src/integrations/siem/__init__.py +6 -0
  64. src/integrations/siem/elastic/__init__.py +6 -0
  65. src/integrations/siem/elastic/elastic_client.py +3319 -0
  66. src/integrations/siem/elastic/elastic_http.py +165 -0
  67. src/mcp/README.md +183 -0
  68. src/mcp/TOOLS.md +2827 -0
  69. src/mcp/__init__.py +13 -0
  70. src/mcp/__main__.py +18 -0
  71. src/mcp/agent_profiles.py +408 -0
  72. src/mcp/flow_agent_profiles.py +424 -0
  73. src/mcp/mcp_server.py +4086 -0
  74. src/mcp/rules_engine.py +487 -0
  75. src/mcp/runbook_manager.py +264 -0
  76. src/orchestrator/__init__.py +11 -0
  77. src/orchestrator/incident_workflow.py +244 -0
  78. src/orchestrator/tools_case.py +1085 -0
  79. src/orchestrator/tools_cti.py +359 -0
  80. src/orchestrator/tools_edr.py +315 -0
  81. src/orchestrator/tools_eng.py +378 -0
  82. src/orchestrator/tools_kb.py +156 -0
  83. src/orchestrator/tools_siem.py +1709 -0
  84. src/web/__init__.py +8 -0
  85. src/web/config_server.py +511 -0
@@ -0,0 +1,12 @@
1
+ """
2
+ AI Controller for SamiGPT.
3
+
4
+ This module provides a web-based controller for managing and executing agent commands,
5
+ with support for multiple concurrent sessions and scheduled auto-runs.
6
+ """
7
+
8
+ from .agent_executor import AgentExecutor
9
+ from .session_manager import SessionManager
10
+
11
+ __all__ = ["AgentExecutor", "SessionManager"]
12
+
@@ -0,0 +1,596 @@
1
+ """
2
+ Agent executor for parsing and executing agent commands.
3
+
4
+ Supports commands like:
5
+ - "run lookup_hash_ti on <hash>"
6
+ - "run get_security_alerts"
7
+ - etc.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import json
14
+ import os
15
+ import re
16
+ import shutil
17
+ import subprocess
18
+ from dataclasses import dataclass, asdict
19
+ from datetime import datetime
20
+ from typing import Any, Callable, Dict, List, Optional, Tuple
21
+ from enum import Enum
22
+
23
+ from ..core.config import SamiConfig
24
+ from ..core.config_storage import load_config_from_file
25
+ from ..core.logging import get_logger
26
+
27
+ logger = get_logger("sami.ai_controller.agent_executor")
28
+
29
+
30
+ class CommandType(Enum):
31
+ """Types of commands that can be executed."""
32
+ RUN_TOOL = "run_tool"
33
+ RUN_RUNBOOK = "run_runbook"
34
+ RUN_AGENT = "run_agent"
35
+ UNKNOWN = "unknown"
36
+
37
+
38
+ @dataclass
39
+ class Command:
40
+ """Represents a parsed command."""
41
+ raw: str
42
+ command_type: CommandType
43
+ tool_name: Optional[str] = None
44
+ arguments: Dict[str, Any] = None
45
+ agent_name: Optional[str] = None
46
+ runbook_name: Optional[str] = None
47
+
48
+ def __post_init__(self):
49
+ if self.arguments is None:
50
+ self.arguments = {}
51
+
52
+
53
+ @dataclass
54
+ class ExecutionResult:
55
+ """Result of an agent execution."""
56
+ success: bool
57
+ output: Any
58
+ error: Optional[str] = None
59
+ timestamp: Optional[datetime] = None
60
+
61
+ def to_dict(self) -> Dict[str, Any]:
62
+ """Convert to dictionary."""
63
+ result = asdict(self)
64
+ if self.timestamp:
65
+ result["timestamp"] = self.timestamp.isoformat()
66
+
67
+ # Ensure output is JSON-serializable
68
+ if result.get("output") is not None:
69
+ try:
70
+ # Try to serialize to ensure it's JSON-compatible
71
+ json.dumps(result["output"])
72
+ except (TypeError, ValueError):
73
+ # If not serializable, convert to string representation
74
+ result["output"] = {
75
+ "raw": str(result["output"]),
76
+ "text": str(result["output"])
77
+ }
78
+
79
+ return result
80
+
81
+
82
+ class AgentExecutor:
83
+ """Executes agent commands and manages tool execution."""
84
+
85
+ def __init__(self, config: Optional[SamiConfig] = None):
86
+ """Initialize the agent executor."""
87
+ self.config = config or load_config_from_file()
88
+ # Track currently running external process (e.g., cursor-agent) so we can cancel it
89
+ self._current_process: Optional[subprocess.Popen] = None
90
+ self._tool_registry: Dict[str, Callable] = {}
91
+ self._initialize_tools()
92
+
93
+ def _initialize_tools(self):
94
+ """Initialize the tool registry with available tools."""
95
+ # Import tools from orchestrator
96
+ from ..orchestrator import tools_cti, tools_case, tools_edr, tools_siem, tools_kb
97
+
98
+ # CTI tools
99
+ self._tool_registry["lookup_hash_ti"] = tools_cti.lookup_hash_ti
100
+ # Add more tools as needed
101
+
102
+ # For now, we'll dynamically import tools as needed
103
+ logger.info(f"Initialized agent executor with {len(self._tool_registry)} tools")
104
+
105
+ def parse_command(self, command_str: str) -> Command:
106
+ """
107
+ Parse a command string into a Command object.
108
+
109
+ Supported formats:
110
+ - "run <tool_name> on <arg_value>"
111
+ - "run <tool_name> with <arg_key>=<arg_value>"
112
+ - "run <tool_name> <arg_value>"
113
+ - "run <agent_name> agent on <target>"
114
+ - "run <runbook_name> runbook on <target>"
115
+
116
+ Supports quoted strings for tool names and values.
117
+ """
118
+ command_str = command_str.strip()
119
+
120
+ # Helper to strip quotes
121
+ def strip_quotes(s: str) -> str:
122
+ s = s.strip()
123
+ if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")):
124
+ return s[1:-1]
125
+ return s
126
+
127
+ # Pattern: "run <tool_name> on <value>" (supports quoted tool names and values)
128
+ # First try with optional quotes around tool name
129
+ match = re.match(r'^run\s+("?)([\w_]+)\1\s+on\s+(.+)$', command_str, re.IGNORECASE)
130
+ if match:
131
+ tool_or_agent = match.group(2) # The tool name (without quotes)
132
+ value = match.group(3).strip() # The value after "on"
133
+ value = strip_quotes(value)
134
+ else:
135
+ # Try without quotes around tool name
136
+ match = re.match(r'^run\s+(\w+)\s+on\s+(.+)$', command_str, re.IGNORECASE)
137
+ if match:
138
+ tool_or_agent = match.group(1)
139
+ value = match.group(2).strip()
140
+ value = strip_quotes(value)
141
+
142
+ if match:
143
+
144
+ # Check if it's an agent
145
+ if tool_or_agent.endswith("_agent") or tool_or_agent in ["soc1", "soc2", "soc3"]:
146
+ return Command(
147
+ raw=command_str,
148
+ command_type=CommandType.RUN_AGENT,
149
+ agent_name=tool_or_agent,
150
+ arguments={"target": value}
151
+ )
152
+
153
+ # Check if it's a runbook
154
+ if "_runbook" in tool_or_agent or tool_or_agent.endswith("_triage") or tool_or_agent.endswith("_investigation"):
155
+ return Command(
156
+ raw=command_str,
157
+ command_type=CommandType.RUN_RUNBOOK,
158
+ runbook_name=tool_or_agent,
159
+ arguments={"target": value}
160
+ )
161
+
162
+ # Default to tool
163
+ # Try to infer the argument name based on tool name
164
+ arg_name = self._infer_argument_name(tool_or_agent)
165
+ return Command(
166
+ raw=command_str,
167
+ command_type=CommandType.RUN_TOOL,
168
+ tool_name=tool_or_agent,
169
+ arguments={arg_name: value}
170
+ )
171
+
172
+ # Pattern: "run <tool_name> with <key>=<value>" (supports quoted tool names)
173
+ match = re.match(r'^run\s+("?)([\w_]+)\1\s+with\s+(.+)$', command_str, re.IGNORECASE)
174
+ if match:
175
+ tool_name = match.group(2) # Tool name (without quotes)
176
+ params_str = match.group(3) # Parameters after "with"
177
+ else:
178
+ match = re.match(r'^run\s+(\w+)\s+with\s+(.+)$', command_str, re.IGNORECASE)
179
+ if match:
180
+ tool_name = match.group(1)
181
+ params_str = match.group(2)
182
+
183
+ if match:
184
+ arguments = self._parse_arguments(params_str)
185
+ return Command(
186
+ raw=command_str,
187
+ command_type=CommandType.RUN_TOOL,
188
+ tool_name=tool_name,
189
+ arguments=arguments
190
+ )
191
+
192
+ # Pattern: "run <tool_name> <value>" (simple single argument, supports quoted)
193
+ match = re.match(r'^run\s+("?)([\w_]+)\1\s+(.+)$', command_str, re.IGNORECASE)
194
+ if match:
195
+ tool_name = match.group(2) # Tool name (without quotes)
196
+ value = match.group(3).strip() # Value
197
+ value = strip_quotes(value)
198
+ else:
199
+ match = re.match(r'^run\s+(\w+)\s+(.+)$', command_str, re.IGNORECASE)
200
+ if match:
201
+ tool_name = match.group(1)
202
+ value = match.group(2).strip()
203
+ value = strip_quotes(value)
204
+
205
+ if match:
206
+ arg_name = self._infer_argument_name(tool_name)
207
+ return Command(
208
+ raw=command_str,
209
+ command_type=CommandType.RUN_TOOL,
210
+ tool_name=tool_name,
211
+ arguments={arg_name: value}
212
+ )
213
+
214
+ # Unknown command - treat as freeform prompt for external agent
215
+ # (e.g., Cursor IDE cursor-agent). We don't log a warning here to
216
+ # avoid confusing users when they enter natural language prompts.
217
+ logger.debug(f"Treating input as freeform prompt: {command_str}")
218
+ return Command(
219
+ raw=command_str,
220
+ command_type=CommandType.UNKNOWN
221
+ )
222
+
223
+ def _infer_argument_name(self, tool_name: str) -> str:
224
+ """Infer the argument name based on tool name."""
225
+ # Common patterns
226
+ if "hash" in tool_name.lower():
227
+ return "hash_value"
228
+ elif "ip" in tool_name.lower():
229
+ return "ip"
230
+ elif "domain" in tool_name.lower():
231
+ return "domain"
232
+ elif "alert" in tool_name.lower():
233
+ return "alert_id"
234
+ elif "case" in tool_name.lower():
235
+ return "case_id"
236
+ elif "user" in tool_name.lower():
237
+ return "username"
238
+ else:
239
+ return "value"
240
+
241
+ def _parse_arguments(self, params_str: str) -> Dict[str, Any]:
242
+ """Parse argument string like 'key1=value1 key2=value2'."""
243
+ arguments = {}
244
+ # Simple parsing - can be enhanced
245
+ for param in params_str.split():
246
+ if "=" in param:
247
+ key, value = param.split("=", 1)
248
+ arguments[key.strip()] = value.strip()
249
+ return arguments
250
+
251
+ async def execute_command(self, command: Command) -> ExecutionResult:
252
+ """
253
+ Execute a parsed command and return the result.
254
+
255
+ This method handles tool execution and can be extended to support
256
+ agents and runbooks.
257
+ """
258
+ try:
259
+ if command.command_type == CommandType.RUN_TOOL:
260
+ return await self._execute_tool(command)
261
+ elif command.command_type == CommandType.RUN_AGENT:
262
+ return await self._execute_agent(command)
263
+ elif command.command_type == CommandType.RUN_RUNBOOK:
264
+ return await self._execute_runbook(command)
265
+ elif command.command_type == CommandType.UNKNOWN:
266
+ # Fallback: treat as freeform prompt and forward to external agent
267
+ return await self._execute_freeform_prompt(command.raw)
268
+ else:
269
+ return ExecutionResult(
270
+ success=False,
271
+ output=None,
272
+ error=f"Unknown command type: {command.command_type}",
273
+ timestamp=datetime.now()
274
+ )
275
+ except Exception as e:
276
+ logger.exception(f"Error executing command: {command.raw}")
277
+ return ExecutionResult(
278
+ success=False,
279
+ output=None,
280
+ error=str(e),
281
+ timestamp=datetime.now()
282
+ )
283
+
284
+ async def _execute_tool(self, command: Command) -> ExecutionResult:
285
+ """Execute a tool command."""
286
+ if not command.tool_name:
287
+ return ExecutionResult(
288
+ success=False,
289
+ output=None,
290
+ error="No tool name specified",
291
+ timestamp=datetime.now()
292
+ )
293
+
294
+ # Check if tool is registered
295
+ if command.tool_name not in self._tool_registry:
296
+ # Try to dynamically import the tool
297
+ tool_func = await self._load_tool(command.tool_name)
298
+ if not tool_func:
299
+ return ExecutionResult(
300
+ success=False,
301
+ output=None,
302
+ error=f"Tool '{command.tool_name}' not found",
303
+ timestamp=datetime.now()
304
+ )
305
+ self._tool_registry[command.tool_name] = tool_func
306
+
307
+ tool_func = self._tool_registry[command.tool_name]
308
+
309
+ # Prepare clients based on tool requirements
310
+ # Prepare clients before the lambda to avoid import issues
311
+ clients = self._prepare_clients(command.tool_name)
312
+
313
+ if not clients:
314
+ return ExecutionResult(
315
+ success=False,
316
+ output=None,
317
+ error=f"No clients available for tool '{command.tool_name}'. Check configuration.",
318
+ timestamp=datetime.now()
319
+ )
320
+
321
+ # Execute tool in executor to avoid blocking
322
+ loop = asyncio.get_event_loop()
323
+ try:
324
+ # Capture clients in closure before lambda
325
+ tool_args = command.arguments.copy()
326
+ if isinstance(clients, list):
327
+ tool_args["clients"] = clients
328
+ else:
329
+ tool_args["client"] = clients
330
+
331
+ result = await loop.run_in_executor(
332
+ None,
333
+ lambda: tool_func(**tool_args)
334
+ )
335
+
336
+ return ExecutionResult(
337
+ success=True,
338
+ output=result,
339
+ timestamp=datetime.now()
340
+ )
341
+ except Exception as e:
342
+ logger.exception(f"Error executing tool {command.tool_name}")
343
+ return ExecutionResult(
344
+ success=False,
345
+ output=None,
346
+ error=str(e),
347
+ timestamp=datetime.now()
348
+ )
349
+
350
+ async def _execute_agent(self, command: Command) -> ExecutionResult:
351
+ """Execute an agent command (for future implementation)."""
352
+ # TODO: Implement agent execution
353
+ return ExecutionResult(
354
+ success=False,
355
+ output=None,
356
+ error="Agent execution not yet implemented",
357
+ timestamp=datetime.now()
358
+ )
359
+
360
+ async def _execute_runbook(self, command: Command) -> ExecutionResult:
361
+ """Execute a runbook command (for future implementation)."""
362
+ # TODO: Implement runbook execution
363
+ return ExecutionResult(
364
+ success=False,
365
+ output=None,
366
+ error="Runbook execution not yet implemented",
367
+ timestamp=datetime.now()
368
+ )
369
+
370
+ async def _execute_freeform_prompt(self, prompt: str) -> ExecutionResult:
371
+ """
372
+ Execute a freeform prompt by forwarding it to an external agent
373
+ (Cursor IDE's cursor-agent binary).
374
+
375
+ This allows the AI Controller UI to behave like a normal terminal
376
+ where arbitrary prompts are handled by the agent, without requiring
377
+ strict 'run <tool>' syntax.
378
+ """
379
+
380
+ loop = asyncio.get_event_loop()
381
+
382
+ def _run_cursor_agent(prompt_text: str) -> Dict[str, Any]:
383
+ """
384
+ Run the external cursor-agent process and capture its output.
385
+
386
+ Uses:
387
+ cursor-agent --print --output-format text "<prompt>"
388
+ """
389
+
390
+ # Try to locate cursor-agent binary
391
+ possible_paths = [
392
+ "/usr/local/bin/cursor-agent",
393
+ "/usr/bin/cursor-agent",
394
+ os.path.expanduser("~/.local/bin/cursor-agent"),
395
+ "/opt/homebrew/bin/cursor-agent",
396
+ ]
397
+
398
+ cursor_agent_bin: Optional[str] = None
399
+
400
+ for path in possible_paths:
401
+ if os.path.exists(path) and os.access(path, os.X_OK):
402
+ cursor_agent_bin = path
403
+ break
404
+
405
+ if cursor_agent_bin is None:
406
+ # Fall back to PATH lookup
407
+ cursor_agent_bin = shutil.which("cursor-agent")
408
+
409
+ if cursor_agent_bin is None:
410
+ raise RuntimeError(
411
+ "Cursor IDE 'cursor-agent' binary not found in common locations or PATH. "
412
+ "Install Cursor and ensure 'cursor-agent' is available."
413
+ )
414
+
415
+ # Build command
416
+ # --approve-mcps: auto-approve MCP server/tool usage so Cursor
417
+ # doesn't prompt interactively for each tool call.
418
+ cmd = [
419
+ cursor_agent_bin,
420
+ "--force",
421
+ "--approve-mcps",
422
+ "--print",
423
+ "--output-format",
424
+ "text",
425
+ prompt_text,
426
+ ]
427
+
428
+ logger.debug(f"Executing external cursor-agent: {' '.join(cmd)}")
429
+
430
+ # Use Popen so we can terminate the process on cancellation
431
+ proc = subprocess.Popen(
432
+ cmd,
433
+ stdout=subprocess.PIPE,
434
+ stderr=subprocess.PIPE,
435
+ text=True,
436
+ )
437
+
438
+ # Register process so it can be cancelled from outside
439
+ self._current_process = proc
440
+ try:
441
+ stdout, stderr = proc.communicate()
442
+ finally:
443
+ # Clear reference once process is finished
444
+ self._current_process = None
445
+
446
+ return {
447
+ "returncode": proc.returncode,
448
+ "stdout": (stdout or "").strip(),
449
+ "stderr": (stderr or "").strip(),
450
+ "command": cmd,
451
+ }
452
+
453
+ try:
454
+ result = await loop.run_in_executor(None, _run_cursor_agent, prompt)
455
+
456
+ success = result.get("returncode", 1) == 0
457
+ output: Dict[str, Any] = {
458
+ "stdout": result.get("stdout", ""),
459
+ "stderr": result.get("stderr", ""),
460
+ "command": " ".join(result.get("command", [])),
461
+ }
462
+
463
+ # Prefer stdout as primary output for UI
464
+ primary_output = result.get("stdout", "") or result.get("stderr", "")
465
+
466
+ return ExecutionResult(
467
+ success=success,
468
+ output={
469
+ "raw": output,
470
+ "text": primary_output,
471
+ },
472
+ error=None if success else result.get("stderr", "cursor-agent failed"),
473
+ timestamp=datetime.now(),
474
+ )
475
+ except Exception as e:
476
+ logger.exception("Error forwarding prompt to external cursor-agent")
477
+ return ExecutionResult(
478
+ success=False,
479
+ output=None,
480
+ error=str(e),
481
+ timestamp=datetime.now(),
482
+ )
483
+
484
+ def cancel_current_execution(self):
485
+ """
486
+ Best-effort cancellation of any currently running external process.
487
+
488
+ This is primarily used to stop a long-running cursor-agent process when
489
+ the user clicks Stop or closes the session in the web UI.
490
+ """
491
+ proc = self._current_process
492
+ if not proc:
493
+ return
494
+
495
+ if proc.poll() is not None:
496
+ # Already finished
497
+ self._current_process = None
498
+ return
499
+
500
+ try:
501
+ logger.info("Attempting to terminate external process (pid=%s)", proc.pid)
502
+ proc.terminate()
503
+ try:
504
+ proc.wait(timeout=5)
505
+ except subprocess.TimeoutExpired:
506
+ logger.warning("Process did not exit after terminate, killing (pid=%s)", proc.pid)
507
+ proc.kill()
508
+ except Exception as e:
509
+ logger.warning("Failed to cancel external process: %s", e)
510
+ finally:
511
+ self._current_process = None
512
+
513
+ async def _load_tool(self, tool_name: str) -> Optional[Callable]:
514
+ """Dynamically load a tool function."""
515
+ try:
516
+ # Map tool names to their modules and functions
517
+ tool_mapping = {
518
+ "lookup_hash_ti": ("..orchestrator.tools_cti", "lookup_hash_ti"),
519
+ "get_security_alerts": ("..orchestrator.tools_siem", "get_security_alerts"),
520
+ "get_security_alert_by_id": ("..orchestrator.tools_siem", "get_security_alert_by_id"),
521
+ "lookup_ip_ti": ("..orchestrator.tools_cti", "lookup_ip_ti"),
522
+ "get_ip_address_report": ("..orchestrator.tools_siem", "get_ip_address_report"),
523
+ # Add more mappings as needed
524
+ }
525
+
526
+ if tool_name in tool_mapping:
527
+ module_path, func_name = tool_mapping[tool_name]
528
+ module = __import__(module_path, fromlist=[func_name])
529
+ return getattr(module, func_name)
530
+
531
+ return None
532
+ except Exception as e:
533
+ logger.error(f"Failed to load tool {tool_name}: {e}")
534
+ return None
535
+
536
+ def _prepare_clients(self, tool_name: str):
537
+ """Prepare client objects needed for tool execution."""
538
+ try:
539
+ # Determine which clients are needed based on tool name
540
+ clients = []
541
+
542
+ if "cti" in tool_name.lower() or "ti" in tool_name.lower():
543
+ # Need CTI clients - load from config dict to support cti_opencti
544
+ from src.core.config_storage import get_config_dict
545
+ from src.core.config import CTIConfig, SamiConfig
546
+
547
+ config_dict = get_config_dict()
548
+
549
+ if self.config.cti:
550
+ from src.integrations.cti.local_tip.local_tip_client import LocalTipCTIClient
551
+ clients.append(LocalTipCTIClient.from_config(self.config))
552
+
553
+ # Check for cti_opencti in config dict (not in SamiConfig dataclass)
554
+ if config_dict.get("cti_opencti"):
555
+ from src.integrations.cti.opencti.opencti_client import OpenCTIClient
556
+ cti_opencti_config = config_dict["cti_opencti"]
557
+ opencti_cfg = CTIConfig(
558
+ cti_type=cti_opencti_config.get("cti_type", "opencti"),
559
+ base_url=cti_opencti_config.get("base_url"),
560
+ api_key=cti_opencti_config.get("api_key"),
561
+ timeout_seconds=cti_opencti_config.get("timeout_seconds", 30),
562
+ verify_ssl=cti_opencti_config.get("verify_ssl", False),
563
+ )
564
+ temp_config = SamiConfig(cti=opencti_cfg)
565
+ clients.append(OpenCTIClient.from_config(temp_config))
566
+
567
+ if "siem" in tool_name.lower() or "alert" in tool_name.lower():
568
+ # Need SIEM client
569
+ if self.config.elastic:
570
+ from src.integrations.siem.elastic.elastic_client import ElasticSIEMClient
571
+ clients.append(ElasticSIEMClient.from_config(self.config))
572
+
573
+ if "edr" in tool_name.lower():
574
+ # Need EDR client
575
+ if self.config.edr:
576
+ from src.integrations.edr.elastic_defend.elastic_defend_client import ElasticDefendEDRClient
577
+ clients.append(ElasticDefendEDRClient.from_config(self.config))
578
+
579
+ if "case" in tool_name.lower():
580
+ # Need case management client
581
+ if self.config.iris:
582
+ from src.integrations.case_management.iris.iris_client import IRISCaseManagementClient
583
+ clients.append(IRISCaseManagementClient.from_config(self.config))
584
+ elif self.config.thehive:
585
+ from src.integrations.case_management.thehive.thehive_client import TheHiveCaseManagementClient
586
+ clients.append(TheHiveCaseManagementClient.from_config(self.config))
587
+
588
+ if not clients:
589
+ logger.warning(f"No clients found for tool {tool_name}. Check configuration.")
590
+ return None
591
+
592
+ return clients[0] if len(clients) == 1 else clients
593
+ except Exception as e:
594
+ logger.exception(f"Failed to prepare clients for {tool_name}: {e}")
595
+ return None
596
+
@@ -0,0 +1,2 @@
1
+ """CLI for AI Controller."""
2
+