Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Webex incoming webhook alerter #1635

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

## New features
- [MicrosoftPowerAutomate] Add support for 'ms_power_automate_webhook_url_from_field' option to dynamically select the webhook URL from the match. - [#1623](https://github.com/jertel/elastalert2/pull/1623) - @aizerin
- Add Webex Incoming Webhook alerter - @dennis-trapp

## Other changes
- Fix `schema.yaml` to support Kibana 8.17 - [#1631](https://github.com/jertel/elastalert2/pull/1631) - @vpiserchia
Expand Down
24 changes: 24 additions & 0 deletions docs/source/alerts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ or
- tencent_sms
- twilio
- victorops
- webex_incoming
- workwechat
- zabbix

Expand Down Expand Up @@ -2524,6 +2525,29 @@ Example with SMS usage::
twilio_auth_token: "abcdefghijklmnopqrstuvwxyz012345"
twilio_account_sid: "ABCDEFGHIJKLMNOPQRSTUVWXYZ01234567"

Webex Incoming Webhook
~~~~~~~~~~

Webex Incoming Webhook alerter will send notification to a predefined incoming webhook in Webex application. The body of the notification is formatted the same as with other alerters.

Official Webex incoming webhook documentation: https://apphub.webex.com/applications/incoming-webhooks-cisco-systems-38054-23307-75252

Required:

``webex_incoming_webhook_id``: Webex incoming webhook ID.
``webex_incoming_msgtype``: Webex incoming webhook message format. default to ``text``. ``markdown``

Example usage::
alert_text: "**{0}** - ALERT on host {1}"
alert_text_args:
- name
- hostname
alert:
- webex_incoming
alert_text_type: alert_text_only
webex_incoming_webhook_id: "your webex incoming webhook id"
webex_incoming_msgtype: "markdown"

WorkWechat
~~~~~~~~~~

Expand Down
1 change: 1 addition & 0 deletions docs/source/elastalert.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Currently, we have support built in for these alert types:
- Tencent SMS
- TheHive
- Twilio
- Webex Incoming Webhook
- WorkWechat
- Zabbix

Expand Down
55 changes: 55 additions & 0 deletions elastalert/alerters/webex_incoming.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import json
import warnings

import requests
from elastalert.alerts import Alerter, DateTimeEncoder
from elastalert.util import EAException, elastalert_logger
from requests import RequestException


class WebexIncomingAlerter(Alerter):
"""Creates a Webex Incoming Webook message for each alert"""

required_options = frozenset(["webex_incoming_webhook_id"])

def __init__(self, rule):
super(WebexIncomingAlerter, self).__init__(rule)
self.webex_incoming_webhook_id = self.rule.get(
"webex_incoming_webhook_id", None
)
self.webex_incoming_webhook_url = f"https://webexapis.com/v1/webhooks/incoming/{self.webex_incoming_webhook_id}"
self.webex_incoming_msgtype = self.rule.get("webex_incoming_msgtype", "text")

def alert(self, matches):
title = self.create_title(matches)
body = self.create_alert_body(matches)

headers = {
"Content-Type": "application/json",
"Accept": "application/json;charset=utf-8",
}

if self.webex_incoming_msgtype == "markdown":
payload = {"markdown": body}
elif self.webex_incoming_msgtype == "text":
payload = {"text": body}

try:
response = requests.post(
self.webex_incoming_webhook_url,
data=json.dumps(payload, cls=DateTimeEncoder),
headers=headers,
)
warnings.resetwarnings()
response.raise_for_status()
except RequestException as e:
raise EAException("Error posting to webex_incoming: %s" % e)

elastalert_logger.info("Trigger sent to webex_incoming")

def get_info(self):
return {
"type": "webex_incoming",
"webex_incoming_msgtype": self.webex_incoming_msgtype,
"webex_incoming_webhook_url": self.webex_incoming_webhook_url,
}
2 changes: 2 additions & 0 deletions elastalert/loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import elastalert.alerters.thehive
import elastalert.alerters.twilio
import elastalert.alerters.victorops
import elastalert.alerters.webex_incoming
import elastalert.alerters.workwechat
from elastalert import alerts
from elastalert import enhancements
Expand Down Expand Up @@ -134,6 +135,7 @@ class RulesLoader(object):
'discord': elastalert.alerters.discord.DiscordAlerter,
'dingtalk': elastalert.alerters.dingtalk.DingTalkAlerter,
'lark': elastalert.alerters.lark.LarkAlerter,
'webex_incoming': elastalert.alerters.webex_incoming.WebexIncomingAlerter,
'workwechat': elastalert.alerters.workwechat.WorkWechatAlerter,
'chatwork': elastalert.alerters.chatwork.ChatworkAlerter,
'datadog': elastalert.alerters.datadog.DatadogAlerter,
Expand Down
4 changes: 4 additions & 0 deletions elastalert/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,10 @@ properties:
twilio_message_service_sid: {type: string}
twilio_use_copilot: {type: boolean}

### Webex Incoming
webex_incoming_webhook_id: { type: string }
webex_incoming_msgtype: { type: string }

### WorkWechat
work_wechat_bot_id: { type: string }
work_wechat_msgtype: { type: string }
Expand Down
123 changes: 123 additions & 0 deletions tests/alerters/webex_incoming_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import json
import logging
from unittest import mock

import pytest
from requests import RequestException

from elastalert.alerters.webex_incoming import WebexIncomingAlerter
from elastalert.loaders import FileRulesLoader
from elastalert.util import EAException


def test_webex_incoming_text(caplog):
caplog.set_level(logging.INFO)
rule = {
'name': 'Test Webex Incoming Rule',
'type': 'any',
'webex_incoming_msgtype': 'text',
'webex_incoming_webhook_id': 'xxxxxxx',
'alert': [],
}
rules_loader = FileRulesLoader({})
rules_loader.load_modules(rule)
alert = WebexIncomingAlerter(rule)
match = {
'@timestamp': '2024-01-30T00:00:00',
'somefield': 'foobar'
}
with mock.patch('requests.post') as mock_post_request:
alert.alert([match])

expected_data = {
'text': 'Test Webex Incoming Rule\n\n@timestamp: 2024-01-30T00:00:00\nsomefield: foobar\n'
}

mock_post_request.assert_called_once_with(
'https://webexapis.com/v1/webhooks/incoming/xxxxxxx',
data=mock.ANY,
headers={
'Content-Type': 'application/json',
'Accept': 'application/json;charset=utf-8'
}
)

actual_data = json.loads(mock_post_request.call_args_list[0][1]['data'])
assert expected_data == actual_data
assert ('elastalert', logging.INFO, 'Trigger sent to webex_incoming') == caplog.record_tuples[0]


def test_webex_incoming_ea_exception():
with pytest.raises(EAException) as ea:
rule = {
'name': 'Test Webex Incoming Rule',
'type': 'any',
'webex_incoming_msgtype': 'text',
'webex_incoming_webhook_id': 'xxxxxxx',
'alert': [],
}
rules_loader = FileRulesLoader({})
rules_loader.load_modules(rule)
alert = WebexIncomingAlerter(rule)
match = {
'@timestamp': '2024-01-30T00:00:00',
'somefield': 'foobar'
}
mock_run = mock.MagicMock(side_effect=RequestException)
with mock.patch('requests.post', mock_run), pytest.raises(RequestException):
alert.alert([match])
assert 'Error posting to webex_incoming: ' in str(ea)


def test_webex_incoming_getinfo():
rule = {
'name': 'Test Webex Incoming Rule',
'type': 'any',
'webex_incoming_msgtype': 'text',
'webex_incoming_webhook_id': 'xxxxxxx',
'alert': [],
}
rules_loader = FileRulesLoader({})
rules_loader.load_modules(rule)
alert = WebexIncomingAlerter(rule)

expected_data = {
'type': 'webex_incoming',
'webex_incoming_msgtype': 'text',
'webex_incoming_webhook_url': 'https://webexapis.com/v1/webhooks/incoming/xxxxxxx'
}
actual_data = alert.get_info()
assert expected_data == actual_data


@pytest.mark.parametrize('webex_incoming_webhook_id, webex_incoming_msgtype, expected_data', [
('', '', 'Missing required option(s): webex_incoming_webhook_id, webex_incoming_msgtype'),
('xxxxxxx', 'yyyyyy',
{
'type': 'webex_incoming',
'webex_incoming_msgtype': 'yyyyyy',
'webex_incoming_webhook_url': 'https://webexapis.com/v1/webhooks/incoming/xxxxxxx'
}),
])
def test_webex_incoming_required_error(webex_incoming_webhook_id, webex_incoming_msgtype, expected_data):
try:
rule = {
'name': 'Test Webex Incoming Rule',
'type': 'any',
'alert': [],
}

if webex_incoming_webhook_id:
rule['webex_incoming_webhook_id'] = webex_incoming_webhook_id

if webex_incoming_msgtype:
rule['webex_incoming_msgtype'] = webex_incoming_msgtype

rules_loader = FileRulesLoader({})
rules_loader.load_modules(rule)
alert = WebexIncomingAlerter(rule)

actual_data = alert.get_info()
assert expected_data == actual_data
except Exception as ea:
assert expected_data in str(ea)
Loading