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,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TheHive implementation of the generic ``CaseManagementClient`` interface.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
from ....api.case_management import (
|
|
10
|
+
Case,
|
|
11
|
+
CaseAssignment,
|
|
12
|
+
CaseComment,
|
|
13
|
+
CaseManagementClient,
|
|
14
|
+
CaseObservable,
|
|
15
|
+
CaseSearchQuery,
|
|
16
|
+
CaseStatus,
|
|
17
|
+
CaseSummary,
|
|
18
|
+
)
|
|
19
|
+
from ....core.config import SamiConfig
|
|
20
|
+
from ....core.errors import IntegrationError
|
|
21
|
+
from ....core.logging import get_logger
|
|
22
|
+
from .thehive_http import TheHiveHttpClient
|
|
23
|
+
from .thehive_mapper import (
|
|
24
|
+
case_to_thehive_payload,
|
|
25
|
+
comment_to_thehive_payload,
|
|
26
|
+
observable_to_thehive_payload,
|
|
27
|
+
status_to_thehive_status,
|
|
28
|
+
thehive_case_to_generic,
|
|
29
|
+
thehive_case_to_summary,
|
|
30
|
+
thehive_comment_to_generic,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
logger = get_logger("sami.integrations.thehive.client")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TheHiveCaseManagementClient(CaseManagementClient):
|
|
38
|
+
"""
|
|
39
|
+
Case management client backed by TheHive.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, http_client: TheHiveHttpClient) -> None:
|
|
43
|
+
self._http = http_client
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_config(cls, config: SamiConfig) -> "TheHiveCaseManagementClient":
|
|
47
|
+
"""
|
|
48
|
+
Factory to construct a client from ``SamiConfig``.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
if not config.thehive:
|
|
52
|
+
raise IntegrationError("TheHive configuration is not set in SamiConfig")
|
|
53
|
+
|
|
54
|
+
http_client = TheHiveHttpClient(
|
|
55
|
+
base_url=config.thehive.base_url,
|
|
56
|
+
api_key=config.thehive.api_key,
|
|
57
|
+
timeout_seconds=config.thehive.timeout_seconds,
|
|
58
|
+
)
|
|
59
|
+
return cls(http_client=http_client)
|
|
60
|
+
|
|
61
|
+
# Core CRUD operations
|
|
62
|
+
|
|
63
|
+
def create_case(self, case: Case) -> Case:
|
|
64
|
+
payload = case_to_thehive_payload(case)
|
|
65
|
+
raw = self._http.post("/api/case", json=payload)
|
|
66
|
+
return thehive_case_to_generic(raw)
|
|
67
|
+
|
|
68
|
+
def get_case(self, case_id: str) -> Case:
|
|
69
|
+
raw = self._http.get(f"/api/case/{case_id}")
|
|
70
|
+
return thehive_case_to_generic(raw)
|
|
71
|
+
|
|
72
|
+
def list_cases(
|
|
73
|
+
self,
|
|
74
|
+
status: Optional[CaseStatus] = None,
|
|
75
|
+
limit: int = 50,
|
|
76
|
+
) -> List[CaseSummary]:
|
|
77
|
+
params = {"range": f"[0,{max(limit - 1, 0)}]"}
|
|
78
|
+
if status is not None:
|
|
79
|
+
params["status"] = status_to_thehive_status(status).value
|
|
80
|
+
raw_list = self._http.get("/api/case", params=params)
|
|
81
|
+
return [thehive_case_to_summary(raw) for raw in raw_list]
|
|
82
|
+
|
|
83
|
+
def search_cases(self, query: CaseSearchQuery) -> List[CaseSummary]:
|
|
84
|
+
# Very simple text/field search mapping; real implementations may need
|
|
85
|
+
# to use TheHive's /api/case/_search endpoint with a JSON query body.
|
|
86
|
+
params: dict = {}
|
|
87
|
+
if query.text:
|
|
88
|
+
params["title"] = query.text
|
|
89
|
+
if query.status:
|
|
90
|
+
params["status"] = status_to_thehive_status(query.status).value
|
|
91
|
+
|
|
92
|
+
raw_list = self._http.get("/api/case", params=params)
|
|
93
|
+
summaries = [thehive_case_to_summary(raw) for raw in raw_list]
|
|
94
|
+
return summaries[: query.limit]
|
|
95
|
+
|
|
96
|
+
def update_case(self, case_id: str, updates: dict) -> Case:
|
|
97
|
+
raw = self._http.patch(f"/api/case/{case_id}", json=updates)
|
|
98
|
+
return thehive_case_to_generic(raw)
|
|
99
|
+
|
|
100
|
+
def delete_case(self, case_id: str) -> None:
|
|
101
|
+
self._http.delete(f"/api/case/{case_id}")
|
|
102
|
+
|
|
103
|
+
# Comments and observables
|
|
104
|
+
|
|
105
|
+
def add_case_comment(
|
|
106
|
+
self,
|
|
107
|
+
case_id: str,
|
|
108
|
+
content: str,
|
|
109
|
+
author: Optional[str] = None,
|
|
110
|
+
) -> CaseComment:
|
|
111
|
+
comment = CaseComment(
|
|
112
|
+
id=None,
|
|
113
|
+
case_id=case_id,
|
|
114
|
+
author=author,
|
|
115
|
+
content=content,
|
|
116
|
+
)
|
|
117
|
+
payload = comment_to_thehive_payload(comment)
|
|
118
|
+
raw = self._http.post(f"/api/case/{case_id}/comment", json=payload)
|
|
119
|
+
return thehive_comment_to_generic(raw, case_id=case_id)
|
|
120
|
+
|
|
121
|
+
def add_case_observable(
|
|
122
|
+
self,
|
|
123
|
+
case_id: str,
|
|
124
|
+
observable: CaseObservable,
|
|
125
|
+
) -> CaseObservable:
|
|
126
|
+
payload = observable_to_thehive_payload(observable)
|
|
127
|
+
raw = self._http.post(f"/api/case/{case_id}/artifact", json=payload)
|
|
128
|
+
from .thehive_models import parse_thehive_observable
|
|
129
|
+
|
|
130
|
+
return CaseObservable(
|
|
131
|
+
type=parse_thehive_observable(raw).data_type,
|
|
132
|
+
value=parse_thehive_observable(raw).data,
|
|
133
|
+
tags=parse_thehive_observable(raw).tags or [],
|
|
134
|
+
description=None,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Status and assignment
|
|
138
|
+
|
|
139
|
+
def update_case_status(
|
|
140
|
+
self,
|
|
141
|
+
case_id: str,
|
|
142
|
+
status: CaseStatus,
|
|
143
|
+
) -> Case:
|
|
144
|
+
payload = {"status": status_to_thehive_status(status).value}
|
|
145
|
+
raw = self._http.patch(f"/api/case/{case_id}", json=payload)
|
|
146
|
+
return thehive_case_to_generic(raw)
|
|
147
|
+
|
|
148
|
+
def assign_case(
|
|
149
|
+
self,
|
|
150
|
+
case_id: str,
|
|
151
|
+
assignee: str,
|
|
152
|
+
) -> CaseAssignment:
|
|
153
|
+
payload = {"owner": assignee}
|
|
154
|
+
raw = self._http.patch(f"/api/case/{case_id}", json=payload)
|
|
155
|
+
case = thehive_case_to_generic(raw)
|
|
156
|
+
from datetime import datetime
|
|
157
|
+
|
|
158
|
+
return CaseAssignment(
|
|
159
|
+
case_id=case.id or case_id,
|
|
160
|
+
assignee=case.assignee or assignee,
|
|
161
|
+
assigned_at=case.updated_at or datetime.utcnow(),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Linking and timeline
|
|
165
|
+
|
|
166
|
+
def link_cases(
|
|
167
|
+
self,
|
|
168
|
+
source_case_id: str,
|
|
169
|
+
target_case_id: str,
|
|
170
|
+
link_type: str,
|
|
171
|
+
) -> None:
|
|
172
|
+
payload = {
|
|
173
|
+
"caseId": target_case_id,
|
|
174
|
+
"nature": link_type,
|
|
175
|
+
}
|
|
176
|
+
self._http.post(f"/api/case/{source_case_id}/link", json=payload)
|
|
177
|
+
|
|
178
|
+
def get_case_timeline(self, case_id: str) -> List[CaseComment]:
|
|
179
|
+
# For now, use the comments endpoint as a simple timeline proxy.
|
|
180
|
+
raw_list = self._http.get(f"/api/case/{case_id}/comment")
|
|
181
|
+
return [thehive_comment_to_generic(raw, case_id=case_id) for raw in raw_list]
|
|
182
|
+
|
|
183
|
+
# Health check
|
|
184
|
+
|
|
185
|
+
def ping(self) -> bool:
|
|
186
|
+
try:
|
|
187
|
+
self._http.get("/api/health")
|
|
188
|
+
return True
|
|
189
|
+
except IntegrationError:
|
|
190
|
+
logger.exception("TheHive ping failed")
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Low-level HTTP client for TheHive.
|
|
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
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, Dict, Optional
|
|
15
|
+
|
|
16
|
+
import requests
|
|
17
|
+
|
|
18
|
+
from ....core.errors import IntegrationError
|
|
19
|
+
from ....core.logging import get_logger
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
logger = get_logger("sami.integrations.thehive.http")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class TheHiveHttpClient:
|
|
27
|
+
"""
|
|
28
|
+
Simple HTTP client for TheHive's REST API.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
base_url: str
|
|
32
|
+
api_key: str
|
|
33
|
+
timeout_seconds: int = 30
|
|
34
|
+
|
|
35
|
+
def _headers(self) -> Dict[str, str]:
|
|
36
|
+
return {
|
|
37
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def authenticate(self) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Placeholder for any explicit authentication logic.
|
|
44
|
+
|
|
45
|
+
TheHive typically uses API keys in headers, so there may be no
|
|
46
|
+
separate login step. This method exists to match the design doc
|
|
47
|
+
and as a hook for future changes.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
logger.debug("TheHiveHttpClient.authenticate called (no-op for API key)")
|
|
51
|
+
|
|
52
|
+
def request(
|
|
53
|
+
self,
|
|
54
|
+
method: str,
|
|
55
|
+
path: str,
|
|
56
|
+
json: Optional[Dict[str, Any]] = None,
|
|
57
|
+
params: Optional[Dict[str, Any]] = None,
|
|
58
|
+
) -> Dict[str, Any]:
|
|
59
|
+
"""
|
|
60
|
+
Make an HTTP request to TheHive and return the parsed JSON body.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
url = build_url(self.base_url, path)
|
|
64
|
+
logger.debug(
|
|
65
|
+
"TheHive HTTP request",
|
|
66
|
+
extra={"method": method, "url": url, "params": params},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
response = requests.request(
|
|
71
|
+
method=method.upper(),
|
|
72
|
+
url=url,
|
|
73
|
+
headers=self._headers(),
|
|
74
|
+
json=json,
|
|
75
|
+
params=params,
|
|
76
|
+
timeout=self.timeout_seconds,
|
|
77
|
+
)
|
|
78
|
+
except requests.RequestException as exc:
|
|
79
|
+
raise IntegrationError(f"TheHive request failed: {exc}") from exc
|
|
80
|
+
|
|
81
|
+
handle_thehive_error(response)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
return response.json()
|
|
85
|
+
except ValueError as exc:
|
|
86
|
+
raise IntegrationError(
|
|
87
|
+
f"TheHive response did not contain valid JSON (status={response.status_code})"
|
|
88
|
+
) from exc
|
|
89
|
+
|
|
90
|
+
def get(
|
|
91
|
+
self,
|
|
92
|
+
path: str,
|
|
93
|
+
params: Optional[Dict[str, Any]] = None,
|
|
94
|
+
) -> Dict[str, Any]:
|
|
95
|
+
return self.request("GET", path, params=params)
|
|
96
|
+
|
|
97
|
+
def post(
|
|
98
|
+
self,
|
|
99
|
+
path: str,
|
|
100
|
+
json: Dict[str, Any],
|
|
101
|
+
) -> Dict[str, Any]:
|
|
102
|
+
return self.request("POST", path, json=json)
|
|
103
|
+
|
|
104
|
+
def patch(
|
|
105
|
+
self,
|
|
106
|
+
path: str,
|
|
107
|
+
json: Dict[str, Any],
|
|
108
|
+
) -> Dict[str, Any]:
|
|
109
|
+
return self.request("PATCH", path, json=json)
|
|
110
|
+
|
|
111
|
+
def delete(self, path: str) -> None:
|
|
112
|
+
self.request("DELETE", path)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def build_url(base_url: str, path: str) -> str:
|
|
116
|
+
"""
|
|
117
|
+
Join base URL and path safely.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
return f"{base_url.rstrip('/')}/{path.lstrip('/')}"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def handle_thehive_error(response: requests.Response) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Raise an IntegrationError for non-success responses from TheHive.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
if 200 <= response.status_code < 300:
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
payload = response.json()
|
|
133
|
+
except ValueError:
|
|
134
|
+
payload = {"raw": response.text}
|
|
135
|
+
|
|
136
|
+
logger.error(
|
|
137
|
+
"TheHive HTTP error",
|
|
138
|
+
extra={
|
|
139
|
+
"status_code": response.status_code,
|
|
140
|
+
"payload": payload,
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
raise IntegrationError(
|
|
144
|
+
f"TheHive error {response.status_code}: {payload}"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mapping logic between generic case management DTOs and TheHive models/payloads.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List
|
|
8
|
+
|
|
9
|
+
from ....api.case_management import (
|
|
10
|
+
Case,
|
|
11
|
+
CaseAssignment,
|
|
12
|
+
CaseComment,
|
|
13
|
+
CaseObservable,
|
|
14
|
+
CasePriority,
|
|
15
|
+
CaseStatus,
|
|
16
|
+
CaseSummary,
|
|
17
|
+
)
|
|
18
|
+
from .thehive_models import (
|
|
19
|
+
TheHiveCase,
|
|
20
|
+
TheHiveCasePriority,
|
|
21
|
+
TheHiveCaseStatus,
|
|
22
|
+
parse_thehive_case,
|
|
23
|
+
parse_thehive_observable,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Generic → TheHive
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def case_to_thehive_payload(case: Case) -> Dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
Convert a generic ``Case`` into a TheHive case creation/update payload.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
payload: Dict[str, Any] = {
|
|
36
|
+
"title": case.title,
|
|
37
|
+
"description": case.description,
|
|
38
|
+
"tags": case.tags or [],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if case.priority is not None:
|
|
42
|
+
payload["severity"] = {
|
|
43
|
+
CasePriority.LOW: 1,
|
|
44
|
+
CasePriority.MEDIUM: 2,
|
|
45
|
+
CasePriority.HIGH: 3,
|
|
46
|
+
CasePriority.CRITICAL: 4,
|
|
47
|
+
}[case.priority]
|
|
48
|
+
|
|
49
|
+
if case.status is not None:
|
|
50
|
+
payload["status"] = status_to_thehive_status(case.status).value
|
|
51
|
+
|
|
52
|
+
if case.assignee:
|
|
53
|
+
payload["owner"] = case.assignee
|
|
54
|
+
|
|
55
|
+
return payload
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def comment_to_thehive_payload(comment: CaseComment) -> Dict[str, Any]:
|
|
59
|
+
return {
|
|
60
|
+
"message": comment.content,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def observable_to_thehive_payload(observable: CaseObservable) -> Dict[str, Any]:
|
|
65
|
+
return {
|
|
66
|
+
"dataType": observable.type,
|
|
67
|
+
"data": observable.value,
|
|
68
|
+
"tags": observable.tags or [],
|
|
69
|
+
"message": observable.description or "",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def status_to_thehive_status(status: CaseStatus) -> TheHiveCaseStatus:
|
|
74
|
+
mapping = {
|
|
75
|
+
CaseStatus.OPEN: TheHiveCaseStatus.OPEN,
|
|
76
|
+
CaseStatus.IN_PROGRESS: TheHiveCaseStatus.IN_PROGRESS,
|
|
77
|
+
CaseStatus.CLOSED: TheHiveCaseStatus.RESOLVED,
|
|
78
|
+
}
|
|
79
|
+
return mapping.get(status, TheHiveCaseStatus.OPEN)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def priority_to_thehive_priority(priority: CasePriority) -> TheHiveCasePriority:
|
|
83
|
+
mapping = {
|
|
84
|
+
CasePriority.LOW: TheHiveCasePriority.LOW,
|
|
85
|
+
CasePriority.MEDIUM: TheHiveCasePriority.MEDIUM,
|
|
86
|
+
CasePriority.HIGH: TheHiveCasePriority.HIGH,
|
|
87
|
+
CasePriority.CRITICAL: TheHiveCasePriority.CRITICAL,
|
|
88
|
+
}
|
|
89
|
+
return mapping[priority]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# TheHive → Generic
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def thehive_case_to_generic(raw: Dict[str, Any]) -> Case:
|
|
96
|
+
hive_case: TheHiveCase = parse_thehive_case(raw)
|
|
97
|
+
|
|
98
|
+
# Map severity back to CasePriority using simple thresholds.
|
|
99
|
+
severity = hive_case.severity or 0
|
|
100
|
+
if severity <= 1:
|
|
101
|
+
priority = CasePriority.LOW
|
|
102
|
+
elif severity == 2:
|
|
103
|
+
priority = CasePriority.MEDIUM
|
|
104
|
+
elif severity == 3:
|
|
105
|
+
priority = CasePriority.HIGH
|
|
106
|
+
else:
|
|
107
|
+
priority = CasePriority.CRITICAL
|
|
108
|
+
|
|
109
|
+
status_mapping = {
|
|
110
|
+
TheHiveCaseStatus.OPEN: CaseStatus.OPEN,
|
|
111
|
+
TheHiveCaseStatus.IN_PROGRESS: CaseStatus.IN_PROGRESS,
|
|
112
|
+
TheHiveCaseStatus.RESOLVED: CaseStatus.CLOSED,
|
|
113
|
+
TheHiveCaseStatus.DELETED: CaseStatus.CLOSED,
|
|
114
|
+
}
|
|
115
|
+
status = status_mapping.get(hive_case.status, CaseStatus.OPEN)
|
|
116
|
+
|
|
117
|
+
return Case(
|
|
118
|
+
id=hive_case.id,
|
|
119
|
+
title=hive_case.title,
|
|
120
|
+
description=hive_case.description or "",
|
|
121
|
+
status=status,
|
|
122
|
+
priority=priority,
|
|
123
|
+
created_at=hive_case.start_date,
|
|
124
|
+
updated_at=hive_case.start_date,
|
|
125
|
+
assignee=hive_case.owner,
|
|
126
|
+
tags=hive_case.tags or [],
|
|
127
|
+
observables=None,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def thehive_case_to_summary(raw: Dict[str, Any]) -> CaseSummary:
|
|
132
|
+
hive_case: TheHiveCase = parse_thehive_case(raw)
|
|
133
|
+
|
|
134
|
+
severity = hive_case.severity or 0
|
|
135
|
+
if severity <= 1:
|
|
136
|
+
priority = CasePriority.LOW
|
|
137
|
+
elif severity == 2:
|
|
138
|
+
priority = CasePriority.MEDIUM
|
|
139
|
+
elif severity == 3:
|
|
140
|
+
priority = CasePriority.HIGH
|
|
141
|
+
else:
|
|
142
|
+
priority = CasePriority.CRITICAL
|
|
143
|
+
|
|
144
|
+
status_mapping = {
|
|
145
|
+
TheHiveCaseStatus.OPEN: CaseStatus.OPEN,
|
|
146
|
+
TheHiveCaseStatus.IN_PROGRESS: CaseStatus.IN_PROGRESS,
|
|
147
|
+
TheHiveCaseStatus.RESOLVED: CaseStatus.CLOSED,
|
|
148
|
+
TheHiveCaseStatus.DELETED: CaseStatus.CLOSED,
|
|
149
|
+
}
|
|
150
|
+
status = status_mapping.get(hive_case.status, CaseStatus.OPEN)
|
|
151
|
+
|
|
152
|
+
return CaseSummary(
|
|
153
|
+
id=hive_case.id,
|
|
154
|
+
title=hive_case.title,
|
|
155
|
+
status=status,
|
|
156
|
+
priority=priority,
|
|
157
|
+
created_at=hive_case.start_date,
|
|
158
|
+
updated_at=hive_case.start_date,
|
|
159
|
+
assignee=hive_case.owner,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def thehive_comment_to_generic(raw: Dict[str, Any], case_id: str) -> CaseComment:
|
|
164
|
+
# TheHive uses "message" and "createdAt" in many comment-like resources.
|
|
165
|
+
from datetime import datetime
|
|
166
|
+
|
|
167
|
+
created_at_value = raw.get("createdAt")
|
|
168
|
+
created_at = None
|
|
169
|
+
if isinstance(created_at_value, (int, float)):
|
|
170
|
+
created_at = datetime.fromtimestamp(created_at_value / 1000.0)
|
|
171
|
+
|
|
172
|
+
return CaseComment(
|
|
173
|
+
id=str(raw.get("id") or raw.get("_id")),
|
|
174
|
+
case_id=case_id,
|
|
175
|
+
author=raw.get("user"),
|
|
176
|
+
content=raw.get("message", ""),
|
|
177
|
+
created_at=created_at,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def thehive_observable_to_generic(raw: Dict[str, Any], case_id: str) -> CaseObservable:
|
|
182
|
+
hive_obs = parse_thehive_observable(raw)
|
|
183
|
+
return CaseObservable(
|
|
184
|
+
type=hive_obs.data_type,
|
|
185
|
+
value=hive_obs.data,
|
|
186
|
+
tags=hive_obs.tags or [],
|
|
187
|
+
description=None,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TheHive-specific data models.
|
|
3
|
+
|
|
4
|
+
These dataclasses are close to TheHive'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 TheHiveCaseStatus(str, Enum):
|
|
17
|
+
OPEN = "Open"
|
|
18
|
+
IN_PROGRESS = "InProgress"
|
|
19
|
+
RESOLVED = "Resolved"
|
|
20
|
+
DELETED = "Deleted"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TheHiveCasePriority(str, Enum):
|
|
24
|
+
LOW = "Low"
|
|
25
|
+
MEDIUM = "Medium"
|
|
26
|
+
HIGH = "High"
|
|
27
|
+
CRITICAL = "Critical"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class TheHiveUser:
|
|
32
|
+
id: str
|
|
33
|
+
login: str
|
|
34
|
+
name: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class TheHiveObservable:
|
|
39
|
+
id: str
|
|
40
|
+
data_type: str
|
|
41
|
+
data: str
|
|
42
|
+
tlp: Optional[int] = None
|
|
43
|
+
tags: Optional[List[str]] = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class TheHiveAlert:
|
|
48
|
+
id: str
|
|
49
|
+
title: str
|
|
50
|
+
description: Optional[str] = None
|
|
51
|
+
severity: Optional[int] = None
|
|
52
|
+
source: Optional[str] = None
|
|
53
|
+
source_ref: Optional[str] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class TheHiveCase:
|
|
58
|
+
id: str
|
|
59
|
+
title: str
|
|
60
|
+
description: Optional[str]
|
|
61
|
+
severity: Optional[int]
|
|
62
|
+
start_date: Optional[datetime]
|
|
63
|
+
status: TheHiveCaseStatus
|
|
64
|
+
owner: Optional[str]
|
|
65
|
+
tags: Optional[List[str]] = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_thehive_case(raw: Dict[str, Any]) -> TheHiveCase:
|
|
69
|
+
"""
|
|
70
|
+
Parse a raw TheHive case dict into a ``TheHiveCase`` instance.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
# TheHive uses numeric timestamps (epoch ms); keep parsing simple and
|
|
74
|
+
# allow None if absent.
|
|
75
|
+
start_date_value = raw.get("startDate")
|
|
76
|
+
start_date: Optional[datetime]
|
|
77
|
+
if isinstance(start_date_value, (int, float)):
|
|
78
|
+
start_date = datetime.fromtimestamp(start_date_value / 1000.0)
|
|
79
|
+
else:
|
|
80
|
+
start_date = None
|
|
81
|
+
|
|
82
|
+
status = raw.get("status", "Open")
|
|
83
|
+
case_status = TheHiveCaseStatus(status) if status in TheHiveCaseStatus._value2member_map_ else TheHiveCaseStatus.OPEN
|
|
84
|
+
|
|
85
|
+
return TheHiveCase(
|
|
86
|
+
id=str(raw.get("id") or raw.get("_id")),
|
|
87
|
+
title=raw.get("title", ""),
|
|
88
|
+
description=raw.get("description"),
|
|
89
|
+
severity=raw.get("severity"),
|
|
90
|
+
start_date=start_date,
|
|
91
|
+
status=case_status,
|
|
92
|
+
owner=raw.get("owner"),
|
|
93
|
+
tags=raw.get("tags") or [],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def parse_thehive_observable(raw: Dict[str, Any]) -> TheHiveObservable:
|
|
98
|
+
"""
|
|
99
|
+
Parse a raw TheHive observable dict.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
return TheHiveObservable(
|
|
103
|
+
id=str(raw.get("id") or raw.get("_id")),
|
|
104
|
+
data_type=raw.get("dataType", ""),
|
|
105
|
+
data=raw.get("data", ""),
|
|
106
|
+
tlp=raw.get("tlp"),
|
|
107
|
+
tags=raw.get("tags") or [],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def parse_thehive_alert(raw: Dict[str, Any]) -> TheHiveAlert:
|
|
112
|
+
"""
|
|
113
|
+
Parse a raw TheHive alert dict.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
return TheHiveAlert(
|
|
117
|
+
id=str(raw.get("id") or raw.get("_id")),
|
|
118
|
+
title=raw.get("title", ""),
|
|
119
|
+
description=raw.get("description"),
|
|
120
|
+
severity=raw.get("severity"),
|
|
121
|
+
source=raw.get("source"),
|
|
122
|
+
source_ref=raw.get("sourceRef"),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|