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,684 @@
1
+ """
2
+ Configuration storage and loading for SamiGPT.
3
+
4
+ This module provides functions to save and load configuration from both JSON and .env files,
5
+ allowing the web UI to manage configurations and supporting manual file editing.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import shutil
13
+ from pathlib import Path
14
+ from typing import Any, Dict, Optional
15
+
16
+ from .config import (
17
+ CTIConfig,
18
+ ClickUpConfig,
19
+ EDRConfig,
20
+ ElasticConfig,
21
+ EngConfig,
22
+ GitHubConfig,
23
+ IrisConfig,
24
+ LoggingConfig,
25
+ SamiConfig,
26
+ TheHiveConfig,
27
+ TrelloConfig,
28
+ )
29
+ from .errors import ConfigError
30
+
31
+
32
+ CONFIG_FILE = os.getenv("SAMIGPT_CONFIG_FILE", "config.json")
33
+ ENV_FILE = os.getenv("SAMIGPT_ENV_FILE", ".env")
34
+ STARTING_CONFIG_FILE = os.getenv("SAMIGPT_STARTING_CONFIG_FILE", "config.json.example")
35
+
36
+
37
+ def _config_to_dict(config: SamiConfig) -> Dict[str, Any]:
38
+ """Convert SamiConfig to a dictionary."""
39
+ result: Dict[str, Any] = {
40
+ "logging": {
41
+ "log_dir": config.logging.log_dir if config.logging else "logs",
42
+ "log_level": config.logging.log_level if config.logging else "INFO",
43
+ },
44
+ }
45
+
46
+ if config.thehive:
47
+ result["thehive"] = {
48
+ "base_url": config.thehive.base_url,
49
+ "api_key": config.thehive.api_key,
50
+ "timeout_seconds": config.thehive.timeout_seconds,
51
+ }
52
+
53
+ if config.iris:
54
+ result["iris"] = {
55
+ "base_url": config.iris.base_url,
56
+ "api_key": config.iris.api_key,
57
+ "timeout_seconds": config.iris.timeout_seconds,
58
+ "verify_ssl": config.iris.verify_ssl,
59
+ }
60
+
61
+ if config.elastic:
62
+ result["elastic"] = {
63
+ "base_url": config.elastic.base_url,
64
+ "api_key": config.elastic.api_key,
65
+ "username": config.elastic.username,
66
+ "password": config.elastic.password,
67
+ "timeout_seconds": config.elastic.timeout_seconds,
68
+ "verify_ssl": config.elastic.verify_ssl,
69
+ }
70
+
71
+ if config.edr:
72
+ result["edr"] = {
73
+ "edr_type": config.edr.edr_type,
74
+ "base_url": config.edr.base_url,
75
+ "api_key": config.edr.api_key,
76
+ "timeout_seconds": config.edr.timeout_seconds,
77
+ "verify_ssl": config.edr.verify_ssl,
78
+ "additional_params": config.edr.additional_params,
79
+ }
80
+
81
+ if config.cti:
82
+ result["cti"] = {
83
+ "cti_type": config.cti.cti_type,
84
+ "base_url": config.cti.base_url,
85
+ "api_key": config.cti.api_key,
86
+ "timeout_seconds": config.cti.timeout_seconds,
87
+ "verify_ssl": config.cti.verify_ssl,
88
+ }
89
+
90
+ if config.eng:
91
+ eng_dict: Dict[str, Any] = {
92
+ "provider": config.eng.provider,
93
+ }
94
+ if config.eng.trello:
95
+ eng_dict["trello"] = {
96
+ "api_key": config.eng.trello.api_key,
97
+ "api_token": config.eng.trello.api_token,
98
+ "fine_tuning_board_id": config.eng.trello.fine_tuning_board_id,
99
+ "engineering_board_id": config.eng.trello.engineering_board_id,
100
+ "timeout_seconds": config.eng.trello.timeout_seconds,
101
+ "verify_ssl": config.eng.trello.verify_ssl,
102
+ }
103
+ if config.eng.clickup:
104
+ eng_dict["clickup"] = {
105
+ "api_token": config.eng.clickup.api_token,
106
+ "fine_tuning_list_id": config.eng.clickup.fine_tuning_list_id,
107
+ "engineering_list_id": config.eng.clickup.engineering_list_id,
108
+ "timeout_seconds": config.eng.clickup.timeout_seconds,
109
+ "verify_ssl": config.eng.clickup.verify_ssl,
110
+ }
111
+ if config.eng.clickup.space_id:
112
+ eng_dict["clickup"]["space_id"] = config.eng.clickup.space_id
113
+ if config.eng.github:
114
+ eng_dict["github"] = {
115
+ "api_token": config.eng.github.api_token,
116
+ "fine_tuning_project_id": config.eng.github.fine_tuning_project_id,
117
+ "engineering_project_id": config.eng.github.engineering_project_id,
118
+ "timeout_seconds": config.eng.github.timeout_seconds,
119
+ "verify_ssl": config.eng.github.verify_ssl,
120
+ }
121
+ if eng_dict:
122
+ result["eng"] = eng_dict
123
+
124
+ return result
125
+
126
+
127
+ def _dict_to_config(data: Dict[str, Any]) -> SamiConfig:
128
+ """Convert a dictionary to SamiConfig."""
129
+ logging_data = data.get("logging", {})
130
+ logging_cfg = LoggingConfig(
131
+ log_dir=logging_data.get("log_dir", "logs"),
132
+ log_level=logging_data.get("log_level", "INFO"),
133
+ ) if logging_data else LoggingConfig()
134
+
135
+ thehive_cfg: Optional[TheHiveConfig] = None
136
+ if "thehive" in data and data["thehive"]:
137
+ th_data = data["thehive"]
138
+ if th_data.get("base_url") and th_data.get("api_key"):
139
+ thehive_cfg = TheHiveConfig(
140
+ base_url=th_data["base_url"],
141
+ api_key=th_data["api_key"],
142
+ timeout_seconds=th_data.get("timeout_seconds", 30),
143
+ )
144
+
145
+ iris_cfg: Optional[IrisConfig] = None
146
+ if "iris" in data and data["iris"]:
147
+ iris_data = data["iris"]
148
+ if iris_data.get("base_url") and iris_data.get("api_key"):
149
+ iris_cfg = IrisConfig(
150
+ base_url=iris_data["base_url"],
151
+ api_key=iris_data["api_key"],
152
+ timeout_seconds=iris_data.get("timeout_seconds", 30),
153
+ verify_ssl=iris_data.get("verify_ssl", True),
154
+ )
155
+
156
+ elastic_cfg: Optional[ElasticConfig] = None
157
+ if "elastic" in data and data["elastic"]:
158
+ el_data = data["elastic"]
159
+ if el_data.get("base_url"):
160
+ elastic_cfg = ElasticConfig(
161
+ base_url=el_data["base_url"],
162
+ api_key=el_data.get("api_key"),
163
+ username=el_data.get("username"),
164
+ password=el_data.get("password"),
165
+ timeout_seconds=el_data.get("timeout_seconds", 30),
166
+ verify_ssl=el_data.get("verify_ssl", True),
167
+ )
168
+
169
+ edr_cfg: Optional[EDRConfig] = None
170
+ if "edr" in data and data["edr"]:
171
+ edr_data = data["edr"]
172
+ if edr_data.get("base_url") and edr_data.get("api_key"):
173
+ edr_cfg = EDRConfig(
174
+ edr_type=edr_data.get("edr_type", "velociraptor"),
175
+ base_url=edr_data["base_url"],
176
+ api_key=edr_data["api_key"],
177
+ timeout_seconds=edr_data.get("timeout_seconds", 30),
178
+ verify_ssl=edr_data.get("verify_ssl", True),
179
+ additional_params=edr_data.get("additional_params"),
180
+ )
181
+
182
+ cti_cfg: Optional[CTIConfig] = None
183
+ if "cti" in data and data["cti"]:
184
+ cti_data = data["cti"]
185
+ if cti_data.get("base_url"):
186
+ # Handle api_key - it's optional for local_tip but required for opencti
187
+ api_key = cti_data.get("api_key")
188
+ cti_cfg = CTIConfig(
189
+ cti_type=cti_data.get("cti_type", "local_tip"),
190
+ base_url=cti_data["base_url"],
191
+ api_key=api_key,
192
+ timeout_seconds=cti_data.get("timeout_seconds", 30),
193
+ verify_ssl=cti_data.get("verify_ssl", True),
194
+ )
195
+
196
+ eng_cfg: Optional[EngConfig] = None
197
+ if "eng" in data and data["eng"]:
198
+ eng_data = data["eng"]
199
+ provider = eng_data.get("provider", "trello")
200
+
201
+ trello_cfg: Optional[TrelloConfig] = None
202
+ if eng_data.get("trello"):
203
+ trello_data = eng_data["trello"]
204
+ if trello_data.get("api_key") and trello_data.get("api_token"):
205
+ trello_cfg = TrelloConfig(
206
+ api_key=trello_data["api_key"],
207
+ api_token=trello_data["api_token"],
208
+ fine_tuning_board_id=trello_data["fine_tuning_board_id"],
209
+ engineering_board_id=trello_data["engineering_board_id"],
210
+ timeout_seconds=trello_data.get("timeout_seconds", 30),
211
+ verify_ssl=trello_data.get("verify_ssl", True),
212
+ )
213
+
214
+ clickup_cfg: Optional[ClickUpConfig] = None
215
+ if eng_data.get("clickup"):
216
+ clickup_data = eng_data["clickup"]
217
+ if clickup_data.get("api_token"):
218
+ clickup_cfg = ClickUpConfig(
219
+ api_token=clickup_data["api_token"],
220
+ fine_tuning_list_id=clickup_data["fine_tuning_list_id"],
221
+ engineering_list_id=clickup_data["engineering_list_id"],
222
+ space_id=clickup_data.get("space_id"),
223
+ timeout_seconds=clickup_data.get("timeout_seconds", 30),
224
+ verify_ssl=clickup_data.get("verify_ssl", True),
225
+ )
226
+
227
+ github_cfg: Optional[GitHubConfig] = None
228
+ if eng_data.get("github"):
229
+ github_data = eng_data["github"]
230
+ if github_data.get("api_token"):
231
+ github_cfg = GitHubConfig(
232
+ api_token=github_data["api_token"],
233
+ fine_tuning_project_id=github_data["fine_tuning_project_id"],
234
+ engineering_project_id=github_data["engineering_project_id"],
235
+ timeout_seconds=github_data.get("timeout_seconds", 30),
236
+ verify_ssl=github_data.get("verify_ssl", True),
237
+ )
238
+
239
+ if trello_cfg or clickup_cfg or github_cfg:
240
+ eng_cfg = EngConfig(
241
+ trello=trello_cfg,
242
+ clickup=clickup_cfg,
243
+ github=github_cfg,
244
+ provider=provider,
245
+ )
246
+
247
+ return SamiConfig(
248
+ thehive=thehive_cfg,
249
+ iris=iris_cfg,
250
+ elastic=elastic_cfg,
251
+ edr=edr_cfg,
252
+ cti=cti_cfg,
253
+ eng=eng_cfg,
254
+ logging=logging_cfg,
255
+ )
256
+
257
+
258
+ def load_config_from_env_file(env_path: str = ENV_FILE) -> Dict[str, Any]:
259
+ """
260
+ Load configuration from a .env file.
261
+
262
+ Args:
263
+ env_path: Path to the .env file.
264
+
265
+ Returns:
266
+ Dictionary with configuration values.
267
+
268
+ Raises:
269
+ ConfigError: If the file cannot be read or parsed.
270
+ """
271
+ env_file = Path(env_path)
272
+ config_dict: Dict[str, Any] = {}
273
+
274
+ if not env_file.exists():
275
+ return config_dict
276
+
277
+ try:
278
+ with open(env_file, "r") as f:
279
+ for line in f:
280
+ line = line.strip()
281
+ # Skip comments and empty lines
282
+ if not line or line.startswith("#"):
283
+ continue
284
+ # Parse KEY=VALUE
285
+ if "=" in line:
286
+ key, value = line.split("=", 1)
287
+ key = key.strip()
288
+ value = value.strip()
289
+ # Remove quotes if present
290
+ if value.startswith('"') and value.endswith('"'):
291
+ value = value[1:-1]
292
+ elif value.startswith("'") and value.endswith("'"):
293
+ value = value[1:-1]
294
+ config_dict[key] = value
295
+ return config_dict
296
+ except Exception as e:
297
+ raise ConfigError(f"Failed to load .env file: {e}") from e
298
+
299
+
300
+ def _env_dict_to_config(env_dict: Dict[str, Any]) -> SamiConfig:
301
+ """Convert .env dictionary to SamiConfig."""
302
+ logging_cfg = LoggingConfig(
303
+ log_dir=env_dict.get("SAMIGPT_LOG_DIR", "logs"),
304
+ log_level=env_dict.get("SAMIGPT_LOG_LEVEL", "INFO"),
305
+ )
306
+
307
+ thehive_cfg: Optional[TheHiveConfig] = None
308
+ thehive_url = env_dict.get("SAMIGPT_THEHIVE_URL")
309
+ thehive_api_key = env_dict.get("SAMIGPT_THEHIVE_API_KEY")
310
+ if thehive_url and thehive_api_key:
311
+ timeout = int(env_dict.get("SAMIGPT_THEHIVE_TIMEOUT_SECONDS", "30"))
312
+ thehive_cfg = TheHiveConfig(
313
+ base_url=thehive_url,
314
+ api_key=thehive_api_key,
315
+ timeout_seconds=timeout,
316
+ )
317
+
318
+ iris_cfg: Optional[IrisConfig] = None
319
+ iris_url = env_dict.get("SAMIGPT_IRIS_URL")
320
+ iris_api_key = env_dict.get("SAMIGPT_IRIS_API_KEY")
321
+ if iris_url and iris_api_key:
322
+ timeout = int(env_dict.get("SAMIGPT_IRIS_TIMEOUT_SECONDS", "30"))
323
+ verify_ssl = env_dict.get("SAMIGPT_IRIS_VERIFY_SSL", "true").lower() in ("true", "1", "yes")
324
+ iris_cfg = IrisConfig(
325
+ base_url=iris_url,
326
+ api_key=iris_api_key,
327
+ timeout_seconds=timeout,
328
+ verify_ssl=verify_ssl,
329
+ )
330
+
331
+ elastic_cfg: Optional[ElasticConfig] = None
332
+ elastic_url = env_dict.get("SAMIGPT_ELASTIC_URL")
333
+ if elastic_url:
334
+ timeout = int(env_dict.get("SAMIGPT_ELASTIC_TIMEOUT_SECONDS", "30"))
335
+ verify_ssl = env_dict.get("SAMIGPT_ELASTIC_VERIFY_SSL", "true").lower() in ("true", "1", "yes")
336
+ elastic_cfg = ElasticConfig(
337
+ base_url=elastic_url,
338
+ api_key=env_dict.get("SAMIGPT_ELASTIC_API_KEY"),
339
+ username=env_dict.get("SAMIGPT_ELASTIC_USERNAME"),
340
+ password=env_dict.get("SAMIGPT_ELASTIC_PASSWORD"),
341
+ timeout_seconds=timeout,
342
+ verify_ssl=verify_ssl,
343
+ )
344
+
345
+ edr_cfg: Optional[EDRConfig] = None
346
+ edr_url = env_dict.get("SAMIGPT_EDR_URL")
347
+ edr_api_key = env_dict.get("SAMIGPT_EDR_API_KEY")
348
+ if edr_url and edr_api_key:
349
+ timeout = int(env_dict.get("SAMIGPT_EDR_TIMEOUT_SECONDS", "30"))
350
+ verify_ssl = env_dict.get("SAMIGPT_EDR_VERIFY_SSL", "true").lower() in ("true", "1", "yes")
351
+ edr_cfg = EDRConfig(
352
+ edr_type=env_dict.get("SAMIGPT_EDR_TYPE", "velociraptor"),
353
+ base_url=edr_url,
354
+ api_key=edr_api_key,
355
+ timeout_seconds=timeout,
356
+ verify_ssl=verify_ssl,
357
+ )
358
+
359
+ return SamiConfig(
360
+ thehive=thehive_cfg,
361
+ iris=iris_cfg,
362
+ elastic=elastic_cfg,
363
+ edr=edr_cfg,
364
+ logging=logging_cfg,
365
+ )
366
+
367
+
368
+ def _ensure_starting_config(config_path: str = CONFIG_FILE, starting_config_path: str = STARTING_CONFIG_FILE) -> None:
369
+ """
370
+ Ensure config.json exists by copying from starting config if needed.
371
+
372
+ Args:
373
+ config_path: Path to the JSON configuration file.
374
+ starting_config_path: Path to the starting/template configuration file.
375
+ """
376
+ config_file = Path(config_path)
377
+ starting_config_file = Path(starting_config_path)
378
+
379
+ # If config.json doesn't exist, but starting config does, copy it
380
+ if not config_file.exists() and starting_config_file.exists():
381
+ try:
382
+ shutil.copy2(starting_config_file, config_file)
383
+ except Exception as e:
384
+ # If copy fails, continue - we'll use defaults
385
+ pass
386
+
387
+
388
+ def load_config_from_file(config_path: str = CONFIG_FILE, env_path: str = ENV_FILE, starting_config_path: str = STARTING_CONFIG_FILE) -> SamiConfig:
389
+ """
390
+ Load configuration from files. Tries .env file first, then JSON file.
391
+
392
+ Priority: .env file > JSON file > starting config > defaults
393
+
394
+ Args:
395
+ config_path: Path to the JSON configuration file.
396
+ env_path: Path to the .env file.
397
+ starting_config_path: Path to the starting/template configuration file.
398
+
399
+ Returns:
400
+ SamiConfig instance.
401
+
402
+ Raises:
403
+ ConfigError: If the files cannot be read or parsed.
404
+ """
405
+ env_file = Path(env_path)
406
+ config_file = Path(config_path)
407
+ starting_config_file = Path(starting_config_path)
408
+
409
+ # Ensure config.json exists by copying from starting config if needed
410
+ _ensure_starting_config(config_path, starting_config_path)
411
+
412
+ # Try .env file first
413
+ if env_file.exists():
414
+ try:
415
+ env_dict = load_config_from_env_file(env_path)
416
+ if env_dict:
417
+ return _env_dict_to_config(env_dict)
418
+ except Exception as e:
419
+ # If .env parsing fails, try JSON
420
+ pass
421
+
422
+ # Fall back to JSON file
423
+ if config_file.exists():
424
+ try:
425
+ with open(config_file, "r") as f:
426
+ data = json.load(f)
427
+ return _dict_to_config(data)
428
+ except json.JSONDecodeError as e:
429
+ raise ConfigError(f"Invalid JSON in config file: {e}") from e
430
+ except Exception as e:
431
+ raise ConfigError(f"Failed to load config file: {e}") from e
432
+
433
+ # Fall back to starting config file
434
+ if starting_config_file.exists():
435
+ try:
436
+ with open(starting_config_file, "r") as f:
437
+ data = json.load(f)
438
+ return _dict_to_config(data)
439
+ except json.JSONDecodeError as e:
440
+ raise ConfigError(f"Invalid JSON in starting config file: {e}") from e
441
+ except Exception as e:
442
+ # If starting config fails, continue to defaults
443
+ pass
444
+
445
+ # Return default config if no files exist
446
+ return SamiConfig(
447
+ thehive=None,
448
+ elastic=None,
449
+ edr=None,
450
+ logging=LoggingConfig(),
451
+ )
452
+
453
+
454
+ def save_config_to_env_file(config: SamiConfig, env_path: str = ENV_FILE) -> None:
455
+ """
456
+ Save configuration to a .env file.
457
+
458
+ Args:
459
+ config: SamiConfig instance to save.
460
+ env_path: Path to the .env file.
461
+
462
+ Raises:
463
+ ConfigError: If the file cannot be written.
464
+ """
465
+ env_file = Path(env_path)
466
+
467
+ try:
468
+ # Create parent directories if needed
469
+ env_file.parent.mkdir(parents=True, exist_ok=True)
470
+
471
+ lines = [
472
+ "# SamiGPT Configuration File",
473
+ "# This file can be edited manually or via the web interface",
474
+ "# Changes are synchronized between this file and config.json",
475
+ "",
476
+ ]
477
+
478
+ # Logging
479
+ lines.append("# Logging Configuration")
480
+ lines.append(f"SAMIGPT_LOG_DIR={config.logging.log_dir}")
481
+ lines.append(f"SAMIGPT_LOG_LEVEL={config.logging.log_level}")
482
+ lines.append("")
483
+
484
+ # TheHive
485
+ if config.thehive:
486
+ lines.append("# TheHive Case Management")
487
+ lines.append(f"SAMIGPT_THEHIVE_URL={config.thehive.base_url}")
488
+ lines.append(f'SAMIGPT_THEHIVE_API_KEY="{config.thehive.api_key}"')
489
+ lines.append(f"SAMIGPT_THEHIVE_TIMEOUT_SECONDS={config.thehive.timeout_seconds}")
490
+ lines.append("")
491
+ else:
492
+ lines.append("# TheHive Case Management (disabled)")
493
+ lines.append("# SAMIGPT_THEHIVE_URL=")
494
+ lines.append("# SAMIGPT_THEHIVE_API_KEY=")
495
+ lines.append("")
496
+
497
+ # IRIS
498
+ if config.iris:
499
+ lines.append("# IRIS Case Management")
500
+ lines.append(f"SAMIGPT_IRIS_URL={config.iris.base_url}")
501
+ lines.append(f'SAMIGPT_IRIS_API_KEY="{config.iris.api_key}"')
502
+ lines.append(f"SAMIGPT_IRIS_TIMEOUT_SECONDS={config.iris.timeout_seconds}")
503
+ lines.append(f"SAMIGPT_IRIS_VERIFY_SSL={'true' if config.iris.verify_ssl else 'false'}")
504
+ lines.append("")
505
+ else:
506
+ lines.append("# IRIS Case Management (disabled)")
507
+ lines.append("# SAMIGPT_IRIS_URL=")
508
+ lines.append("# SAMIGPT_IRIS_API_KEY=")
509
+ lines.append("")
510
+
511
+ # Elastic
512
+ if config.elastic:
513
+ lines.append("# Elastic (SIEM)")
514
+ lines.append(f"SAMIGPT_ELASTIC_URL={config.elastic.base_url}")
515
+ if config.elastic.api_key:
516
+ lines.append(f'SAMIGPT_ELASTIC_API_KEY="{config.elastic.api_key}"')
517
+ if config.elastic.username:
518
+ lines.append(f'SAMIGPT_ELASTIC_USERNAME="{config.elastic.username}"')
519
+ if config.elastic.password:
520
+ lines.append(f'SAMIGPT_ELASTIC_PASSWORD="{config.elastic.password}"')
521
+ lines.append(f"SAMIGPT_ELASTIC_TIMEOUT_SECONDS={config.elastic.timeout_seconds}")
522
+ lines.append(f"SAMIGPT_ELASTIC_VERIFY_SSL={'true' if config.elastic.verify_ssl else 'false'}")
523
+ lines.append("")
524
+ else:
525
+ lines.append("# Elastic (SIEM) (disabled)")
526
+ lines.append("# SAMIGPT_ELASTIC_URL=")
527
+ lines.append("")
528
+
529
+ # EDR
530
+ if config.edr:
531
+ lines.append("# EDR Configuration")
532
+ lines.append(f"SAMIGPT_EDR_TYPE={config.edr.edr_type}")
533
+ lines.append(f"SAMIGPT_EDR_URL={config.edr.base_url}")
534
+ lines.append(f'SAMIGPT_EDR_API_KEY="{config.edr.api_key}"')
535
+ lines.append(f"SAMIGPT_EDR_TIMEOUT_SECONDS={config.edr.timeout_seconds}")
536
+ lines.append(f"SAMIGPT_EDR_VERIFY_SSL={'true' if config.edr.verify_ssl else 'false'}")
537
+ lines.append("")
538
+ else:
539
+ lines.append("# EDR Configuration (disabled)")
540
+ lines.append("# SAMIGPT_EDR_URL=")
541
+ lines.append("# SAMIGPT_EDR_API_KEY=")
542
+ lines.append("")
543
+
544
+ with open(env_file, "w") as f:
545
+ f.write("\n".join(lines))
546
+ except Exception as e:
547
+ raise ConfigError(f"Failed to save .env file: {e}") from e
548
+
549
+
550
+ def save_config_to_file(
551
+ config: SamiConfig, config_path: str = CONFIG_FILE, env_path: str = ENV_FILE, save_both: bool = True
552
+ ) -> None:
553
+ """
554
+ Save configuration to both JSON and .env files for synchronization.
555
+
556
+ Args:
557
+ config: SamiConfig instance to save.
558
+ config_path: Path to the JSON configuration file.
559
+ env_path: Path to the .env file.
560
+ save_both: If True, save to both JSON and .env. If False, only save to JSON.
561
+
562
+ Raises:
563
+ ConfigError: If the files cannot be written.
564
+ """
565
+ config_file = Path(config_path)
566
+
567
+ try:
568
+ # Create parent directories if needed
569
+ config_file.parent.mkdir(parents=True, exist_ok=True)
570
+
571
+ data = _config_to_dict(config)
572
+ with open(config_file, "w") as f:
573
+ json.dump(data, f, indent=2)
574
+
575
+ # Also save to .env file for manual editing
576
+ if save_both:
577
+ save_config_to_env_file(config, env_path)
578
+ except Exception as e:
579
+ raise ConfigError(f"Failed to save config file: {e}") from e
580
+
581
+
582
+ def get_config_dict(config_path: str = CONFIG_FILE, env_path: str = ENV_FILE) -> Dict[str, Any]:
583
+ """
584
+ Get configuration as a dictionary (for API responses).
585
+
586
+ Args:
587
+ config_path: Path to the JSON configuration file.
588
+ env_path: Path to the .env file.
589
+
590
+ Returns:
591
+ Dictionary representation of the configuration.
592
+ """
593
+ config = load_config_from_file(config_path, env_path)
594
+ return _config_to_dict(config)
595
+
596
+
597
+ def update_config_dict(
598
+ updates: Dict[str, Any], config_path: str = CONFIG_FILE, env_path: str = ENV_FILE, save_both: bool = True
599
+ ) -> SamiConfig:
600
+ """
601
+ Update configuration with new values and save to both JSON and .env files.
602
+
603
+ Args:
604
+ updates: Dictionary with configuration updates.
605
+ config_path: Path to the JSON configuration file.
606
+ env_path: Path to the .env file.
607
+ save_both: If True, save to both JSON and .env. If False, only save to JSON.
608
+
609
+ Returns:
610
+ Updated SamiConfig instance.
611
+
612
+ Raises:
613
+ ConfigError: If the update fails.
614
+ """
615
+ # Load existing config (checks both .env and JSON)
616
+ config = load_config_from_file(config_path, env_path)
617
+
618
+ # Update logging
619
+ if "logging" in updates:
620
+ logging_updates = updates["logging"]
621
+ if "log_dir" in logging_updates:
622
+ config.logging.log_dir = logging_updates["log_dir"]
623
+ if "log_level" in logging_updates:
624
+ config.logging.log_level = logging_updates["log_level"]
625
+
626
+ # Update TheHive
627
+ if "thehive" in updates:
628
+ th_updates = updates["thehive"]
629
+ if th_updates is None:
630
+ config.thehive = None
631
+ elif th_updates.get("base_url") and th_updates.get("api_key"):
632
+ config.thehive = TheHiveConfig(
633
+ base_url=th_updates["base_url"],
634
+ api_key=th_updates["api_key"],
635
+ timeout_seconds=th_updates.get("timeout_seconds", 30),
636
+ )
637
+
638
+ # Update IRIS
639
+ if "iris" in updates:
640
+ iris_updates = updates["iris"]
641
+ if iris_updates is None:
642
+ config.iris = None
643
+ elif iris_updates.get("base_url") and iris_updates.get("api_key"):
644
+ config.iris = IrisConfig(
645
+ base_url=iris_updates["base_url"],
646
+ api_key=iris_updates["api_key"],
647
+ timeout_seconds=iris_updates.get("timeout_seconds", 30),
648
+ verify_ssl=iris_updates.get("verify_ssl", True),
649
+ )
650
+
651
+ # Update Elastic
652
+ if "elastic" in updates:
653
+ el_updates = updates["elastic"]
654
+ if el_updates is None:
655
+ config.elastic = None
656
+ elif el_updates.get("base_url"):
657
+ config.elastic = ElasticConfig(
658
+ base_url=el_updates["base_url"],
659
+ api_key=el_updates.get("api_key"),
660
+ username=el_updates.get("username"),
661
+ password=el_updates.get("password"),
662
+ timeout_seconds=el_updates.get("timeout_seconds", 30),
663
+ verify_ssl=el_updates.get("verify_ssl", True),
664
+ )
665
+
666
+ # Update EDR
667
+ if "edr" in updates:
668
+ edr_updates = updates["edr"]
669
+ if edr_updates is None:
670
+ config.edr = None
671
+ elif edr_updates.get("base_url") and edr_updates.get("api_key"):
672
+ config.edr = EDRConfig(
673
+ edr_type=edr_updates.get("edr_type", "velociraptor"),
674
+ base_url=edr_updates["base_url"],
675
+ api_key=edr_updates["api_key"],
676
+ timeout_seconds=edr_updates.get("timeout_seconds", 30),
677
+ verify_ssl=edr_updates.get("verify_ssl", True),
678
+ additional_params=edr_updates.get("additional_params"),
679
+ )
680
+
681
+ # Save updated config to both files
682
+ save_config_to_file(config, config_path, env_path, save_both=save_both)
683
+ return config
684
+