From 0da7dc10207e33c360a51f61cc27d2bab35e2f8d Mon Sep 17 00:00:00 2001 From: thib12 Date: Tue, 18 Jan 2022 16:44:35 +0100 Subject: [PATCH 1/3] [Teams] - Kibana Discover URL and Facts --- docs/source/ruletypes.rst | 36 +++++++ elastalert/alerters/teams.py | 40 ++++++- elastalert/schema.yaml | 14 +++ tests/alerters/teams_test.py | 203 ++++++++++++++++++++++++++++++++++- 4 files changed, 284 insertions(+), 9 deletions(-) diff --git a/docs/source/ruletypes.rst b/docs/source/ruletypes.rst index 6a5a2dcd..738cd453 100644 --- a/docs/source/ruletypes.rst +++ b/docs/source/ruletypes.rst @@ -2500,6 +2500,42 @@ Optional: ``ms_teams_alert_fixed_width``: By default this is ``False`` and the notification will be sent to MS Teams as-is. Teams supports a partial Markdown implementation, which means asterisk, underscore and other characters may be interpreted as Markdown. Currenlty, Teams does not fully implement code blocks. Setting this attribute to ``True`` will enable line by line code blocks. It is recommended to enable this to get clearer notifications in Teams. +``ms_teams_alert_facts``: You can add additional facts to your MS Teams alerts using this field. Specify the title using `name` and a value for the field using `value`. + +Example ms_teams_alert_facts:: + + ms_teams_alert_facts: + - name: Host + value: monitor.host + - name: Status + value: monitor.status + - name: Zone + value: beat.name + +``ms_teams_attach_kibana_discover_url``: Enables the attachment of the ``kibana_discover_url`` to the MS Teams notification. The config ``generate_kibana_discover_url`` must also be ``True`` in order to generate the url. Defaults to ``False``. + +``ms_teams_kibana_discover_title``: The title of the Kibana Discover url attachment. Defaults to ``Discover in Kibana``. + +Example ms_teams_attach_kibana_discover_url, ms_teams_kibana_discover_title:: + + # (Required) + generate_kibana_discover_url: True + kibana_discover_app_url: "http://localhost:5601/app/discover#/" + kibana_discover_index_pattern_id: "4babf380-c3b1-11eb-b616-1b59c2feec54" + kibana_discover_version: "7.15" + + # (Optional) + kibana_discover_from_timedelta: + minutes: 10 + kibana_discover_to_timedelta: + minutes: 10 + + # (Required) + ms_teams_attach_kibana_discover_url: True + + # (Optional) + ms_teams_kibana_discover_title: "Discover in Kibana" + ``ms_teams_ca_certs``: Set this option to ``True`` if you want to validate the SSL certificate. ``ms_teams_ignore_ssl_errors``: By default ElastAlert 2 will verify SSL certificate. Set this option to ``False`` if you want to ignore SSL errors. diff --git a/elastalert/alerters/teams.py b/elastalert/alerters/teams.py index 39b2a1e1..0f58cb0a 100644 --- a/elastalert/alerters/teams.py +++ b/elastalert/alerters/teams.py @@ -1,8 +1,9 @@ +import copy import json import requests from elastalert.alerts import Alerter, DateTimeEncoder -from elastalert.util import EAException, elastalert_logger +from elastalert.util import EAException, elastalert_logger, lookup_es_key from requests.exceptions import RequestException @@ -21,6 +22,9 @@ def __init__(self, rule): self.ms_teams_theme_color = self.rule.get('ms_teams_theme_color', '') self.ms_teams_ca_certs = self.rule.get('ms_teams_ca_certs') self.ms_teams_ignore_ssl_errors = self.rule.get('ms_teams_ignore_ssl_errors', False) + self.ms_teams_alert_facts = self.rule.get('ms_teams_alert_facts', '') + self.ms_teams_attach_kibana_discover_url = self.rule.get('ms_teams_attach_kibana_discover_url', False) + self.ms_teams_kibana_discover_title = self.rule.get('ms_teams_kibana_discover_title', 'Discover in Kibana') def format_body(self, body): if self.ms_teams_alert_fixed_width: @@ -28,13 +32,21 @@ def format_body(self, body): body = "```{0}```".format('```\n\n```'.join(x for x in body.split('\n'))).replace('\n``````', '') return body + def populate_facts(self, matches): + alert_facts = [] + for arg in self.ms_teams_alert_facts: + arg = copy.copy(arg) + arg['value'] = lookup_es_key(matches[0], arg['value']) + alert_facts.append(arg) + return alert_facts + def alert(self, matches): body = self.create_alert_body(matches) body = self.format_body(body) # post to Teams headers = {'content-type': 'application/json'} - + if self.ms_teams_ca_certs: verify = self.ms_teams_ca_certs else: @@ -49,18 +61,38 @@ def alert(self, matches): '@context': 'http://schema.org/extensions', 'summary': self.ms_teams_alert_summary, 'title': self.create_title(matches), - 'text': body + 'sections': [{'text': body}], } + + if self.ms_teams_alert_facts != '': + payload['sections'][0]['facts'] = self.populate_facts(matches) + if self.ms_teams_theme_color != '': payload['themeColor'] = self.ms_teams_theme_color + if self.ms_teams_attach_kibana_discover_url: + kibana_discover_url = lookup_es_key(matches[0], 'kibana_discover_url') + if kibana_discover_url: + payload['potentialAction'] = [ + { + '@type': 'OpenUri', + 'name': self.ms_teams_kibana_discover_title, + 'targets': [ + { + 'os': 'default', + 'uri': kibana_discover_url, + } + ], + } + ] + for url in self.ms_teams_webhook_url: try: response = requests.post(url, data=json.dumps(payload, cls=DateTimeEncoder), headers=headers, proxies=proxies, verify=verify) response.raise_for_status() except RequestException as e: - raise EAException("Error posting to ms teams: %s" % e) + raise EAException("Error posting to MS Teams: %s" % e) elastalert_logger.info("Alert sent to MS Teams") def get_info(self): diff --git a/elastalert/schema.yaml b/elastalert/schema.yaml index fa6b65c4..db7a00c9 100644 --- a/elastalert/schema.yaml +++ b/elastalert/schema.yaml @@ -46,6 +46,17 @@ definitions: type: array items: *slackField + msTeamsFact: &msTeamsFact + type: object + additionalProperties: false + properties: + name: {type: string} + value: {type: string} + + arrayOfMsTeamsFacts: &arrayOfMsTeamsFacts + type: array + items: *msTeamsFact + mattermostField: &mattermostField type: object additionalProperties: false @@ -508,6 +519,9 @@ properties: ms_teams_theme_color: {type: string} ms_teams_proxy: {type: string} ms_teams_alert_fixed_width: {type: boolean} + ms_teams_alert_facts: *arrayOfMsTeamsFacts + ms_teams_attach_kibana_discover_url: {type: boolean} + ms_teams_kibana_discover_title: {type: string} ms_teams_ca_certs: {type: boolean} ms_teams_ignore_ssl_errors: {type: boolean} diff --git a/tests/alerters/teams_test.py b/tests/alerters/teams_test.py index 1feea77b..39da9e32 100644 --- a/tests/alerters/teams_test.py +++ b/tests/alerters/teams_test.py @@ -36,7 +36,7 @@ def test_ms_teams(caplog): '@context': 'http://schema.org/extensions', 'summary': rule['ms_teams_alert_summary'], 'title': rule['alert_subject'], - 'text': BasicMatchString(rule, match).__str__() + 'sections': [{'text': BasicMatchString(rule, match).__str__()}] } mock_post_request.assert_called_once_with( rule['ms_teams_webhook_url'], @@ -78,7 +78,7 @@ def test_ms_teams_uses_color_and_fixed_width_text(): 'summary': rule['ms_teams_alert_summary'], 'title': rule['alert_subject'], 'themeColor': '#124578', - 'text': body + 'sections': [{'text': body}] } mock_post_request.assert_called_once_with( rule['ms_teams_webhook_url'], @@ -115,7 +115,7 @@ def test_ms_teams_proxy(): '@context': 'http://schema.org/extensions', 'summary': rule['ms_teams_alert_summary'], 'title': rule['alert_subject'], - 'text': BasicMatchString(rule, match).__str__() + 'sections': [{'text': BasicMatchString(rule, match).__str__()}] } mock_post_request.assert_called_once_with( rule['ms_teams_webhook_url'], @@ -147,7 +147,7 @@ def test_ms_teams_ea_exception(): mock_run = mock.MagicMock(side_effect=RequestException) with mock.patch('requests.post', mock_run), pytest.raises(RequestException): alert.alert([match]) - assert 'Error posting to ms teams: ' in str(ea) + assert 'Error posting to MS Teams: ' in str(ea) def test_ms_teams_getinfo(): @@ -240,7 +240,7 @@ def test_ms_teams_ca_certs(ca_certs, ignore_ssl_errors, excpet_verify): '@context': 'http://schema.org/extensions', 'summary': rule['ms_teams_alert_summary'], 'title': rule['alert_subject'], - 'text': BasicMatchString(rule, match).__str__() + 'sections': [{'text': BasicMatchString(rule, match).__str__()}] } mock_post_request.assert_called_once_with( rule['ms_teams_webhook_url'], @@ -250,3 +250,196 @@ def test_ms_teams_ca_certs(ca_certs, ignore_ssl_errors, excpet_verify): verify=excpet_verify ) assert expected_data == json.loads(mock_post_request.call_args_list[0][1]['data']) + + +def test_ms_teams_attach_kibana_discover_url_when_generated(): + rule = { + 'name': 'Test Rule', + 'type': 'any', + 'ms_teams_attach_kibana_discover_url': True, + 'ms_teams_webhook_url': 'http://test.webhook.url', + 'ms_teams_alert_summary': 'Alert from ElastAlert', + 'alert': [], + 'alert_subject': 'Cool subject', + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = MsTeamsAlerter(rule) + match = { + '@timestamp': '2016-01-01T00:00:00', + 'kibana_discover_url': 'http://kibana#discover' + } + with mock.patch('requests.post') as mock_post_request: + alert.alert([match]) + + expected_data = { + '@type': 'MessageCard', + '@context': 'http://schema.org/extensions', + 'summary': rule['ms_teams_alert_summary'], + 'title': rule['alert_subject'], + 'sections': [{'text': BasicMatchString(rule, match).__str__()}], + 'potentialAction': [ + { + '@type': 'OpenUri', + 'name': 'Discover in Kibana', + 'targets': [ + { + 'os': 'default', + 'uri': 'http://kibana#discover', + } + ], + } + ], + } + mock_post_request.assert_called_once_with( + rule['ms_teams_webhook_url'], + data=mock.ANY, + headers={'content-type': 'application/json'}, + proxies=None, + verify=True + ) + actual_data = json.loads(mock_post_request.call_args_list[0][1]['data']) + assert expected_data == actual_data + + +def test_ms_teams_attach_kibana_discover_url_when_not_generated(): + rule = { + 'name': 'Test Rule', + 'type': 'any', + 'ms_teams_attach_kibana_discover_url': True, + 'ms_teams_webhook_url': 'http://test.webhook.url', + 'ms_teams_alert_summary': 'Alert from ElastAlert', + 'alert': [], + 'alert_subject': 'Cool subject', + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = MsTeamsAlerter(rule) + match = { + '@timestamp': '2016-01-01T00:00:00' + } + with mock.patch('requests.post') as mock_post_request: + alert.alert([match]) + + expected_data = { + '@type': 'MessageCard', + '@context': 'http://schema.org/extensions', + 'summary': rule['ms_teams_alert_summary'], + 'title': rule['alert_subject'], + 'sections': [{'text': BasicMatchString(rule, match).__str__()}], + } + mock_post_request.assert_called_once_with( + rule['ms_teams_webhook_url'], + data=mock.ANY, + headers={'content-type': 'application/json'}, + proxies=None, + verify=True + ) + actual_data = json.loads(mock_post_request.call_args_list[0][1]['data']) + assert expected_data == actual_data + + +def test_ms_teams_kibana_discover_title(): + rule = { + 'name': 'Test Rule', + 'type': 'any', + 'ms_teams_attach_kibana_discover_url': True, + 'ms_teams_kibana_discover_title': 'Click to discover in Kibana', + 'ms_teams_webhook_url': 'http://test.webhook.url', + 'ms_teams_alert_summary': 'Alert from ElastAlert', + 'alert': [], + 'alert_subject': 'Cool subject', + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = MsTeamsAlerter(rule) + match = { + '@timestamp': '2016-01-01T00:00:00', + 'kibana_discover_url': 'http://kibana#discover' + } + with mock.patch('requests.post') as mock_post_request: + alert.alert([match]) + + expected_data = { + '@type': 'MessageCard', + '@context': 'http://schema.org/extensions', + 'summary': rule['ms_teams_alert_summary'], + 'title': rule['alert_subject'], + 'sections': [{'text': BasicMatchString(rule, match).__str__()}], + 'potentialAction': [ + { + '@type': 'OpenUri', + 'name': 'Click to discover in Kibana', + 'targets': [ + { + 'os': 'default', + 'uri': 'http://kibana#discover', + } + ], + } + ], + } + mock_post_request.assert_called_once_with( + rule['ms_teams_webhook_url'], + data=mock.ANY, + headers={'content-type': 'application/json'}, + proxies=None, + verify=True + ) + actual_data = json.loads(mock_post_request.call_args_list[0][1]['data']) + assert expected_data == actual_data + + +def test_ms_teams_alert_facts(): + rule = { + 'name': 'Test Rule', + 'type': 'any', + 'ms_teams_webhook_url': 'http://test.webhook.url', + 'ms_teams_alert_summary': 'Alert from ElastAlert', + 'ms_teams_alert_facts': [ + { + 'name': 'Host', + 'value': 'somefield', + }, + { + 'name': 'Sensors', + 'value': '@timestamp', + } + ], + 'alert_subject': 'Cool subject', + 'alert': [] + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = MsTeamsAlerter(rule) + match = { + '@timestamp': '2016-01-01T00:00:00', + 'somefield': 'foobarbaz' + } + with mock.patch('requests.post') as mock_post_request: + alert.alert([match]) + + expected_data = { + '@type': 'MessageCard', + '@context': 'http://schema.org/extensions', + 'summary': rule['ms_teams_alert_summary'], + 'title': rule['alert_subject'], + 'sections': [ + { + 'text': BasicMatchString(rule, match).__str__(), + 'facts': [ + {'name': 'Host', 'value': 'foobarbaz'}, + {'name': 'Sensors', 'value': '2016-01-01T00:00:00'}, + ], + } + ], + } + + mock_post_request.assert_called_once_with( + rule['ms_teams_webhook_url'], + data=mock.ANY, + headers={'content-type': 'application/json'}, + proxies=None, + verify=True + ) + assert expected_data == json.loads(mock_post_request.call_args_list[0][1]['data']) From 21de78d101cf0f3f8fde1ef42bd0a12300abbd82 Mon Sep 17 00:00:00 2001 From: thib12 Date: Tue, 18 Jan 2022 16:48:35 +0100 Subject: [PATCH 2/3] Update Changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac69352d..05b92e08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ - Dockerfile refactor for app home and user home to be the same directory (/opt/elastalert/). Before app home is /opt/elastalert/ and user home is /opt/elastalert/elastalert. After app home and user home are the same /opt/elastalert/ - [#656](https://github.com/jertel/elastalert2/pull/656) ## New features -- TBD - [#000](https://github.com/jertel/elastalert2/pull/000) - @some_elastic_contributor_tbd +- [Teams] - Kibana Discover URL and Facts - [#660](https://github.com/jertel/elastalert2/pull/660) - @thib12 ## Other changes - Load Jinja template when loading an alert - [#654](https://github.com/jertel/elastalert2/pull/654) - @thib12 From 7a849ab15ad96cd0e9fb42a8df04137e92107bfc Mon Sep 17 00:00:00 2001 From: thib12 Date: Tue, 18 Jan 2022 16:50:19 +0100 Subject: [PATCH 3/3] Fix Changelog message --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05b92e08..de0c3c28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ - Dockerfile refactor for app home and user home to be the same directory (/opt/elastalert/). Before app home is /opt/elastalert/ and user home is /opt/elastalert/elastalert. After app home and user home are the same /opt/elastalert/ - [#656](https://github.com/jertel/elastalert2/pull/656) ## New features -- [Teams] - Kibana Discover URL and Facts - [#660](https://github.com/jertel/elastalert2/pull/660) - @thib12 +- [MS Teams] Kibana Discover URL and Facts - [#660](https://github.com/jertel/elastalert2/pull/660) - @thib12 ## Other changes - Load Jinja template when loading an alert - [#654](https://github.com/jertel/elastalert2/pull/654) - @thib12