Portainer things (#407)
* fix: enhance logging for Shuffle API requests and add ignore rule for Velociraptor * portainer connector and jwt token * fetch portainer nodes * fix: update log message for fetching swarm node details * feat: add endpoint to create Wazuh customer stack and implement swarm ID retrieval
taylor_socfortress committed
Feb 19, 2025 at 20:45 UTC
52762d3d36c9e6c302429fb8cd650f0fd1d8ea32
14 files changed
+701
-1
backend/app/connectors/portainer/routes/portainer.py
new
+49
@@ -0,0 +1,49 @@
1
+from fastapi import APIRouter
2
+from fastapi import Security
3
+from loguru import logger
4
+
5
+from app.auth.utils import AuthHandler
6
+#from app.connectors.portainer.schema.integrations import ExecuteWorkflowRequest
7
+from app.connectors.portainer.schema.nodes import NodesResponse
8
+from app.connectors.portainer.services.nodes import get_node_details
9
+from app.connectors.portainer.services.stack import create_wazuh_customer_stack
10
+from app.customer_provisioning.schema.provision import ProvisionNewCustomer
11
+from app.connectors.portainer.schema.stack import StackResponse
12
+
13
+portainer_integrations_router = APIRouter()
14
+
15
+
16
+@portainer_integrations_router.get(
17
+ "/node-details",
18
+ description="Execute a portainer Integration.",
19
+ dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
20
+ response_model=NodesResponse,
21
+)
22
+async def node_details_route():
23
+ """
24
+ Get the IP addresses of all nodes in the swarm.
25
+
26
+ Returns:
27
+ list: The list of IP addresses.
28
+ """
29
+ logger.info("Getting swarm node details")
30
+ return await get_node_details()
31
+
32
+
33
+@portainer_integrations_router.post(
34
+ "/create-wazuh-customer-stack",
35
+ description="Create a Wazuh stack for a customer.",
36
+ dependencies=[Security(AuthHandler().require_any_scope("admin"))],
37
+ response_model=StackResponse,
38
+)
39
+async def create_wazuh_customer_stack_route(request: ProvisionNewCustomer):
40
+ """
41
+ Create a Wazuh stack for a customer.
42
+
43
+ Args:
44
+ request (ProvisionNewCustomer): The request object.
45
+
46
+ Returns:
47
+ dict: The response object.
48
+ """
49
+ return await create_wazuh_customer_stack(request)
backend/app/connectors/portainer/schema/nodes.py
new
+64
@@ -0,0 +1,64 @@
1
+from typing import List, Optional
2
+from pydantic import BaseModel
3
+from datetime import datetime
4
+
5
+class Version(BaseModel):
6
+ Index: int
7
+
8
+class NodeSpec(BaseModel):
9
+ Labels: dict
10
+ Role: str
11
+ Availability: str
12
+
13
+class Platform(BaseModel):
14
+ Architecture: str
15
+ OS: str
16
+
17
+class Resources(BaseModel):
18
+ NanoCPUs: int
19
+ MemoryBytes: int
20
+
21
+class Plugin(BaseModel):
22
+ Type: str
23
+ Name: str
24
+
25
+class TLSInfo(BaseModel):
26
+ TrustRoot: Optional[str] = None
27
+ CertIssuerSubject: Optional[str] = None
28
+ CertIssuerPublicKey: Optional[str] = None
29
+
30
+class Engine(BaseModel):
31
+ EngineVersion: str
32
+ Plugins: List[Plugin]
33
+ TLSInfo: Optional[TLSInfo] = None
34
+
35
+class Description(BaseModel):
36
+ Hostname: str
37
+ Platform: Platform
38
+ Resources: Resources
39
+ Engine: Engine
40
+
41
+class ManagerStatusResults(BaseModel):
42
+ Leader: bool
43
+ Reachability: str
44
+ Addr: str
45
+
46
+class Status(BaseModel):
47
+ State: str
48
+ Addr: str
49
+
50
+class Node(BaseModel):
51
+ ID: str
52
+ Version: Version
53
+ CreatedAt: datetime
54
+ UpdatedAt: datetime
55
+ Spec: NodeSpec
56
+ Description: Description
57
+ Status: Status
58
+ ManagerStatus: Optional[ManagerStatusResults] = None
59
+
60
+class NodesResponse(BaseModel):
61
+ nodes: List[Node]
62
+ success: bool
63
+ message: str
64
+
backend/app/connectors/portainer/schema/stack.py
new
+45
@@ -0,0 +1,45 @@
1
+from typing import List, Optional, Dict, Any
2
+from pydantic import BaseModel
3
+from datetime import datetime
4
+
5
+
6
+class ResourceControl(BaseModel):
7
+ Id: int
8
+ ResourceId: str
9
+ SubResourceIds: List[str]
10
+ Type: int
11
+ UserAccesses: List[str]
12
+ TeamAccesses: List[str]
13
+ Public: bool
14
+ AdministratorsOnly: bool
15
+ System: bool
16
+
17
+
18
+class StackData(BaseModel):
19
+ Id: int
20
+ Name: str
21
+ Type: int
22
+ EndpointId: int
23
+ SwarmId: str
24
+ EntryPoint: str
25
+ Env: List[Any]
26
+ ResourceControl: ResourceControl
27
+ Status: int
28
+ ProjectPath: str
29
+ CreationDate: int
30
+ CreatedBy: str
31
+ UpdateDate: int
32
+ UpdatedBy: str
33
+ AdditionalFiles: Optional[Any] = None
34
+ AutoUpdate: Optional[Any] = None
35
+ Option: Optional[Any] = None
36
+ GitConfig: Optional[Any] = None
37
+ FromAppTemplate: bool
38
+ Namespace: str
39
+ IsComposeFormat: bool
40
+
41
+
42
+class StackResponse(BaseModel):
43
+ data: StackData
44
+ success: bool
45
+ message: str
backend/app/connectors/portainer/services/nodes.py
new
+17
@@ -0,0 +1,17 @@
1
+from loguru import logger
2
+
3
+from app.connectors.portainer.schema.nodes import NodesResponse
4
+
5
+from app.connectors.portainer.utils.universal import send_get_request, get_endpoint_id
6
+
7
+
8
+async def get_node_details() -> NodesResponse:
9
+ """
10
+ Get the node details from the Portainer API.
11
+ """
12
+ logger.info("Getting swarm node IPs")
13
+ endpoint_id = await get_endpoint_id()
14
+ logger.info(f"Endpoint ID: {endpoint_id}")
15
+ nodes_response = await send_get_request(f"/api/endpoints/{endpoint_id}/docker/nodes")
16
+ # Transform the response to match our simpler model
17
+ return NodesResponse(nodes=nodes_response["data"], success=True, message="Nodes fetched successfully")
backend/app/connectors/portainer/services/stack.py
new
+46
@@ -0,0 +1,46 @@
1
+from loguru import logger
2
+from pathlib import Path
3
+from app.connectors.portainer.utils.universal import send_post_request, get_endpoint_id, get_swarm_id
4
+from app.agents.routes.agents import get_wazuh_manager_version
5
+from app.customer_provisioning.schema.provision import ProvisionNewCustomer
6
+from app.connectors.portainer.schema.stack import StackResponse
7
+
8
+
9
+async def create_wazuh_customer_stack(request: ProvisionNewCustomer) -> StackResponse:
10
+ """
11
+ Create a Wazuh stack.
12
+ """
13
+ logger.info(f"Creating Wazuh stack for customer {request.customer_name}")
14
+ formatted_customer_name = request.customer_name.replace(" ", "_")
15
+ wazuh_manager_version = await get_wazuh_manager_version()
16
+ logger.info(f"Wazuh Manager version: {wazuh_manager_version}")
17
+ # Get the template file from one directory up and under `templates` and the file is `wazuh_worker_stack.yml`
18
+ template_path = Path(__file__).parent.parent / "templates" / "wazuh_worker_stack.yml"
19
+ with open(template_path, "r") as file:
20
+ template = file.read()
21
+
22
+ # Replace the placeholders in the template with the actual values
23
+ template = template.replace("{{ wazuh_worker_customer_code }}", formatted_customer_name)
24
+ template = template.replace("{{ wazuh_manager_version }}", wazuh_manager_version)
25
+ template = template.replace('REPLACE_LOG', request.wazuh_logs_port)
26
+ template = template.replace('REPLACE_REGISTRATION', request.wazuh_registration_port)
27
+ template = template.replace('REPLACE_API', request.wazuh_api_port)
28
+ logger.info(f"Template: {template}")
29
+
30
+ endpoint_id = await get_endpoint_id()
31
+ logger.info(f"Endpoint ID: {endpoint_id}")
32
+ swarm_id = await get_swarm_id()
33
+ logger.info(f"Swarm ID: {swarm_id}")
34
+
35
+ create_stack_url = f"/api/stacks?type=1&method=string&endpointId={endpoint_id}"
36
+ payload = {
37
+ "Name": f"wazuh-worker-{formatted_customer_name}",
38
+ "StackFileContent": template,
39
+ "SwarmID": swarm_id,
40
+ "Env": [],
41
+ }
42
+ response = await send_post_request(endpoint=create_stack_url, data=payload)
43
+ logger.info(f"Response: {response}")
44
+ return StackResponse(**response)
45
+
46
+
backend/app/connectors/portainer/templates/wazuh_worker_stack.yml
new
+61
@@ -0,0 +1,61 @@
1
+# SOCFortress Copyright (C) 2025, SOCFortress LLC. (License GPLv2)
2
+services:
3
+ wazuh-worker-{{ wazuh_worker_customer_code }}:
4
+ image: ghcr.io/socfortress/wazuh-manager:{{ wazuh_manager_version }}
5
+ hostname: wazuh-worker-{{ wazuh_worker_customer_code }}
6
+ restart: always
7
+ # Docker Swarm deployment configuration
8
+ deploy:
9
+ replicas: 1
10
+ # If you want to let Swarm auto-balance, omit constraints:
11
+ # placement:
12
+ # preferences:
13
+ # - spread: node.id
14
+
15
+ ports:
16
+ - "REPLACE_LOG:1514"
17
+ - "REPLACE_REGISTRATION:1515"
18
+ - "REPLACE_API:55000"
19
+ volumes:
20
+ # common
21
+ - worker-wazuh-{{ wazuh_worker_customer_code }}-api-configuration:/var/ossec/api/configuration
22
+ - worker-wazuh-{{ wazuh_worker_customer_code }}-etc:/var/ossec/etc
23
+ - worker-wazuh-{{ wazuh_worker_customer_code }}-logs:/var/ossec/logs
24
+ - worker-wazuh-{{ wazuh_worker_customer_code }}-queue:/var/ossec/queue
25
+ - worker-wazuh-{{ wazuh_worker_customer_code }}-var-multigroups:/var/ossec/var/multigroups
26
+ - worker-wazuh-{{ wazuh_worker_customer_code }}-integrations:/var/ossec/integrations
27
+ - worker-wazuh-{{ wazuh_worker_customer_code }}-active-response:/var/ossec/active-response/bin
28
+ - worker-wazuh-{{ wazuh_worker_customer_code }}-agentless:/var/ossec/agentless
29
+ - worker-wazuh-{{ wazuh_worker_customer_code }}-wodles:/var/ossec/wodles
30
+ - /wazuh-workers/client/data/{{ wazuh_worker_customer_code }}/wazuh_worker.conf:/wazuh-config-mount/etc/ossec.conf
31
+ - /wazuh-workers/client/data/{{ wazuh_worker_customer_code }}/authd.pass:/wazuh-config-mount/etc/authd.pass
32
+ - /wazuh-workers/client/data/{{ wazuh_worker_customer_code }}/manager.pem:/wazuh-config-mount/etc/manager.pem
33
+ - /wazuh-workers/client/data/{{ wazuh_worker_customer_code }}/manager-key.pem:/wazuh-config-mount/etc/manager-key.pem
34
+ - /wazuh-workers/client/data/{{ wazuh_worker_customer_code }}/root-ca.pem:/wazuh-config-mount/etc/root-ca.pem
35
+ # client specific
36
+ healthcheck:
37
+ test:
38
+ [
39
+ "CMD",
40
+ "bash",
41
+ "-c",
42
+ "/var/ossec/bin/wazuh-control status | grep -q 'wazuh-apid is running' && /var/ossec/bin/wazuh-control status | grep -q 'wazuh-clusterd is running' && /var/ossec/bin/wazuh-control status | grep -q 'wazuh-modulesd is running' && /var/ossec/bin/wazuh-control status | grep -q 'wazuh-monitord is running' && /var/ossec/bin/wazuh-control status | grep -q 'wazuh-logcollector is running' && /var/ossec/bin/wazuh-control status | grep -q 'wazuh-remoted is running' && /var/ossec/bin/wazuh-control status | grep -q 'wazuh-syscheckd is running' && /var/ossec/bin/wazuh-control status | grep -q 'wazuh-analysisd is running' && /var/ossec/bin/wazuh-control status | grep -q 'wazuh-execd is running' && /var/ossec/bin/wazuh-control status | grep -q 'wazuh-db is running' && /var/ossec/bin/wazuh-control status | grep -q 'wazuh-authd is running' || /bin/true",
43
+ ]
44
+ interval: 30s
45
+ retries: 3
46
+
47
+volumes:
48
+ worker-wazuh-{{ wazuh_worker_customer_code }}-api-configuration:
49
+ worker-wazuh-{{ wazuh_worker_customer_code }}-etc:
50
+ worker-wazuh-{{ wazuh_worker_customer_code }}-logs:
51
+ driver: local
52
+ driver_opts:
53
+ type: "none"
54
+ o: "bind"
55
+ device: "/wazuh-workers/client/data/{{ wazuh_worker_customer_code }}/logs"
56
+ worker-wazuh-{{ wazuh_worker_customer_code }}-queue:
57
+ worker-wazuh-{{ wazuh_worker_customer_code }}-var-multigroups:
58
+ worker-wazuh-{{ wazuh_worker_customer_code }}-integrations:
59
+ worker-wazuh-{{ wazuh_worker_customer_code }}-active-response:
60
+ worker-wazuh-{{ wazuh_worker_customer_code }}-agentless:
61
+ worker-wazuh-{{ wazuh_worker_customer_code }}-wodles:
backend/app/connectors/portainer/utils/universal.py
new
+373
@@ -0,0 +1,373 @@
1
+from typing import Any
2
+from typing import Dict
3
+from typing import Optional
4
+
5
+import requests
6
+from fastapi import HTTPException
7
+from loguru import logger
8
+from urllib.parse import urljoin
9
+from app.connectors.utils import get_connector_info_from_db
10
+from app.db.db_session import get_db_session
11
+
12
+
13
+async def get_endpoint_id() -> int:
14
+ """
15
+ Returns the ID of the endpoint.
16
+ """
17
+ logger.info("Getting endpoint ID")
18
+ list_endpoints = await send_get_request("/api/endpoints")
19
+ logger.info(f"List of endpoints: {list_endpoints}")
20
+
21
+ for endpoint in list_endpoints["data"]:
22
+ if endpoint["Name"] == "local":
23
+ # Convert the ID to an integer
24
+ return int(endpoint["Id"])
25
+ return None
26
+
27
+async def get_swarm_id() -> int:
28
+ """
29
+ Returns the ID of the swarm.
30
+ """
31
+ logger.info("Getting swarm ID")
32
+ endpoint_id = await get_endpoint_id()
33
+ logger.info(f"Endpoint ID: {endpoint_id}")
34
+ swarm_id = await send_get_request(f"/api/endpoints/{endpoint_id}/docker/swarm")
35
+ logger.info(f"Swarm ID: {swarm_id}")
36
+ return swarm_id["data"]["ID"]
37
+
38
+
39
+async def get_portainer_jwt() -> str:
40
+ """Get JWT token from Portainer API."""
41
+ logger.info("Getting portainer authentication token")
42
+ async with get_db_session() as session: # This will correctly enter the context manager
43
+ attributes = await get_connector_info_from_db('Portainer', session)
44
+ logger.info(f"Attributes: {attributes}")
45
+ try:
46
+ auth_endpoint = urljoin(attributes["connector_url"], "/api/auth")
47
+
48
+ auth_payload = {
49
+ "username": attributes["connector_username"],
50
+ "password": attributes["connector_password"]
51
+ }
52
+
53
+ response = requests.post(
54
+ auth_endpoint,
55
+ json=auth_payload,
56
+ verify=False # If using self-signed cert
57
+ )
58
+
59
+ response.raise_for_status()
60
+ # The JWT token is in response.json()["jwt"]
61
+ jwt_token = response.json()["jwt"]
62
+ logger.info(f"Authenticated with Portainer, obtained JWT: {jwt_token}")
63
+ return jwt_token
64
+
65
+ except requests.exceptions.RequestException as e:
66
+ error_msg = f"Failed to authenticate with Portainer: {str(e)}"
67
+ logger.error(error_msg)
68
+ raise HTTPException(status_code=500, detail=error_msg)
69
+
70
+
71
+async def verify_portainer_credentials(attributes: Dict[str, Any]) -> Dict[str, Any]:
72
+ """
73
+ Verifies the connection to portainer service by attempting both API key and JWT authentication.
74
+
75
+ Returns:
76
+ dict: A dictionary containing 'connectionSuccessful' status and authentication details.
77
+ """
78
+ logger.info(
79
+ f"Verifying the portainer connection to {attributes['connector_url']}",
80
+ )
81
+ try:
82
+ # First try API key authentication
83
+ headers = {
84
+ "Authorization": f"Bearer {attributes['connector_api_key']}",
85
+ }
86
+ portainer_apps = requests.get(
87
+ f"{attributes['connector_url']}/api/v1/apps/authentication",
88
+ headers=headers,
89
+ verify=False,
90
+ )
91
+
92
+ # If API key auth fails, try JWT authentication
93
+ if portainer_apps.status_code != 200:
94
+ auth_endpoint = urljoin(attributes["connector_url"], "/api/auth")
95
+ auth_payload = {
96
+ "username": attributes["connector_username"],
97
+ "password": attributes["connector_password"]
98
+ }
99
+
100
+ jwt_response = requests.post(
101
+ auth_endpoint,
102
+ json=auth_payload,
103
+ verify=False
104
+ )
105
+
106
+ if jwt_response.status_code == 200:
107
+ jwt_token = jwt_response.json()["jwt"]
108
+ logger.info("JWT authentication successful")
109
+ return {
110
+ "connectionSuccessful": True,
111
+ "message": "Portainer connection successful via JWT",
112
+ "authMethod": "jwt",
113
+ "jwt": jwt_token
114
+ }
115
+ else:
116
+ logger.error(
117
+ f"Both API key and JWT authentication failed. JWT error: {jwt_response.text}"
118
+ )
119
+ return {
120
+ "connectionSuccessful": False,
121
+ "message": "Both API key and JWT authentication failed",
122
+ "authMethod": None
123
+ }
124
+
125
+ logger.info(
126
+ f"Connection to {attributes['connector_url']} successful via API key",
127
+ )
128
+ return {
129
+ "connectionSuccessful": True,
130
+ "message": "Portainer connection successful via API key",
131
+ "authMethod": "api_key"
132
+ }
133
+
134
+ except Exception as e:
135
+ logger.error(
136
+ f"Connection to {attributes['connector_url']} failed with error: {e}",
137
+ )
138
+ return {
139
+ "connectionSuccessful": False,
140
+ "message": f"Connection to {attributes['connector_url']} failed with error: {e}",
141
+ "authMethod": None
142
+ }
143
+
144
+
145
+async def verify_portainer_connection(connector_name: str) -> str:
146
+ """
147
+ Returns if connection to portainer service is successful.
148
+ """
149
+ logger.info("Getting portainer authentication token")
150
+ async with get_db_session() as session: # This will correctly enter the context manager
151
+ attributes = await get_connector_info_from_db(connector_name, session)
152
+ if attributes is None:
153
+ logger.error("No portainer connector found in the database")
154
+ return None
155
+ return await verify_portainer_credentials(attributes)
156
+
157
+
158
+async def send_get_request(
159
+ endpoint: str,
160
+ params: Optional[Dict[str, Any]] = None,
161
+ connector_name: str = "Portainer",
162
+) -> Dict[str, Any]:
163
+ """
164
+ Sends a GET request to the portainer service.
165
+
166
+ Args:
167
+ endpoint (str): The endpoint to send the GET request to.
168
+ params (Optional[Dict[str, Any]], optional): The parameters to send with the GET request. Defaults to None.
169
+ connector_name (str, optional): The name of the connector to use. Defaults to "portainer".
170
+
171
+ Returns:
172
+ Dict[str, Any]: The response from the GET request.
173
+ """
174
+ logger.info(f"Sending GET request to {endpoint}")
175
+ async with get_db_session() as session: # This will correctly enter the context manager
176
+ attributes = await get_connector_info_from_db(connector_name, session)
177
+ if attributes is None:
178
+ logger.error("No portainer connector found in the database")
179
+ return None
180
+ logger.info(f"Attributes: {attributes}")
181
+ jwt_token = await get_portainer_jwt()
182
+ logger.info(f"JWT token: {jwt_token}")
183
+ try:
184
+ HEADERS = {
185
+ "Authorization": f"Bearer {jwt_token}",
186
+ "Content-Type": "application/json",
187
+ }
188
+ response = requests.get(
189
+ f"{attributes['connector_url']}{endpoint}",
190
+ headers=HEADERS,
191
+ params=params,
192
+ verify=False,
193
+ )
194
+ logger.info(f"Response from portainer API: {response.json()}")
195
+ return {
196
+ "data": response.json(),
197
+ "success": True,
198
+ "message": "Successfully retrieved data",
199
+ }
200
+ except Exception as e:
201
+ logger.error(f"Failed to send GET request to {endpoint} with error: {e}")
202
+ raise HTTPException(
203
+ status_code=500,
204
+ detail=f"Failed to send GET request to {endpoint} with error: {e}",
205
+ )
206
+ return {
207
+ "success": False,
208
+ "message": f"Failed to send GET request to {endpoint} with error: {e}",
209
+ }
210
+
211
+
212
+async def send_post_request(
213
+ endpoint: str,
214
+ data: Dict[str, Any] = None,
215
+ connector_name: str = "Portainer",
216
+) -> Dict[str, Any]:
217
+ """
218
+ Sends a POST request to the portainer service.
219
+
220
+ Args:
221
+ endpoint (str): The endpoint to send the POST request to.
222
+ data (Dict[str, Any]): The data to send with the POST request.
223
+ connector_name (str, optional): The name of the connector to use. Defaults to "portainer".
224
+
225
+ Returns:
226
+ Dict[str, Any]: The response from the POST request.
227
+ """
228
+ logger.info(f"Sending POST request to {endpoint}")
229
+ async with get_db_session() as session: # This will correctly enter the context manager
230
+ attributes = await get_connector_info_from_db(connector_name, session)
231
+ if attributes is None:
232
+ logger.error("No portainer connector found in the database")
233
+ return None
234
+ logger.info(f"Attributes: {attributes}")
235
+ jwt_token = await get_portainer_jwt()
236
+ logger.info(f"JWT token: {jwt_token}")
237
+
238
+ try:
239
+ HEADERS = {
240
+ "Authorization": f"Bearer {jwt_token}",
241
+ "Content-Type": "application/json",
242
+ }
243
+ logger.info(f"Sending POST request to {attributes['connector_url']}{endpoint}")
244
+ response = requests.post(
245
+ f"{attributes['connector_url']}{endpoint}",
246
+ headers=HEADERS,
247
+ json=data,
248
+ verify=False,
249
+ )
250
+
251
+ if response.status_code == 204:
252
+ return {
253
+ "data": None,
254
+ "success": True,
255
+ "message": "Successfully completed request with no content",
256
+ }
257
+ else:
258
+ return {
259
+ "data": response.json(),
260
+ "success": False if response.status_code >= 400 else True,
261
+ "message": "Successfully retrieved data" if response.status_code < 400 else "Failed to retrieve data",
262
+ }
263
+ except Exception as e:
264
+ logger.debug(f"Response: {response}")
265
+ logger.error(f"Failed to send POST request to {endpoint} with error: {e}")
266
+ raise HTTPException(
267
+ status_code=500,
268
+ detail=f"Failed to send POST request to {endpoint} with error: {e}",
269
+ )
270
+ return {
271
+ "success": False,
272
+ "message": f"Failed to send POST request to {endpoint} with error: {e}",
273
+ }
274
+
275
+
276
+def send_delete_request(
277
+ endpoint: str,
278
+ params: Optional[Dict[str, Any]] = None,
279
+ connector_name: str = "portainer",
280
+) -> Dict[str, Any]:
281
+ """
282
+ Sends a DELETE request to the portainer service.
283
+
284
+ Args:
285
+ endpoint (str): The endpoint to send the DELETE request to.
286
+ params (Optional[Dict[str, Any]], optional): The parameters to send with the DELETE request. Defaults to None.
287
+ connector_name (str, optional): The name of the connector to use. Defaults to "portainer".
288
+
289
+ Returns:
290
+ Dict[str, Any]: The response from the DELETE request.
291
+ """
292
+ logger.info(f"Sending DELETE request to {endpoint}")
293
+ attributes = get_connector_info_from_db(connector_name)
294
+ if attributes is None:
295
+ logger.error("No portainer connector found in the database")
296
+ return None
297
+ try:
298
+ HEADERS = {
299
+ "Authorization": f"Bearer {attributes['connector_api_key']}",
300
+ }
301
+ response = requests.delete(
302
+ f"{attributes['connector_url']}{endpoint}",
303
+ headers=HEADERS,
304
+ auth=(
305
+ attributes["connector_username"],
306
+ attributes["connector_password"],
307
+ ),
308
+ params=params,
309
+ verify=False,
310
+ )
311
+ return {
312
+ "data": response.json(),
313
+ "success": True,
314
+ "message": "Successfully retrieved data",
315
+ }
316
+ except Exception as e:
317
+ logger.error(f"Failed to send DELETE request to {endpoint} with error: {e}")
318
+ raise HTTPException(
319
+ status_code=500,
320
+ detail=f"Failed to send DELETE request to {endpoint} with error: {e}",
321
+ )
322
+ return {
323
+ "success": False,
324
+ "message": f"Failed to send DELETE request to {endpoint} with error: {e}",
325
+ }
326
+
327
+
328
+def send_put_request(
329
+ endpoint: str,
330
+ data: Optional[Dict[str, Any]] = None,
331
+ connector_name: str = "portainer",
332
+) -> Dict[str, Any]:
333
+ """
334
+ Sends a PUT request to the portainer service.
335
+
336
+ Args:
337
+ endpoint (str): The endpoint to send the PUT request to.
338
+ data (Optional[Dict[str, Any]]): The data to send with the PUT request.
339
+ connector_name (str, optional): The name of the connector to use. Defaults to "portainer".
340
+
341
+ Returns:
342
+ Dict[str, Any]: The response from the PUT request.
343
+ """
344
+ logger.info(f"Sending PUT request to {endpoint}")
345
+ attributes = get_connector_info_from_db(connector_name)
346
+ if attributes is None:
347
+ logger.error("No portainer connector found in the database")
348
+ return None
349
+ try:
350
+ HEADERS = {
351
+ "Authorization": f"Bearer {attributes['connector_api_key']}",
352
+ }
353
+ response = requests.put(
354
+ f"{attributes['connector_url']}{endpoint}",
355
+ headers=HEADERS,
356
+ auth=(
357
+ attributes["connector_username"],
358
+ attributes["connector_password"],
359
+ ),
360
+ json=data,
361
+ verify=False,
362
+ )
363
+ return {
364
+ "data": response.json(),
365
+ "success": True,
366
+ "message": "Successfully retrieved data",
367
+ }
368
+ except Exception as e:
369
+ logger.error(f"Failed to send PUT request to {endpoint} with error: {e}")
370
+ raise HTTPException(
371
+ status_code=500,
372
+ detail=f"Failed to send PUT request to {endpoint} with error: {e}",
373
+ )
backend/app/connectors/services.py
+10
@@ -17,6 +17,7 @@ from app.connectors.cortex.utils.universal import verify_cortex_connection
17
from app.connectors.grafana.utils.universal import verify_grafana_connection
18
from app.connectors.graylog.utils.universal import verify_graylog_connection
19
from app.connectors.influxdb.utils.universal import verify_influxdb_connection
20
+from app.connectors.portainer.utils.universal import verify_portainer_connection
21
from app.connectors.models import Connectors
22
from app.connectors.schema import ConnectorResponse
23
from app.connectors.shuffle.utils.universal import verify_shuffle_connection
@@ -178,6 +179,14 @@ class VirustotalService(ConnectorServiceInterface):
179
) -> Optional[ConnectorResponse]:
180
return await verify_virustotal_connection(connector.connector_name)
181
182
+# Portainer Service
183
+class PortainerService(ConnectorServiceInterface):
184
+ async def verify_authentication(
185
+ self,
186
+ connector: ConnectorResponse,
187
+ ) -> Optional[ConnectorResponse]:
188
+ return await verify_portainer_connection(connector.connector_name)
189
+
190
191
# Factory function to create a service instance based on connector name
192
def get_connector_service(connector_name: str) -> Type[ConnectorServiceInterface]:
@@ -205,6 +214,7 @@ def get_connector_service(connector_name: str) -> Type[ConnectorServiceInterface
214
"Event Shipper": EventShipperService,
215
"Alert Creation Provisioning": AlertCreationService,
216
"VirusTotal": VirustotalService,
217
+ "Portainer": PortainerService,
218
}
219
return service_map.get(connector_name, None)
220
backend/app/connectors/shuffle/utils/universal.py
+2
-1
@@ -90,6 +90,7 @@ async def send_get_request(
90
if attributes is None:
91
logger.error("No Shuffle connector found in the database")
92
return None
93
+ logger.info(f"Attributes: {attributes}")
94
try:
95
HEADERS = {
96
"Authorization": f"Bearer {attributes['connector_api_key']}",
@@ -100,6 +101,7 @@ async def send_get_request(
101
params=params,
102
verify=False,
103
)
104
+ logger.info(f"Response from Shuffle API: {response.json()}")
105
return {
106
"data": response.json(),
107
"success": True,
@@ -151,7 +153,6 @@ async def send_post_request(
153
json=data,
154
verify=False,
155
)
154
- logger.info(f"Response from Shuffle API: {response.json()}")
156
157
if response.status_code == 204:
158
return {
backend/app/customer_provisioning/templates/linux_agent.conf
+1
@@ -65,6 +65,7 @@
65
<ignore>/sys/kernel/security</ignore>
66
<ignore>/sys/kernel/debug</ignore>
67
<ignore>/opt/CoPilot</ignore>
68
+ <ignore>/opt/velociraptor</ignore>
69
<!-- File types to ignore -->
70
<ignore type="sregex">.log$|.swp$</ignore>
71
<!-- Check the file, but never compute the diff -->
backend/app/db/db_populate.py
+2
@@ -116,6 +116,7 @@ def get_connectors_list():
116
# "Connection to Cortex. Make sure you have created an API key.",
117
# ),
118
("Grafana", "3", "username_password", "Connection to Grafana."),
119
+ # ! TODO - LOOK TO REMOVE WAZUH WORKER PROVISIONING FROM CONNECTORS LIST ! #
120
(
121
"Wazuh Worker Provisioning",
122
"3",
@@ -149,6 +150,7 @@ def get_connectors_list():
150
"api_key",
151
"Connection to VirusTotal. Make sure you have created an API key.",
152
),
153
+ ("Portainer", "3", "username_password", "Connection to Portainer.", "PORTAINER_ENDPOINT_ID"),
154
# ... Add more connectors as needed ...
155
]
156
backend/app/routers/portainer.py
new
+13
@@ -0,0 +1,13 @@
1
+from fastapi import APIRouter
2
+
3
+from app.connectors.portainer.routes.portainer import portainer_integrations_router
4
+
5
+# Instantiate the APIRouter
6
+router = APIRouter()
7
+
8
+# Include the portainer related routes
9
+router.include_router(
10
+ portainer_integrations_router,
11
+ prefix="/portainer",
12
+ tags=["portainer"],
13
+)
backend/copilot.py
+2
@@ -62,6 +62,7 @@ from app.routers import network_connectors
62
from app.routers import nuclei
63
from app.routers import office365
64
from app.routers import sap_siem
65
+from app.routers import portainer
66
from app.routers import scheduler
67
from app.routers import scoutsuite
68
from app.routers import shuffle
@@ -158,6 +159,7 @@ api_router.include_router(bitdefender.router)
159
api_router.include_router(scoutsuite.router)
160
api_router.include_router(nuclei.router)
161
api_router.include_router(duo.router)
162
+api_router.include_router(portainer.router)
163
api_router.include_router(incidents.router)
164
api_router.include_router(darktrace.router)
165
frontend/public/images/connectors/portainer.svg
new
+16
@@ -0,0 +1,16 @@
1
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128px" height="128px" fill="#3BBCED">
2
+ <path d="M49.32,29.44h-1.88v9.75h1.88V29.44z M57.41,29.44h-1.88v9.75h1.88V29.44z M68.47,10.84L66.54,7.5L34.36,26.1
3
+ l1.93,3.34L68.47,10.84z" />
4
+ <path d="M68.28,10.84l1.93-3.34l32.18,18.6l-1.93,3.34L68.28,10.84z" />
5
+ <path d="M108.08,29.49v-3.86H19.92v3.86H108.08z" />
6
+ <path d="M73.17,84.01V26.8h3.86v59.98C76,85.61,74.68,84.76,73.17,84.01z M66.59,83.02V2.32h3.86v81.17
7
+ C69.36,82.97,66.73,83.02,66.59,83.02z M30.04,91.07c-4.7-3.48-7.81-9.04-7.81-15.35c0-3.34,0.89-6.64,2.54-9.51H57.7
8
+ c1.69,2.87,2.54,6.17,2.54,9.51c0,2.92-0.38,5.65-1.55,8.1c-2.49-2.4-6.16-3.44-9.88-3.44c-6.59,0-12.23,4.1-13.69,10.22
9
+ c-0.52-0.05-0.85-0.09-1.36-0.09C32.48,90.55,31.26,90.74,30.04,91.07L30.04,91.07z" />
10
+ <path d="M46.22,43.52h-9.97v10.03h9.97V43.52z M34.97,43.52H25v10.03h9.97V43.52z M34.97,54.68H25v10.03h9.97V54.68z
11
+ M46.22,54.68h-9.97v10.03h9.97V54.68z M57.41,54.68h-9.97v10.03h9.97V54.68z M57.41,38.15h-9.97v10.03h9.97V38.15z" />
12
+ <path d="M36.76,92.58c1.36-5.79,6.59-10.12,12.8-10.12c4,0,7.57,1.79,10.02,4.62c2.12-1.46,4.66-2.31,7.43-2.31
13
+ c7.24,0,13.12,5.88,13.12,13.14c0,1.51-0.23,2.92-0.7,4.28c1.6,2.17,2.59,4.9,2.59,7.82c0,7.25-5.88,13.14-13.13,13.14
14
+ c-3.2,0-6.12-1.13-8.37-3.01c-2.4,3.34-6.3,5.56-10.73,5.56c-5.08,0-9.5-2.92-11.71-7.16c-0.89,0.19-1.79,0.28-2.73,0.28
15
+ c-7.24,0-13.17-5.89-13.17-13.14s5.88-13.14,13.17-13.14C35.82,92.49,36.29,92.49,36.76,92.58L36.76,92.58z" />
16
+</svg>