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,1709 @@
1
+ """
2
+ LLM-callable tools for SIEM operations.
3
+
4
+ These functions wrap the generic SIEMClient interface and provide
5
+ LLM-friendly error handling and return values.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Dict, Optional
11
+
12
+ from ..api.siem import SIEMClient
13
+ from ..core.errors import IntegrationError
14
+
15
+
16
+ def search_security_events(
17
+ query: str,
18
+ limit: int = 100,
19
+ client: SIEMClient = None, # type: ignore
20
+ ) -> Dict[str, Any]:
21
+ """
22
+ Search security events across all environments.
23
+
24
+ Tool schema:
25
+ - name: search_security_events
26
+ - description: Search security events and logs across all environments
27
+ using a query string. Returns matching events with details.
28
+ - parameters:
29
+ - query (str, required): Search query (vendor-specific query language).
30
+ - limit (int, optional): Maximum number of events to return (default: 100).
31
+
32
+ Args:
33
+ query: Search query string.
34
+ limit: Maximum number of events to return.
35
+ client: The SIEM client.
36
+
37
+ Returns:
38
+ Dictionary containing search results with events.
39
+
40
+ Raises:
41
+ IntegrationError: If search fails.
42
+ """
43
+ if client is None:
44
+ raise IntegrationError("SIEM client not provided")
45
+
46
+ try:
47
+ result = client.search_security_events(query=query, limit=limit)
48
+
49
+ return {
50
+ "success": True,
51
+ "query": result.query,
52
+ "total_count": result.total_count,
53
+ "returned_count": len(result.events),
54
+ "events": [
55
+ {
56
+ "id": event.id,
57
+ "timestamp": event.timestamp.isoformat(),
58
+ "source_type": event.source_type.value,
59
+ "message": event.message,
60
+ "host": event.host,
61
+ "username": event.username,
62
+ "ip": event.ip,
63
+ "process_name": event.process_name,
64
+ "file_hash": event.file_hash,
65
+ }
66
+ for event in result.events
67
+ ],
68
+ }
69
+ except Exception as e:
70
+ raise IntegrationError(f"Failed to search security events: {str(e)}") from e
71
+
72
+
73
+ def get_file_report(
74
+ file_hash: str,
75
+ client: SIEMClient = None, # type: ignore
76
+ ) -> Dict[str, Any]:
77
+ """
78
+ Get a report about a file by its hash.
79
+
80
+ Tool schema:
81
+ - name: get_file_report
82
+ - description: Retrieve an aggregated report about a file identified by
83
+ its hash, including when it was first/last seen, detection count, and
84
+ affected hosts.
85
+ - parameters:
86
+ - file_hash (str, required): The file hash (MD5, SHA256, etc.).
87
+
88
+ Args:
89
+ file_hash: The file hash.
90
+ client: The SIEM client.
91
+
92
+ Returns:
93
+ Dictionary containing file report details.
94
+
95
+ Raises:
96
+ IntegrationError: If retrieving report fails.
97
+ """
98
+ if client is None:
99
+ raise IntegrationError("SIEM client not provided")
100
+
101
+ try:
102
+ report = client.get_file_report(file_hash)
103
+
104
+ return {
105
+ "success": True,
106
+ "file_hash": report.file_hash,
107
+ "first_seen": report.first_seen.isoformat() if report.first_seen else None,
108
+ "last_seen": report.last_seen.isoformat() if report.last_seen else None,
109
+ "detection_count": report.detection_count,
110
+ "affected_hosts": report.affected_hosts or [],
111
+ }
112
+ except Exception as e:
113
+ raise IntegrationError(f"Failed to get file report for {file_hash}: {str(e)}") from e
114
+
115
+
116
+ def get_file_behavior_summary(
117
+ file_hash: str,
118
+ client: SIEMClient = None, # type: ignore
119
+ ) -> Dict[str, Any]:
120
+ """
121
+ Get a behavior summary for a file.
122
+
123
+ Tool schema:
124
+ - name: get_file_behavior_summary
125
+ - description: Retrieve a high-level behavior summary for a file,
126
+ including process trees, network activity, and persistence mechanisms.
127
+ - parameters:
128
+ - file_hash (str, required): The file hash.
129
+
130
+ Args:
131
+ file_hash: The file hash.
132
+ client: The SIEM client.
133
+
134
+ Returns:
135
+ Dictionary containing behavior summary.
136
+
137
+ Raises:
138
+ IntegrationError: If retrieving summary fails.
139
+ """
140
+ if client is None:
141
+ raise IntegrationError("SIEM client not provided")
142
+
143
+ try:
144
+ summary = client.get_file_behavior_summary(file_hash)
145
+
146
+ return {
147
+ "success": True,
148
+ "file_hash": summary.file_hash,
149
+ "process_trees": summary.process_trees or [],
150
+ "network_activity": summary.network_activity or [],
151
+ "persistence_mechanisms": summary.persistence_mechanisms or [],
152
+ "notes": summary.notes,
153
+ }
154
+ except Exception as e:
155
+ raise IntegrationError(
156
+ f"Failed to get file behavior summary for {file_hash}: {str(e)}"
157
+ ) from e
158
+
159
+
160
+ def get_entities_related_to_file(
161
+ file_hash: str,
162
+ client: SIEMClient = None, # type: ignore
163
+ ) -> Dict[str, Any]:
164
+ """
165
+ Get entities related to a file.
166
+
167
+ Tool schema:
168
+ - name: get_entities_related_to_file
169
+ - description: Retrieve entities related to a file hash, such as hosts
170
+ where it was seen, users who executed it, related processes, and alerts.
171
+ - parameters:
172
+ - file_hash (str, required): The file hash.
173
+
174
+ Args:
175
+ file_hash: The file hash.
176
+ client: The SIEM client.
177
+
178
+ Returns:
179
+ Dictionary containing related entities.
180
+
181
+ Raises:
182
+ IntegrationError: If retrieving entities fails.
183
+ """
184
+ if client is None:
185
+ raise IntegrationError("SIEM client not provided")
186
+
187
+ try:
188
+ entities = client.get_entities_related_to_file(file_hash)
189
+
190
+ return {
191
+ "success": True,
192
+ "indicator": entities.indicator,
193
+ "hosts": entities.hosts or [],
194
+ "users": entities.users or [],
195
+ "processes": entities.processes or [],
196
+ "alerts": entities.alerts or [],
197
+ }
198
+ except Exception as e:
199
+ raise IntegrationError(
200
+ f"Failed to get entities related to file {file_hash}: {str(e)}"
201
+ ) from e
202
+
203
+
204
+ def get_ip_address_report(
205
+ ip: str,
206
+ client: SIEMClient = None, # type: ignore
207
+ ) -> Dict[str, Any]:
208
+ """
209
+ Get a report about an IP address.
210
+
211
+ Tool schema:
212
+ - name: get_ip_address_report
213
+ - description: Retrieve an aggregated report about an IP address,
214
+ including reputation, geolocation, and related alerts.
215
+ - parameters:
216
+ - ip (str, required): The IP address.
217
+
218
+ Args:
219
+ ip: The IP address.
220
+ client: The SIEM client.
221
+
222
+ Returns:
223
+ Dictionary containing IP report details.
224
+
225
+ Raises:
226
+ IntegrationError: If retrieving report fails.
227
+ """
228
+ if client is None:
229
+ raise IntegrationError("SIEM client not provided")
230
+
231
+ try:
232
+ report = client.get_ip_address_report(ip)
233
+
234
+ return {
235
+ "success": True,
236
+ "ip": report.ip,
237
+ "reputation": report.reputation,
238
+ "geo": report.geo or {},
239
+ "related_alerts": report.related_alerts or [],
240
+ }
241
+ except Exception as e:
242
+ raise IntegrationError(f"Failed to get IP address report for {ip}: {str(e)}") from e
243
+
244
+
245
+ def search_user_activity(
246
+ username: str,
247
+ limit: int = 100,
248
+ client: SIEMClient = None, # type: ignore
249
+ ) -> Dict[str, Any]:
250
+ """
251
+ Search for user activity in security logs.
252
+
253
+ Tool schema:
254
+ - name: search_user_activity
255
+ - description: Search for security events related to a specific user,
256
+ including authentication events, file access, and other activities.
257
+ - parameters:
258
+ - username (str, required): The username to search for.
259
+ - limit (int, optional): Maximum number of events to return (default: 100).
260
+
261
+ Args:
262
+ username: The username.
263
+ limit: Maximum number of events.
264
+ client: The SIEM client.
265
+
266
+ Returns:
267
+ Dictionary containing user activity events.
268
+
269
+ Raises:
270
+ IntegrationError: If search fails.
271
+ """
272
+ if client is None:
273
+ raise IntegrationError("SIEM client not provided")
274
+
275
+ try:
276
+ result = client.search_user_activity(username=username, limit=limit)
277
+
278
+ return {
279
+ "success": True,
280
+ "username": username,
281
+ "total_count": result.total_count,
282
+ "returned_count": len(result.events),
283
+ "events": [
284
+ {
285
+ "id": event.id,
286
+ "timestamp": event.timestamp.isoformat(),
287
+ "source_type": event.source_type.value,
288
+ "message": event.message,
289
+ "host": event.host,
290
+ "username": event.username,
291
+ "ip": event.ip,
292
+ "process_name": event.process_name,
293
+ "file_hash": event.file_hash,
294
+ }
295
+ for event in result.events
296
+ ],
297
+ }
298
+ except Exception as e:
299
+ raise IntegrationError(f"Failed to search user activity for {username}: {str(e)}") from e
300
+
301
+
302
+ def pivot_on_indicator(
303
+ indicator: str,
304
+ limit: int = 200,
305
+ client: SIEMClient = None, # type: ignore
306
+ ) -> Dict[str, Any]:
307
+ """
308
+ Pivot on an indicator of compromise (IOC).
309
+
310
+ Tool schema:
311
+ - name: pivot_on_indicator
312
+ - description: Given an IOC (file hash, IP address, domain, etc.),
313
+ search for all related security events across environments for
314
+ further investigation.
315
+ - parameters:
316
+ - indicator (str, required): The IOC (hash, IP, domain, etc.).
317
+ - limit (int, optional): Maximum number of events to return (default: 200).
318
+
319
+ Args:
320
+ indicator: The IOC value.
321
+ limit: Maximum number of events.
322
+ client: The SIEM client.
323
+
324
+ Returns:
325
+ Dictionary containing related events.
326
+
327
+ Raises:
328
+ IntegrationError: If pivot search fails.
329
+ """
330
+ if client is None:
331
+ raise IntegrationError("SIEM client not provided")
332
+
333
+ try:
334
+ result = client.pivot_on_indicator(indicator=indicator, limit=limit)
335
+
336
+ return {
337
+ "success": True,
338
+ "indicator": indicator,
339
+ "query": result.query,
340
+ "total_count": result.total_count,
341
+ "returned_count": len(result.events),
342
+ "events": [
343
+ {
344
+ "id": event.id,
345
+ "timestamp": event.timestamp.isoformat(),
346
+ "source_type": event.source_type.value,
347
+ "message": event.message,
348
+ "host": event.host,
349
+ "username": event.username,
350
+ "ip": event.ip,
351
+ "process_name": event.process_name,
352
+ "file_hash": event.file_hash,
353
+ }
354
+ for event in result.events
355
+ ],
356
+ }
357
+ except Exception as e:
358
+ raise IntegrationError(f"Failed to pivot on indicator {indicator}: {str(e)}") from e
359
+
360
+
361
+ def get_recent_alerts(
362
+ hours_back: int = 1,
363
+ max_alerts: int = 100,
364
+ status_filter: Optional[str] = None,
365
+ severity: Optional[str] = None,
366
+ hostname: Optional[str] = None,
367
+ client: SIEMClient = None, # type: ignore
368
+ ) -> Dict[str, Any]:
369
+ """
370
+ Summarize and smart-group recent alerts from the SIEM.
371
+
372
+ Tool schema:
373
+ - name: get_recent_alerts
374
+ - description: Get recent SIEM alerts (last N hours) and group similar
375
+ alerts together to help the AI decide what to investigate first.
376
+ **CRITICAL: Automatically excludes alerts that have already been investigated**
377
+ (alerts with signal.ai.verdict field) to prevent duplicate work. SOC1 should
378
+ never re-investigate alerts that have already been triaged.
379
+ - parameters:
380
+ - hours_back (int, optional): How many hours to look back (default: 1)
381
+ - max_alerts (int, optional): Maximum number of alerts to retrieve (default: 100)
382
+ - status_filter (str, optional): Filter by status (implementation-specific)
383
+ - severity (str, optional): Filter by severity (low, medium, high, critical)
384
+ - hostname (str, optional): Filter alerts by hostname (matches host.name field)
385
+
386
+ **Important:** This tool automatically filters out alerts that have a `verdict` field
387
+ (signal.ai.verdict in Elasticsearch). Alerts with verdicts have already been investigated
388
+ and should not be re-triaged by SOC1. This prevents duplicate work and ensures SOC1 only
389
+ processes new, uninvestigated alerts.
390
+
391
+ The tool groups alerts by a composite of title, severity, status, rule ID,
392
+ and alert type/category. For each group it returns:
393
+ - a stable group_id
394
+ - title and primary_severity
395
+ - count of alerts in the group
396
+ - list of alert_ids in the group
397
+ - statuses and severities seen in the group
398
+ - earliest_created_at and latest_created_at
399
+ - up to 3 example_alerts with key fields (id, title, severity, status, timestamps).
400
+ """
401
+ if client is None:
402
+ raise IntegrationError("SIEM client not provided")
403
+
404
+ if not hasattr(client, "get_security_alerts"):
405
+ raise IntegrationError("SIEM client does not support get_security_alerts")
406
+
407
+ try:
408
+ alerts = client.get_security_alerts(
409
+ hours_back=hours_back,
410
+ max_alerts=max_alerts,
411
+ status_filter=status_filter,
412
+ severity=severity,
413
+ hostname=hostname,
414
+ )
415
+ except Exception as e:
416
+ raise IntegrationError(f"Failed to get recent alerts: {str(e)}") from e
417
+
418
+ if not isinstance(alerts, list):
419
+ raise IntegrationError(
420
+ "SIEM client get_security_alerts returned unexpected type "
421
+ f"{type(alerts).__name__}, expected list"
422
+ )
423
+
424
+ def _severity_rank(value: Optional[str]) -> int:
425
+ mapping = {"critical": 4, "high": 3, "medium": 2, "low": 1}
426
+ if value is None:
427
+ return 0
428
+ return mapping.get(str(value).lower(), 0)
429
+
430
+ # First, filter out investigated alerts and limit to max_alerts uninvestigated alerts.
431
+ # While doing this, also track the single oldest uninvestigated alert so we can
432
+ # expose it as a suggested starting point for triage.
433
+ uninvestigated_alerts = []
434
+ suggested_alert = None
435
+ suggested_alert_created_at = None
436
+
437
+ for alert in alerts:
438
+ if not isinstance(alert, dict):
439
+ continue
440
+
441
+ # CRITICAL: Skip alerts that have already been investigated (have verdict field)
442
+ # The verdict field comes from signal.ai.verdict in Elasticsearch and indicates the alert has already been triaged
443
+ # SOC1 should never re-investigate alerts that have already been processed
444
+ # Check both direct verdict field and signal.ai.verdict path for compatibility
445
+ verdict = alert.get("verdict") or alert.get("signal", {}).get("ai", {}).get("verdict")
446
+ if verdict:
447
+ continue # Skip this alert - it has already been investigated (has signal.ai.verdict)
448
+
449
+ # Limit to max_alerts uninvestigated alerts
450
+ if len(uninvestigated_alerts) >= max_alerts:
451
+ break
452
+
453
+ uninvestigated_alerts.append(alert)
454
+
455
+ # Track the oldest uninvestigated alert based on created_at / @timestamp
456
+ created_at_value = alert.get("created_at") or alert.get("@timestamp")
457
+ if created_at_value is not None and (
458
+ suggested_alert_created_at is None or created_at_value < suggested_alert_created_at
459
+ ):
460
+ suggested_alert = alert
461
+ suggested_alert_created_at = created_at_value
462
+
463
+ # If no uninvestigated alerts, return early with a clear message
464
+ if not uninvestigated_alerts:
465
+ return {
466
+ "success": True,
467
+ "hours_back": hours_back,
468
+ "max_alerts": max_alerts,
469
+ "status_filter": status_filter,
470
+ "severity": severity,
471
+ "hostname": hostname,
472
+ "total_alerts": len(alerts),
473
+ "uninvestigated_alerts": 0,
474
+ "group_count": 0,
475
+ "groups": [],
476
+ "message": "No recent security alerts to investigate. All alerts in the specified timeframe have already been investigated (have verdict field).",
477
+ "suggested_alert_to_triage": None,
478
+ }
479
+
480
+ groups: Dict[str, Dict[str, Any]] = {}
481
+
482
+ for alert in uninvestigated_alerts:
483
+ alert_id = alert.get("id")
484
+
485
+ # Skip alerts without a title or name
486
+ title = alert.get("title") or alert.get("name")
487
+ if not title or not title.strip():
488
+ continue
489
+
490
+ severity_value = (alert.get("severity") or "unknown").lower()
491
+ status_value = (alert.get("status") or "unknown").lower()
492
+
493
+ rule_id = alert.get("rule_id") or alert.get("detection_rule_id")
494
+ rule = alert.get("rule")
495
+ if isinstance(rule, dict):
496
+ rule_id = rule_id or rule.get("id")
497
+
498
+ alert_type = (
499
+ alert.get("type")
500
+ or alert.get("category")
501
+ or (rule.get("name") if isinstance(rule, dict) else None)
502
+ )
503
+
504
+ key_parts = [
505
+ title.strip().lower(),
506
+ severity_value,
507
+ status_value,
508
+ ]
509
+ if rule_id:
510
+ key_parts.append(f"rule:{rule_id}")
511
+ if alert_type:
512
+ key_parts.append(f"type:{str(alert_type).lower()}")
513
+
514
+ group_key = "|".join(key_parts)
515
+ group = groups.get(group_key)
516
+ if group is None:
517
+ group = {
518
+ "group_key": group_key,
519
+ "title": title,
520
+ "primary_severity": severity_value,
521
+ "primary_status": status_value,
522
+ "rule_id": rule_id,
523
+ "alert_type": alert_type,
524
+ "count": 0,
525
+ "alert_ids": [],
526
+ "statuses": set(),
527
+ "severities": set(),
528
+ "earliest_created_at": None,
529
+ "latest_created_at": None,
530
+ "example_alerts": [],
531
+ }
532
+ groups[group_key] = group
533
+
534
+ group["count"] += 1
535
+ if alert_id is not None:
536
+ group["alert_ids"].append(alert_id)
537
+
538
+ group["statuses"].add(status_value)
539
+ group["severities"].add(severity_value)
540
+
541
+ created_at = alert.get("created_at") or alert.get("@timestamp")
542
+ if created_at is not None:
543
+ earliest = group["earliest_created_at"]
544
+ latest = group["latest_created_at"]
545
+ if earliest is None or created_at < earliest:
546
+ group["earliest_created_at"] = created_at
547
+ if latest is None or created_at > latest:
548
+ group["latest_created_at"] = created_at
549
+
550
+ if _severity_rank(severity_value) > _severity_rank(group.get("primary_severity")):
551
+ group["primary_severity"] = severity_value
552
+
553
+ examples = group["example_alerts"]
554
+ if len(examples) < 3:
555
+ examples.append(
556
+ {
557
+ "id": alert_id,
558
+ "title": title,
559
+ "severity": severity_value,
560
+ "status": status_value,
561
+ "created_at": created_at,
562
+ "source": alert.get("source"),
563
+ "rule_id": rule_id,
564
+ "type": alert_type,
565
+ "description": alert.get("description"),
566
+ }
567
+ )
568
+
569
+ grouped_list = []
570
+ for idx, group in enumerate(groups.values(), start=1):
571
+ # Sort example_alerts within each group from oldest to most recent
572
+ example_alerts_sorted = sorted(
573
+ group["example_alerts"],
574
+ key=lambda a: a.get("created_at") or "",
575
+ )
576
+
577
+ grouped_list.append(
578
+ {
579
+ "group_id": f"alert_group_{idx}",
580
+ "title": group["title"],
581
+ "primary_severity": group["primary_severity"],
582
+ "primary_status": group["primary_status"],
583
+ "rule_id": group["rule_id"],
584
+ "alert_type": group["alert_type"],
585
+ "count": group["count"],
586
+ "alert_ids": group["alert_ids"],
587
+ "statuses": sorted(s for s in group["statuses"] if s),
588
+ "severities": sorted(s for s in group["severities"] if s),
589
+ "earliest_created_at": group["earliest_created_at"],
590
+ "latest_created_at": group["latest_created_at"],
591
+ "example_alerts": example_alerts_sorted,
592
+ }
593
+ )
594
+
595
+ # Sort groups from oldest to most recent based on their earliest_created_at.
596
+ # If timestamps are missing, they will naturally fall to the start of the list.
597
+ grouped_list.sort(
598
+ key=lambda g: g.get("earliest_created_at") or "",
599
+ )
600
+
601
+ # Build a compact "suggested alert to triage" view based on the oldest uninvestigated alert
602
+ suggested_alert_view = None
603
+ if suggested_alert is not None:
604
+ suggested_title = suggested_alert.get("title") or suggested_alert.get("name")
605
+ suggested_severity = (suggested_alert.get("severity") or "unknown").lower()
606
+ suggested_status = (suggested_alert.get("status") or "unknown").lower()
607
+ suggested_rule_id = (
608
+ suggested_alert.get("rule_id")
609
+ or suggested_alert.get("detection_rule_id")
610
+ )
611
+ suggested_alert_view = {
612
+ "id": suggested_alert.get("id"),
613
+ "title": suggested_title,
614
+ "severity": suggested_severity,
615
+ "status": suggested_status,
616
+ "created_at": suggested_alert_created_at,
617
+ "rule_id": suggested_rule_id,
618
+ "source": suggested_alert.get("source"),
619
+ "type": suggested_alert.get("type")
620
+ or suggested_alert.get("category"),
621
+ "description": suggested_alert.get("description"),
622
+ }
623
+
624
+ return {
625
+ "success": True,
626
+ "hours_back": hours_back,
627
+ "max_alerts": max_alerts,
628
+ "status_filter": status_filter,
629
+ "severity": severity,
630
+ "hostname": hostname,
631
+ "total_alerts": len(alerts),
632
+ "uninvestigated_alerts": len(uninvestigated_alerts),
633
+ "group_count": len(grouped_list),
634
+ "groups": grouped_list,
635
+ "suggested_alert_to_triage": suggested_alert_view,
636
+ }
637
+
638
+
639
+ def get_security_alerts(
640
+ hours_back: int = 24,
641
+ max_alerts: int = 10,
642
+ status_filter: Optional[str] = None,
643
+ severity: Optional[str] = None,
644
+ client: SIEMClient = None, # type: ignore
645
+ ) -> Dict[str, Any]:
646
+ """
647
+ Get security alerts from the SIEM platform.
648
+
649
+ Tool schema:
650
+ - name: get_security_alerts
651
+ - description: Get security alerts directly from the SIEM platform
652
+ - parameters:
653
+ - hours_back (int, optional): How many hours to look back (default: 24)
654
+ - max_alerts (int, optional): Maximum number of alerts to return (default: 10)
655
+ - status_filter (str, optional): Filter by status
656
+ - severity (str, optional): Filter by severity (low, medium, high, critical)
657
+ """
658
+ if client is None:
659
+ raise IntegrationError("SIEM client not provided")
660
+
661
+ # Check if client has this method
662
+ if not hasattr(client, "get_security_alerts"):
663
+ raise IntegrationError("SIEM client does not support get_security_alerts")
664
+
665
+ try:
666
+ alerts = client.get_security_alerts(
667
+ hours_back=hours_back,
668
+ max_alerts=max_alerts,
669
+ status_filter=status_filter,
670
+ severity=severity,
671
+ )
672
+
673
+ return {
674
+ "success": True,
675
+ "count": len(alerts),
676
+ "alerts": alerts,
677
+ }
678
+ except Exception as e:
679
+ raise IntegrationError(f"Failed to get security alerts: {str(e)}") from e
680
+
681
+
682
+ def get_security_alert_by_id(
683
+ alert_id: str,
684
+ include_detections: bool = True,
685
+ client: SIEMClient = None, # type: ignore
686
+ ) -> Dict[str, Any]:
687
+ """
688
+ Get detailed information about a specific security alert.
689
+
690
+ Tool schema:
691
+ - name: get_security_alert_by_id
692
+ - description: Get detailed information about a specific security alert by its ID
693
+ - parameters:
694
+ - alert_id (str, required): The ID of the alert
695
+ - include_detections (bool, optional): Whether to include detection details (default: true)
696
+ """
697
+ if client is None:
698
+ raise IntegrationError("SIEM client not provided")
699
+
700
+ if not hasattr(client, "get_security_alert_by_id"):
701
+ raise IntegrationError("SIEM client does not support get_security_alert_by_id")
702
+
703
+ try:
704
+ alert = client.get_security_alert_by_id(
705
+ alert_id=alert_id,
706
+ include_detections=include_detections,
707
+ )
708
+
709
+ return {
710
+ "success": True,
711
+ "alert": alert,
712
+ }
713
+ except Exception as e:
714
+ raise IntegrationError(f"Failed to get security alert: {str(e)}") from e
715
+
716
+
717
+ def get_siem_event_by_id(
718
+ event_id: str,
719
+ client: SIEMClient = None, # type: ignore
720
+ ) -> Dict[str, Any]:
721
+ """
722
+ Get detailed information about a specific security event by its ID.
723
+
724
+ Tool schema:
725
+ - name: get_siem_event_by_id
726
+ - description: Retrieve a specific security event by its unique identifier (event ID).
727
+ This tool allows you to get the exact event details when you know the event ID.
728
+ - parameters:
729
+ - event_id (str, required): The unique identifier of the event to retrieve
730
+ """
731
+ if client is None:
732
+ raise IntegrationError("SIEM client not provided")
733
+
734
+ if not hasattr(client, "get_siem_event_by_id"):
735
+ raise IntegrationError("SIEM client does not support get_siem_event_by_id")
736
+
737
+ try:
738
+ event = client.get_siem_event_by_id(event_id=event_id)
739
+
740
+ return {
741
+ "success": True,
742
+ "event": {
743
+ "id": event.id,
744
+ "timestamp": event.timestamp.isoformat(),
745
+ "source_type": event.source_type.value,
746
+ "message": event.message,
747
+ "host": event.host,
748
+ "username": event.username,
749
+ "ip": event.ip,
750
+ "process_name": event.process_name,
751
+ "file_hash": event.file_hash,
752
+ "raw": event.raw,
753
+ },
754
+ }
755
+ except Exception as e:
756
+ raise IntegrationError(f"Failed to get event by ID: {str(e)}") from e
757
+
758
+
759
+ def lookup_entity(
760
+ entity_value: str,
761
+ entity_type: Optional[str] = None,
762
+ hours_back: int = 24,
763
+ client: SIEMClient = None, # type: ignore
764
+ ) -> Dict[str, Any]:
765
+ """
766
+ Look up an entity for enrichment.
767
+
768
+ Tool schema:
769
+ - name: lookup_entity
770
+ - description: Look up an entity (IP address, domain, hash, user, etc.) in the SIEM for enrichment
771
+ - parameters:
772
+ - entity_value (str, required): Value to look up
773
+ - entity_type (str, optional): Type of entity (ip, domain, hash, user, etc.)
774
+ - hours_back (int, optional): How many hours of historical data (default: 24)
775
+ """
776
+ if client is None:
777
+ raise IntegrationError("SIEM client not provided")
778
+
779
+ if not hasattr(client, "lookup_entity"):
780
+ raise IntegrationError("SIEM client does not support lookup_entity")
781
+
782
+ try:
783
+ result = client.lookup_entity(
784
+ entity_value=entity_value,
785
+ entity_type=entity_type,
786
+ hours_back=hours_back,
787
+ )
788
+
789
+ return {
790
+ "success": True,
791
+ **result,
792
+ }
793
+ except Exception as e:
794
+ raise IntegrationError(f"Failed to lookup entity: {str(e)}") from e
795
+
796
+
797
+ def get_ioc_matches(
798
+ hours_back: int = 24,
799
+ max_matches: int = 20,
800
+ ioc_type: Optional[str] = None,
801
+ severity: Optional[str] = None,
802
+ client: SIEMClient = None, # type: ignore
803
+ ) -> Dict[str, Any]:
804
+ """
805
+ Get Indicators of Compromise (IoC) matches.
806
+
807
+ Tool schema:
808
+ - name: get_ioc_matches
809
+ - description: Get Indicators of Compromise (IoC) matches from the SIEM
810
+ - parameters:
811
+ - hours_back (int, optional): How many hours back to look (default: 24)
812
+ - max_matches (int, optional): Maximum number of matches (default: 20)
813
+ - ioc_type (str, optional): Filter by IoC type (ip, domain, hash, url, etc.)
814
+ - severity (str, optional): Filter by severity level
815
+ """
816
+ if client is None:
817
+ raise IntegrationError("SIEM client not provided")
818
+
819
+ if not hasattr(client, "get_ioc_matches"):
820
+ raise IntegrationError("SIEM client does not support get_ioc_matches")
821
+
822
+ try:
823
+ matches = client.get_ioc_matches(
824
+ hours_back=hours_back,
825
+ max_matches=max_matches,
826
+ ioc_type=ioc_type,
827
+ severity=severity,
828
+ )
829
+
830
+ return {
831
+ "success": True,
832
+ "count": len(matches),
833
+ "matches": matches,
834
+ }
835
+ except Exception as e:
836
+ raise IntegrationError(f"Failed to get IoC matches: {str(e)}") from e
837
+
838
+
839
+ def get_threat_intel(
840
+ query: str,
841
+ context: Optional[Dict[str, Any]] = None,
842
+ client: SIEMClient = None, # type: ignore
843
+ ) -> Dict[str, Any]:
844
+ """
845
+ Get threat intelligence answers.
846
+
847
+ Tool schema:
848
+ - name: get_threat_intel
849
+ - description: Get answers to security questions using integrated threat intelligence
850
+ - parameters:
851
+ - query (str, required): The security or threat intelligence question
852
+ - context (object, optional): Additional context (indicators, events, etc.)
853
+ """
854
+ if client is None:
855
+ raise IntegrationError("SIEM client not provided")
856
+
857
+ if not hasattr(client, "get_threat_intel"):
858
+ raise IntegrationError("SIEM client does not support get_threat_intel")
859
+
860
+ try:
861
+ result = client.get_threat_intel(
862
+ query=query,
863
+ context=context,
864
+ )
865
+
866
+ return {
867
+ "success": True,
868
+ **result,
869
+ }
870
+ except Exception as e:
871
+ raise IntegrationError(f"Failed to get threat intelligence: {str(e)}") from e
872
+
873
+
874
+ def list_security_rules(
875
+ enabled_only: bool = False,
876
+ limit: int = 100,
877
+ client: SIEMClient = None, # type: ignore
878
+ ) -> Dict[str, Any]:
879
+ """
880
+ List security detection rules.
881
+
882
+ Tool schema:
883
+ - name: list_security_rules
884
+ - description: List all security detection rules configured in the SIEM platform
885
+ - parameters:
886
+ - enabled_only (bool, optional): Only return enabled rules (default: false)
887
+ - limit (int, optional): Maximum number of rules (default: 100)
888
+ """
889
+ if client is None:
890
+ raise IntegrationError("SIEM client not provided")
891
+
892
+ if not hasattr(client, "list_security_rules"):
893
+ raise IntegrationError("SIEM client does not support list_security_rules")
894
+
895
+ try:
896
+ rules = client.list_security_rules(
897
+ enabled_only=enabled_only,
898
+ limit=limit,
899
+ )
900
+
901
+ return {
902
+ "success": True,
903
+ "count": len(rules),
904
+ "rules": rules,
905
+ }
906
+ except Exception as e:
907
+ raise IntegrationError(f"Failed to list security rules: {str(e)}") from e
908
+
909
+
910
+ def search_security_rules(
911
+ query: str,
912
+ category: Optional[str] = None,
913
+ enabled_only: bool = False,
914
+ client: SIEMClient = None, # type: ignore
915
+ ) -> Dict[str, Any]:
916
+ """
917
+ Search for security detection rules.
918
+
919
+ Tool schema:
920
+ - name: search_security_rules
921
+ - description: Search for security detection rules by name, description, or other criteria
922
+ - parameters:
923
+ - query (str, required): Search query (supports regex patterns)
924
+ - category (str, optional): Filter by rule category
925
+ - enabled_only (bool, optional): Only search enabled rules (default: false)
926
+ """
927
+ if client is None:
928
+ raise IntegrationError("SIEM client not provided")
929
+
930
+ if not hasattr(client, "search_security_rules"):
931
+ raise IntegrationError("SIEM client does not support search_security_rules")
932
+
933
+ try:
934
+ rules = client.search_security_rules(
935
+ query=query,
936
+ category=category,
937
+ enabled_only=enabled_only,
938
+ )
939
+
940
+ return {
941
+ "success": True,
942
+ "count": len(rules),
943
+ "rules": rules,
944
+ }
945
+ except Exception as e:
946
+ raise IntegrationError(f"Failed to search security rules: {str(e)}") from e
947
+
948
+
949
+ def get_rule_detections(
950
+ rule_id: str,
951
+ alert_state: Optional[str] = None,
952
+ hours_back: int = 24,
953
+ limit: int = 50,
954
+ client: SIEMClient = None, # type: ignore
955
+ ) -> Dict[str, Any]:
956
+ """
957
+ Get historical detections from a specific rule.
958
+
959
+ Tool schema:
960
+ - name: get_rule_detections
961
+ - description: Retrieve historical detections generated by a specific security detection rule
962
+ - parameters:
963
+ - rule_id (str, required): Unique ID of the rule
964
+ - alert_state (str, optional): Filter by alert state
965
+ - hours_back (int, optional): How many hours back (default: 24)
966
+ - limit (int, optional): Maximum number of detections (default: 50)
967
+ """
968
+ if client is None:
969
+ raise IntegrationError("SIEM client not provided")
970
+
971
+ if not hasattr(client, "get_rule_detections"):
972
+ raise IntegrationError("SIEM client does not support get_rule_detections")
973
+
974
+ try:
975
+ detections = client.get_rule_detections(
976
+ rule_id=rule_id,
977
+ alert_state=alert_state,
978
+ hours_back=hours_back,
979
+ limit=limit,
980
+ )
981
+
982
+ return {
983
+ "success": True,
984
+ "rule_id": rule_id,
985
+ "count": len(detections),
986
+ "detections": detections,
987
+ }
988
+ except Exception as e:
989
+ raise IntegrationError(f"Failed to get rule detections: {str(e)}") from e
990
+
991
+
992
+ def list_rule_errors(
993
+ rule_id: str,
994
+ hours_back: int = 24,
995
+ client: SIEMClient = None, # type: ignore
996
+ ) -> Dict[str, Any]:
997
+ """
998
+ List execution errors for a specific rule.
999
+
1000
+ Tool schema:
1001
+ - name: list_rule_errors
1002
+ - description: List execution errors for a specific security detection rule
1003
+ - parameters:
1004
+ - rule_id (str, required): Unique ID of the rule
1005
+ - hours_back (int, optional): How many hours back to look (default: 24)
1006
+ """
1007
+ if client is None:
1008
+ raise IntegrationError("SIEM client not provided")
1009
+
1010
+ if not hasattr(client, "list_rule_errors"):
1011
+ raise IntegrationError("SIEM client does not support list_rule_errors")
1012
+
1013
+ try:
1014
+ errors = client.list_rule_errors(
1015
+ rule_id=rule_id,
1016
+ hours_back=hours_back,
1017
+ )
1018
+
1019
+ return {
1020
+ "success": True,
1021
+ "rule_id": rule_id,
1022
+ "error_count": len(errors),
1023
+ "errors": errors,
1024
+ }
1025
+ except Exception as e:
1026
+ raise IntegrationError(f"Failed to list rule errors: {str(e)}") from e
1027
+
1028
+
1029
+ def close_alert(
1030
+ alert_id: str,
1031
+ reason: Optional[str] = None,
1032
+ comment: Optional[str] = None,
1033
+ client: SIEMClient = None, # type: ignore
1034
+ ) -> Dict[str, Any]:
1035
+ """
1036
+ Close an alert in the SIEM, typically used for false positives.
1037
+
1038
+ Tool schema:
1039
+ - name: close_alert
1040
+ - description: Close a security alert in the SIEM platform. Use this when an alert
1041
+ has been determined to be a false positive or benign true positive during triage.
1042
+ - parameters:
1043
+ - alert_id (str, required): The ID of the alert to close
1044
+ - reason (str, optional): Reason for closing (e.g., "false_positive", "benign_true_positive")
1045
+ - comment (str, optional): Comment explaining why the alert is being closed
1046
+
1047
+ Args:
1048
+ alert_id: The ID of the alert to close.
1049
+ reason: Optional reason for closing.
1050
+ comment: Optional comment explaining why the alert is being closed.
1051
+ client: The SIEM client.
1052
+
1053
+ Returns:
1054
+ Dictionary containing success status and alert details.
1055
+
1056
+ Raises:
1057
+ IntegrationError: If closing the alert fails.
1058
+ """
1059
+ if client is None:
1060
+ raise IntegrationError("SIEM client not provided")
1061
+
1062
+ if not hasattr(client, "close_alert"):
1063
+ raise IntegrationError("SIEM client does not support close_alert")
1064
+
1065
+ try:
1066
+ result = client.close_alert(
1067
+ alert_id=alert_id,
1068
+ reason=reason,
1069
+ comment=comment,
1070
+ )
1071
+
1072
+ return {
1073
+ "success": True,
1074
+ "alert_id": result.get("alert_id"),
1075
+ "status": result.get("status"),
1076
+ "reason": result.get("reason"),
1077
+ "comment": result.get("comment"),
1078
+ "alert": result.get("alert"),
1079
+ }
1080
+ except Exception as e:
1081
+ raise IntegrationError(f"Failed to close alert {alert_id}: {str(e)}") from e
1082
+
1083
+
1084
+ def update_alert_verdict(
1085
+ alert_id: str,
1086
+ verdict: str,
1087
+ comment: Optional[str] = None,
1088
+ client: SIEMClient = None, # type: ignore
1089
+ ) -> Dict[str, Any]:
1090
+ """
1091
+ Update the verdict for an alert in the SIEM.
1092
+
1093
+ Tool schema:
1094
+ - name: update_alert_verdict
1095
+ - description: Update the verdict for a security alert. Use this to set or update the verdict
1096
+ field (e.g., "in-progress", "false_positive", "benign_true_positive", "true_positive", "uncertain").
1097
+ This is the preferred method for setting verdicts as it clearly indicates the intent to
1098
+ update the verdict rather than close the alert.
1099
+ - parameters:
1100
+ - alert_id (str, required): The ID of the alert to update
1101
+ - verdict (str, required): The verdict value. Valid values: "in-progress", "false_positive",
1102
+ "benign_true_positive", "true_positive", "uncertain"
1103
+ - comment (str, optional): Optional comment explaining the verdict
1104
+
1105
+ Args:
1106
+ alert_id: The ID of the alert to update.
1107
+ verdict: The verdict value to set. Valid values: "in-progress", "false_positive",
1108
+ "benign_true_positive", "true_positive", "uncertain".
1109
+ comment: Optional comment explaining the verdict.
1110
+ client: The SIEM client.
1111
+
1112
+ Returns:
1113
+ Dictionary containing success status, alert_id, verdict, and updated alert details.
1114
+
1115
+ Raises:
1116
+ IntegrationError: If updating the verdict fails.
1117
+ """
1118
+ if client is None:
1119
+ raise IntegrationError("SIEM client not provided")
1120
+
1121
+ if not hasattr(client, "update_alert_verdict"):
1122
+ raise IntegrationError("SIEM client does not support update_alert_verdict")
1123
+
1124
+ try:
1125
+ result = client.update_alert_verdict(
1126
+ alert_id=alert_id,
1127
+ verdict=verdict,
1128
+ comment=comment,
1129
+ )
1130
+
1131
+ return {
1132
+ "success": True,
1133
+ "alert_id": result.get("alert_id"),
1134
+ "verdict": result.get("verdict"),
1135
+ "comment": result.get("comment"),
1136
+ "alert": result.get("alert"),
1137
+ }
1138
+ except Exception as e:
1139
+ raise IntegrationError(f"Failed to update alert verdict for {alert_id}: {str(e)}") from e
1140
+
1141
+
1142
+ def tag_alert(
1143
+ alert_id: str,
1144
+ tag: str,
1145
+ client: SIEMClient = None, # type: ignore
1146
+ ) -> Dict[str, Any]:
1147
+ """
1148
+ Tag an alert with a classification tag (FP, TP, or NMI).
1149
+
1150
+ Tool schema:
1151
+ - name: tag_alert
1152
+ - description: Tag a security alert in the SIEM platform with a classification.
1153
+ Use this to mark alerts as FP (False Positive), TP (True Positive), or NMI (Need More Investigation).
1154
+ - parameters:
1155
+ - alert_id (str, required): The ID of the alert to tag
1156
+ - tag (str, required): The tag to apply. Must be one of: "FP" (False Positive),
1157
+ "TP" (True Positive), or "NMI" (Need More Investigation)
1158
+
1159
+ Args:
1160
+ alert_id: The ID of the alert to tag.
1161
+ tag: The tag to apply. Must be one of: "FP", "TP", or "NMI".
1162
+ client: The SIEM client.
1163
+
1164
+ Returns:
1165
+ Dictionary containing success status and alert details with updated tags.
1166
+
1167
+ Raises:
1168
+ IntegrationError: If tagging the alert fails.
1169
+ """
1170
+ # Validate tag value first before checking client
1171
+ valid_tags = {"FP", "TP", "NMI"}
1172
+ tag_upper = tag.upper()
1173
+ if tag_upper not in valid_tags:
1174
+ raise IntegrationError(
1175
+ f"Invalid tag '{tag}'. Must be one of: FP (False Positive), "
1176
+ f"TP (True Positive), or NMI (Need More Investigation)"
1177
+ )
1178
+
1179
+ if client is None:
1180
+ raise IntegrationError("SIEM client not provided")
1181
+
1182
+ if not hasattr(client, "tag_alert"):
1183
+ raise IntegrationError("SIEM client does not support tag_alert")
1184
+
1185
+ try:
1186
+ result = client.tag_alert(
1187
+ alert_id=alert_id,
1188
+ tag=tag_upper,
1189
+ )
1190
+
1191
+ return {
1192
+ "success": True,
1193
+ "alert_id": result.get("alert_id"),
1194
+ "tag": result.get("tag"),
1195
+ "tags": result.get("tags", []),
1196
+ "alert": result.get("alert"),
1197
+ }
1198
+ except Exception as e:
1199
+ raise IntegrationError(f"Failed to tag alert {alert_id}: {str(e)}") from e
1200
+
1201
+
1202
+ def add_alert_note(
1203
+ alert_id: str,
1204
+ note: str,
1205
+ client: SIEMClient = None, # type: ignore
1206
+ ) -> Dict[str, Any]:
1207
+ """
1208
+ Add a note/comment to an alert in the SIEM.
1209
+
1210
+ Tool schema:
1211
+ - name: add_alert_note
1212
+ - description: Add a note or comment to a security alert in the SIEM platform.
1213
+ Use this to document investigation findings, recommendations for detection rule improvements,
1214
+ case numbers, or other relevant information about the alert.
1215
+ - parameters:
1216
+ - alert_id (str, required): The ID of the alert to add a note to
1217
+ - note (str, required): The note/comment text to add
1218
+
1219
+ Args:
1220
+ alert_id: The ID of the alert to add a note to.
1221
+ note: The note/comment text to add.
1222
+ client: The SIEM client.
1223
+
1224
+ Returns:
1225
+ Dictionary containing success status and alert details with the note.
1226
+
1227
+ Raises:
1228
+ IntegrationError: If adding the note fails.
1229
+ """
1230
+ if client is None:
1231
+ raise IntegrationError("SIEM client not provided")
1232
+
1233
+ if not hasattr(client, "add_alert_note"):
1234
+ raise IntegrationError("SIEM client does not support add_alert_note")
1235
+
1236
+ try:
1237
+ result = client.add_alert_note(
1238
+ alert_id=alert_id,
1239
+ note=note,
1240
+ )
1241
+
1242
+ return {
1243
+ "success": True,
1244
+ "alert_id": result.get("alert_id"),
1245
+ "note": result.get("note"),
1246
+ "alert": result.get("alert"),
1247
+ }
1248
+ except Exception as e:
1249
+ raise IntegrationError(f"Failed to add note to alert {alert_id}: {str(e)}") from e
1250
+
1251
+
1252
+ def search_kql_query(
1253
+ kql_query: str,
1254
+ limit: int = 500,
1255
+ hours_back: Optional[int] = None,
1256
+ client: SIEMClient = None, # type: ignore
1257
+ ) -> Dict[str, Any]:
1258
+ """
1259
+ Execute a KQL (Kusto Query Language) or advanced query for deeper investigations.
1260
+
1261
+ Tool schema:
1262
+ - name: search_kql_query
1263
+ - description: Execute a KQL (Kusto Query Language) or advanced query for deeper investigations.
1264
+ This tool allows for complex queries including advanced filtering, aggregations, time-based
1265
+ analysis, cross-index searches, and complex joins. Supports both KQL syntax and vendor-specific
1266
+ query DSL (e.g., Elasticsearch Query DSL).
1267
+ - parameters:
1268
+ - kql_query (str, required): KQL query string or advanced query DSL (JSON for Elasticsearch)
1269
+ - limit (int, optional): Maximum number of events to return (default: 500)
1270
+ - hours_back (int, optional): Optional time window in hours to limit the search
1271
+
1272
+ Args:
1273
+ kql_query: KQL query string or advanced query DSL.
1274
+ limit: Maximum number of events to return.
1275
+ hours_back: Optional time window in hours.
1276
+ client: The SIEM client.
1277
+
1278
+ Returns:
1279
+ Dictionary containing search results with events.
1280
+
1281
+ Raises:
1282
+ IntegrationError: If search fails.
1283
+ """
1284
+ if client is None:
1285
+ raise IntegrationError("SIEM client not provided")
1286
+
1287
+ if not hasattr(client, "search_kql_query"):
1288
+ raise IntegrationError("SIEM client does not support search_kql_query")
1289
+
1290
+ try:
1291
+ result = client.search_kql_query(
1292
+ kql_query=kql_query,
1293
+ limit=limit,
1294
+ hours_back=hours_back,
1295
+ )
1296
+
1297
+ return {
1298
+ "success": True,
1299
+ "query": result.query,
1300
+ "total_count": result.total_count,
1301
+ "returned_count": len(result.events),
1302
+ "events": [
1303
+ {
1304
+ "id": event.id,
1305
+ "timestamp": event.timestamp.isoformat(),
1306
+ "source_type": event.source_type.value,
1307
+ "message": event.message,
1308
+ "host": event.host,
1309
+ "username": event.username,
1310
+ "ip": event.ip,
1311
+ "process_name": event.process_name,
1312
+ "file_hash": event.file_hash,
1313
+ }
1314
+ for event in result.events
1315
+ ],
1316
+ }
1317
+ except Exception as e:
1318
+ raise IntegrationError(f"Failed to execute KQL query: {str(e)}") from e
1319
+
1320
+
1321
+ def get_network_events(
1322
+ source_ip: Optional[str] = None,
1323
+ destination_ip: Optional[str] = None,
1324
+ port: Optional[int] = None,
1325
+ protocol: Optional[str] = None,
1326
+ hours_back: int = 24,
1327
+ limit: int = 100,
1328
+ event_type: Optional[str] = None,
1329
+ client: SIEMClient = None, # type: ignore
1330
+ ) -> Dict[str, Any]:
1331
+ """
1332
+ Retrieve network traffic events (firewall, netflow, proxy logs) with structured fields.
1333
+
1334
+ Tool schema:
1335
+ - name: get_network_events
1336
+ - description: Retrieve network traffic events (firewall, netflow, proxy logs) with structured
1337
+ fields for analysis. Returns network events with source/destination IPs, ports, protocols,
1338
+ bytes, packets, and connection duration.
1339
+ - parameters:
1340
+ - source_ip (str, optional): Source IP address
1341
+ - destination_ip (str, optional): Destination IP address
1342
+ - port (int, optional): Port number
1343
+ - protocol (str, optional): Protocol (tcp, udp, icmp, etc.)
1344
+ - hours_back (int, optional): Time window (default: 24)
1345
+ - limit (int, optional): Max results (default: 100)
1346
+ - event_type (str, optional): Filter by event type ("firewall", "netflow", "proxy", "all")
1347
+
1348
+ Args:
1349
+ source_ip: Source IP address.
1350
+ destination_ip: Destination IP address.
1351
+ port: Port number.
1352
+ protocol: Protocol (tcp, udp, icmp, etc.).
1353
+ hours_back: Time window in hours.
1354
+ limit: Maximum number of events to return.
1355
+ event_type: Filter by event type.
1356
+ client: The SIEM client.
1357
+
1358
+ Returns:
1359
+ Dictionary containing network events with structured fields.
1360
+
1361
+ Raises:
1362
+ IntegrationError: If retrieval fails.
1363
+ """
1364
+ if client is None:
1365
+ raise IntegrationError("SIEM client not provided")
1366
+
1367
+ if not hasattr(client, "get_network_events"):
1368
+ raise IntegrationError("SIEM client does not support get_network_events")
1369
+
1370
+ try:
1371
+ result = client.get_network_events(
1372
+ source_ip=source_ip,
1373
+ destination_ip=destination_ip,
1374
+ port=port,
1375
+ protocol=protocol,
1376
+ hours_back=hours_back,
1377
+ limit=limit,
1378
+ event_type=event_type,
1379
+ )
1380
+
1381
+ # Result is a dictionary with events array
1382
+ events = result.get("events", [])
1383
+ return {
1384
+ "success": True,
1385
+ "total_count": result.get("total_count", len(events)),
1386
+ "returned_count": len(events),
1387
+ "events": events,
1388
+ }
1389
+ except Exception as e:
1390
+ raise IntegrationError(f"Failed to get network events: {str(e)}") from e
1391
+
1392
+
1393
+ def get_dns_events(
1394
+ domain: Optional[str] = None,
1395
+ ip_address: Optional[str] = None,
1396
+ resolved_ip: Optional[str] = None,
1397
+ query_type: Optional[str] = None,
1398
+ hours_back: int = 24,
1399
+ limit: int = 100,
1400
+ client: SIEMClient = None, # type: ignore
1401
+ ) -> Dict[str, Any]:
1402
+ """
1403
+ Retrieve DNS query and response events with structured fields.
1404
+
1405
+ Tool schema:
1406
+ - name: get_dns_events
1407
+ - description: Retrieve DNS query and response events with structured fields for analysis.
1408
+ Returns DNS events with domain, query type, resolved IP, source IP, and response codes.
1409
+ - parameters:
1410
+ - domain (str, optional): Domain name queried
1411
+ - ip_address (str, optional): IP that made the query
1412
+ - resolved_ip (str, optional): Resolved IP address
1413
+ - query_type (str, optional): DNS query type (A, AAAA, MX, TXT, etc.)
1414
+ - hours_back (int, optional): Time window (default: 24)
1415
+ - limit (int, optional): Max results (default: 100)
1416
+
1417
+ Args:
1418
+ domain: Domain name queried.
1419
+ ip_address: IP that made the query.
1420
+ resolved_ip: Resolved IP address.
1421
+ query_type: DNS query type (A, AAAA, MX, TXT, etc.).
1422
+ hours_back: Time window in hours.
1423
+ limit: Maximum number of events to return.
1424
+ client: The SIEM client.
1425
+
1426
+ Returns:
1427
+ Dictionary containing DNS events with structured fields.
1428
+
1429
+ Raises:
1430
+ IntegrationError: If retrieval fails.
1431
+ """
1432
+ if client is None:
1433
+ raise IntegrationError("SIEM client not provided")
1434
+
1435
+ if not hasattr(client, "get_dns_events"):
1436
+ raise IntegrationError("SIEM client does not support get_dns_events")
1437
+
1438
+ try:
1439
+ result = client.get_dns_events(
1440
+ domain=domain,
1441
+ ip_address=ip_address,
1442
+ resolved_ip=resolved_ip,
1443
+ query_type=query_type,
1444
+ hours_back=hours_back,
1445
+ limit=limit,
1446
+ )
1447
+
1448
+ # Result is a dictionary with events array
1449
+ events = result.get("events", [])
1450
+ return {
1451
+ "success": True,
1452
+ "total_count": result.get("total_count", len(events)),
1453
+ "returned_count": len(events),
1454
+ "events": events,
1455
+ }
1456
+ except Exception as e:
1457
+ raise IntegrationError(f"Failed to get DNS events: {str(e)}") from e
1458
+
1459
+
1460
+ def get_alerts_by_entity(
1461
+ entity_value: str,
1462
+ entity_type: Optional[str] = None,
1463
+ hours_back: int = 24,
1464
+ limit: int = 50,
1465
+ severity: Optional[str] = None,
1466
+ client: SIEMClient = None, # type: ignore
1467
+ ) -> Dict[str, Any]:
1468
+ """
1469
+ Retrieve alerts filtered by specific entity (IP, user, host, domain, hash) for correlation analysis.
1470
+
1471
+ Tool schema:
1472
+ - name: get_alerts_by_entity
1473
+ - description: Retrieve alerts filtered by specific entity (IP, user, host, domain, hash) for
1474
+ correlation analysis. Returns alerts that contain the specified entity.
1475
+ - parameters:
1476
+ - entity_value (str, required): Entity value (IP, user, hostname, domain, hash)
1477
+ - entity_type (str, optional): Entity type (auto-detected if not provided: "ip", "user", "host", "domain", "hash")
1478
+ - hours_back (int, optional): Lookback period (default: 24)
1479
+ - limit (int, optional): Max results (default: 50)
1480
+ - severity (str, optional): Filter by severity ("low", "medium", "high", "critical")
1481
+
1482
+ Args:
1483
+ entity_value: Entity value (IP, user, hostname, domain, hash).
1484
+ entity_type: Entity type (auto-detected if not provided).
1485
+ hours_back: Lookback period in hours.
1486
+ limit: Maximum number of alerts to return.
1487
+ severity: Filter by severity level.
1488
+ client: The SIEM client.
1489
+
1490
+ Returns:
1491
+ Dictionary containing alerts related to the entity.
1492
+
1493
+ Raises:
1494
+ IntegrationError: If retrieval fails.
1495
+ """
1496
+ if client is None:
1497
+ raise IntegrationError("SIEM client not provided")
1498
+
1499
+ if not hasattr(client, "get_alerts_by_entity"):
1500
+ raise IntegrationError("SIEM client does not support get_alerts_by_entity")
1501
+
1502
+ try:
1503
+ result = client.get_alerts_by_entity(
1504
+ entity_value=entity_value,
1505
+ entity_type=entity_type,
1506
+ hours_back=hours_back,
1507
+ limit=limit,
1508
+ severity=severity,
1509
+ )
1510
+
1511
+ return {
1512
+ "success": True,
1513
+ "entity_value": result.get("entity_value"),
1514
+ "entity_type": result.get("entity_type"),
1515
+ "total_count": result.get("total_count", 0),
1516
+ "returned_count": result.get("returned_count", 0),
1517
+ "alerts": result.get("alerts", []),
1518
+ }
1519
+ except Exception as e:
1520
+ raise IntegrationError(f"Failed to get alerts by entity: {str(e)}") from e
1521
+
1522
+
1523
+ def get_all_uncertain_alerts_for_host(
1524
+ hostname: str,
1525
+ hours_back: int = 7 * 24, # Default 7 days
1526
+ limit: int = 100,
1527
+ client: SIEMClient = None, # type: ignore
1528
+ ) -> Dict[str, Any]:
1529
+ """
1530
+ Retrieve all alerts with verdict="uncertain" for a specific host.
1531
+
1532
+ Tool schema:
1533
+ - name: get_all_uncertain_alerts_for_host
1534
+ - description: Retrieve all alerts with verdict="uncertain" for a specific host.
1535
+ This is useful for pattern analysis when investigating uncertain alerts to determine
1536
+ if multiple uncertain alerts on the same host indicate a broader issue requiring
1537
+ case creation and escalation.
1538
+ - parameters:
1539
+ - hostname (str, required): The hostname to search for
1540
+ - hours_back (int, optional): How many hours to look back (default: 168 = 7 days)
1541
+ - limit (int, optional): Maximum number of alerts to return (default: 100)
1542
+
1543
+ Args:
1544
+ hostname: The hostname to search for.
1545
+ hours_back: How many hours to look back (default: 168 = 7 days).
1546
+ limit: Maximum number of alerts to return.
1547
+ client: The SIEM client.
1548
+
1549
+ Returns:
1550
+ Dictionary containing uncertain alerts for the host.
1551
+
1552
+ Raises:
1553
+ IntegrationError: If retrieval fails.
1554
+ """
1555
+ if client is None:
1556
+ raise IntegrationError("SIEM client not provided")
1557
+
1558
+ if not hasattr(client, "get_all_uncertain_alerts_for_host"):
1559
+ raise IntegrationError("SIEM client does not support get_all_uncertain_alerts_for_host")
1560
+
1561
+ try:
1562
+ result = client.get_all_uncertain_alerts_for_host(
1563
+ hostname=hostname,
1564
+ hours_back=hours_back,
1565
+ limit=limit,
1566
+ )
1567
+
1568
+ return {
1569
+ "success": True,
1570
+ "hostname": result.get("hostname"),
1571
+ "hours_back": result.get("hours_back"),
1572
+ "total_count": result.get("total_count", 0),
1573
+ "returned_count": result.get("returned_count", 0),
1574
+ "alerts": result.get("alerts", []),
1575
+ }
1576
+ except Exception as e:
1577
+ raise IntegrationError(f"Failed to get uncertain alerts for host: {str(e)}") from e
1578
+
1579
+
1580
+ def get_alerts_by_time_window(
1581
+ start_time: str,
1582
+ end_time: str,
1583
+ limit: int = 100,
1584
+ severity: Optional[str] = None,
1585
+ alert_type: Optional[str] = None,
1586
+ client: SIEMClient = None, # type: ignore
1587
+ ) -> Dict[str, Any]:
1588
+ """
1589
+ Retrieve alerts within a specific time window for temporal correlation.
1590
+
1591
+ Tool schema:
1592
+ - name: get_alerts_by_time_window
1593
+ - description: Retrieve alerts within a specific time window for temporal correlation.
1594
+ Returns alerts that occurred between start_time and end_time.
1595
+ - parameters:
1596
+ - start_time (str, required): Start time (ISO format)
1597
+ - end_time (str, required): End time (ISO format)
1598
+ - limit (int, optional): Max results (default: 100)
1599
+ - severity (str, optional): Filter by severity
1600
+ - alert_type (str, optional): Filter by alert type
1601
+
1602
+ Args:
1603
+ start_time: Start time in ISO format.
1604
+ end_time: End time in ISO format.
1605
+ limit: Maximum number of alerts to return.
1606
+ severity: Filter by severity level.
1607
+ alert_type: Filter by alert type.
1608
+ client: The SIEM client.
1609
+
1610
+ Returns:
1611
+ Dictionary containing alerts in the time window.
1612
+
1613
+ Raises:
1614
+ IntegrationError: If retrieval fails.
1615
+ """
1616
+ if client is None:
1617
+ raise IntegrationError("SIEM client not provided")
1618
+
1619
+ if not hasattr(client, "get_alerts_by_time_window"):
1620
+ raise IntegrationError("SIEM client does not support get_alerts_by_time_window")
1621
+
1622
+ try:
1623
+ result = client.get_alerts_by_time_window(
1624
+ start_time=start_time,
1625
+ end_time=end_time,
1626
+ limit=limit,
1627
+ severity=severity,
1628
+ alert_type=alert_type,
1629
+ )
1630
+
1631
+ return {
1632
+ "success": True,
1633
+ "total_count": result.get("total_count", 0),
1634
+ "returned_count": result.get("returned_count", 0),
1635
+ "alerts": result.get("alerts", []),
1636
+ }
1637
+ except Exception as e:
1638
+ raise IntegrationError(f"Failed to get alerts by time window: {str(e)}") from e
1639
+
1640
+
1641
+ def get_email_events(
1642
+ sender_email: Optional[str] = None,
1643
+ recipient_email: Optional[str] = None,
1644
+ subject: Optional[str] = None,
1645
+ email_id: Optional[str] = None,
1646
+ hours_back: int = 24,
1647
+ limit: int = 100,
1648
+ event_type: Optional[str] = None,
1649
+ client: SIEMClient = None, # type: ignore
1650
+ ) -> Dict[str, Any]:
1651
+ """
1652
+ Retrieve email security events with structured fields for phishing analysis.
1653
+
1654
+ Tool schema:
1655
+ - name: get_email_events
1656
+ - description: Retrieve email security events with structured fields for phishing analysis.
1657
+ Returns email events with sender, recipient, subject, headers, authentication, URLs, and attachments.
1658
+ - parameters:
1659
+ - sender_email (str, optional): Sender email address
1660
+ - recipient_email (str, optional): Recipient email address
1661
+ - subject (str, optional): Email subject (partial match)
1662
+ - email_id (str, optional): Email message ID
1663
+ - hours_back (int, optional): Time window (default: 24)
1664
+ - limit (int, optional): Max results (default: 100)
1665
+ - event_type (str, optional): Filter by event type ("delivered", "blocked", "quarantined", "all")
1666
+
1667
+ Args:
1668
+ sender_email: Sender email address.
1669
+ recipient_email: Recipient email address.
1670
+ subject: Email subject (partial match).
1671
+ email_id: Email message ID.
1672
+ hours_back: Time window in hours.
1673
+ limit: Maximum number of events to return.
1674
+ event_type: Filter by event type.
1675
+ client: The SIEM client.
1676
+
1677
+ Returns:
1678
+ Dictionary containing email events with structured fields.
1679
+
1680
+ Raises:
1681
+ IntegrationError: If retrieval fails.
1682
+ """
1683
+ if client is None:
1684
+ raise IntegrationError("SIEM client not provided")
1685
+
1686
+ if not hasattr(client, "get_email_events"):
1687
+ raise IntegrationError("SIEM client does not support get_email_events")
1688
+
1689
+ try:
1690
+ result = client.get_email_events(
1691
+ sender_email=sender_email,
1692
+ recipient_email=recipient_email,
1693
+ subject=subject,
1694
+ email_id=email_id,
1695
+ hours_back=hours_back,
1696
+ limit=limit,
1697
+ event_type=event_type,
1698
+ )
1699
+
1700
+ return {
1701
+ "success": True,
1702
+ "total_count": result.get("total_count", 0),
1703
+ "returned_count": result.get("returned_count", 0),
1704
+ "events": result.get("events", []),
1705
+ }
1706
+ except Exception as e:
1707
+ raise IntegrationError(f"Failed to get email events: {str(e)}") from e
1708
+
1709
+