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,351 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Elastic Defend (Endpoint Security) implementation of the generic ``EDRClient`` interface.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from ....api.edr import (
|
|
11
|
+
ActionResult,
|
|
12
|
+
ArtifactCollectionRequest,
|
|
13
|
+
Detection,
|
|
14
|
+
DetectionType,
|
|
15
|
+
EDRClient,
|
|
16
|
+
Endpoint,
|
|
17
|
+
KillProcessAction,
|
|
18
|
+
Platform,
|
|
19
|
+
Process,
|
|
20
|
+
QuarantineAction,
|
|
21
|
+
)
|
|
22
|
+
from ....core.config import SamiConfig
|
|
23
|
+
from ....core.errors import IntegrationError
|
|
24
|
+
from ....core.logging import get_logger
|
|
25
|
+
from .elastic_defend_http import ElasticDefendHttpClient
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
logger = get_logger("sami.integrations.elastic_defend.client")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ElasticDefendEDRClient:
|
|
32
|
+
"""
|
|
33
|
+
EDR client backed by Elastic Defend (Endpoint Security).
|
|
34
|
+
|
|
35
|
+
This implementation uses Elastic Fleet API and Endpoint Security API
|
|
36
|
+
for endpoint management and response actions.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, http_client: ElasticDefendHttpClient) -> None:
|
|
40
|
+
self._http = http_client
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_config(cls, config: SamiConfig) -> "ElasticDefendEDRClient":
|
|
44
|
+
"""
|
|
45
|
+
Factory to construct a client from ``SamiConfig``.
|
|
46
|
+
"""
|
|
47
|
+
if not config.edr:
|
|
48
|
+
raise IntegrationError("EDR configuration is not set in SamiConfig")
|
|
49
|
+
|
|
50
|
+
if config.edr.edr_type != "elastic_defend":
|
|
51
|
+
raise IntegrationError(f"EDR type '{config.edr.edr_type}' is not supported. Use 'elastic_defend' for Elastic Defend.")
|
|
52
|
+
|
|
53
|
+
http_client = ElasticDefendHttpClient(
|
|
54
|
+
base_url=config.edr.base_url,
|
|
55
|
+
api_key=config.edr.api_key,
|
|
56
|
+
timeout_seconds=config.edr.timeout_seconds,
|
|
57
|
+
verify_ssl=config.edr.verify_ssl,
|
|
58
|
+
)
|
|
59
|
+
return cls(http_client=http_client)
|
|
60
|
+
|
|
61
|
+
def get_endpoint_summary(self, endpoint_id: str) -> Endpoint:
|
|
62
|
+
"""Get endpoint details by ID."""
|
|
63
|
+
try:
|
|
64
|
+
# Use Fleet API to get agent details
|
|
65
|
+
response = self._http.get(f"/api/fleet/agents/{endpoint_id}")
|
|
66
|
+
|
|
67
|
+
agent = response.get("item", {})
|
|
68
|
+
local_metadata = agent.get("local_metadata", {})
|
|
69
|
+
host = local_metadata.get("host", {})
|
|
70
|
+
os_info = host.get("os", {})
|
|
71
|
+
|
|
72
|
+
# Determine platform
|
|
73
|
+
platform_name = os_info.get("name", "").lower()
|
|
74
|
+
if "windows" in platform_name:
|
|
75
|
+
platform = Platform.WINDOWS
|
|
76
|
+
elif "linux" in platform_name:
|
|
77
|
+
platform = Platform.LINUX
|
|
78
|
+
elif "mac" in platform_name or "darwin" in platform_name:
|
|
79
|
+
platform = Platform.MACOS
|
|
80
|
+
else:
|
|
81
|
+
platform = Platform.OTHER
|
|
82
|
+
|
|
83
|
+
# Parse last seen
|
|
84
|
+
last_seen = None
|
|
85
|
+
last_checkin = agent.get("last_checkin")
|
|
86
|
+
if last_checkin:
|
|
87
|
+
try:
|
|
88
|
+
last_seen = datetime.fromisoformat(last_checkin.replace("Z", "+00:00"))
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
return Endpoint(
|
|
93
|
+
id=endpoint_id,
|
|
94
|
+
hostname=host.get("hostname", endpoint_id),
|
|
95
|
+
platform=platform,
|
|
96
|
+
last_seen=last_seen,
|
|
97
|
+
primary_user=local_metadata.get("user", {}).get("name"),
|
|
98
|
+
is_isolated=agent.get("status") == "isolated",
|
|
99
|
+
)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.exception(f"Error getting endpoint summary: {e}")
|
|
102
|
+
raise IntegrationError(f"Failed to get endpoint summary: {e}") from e
|
|
103
|
+
|
|
104
|
+
def list_endpoints(self, limit: int = 50) -> List[Endpoint]:
|
|
105
|
+
"""List all endpoints."""
|
|
106
|
+
try:
|
|
107
|
+
response = self._http.get("/api/fleet/agents", params={"perPage": limit})
|
|
108
|
+
|
|
109
|
+
agents = response.get("items", [])
|
|
110
|
+
endpoints = []
|
|
111
|
+
|
|
112
|
+
for agent in agents:
|
|
113
|
+
agent_id = agent.get("id", "")
|
|
114
|
+
if not agent_id:
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
endpoint = self.get_endpoint_summary(agent_id)
|
|
119
|
+
endpoints.append(endpoint)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.warning(f"Failed to get details for endpoint {agent_id}: {e}")
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
return endpoints[:limit]
|
|
125
|
+
except Exception as e:
|
|
126
|
+
logger.exception(f"Error listing endpoints: {e}")
|
|
127
|
+
raise IntegrationError(f"Failed to list endpoints: {e}") from e
|
|
128
|
+
|
|
129
|
+
def get_detection_details(self, detection_id: str) -> Detection:
|
|
130
|
+
"""Get detection/alert details by ID."""
|
|
131
|
+
try:
|
|
132
|
+
# Search for detection in security events
|
|
133
|
+
query = {
|
|
134
|
+
"query": {
|
|
135
|
+
"bool": {
|
|
136
|
+
"must": [
|
|
137
|
+
{"term": {"_id": detection_id}},
|
|
138
|
+
{"term": {"event.category": "malware"}}
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Search in security indices
|
|
145
|
+
indices = "logs-endpoint.events.*"
|
|
146
|
+
response = self._http.post(f"/{indices}/_search", json_data=query)
|
|
147
|
+
|
|
148
|
+
hits = response.get("hits", {}).get("hits", [])
|
|
149
|
+
if not hits:
|
|
150
|
+
raise IntegrationError(f"Detection {detection_id} not found")
|
|
151
|
+
|
|
152
|
+
hit = hits[0]
|
|
153
|
+
source = hit.get("_source", {})
|
|
154
|
+
event = source.get("event", {})
|
|
155
|
+
|
|
156
|
+
# Determine detection type
|
|
157
|
+
detection_type = DetectionType.OTHER
|
|
158
|
+
if "malware" in event.get("category", []):
|
|
159
|
+
detection_type = DetectionType.MALWARE
|
|
160
|
+
elif "suspicious" in event.get("category", []):
|
|
161
|
+
detection_type = DetectionType.SUSPICIOUS_ACTIVITY
|
|
162
|
+
|
|
163
|
+
# Parse timestamp
|
|
164
|
+
timestamp_str = source.get("@timestamp")
|
|
165
|
+
created_at = datetime.utcnow()
|
|
166
|
+
if timestamp_str:
|
|
167
|
+
try:
|
|
168
|
+
created_at = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
|
|
169
|
+
except Exception:
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
# Extract process info
|
|
173
|
+
process_data = source.get("process", {})
|
|
174
|
+
process = None
|
|
175
|
+
if process_data:
|
|
176
|
+
process = Process(
|
|
177
|
+
pid=process_data.get("pid", 0),
|
|
178
|
+
name=process_data.get("name", ""),
|
|
179
|
+
path=process_data.get("executable"),
|
|
180
|
+
user=process_data.get("user", {}).get("name") if isinstance(process_data.get("user"), dict) else None,
|
|
181
|
+
command_line=process_data.get("command_line"),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return Detection(
|
|
185
|
+
id=detection_id,
|
|
186
|
+
endpoint_id=source.get("agent", {}).get("id", "") if isinstance(source.get("agent"), dict) else "",
|
|
187
|
+
created_at=created_at,
|
|
188
|
+
detection_type=detection_type,
|
|
189
|
+
severity=event.get("severity"),
|
|
190
|
+
description=event.get("action") or event.get("reason"),
|
|
191
|
+
file_hash=source.get("file", {}).get("hash", {}).get("sha256") if isinstance(source.get("file"), dict) else None,
|
|
192
|
+
process=process,
|
|
193
|
+
raw=source,
|
|
194
|
+
)
|
|
195
|
+
except Exception as e:
|
|
196
|
+
logger.exception(f"Error getting detection details: {e}")
|
|
197
|
+
raise IntegrationError(f"Failed to get detection details: {e}") from e
|
|
198
|
+
|
|
199
|
+
def list_detections(
|
|
200
|
+
self,
|
|
201
|
+
endpoint_id: Optional[str] = None,
|
|
202
|
+
limit: int = 50,
|
|
203
|
+
) -> List[Detection]:
|
|
204
|
+
"""List detections, optionally filtered by endpoint."""
|
|
205
|
+
try:
|
|
206
|
+
query = {
|
|
207
|
+
"query": {
|
|
208
|
+
"bool": {
|
|
209
|
+
"must": [
|
|
210
|
+
{"term": {"event.category": "malware"}}
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
"size": limit,
|
|
215
|
+
"sort": [{"@timestamp": {"order": "desc"}}]
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if endpoint_id:
|
|
219
|
+
query["query"]["bool"]["must"].append({
|
|
220
|
+
"term": {"agent.id": endpoint_id}
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
indices = "logs-endpoint.events.*"
|
|
224
|
+
response = self._http.post(f"/{indices}/_search", json_data=query)
|
|
225
|
+
|
|
226
|
+
hits = response.get("hits", {}).get("hits", [])
|
|
227
|
+
detections = []
|
|
228
|
+
|
|
229
|
+
for hit in hits:
|
|
230
|
+
detection_id = hit.get("_id", "")
|
|
231
|
+
try:
|
|
232
|
+
detection = self.get_detection_details(detection_id)
|
|
233
|
+
detections.append(detection)
|
|
234
|
+
except Exception as e:
|
|
235
|
+
logger.warning(f"Failed to get details for detection {detection_id}: {e}")
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
return detections[:limit]
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.exception(f"Error listing detections: {e}")
|
|
241
|
+
raise IntegrationError(f"Failed to list detections: {e}") from e
|
|
242
|
+
|
|
243
|
+
def isolate_endpoint(self, endpoint_id: str) -> QuarantineAction:
|
|
244
|
+
"""Isolate an endpoint (quarantine)."""
|
|
245
|
+
try:
|
|
246
|
+
# Use Endpoint Security API to isolate
|
|
247
|
+
payload = {
|
|
248
|
+
"endpoint_ids": [endpoint_id],
|
|
249
|
+
"action_type": "isolate"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
response = self._http.post("/api/endpoint/action/isolate", json_data=payload)
|
|
253
|
+
|
|
254
|
+
action_id = response.get("data", {}).get("id")
|
|
255
|
+
|
|
256
|
+
return QuarantineAction(
|
|
257
|
+
endpoint_id=endpoint_id,
|
|
258
|
+
requested_at=datetime.utcnow(),
|
|
259
|
+
result=ActionResult.PENDING,
|
|
260
|
+
message=f"Isolation action submitted: {action_id}",
|
|
261
|
+
)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
logger.exception(f"Error isolating endpoint: {e}")
|
|
264
|
+
raise IntegrationError(f"Failed to isolate endpoint: {e}") from e
|
|
265
|
+
|
|
266
|
+
def release_endpoint_isolation(self, endpoint_id: str) -> QuarantineAction:
|
|
267
|
+
"""Release endpoint from isolation."""
|
|
268
|
+
try:
|
|
269
|
+
# Use Endpoint Security API to unisolate
|
|
270
|
+
payload = {
|
|
271
|
+
"endpoint_ids": [endpoint_id],
|
|
272
|
+
"action_type": "unisolate"
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
response = self._http.post("/api/endpoint/action/unisolate", json_data=payload)
|
|
276
|
+
|
|
277
|
+
action_id = response.get("data", {}).get("id")
|
|
278
|
+
|
|
279
|
+
return QuarantineAction(
|
|
280
|
+
endpoint_id=endpoint_id,
|
|
281
|
+
requested_at=datetime.utcnow(),
|
|
282
|
+
completed_at=datetime.utcnow(),
|
|
283
|
+
result=ActionResult.SUCCESS,
|
|
284
|
+
message=f"Isolation released: {action_id}",
|
|
285
|
+
)
|
|
286
|
+
except Exception as e:
|
|
287
|
+
logger.exception(f"Error releasing endpoint isolation: {e}")
|
|
288
|
+
raise IntegrationError(f"Failed to release endpoint isolation: {e}") from e
|
|
289
|
+
|
|
290
|
+
def kill_process_on_endpoint(
|
|
291
|
+
self,
|
|
292
|
+
endpoint_id: str,
|
|
293
|
+
pid: int,
|
|
294
|
+
) -> KillProcessAction:
|
|
295
|
+
"""Kill a process on an endpoint."""
|
|
296
|
+
try:
|
|
297
|
+
# Use Endpoint Security API to kill process
|
|
298
|
+
payload = {
|
|
299
|
+
"endpoint_ids": [endpoint_id],
|
|
300
|
+
"action_type": "kill-process",
|
|
301
|
+
"parameters": {
|
|
302
|
+
"pid": pid
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
response = self._http.post("/api/endpoint/action/kill-process", json_data=payload)
|
|
307
|
+
|
|
308
|
+
action_id = response.get("data", {}).get("id")
|
|
309
|
+
|
|
310
|
+
return KillProcessAction(
|
|
311
|
+
endpoint_id=endpoint_id,
|
|
312
|
+
pid=pid,
|
|
313
|
+
requested_at=datetime.utcnow(),
|
|
314
|
+
result=ActionResult.PENDING,
|
|
315
|
+
message=f"Kill process action submitted: {action_id}",
|
|
316
|
+
)
|
|
317
|
+
except Exception as e:
|
|
318
|
+
logger.exception(f"Error killing process: {e}")
|
|
319
|
+
raise IntegrationError(f"Failed to kill process: {e}") from e
|
|
320
|
+
|
|
321
|
+
def collect_forensic_artifacts(
|
|
322
|
+
self,
|
|
323
|
+
endpoint_id: str,
|
|
324
|
+
artifact_types: List[str],
|
|
325
|
+
) -> ArtifactCollectionRequest:
|
|
326
|
+
"""Collect forensic artifacts from an endpoint."""
|
|
327
|
+
try:
|
|
328
|
+
# Use Endpoint Security API to collect artifacts
|
|
329
|
+
payload = {
|
|
330
|
+
"endpoint_ids": [endpoint_id],
|
|
331
|
+
"action_type": "collect-artifact",
|
|
332
|
+
"parameters": {
|
|
333
|
+
"artifacts": artifact_types
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
response = self._http.post("/api/endpoint/action/collect-artifact", json_data=payload)
|
|
338
|
+
|
|
339
|
+
action_id = response.get("data", {}).get("id")
|
|
340
|
+
|
|
341
|
+
return ArtifactCollectionRequest(
|
|
342
|
+
endpoint_id=endpoint_id,
|
|
343
|
+
requested_at=datetime.utcnow(),
|
|
344
|
+
artifact_types=artifact_types,
|
|
345
|
+
result=ActionResult.PENDING,
|
|
346
|
+
message=f"Artifact collection submitted: {action_id}",
|
|
347
|
+
)
|
|
348
|
+
except Exception as e:
|
|
349
|
+
logger.exception(f"Error collecting artifacts: {e}")
|
|
350
|
+
raise IntegrationError(f"Failed to collect forensic artifacts: {e}") from e
|
|
351
|
+
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Low-level HTTP client for Elastic Defend (Endpoint Security).
|
|
3
|
+
|
|
4
|
+
This module is responsible for:
|
|
5
|
+
- authentication (API key)
|
|
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.elastic_defend.http")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ElasticDefendHttpClient:
|
|
28
|
+
"""
|
|
29
|
+
Simple HTTP client for Elastic Defend API.
|
|
30
|
+
|
|
31
|
+
Elastic Defend uses the Elasticsearch API with API key authentication.
|
|
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
|
+
"""Build request headers with authentication."""
|
|
41
|
+
headers = {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
"Accept": "application/json",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Elastic API key format: "ApiKey <base64-encoded-key>"
|
|
47
|
+
if not self.api_key.startswith("ApiKey "):
|
|
48
|
+
headers["Authorization"] = f"ApiKey {self.api_key}"
|
|
49
|
+
else:
|
|
50
|
+
headers["Authorization"] = self.api_key
|
|
51
|
+
|
|
52
|
+
return headers
|
|
53
|
+
|
|
54
|
+
def _build_url(self, endpoint: str) -> str:
|
|
55
|
+
"""
|
|
56
|
+
Build a full URL from a base URL and an endpoint.
|
|
57
|
+
|
|
58
|
+
Elastic Defend uses endpoints like:
|
|
59
|
+
- /api/fleet/agents
|
|
60
|
+
- /api/endpoint/actions
|
|
61
|
+
- /api/endpoint/isolate
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
endpoint: API endpoint path (e.g., "/api/fleet/agents" or "api/fleet/agents")
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Full URL string
|
|
68
|
+
"""
|
|
69
|
+
base = self.base_url.rstrip("/")
|
|
70
|
+
endpoint = endpoint.lstrip("/")
|
|
71
|
+
|
|
72
|
+
return f"{base}/{endpoint}"
|
|
73
|
+
|
|
74
|
+
def _handle_elastic_error(self, response: requests.Response) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Raise IntegrationError if the response indicates an error.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
response: HTTP response object
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
IntegrationError: If the response indicates an error
|
|
83
|
+
"""
|
|
84
|
+
if response.status_code < 400:
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
error_data = response.json()
|
|
89
|
+
error_type = error_data.get("error", {}).get("type", "Unknown")
|
|
90
|
+
error_reason = error_data.get("error", {}).get("reason", f"HTTP {response.status_code}")
|
|
91
|
+
full_message = f"{error_type}: {error_reason}"
|
|
92
|
+
except Exception:
|
|
93
|
+
full_message = f"HTTP {response.status_code}: {response.text[:200]}"
|
|
94
|
+
|
|
95
|
+
raise IntegrationError(f"Elastic Defend API error: {full_message}")
|
|
96
|
+
|
|
97
|
+
def request(
|
|
98
|
+
self,
|
|
99
|
+
method: str,
|
|
100
|
+
endpoint: str,
|
|
101
|
+
json_data: Optional[Dict[str, Any]] = None,
|
|
102
|
+
params: Optional[Dict[str, Any]] = None,
|
|
103
|
+
) -> Dict[str, Any]:
|
|
104
|
+
"""
|
|
105
|
+
Make an HTTP request to Elastic Defend API.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
method: HTTP method (GET, POST, PUT, PATCH, DELETE)
|
|
109
|
+
endpoint: API endpoint path
|
|
110
|
+
json_data: JSON payload (for POST, PUT, PATCH)
|
|
111
|
+
params: Query parameters (for GET, etc.)
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Response JSON as dictionary
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
IntegrationError: If the request fails
|
|
118
|
+
"""
|
|
119
|
+
url = self._build_url(endpoint)
|
|
120
|
+
headers = self._headers()
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
logger.debug(f"Elastic Defend {method} {url}")
|
|
124
|
+
if params:
|
|
125
|
+
logger.debug(f" Query params: {params}")
|
|
126
|
+
if json_data:
|
|
127
|
+
logger.debug(f" JSON payload: {json.dumps(json_data)[:200]}...")
|
|
128
|
+
|
|
129
|
+
response = requests.request(
|
|
130
|
+
method=method,
|
|
131
|
+
url=url,
|
|
132
|
+
headers=headers,
|
|
133
|
+
json=json_data,
|
|
134
|
+
params=params,
|
|
135
|
+
timeout=self.timeout_seconds,
|
|
136
|
+
verify=self.verify_ssl,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
logger.debug(f"Elastic Defend response status: {response.status_code}")
|
|
140
|
+
if response.status_code >= 400:
|
|
141
|
+
logger.error(f"Elastic Defend API error - Status: {response.status_code}, URL: {url}, Response: {response.text[:500]}")
|
|
142
|
+
|
|
143
|
+
self._handle_elastic_error(response)
|
|
144
|
+
|
|
145
|
+
if response.status_code == 204: # No Content
|
|
146
|
+
return {}
|
|
147
|
+
|
|
148
|
+
return response.json()
|
|
149
|
+
|
|
150
|
+
except requests.exceptions.Timeout as e:
|
|
151
|
+
raise IntegrationError(f"Elastic Defend API request timeout: {e}") from e
|
|
152
|
+
except requests.exceptions.RequestException as e:
|
|
153
|
+
raise IntegrationError(f"Elastic Defend API request failed: {e}") from e
|
|
154
|
+
|
|
155
|
+
def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
156
|
+
"""GET request."""
|
|
157
|
+
return self.request("GET", endpoint, params=params)
|
|
158
|
+
|
|
159
|
+
def post(self, endpoint: str, json_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
160
|
+
"""POST request."""
|
|
161
|
+
return self.request("POST", endpoint, json_data=json_data)
|
|
162
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Engineering integrations (e.g., Trello, ClickUp, GitHub).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .trello.trello_client import TrelloClient
|
|
6
|
+
from .clickup.clickup_client import ClickUpClient
|
|
7
|
+
from .github.github_client import GitHubClient
|
|
8
|
+
|
|
9
|
+
__all__ = ["TrelloClient", "ClickUpClient", "GitHubClient"]
|
|
10
|
+
|