@cryptotaxi247 / CoPilot / commits / 52762d3d

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>