From 36679c2352ff481b2fabddaa5152354eebbac1c8 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Fri, 15 Nov 2024 19:12:52 +0530 Subject: [PATCH 1/2] feat: StatusCake Webhook integration Signed-off-by: 35C4n0r --- .../documentation/statuscake-provider.mdx | 4 +- .../statuscake_provider.py | 325 ++++++++++++------ 2 files changed, 214 insertions(+), 115 deletions(-) diff --git a/docs/providers/documentation/statuscake-provider.mdx b/docs/providers/documentation/statuscake-provider.mdx index d4bde3644..2c18895af 100644 --- a/docs/providers/documentation/statuscake-provider.mdx +++ b/docs/providers/documentation/statuscake-provider.mdx @@ -1,14 +1,14 @@ --- title: "StatusCake" sidebarTitle: "StatusCake Provider" -description: "StatusCake allows you to monitor your website and APIs and send alert to keep" +description: "StatusCake allows you to monitor your website and APIs. Keep allows to read alerts and install webhook in StatusCake" --- ## Authentication Parameters The StatusCake provider requires the following authentication parameters: -- `Statuscake API Key`: The API key for the StatusCake account. This is required for the StatusCake provider. +- `Statuscake API Key` (required): The API key for the StatusCake account. This is required for the StatusCake provider. ## Connecting with the Provider diff --git a/keep/providers/statuscake_provider/statuscake_provider.py b/keep/providers/statuscake_provider/statuscake_provider.py index 6b17ad30b..23d93e232 100644 --- a/keep/providers/statuscake_provider/statuscake_provider.py +++ b/keep/providers/statuscake_provider/statuscake_provider.py @@ -1,8 +1,10 @@ """ -Statuscake is a class that provides a way to read alerts from the Statuscake API +Statuscake is a class that provides a way to read alerts from the Statuscake API and install webhook in StatuCake """ import dataclasses +from typing import List +from urllib.parse import urljoin, urlencode import pydantic import requests @@ -45,10 +47,12 @@ class StatuscakeProvider(BaseProvider): } STATUS_MAP = { - "up": AlertStatus.RESOLVED, - "down": AlertStatus.FIRING, + "Up": AlertStatus.RESOLVED, + "Down": AlertStatus.FIRING, } + FINGERPRINT_FIELDS = ["test_id"] + def __init__( self, context_manager: ContextManager, provider_id: str, config: ProviderConfig ): @@ -57,13 +61,28 @@ def __init__( def dispose(self): pass + def __get_url(self, paths: List[str] = [], query_params: dict = None, **kwargs): + """ + Helper method to build the url for Graylog api requests. + """ + host = "https://api.statuscake.com/v1/" + url = urljoin( + host, + "/".join(str(path) for path in paths), + ) + + # add query params + if query_params: + url = f"{url}?{urlencode(query_params)}" + return url + def validate_scopes(self): """ Validate that the user has the required scopes to use the provider """ try: response = requests.get( - "https://api.statuscake.com/v1/uptime/", + url=self.__get_url(paths=["uptime"]), headers=self.__get_auth_headers(), ) @@ -94,155 +113,223 @@ def validate_config(self): def __get_auth_headers(self): if self.authentication_config.api_key is not None: - return {"Authorization": f"Bearer {self.authentication_config.api_key}"} + return { + "Authorization": f"Bearer {self.authentication_config.api_key}", + "Content-Type": "application/x-www-form-urlencoded", + } - def __get_heartbeat_alerts(self) -> list[AlertDto]: + def __get_paginated_data(self, paths: list, query_params: dict = {}): + data = [] try: - response = requests.get( - "https://api.statuscake.com/v1/uptime/", - headers=self.__get_auth_headers(), - ) - - if not response.ok: - self.logger.error( - "Failed to get heartbeat from Statuscake: %s", response.json() - ) - raise Exception("Could not get heartbeat from Statuscake") - - return [ - AlertDto( - id=alert["id"], - name=alert["name"], - status=alert["status"], - url=alert["website_url"], - uptime=alert["uptime"], - source="statuscake", + page = 1 + while True: + self.logger.info(f"Getting page: {page} for {paths}") + response = requests.get( + url=self.__get_url( + paths=paths, query_params={**query_params, "page": page} + ), + headers=self.__get_auth_headers(), ) - for alert in response.json()["data"] - ] + + if not response.ok: + raise Exception(response.text) + + response = response.json() + data.extend(response["data"]) + if page == response["metadata"]["page_count"]: + break + return data except Exception as e: - self.logger.error("Error getting heartbeat from Statuscake: %s", e) - raise Exception(f"Error getting heartbeat from Statuscake: {e}") + self.logger.error( + f"Error while getting {paths}", extra={"exception": str(e)} + ) + raise e - def __get_pagespeed_alerts(self) -> list[AlertDto]: + def __update_contact_group(self, contact_group_id, keep_api_url): try: - response = requests.get( - "https://api.statuscake.com/v1/pagespeed/", + response = requests.put( + url=self.__get_url(["contact-groups", contact_group_id]), headers=self.__get_auth_headers(), + data={ + "ping_url": keep_api_url, + }, ) - - if not response.ok: - self.logger.error( - "Failed to get pagespeed from Statuscake: %s", response.json() - ) - raise Exception("Could not get pagespeed from Statuscake") - - return [ - AlertDto( - name=alert["name"], - url=alert["website_url"], - location=alert["location"], - alert_smaller=alert["alert_smaller"], - alert_bigger=alert["alert_bigger"], - alert_slower=alert["alert_slower"], - status=alert["status"], - source="statuscake", - ) - for alert in response.json()["data"] - ] - + if response.status_code != 204: + raise Exception(response.text) except Exception as e: - self.logger.error("Error getting pagespeed from Statuscake: %s", e) - raise Exception(f"Error getting pagespeed from Statuscake: {e}") + self.logger.error( + "Error while updating contact group", extra={"exception": str(e)} + ) + raise e - def __get_ssl_alerts(self) -> list[AlertDto]: + def __create_contact_group(self, keep_api_url: str, contact_group_name: str): try: - response = requests.get( - "https://api.statuscake.com/v1/ssl/", headers=self.__get_auth_headers() + response = requests.post( + url=self.__get_url(paths=["contact-groups"]), + headers=self.__get_auth_headers(), + data={ + "ping_url": keep_api_url, + "name": contact_group_name, + }, ) + if response.status_code != 201: + raise Exception(response.text) + self.logger.info("Successfully created contact group") + return response.json()["data"]["new_id"] + except Exception as e: + self.logger.error( + "Error while creating contact group", extra={"exception": str(e)} + ) + raise e - if not response.ok: - self.logger.error( - "Failed to get ssl from Statuscake: %s", response.json() + def setup_webhook( + self, tenant_id: str, keep_api_url: str, api_key: str, setup_alerts: bool = True + ): + # Getting all the contact groups + self.logger.info("Attempting to install webhook in statuscake") + keep_api_url = f"{keep_api_url}&api_key={api_key}" + contact_group_name = f"Keep-{self.provider_id}" + contact_groups = self.__get_paginated_data(paths=["contact-groups"]) + for contact_group in contact_groups: + if contact_group["name"] == contact_group_name: + self.logger.info( + "Webhook already exists, updating the ping_url, just for safe measures" ) - raise Exception("Could not get ssl from Statuscake") - - return [ - AlertDto( - id=alert["id"], - url=alert["website_url"], - issuer_common_name=alert["issuer_common_name"], - cipher=alert["cipher"], - cipher_score=alert["cipher_score"], - certificate_score=alert["certificate_score"], - certificate_status=alert["certificate_status"], - valid_from=alert["valid_from"], - valid_until=alert["valid_until"], - source="statuscake", + contact_group_id = contact_group["id"] + self.__update_contact_group( + contact_group_id=contact_group_id, keep_api_url=keep_api_url ) - for alert in response.json()["data"] - ] + break + else: + self.logger.info("Creating a new contact group") + contact_group_id = self.__create_contact_group( + contact_group_name=contact_group_name, keep_api_url=keep_api_url + ) - except Exception as e: - self.logger.error("Error getting ssl from Statuscake: %s", e) - raise Exception(f"Error getting ssl from Statuscake: {e}") + alerts_to_update = ["heartbeat", "uptime", "pagespeed", "ssl"] + + for alert_type in alerts_to_update: + alerts = self.__get_paginated_data(paths=[alert_type]) + for alert in alerts: + if contact_group_id not in alert["contact_groups"]: + alert["contact_groups"].append(contact_group_id) + self.__update_alert( + data={"contact_groups[]": alert["contact_groups"]}, + paths=[alert_type, alert["id"]], + ) - def __get_uptime_alerts(self) -> list[AlertDto]: + def __update_alert(self, data: dict, paths: list): try: - response = requests.get( - "https://api.statuscake.com/v1/uptime/", + self.logger.info(f"Attempting to updated alert: {paths}") + response = requests.put( + url=self.__get_url(paths=paths), headers=self.__get_auth_headers(), + data=data, ) - if not response.ok: - self.logger.error( - "Failed to get uptime from Statuscake: %s", response.json() - ) - raise Exception("Could not get uptime from Statuscake") - - return [ - AlertDto( - id=alert["id"], - name=alert["name"], - status=alert["status"], - url=alert["website_url"], - uptime=alert["uptime"], - source="statuscake", - ) - for alert in response.json()["data"] - ] - + raise Exception(response.text) + self.logger.info("Successfully updated alert", extra={"data": data, "paths": paths}) except Exception as e: - self.logger.error("Error getting uptime from Statuscake: %s", e) - raise Exception(f"Error getting uptime from Statuscake: {e}") + self.logger.error("Error while updating alert", extra={"exception": str(e)}) + raise e + + def __get_heartbeat_alerts_dto(self) -> list[AlertDto]: + + response = self.__get_paginated_data(paths=["heartbeat"]) + + return [ + AlertDto( + id=alert["id"], + name=alert["name"], + status=alert["status"], + url=alert["website_url"], + uptime=alert["uptime"], + source="statuscake", + ) + for alert in response + ] + + def __get_pagespeed_alerts_dto(self) -> list[AlertDto]: + + response = self.__get_paginated_data(paths=["pagespeed"]) + + return [ + AlertDto( + name=alert["name"], + url=alert["website_url"], + location=alert["location"], + alert_smaller=alert["alert_smaller"], + alert_bigger=alert["alert_bigger"], + alert_slower=alert["alert_slower"], + status=alert["status"], + source="statuscake", + ) + for alert in response + ] + + def __get_ssl_alerts_dto(self) -> list[AlertDto]: + + response = self.__get_paginated_data(paths=["ssl"]) + + return [ + AlertDto( + id=alert["id"], + url=alert["website_url"], + issuer_common_name=alert["issuer_common_name"], + cipher=alert["cipher"], + cipher_score=alert["cipher_score"], + certificate_score=alert["certificate_score"], + certificate_status=alert["certificate_status"], + valid_from=alert["valid_from"], + valid_until=alert["valid_until"], + source="statuscake", + ) + for alert in response + ] + + def __get_uptime_alerts_dto(self) -> list[AlertDto]: + + response = self.__get_paginated_data(paths=["uptime"]) + + return [ + AlertDto( + id=alert["id"], + name=alert["name"], + status=alert["status"], + url=alert["website_url"], + uptime=alert["uptime"], + source="statuscake", + ) + for alert in response + ] def _get_alerts(self) -> list[AlertDto]: alerts = [] try: self.logger.info("Collecting alerts (heartbeats) from Statuscake") - heartbeat_alerts = self.__get_heartbeat_alerts() + heartbeat_alerts = self.__get_heartbeat_alerts_dto() alerts.extend(heartbeat_alerts) except Exception as e: self.logger.error("Error getting heartbeat from Statuscake: %s", e) try: self.logger.info("Collecting alerts (pagespeed) from Statuscake") - pagespeed_alerts = self.__get_pagespeed_alerts() + pagespeed_alerts = self.__get_pagespeed_alerts_dto() alerts.extend(pagespeed_alerts) except Exception as e: self.logger.error("Error getting pagespeed from Statuscake: %s", e) try: self.logger.info("Collecting alerts (ssl) from Statuscake") - ssl_alerts = self.__get_ssl_alerts() + ssl_alerts = self.__get_ssl_alerts_dto() alerts.extend(ssl_alerts) except Exception as e: self.logger.error("Error getting ssl from Statuscake: %s", e) try: self.logger.info("Collecting alerts (uptime) from Statuscake") - uptime_alerts = self.__get_uptime_alerts() + uptime_alerts = self.__get_uptime_alerts_dto() alerts.extend(uptime_alerts) except Exception as e: self.logger.error("Error getting uptime from Statuscake: %s", e) @@ -253,22 +340,34 @@ def _get_alerts(self) -> list[AlertDto]: def _format_alert( event: dict, provider_instance: "BaseProvider" = None ) -> AlertDto: - + # https://www.statuscake.com/kb/knowledge-base/how-to-use-the-web-hook-url/ status = StatuscakeProvider.STATUS_MAP.get( - event.get("status"), AlertStatus.FIRING + event.get("Status"), AlertStatus.FIRING ) # Statuscake does not provide severity information severity = AlertSeverity.HIGH alert = AlertDto( - id=event.get("id"), - name=event.get("name"), + id=event.get('TestID', event.get("Name")), + name=event.get("Name"), status=status if status is not None else AlertStatus.FIRING, severity=severity, - url=event["website_url"] if "website_url" in event else None, - source="statuscake", + url=event.get("URL", None), + ip=event.get("IP", None), + tags=event.get("Tags", None), + test_id=event.get('TestID', None), + method=event.get("Method", None), + checkrate=event.get("Checkrate", None), + status_code=event.get("StatusCode", None), + source=["statuscake"], ) + alert.fingerprint = StatuscakeProvider.get_alert_fingerprint( + alert, + ( + StatuscakeProvider.FINGERPRINT_FIELDS + ), + ) if event.get("TestID", None) else None return alert From e7c1a377b86ba36d50336de58248772bf53a3fed Mon Sep 17 00:00:00 2001 From: Jay Kumar <70096901+35C4n0r@users.noreply.github.com> Date: Sat, 16 Nov 2024 21:57:20 +0530 Subject: [PATCH 2/2] Update keep/providers/statuscake_provider/statuscake_provider.py Co-authored-by: Tal Signed-off-by: Jay Kumar <70096901+35C4n0r@users.noreply.github.com> --- keep/providers/statuscake_provider/statuscake_provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keep/providers/statuscake_provider/statuscake_provider.py b/keep/providers/statuscake_provider/statuscake_provider.py index 23d93e232..ec526648d 100644 --- a/keep/providers/statuscake_provider/statuscake_provider.py +++ b/keep/providers/statuscake_provider/statuscake_provider.py @@ -63,7 +63,7 @@ def dispose(self): def __get_url(self, paths: List[str] = [], query_params: dict = None, **kwargs): """ - Helper method to build the url for Graylog api requests. + Helper method to build the url for StatucCake api requests. """ host = "https://api.statuscake.com/v1/" url = urljoin(