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

Wechat alerter #355

Closed
wants to merge 9 commits into from
Closed
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
70 changes: 68 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,71 @@
## Other changes
- None


# 2.x.x

## Breaking changes
- None

## New features
- [Wechat] Add support for wechat alerter - [#355](https://github.com/jertel/elastalert2/pull/355) - @daiwei233

## Other changes
- [Tests] Add test code. Changed ubuntu version of Dockerfile-test from latest to 21.10. - [#354](https://github.com/jertel/elastalert2/pull/354) - @nsano-rururu
- Remove Python 2.x compatibility code - [#354](https://github.com/jertel/elastalert2/pull/354) - @nsano-rururu


# 2.1.2
## Breaking changes
- None

## New features
- [Rocket.Chat] Add support for generating Kibana Discover URLs to Rocket.Chat alerter - [#260](https://github.com/jertel/elastalert2/pull/260) - @nsano-rururu
- [Jinja] Provide rule key/values as possible Jinja data inputs - [#281](https://github.com/jertel/elastalert2/pull/281) - @mrfroggg
- [Kubernetes] Add securityContext and podSecurityContext to Helm chart - [#289](https://github.com/jertel/elastalert2/pull/289) - @lepouletsuisse
- [Rocket.Chat] Add options: rocket_chat_ca_certs, rocket_chat_ignore_ssl_errors, rocket_chat_timeout - [#302](https://github.com/jertel/elastalert2/pull/302) - @nsano-rururu
- [Jinja] Favor match keys over colliding rule keys when resolving Jinja vars; also add alert_text_jinja unit test - [#311](https://github.com/jertel/elastalert2/pull/311) - @mrfroggg
- [Opsgenie] Added possibility to specify source and entity attrs - [#315](https://github.com/jertel/elastalert2/pull/315) - @konstantin-kornienko
- [ServiceNow] Add support for `servicenow_impact` and `servicenow_urgency` parameters for ServiceNow alerter - [#316](https://github.com/jertel/elastalert2/pull/316) - @randolph-esnet
- [Jinja] Add Jinja support to alert_subject - [#318](https://github.com/jertel/elastalert2/pull/318) - @mrfroggg
@lepouletsuisse
- Metrics will now include time_taken, representing the execution duration of the rule - [#324](https://github.com/jertel/elastalert2/pull/324) - @JeffAshton

## Other changes
- [Prometheus] Continue fix for prometheus wrapper writeback function signature - [#256](https://github.com/jertel/elastalert2/pull/256) - @greut
- [Stomp] Improve exception handling in alerter - [#261](https://github.com/jertel/elastalert2/pull/261) - @nsano-rururu
- [AWS] Improve exception handling in Amazon SES and SNS alerters - [#264](https://github.com/jertel/elastalert2/pull/264) - @nsano-rururu
- [Docs] Clarify documentation for starting ElastAlert 2 - [#265](https://github.com/jertel/elastalert2/pull/265) - @ferozsalam
- Add exception handling for unsupported operand type - [#266](https://github.com/jertel/elastalert2/pull/266) - @nsano-rururu
- [Docs] Improve documentation for Python build requirements - [#267](https://github.com/jertel/elastalert2/pull/267) - @nsano-rururu
- [DataDog] Correct alerter logging - [#268](https://github.com/jertel/elastalert2/pull/268) - @nsano-rururu
- [Docs] Correct parameter code documentation for main ElastAlert runner - [#269](https://github.com/jertel/elastalert2/pull/269) - @ferozsalam
- [Command] alerter will now fail during init instead of during alert if given invalid command setting - [#270](https://github.com/jertel/elastalert2/pull/270) - @nsano-rururu
- [Docs] Consolidate all examples into a new examples/ sub folder - [#271](https://github.com/jertel/elastalert2/pull/271) - @ferozsalam
- [TheHive] Add example rule with Kibana Discover URL and query values in alert text - [#276](https://github.com/jertel/elastalert2/pull/276) - @markus-nclose
- Upgrade pytest-xdist from 2.2.1 to 2.3.0; clarify HTTPS support in docs; Add additional logging - [#283](https://github.com/jertel/elastalert2/pull/283) - @nsano-rururu
- [Tests] Add more alerter test coverage - [#284](https://github.com/jertel/elastalert2/pull/284) - @nsano-rururu
- [Tests] Improve structure and placement of test-related files in project tree - [#287](https://github.com/jertel/elastalert2/pull/287) - @ferozsalam
- Only attempt to adjust timezone if timezone is set to a non-empty string - [#288](https://github.com/jertel/elastalert2/pull/288) - @ferozsalam
- [Kubernetes] Deprecated `podSecurityPolicy` feature in Helm Chart as [it's deprecated in Kubernetes 1.21](https://kubernetes.io/blog/2021/04/06/podsecuritypolicy-deprecation-past-present-and-future/) - [#289](https://github.com/jertel/elastalert2/pull/289) - @lepouletsuisse
- [Slack] Fix slack_channel_override schema - [#291](https://github.com/jertel/elastalert2/pull/291) - @JeffAshton
- [Rocket.Chat] Fix rocket_chat_channel_override schema - [#293](https://github.com/jertel/elastalert2/pull/293) - @nsano-rururu
- [Tests] Increase code coverage - [#294](https://github.com/jertel/elastalert2/pull/294) - @nsano-rururu
- [Docs] Added Kibana Discover sample - [#295](https://github.com/jertel/elastalert2/pull/295) - @nsano-rururu
- [AWS] Remove deprecated boto_profile setting - [#299](https://github.com/jertel/elastalert2/pull/299) - @nsano-rururu
- [Slack] Correct slack_alert_fields schema definition - [#300](https://github.com/jertel/elastalert2/pull/300) - @nsano-rururu
- [Tests] Correct code coverage to eliminate warnings - [#301](https://github.com/jertel/elastalert2/pull/301) - @nsano-rururu
- Eliminate unnecessary calls to Elasticsearch - [#303](https://github.com/jertel/elastalert2/pull/303) - @JeffAshton
- [Zabbix] Fix timezone parsing - [#304](https://github.com/jertel/elastalert2/pull/304) - @JeffAshton
- Improve logging of scheduler - [#305](https://github.com/jertel/elastalert2/pull/305) - @JeffAshton
- [Jinja] Update Jinja from 2.11.3 to 3.0.1; Improve handling of colliding variables - [#311](https://github.com/jertel/elastalert2/pull/311) - @mrfroggg
- [TheHive] Force observable artifacts to be strings - [#313](https://github.com/jertel/elastalert2/pull/313) - @pandvan
- Upgrade pylint from <2.9 to <2.10 - [#314](https://github.com/jertel/elastalert2/pull/314) - @nsano-rururu
- [ChatWork] Enforce character limit - [#319](https://github.com/jertel/elastalert2/pull/319) - @nsano-rururu
- [LineNotify] Enforce character limit - [#320](https://github.com/jertel/elastalert2/pull/320) - @nsano-rururu
- [Discord] Remove trailing backticks from alert body - [#321](https://github.com/jertel/elastalert2/pull/321) - @nsano-rururu
- Redirecting warnings to logging module - [#325](https://github.com/jertel/elastalert2/pull/325) - @JeffAshton

# 2.1.1

## Breaking changes
Expand Down Expand Up @@ -54,7 +119,7 @@
# 2.1.0

## Breaking changes
- TheHive alerter refactoring - [#142](https://github.com/jertel/elastalert2/pull/142) - @ferozsalam
- TheHive alerter refactoring - [#142](https://github.com/jertel/elastalert2/pull/142) - @ferozsalam
- See the updated documentation for changes required to alert formatting
- Dockerfile refactor for performance and size improvements - [#102](https://github.com/jertel/elastalert2/pull/102) - @jgregmac
- Dockerfile base image changed from `python/alpine` to `python/slim-buster` to take advantage of pre-build python wheels, accelerate build times, and reduce image size. If you have customized an image, based on jertel/elastalert2, you may need to make adjustments.
Expand All @@ -66,7 +131,7 @@
## New features
- Support for multiple rules directories and fix `..data` Kubernetes/Openshift recursive directories in FileRulesLoader [#157](https://github.com/jertel/elastalert2/pull/157) - @mrfroggg
- Support environment variable substition in yaml files - [#149](https://github.com/jertel/elastalert2/pull/149) - @archfz
- Update schema.yaml and enhance documentation for Email alerter - [#144](https://github.com/jertel/elastalert2/pull/144) - @nsano-rururu
- Update schema.yaml and enhance documentation for Email alerter - [#144](https://github.com/jertel/elastalert2/pull/144) - @nsano-rururu
- Default Email alerter to use port 25, and require http_post_url for HTTP Post alerter - [#143](https://github.com/jertel/elastalert2/pull/143) - @nsano-rururu
- Support extra message features for Slack and Mattermost - [#140](https://github.com/jertel/elastalert2/pull/140) - @nsano-rururu
- Support a footer in alert text - [#133](https://github.com/jertel/elastalert2/pull/133) - @nsano-rururu
Expand Down Expand Up @@ -106,3 +171,4 @@
- Container images are now built and published via GitHub actions instead of relying on DockerHub's automated builds.
- Update PIP library description and Helm chart description to be consistent.
- Continue updates to change references from _ElastAlert_ to _ElastAlert 2_

1 change: 1 addition & 0 deletions docs/source/elastalert.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Currently, we have support built in for these alert types:
- Telegram
- TheHive
- Twilio
- Wechat
- Zabbix

Additional rule types and alerts can be easily imported or written. (See :ref:`Writing rule types <writingrules>` and :ref:`Writing alerts <writingalerts>`)
Expand Down
64 changes: 64 additions & 0 deletions docs/source/ruletypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2780,6 +2780,70 @@ Example With SMS usage::
twilio_auth_token: "abcdefghijklmnopqrstuvwxyz012345"
twilio_account_sid: "ABCDEFGHIJKLMNOPQRSTUVWXYZ01234567"

Wechat
~~~~~~~~

Wechat will send notification with a application. The body of the notification is formatted the same as with other alerters. See: https://work.weixin.qq.com/api/doc/90000/90135/90236

Required:

``wechat_corp_id``: Wechat corp id.

``wechat_agent_id``: The wechat agent id you are going to send the message.

``wechat_secret``: The wechat agent secret.

``wechat_to_party`` & ``wechat_to_user`` & ``wechat_to_tag``: One of wechat_to_party, wechat_to_user, or wechat_to_tag.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should change the & to | to avoid confusion since not all are required.


Optional:

``wechat_msgtype``: Wechat msgtype, defaults to ``text``. ``textcard``, ``markdown``.

``wechat_textcard_url``: Wechat textcard url while ``wechat_msgtype`` is ``textcard``, default to ``null_url``.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
``wechat_textcard_url``: Wechat textcard url while ``wechat_msgtype`` is ``textcard``, default to ``null_url``.
``wechat_textcard_url``: Wechat textcard url while ``wechat_msgtype`` is ``textcard``, defaults to ``null_url``.


``wechat_enable_duplicate_check``: ``enable_duplicate_check``, defaults to ``0``.

``wechat_duplicate_check_interval``: ``duplicate_check_interval``, defaults to ``1800``.

``wechat_proxy``: HTTP proxy.

``wechat_proxy_login``: HTTP proxy login.

``wechat_proxy_pass``: HTTP proxy pass.

``wechat_textcard_url``: The url of a textcard button..


Example msgtype : text::

alert:
- "wechat"
wechat_corp_id: "a_corp_id"
wechat_secret: "a_secret"
wechat_agent_id: "a_agent_id"
wechat_to_user: "user1|user2|user3"

Example msgtype : textcard::

alert:
- "wechat"
wechat_corp_id: "a_corp_id"
wechat_secret: "a_secret"
wechat_agent_id: "a_agent_id"
wechat_to_user: "@all"
wechat_msgtype: "textcard"
wechat_textcard_url: "http://{your_kibana_url}"

Example msgtype : markdown::

alert:
- "wechat"
wechat_corp_id: "a_corp_id"
wechat_secret: "a_secret"
wechat_agent_id: "a_agent_id"
wechat_to_tag: "tag1"
wechat_msgtype: "markdown"

Zabbix
~~~~~~

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

import requests
from requests.exceptions import RequestException
from requests.auth import HTTPProxyAuth

from elastalert.alerts import Alerter
from elastalert.util import elastalert_logger, EAException


class WechatAlerter(Alerter):
required_options = frozenset(['wechat_corp_id', 'wechat_secret', 'wechat_agent_id'])

def __init__(self, *args):
super(WechatAlerter, self).__init__(*args)
self.wechat_corp_id = self.rule.get('wechat_corp_id', '')
self.wechat_secret = self.rule.get('wechat_secret', '')
self.wechat_agent_id = self.rule.get('wechat_agent_id', '')
self.wechat_msgtype = self.rule.get('wechat_msgtype', 'text')
self.wechat_to_party = self.rule.get('wechat_to_party', None)
self.wechat_to_user = self.rule.get('wechat_to_user', None)
self.wechat_to_tag = self.rule.get('wechat_to_tag', None)
self.wechat_textcard_url = self.rule.get('wechat_textcard_url', 'null_url')
self.wechat_enable_duplicate_check = self.rule.get('wechat_enable_duplicate_check', 0)
self.wechat_duplicate_check_interval = self.rule.get('wechat_duplicate_check_interval', 1800)

self.wechat_proxy = self.rule.get('wechat_proxy', None)
self.wechat_proxy_login = self.rule.get('wechat_proxy_login', None)
self.wechat_proxy_password = self.rule.get('wechat_proxy_pass', None)
self.proxies = {'https': self.wechat_proxy} if self.wechat_proxy else None
self.auth = HTTPProxyAuth(self.wechat_proxy_login, self.wechat_proxy_password) if self.wechat_proxy_login else None

self.wechat_access_token = ''
self.wechat_token_url = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={}&corpsecret={}'
self.wechat_message_url = 'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={}'
self.expires_in = datetime.datetime.now() - datetime.timedelta(seconds=3600)

def get_token(self):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function could use some unit test coverage.

if self.expires_in >= datetime.datetime.now() and self.wechat_access_token:
return

try:
response = requests.get(self.wechat_token_url.format(self.wechat_corp_id,
self.wechat_secret), proxies=self.proxies, auth=self.auth)
response.raise_for_status()
except RequestException as e:
raise EAException('Get wechat access_token failed , stacktrace:%s' % e)

token_json = response.json()

if 'access_token' not in token_json:
raise EAException('Get wechat access_token failed, cause :%s' % response.text())

self.wechat_access_token = token_json['access_token']
self.expires_in = datetime.datetime.now() + datetime.timedelta(seconds=token_json['expires_in'])

def format_body(self, body):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use a simple unit test.

return body.encode('utf8')

def alert(self, matches):
if not self.wechat_to_user and not self.wechat_to_party and not self.wechat_to_tag:
raise EAException('All wechat_to_user & wechat_to_party & wechat_to_tag invalid.')

self.get_token()
headers = {'content-type': 'application/json'}
title = self.create_title(matches)
body = self.create_alert_body(matches)

# message was cropped, see: https://work.weixin.qq.com/api/doc/90000/90135/90236
if len(body) > 2048:
body = body[:2045] + "..."

payload = {
'touser': self.wechat_to_user or '',
'toparty': self.wechat_to_party or '',
'totag': self.wechat_to_tag or '',
'agentid': self.wechat_agent_id,
'enable_duplicate_check': self.wechat_enable_duplicate_check,
'duplicate_check_interval': self.wechat_duplicate_check_interval
}

if self.wechat_msgtype == 'text':
payload['msgtype'] = 'text'
payload['text'] = {
'content': body
}
elif self.wechat_msgtype == 'textcard':
payload['msgtype'] = 'textcard'
payload['textcard'] = {
'title': title,
'description': body,
'url': self.wechat_textcard_url
}
elif self.wechat_msgtype == 'markdown':
payload['msgtype'] = 'markdown'
payload['markdown'] = {
'content': body
}

try:
response = requests.post(self.wechat_message_url.format(self.wechat_access_token), data=json.dumps(
payload, ensure_ascii=False), headers=headers, proxies=self.proxies, auth=self.auth)
response.raise_for_status()
except RequestException as e:
raise EAException('Error sending wechat msg: %s' % e)
elastalert_logger.info('Alert sent to wechat.')

def get_info(self):
return {'type': 'wechat'}
4 changes: 3 additions & 1 deletion elastalert/loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import elastalert.alerters.thehive
import elastalert.alerters.twilio
import elastalert.alerters.victorops
import elastalert.alerters.wechat
from elastalert import alerts
from elastalert import enhancements
from elastalert import ruletypes
Expand Down Expand Up @@ -116,7 +117,8 @@ class RulesLoader(object):
'chatwork': elastalert.alerters.chatwork.ChatworkAlerter,
'datadog': elastalert.alerters.datadog.DatadogAlerter,
'ses': elastalert.alerters.ses.SesAlerter,
'rocketchat': elastalert.alerters.rocketchat.RocketChatAlerter
'rocketchat': elastalert.alerters.rocketchat.RocketChatAlerter,
'wechat': elastalert.alerters.wechat.WechatAlerter
}

# A partial ordering of alert types. Relative order will be preserved in the resulting alerts list
Expand Down
7 changes: 6 additions & 1 deletion elastalert/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ properties:
dingtalk_single_title: {type: string}
dingtalk_single_url: {type: string}
dingtalk_btn_orientation: {type: string}

## Discord
discord_webhook_url: {type: string}
discord_emoji_title: {type: string}
Expand Down Expand Up @@ -551,6 +551,11 @@ properties:
twilio_message_service_sid: {type: string}
twilio_use_copilot: {type: boolean}

### Wechat
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be more schema definitions listed here since this alerter has a large number of input parameters.

wechat_corp_id: {type: string}
wechat_secret: {type: string}
wechat_agent_id: {type: integer}

### Zabbix
zbx_sender_host: {type: string}
zbx_sender_port: {type: integer}
Expand Down
65 changes: 65 additions & 0 deletions tests/alerters/wechat_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import json

import pytest

from elastalert.loaders import FileRulesLoader
from elastalert.alerters.wechat import WechatAlerter


def test_wechat_getinfo():
rule = {
'name': 'Test Wechat Alerter',
'type': 'any',
'wechat_corp_id': 'test_wechat_corp_id',
'wechat_secret': 'test_wechat_secret',
'wechat_agent_id': 'test_wechat_agent_id',
'alert': [],
'alert_subject': 'Test Wechat Alert'
}
rules_loader = FileRulesLoader({})
rules_loader.load_modules(rule)
alert = WechatAlerter(rule)

expected_data = {'type': 'wechat'}
actual_data = alert.get_info()
assert expected_data == actual_data


@pytest.mark.parametrize('wechat_corp_id, wechat_secret, wechat_agent_id, expected_data', [
('', '', '', 'Missing required option(s): wechat_corp_id, wechat_secret, wechat_agent_id'),
('xxxx1', '', '', 'Missing required option(s): wechat_corp_id, wechat_secret, wechat_agent_id'),
('', 'xxxx2', '', 'Missing required option(s): wechat_corp_id, wechat_secret, wechat_agent_id'),
('xxxx1', 'xxxx2', '', 'Missing required option(s): wechat_corp_id, wechat_secret, wechat_agent_id'),
('xxxx1', '', 'xxxx3', 'Missing required option(s): wechat_corp_id, wechat_secret, wechat_agent_id'),
('', 'xxxx2', 'xxxx3', 'Missing required option(s): wechat_corp_id, wechat_secret, wechat_agent_id'),
('xxxx1', 'xxxx2', 'xxxx3',
{
'type': 'wechat'
}),
])
def test_wechat_required_error(wechat_corp_id, wechat_secret, wechat_agent_id, expected_data):
try:
rule = {
'name': 'Test Wechat Rule',
'type': 'any',
'alert': [],
'alert_subject': 'Test Wechat'
}

if wechat_corp_id != '':
rule['wechat_corp_id'] = wechat_corp_id

if wechat_secret != '':
rule['wechat_secret']=wechat_secret

if wechat_agent_id != '':
rule['wechat_agent_id'] = wechat_agent_id

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

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