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,274 @@
1
+ """
2
+ Low-level HTTP client for IRIS.
3
+
4
+ This module is responsible for:
5
+ - authentication (API key header)
6
+ - building URLs
7
+ - making HTTP requests
8
+ - basic error handling
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from dataclasses import dataclass
15
+ from typing import Any, Dict, Optional
16
+
17
+ import requests
18
+
19
+ from ....core.errors import IntegrationError
20
+ from ....core.logging import get_logger
21
+
22
+
23
+ logger = get_logger("sami.integrations.iris.http")
24
+
25
+
26
+ @dataclass
27
+ class IrisHttpClient:
28
+ """
29
+ Simple HTTP client for IRIS's REST API.
30
+
31
+ IRIS API documentation: https://docs.dfir-iris.org/
32
+ """
33
+
34
+ base_url: str
35
+ api_key: str
36
+ timeout_seconds: int = 30
37
+ verify_ssl: bool = True
38
+
39
+ def _headers(self) -> Dict[str, str]:
40
+ return {
41
+ "Authorization": f"Bearer {self.api_key}",
42
+ "Content-Type": "application/json",
43
+ "Accept": "application/json",
44
+ }
45
+
46
+ def authenticate(self) -> None:
47
+ """
48
+ Placeholder for any explicit authentication logic.
49
+
50
+ IRIS uses API keys in headers, so there may be no
51
+ separate login step. This method exists to match the design doc
52
+ and as a hook for future changes.
53
+ """
54
+ logger.debug("IrisHttpClient.authenticate called (no-op for API key)")
55
+
56
+ def _build_url(self, endpoint: str) -> str:
57
+ """
58
+ Build a full URL from a base URL and an endpoint.
59
+
60
+ IRIS API v2.0.0+ uses direct paths without /api/v1/ prefix for manage endpoints.
61
+ API endpoints (like /api/ping, /api/versions) use /api/ prefix.
62
+
63
+ Args:
64
+ endpoint: API endpoint path (e.g., "/manage/cases/list" or "manage/cases/list")
65
+
66
+ Returns:
67
+ Full URL string
68
+ """
69
+ base = self.base_url.rstrip("/")
70
+ endpoint = endpoint.lstrip("/")
71
+
72
+ # IRIS API structure:
73
+ # - /api/* for API endpoints (ping, versions, etc.)
74
+ # - /manage/* for management endpoints (cases, users, etc.)
75
+ # - /case/* for case-specific operations
76
+ # No need to add /api/v1/ prefix - use endpoint as-is
77
+
78
+ return f"{base}/{endpoint}"
79
+
80
+ def _handle_iris_error(self, response: requests.Response) -> None:
81
+ """
82
+ Raise IntegrationError if the response indicates an error.
83
+
84
+ IRIS API returns responses in format: {"status": "success|error", "message": "...", "data": ...}
85
+
86
+ Args:
87
+ response: HTTP response object
88
+
89
+ Raises:
90
+ IntegrationError: If the response indicates an error
91
+ """
92
+ if response.status_code < 400:
93
+ # Check if response body indicates an error
94
+ try:
95
+ error_data = response.json()
96
+ if error_data.get("status") == "error":
97
+ message = error_data.get("message", "Unknown error")
98
+ raise IntegrationError(f"IRIS API error: {message}")
99
+ except (ValueError, IntegrationError):
100
+ # If it's already an IntegrationError, re-raise it
101
+ if isinstance(error_data, dict) and error_data.get("status") == "error":
102
+ raise
103
+ # Otherwise continue (might not be JSON or might be success)
104
+ return
105
+
106
+ # HTTP error status code
107
+ try:
108
+ error_data = response.json()
109
+ # IRIS API error format
110
+ if error_data.get("status") == "error":
111
+ message = error_data.get("message", f"HTTP {response.status_code}")
112
+ else:
113
+ message = error_data.get("message", f"HTTP {response.status_code}")
114
+ details = error_data.get("detail") or error_data.get("error", "")
115
+ full_message = f"{message}: {details}" if details else message
116
+ except Exception:
117
+ full_message = f"HTTP {response.status_code}: {response.text[:200]}"
118
+
119
+ raise IntegrationError(f"IRIS API error: {full_message}")
120
+
121
+ def request(
122
+ self,
123
+ method: str,
124
+ endpoint: str,
125
+ json_data: Optional[Dict[str, Any]] = None,
126
+ params: Optional[Dict[str, Any]] = None,
127
+ ) -> Dict[str, Any]:
128
+ """
129
+ Make an HTTP request to IRIS API.
130
+
131
+ Args:
132
+ method: HTTP method (GET, POST, PUT, PATCH, DELETE)
133
+ endpoint: API endpoint path
134
+ json_data: JSON payload (for POST, PUT, PATCH)
135
+ params: Query parameters (for GET, etc.)
136
+
137
+ Returns:
138
+ Response JSON as dictionary
139
+
140
+ Raises:
141
+ IntegrationError: If the request fails
142
+ """
143
+ url = self._build_url(endpoint)
144
+ headers = self._headers()
145
+
146
+ try:
147
+ logger.debug(f"IRIS {method} {url}")
148
+ if params:
149
+ logger.debug(f" Query params: {params}")
150
+ if json_data:
151
+ logger.debug(f" JSON payload: {json.dumps(json_data)[:200]}...")
152
+
153
+ response = requests.request(
154
+ method=method,
155
+ url=url,
156
+ headers=headers,
157
+ json=json_data,
158
+ params=params,
159
+ timeout=self.timeout_seconds,
160
+ verify=self.verify_ssl,
161
+ )
162
+
163
+ logger.debug(f"IRIS response status: {response.status_code}")
164
+ if response.status_code >= 400:
165
+ logger.error(f"IRIS API error - Status: {response.status_code}, URL: {url}, Response: {response.text[:500]}")
166
+
167
+ self._handle_iris_error(response)
168
+
169
+ if response.status_code == 204: # No Content
170
+ return {}
171
+
172
+ response_data = response.json()
173
+
174
+ # IRIS API wraps responses in {"status": "success", "message": "", "data": ...}
175
+ # Extract the data field if present
176
+ if isinstance(response_data, dict):
177
+ if response_data.get("status") == "success" and "data" in response_data:
178
+ return response_data["data"]
179
+ elif response_data.get("status") == "error":
180
+ # Error already handled by _handle_iris_error, but just in case
181
+ message = response_data.get("message", "Unknown error")
182
+ raise IntegrationError(f"IRIS API error: {message}")
183
+
184
+ return response_data
185
+
186
+ except requests.exceptions.Timeout as e:
187
+ raise IntegrationError(f"IRIS API request timeout: {e}") from e
188
+ except requests.exceptions.RequestException as e:
189
+ raise IntegrationError(f"IRIS API request failed: {e}") from e
190
+
191
+ def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
192
+ """GET request."""
193
+ return self.request("GET", endpoint, params=params)
194
+
195
+ def post(self, endpoint: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
196
+ """POST request."""
197
+ return self.request("POST", endpoint, json_data=json_data, params=params)
198
+
199
+ def post_file(
200
+ self,
201
+ endpoint: str,
202
+ file_path: str,
203
+ file_field: str = "file",
204
+ additional_data: Optional[Dict[str, Any]] = None,
205
+ params: Optional[Dict[str, Any]] = None,
206
+ ) -> Dict[str, Any]:
207
+ """
208
+ POST request with file upload.
209
+
210
+ Args:
211
+ endpoint: API endpoint path
212
+ file_path: Path to file to upload
213
+ file_field: Form field name for the file (default: "file")
214
+ additional_data: Additional form data to include
215
+ params: Query parameters to include in the URL
216
+
217
+ Returns:
218
+ Response JSON as dictionary
219
+ """
220
+ import os
221
+ from pathlib import Path
222
+
223
+ url = self._build_url(endpoint)
224
+ headers = {
225
+ "Authorization": f"Bearer {self.api_key}",
226
+ # Don't set Content-Type - let requests set it for multipart/form-data
227
+ }
228
+
229
+ if not os.path.exists(file_path):
230
+ raise IntegrationError(f"File not found: {file_path}")
231
+
232
+ try:
233
+ with open(file_path, "rb") as f:
234
+ files = {file_field: (os.path.basename(file_path), f)}
235
+ data = additional_data or {}
236
+
237
+ logger.debug(f"IRIS POST FILE {url}")
238
+ if params:
239
+ logger.debug(f" Query params: {params}")
240
+ logger.debug(f" File: {file_path}, Field: {file_field}")
241
+
242
+ response = requests.post(
243
+ url,
244
+ headers=headers,
245
+ files=files,
246
+ data=data,
247
+ params=params,
248
+ timeout=self.timeout_seconds,
249
+ verify=self.verify_ssl,
250
+ )
251
+
252
+ logger.debug(f"IRIS response status: {response.status_code}")
253
+ if response.status_code >= 400:
254
+ logger.error(f"IRIS API error - Status: {response.status_code}, URL: {url}, Response: {response.text[:500]}")
255
+
256
+ self._handle_iris_error(response)
257
+
258
+ if response.status_code == 204:
259
+ return {}
260
+
261
+ return response.json()
262
+ except requests.exceptions.Timeout as e:
263
+ raise IntegrationError(f"IRIS API request timeout: {e}") from e
264
+ except requests.exceptions.RequestException as e:
265
+ raise IntegrationError(f"IRIS API request failed: {e}") from e
266
+
267
+ def patch(self, endpoint: str, json_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
268
+ """PATCH request."""
269
+ return self.request("PATCH", endpoint, json_data=json_data)
270
+
271
+ def delete(self, endpoint: str) -> Dict[str, Any]:
272
+ """DELETE request."""
273
+ return self.request("DELETE", endpoint)
274
+
@@ -0,0 +1,263 @@
1
+ """
2
+ Mapping logic between generic case management DTOs and IRIS models/payloads.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ from ....api.case_management import (
10
+ Case,
11
+ CaseAssignment,
12
+ CaseComment,
13
+ CaseObservable,
14
+ CasePriority,
15
+ CaseStatus,
16
+ CaseSummary,
17
+ )
18
+ from .iris_models import (
19
+ IrisCase,
20
+ IrisCasePriority,
21
+ IrisCaseStatus,
22
+ parse_iris_case,
23
+ parse_iris_ioc,
24
+ )
25
+
26
+
27
+ # Generic → IRIS
28
+
29
+
30
+ def case_to_iris_payload(case: Case) -> Dict[str, Any]:
31
+ """
32
+ Convert a generic ``Case`` into an IRIS case creation/update payload.
33
+ """
34
+ payload: Dict[str, Any] = {
35
+ "case_name": case.title,
36
+ "case_description": case.description or "",
37
+ "case_customer": 3, # Always use "All" client (customer_id: 3)
38
+ "case_soc_id": "soc_id_default", # Required field - default SOC ID
39
+ }
40
+
41
+ # Convert tags list to comma-separated string (only if tags exist)
42
+ if case.tags:
43
+ tags_str = ", ".join(case.tags)
44
+ payload["case_tags"] = tags_str # IRIS expects tags as a string, not a list
45
+
46
+ # Note: state_id and severity_id are not set during creation
47
+ # IRIS will set defaults (state_id: 3, severity_id: 4)
48
+ # These can be updated after case creation if needed
49
+
50
+ if case.assignee:
51
+ # Note: IRIS uses user_id, so we'd need to resolve username to ID
52
+ # For now, assume assignee can be a username or ID string
53
+ payload["case_owner"] = case.assignee
54
+
55
+ return payload
56
+
57
+
58
+ def comment_to_iris_payload(comment: CaseComment) -> Dict[str, Any]:
59
+ """Convert generic comment to IRIS payload."""
60
+ return {
61
+ "comment_content": comment.content,
62
+ }
63
+
64
+
65
+ def observable_type_to_iris_ioc_type_id(observable_type: str) -> int:
66
+ """
67
+ Map generic observable type string to IRIS IOC type ID.
68
+
69
+ Common mappings:
70
+ - ip, ip-any -> 76 (ip-any)
71
+ - ip-dst -> 77
72
+ - ip-src -> 79
73
+ - domain -> 20
74
+ - url -> 141
75
+ - hash, md5 -> 90
76
+ - sha1 -> 111
77
+ - sha256 -> 113
78
+ - email -> 22
79
+ """
80
+ type_lower = observable_type.lower()
81
+
82
+ # IP addresses
83
+ if type_lower in ("ip", "ip-any", "ip_address"):
84
+ return 76 # ip-any
85
+ elif type_lower == "ip-dst":
86
+ return 77
87
+ elif type_lower == "ip-src":
88
+ return 79
89
+
90
+ # Domains
91
+ elif type_lower in ("domain", "fqdn", "hostname"):
92
+ return 20 # domain
93
+
94
+ # URLs
95
+ elif type_lower in ("url", "uri"):
96
+ return 141 # url
97
+
98
+ # Hashes
99
+ elif type_lower in ("hash", "md5"):
100
+ return 90 # md5
101
+ elif type_lower == "sha1":
102
+ return 111
103
+ elif type_lower == "sha256":
104
+ return 113
105
+
106
+ # Email
107
+ elif type_lower in ("email", "email-address"):
108
+ return 22 # email
109
+
110
+ # Default to "other" if type not recognized
111
+ return 96 # other
112
+
113
+
114
+ def observable_to_iris_payload(observable: CaseObservable) -> Dict[str, Any]:
115
+ """Convert generic observable to IRIS IOC payload."""
116
+ # Convert tags list to comma-separated string if needed
117
+ tags_str = ", ".join(observable.tags) if observable.tags else ""
118
+
119
+ return {
120
+ "ioc_type_id": observable_type_to_iris_ioc_type_id(observable.type),
121
+ "ioc_value": observable.value,
122
+ "ioc_tags": tags_str, # IRIS may expect string or list - try string first
123
+ "ioc_description": observable.description or "",
124
+ "ioc_tlp_id": 2, # Default to TLP:AMBER (1=WHITE, 2=AMBER, 3=GREEN, 4=RED)
125
+ }
126
+
127
+
128
+ def status_to_iris_status(case_status: CaseStatus) -> IrisCaseStatus:
129
+ """Map generic CaseStatus to IRIS CaseStatus enum."""
130
+ mapping = {
131
+ CaseStatus.OPEN: IrisCaseStatus.OPEN,
132
+ CaseStatus.IN_PROGRESS: IrisCaseStatus.ONGOING,
133
+ CaseStatus.CLOSED: IrisCaseStatus.CLOSED,
134
+ }
135
+ return mapping.get(case_status, IrisCaseStatus.OPEN)
136
+
137
+
138
+ def status_to_iris_status_id(case_status: CaseStatus) -> int:
139
+ """Map generic CaseStatus to IRIS status ID."""
140
+ # IRIS status IDs: typically 1=open, 2=ongoing, 3=closed, 4=archived
141
+ status_id_map = {
142
+ CaseStatus.OPEN: 1,
143
+ CaseStatus.IN_PROGRESS: 2,
144
+ CaseStatus.CLOSED: 3,
145
+ }
146
+ return status_id_map.get(case_status, 1)
147
+
148
+
149
+ def priority_to_iris_priority(priority: CasePriority) -> IrisCasePriority:
150
+ """Map generic CasePriority to IRIS CasePriority enum."""
151
+ mapping = {
152
+ CasePriority.LOW: IrisCasePriority.LOW,
153
+ CasePriority.MEDIUM: IrisCasePriority.MEDIUM,
154
+ CasePriority.HIGH: IrisCasePriority.HIGH,
155
+ CasePriority.CRITICAL: IrisCasePriority.CRITICAL,
156
+ }
157
+ return mapping[priority]
158
+
159
+
160
+ def priority_id_to_case_priority(priority_id: Optional[int]) -> CasePriority:
161
+ """Map IRIS priority ID to generic CasePriority."""
162
+ if priority_id is None:
163
+ return CasePriority.MEDIUM
164
+
165
+ # IRIS priority mapping: 4=low, 3=medium, 2=high, 1=critical
166
+ if priority_id >= 4:
167
+ return CasePriority.LOW
168
+ elif priority_id == 3:
169
+ return CasePriority.MEDIUM
170
+ elif priority_id == 2:
171
+ return CasePriority.HIGH
172
+ else:
173
+ return CasePriority.CRITICAL
174
+
175
+
176
+ def status_id_to_case_status(status_id: Optional[int]) -> CaseStatus:
177
+ """Map IRIS status ID to generic CaseStatus."""
178
+ if status_id is None:
179
+ return CaseStatus.OPEN
180
+
181
+ # IRIS status IDs: typically 1=open, 2=ongoing, 3=closed, 4=archived
182
+ if status_id == 1:
183
+ return CaseStatus.OPEN
184
+ elif status_id == 2:
185
+ return CaseStatus.IN_PROGRESS
186
+ else:
187
+ return CaseStatus.CLOSED
188
+
189
+
190
+ # IRIS → Generic
191
+
192
+
193
+ def iris_case_to_generic(raw: Dict[str, Any]) -> Case:
194
+ """Convert IRIS case to generic Case."""
195
+ iris_case: IrisCase = parse_iris_case(raw)
196
+
197
+ priority = priority_id_to_case_priority(iris_case.case_priority_id)
198
+ status = status_id_to_case_status(iris_case.case_status_id)
199
+
200
+ return Case(
201
+ id=str(iris_case.case_id),
202
+ title=iris_case.case_name,
203
+ description=iris_case.case_description or "",
204
+ status=status,
205
+ priority=priority,
206
+ created_at=iris_case.case_open_date,
207
+ updated_at=iris_case.case_update_date or iris_case.case_open_date,
208
+ assignee=str(iris_case.case_owner_id) if iris_case.case_owner_id else None,
209
+ tags=iris_case.case_tags or [],
210
+ observables=None,
211
+ )
212
+
213
+
214
+ def iris_case_to_summary(raw: Dict[str, Any]) -> CaseSummary:
215
+ """Convert IRIS case to generic CaseSummary."""
216
+ iris_case: IrisCase = parse_iris_case(raw)
217
+
218
+ priority = priority_id_to_case_priority(iris_case.case_priority_id)
219
+ status = status_id_to_case_status(iris_case.case_status_id)
220
+
221
+ return CaseSummary(
222
+ id=str(iris_case.case_id),
223
+ title=iris_case.case_name,
224
+ status=status,
225
+ priority=priority,
226
+ created_at=iris_case.case_open_date,
227
+ updated_at=iris_case.case_update_date or iris_case.case_open_date,
228
+ assignee=str(iris_case.case_owner_id) if iris_case.case_owner_id else None,
229
+ )
230
+
231
+
232
+ def iris_comment_to_generic(raw: Dict[str, Any], case_id: str) -> CaseComment:
233
+ """Convert IRIS comment to generic CaseComment."""
234
+ from datetime import datetime
235
+
236
+ created_at_value = raw.get("comment_date") or raw.get("comment_added")
237
+ created_at = None
238
+ if isinstance(created_at_value, (int, float)):
239
+ created_at = datetime.fromtimestamp(created_at_value)
240
+ elif isinstance(created_at_value, str):
241
+ try:
242
+ created_at = datetime.fromisoformat(created_at_value.replace("Z", "+00:00"))
243
+ except Exception:
244
+ pass
245
+
246
+ return CaseComment(
247
+ id=str(raw.get("comment_id", raw.get("id", 0))),
248
+ case_id=case_id,
249
+ author=raw.get("comment_user") or raw.get("user_name") or raw.get("user"),
250
+ content=raw.get("comment_content", raw.get("content", "")),
251
+ created_at=created_at,
252
+ )
253
+
254
+
255
+ def iris_ioc_to_observable(raw: Dict[str, Any], case_id: str) -> CaseObservable:
256
+ """Convert IRIS IOC to generic CaseObservable."""
257
+ iris_ioc = parse_iris_ioc(raw)
258
+ return CaseObservable(
259
+ type=iris_ioc.ioc_type,
260
+ value=iris_ioc.ioc_value,
261
+ tags=iris_ioc.ioc_tags or [],
262
+ description=iris_ioc.ioc_description,
263
+ )
@@ -0,0 +1,128 @@
1
+ """
2
+ IRIS-specific data models.
3
+
4
+ These dataclasses are close to IRIS's API payloads but kept separate
5
+ from the generic DTOs defined under ``src/api``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from datetime import datetime
12
+ from enum import Enum
13
+ from typing import Any, Dict, List, Optional
14
+
15
+
16
+ class IrisCaseStatus(str, Enum):
17
+ """IRIS case status values."""
18
+ OPEN = "open"
19
+ ONGOING = "ongoing"
20
+ CLOSED = "closed"
21
+ ARCHIVED = "archived"
22
+
23
+
24
+ class IrisCasePriority(str, Enum):
25
+ """IRIS case priority values."""
26
+ LOW = "low"
27
+ MEDIUM = "medium"
28
+ HIGH = "high"
29
+ CRITICAL = "critical"
30
+
31
+
32
+ @dataclass
33
+ class IrisUser:
34
+ """IRIS user model."""
35
+ id: int
36
+ user: str
37
+ name: Optional[str] = None
38
+ email: Optional[str] = None
39
+
40
+
41
+ @dataclass
42
+ class IrisIOC:
43
+ """IRIS IOCs (Indicators of Compromise) model."""
44
+ id: int
45
+ ioc_type: str # e.g., "ip", "domain", "hash", etc.
46
+ ioc_value: str
47
+ ioc_tags: Optional[List[str]] = None
48
+ ioc_description: Optional[str] = None
49
+
50
+
51
+ @dataclass
52
+ class IrisCase:
53
+ """IRIS case model."""
54
+ case_id: int
55
+ case_name: str
56
+ case_description: Optional[str]
57
+ case_priority_id: Optional[int] # Maps to priority: 4=low, 3=medium, 2=high, 1=critical
58
+ case_open_date: Optional[datetime]
59
+ case_update_date: Optional[datetime]
60
+ case_status_id: Optional[int] # Maps to status
61
+ case_owner_id: Optional[int]
62
+ case_tags: Optional[List[str]] = None
63
+
64
+
65
+ def parse_iris_case(raw: Dict[str, Any]) -> IrisCase:
66
+ """
67
+ Parse a raw IRIS case dict into an ``IrisCase`` instance.
68
+
69
+ IRIS API uses integer IDs and different field names than TheHive.
70
+ """
71
+ # IRIS uses timestamps or ISO format strings
72
+ open_date_value = raw.get("case_open_date") or raw.get("open_date")
73
+ open_date: Optional[datetime] = None
74
+ if isinstance(open_date_value, (int, float)):
75
+ open_date = datetime.fromtimestamp(open_date_value)
76
+ elif isinstance(open_date_value, str):
77
+ try:
78
+ open_date = datetime.fromisoformat(open_date_value.replace("Z", "+00:00"))
79
+ except Exception:
80
+ pass
81
+
82
+ update_date_value = raw.get("case_update_date") or raw.get("update_date")
83
+ update_date: Optional[datetime] = None
84
+ if isinstance(update_date_value, (int, float)):
85
+ update_date = datetime.fromtimestamp(update_date_value)
86
+ elif isinstance(update_date_value, str):
87
+ try:
88
+ update_date = datetime.fromisoformat(update_date_value.replace("Z", "+00:00"))
89
+ except Exception:
90
+ pass
91
+
92
+ return IrisCase(
93
+ case_id=int(raw.get("case_id", raw.get("id", 0))),
94
+ case_name=raw.get("case_name", raw.get("name", "")),
95
+ case_description=raw.get("case_description", raw.get("description")),
96
+ case_priority_id=raw.get("case_priority_id", raw.get("priority_id")),
97
+ case_open_date=open_date,
98
+ case_update_date=update_date,
99
+ case_status_id=raw.get("case_status_id", raw.get("status_id")),
100
+ case_owner_id=raw.get("case_owner_id", raw.get("owner_id")),
101
+ case_tags=raw.get("case_tags", raw.get("tags")) or [],
102
+ )
103
+
104
+
105
+ def parse_iris_ioc(raw: Dict[str, Any]) -> IrisIOC:
106
+ """
107
+ Parse a raw IRIS IOC dict.
108
+ """
109
+ return IrisIOC(
110
+ id=int(raw.get("ioc_id", 0)),
111
+ ioc_type=raw.get("ioc_type", ""),
112
+ ioc_value=raw.get("ioc_value", ""),
113
+ ioc_tags=raw.get("ioc_tags") or [],
114
+ ioc_description=raw.get("ioc_description"),
115
+ )
116
+
117
+
118
+ def parse_iris_user(raw: Dict[str, Any]) -> IrisUser:
119
+ """
120
+ Parse a raw IRIS user dict.
121
+ """
122
+ return IrisUser(
123
+ id=int(raw.get("user_id", 0)),
124
+ user=raw.get("user", ""),
125
+ name=raw.get("name"),
126
+ email=raw.get("email"),
127
+ )
128
+
@@ -0,0 +1,8 @@
1
+ """
2
+ TheHive case management integration for SamiGPT.
3
+
4
+ This package implements the generic ``CaseManagementClient`` interface
5
+ defined in ``src/api/case_management.py`` using TheHive's HTTP API.
6
+ """
7
+
8
+