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.
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/METADATA +410 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/RECORD +85 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/WHEEL +5 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/top_level.txt +1 -0
- src/__init__.py +8 -0
- src/ai_controller/README.md +139 -0
- src/ai_controller/__init__.py +12 -0
- src/ai_controller/agent_executor.py +596 -0
- src/ai_controller/cli/__init__.py +2 -0
- src/ai_controller/cli/main.py +243 -0
- src/ai_controller/session_manager.py +409 -0
- src/ai_controller/web/__init__.py +2 -0
- src/ai_controller/web/server.py +1181 -0
- src/ai_controller/web/static/css/README.md +102 -0
- src/api/__init__.py +13 -0
- src/api/case_management.py +271 -0
- src/api/edr.py +187 -0
- src/api/kb.py +136 -0
- src/api/siem.py +308 -0
- src/core/__init__.py +10 -0
- src/core/config.py +242 -0
- src/core/config_storage.py +684 -0
- src/core/dto.py +50 -0
- src/core/errors.py +36 -0
- src/core/logging.py +128 -0
- src/integrations/__init__.py +8 -0
- src/integrations/case_management/__init__.py +5 -0
- src/integrations/case_management/iris/__init__.py +11 -0
- src/integrations/case_management/iris/iris_client.py +885 -0
- src/integrations/case_management/iris/iris_http.py +274 -0
- src/integrations/case_management/iris/iris_mapper.py +263 -0
- src/integrations/case_management/iris/iris_models.py +128 -0
- src/integrations/case_management/thehive/__init__.py +8 -0
- src/integrations/case_management/thehive/thehive_client.py +193 -0
- src/integrations/case_management/thehive/thehive_http.py +147 -0
- src/integrations/case_management/thehive/thehive_mapper.py +190 -0
- src/integrations/case_management/thehive/thehive_models.py +125 -0
- src/integrations/cti/__init__.py +6 -0
- src/integrations/cti/local_tip/__init__.py +10 -0
- src/integrations/cti/local_tip/local_tip_client.py +90 -0
- src/integrations/cti/local_tip/local_tip_http.py +110 -0
- src/integrations/cti/opencti/__init__.py +10 -0
- src/integrations/cti/opencti/opencti_client.py +101 -0
- src/integrations/cti/opencti/opencti_http.py +418 -0
- src/integrations/edr/__init__.py +6 -0
- src/integrations/edr/elastic_defend/__init__.py +6 -0
- src/integrations/edr/elastic_defend/elastic_defend_client.py +351 -0
- src/integrations/edr/elastic_defend/elastic_defend_http.py +162 -0
- src/integrations/eng/__init__.py +10 -0
- src/integrations/eng/clickup/__init__.py +8 -0
- src/integrations/eng/clickup/clickup_client.py +513 -0
- src/integrations/eng/clickup/clickup_http.py +156 -0
- src/integrations/eng/github/__init__.py +8 -0
- src/integrations/eng/github/github_client.py +169 -0
- src/integrations/eng/github/github_http.py +158 -0
- src/integrations/eng/trello/__init__.py +8 -0
- src/integrations/eng/trello/trello_client.py +207 -0
- src/integrations/eng/trello/trello_http.py +162 -0
- src/integrations/kb/__init__.py +12 -0
- src/integrations/kb/fs_kb_client.py +313 -0
- src/integrations/siem/__init__.py +6 -0
- src/integrations/siem/elastic/__init__.py +6 -0
- src/integrations/siem/elastic/elastic_client.py +3319 -0
- src/integrations/siem/elastic/elastic_http.py +165 -0
- src/mcp/README.md +183 -0
- src/mcp/TOOLS.md +2827 -0
- src/mcp/__init__.py +13 -0
- src/mcp/__main__.py +18 -0
- src/mcp/agent_profiles.py +408 -0
- src/mcp/flow_agent_profiles.py +424 -0
- src/mcp/mcp_server.py +4086 -0
- src/mcp/rules_engine.py +487 -0
- src/mcp/runbook_manager.py +264 -0
- src/orchestrator/__init__.py +11 -0
- src/orchestrator/incident_workflow.py +244 -0
- src/orchestrator/tools_case.py +1085 -0
- src/orchestrator/tools_cti.py +359 -0
- src/orchestrator/tools_edr.py +315 -0
- src/orchestrator/tools_eng.py +378 -0
- src/orchestrator/tools_kb.py +156 -0
- src/orchestrator/tools_siem.py +1709 -0
- src/web/__init__.py +8 -0
- src/web/config_server.py +511 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Low-level HTTP client for OpenCTI (Open Cyber Threat Intelligence Platform).
|
|
3
|
+
|
|
4
|
+
This module handles GraphQL requests to the OpenCTI API for hash lookups.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
from ....core.errors import IntegrationError
|
|
15
|
+
from ....core.logging import get_logger
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = get_logger("sami.integrations.cti.opencti.http")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OpenCTIHttpClient:
|
|
22
|
+
"""
|
|
23
|
+
HTTP client for OpenCTI GraphQL API.
|
|
24
|
+
|
|
25
|
+
Handles hash lookups via GraphQL queries.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
base_url: str,
|
|
31
|
+
api_key: str,
|
|
32
|
+
timeout_seconds: int = 30,
|
|
33
|
+
verify_ssl: bool = True,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""
|
|
36
|
+
Initialize the OpenCTI HTTP client.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
base_url: Base URL of the OpenCTI API (e.g., "https://opencti.example.com")
|
|
40
|
+
api_key: API key/token for authentication
|
|
41
|
+
timeout_seconds: Request timeout in seconds
|
|
42
|
+
verify_ssl: Whether to verify SSL certificates
|
|
43
|
+
"""
|
|
44
|
+
self.base_url = base_url.rstrip("/")
|
|
45
|
+
self.api_key = api_key
|
|
46
|
+
self.timeout_seconds = timeout_seconds
|
|
47
|
+
self.verify_ssl = verify_ssl
|
|
48
|
+
# Try /api/graphql first, fallback to /graphql
|
|
49
|
+
self.graphql_endpoint = f"{self.base_url}/api/graphql"
|
|
50
|
+
self.graphql_endpoint_fallback = f"{self.base_url}/graphql"
|
|
51
|
+
|
|
52
|
+
logger.info(
|
|
53
|
+
f"OpenCTI HTTP client initialized: base_url={self.base_url}, "
|
|
54
|
+
f"primary_endpoint={self.graphql_endpoint}, "
|
|
55
|
+
f"fallback_endpoint={self.graphql_endpoint_fallback}, "
|
|
56
|
+
f"timeout={timeout_seconds}s, verify_ssl={verify_ssl}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def _headers(self) -> Dict[str, str]:
|
|
60
|
+
"""Build request headers."""
|
|
61
|
+
return {
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
"Accept": "application/json",
|
|
64
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def _get_hash_type(self, hash_value: str) -> str:
|
|
68
|
+
"""
|
|
69
|
+
Determine hash type based on length.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
hash_value: The hash value
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Hash algorithm name (MD5, SHA1, SHA256, SHA512)
|
|
76
|
+
"""
|
|
77
|
+
hash_length = len(hash_value.strip())
|
|
78
|
+
if hash_length == 32:
|
|
79
|
+
return "MD5"
|
|
80
|
+
elif hash_length == 40:
|
|
81
|
+
return "SHA1"
|
|
82
|
+
elif hash_length == 64:
|
|
83
|
+
return "SHA256"
|
|
84
|
+
elif hash_length == 128:
|
|
85
|
+
return "SHA512"
|
|
86
|
+
else:
|
|
87
|
+
# Default to SHA256 for unknown lengths
|
|
88
|
+
return "SHA256"
|
|
89
|
+
|
|
90
|
+
def _get_observable_type(self, hash_type: str) -> str:
|
|
91
|
+
"""
|
|
92
|
+
Get OpenCTI observable type for hash.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
hash_type: Hash algorithm name (MD5, SHA1, SHA256, SHA512)
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
OpenCTI observable type (e.g., "File-SHA256")
|
|
99
|
+
"""
|
|
100
|
+
type_map = {
|
|
101
|
+
"MD5": "File-MD5",
|
|
102
|
+
"SHA1": "File-SHA1",
|
|
103
|
+
"SHA256": "File-SHA256",
|
|
104
|
+
"SHA512": "File-SHA512",
|
|
105
|
+
}
|
|
106
|
+
return type_map.get(hash_type, "File-SHA256")
|
|
107
|
+
|
|
108
|
+
def lookup_hash(self, hash_value: str) -> Optional[Dict[str, Any]]:
|
|
109
|
+
"""
|
|
110
|
+
Look up a hash via the GraphQL API.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
hash_value: The hash value to look up (MD5, SHA1, SHA256, SHA512)
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Dictionary containing hash information and related indicators, or None if not found
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
IntegrationError: If the API request fails
|
|
120
|
+
"""
|
|
121
|
+
hash_type = self._get_hash_type(hash_value)
|
|
122
|
+
observable_type = self._get_observable_type(hash_type)
|
|
123
|
+
hash_value_clean = hash_value.strip()
|
|
124
|
+
|
|
125
|
+
logger.info(
|
|
126
|
+
f"Starting OpenCTI hash lookup: hash={hash_value_clean[:16]}... "
|
|
127
|
+
f"(type={hash_type}, observable_type={observable_type})"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# OpenCTI GraphQL query using stixCyberObservables
|
|
131
|
+
# This is the correct way to query observables in OpenCTI
|
|
132
|
+
query = """
|
|
133
|
+
query HashLookup($filters: [StixCyberObservablesFiltering!], $first: Int) {
|
|
134
|
+
stixCyberObservables(filters: $filters, first: $first) {
|
|
135
|
+
edges {
|
|
136
|
+
node {
|
|
137
|
+
id
|
|
138
|
+
observable_value
|
|
139
|
+
entity_type
|
|
140
|
+
indicators {
|
|
141
|
+
edges {
|
|
142
|
+
node {
|
|
143
|
+
id
|
|
144
|
+
pattern
|
|
145
|
+
pattern_type
|
|
146
|
+
valid_from
|
|
147
|
+
valid_until
|
|
148
|
+
x_opencti_score
|
|
149
|
+
x_opencti_detection
|
|
150
|
+
created_at
|
|
151
|
+
updated_at
|
|
152
|
+
labels {
|
|
153
|
+
edges {
|
|
154
|
+
node {
|
|
155
|
+
id
|
|
156
|
+
value
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
killChainPhases {
|
|
161
|
+
edges {
|
|
162
|
+
node {
|
|
163
|
+
id
|
|
164
|
+
kill_chain_name
|
|
165
|
+
phase_name
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
# Build filters for OpenCTI
|
|
179
|
+
# Filter by observable type (e.g., "File-SHA256") and hash value
|
|
180
|
+
filters = [
|
|
181
|
+
{
|
|
182
|
+
"key": "entity_type",
|
|
183
|
+
"values": [observable_type]
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"key": "observable_value",
|
|
187
|
+
"values": [hash_value_clean]
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
variables = {
|
|
192
|
+
"filters": filters,
|
|
193
|
+
"first": 10 # Limit to first 10 results
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
payload = {
|
|
197
|
+
"query": query,
|
|
198
|
+
"variables": variables
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
logger.debug(
|
|
203
|
+
f"OpenCTI request details: endpoint={self.graphql_endpoint}, "
|
|
204
|
+
f"hash_type={hash_type}, observable_type={observable_type}, "
|
|
205
|
+
f"filters={filters}, timeout={self.timeout_seconds}s"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Try primary endpoint first
|
|
209
|
+
logger.debug(f"Sending POST request to primary endpoint: {self.graphql_endpoint}")
|
|
210
|
+
response = requests.post(
|
|
211
|
+
self.graphql_endpoint,
|
|
212
|
+
json=payload,
|
|
213
|
+
headers=self._headers(),
|
|
214
|
+
timeout=self.timeout_seconds,
|
|
215
|
+
verify=self.verify_ssl,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
logger.debug(f"Primary endpoint response: status_code={response.status_code}")
|
|
219
|
+
|
|
220
|
+
# If 404 on primary endpoint, try fallback
|
|
221
|
+
if response.status_code == 404:
|
|
222
|
+
logger.warning(
|
|
223
|
+
f"Primary endpoint {self.graphql_endpoint} returned 404 (Not Found). "
|
|
224
|
+
f"Attempting fallback endpoint: {self.graphql_endpoint_fallback}"
|
|
225
|
+
)
|
|
226
|
+
response = requests.post(
|
|
227
|
+
self.graphql_endpoint_fallback,
|
|
228
|
+
json=payload,
|
|
229
|
+
headers=self._headers(),
|
|
230
|
+
timeout=self.timeout_seconds,
|
|
231
|
+
verify=self.verify_ssl,
|
|
232
|
+
)
|
|
233
|
+
logger.debug(f"Fallback endpoint response: status_code={response.status_code}")
|
|
234
|
+
|
|
235
|
+
# Update endpoint for future requests if fallback works
|
|
236
|
+
if response.status_code != 404:
|
|
237
|
+
logger.info(
|
|
238
|
+
f"Fallback endpoint successful. Updating primary endpoint to: "
|
|
239
|
+
f"{self.graphql_endpoint_fallback}"
|
|
240
|
+
)
|
|
241
|
+
self.graphql_endpoint = self.graphql_endpoint_fallback
|
|
242
|
+
else:
|
|
243
|
+
logger.error(
|
|
244
|
+
f"Both endpoints returned 404: primary={self.graphql_endpoint}, "
|
|
245
|
+
f"fallback={self.graphql_endpoint_fallback}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Raise for any HTTP errors (including 404 if both endpoints fail)
|
|
249
|
+
response.raise_for_status()
|
|
250
|
+
|
|
251
|
+
logger.debug(f"HTTP request successful: status_code={response.status_code}")
|
|
252
|
+
result = response.json()
|
|
253
|
+
|
|
254
|
+
# Log response structure for debugging (without sensitive data)
|
|
255
|
+
logger.debug(
|
|
256
|
+
f"GraphQL response received: has_data={'data' in result}, "
|
|
257
|
+
f"has_errors={'errors' in result}, response_keys={list(result.keys())}"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Check for GraphQL errors
|
|
261
|
+
if "errors" in result:
|
|
262
|
+
error_messages = [err.get("message", "Unknown error") for err in result["errors"]]
|
|
263
|
+
error_msg = "; ".join(error_messages)
|
|
264
|
+
logger.error(
|
|
265
|
+
f"GraphQL query returned errors for hash {hash_value_clean[:16]}...: "
|
|
266
|
+
f"{error_msg}. Full errors: {json.dumps(result['errors'], indent=2)}"
|
|
267
|
+
)
|
|
268
|
+
raise IntegrationError(f"GraphQL query failed: {error_msg}")
|
|
269
|
+
|
|
270
|
+
# Extract hash data from GraphQL response
|
|
271
|
+
data = result.get("data", {})
|
|
272
|
+
observables = data.get("stixCyberObservables", {})
|
|
273
|
+
edges = observables.get("edges", [])
|
|
274
|
+
|
|
275
|
+
logger.debug(
|
|
276
|
+
f"Parsed GraphQL response: observables_found={len(edges)}, "
|
|
277
|
+
f"observable_keys={list(observables.keys()) if observables else 'N/A'}"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if not edges:
|
|
281
|
+
logger.info(
|
|
282
|
+
f"Hash {hash_value_clean[:16]}... (type={hash_type}) not found in OpenCTI. "
|
|
283
|
+
f"No observables matching the criteria."
|
|
284
|
+
)
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
# Get the first observable result
|
|
288
|
+
observable_node = edges[0]["node"]
|
|
289
|
+
observable_id = observable_node.get("id", "unknown")
|
|
290
|
+
observable_value = observable_node.get("observable_value", hash_value_clean)
|
|
291
|
+
|
|
292
|
+
logger.info(
|
|
293
|
+
f"Found observable in OpenCTI: id={observable_id}, "
|
|
294
|
+
f"value={observable_value[:16]}..., type={observable_type}"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Build result dictionary
|
|
298
|
+
hash_result = {
|
|
299
|
+
"value": observable_value,
|
|
300
|
+
"algorithm": hash_type, # Use detected hash type
|
|
301
|
+
"id": observable_id,
|
|
302
|
+
"indicators": [],
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# Extract indicators
|
|
306
|
+
indicators = observable_node.get("indicators", {}).get("edges", [])
|
|
307
|
+
logger.debug(f"Found {len(indicators)} indicator(s) associated with observable")
|
|
308
|
+
|
|
309
|
+
for idx, indicator_edge in enumerate(indicators):
|
|
310
|
+
indicator_node = indicator_edge["node"]
|
|
311
|
+
indicator_id = indicator_node.get("id", "unknown")
|
|
312
|
+
indicator_score = indicator_node.get("x_opencti_score")
|
|
313
|
+
indicator_detection = indicator_node.get("x_opencti_detection")
|
|
314
|
+
|
|
315
|
+
logger.debug(
|
|
316
|
+
f"Processing indicator {idx + 1}/{len(indicators)}: "
|
|
317
|
+
f"id={indicator_id}, score={indicator_score}, "
|
|
318
|
+
f"detection={indicator_detection}"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
indicator = {
|
|
322
|
+
"id": indicator_id,
|
|
323
|
+
"pattern": indicator_node.get("pattern"),
|
|
324
|
+
"pattern_type": indicator_node.get("pattern_type"),
|
|
325
|
+
"valid_from": indicator_node.get("valid_from"),
|
|
326
|
+
"valid_until": indicator_node.get("valid_until"),
|
|
327
|
+
"score": indicator_score,
|
|
328
|
+
"detection": indicator_detection,
|
|
329
|
+
"created_at": indicator_node.get("created_at"),
|
|
330
|
+
"updated_at": indicator_node.get("updated_at"),
|
|
331
|
+
"labels": [],
|
|
332
|
+
"kill_chain_phases": [],
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
# Extract labels
|
|
336
|
+
labels = indicator_node.get("labels", {}).get("edges", [])
|
|
337
|
+
for label_edge in labels:
|
|
338
|
+
label_value = label_edge["node"].get("value")
|
|
339
|
+
indicator["labels"].append(label_value)
|
|
340
|
+
|
|
341
|
+
if labels:
|
|
342
|
+
logger.debug(f"Indicator {indicator_id} has {len(labels)} label(s): {[l['node'].get('value') for l in labels]}")
|
|
343
|
+
|
|
344
|
+
# Extract kill chain phases
|
|
345
|
+
kill_chain = indicator_node.get("killChainPhases", {}).get("edges", [])
|
|
346
|
+
for phase_edge in kill_chain:
|
|
347
|
+
phase_node = phase_edge["node"]
|
|
348
|
+
indicator["kill_chain_phases"].append({
|
|
349
|
+
"kill_chain_name": phase_node.get("kill_chain_name"),
|
|
350
|
+
"phase_name": phase_node.get("phase_name"),
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
if kill_chain:
|
|
354
|
+
logger.debug(
|
|
355
|
+
f"Indicator {indicator_id} has {len(kill_chain)} kill chain phase(s): "
|
|
356
|
+
f"{[p['node'].get('phase_name') for p in kill_chain]}"
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
hash_result["indicators"].append(indicator)
|
|
360
|
+
|
|
361
|
+
logger.info(
|
|
362
|
+
f"OpenCTI hash lookup successful: hash={hash_value_clean[:16]}..., "
|
|
363
|
+
f"observable_id={observable_id}, indicators_count={len(hash_result['indicators'])}"
|
|
364
|
+
)
|
|
365
|
+
return hash_result
|
|
366
|
+
|
|
367
|
+
except requests.exceptions.Timeout as e:
|
|
368
|
+
logger.error(
|
|
369
|
+
f"Timeout looking up hash {hash_value_clean[:16]}... in OpenCTI: "
|
|
370
|
+
f"endpoint={self.graphql_endpoint}, timeout={self.timeout_seconds}s, error={e}"
|
|
371
|
+
)
|
|
372
|
+
raise IntegrationError(f"Timeout looking up hash: {e}") from e
|
|
373
|
+
except requests.exceptions.RequestException as e:
|
|
374
|
+
logger.error(
|
|
375
|
+
f"API request failed for hash {hash_value_clean[:16]}... in OpenCTI: "
|
|
376
|
+
f"endpoint={self.graphql_endpoint}, error_type={type(e).__name__}, error={e}"
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Try to extract error details from response
|
|
380
|
+
error_detail = None
|
|
381
|
+
status_code = None
|
|
382
|
+
if hasattr(e, "response") and e.response is not None:
|
|
383
|
+
status_code = e.response.status_code
|
|
384
|
+
logger.debug(
|
|
385
|
+
f"Error response details: status_code={status_code}, "
|
|
386
|
+
f"headers={dict(e.response.headers) if e.response.headers else 'N/A'}"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
try:
|
|
390
|
+
error_detail = e.response.json()
|
|
391
|
+
logger.debug(f"Error response JSON: {json.dumps(error_detail, indent=2)}")
|
|
392
|
+
|
|
393
|
+
if "errors" in error_detail:
|
|
394
|
+
error_messages = [err.get("message", "Unknown error") for err in error_detail["errors"]]
|
|
395
|
+
error_detail = "; ".join(error_messages)
|
|
396
|
+
elif "detail" in error_detail:
|
|
397
|
+
error_detail = error_detail["detail"]
|
|
398
|
+
except Exception as parse_error:
|
|
399
|
+
logger.debug(f"Could not parse error response as JSON: {parse_error}")
|
|
400
|
+
if e.response.text:
|
|
401
|
+
error_detail = e.response.text[:200]
|
|
402
|
+
logger.debug(f"Error response text (first 200 chars): {error_detail}")
|
|
403
|
+
|
|
404
|
+
error_msg = f"API request failed: {e}"
|
|
405
|
+
if status_code:
|
|
406
|
+
error_msg += f" (status_code={status_code})"
|
|
407
|
+
if error_detail:
|
|
408
|
+
error_msg += f" - {error_detail}"
|
|
409
|
+
|
|
410
|
+
logger.error(f"OpenCTI hash lookup failed: {error_msg}")
|
|
411
|
+
raise IntegrationError(error_msg) from e
|
|
412
|
+
except Exception as e:
|
|
413
|
+
logger.exception(
|
|
414
|
+
f"Unexpected error during OpenCTI hash lookup for {hash_value_clean[:16]}...: "
|
|
415
|
+
f"error_type={type(e).__name__}, error={e}"
|
|
416
|
+
)
|
|
417
|
+
raise IntegrationError(f"Unexpected error during hash lookup: {e}") from e
|
|
418
|
+
|