| 1 | from fastapi import HTTPException |
| 2 | from loguru import logger |
| 3 | from sqlalchemy.ext.asyncio import AsyncSession |
| 4 | |
| 5 | from app.connectors.grafana.utils.universal import create_grafana_client |
| 6 | from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client |
| 7 | from app.customer_provisioning.schema.grafana import GrafanaDatasource |
| 8 | from app.customer_provisioning.schema.grafana import GrafanaDataSourceCreationResponse |
| 9 | from app.customer_provisioning.schema.grafana import GrafanaFolderCreationResponse |
| 10 | from app.customer_provisioning.schema.grafana import GrafanaOrganizationCreation |
| 11 | from app.customer_provisioning.schema.grafana import NodesVersionResponse |
| 12 | from app.customer_provisioning.schema.provision import ProvisionNewCustomer |
| 13 | from app.utils import get_connector_attribute |
| 14 | |
| 15 | |
| 16 | ################# ! GRAFANA PROVISIONING ! ################# |
| 17 | async def create_grafana_organization( |
| 18 | request: ProvisionNewCustomer, |
| 19 | ) -> GrafanaOrganizationCreation: |
| 20 | """ |
| 21 | Creates a Grafana organization for a customer. |
| 22 | |
| 23 | Args: |
| 24 | request (ProvisionNewCustomer): The request object containing customer information. |
| 25 | |
| 26 | Returns: |
| 27 | GrafanaOrganizationCreation: The created Grafana organization. |
| 28 | |
| 29 | """ |
| 30 | logger.info(f"Creating Grafana organization for customer {request.customer_name}") |
| 31 | grafana_client = await create_grafana_client("Grafana") |
| 32 | results = grafana_client.organization.create_organization( |
| 33 | organization={ |
| 34 | "name": request.customer_grafana_org_name, |
| 35 | }, |
| 36 | ) |
| 37 | return GrafanaOrganizationCreation(**results) |
| 38 | |
| 39 | |
| 40 | async def create_grafana_datasource( |
| 41 | request: ProvisionNewCustomer, |
| 42 | organization_id: int, |
| 43 | session: AsyncSession, |
| 44 | ) -> GrafanaDataSourceCreationResponse: |
| 45 | """ |
| 46 | Creates a Grafana Wazuh datasource for a new customer using the OpenSearch Data Source. |
| 47 | |
| 48 | Args: |
| 49 | request (ProvisionNewCustomer): The request object containing customer information. |
| 50 | organization_id (int): The ID of the organization to create the datasource for. |
| 51 | session (AsyncSession): The database session. |
| 52 | |
| 53 | Returns: |
| 54 | GrafanaDataSourceCreationResponse: The response object containing the result of the datasource creation. |
| 55 | """ |
| 56 | logger.info("Creating Grafana datasource") |
| 57 | grafana_client = await create_grafana_client("Grafana") |
| 58 | # Switch to the newly created organization |
| 59 | grafana_client.user.switch_actual_user_organisation(organization_id) |
| 60 | datasource_payload = GrafanaDatasource( |
| 61 | name="WAZUH", |
| 62 | type="grafana-opensearch-datasource", |
| 63 | typeName="OpenSearch", |
| 64 | access="proxy", |
| 65 | url=await get_connector_attribute( |
| 66 | connector_name="Wazuh-Indexer", |
| 67 | column_name="connector_url", |
| 68 | session=session, |
| 69 | ), |
| 70 | database=f"{request.customer_index_name}*", |
| 71 | basicAuth=True, |
| 72 | basicAuthUser=await get_connector_attribute( |
| 73 | connector_name="Wazuh-Indexer", |
| 74 | column_name="connector_username", |
| 75 | session=session, |
| 76 | ), |
| 77 | secureJsonData={ |
| 78 | "basicAuthPassword": await get_connector_attribute( |
| 79 | connector_name="Wazuh-Indexer", |
| 80 | column_name="connector_password", |
| 81 | session=session, |
| 82 | ), |
| 83 | }, |
| 84 | isDefault=False, |
| 85 | jsonData={ |
| 86 | "dataLinks": [ |
| 87 | {"field": "^data_vulnerability_cve$", "url": "https://nvd.nist.gov/vuln/detail/${__value.raw}"}, |
| 88 | { |
| 89 | "field": "^_id$", |
| 90 | "url": ( |
| 91 | "{}/explore?left=%7B%22datasource%22:%22WAZUH%22,%22queries%22:%5B%7B" |
| 92 | "%22refId%22:%22A%22,%22query%22:%22_id:${{__value.raw}}%22,%22alias%22:%22%22," |
| 93 | "%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:" |
| 94 | "%7B%22limit%22:%22500%22%7D%7D%5D,%22bucketAggs%22:%5B%7B%22type%22:%22date_histogram%22,%22id%22:%221%22,%22settings%22:%7B%22interval%22:%22auto%22%7D%7D%5D,%22timeField%22:" |
| 95 | "%22timestamp%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D" |
| 96 | ).format(request.grafana_url), |
| 97 | }, |
| 98 | ], |
| 99 | "database": f"{request.customer_index_name}*", |
| 100 | "flavor": "opensearch", |
| 101 | "includeFrozen": False, |
| 102 | "logLevelField": "syslog_level", |
| 103 | "logMessageField": "rule_description", |
| 104 | "maxConcurrentShardRequests": 5, |
| 105 | "pplEnabled": True, |
| 106 | "timeField": "timestamp", |
| 107 | "tlsSkipVerify": True, |
| 108 | "version": await get_opensearch_version(), |
| 109 | }, |
| 110 | readOnly=True, |
| 111 | ) |
| 112 | results = grafana_client.datasource.create_datasource( |
| 113 | datasource=datasource_payload.model_dump(), |
| 114 | ) |
| 115 | return GrafanaDataSourceCreationResponse(**results) |
| 116 | |
| 117 | |
| 118 | async def create_vulnerability_datasource( |
| 119 | request: ProvisionNewCustomer, |
| 120 | organization_id: int, |
| 121 | session: AsyncSession, |
| 122 | ) -> GrafanaDataSourceCreationResponse: |
| 123 | """ |
| 124 | Creates a Grafana Wazuh Vulnerabilites datasource for a new customer using the OpenSearch Data Source. |
| 125 | USED WHEN WAZUH VERSION IS 4.8.0 and above |
| 126 | |
| 127 | Args: |
| 128 | request (ProvisionNewCustomer): The request object containing customer information. |
| 129 | organization_id (int): The ID of the organization to create the datasource for. |
| 130 | session (AsyncSession): The database session. |
| 131 | |
| 132 | Returns: |
| 133 | GrafanaDataSourceCreationResponse: The response object containing the result of the datasource creation. |
| 134 | """ |
| 135 | logger.info("Creating Grafana datasource") |
| 136 | grafana_client = await create_grafana_client("Grafana") |
| 137 | # Switch to the newly created organization |
| 138 | grafana_client.user.switch_actual_user_organisation(organization_id) |
| 139 | datasource_payload = GrafanaDatasource( |
| 140 | name="VULNERABILITIES", |
| 141 | type="grafana-opensearch-datasource", |
| 142 | typeName="OpenSearch", |
| 143 | access="proxy", |
| 144 | url=await get_connector_attribute( |
| 145 | connector_id=1, |
| 146 | column_name="connector_url", |
| 147 | session=session, |
| 148 | ), |
| 149 | database=f"{request.customer_index_name}*", |
| 150 | basicAuth=True, |
| 151 | basicAuthUser=await get_connector_attribute( |
| 152 | connector_id=1, |
| 153 | column_name="connector_username", |
| 154 | session=session, |
| 155 | ), |
| 156 | secureJsonData={ |
| 157 | "basicAuthPassword": await get_connector_attribute( |
| 158 | connector_id=1, |
| 159 | column_name="connector_password", |
| 160 | session=session, |
| 161 | ), |
| 162 | }, |
| 163 | isDefault=False, |
| 164 | jsonData={ |
| 165 | "dataLinks": [ |
| 166 | {"field": "^vulnerability.id$", "url": "https://nvd.nist.gov/vuln/detail/${__value.raw}"}, |
| 167 | { |
| 168 | "field": "^_id$", |
| 169 | "url": ( |
| 170 | "{}/explore?left=%7B%22datasource%22:%22WAZUH%22,%22queries%22:%5B%7B" |
| 171 | "%22refId%22:%22A%22,%22query%22:%22_id:${{__value.raw}}%22,%22alias%22:%22%22," |
| 172 | "%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:" |
| 173 | "%7B%22limit%22:%22500%22%7D%7D%5D,%22bucketAggs%22:%5B%7B%22type%22:%22date_histogram%22,%22id%22:%221%22,%22settings%22:%7B%22interval%22:%22auto%22%7D%7D%5D,%22timeField%22:" |
| 174 | "%22timestamp%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D" |
| 175 | ).format(request.grafana_url), |
| 176 | }, |
| 177 | ], |
| 178 | "database": "wazuh-states-vulnerabilities*", |
| 179 | "flavor": "opensearch", |
| 180 | "includeFrozen": False, |
| 181 | "logLevelField": "vulnerability.score.base", |
| 182 | "logMessageField": "vulnerability.id", |
| 183 | "maxConcurrentShardRequests": 5, |
| 184 | "pplEnabled": True, |
| 185 | "timeField": "timestamp", |
| 186 | "tlsSkipVerify": True, |
| 187 | "version": await get_opensearch_version(), |
| 188 | }, |
| 189 | readOnly=True, |
| 190 | ) |
| 191 | results = grafana_client.datasource.create_datasource( |
| 192 | datasource=datasource_payload.model_dump(), |
| 193 | ) |
| 194 | return GrafanaDataSourceCreationResponse(**results) |
| 195 | |
| 196 | |
| 197 | async def create_grafana_folder( |
| 198 | organization_id: int, |
| 199 | folder_title: str, |
| 200 | ) -> GrafanaFolderCreationResponse: |
| 201 | """ |
| 202 | Creates a Grafana folder in the specified organization. |
| 203 | |
| 204 | Args: |
| 205 | organization_id (int): The ID of the organization where the folder will be created. |
| 206 | folder_title (str): The title of the folder. |
| 207 | |
| 208 | Returns: |
| 209 | GrafanaFolderCreationResponse: The response object containing the details of the created folder. |
| 210 | """ |
| 211 | logger.info("Creating Grafana folder") |
| 212 | grafana_client = await create_grafana_client("Grafana") |
| 213 | # Switch to the newly created organization |
| 214 | grafana_client.user.switch_actual_user_organisation(organization_id) |
| 215 | results = grafana_client.folder.create_folder( |
| 216 | title=folder_title, |
| 217 | ) |
| 218 | logger.info(f"Folder creation results: {results}") |
| 219 | return GrafanaFolderCreationResponse(**results) |
| 220 | |
| 221 | |
| 222 | async def get_opensearch_version() -> str: |
| 223 | """ |
| 224 | Retrieves the version of OpenSearch. |
| 225 | |
| 226 | Returns: |
| 227 | str: The version of OpenSearch. |
| 228 | |
| 229 | Raises: |
| 230 | HTTPException: If the OpenSearch version cannot be retrieved. |
| 231 | """ |
| 232 | logger.info("Getting OpenSearch version") |
| 233 | opensearch_client = await create_wazuh_indexer_client("Wazuh-Indexer") |
| 234 | |
| 235 | # Retrieve version information |
| 236 | version_response = opensearch_client.nodes.info( |
| 237 | node_id="_local", |
| 238 | filter_path=["nodes.*.version"], |
| 239 | ) |
| 240 | |
| 241 | # Parse the response to get the first version found |
| 242 | nodes_version_response = NodesVersionResponse(**version_response) |
| 243 | for node_id, node_info in nodes_version_response.nodes.items(): |
| 244 | return node_info.version |
| 245 | |
| 246 | # If no version is found, raise an exception |
| 247 | raise HTTPException( |
| 248 | status_code=500, |
| 249 | detail="Failed to retrieve OpenSearch version.", |
| 250 | ) |
| 251 | |
| 252 | |
| 253 | ################# ! GRAFANA DECOMISSIONING ! ################# |
| 254 | async def delete_grafana_organization(organization_id: int): |
| 255 | """ |
| 256 | Deletes a Grafana organization. |
| 257 | |
| 258 | Args: |
| 259 | organization_id (int): The ID of the organization to delete. |
| 260 | """ |
| 261 | logger.info("Deleting Grafana organization") |
| 262 | grafana_client = await create_grafana_client("Grafana") |
| 263 | try: |
| 264 | organization_deleted = grafana_client.organizations.delete_organization( |
| 265 | organization_id=organization_id, |
| 266 | ) |
| 267 | logger.info(f"Organization deleted: {organization_deleted}") |
| 268 | except Exception as e: |
| 269 | # Switch the organization to the default and try again |
| 270 | logger.info( |
| 271 | f"Failed to delete organization: {e}. Switching to default organization and trying again.", |
| 272 | ) |
| 273 | grafana_client.user.switch_actual_user_organisation(1) |
| 274 | organization_deleted = grafana_client.organizations.delete_organization( |
| 275 | organization_id=organization_id, |
| 276 | ) |
| 277 | logger.info(f"Organization deleted: {organization_deleted}") |
| 278 | return organization_deleted |
| 279 | return organization_deleted |
| 280 | |
| 281 | |
| 282 | async def delete_grafana_dashboard_folder(organization_id: int, folder_uid: str): |
| 283 | """ |
| 284 | Deletes a Grafana dashboard folder. |
| 285 | |
| 286 | Args: |
| 287 | folder_uid (str): The ID of the folder to delete. |
| 288 | """ |
| 289 | logger.info("Deleting Grafana folder") |
| 290 | grafana_client = await create_grafana_client("Grafana") |
| 291 | grafana_client.user.switch_actual_user_organisation(organization_id) |
| 292 | folder_deleted = grafana_client.folder.delete_folder( |
| 293 | uid=folder_uid, |
| 294 | ) |
| 295 | logger.info(f"Folder deleted: {folder_deleted}") |
| 296 | return folder_deleted |
| 297 | |
| 298 | |
| 299 | async def delete_grafana_datasource(organization_id: int, datasource_uid: str): |
| 300 | """ |
| 301 | Deletes a Grafana datasource. |
| 302 | |
| 303 | Args: |
| 304 | datasource_uid (int): The ID of the datasource to delete. |
| 305 | """ |
| 306 | logger.info("Deleting Grafana datasource") |
| 307 | grafana_client = await create_grafana_client("Grafana") |
| 308 | grafana_client.user.switch_actual_user_organisation(organization_id) |
| 309 | datasource_deleted = grafana_client.datasource.delete_datasource_by_uid( |
| 310 | datasource_uid=datasource_uid, |
| 311 | ) |
| 312 | logger.info(f"Datasource deleted: {datasource_deleted}") |
| 313 | return datasource_deleted |