diff --git a/keep-ui/app/alerts/alert-table-utils.tsx b/keep-ui/app/alerts/alert-table-utils.tsx index 01a26fb09..b1fb285ac 100644 --- a/keep-ui/app/alerts/alert-table-utils.tsx +++ b/keep-ui/app/alerts/alert-table-utils.tsx @@ -18,7 +18,10 @@ import AlertExtraPayload from "./alert-extra-payload"; import AlertMenu from "./alert-menu"; import { isSameDay, isValid, isWithinInterval, startOfDay } from "date-fns"; import { severityMapping } from "./models"; -import { MdOutlineNotificationsActive, MdOutlineNotificationsOff } from "react-icons/md"; +import { + MdOutlineNotificationsActive, + MdOutlineNotificationsOff, +} from "react-icons/md"; export const DEFAULT_COLS = [ "noise", @@ -82,12 +85,13 @@ export const isDateWithinRange: FilterFn = (row, columnId, value) => { const columnHelper = createColumnHelper(); -const invertedSeverityMapping = Object.entries(severityMapping).reduce<{ [key: string]: number }>((acc, [key, value]) => { +const invertedSeverityMapping = Object.entries(severityMapping).reduce<{ + [key: string]: number; +}>((acc, [key, value]) => { acc[value as keyof typeof acc] = Number(key); return acc; }, {}); - const customSeveritySortFn = (rowA: any, rowB: any) => { // Adjust the way to access severity values according to your data structure const severityValueA = rowA.original?.severity; // or rowA.severity; @@ -160,39 +164,38 @@ export const useAlertTableCols = ( ) as ColumnDef[]; return [ - // noisy column - columnHelper.display({ - id: "noise", - size: 5, - header: () => <>, - cell: (context) => { - // Get the status of the alert - const status = context.row.original.status; - const isNoisy = context.row.original.isNoisy; + // noisy column + columnHelper.display({ + id: "noise", + size: 5, + header: () => <>, + cell: (context) => { + // Get the status of the alert + const status = context.row.original.status; + const isNoisy = context.row.original.isNoisy; - // Return null if presetNoisy is not true - if (!presetNoisy && !isNoisy) { - return null; - } - else if (presetNoisy) { - // Decide which icon to display based on the status - if (status === "firing") { - return ; - } else { - return ; - } + // Return null if presetNoisy is not true + if (!presetNoisy && !isNoisy) { + return null; + } else if (presetNoisy) { + // Decide which icon to display based on the status + if (status === "firing") { + return ; + } else { + return ; } - // else, noisy alert in non noisy preset - else { - if (status === "firing") { - return ; - } else { - return null; - } + } + // else, noisy alert in non noisy preset + else { + if (status === "firing") { + return ; + } else { + return null; } - }, - enableSorting: false, - }), + } + }, + enableSorting: false, + }), , ...(isCheckboxDisplayed ? [ @@ -222,7 +225,6 @@ export const useAlertTableCols = ( minSize: 100, cell: (context) => , sortingFn: customSeveritySortFn, - }), columnHelper.display({ id: "name", @@ -267,17 +269,23 @@ export const useAlertTableCols = ( header: "Source", minSize: 100, cell: (context) => - (context.getValue() ?? []).map((source, index) => ( - {source} - )), + (context.getValue() ?? []).map((source, index) => { + let imagePath = `/icons/${source}-icon.png`; + if (source.includes("@")) { + imagePath = "/icons/mailgun-icon.png"; + } + return ( + {source} + ); + }), }), columnHelper.accessor("assignee", { id: "assignee", diff --git a/keep-ui/public/icons/mailgun-icon.png b/keep-ui/public/icons/mailgun-icon.png new file mode 100644 index 000000000..e9f3f35cd Binary files /dev/null and b/keep-ui/public/icons/mailgun-icon.png differ diff --git a/keep/api/core/dependencies.py b/keep/api/core/dependencies.py index 6066fc1dd..f40b4fecd 100644 --- a/keep/api/core/dependencies.py +++ b/keep/api/core/dependencies.py @@ -1,6 +1,8 @@ import logging import os +from fastapi import Request +from fastapi.datastructures import FormData from pusher import Pusher logger = logging.getLogger(__name__) @@ -11,6 +13,25 @@ SINGLE_TENANT_EMAIL = "admin@keephq" +async def extract_generic_body(request: Request) -> dict | bytes | FormData: + """ + Extracts the body of the request based on the content type. + + Args: + request (Request): The request object. + + Returns: + dict | bytes | FormData: The body of the request. + """ + content_type = request.headers.get("Content-Type") + if content_type == "application/json": + return await request.json() + elif content_type == "application/x-www-form-urlencoded": + return await request.form() + else: + return await request.body() + + def get_pusher_client() -> Pusher | None: if os.environ.get("PUSHER_DISABLED", "false") == "true": return None diff --git a/keep/api/routes/alerts.py b/keep/api/routes/alerts.py index 210d5adea..ec02e8018 100644 --- a/keep/api/routes/alerts.py +++ b/keep/api/routes/alerts.py @@ -26,7 +26,7 @@ from keep.api.core.config import config from keep.api.core.db import get_alert_audit as get_alert_audit_db from keep.api.core.db import get_alerts_by_fingerprint, get_enrichment, get_last_alerts -from keep.api.core.dependencies import get_pusher_client +from keep.api.core.dependencies import extract_generic_body, get_pusher_client from keep.api.core.elastic import ElasticClient from keep.api.models.alert import ( AlertDto, @@ -347,18 +347,30 @@ async def webhook_challenge(): ) async def receive_event( provider_type: str, - event: dict | bytes, bg_tasks: BackgroundTasks, request: Request, provider_id: str | None = None, fingerprint: str | None = None, + event=Depends(extract_generic_body), authenticated_entity: AuthenticatedEntity = Depends( IdentityManagerFactory.get_auth_verifier(["write:alert"]) ), pusher_client: Pusher = Depends(get_pusher_client), ) -> dict[str, str]: trace_id = request.state.trace_id - provider_class = ProvidersFactory.get_provider_class(provider_type) + + provider_class = None + try: + provider_class = ProvidersFactory.get_provider_class(provider_type) + except ModuleNotFoundError: + raise HTTPException( + status_code=400, detail=f"Provider {provider_type} not found" + ) + if not provider_class: + raise HTTPException( + status_code=400, detail=f"Provider {provider_type} not found" + ) + # Parse the raw body event = provider_class.parse_event_raw_body(event) diff --git a/keep/api/tasks/process_event_task.py b/keep/api/tasks/process_event_task.py index b42cf407a..7630fbc5a 100644 --- a/keep/api/tasks/process_event_task.py +++ b/keep/api/tasks/process_event_task.py @@ -10,6 +10,7 @@ # third-parties from arq import Retry +from fastapi.datastructures import FormData from sqlmodel import Session # internals @@ -516,7 +517,11 @@ def process_event( except Exception: logger.exception("Failed to run pre-formatting extraction rules") - if provider_type is not None and isinstance(event, dict): + if ( + provider_type is not None + and isinstance(event, dict) + or isinstance(event, FormData) + ): provider_class = ProvidersFactory.get_provider_class(provider_type) event = provider_class.format_alert( tenant_id=tenant_id, diff --git a/keep/providers/mailgun_provider/__init__.py b/keep/providers/mailgun_provider/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/keep/providers/mailgun_provider/mailgun_provider.py b/keep/providers/mailgun_provider/mailgun_provider.py new file mode 100644 index 000000000..6919199f4 --- /dev/null +++ b/keep/providers/mailgun_provider/mailgun_provider.py @@ -0,0 +1,89 @@ +""" +Simple Console Output Provider +""" + +import dataclasses +import datetime +import typing + +import pydantic +from fastapi.datastructures import FormData + +from keep.api.models.alert import AlertDto +from keep.contextmanager.contextmanager import ContextManager +from keep.providers.base.base_provider import BaseProvider +from keep.providers.models.provider_config import ProviderConfig +from keep.providers.providers_factory import ProvidersFactory + + +@pydantic.dataclasses.dataclass +class MailgunProviderAuthConfig: + + extraction: typing.Optional[dict[str, str]] = dataclasses.field( + default=lambda: {}, + metadata={ + "description": "Extraction Rules", + "type": "form", + }, + ) + + +class MailgunProvider(BaseProvider): + def __init__( + self, context_manager: ContextManager, provider_id: str, config: ProviderConfig + ): + super().__init__(context_manager, provider_id, config) + + def validate_config(self): + self.authentication_config = MailgunProviderAuthConfig( + **self.config.authentication + ) + + def setup_webhook( + self, tenant_id: str, keep_api_url: str, api_key: str, setup_alerts: bool = True + ): + return super().setup_webhook(tenant_id, keep_api_url, api_key, setup_alerts) + + @staticmethod + def _format_alert(event: FormData) -> AlertDto: + name = event["subject"] + source = event["from"] + message = event["stripped-text"] + timestamp = datetime.datetime.fromtimestamp( + float(event["timestamp"]) + ).isoformat() + severity = "info" # to extract + status = "firing" # to extract + return AlertDto( + name=name, + source=[source], + message=message, + description=message, + lastReceived=timestamp, + severity=severity, + status=status, + raw_email={**event}, + ) + + +if __name__ == "__main__": + # Output debug messages + import logging + + logging.basicConfig(level=logging.DEBUG, handlers=[logging.StreamHandler()]) + context_manager = ContextManager( + tenant_id="singletenant", + workflow_id="test", + ) + # Initalize the provider and provider config + config = { + "description": "Console Output Provider", + "authentication": {}, + } + provider = ProvidersFactory.get_provider( + context_manager, + provider_id="mock", + provider_type="console", + provider_config=config, + ) + provider.notify(alert_message="Simple alert showing context with name: John Doe")