main
py 444 lines 18.9 KB
Raw
1 from typing import Callable
2
3 import requests
4 from fastapi import HTTPException
5 from loguru import logger
6 from sqlalchemy import update
7 from sqlalchemy.ext.asyncio import AsyncSession
8 from sqlalchemy.future import select
9
10 # from app.agents.routes.agents import check_wazuh_manager_version
11 from app.agents.routes.agents import get_wazuh_manager_version
12 from app.connectors.grafana.schema.dashboards import DashboardProvisionRequest
13 from app.connectors.grafana.services.dashboards import provision_dashboards
14 from app.connectors.grafana.utils.universal import verify_grafana_connection
15 from app.connectors.graylog.services.management import start_stream
16 from app.connectors.graylog.utils.universal import verify_graylog_connection
17 from app.connectors.portainer.services.stack import create_wazuh_customer_stack
18 from app.connectors.utils import is_connector_verified
19 from app.connectors.wazuh_manager.utils.universal import verify_wazuh_manager_connection
20 from app.customer_provisioning.schema.graylog import StreamConnectionToPipelineRequest
21 from app.customer_provisioning.schema.provision import CustomerProvisionMeta
22 from app.customer_provisioning.schema.provision import CustomerProvisionResponse
23 from app.customer_provisioning.schema.provision import ProvisionHaProxyRequest
24 from app.customer_provisioning.schema.provision import ProvisionNewCustomer
25 from app.customer_provisioning.schema.wazuh_worker import ProvisionWorkerRequest
26 from app.customer_provisioning.schema.wazuh_worker import ProvisionWorkerResponse
27 from app.customer_provisioning.services.grafana import create_grafana_datasource
28 from app.customer_provisioning.services.grafana import create_grafana_folder
29 from app.customer_provisioning.services.grafana import create_grafana_organization
30
31 # from app.customer_provisioning.services.grafana import create_vulnerability_datasource
32 from app.customer_provisioning.services.graylog import connect_stream_to_pipeline
33 from app.customer_provisioning.services.graylog import create_event_stream
34 from app.customer_provisioning.services.graylog import create_index_set
35 from app.customer_provisioning.services.graylog import get_pipeline_id
36 from app.customer_provisioning.services.portainer import list_node_ips
37 from app.customer_provisioning.services.wazuh_manager import apply_group_configurations
38 from app.customer_provisioning.services.wazuh_manager import create_wazuh_groups
39 from app.db.universal_models import CustomersMeta
40 from app.integrations.alert_creation_settings.models.alert_creation_settings import (
41 AlertCreationSettings,
42 )
43 from app.utils import get_connector_attribute
44
45
46 async def verify_connection(service_name: str, verify_connection_func: Callable) -> None:
47 connection = await verify_connection_func(service_name)
48 if connection["connectionSuccessful"] is False:
49 raise HTTPException(
50 status_code=500,
51 detail=f"Failed to connect to {service_name}. {service_name} connection must be established to proceed.",
52 )
53
54
55 async def verify_required_tools() -> None:
56 """
57 Verify the required tools for customer provisioning.
58 """
59 logger.info("Verifying required tools")
60 await verify_connection("Graylog", verify_graylog_connection)
61 await verify_connection("Wazuh-Manager", verify_wazuh_manager_connection)
62 await verify_connection("Grafana", verify_grafana_connection)
63
64
65 # ! MAIN FUNCTION ! #
66 async def provision_wazuh_customer(
67 request: ProvisionNewCustomer,
68 session: AsyncSession,
69 ) -> CustomerProvisionResponse:
70 """
71 This function is the main function for provisioning a new customer for their Wazuh instance.
72 It will call all the other functions to provision the customer.
73
74 Args:
75 request (ProvisionNewCustomer): The request body from the API endpoint
76 session (AsyncSession): The database session
77
78 Raises:
79 HTTPException: If the stream fails to start
80
81 Returns:
82 CustomerProvisionResponse: The response object containing the provisioned customer's information
83 """
84 await verify_required_tools()
85 logger.info(f"Provisioning new customer {request}")
86 # Initialize an empty dictionary to store the meta data
87 provision_meta_data = {}
88 provision_meta_data["pipeline_ids"] = await get_pipeline_id(subscription="Wazuh")
89 provision_meta_data["index_set_id"] = (await create_index_set(request)).data.id
90 provision_meta_data["stream_id"] = (await create_event_stream(request, provision_meta_data["index_set_id"])).data.stream_id
91 stream_and_pipeline = StreamConnectionToPipelineRequest(
92 stream_id=provision_meta_data["stream_id"],
93 pipeline_ids=provision_meta_data["pipeline_ids"],
94 )
95 await connect_stream_to_pipeline(stream_and_pipeline)
96 if await start_stream(stream_id=provision_meta_data["stream_id"]) is False:
97 raise HTTPException(
98 status_code=500,
99 detail=f"Failed to start stream {provision_meta_data['stream_id']}",
100 )
101 await create_wazuh_groups(request)
102 await apply_group_configurations(request)
103 provision_meta_data["grafana_organization_id"] = (await create_grafana_organization(request)).orgId
104 provision_meta_data["wazuh_datasource_uid"] = (
105 await create_grafana_datasource(
106 request=request,
107 organization_id=provision_meta_data["grafana_organization_id"],
108 session=session,
109 )
110 ).datasource.uid
111 # ! CREATE THE VULNERABILITY DATASOURCE IF WAZUH VERSION 4.8.0 OR HIGHER ! #
112 # ! Commenting out because we use CoPilot dashboard for Vulns Now ! #
113 # if await check_wazuh_manager_version() is True:
114 # logger.info("Creating vulnerability datasource since Wazuh version is 4.8.0 or higher")
115 # await create_vulnerability_datasource(
116 # request=request,
117 # organization_id=provision_meta_data["grafana_organization_id"],
118 # session=session,
119 # )
120 logger.info("Creating EDR folder and dashboards")
121 provision_meta_data["grafana_edr_folder_id"] = (
122 await create_grafana_folder(
123 organization_id=provision_meta_data["grafana_organization_id"],
124 folder_title="EDR",
125 )
126 ).id
127 await provision_dashboards(
128 DashboardProvisionRequest(
129 dashboards=request.dashboards_to_include.dashboards,
130 organizationId=provision_meta_data["grafana_organization_id"],
131 folderId=provision_meta_data["grafana_edr_folder_id"],
132 datasourceUid=provision_meta_data["wazuh_datasource_uid"],
133 grafana_url=request.grafana_url,
134 ),
135 )
136
137 customer_provision_meta = CustomerProvisionMeta(**provision_meta_data)
138 customer_meta = await update_customer_meta_table(
139 request,
140 customer_provision_meta,
141 session,
142 )
143 await update_customer_alert_settings_table(
144 request,
145 customer_provision_meta,
146 session,
147 )
148
149 if request.provision_wazuh_worker is True:
150 provision_worker = await provision_wazuh_worker(
151 ProvisionWorkerRequest(
152 customer_name=request.customer_name,
153 customer_code=request.customer_code,
154 wazuh_auth_password=request.wazuh_auth_password,
155 wazuh_registration_port=request.wazuh_registration_port,
156 wazuh_logs_port=request.wazuh_logs_port,
157 wazuh_api_port=request.wazuh_api_port,
158 wazuh_cluster_name=request.wazuh_cluster_name,
159 wazuh_cluster_key=request.wazuh_cluster_key,
160 wazuh_master_ip=request.wazuh_master_ip,
161 ),
162 session,
163 )
164
165 if provision_worker.success is False:
166 return CustomerProvisionResponse(
167 message=f"Customer {request.customer_name} provisioned successfully, but the Wazuh worker failed to provision",
168 success=True,
169 customer_meta=customer_meta.model_dump(),
170 wazuh_worker_provisioned=False,
171 )
172
173 if request.provision_ha_proxy is True:
174 provsion_haproxy = await provision_haproxy(
175 ProvisionHaProxyRequest(
176 customer_name=request.customer_name,
177 wazuh_registration_port=request.wazuh_registration_port,
178 wazuh_logs_port=request.wazuh_logs_port,
179 wazuh_worker_hostname=request.wazuh_worker_hostname,
180 ),
181 session,
182 )
183
184 if provsion_haproxy.success is False:
185 return CustomerProvisionResponse(
186 message=f"Customer {request.customer_name} provisioned successfully, but the HAProxy failed to provision",
187 success=True,
188 customer_meta=customer_meta.model_dump(),
189 wazuh_worker_provisioned=True,
190 )
191
192 return CustomerProvisionResponse(
193 message=f"Customer {request.customer_name} provisioned successfully",
194 success=True,
195 customer_meta=customer_meta.model_dump(),
196 wazuh_worker_provisioned=True,
197 )
198
199
200 ######### ! Update CustomerMeta Table ! ############
201 async def update_customer_meta_table(
202 request: ProvisionNewCustomer,
203 customer_meta: CustomerProvisionMeta,
204 session: AsyncSession,
205 ):
206 """
207 Update the customer meta table with the provided information.
208
209 Args:
210 request (ProvisionNewCustomer): The request object containing customer information.
211 customer_meta (CustomerProvisionMeta): The customer meta object containing additional information.
212 session (AsyncSession): The database session.
213
214 Returns:
215 CustomerProvisionMeta: The updated customer meta object.
216 """
217 logger.info(f"Updating customer meta table for customer {request.customer_name}")
218 customer_meta = CustomersMeta(
219 customer_code=request.customer_code,
220 customer_name=request.customer_name,
221 customer_meta_graylog_index=customer_meta.index_set_id,
222 customer_meta_graylog_stream=customer_meta.stream_id,
223 customer_meta_grafana_org_id=customer_meta.grafana_organization_id,
224 customer_meta_wazuh_group=request.customer_code,
225 customer_meta_index_retention=str(request.hot_data_retention),
226 customer_meta_wazuh_registration_port=request.wazuh_registration_port,
227 customer_meta_wazuh_log_ingestion_port=request.wazuh_logs_port,
228 customer_meta_wazuh_api_port=request.wazuh_api_port,
229 customer_meta_wazuh_auth_password=request.wazuh_auth_password,
230 customer_meta_iris_customer_id=customer_meta.iris_customer_id,
231 )
232 session.add(customer_meta)
233 await session.commit()
234 return customer_meta
235
236
237 async def update_customer_portainer_stack_id(
238 customer_name: str,
239 stack_id: int,
240 session: AsyncSession,
241 ) -> None:
242 """
243 Update the customer's Portainer stack ID in the CustomersMeta table.
244
245 Args:
246 customer_name (str): The name of the customer
247 stack_id (int): The Portainer stack ID
248 session (AsyncSession): The database session
249 """
250 logger.info(f"Updating Portainer stack ID {stack_id} for customer {customer_name}")
251
252 # Find the customer record
253 stmt = select(CustomersMeta).where(CustomersMeta.customer_name == customer_name)
254 result = await session.execute(stmt)
255 customer = result.scalar_one_or_none()
256
257 if customer:
258 # Update the customer's Portainer stack ID
259 stmt = update(CustomersMeta).where(CustomersMeta.customer_name == customer_name).values(customer_meta_portainer_stack_id=stack_id)
260 await session.execute(stmt)
261 await session.commit()
262 logger.info(f"Successfully updated Portainer stack ID for customer {customer_name}")
263 else:
264 logger.error(f"Customer {customer_name} not found in database")
265 raise HTTPException(status_code=404, detail=f"Customer {customer_name} not found in database")
266
267
268 ######### ! Update Customer Alert Settings Table ! ############
269 async def update_customer_alert_settings_table(
270 request: ProvisionNewCustomer,
271 customer_meta: CustomerProvisionMeta,
272 session: AsyncSession,
273 ):
274 """
275 Update the customer alert settings table with the provided information.
276
277 Args:
278 request (ProvisionNewCustomer): The request object containing customer information.
279 customer_meta (CustomerProvisionMeta): The customer meta object containing additional information.
280 session (AsyncSession): The database session.
281
282 Returns:
283 AlertCreationSettings: The updated customer meta object.
284 """
285 logger.info(
286 f"Updating customer alert settings table for customer {request.customer_name}",
287 )
288 customer_alert_settings = AlertCreationSettings(
289 customer_code=request.customer_code,
290 customer_name=request.customer_name,
291 timefield="timestamp_utc",
292 iris_customer_id=customer_meta.iris_customer_id,
293 iris_customer_name=request.customer_name,
294 iris_index=f'dfir_iris_{request.customer_name.lower().replace(" ", "_")}',
295 grafana_url=request.grafana_url,
296 custom_message="Open In SOCFortress",
297 nvd_url="https://services.nvd.nist.gov/rest/json/cves/2.0?cveId",
298 )
299 session.add(customer_alert_settings)
300 await session.commit()
301 return customer_alert_settings
302
303
304 ######### ! Provision Wazuh Worker ! ############
305 async def provision_wazuh_worker(
306 request: ProvisionWorkerRequest,
307 session: AsyncSession,
308 ) -> ProvisionWorkerResponse:
309 """
310 Provisions a Wazuh worker. https://github.com/socfortress/Customer-Provisioning-Worker
311 This can be done either by directly invoking the worker or by invoking the worker via Portainer.
312
313 Args:
314 request (ProvisionWorkerRequest): The request object containing the necessary information for provisioning.
315 session (AsyncSession): The async session object for making HTTP requests.
316
317 Returns:
318 ProvisionWorkerResponse: The response object indicating the success or failure of the provisioning operation.
319 """
320 logger.info(f"Provisioning Wazuh worker {request}")
321 if await is_connector_verified(connector_name="Portainer", db=session) is False:
322 api_endpoint = await get_connector_attribute(
323 connector_name="Wazuh Worker Provisioning",
324 column_name="connector_url",
325 session=session,
326 )
327 logger.info(f"Wazuh Worker API endpoint: {api_endpoint}")
328 # Send the POST request to the Wazuh worker
329 request.portainer_deployment = False
330 request.wazuh_manager_version = await get_wazuh_manager_version()
331 response = requests.post(
332 url=f"{api_endpoint}/provision_worker",
333 json=request.model_dump(),
334 )
335 logger.info(f"Status code from Wazuh Worker: {response.status_code}")
336 # Check the response status code
337 if response.status_code != 200:
338 return ProvisionWorkerResponse(
339 success=False,
340 message=f"Failed to provision Wazuh worker: {response.text}",
341 )
342 # Return the response
343 return ProvisionWorkerResponse(
344 success=True,
345 message="Wazuh worker provisioned successfully",
346 )
347 else:
348 request.portainer_deployment = True
349 swarm_node_ips = await list_node_ips()
350 logger.info(f"Invoking the customer provisioning application on the swarm node IPs: {swarm_node_ips}")
351 # Loop through each node IP and set the node_id based on position
352 for index, ip in enumerate(swarm_node_ips, start=1):
353 # Set the node_id to the current position in the list (1, 2, 3, etc.)
354 request.node_id = str(index)
355 logger.info(f"Provisioning Wazuh worker on IP: {ip} with node_id: {request.node_id}")
356
357 response = requests.post(
358 url=f"http://{ip}:5003/provision_worker",
359 json=request.model_dump(),
360 )
361 logger.info(f"Status code from Wazuh Worker: {response.status_code}")
362 if response.status_code != 200:
363 return ProvisionWorkerResponse(
364 success=False,
365 message=f"Failed to provision Wazuh worker: {response.text}",
366 )
367
368 # Create the stack and get the response
369 stack_response = await create_wazuh_customer_stack(request)
370
371 # Update the customer's Portainer stack ID
372 await update_customer_portainer_stack_id(customer_name=request.customer_name, stack_id=stack_response.data.Id, session=session)
373
374 return ProvisionWorkerResponse(
375 success=True,
376 message="Wazuh worker provisioned successfully via Portainer",
377 )
378
379
380 ######### ! Provision HAProxy ! ############
381 async def provision_haproxy(
382 request: ProvisionWorkerRequest,
383 session: AsyncSession,
384 ) -> ProvisionWorkerResponse:
385 """
386 Provisions a HAProxy.
387
388 Args:
389 request (ProvisionWorkerRequest): The request object containing the necessary information for provisioning.
390 session (AsyncSession): The async session object for making HTTP requests.
391
392 Returns:
393 ProvisionWorkerResponse: The response object indicating the success or failure of the provisioning operation.
394 """
395 logger.info(f"Provisioning HAProxy {request}")
396 if await is_connector_verified(connector_name="Portainer", db=session) is False:
397 api_endpoint = await get_connector_attribute(
398 connector_name="HAProxy Provisioning",
399 column_name="connector_url",
400 session=session,
401 )
402 logger.info(f"HAProxy API endpoint: {api_endpoint}")
403 request.portainer_deployment = False
404 # Send the POST request to the Wazuh worker
405 response = requests.post(
406 url=f"{api_endpoint}/provision_worker/haproxy",
407 json=request.model_dump(),
408 )
409 # Check the response status code
410 if response.status_code != 200:
411 return ProvisionWorkerResponse(
412 success=False,
413 message=f"Failed to provision HAProxy: {response.text}",
414 )
415 # Return the response
416 return ProvisionWorkerResponse(
417 success=True,
418 message="HAProxy provisioned successfully",
419 )
420 else:
421 request.portainer_deployment = True
422 request.swarm_nodes = await list_node_ips()
423 api_endpoint = await get_connector_attribute(
424 connector_name="HAProxy Provisioning",
425 column_name="connector_url",
426 session=session,
427 )
428 logger.info(f"HAProxy API endpoint: {api_endpoint}")
429 logger.info(f"Invoking the customer provisioning application on the swarm node IPs: {request.swarm_nodes}")
430 response = requests.post(
431 url=f"{api_endpoint}/provision_worker/haproxy",
432 json=request.model_dump(),
433 )
434 # Check the response status code
435 if response.status_code != 200:
436 return ProvisionWorkerResponse(
437 success=False,
438 message=f"Failed to provision HAProxy: {response.text}",
439 )
440 # Return the response
441 return ProvisionWorkerResponse(
442 success=True,
443 message="HAProxy provisioned successfully via Portainer",
444 )