From 3b4a05a3e02f64c1859347ef7dce17eab665a691 Mon Sep 17 00:00:00 2001 From: Jacob Date: Fri, 26 Jun 2026 11:32:34 -0400 Subject: [PATCH] Add Flowtriq notification plugin Add a notification plugin for Flowtriq (https://flowtriq.com), a DDoS detection and traffic analytics platform. The plugin POSTs alert payloads to a configurable webhook URL with an optional API key (sent as X-API-Key header). The JSON payload includes source, host, host_address, service, state, output, notification_type, and an optional Checkmk URL for linking back to the monitored object. New files: - cmk/notification_plugins/flowtriq.py (plugin logic) - notifications/flowtriq.py (entry point script) - cmk/gui/wato/_notification_parameter/_flowtriq.py (WATO GUI form) - tests/test_flowtriq.py (unit tests) Updated: - registration.py (GUI parameter registration) - notify_types.py (FlowtriqPluginModel type definitions) - BUILD (Bazel packaging) - test_registry.py, test_configuration_entity.py, test_binaries.py --- .../wato/_notification_parameter/_flowtriq.py | 106 ++++++++++++++++++ .../_notification_parameter/registration.py | 8 ++ cmk/utils/notify_types.py | 14 +++ packages/cmk-notification-plugins/BUILD | 1 + .../cmk/notification_plugins/flowtriq.py | 71 ++++++++++++ .../notifications/flowtriq.py | 13 +++ .../tests/test_flowtriq.py | 87 ++++++++++++++ tests/integration/test_binaries.py | 1 + .../notification_parameter/test_registry.py | 1 + .../gui/watolib/test_configuration_entity.py | 1 + 10 files changed, 303 insertions(+) create mode 100644 cmk/gui/wato/_notification_parameter/_flowtriq.py create mode 100644 packages/cmk-notification-plugins/cmk/notification_plugins/flowtriq.py create mode 100644 packages/cmk-notification-plugins/notifications/flowtriq.py create mode 100644 packages/cmk-notification-plugins/tests/test_flowtriq.py diff --git a/cmk/gui/wato/_notification_parameter/_flowtriq.py b/cmk/gui/wato/_notification_parameter/_flowtriq.py new file mode 100644 index 00000000000..363dc50bc48 --- /dev/null +++ b/cmk/gui/wato/_notification_parameter/_flowtriq.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# Copyright (C) 2024 Checkmk GmbH - License: GNU General Public License v2 +# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and +# conditions defined in the file COPYING, which is part of this source code package. + + +from cmk.gui.watolib.password_store import passwordstore_choices_without_user +from cmk.rulesets.internal.form_specs import SingleChoiceElementExtended, SingleChoiceExtended +from cmk.rulesets.v1 import Help, Label, Message, Title +from cmk.rulesets.v1.form_specs import ( + CascadingSingleChoice, + CascadingSingleChoiceElement, + DefaultValue, + DictElement, + Dictionary, + FixedValue, + migrate_to_password, + migrate_to_proxy, + Password, + Proxy, + String, +) +from cmk.rulesets.v1.form_specs.validators import LengthInRange, MatchRegex + +from ._helpers import _get_url_prefix_setting + + +def form_spec() -> Dictionary: + return Dictionary( + title=Title("Flowtriq parameters"), + elements={ + "webhook_url": DictElement( + required=True, + parameter_form=CascadingSingleChoice( + title=Title("Flowtriq webhook URL"), + prefill=DefaultValue("webhook_url"), + help_text=Help( + "The URL of your Flowtriq webhook endpoint. " + "Alerts will be sent as JSON POST requests to this URL." + "
This URL can also be collected from the password store of Checkmk." + ), + elements=[ + CascadingSingleChoiceElement( + name="webhook_url", + title=Title("Explicit"), + parameter_form=String( + custom_validate=[ + LengthInRange( + min_value=1, + error_msg=Message( + "Please enter a valid webhook URL" + ), + ), + MatchRegex( + regex=r"^https?://.+", + error_msg=Message( + "The webhook URL must begin with " + "https:// or http://" + ), + ), + ], + ), + ), + CascadingSingleChoiceElement( + name="store", + title=Title("From password store"), + parameter_form=SingleChoiceExtended( + no_elements_text=Message( + "There are no passwords defined for this selection yet." + ), + elements=[ + SingleChoiceElementExtended( + title=Title("%s") % title, name=ident + ) + for ident, title in passwordstore_choices_without_user() + if ident is not None + ], + ), + ), + ], + ), + ), + "api_key": DictElement( + parameter_form=Password( + title=Title("API Key"), + help_text=Help( + "An optional API key for authenticating with the Flowtriq webhook. " + "If set, it will be sent as an X-API-Key HTTP header." + ), + migrate=migrate_to_password, + ), + ), + "ignore_ssl": DictElement( + parameter_form=FixedValue( + value=True, + title=Title("Disable SSL certificate verification"), + label=Label("Disable SSL certificate verification"), + help_text=Help( + "Ignore unverified HTTPS request warnings. Use with caution." + ), + ), + ), + "proxy_url": DictElement(parameter_form=Proxy(migrate=migrate_to_proxy)), + "url_prefix": _get_url_prefix_setting(), + }, + ) diff --git a/cmk/gui/wato/_notification_parameter/registration.py b/cmk/gui/wato/_notification_parameter/registration.py index cf3a56ec18e..8956e2aefd1 100644 --- a/cmk/gui/wato/_notification_parameter/registration.py +++ b/cmk/gui/wato/_notification_parameter/registration.py @@ -11,6 +11,7 @@ ) from . import _cisco_webex_teams as cisco_webex_teams +from . import _flowtriq as flowtriq from . import _ilert as ilert from . import _mail as mail from . import _ms_teams as ms_teams @@ -42,6 +43,13 @@ def register( form_spec=cisco_webex_teams.form_spec, ) ) + notification_parameter_registry.register( + NotificationParameter( + ident="flowtriq", + spec=lambda: convert_dictionary_formspec_to_valuespec(flowtriq.form_spec), + form_spec=flowtriq.form_spec, + ) + ) notification_parameter_registry.register( NotificationParameter( ident="victorops", diff --git a/cmk/utils/notify_types.py b/cmk/utils/notify_types.py index 0acd80117c6..9bcc7ce91ff 100644 --- a/cmk/utils/notify_types.py +++ b/cmk/utils/notify_types.py @@ -714,9 +714,20 @@ class SplunkPluginModel(TypedDict, total=False): url_prefix: URLPrefix +class FlowtriqPluginModel(TypedDict, total=False): + webhook_url: Required[WebhookURL] + api_key: CheckmkPassword + ignore_ssl: Literal[True] + proxy_url: ProxyUrl + url_prefix: URLPrefix + + CiscoPluginName = Literal["cisco_webex_teams"] CiscoNotify = tuple[CiscoPluginName, CiscoPluginModel | None] +FlowtriqPluginName = Literal["flowtriq"] +FlowtriqNotify = tuple[FlowtriqPluginName, FlowtriqPluginModel | None] + MkeventdPluginName = Literal["mkeventd"] MkeventdNotify = tuple[MkeventdPluginName, MKEventdPluginModel | None] @@ -775,6 +786,7 @@ class SplunkPluginModel(TypedDict, total=False): MailNotify | AsciiMailNotify | CiscoNotify + | FlowtriqNotify | MkeventdNotify | IlertNotify | JiraNotify @@ -794,6 +806,7 @@ class SplunkPluginModel(TypedDict, total=False): BuiltInPluginNames = ( CiscoPluginName + | FlowtriqPluginName | MkeventdPluginName | AsciiMailPluginName | MailPluginName @@ -817,6 +830,7 @@ class SplunkPluginModel(TypedDict, total=False): MailPluginModel | AsciiMailPluginModel | CiscoPluginModel + | FlowtriqPluginModel | MKEventdPluginModel | IlertPluginModel | JiraIssuePluginModel diff --git a/packages/cmk-notification-plugins/BUILD b/packages/cmk-notification-plugins/BUILD index 0fd3275bf48..1b65f2c75ae 100644 --- a/packages/cmk-notification-plugins/BUILD +++ b/packages/cmk-notification-plugins/BUILD @@ -93,6 +93,7 @@ pkg_files( renames = { "notifications/asciimail.py": "asciimail", "notifications/cisco_webex_teams.py": "cisco_webex_teams", + "notifications/flowtriq.py": "flowtriq", "notifications/ilert.py": "ilert", "notifications/mail.py": "mail", "notifications/msteams.py": "msteams", diff --git a/packages/cmk-notification-plugins/cmk/notification_plugins/flowtriq.py b/packages/cmk-notification-plugins/cmk/notification_plugins/flowtriq.py new file mode 100644 index 00000000000..5b01079fb9f --- /dev/null +++ b/packages/cmk-notification-plugins/cmk/notification_plugins/flowtriq.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# Copyright (C) 2024 Checkmk GmbH - License: GNU General Public License v2 +# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and +# conditions defined in the file COPYING, which is part of this source code package. +r""" +Send notification messages to Flowtriq +======================================= + +Post alerts to a Flowtriq webhook endpoint for DDoS detection +and traffic analytics. +""" + +from cmk.notification_plugins.utils import ( + get_password_from_env_or_context, + host_url_from_context, + post_request, + process_by_status_code, + service_url_from_context, +) + + +def _flowtriq_msg(context: dict[str, str]) -> dict[str, object]: + """Build the message payload for Flowtriq""" + + if context.get("WHAT") == "SERVICE": + state = context["SERVICESTATE"] + service = context["SERVICEDESC"] + output = context["SERVICEOUTPUT"] + url = service_url_from_context(context) + else: + state = context["HOSTSTATE"] + service = "" + output = context["HOSTOUTPUT"] + url = host_url_from_context(context) + + msg: dict[str, object] = { + "source": "checkmk", + "host": context["HOSTNAME"], + "host_address": context.get("HOSTADDRESS", ""), + "service": service, + "state": state, + "output": output, + "notification_type": context["NOTIFICATIONTYPE"], + } + + if url: + msg["url"] = url + + return msg + + +def _get_headers() -> dict[str, str]: + """Build request headers, including optional API key""" + headers: dict[str, str] = { + "Content-type": "application/json", + } + + try: + api_key = get_password_from_env_or_context(key="NOTIFY_PARAMETER_API_KEY") + headers["X-API-Key"] = api_key + except (KeyError, IndexError): + pass + + return headers + + +def main() -> int: + return process_by_status_code( + post_request(_flowtriq_msg, headers=_get_headers()), + success_code=(200, 201, 202), + ) diff --git a/packages/cmk-notification-plugins/notifications/flowtriq.py b/packages/cmk-notification-plugins/notifications/flowtriq.py new file mode 100644 index 00000000000..cb81b581542 --- /dev/null +++ b/packages/cmk-notification-plugins/notifications/flowtriq.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# Flowtriq + +# Copyright (C) 2024 Checkmk GmbH - License: GNU General Public License v2 +# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and +# conditions defined in the file COPYING, which is part of this source code package. + +import sys + +from cmk.notification_plugins.flowtriq import main + +if __name__ == "__main__": + sys.exit(main()) diff --git a/packages/cmk-notification-plugins/tests/test_flowtriq.py b/packages/cmk-notification-plugins/tests/test_flowtriq.py new file mode 100644 index 00000000000..963f06d28e9 --- /dev/null +++ b/packages/cmk-notification-plugins/tests/test_flowtriq.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# Copyright (C) 2024 Checkmk GmbH - License: GNU General Public License v2 +# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and +# conditions defined in the file COPYING, which is part of this source code package. + +import pytest + +from cmk.notification_plugins.flowtriq import _flowtriq_msg + + +@pytest.mark.parametrize( + "context, result", + [ + ( + { + "PARAMETER_URL_PREFIX_1": "automatic_http", + "MONITORING_HOST": "localhost", + "OMD_SITE": "testsite", + "HOSTURL": "/view?key=val", + "SERVICEURL": "/view?key=val2", + "HOSTNAME": "site1", + "HOSTADDRESS": "127.0.0.1", + "SERVICEDESC": "CPU load", + "SERVICESTATE": "CRITICAL", + "SERVICEOUTPUT": "CPU load is critical", + "NOTIFICATIONTYPE": "PROBLEM", + "WHAT": "SERVICE", + }, + { + "source": "checkmk", + "host": "site1", + "host_address": "127.0.0.1", + "service": "CPU load", + "state": "CRITICAL", + "output": "CPU load is critical", + "notification_type": "PROBLEM", + "url": "http://localhost/testsite/view?key=val2", + }, + ), + ( + { + "PARAMETER_URL_PREFIX_1": "automatic_https", + "MONITORING_HOST": "localhost", + "OMD_SITE": "testsite", + "HOSTURL": "/view?key=val", + "HOSTNAME": "webserver01", + "HOSTADDRESS": "10.0.0.5", + "HOSTSTATE": "DOWN", + "HOSTOUTPUT": "PING CRITICAL - Packet loss = 100%", + "NOTIFICATIONTYPE": "PROBLEM", + "WHAT": "HOST", + }, + { + "source": "checkmk", + "host": "webserver01", + "host_address": "10.0.0.5", + "service": "", + "state": "DOWN", + "output": "PING CRITICAL - Packet loss = 100%", + "notification_type": "PROBLEM", + "url": "https://localhost/testsite/view?key=val", + }, + ), + ( + { + "HOSTNAME": "router01", + "HOSTADDRESS": "192.168.1.1", + "HOSTSTATE": "UP", + "HOSTOUTPUT": "PING OK - Packet loss = 0%", + "NOTIFICATIONTYPE": "RECOVERY", + "WHAT": "HOST", + }, + { + "source": "checkmk", + "host": "router01", + "host_address": "192.168.1.1", + "service": "", + "state": "UP", + "output": "PING OK - Packet loss = 0%", + "notification_type": "RECOVERY", + }, + ), + ], +) +def test_flowtriq_message(context: dict[str, str], result: dict[str, str]) -> None: + msg = _flowtriq_msg(context) + assert msg == result diff --git a/tests/integration/test_binaries.py b/tests/integration/test_binaries.py index c255df8d52e..52cf8b5e7cb 100644 --- a/tests/integration/test_binaries.py +++ b/tests/integration/test_binaries.py @@ -49,6 +49,7 @@ class RepoScript: NOTIFICATION_PLUGINS: Sequence[BinarySmoke] = [ BinarySmoke("asciimail", expected_stderr=r"KeyError.*NOTIF", path=_NOTIF_PATH), BinarySmoke("cisco_webex_teams", expected_stderr=r"KeyError.*NOTIF", path=_NOTIF_PATH), + BinarySmoke("flowtriq", expected_stderr=r"KeyError.*NOTIF", path=_NOTIF_PATH), BinarySmoke("ilert", expected_stderr=r"IndexError.*list index out of range", path=_NOTIF_PATH), BinarySmoke("mail", expected_stderr=r"KeyError.*NOTIF", path=_NOTIF_PATH), BinarySmoke("msteams", expected_stderr=r"KeyError.*NOTIF", path=_NOTIF_PATH), diff --git a/tests/unit/cmk/gui/watolib/notification_parameter/test_registry.py b/tests/unit/cmk/gui/watolib/notification_parameter/test_registry.py index ee340abfd75..e2ffa50aa89 100644 --- a/tests/unit/cmk/gui/watolib/notification_parameter/test_registry.py +++ b/tests/unit/cmk/gui/watolib/notification_parameter/test_registry.py @@ -20,6 +20,7 @@ def test_registered_notification_parameters() -> None: expected_plugins = [ "asciimail", "cisco_webex_teams", + "flowtriq", "ilert", "mail", "mkeventd", diff --git a/tests/unit/cmk/gui/watolib/test_configuration_entity.py b/tests/unit/cmk/gui/watolib/test_configuration_entity.py index aef679bb30a..e8feb28adbb 100644 --- a/tests/unit/cmk/gui/watolib/test_configuration_entity.py +++ b/tests/unit/cmk/gui/watolib/test_configuration_entity.py @@ -14,6 +14,7 @@ [ ("asciimail", "Legacy email (ASCII) parameter"), ("cisco_webex_teams", "Cisco Webex Teams parameter"), + ("flowtriq", "Flowtriq parameter"), ("ilert", "iLert parameter"), ("mail", "Email (HTML) parameter"), ("mkeventd", "Forward Notification to Event Console parameter"),