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"),