From 5f21b084b1a277ba3072b0c6aca9edb0d187d5ed Mon Sep 17 00:00:00 2001 From: Oleh Date: Tue, 28 May 2024 20:25:01 +0300 Subject: [PATCH 1/8] Added new alerter to send alerts to Opensearch --- docs/source/alerts.rst | 86 ++++++++++++++++++++++- elastalert/alerters/opensearch.py | 113 ++++++++++++++++++++++++++++++ elastalert/loaders.py | 2 + tests/alerters/opensearch_test.py | 101 ++++++++++++++++++++++++++ 4 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 elastalert/alerters/opensearch.py create mode 100644 tests/alerters/opensearch_test.py diff --git a/docs/source/alerts.rst b/docs/source/alerts.rst index cdbc5efb..d75a8acd 100644 --- a/docs/source/alerts.rst +++ b/docs/source/alerts.rst @@ -49,8 +49,9 @@ or - tencent_sms - twilio - victorops - - workwechat + - workwechat - zabbix + - opensearch Options for each alerter can either defined at the top level of the YAML file, or nested within the alert name, allowing for different settings for multiple of the same alerter. For example, consider sending multiple emails, but with different 'To' and 'From' fields: @@ -2353,3 +2354,86 @@ Example usage:: zbx_key: "sender_load1" where ``hostname`` is the available elasticsearch field. + +Opensearch +~~~~~~~~~~ + +Description: Create and manage separately index for all alerts for statistics and report purpose. + +Opensearch alerter can be used to create a new alert in existen Opensearch. The alerter supports +custom fields, and observables from the alert matches and rule data. + +Required: + +``opensearch_alert_config``: Configuration options for the alert, see example below for structure. + +``customFields`` Fields must be manually added, all of them will exist in the newly created index. You can set own field or use existed field fron match(see example below for structure). + +``index_alerts_name``: This field setup the output index for alerts. + +One of below is required: + +``opensearch_connection``: Options the connection details to your instance (see example below for the required syntax Example 1). + +``opensearch_config``: Options for the get connection details to your instance from file (see example below for the required syntax Example 2). + + +Example 1 usage:: + + alert: opensearch + + opensearch_connection: + es_host: localhost + es_port: es_port + ssl_show_warn: False + use_ssl: True + verify_certs: False + es_username: user + es_password: password + index_alerts_name: opensearch_elastalert2 # You can create own config or use global config just added ``index_alerts_name`` in global config + + opensearch_alert_config: + #Existing fields from match alert + message: message + host.name: host.name + event.action: event.action + event.type: event.type + winlog.computer_name: winlog.computer_name + winlog.event_id: winlog.event_id + winlog.task: winlog.task + #Enrich existen event with additional fields + customFields: + - name: original_time + value: "@timestamp" + - name: severity + value: high + - name: risk_score + value: 73 + - name: description + value: General description. + +Example 2 usage:: + + alert: opensearch + + opensearch_config: /opt/elastalert/config/config.yaml # You can create own config or use global config just added ``index_alerts_name`` in global config + + opensearch_alert_config: + #Existing fields from match alert + message: message + host.name: host.name + event.action: event.action + event.type: event.type + winlog.computer_name: winlog.computer_name + winlog.event_id: winlog.event_id + winlog.task: winlog.task + #Enrich existen event with additional fields + customFields: + - name: original_time + value: "@timestamp" + - name: severity + value: high + - name: risk_score + value: 73 + - name: description + value: General description. \ No newline at end of file diff --git a/elastalert/alerters/opensearch.py b/elastalert/alerters/opensearch.py new file mode 100644 index 00000000..f36f9e91 --- /dev/null +++ b/elastalert/alerters/opensearch.py @@ -0,0 +1,113 @@ +import os +import yaml +from datetime import datetime +from elasticsearch.exceptions import TransportError +from elastalert.alerts import Alerter +from elastalert.util import lookup_es_key, EAException, elastalert_logger, elasticsearch_client + +class OpenSearchAlerter(Alerter): + """ + Use matched data to create alerts on Opensearch + """ + required_options = frozenset(['opensearch_alert_config']) + + def lookup_field(self, match: dict, field_name: str, default): + field_value = lookup_es_key(match, field_name) + if field_value is None: + field_value = self.rule.get(field_name, default) + + return field_value + + def get_query(self,body_request_raw): + original = body_request_raw[0] + for orig in original.values(): + for query_string in orig.values(): + query = query_string + return query['query'] + + def lookup_list_fields(self, original_fields_raw: list, match: dict): + original_fields = {} + for field in original_fields_raw: + if field.get('value'): + if (isinstance(field['value'], str)): + if field['value'] == 'filter': + body_request_raw = self.rule.get(field['value']) + value = self.get_query(body_request_raw) + else: + value = self.lookup_field(match, field['value'], field['value']) + else: + value = field['value'] + original_fields[field['name']] = value + else: + for k,v in field.items(): + original_fields[k] = self.lookup_list_fields(v) + + return original_fields + + def event_orig_fields(self, original_fields_raw, match: dict): + if (isinstance(original_fields_raw, str)): + value = self.lookup_field(match, original_fields_raw, original_fields_raw) + elif (isinstance(original_fields_raw, list)): + value = self.lookup_list_fields(original_fields_raw, match) + else: + value = original_fields_raw + return value + + def make_nested_fields(self, data): + nested_data = {} + for key, value in data.items(): + keys = key.split(".") + current_nested_data = nested_data + for nested_key in keys[:-1]: + current_nested_data = current_nested_data.setdefault(nested_key, {}) + current_nested_data[keys[-1]] = value + return nested_data + + def flatten_dict(self, data, prefix='', sep='.'): + nd = {} + for k, v in data.items(): + if isinstance(v, dict): + nd.update(self.flatten_dict(v, f'{prefix}{k}{sep}')) + else: + nd[f'{prefix}{k}'] = v + return nd + + def remove_matching_pairs(self, input_dict): + return {key: value for key, value in input_dict.items() if key != value} + + def alert(self, matches): + alert_config = { + '@timestamp': datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%fZ') + } + alert_config.update(self.rule.get('opensearch_alert_config', {})) + + if len(matches) > 0: + alert_config = self.flatten_dict(alert_config) + for event_orig in alert_config: + alert_config[event_orig] = self.event_orig_fields(alert_config[event_orig],matches[0]) + alert_config = self.remove_matching_pairs(self.flatten_dict(alert_config)) + alert_config = self.make_nested_fields(alert_config) + + + # POST the alert to Opensearch + try: + data = self.rule.get('opensearch_connection', '') + if not data: + if os.path.isfile(self.rule.get('opensearch_config', '')): + filename = self.rule.get('opensearch_config', '') + else: + filename = '' + + if filename: + with open(filename) as config_file: + data = yaml.load(config_file, Loader=yaml.FullLoader) + elasticsearch_client(data).index(index = data.get('index_alerts_name'), + body = alert_config, + refresh = True) + + except TransportError as e: + raise EAException(f"Error posting to Opensearch: {e}") + elastalert_logger.info("Alert sent to Opensearch") + + def get_info(self): + return {'type': 'opensearch_alerter'} \ No newline at end of file diff --git a/elastalert/loaders.py b/elastalert/loaders.py index 1bd18aab..4606c9df 100644 --- a/elastalert/loaders.py +++ b/elastalert/loaders.py @@ -52,6 +52,7 @@ from elastalert.alerters.teams import MsTeamsAlerter from elastalert.alerters.zabbix import ZabbixAlerter from elastalert.alerters.tencentsms import TencentSMSAlerter +from elastalert.alerters.opensearch import OpenSearchAlerter from elastalert.util import dt_to_ts from elastalert.util import dt_to_ts_with_format from elastalert.util import dt_to_unix @@ -137,6 +138,7 @@ class RulesLoader(object): 'rocketchat': elastalert.alerters.rocketchat.RocketChatAlerter, 'gelf': elastalert.alerters.gelf.GelfAlerter, 'iris': elastalert.alerters.iris.IrisAlerter, + 'opensearch': OpenSearchAlerter, } # A partial ordering of alert types. Relative order will be preserved in the resulting alerts list diff --git a/tests/alerters/opensearch_test.py b/tests/alerters/opensearch_test.py new file mode 100644 index 00000000..1fc1937b --- /dev/null +++ b/tests/alerters/opensearch_test.py @@ -0,0 +1,101 @@ +import logging +from unittest import mock +import pytest +from elasticsearch.exceptions import TransportError +from elastalert.util import EAException +from elastalert.loaders import FileRulesLoader +from elastalert.alerters.opensearch import OpenSearchAlerter + + +def test_opensearch_alerter(caplog): + + caplog.set_level(logging.INFO) + rule = { + 'alert': [], + 'name': 'test-alert', + 'index': 'my-index', + 'query': 'some query', + 'description': 'test', + 'opensearch_connection': { + 'es_host': 'localhost', + 'es_port': 9200, + 'index_alerts_name': 'test_index' + }, + 'opensearch_alert_config': { + 'get_index': 'index', + 'get_type': 'type', + 'get_field1': 'field1', + 'get_field2': 'field2', + '@timestamp': '@timestamp' + }, + 'type': 'any' + } + + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = OpenSearchAlerter(rule) + + match = { + 'index': 'test-index', + 'type': 'test-type', + 'field1': 'value1', + 'field2': 'value2', + '@timestamp': '2021-05-09T14:43:30' + } + + with mock.patch('elasticsearch.Elasticsearch.index') as mock_create: + alert.alert([match]) + + expected_data = { + 'get_index': 'test-index', + 'get_type': 'test-type', + 'get_field1': 'value1', + 'get_field2': 'value2', + '@timestamp': '2021-05-09T14:43:30' + } + + mock_create.assert_called_once_with( + index='test_index', body=mock.ANY, refresh=True + ) + actual_data = mock_create.call_args_list[0][1]['body'] + assert expected_data == actual_data + assert ('elastalert', logging.INFO, 'Alert sent to Opensearch') == caplog.record_tuples[0] + + +def test_alert_with_transport_error(): + + with pytest.raises(EAException) as ea: + rule = { + 'alert': [], + 'name': 'test-alert', + 'index': 'my-index', + 'query': 'some query', + 'description': 'test', + 'opensearch_connection': { + 'es_host': 'localhost', + 'es_port': 9200, + 'index_alerts_name': 'test_index' + }, + 'opensearch_alert_config': { + 'get_index': 'index', + 'get_type': 'type', + 'get_field1': 'field1', + 'get_field2': 'field2', + '@timestamp': '@timestamp' + }, + 'type': 'any' + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = OpenSearchAlerter(rule) + match = { + '@timestamp': '2021-01-01T00:00:00', + 'somefield': 'foobarbaz' + } + + mock_run = mock.MagicMock(side_effect=TransportError(500, "Error creating index")) + # Mocking the Elasticsearch create method to raise TransportError + with mock.patch('elasticsearch.Elasticsearch.index', mock_run), pytest.raises(TransportError): + alert.alert([match]) + + assert "Error posting to Opensearch" in str(ea) From fa60a960dd3cb06cc59878e417bf883843a74b86 Mon Sep 17 00:00:00 2001 From: Oleh Date: Fri, 31 May 2024 16:14:12 +0300 Subject: [PATCH 2/8] updated with proposed suggestions --- docs/source/alerts.rst | 34 +- .../alerters/{opensearch.py => indexer.py} | 24 +- elastalert/loaders.py | 4 +- tests/alerters/indexer_test.py | 362 ++++++++++++++++++ tests/alerters/opensearch_test.py | 101 ----- 5 files changed, 393 insertions(+), 132 deletions(-) rename elastalert/alerters/{opensearch.py => indexer.py} (83%) create mode 100644 tests/alerters/indexer_test.py delete mode 100644 tests/alerters/opensearch_test.py diff --git a/docs/source/alerts.rst b/docs/source/alerts.rst index d75a8acd..bfe32614 100644 --- a/docs/source/alerts.rst +++ b/docs/source/alerts.rst @@ -51,7 +51,7 @@ or - victorops - workwechat - zabbix - - opensearch + - indexeralerter Options for each alerter can either defined at the top level of the YAML file, or nested within the alert name, allowing for different settings for multiple of the same alerter. For example, consider sending multiple emails, but with different 'To' and 'From' fields: @@ -2355,34 +2355,34 @@ Example usage:: where ``hostname`` is the available elasticsearch field. -Opensearch -~~~~~~~~~~ +Indexer +~~~~~~~ Description: Create and manage separately index for all alerts for statistics and report purpose. -Opensearch alerter can be used to create a new alert in existen Opensearch. The alerter supports +Indexer alerter can be used to create a new alert in existing Opensearch/Elasticsearch. The alerter supports custom fields, and observables from the alert matches and rule data. Required: -``opensearch_alert_config``: Configuration options for the alert, see example below for structure. +``indexer_alert_config``: Configuration options for the alert, see example below for structure. -``customFields`` Fields must be manually added, all of them will exist in the newly created index. You can set own field or use existed field fron match(see example below for structure). +``customFields`` Fields must be manually added, all of them will exist in the newly created index. You can set own field or use existing field fron match(see example below for structure). ``index_alerts_name``: This field setup the output index for alerts. One of below is required: -``opensearch_connection``: Options the connection details to your instance (see example below for the required syntax Example 1). +``indexer_connection``: Options the connection details to your instance (see example below for the required syntax Example 1). -``opensearch_config``: Options for the get connection details to your instance from file (see example below for the required syntax Example 2). +``indexer_config``: Options for the get connection details to your instance from file (see example below for the required syntax Example 2). Example 1 usage:: - alert: opensearch + alert: indexeralerter - opensearch_connection: + indexer_connection: es_host: localhost es_port: es_port ssl_show_warn: False @@ -2390,9 +2390,9 @@ Example 1 usage:: verify_certs: False es_username: user es_password: password - index_alerts_name: opensearch_elastalert2 # You can create own config or use global config just added ``index_alerts_name`` in global config + index_alerts_name: elastalert2 # You can create own config or use global config just added ``index_alerts_name`` in global config - opensearch_alert_config: + indexer_alert_config: #Existing fields from match alert message: message host.name: host.name @@ -2401,7 +2401,7 @@ Example 1 usage:: winlog.computer_name: winlog.computer_name winlog.event_id: winlog.event_id winlog.task: winlog.task - #Enrich existen event with additional fields + #Enrich existing event with additional fields customFields: - name: original_time value: "@timestamp" @@ -2414,11 +2414,11 @@ Example 1 usage:: Example 2 usage:: - alert: opensearch + alert: indexeralerter - opensearch_config: /opt/elastalert/config/config.yaml # You can create own config or use global config just added ``index_alerts_name`` in global config + indexer_config: /opt/elastalert/config/config.yaml # You can create own config or use global config just added ``index_alerts_name`` in global config - opensearch_alert_config: + indexer_alert_config: #Existing fields from match alert message: message host.name: host.name @@ -2427,7 +2427,7 @@ Example 2 usage:: winlog.computer_name: winlog.computer_name winlog.event_id: winlog.event_id winlog.task: winlog.task - #Enrich existen event with additional fields + #Enrich existing event with additional fields customFields: - name: original_time value: "@timestamp" diff --git a/elastalert/alerters/opensearch.py b/elastalert/alerters/indexer.py similarity index 83% rename from elastalert/alerters/opensearch.py rename to elastalert/alerters/indexer.py index f36f9e91..79c9f6dc 100644 --- a/elastalert/alerters/opensearch.py +++ b/elastalert/alerters/indexer.py @@ -5,11 +5,11 @@ from elastalert.alerts import Alerter from elastalert.util import lookup_es_key, EAException, elastalert_logger, elasticsearch_client -class OpenSearchAlerter(Alerter): +class IndexerAlerter(Alerter): """ - Use matched data to create alerts on Opensearch + Use matched data to create alerts on Opensearch/Elasticsearch """ - required_options = frozenset(['opensearch_alert_config']) + required_options = frozenset(['indexer_alert_config']) def lookup_field(self, match: dict, field_name: str, default): field_value = lookup_es_key(match, field_name) @@ -40,7 +40,7 @@ def lookup_list_fields(self, original_fields_raw: list, match: dict): original_fields[field['name']] = value else: for k,v in field.items(): - original_fields[k] = self.lookup_list_fields(v) + original_fields[k] = self.lookup_list_fields(v, match) return original_fields @@ -79,7 +79,7 @@ def alert(self, matches): alert_config = { '@timestamp': datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%fZ') } - alert_config.update(self.rule.get('opensearch_alert_config', {})) + alert_config.update(self.rule.get('indexer_alert_config', {})) if len(matches) > 0: alert_config = self.flatten_dict(alert_config) @@ -89,12 +89,12 @@ def alert(self, matches): alert_config = self.make_nested_fields(alert_config) - # POST the alert to Opensearch + # POST the alert to SIEM try: - data = self.rule.get('opensearch_connection', '') + data = self.rule.get('indexer_connection', '') if not data: - if os.path.isfile(self.rule.get('opensearch_config', '')): - filename = self.rule.get('opensearch_config', '') + if os.path.isfile(self.rule.get('indexer_config', '')): + filename = self.rule.get('indexer_config', '') else: filename = '' @@ -106,8 +106,8 @@ def alert(self, matches): refresh = True) except TransportError as e: - raise EAException(f"Error posting to Opensearch: {e}") - elastalert_logger.info("Alert sent to Opensearch") + raise EAException(f"Error posting to SIEM: {e}") + elastalert_logger.info("Alert sent to SIEM") def get_info(self): - return {'type': 'opensearch_alerter'} \ No newline at end of file + return {'type': 'indexer_alerter'} diff --git a/elastalert/loaders.py b/elastalert/loaders.py index 4606c9df..25940c2e 100644 --- a/elastalert/loaders.py +++ b/elastalert/loaders.py @@ -52,7 +52,7 @@ from elastalert.alerters.teams import MsTeamsAlerter from elastalert.alerters.zabbix import ZabbixAlerter from elastalert.alerters.tencentsms import TencentSMSAlerter -from elastalert.alerters.opensearch import OpenSearchAlerter +from elastalert.alerters.indexer import IndexerAlerter from elastalert.util import dt_to_ts from elastalert.util import dt_to_ts_with_format from elastalert.util import dt_to_unix @@ -138,7 +138,7 @@ class RulesLoader(object): 'rocketchat': elastalert.alerters.rocketchat.RocketChatAlerter, 'gelf': elastalert.alerters.gelf.GelfAlerter, 'iris': elastalert.alerters.iris.IrisAlerter, - 'opensearch': OpenSearchAlerter, + 'indexeralerter': IndexerAlerter, } # A partial ordering of alert types. Relative order will be preserved in the resulting alerts list diff --git a/tests/alerters/indexer_test.py b/tests/alerters/indexer_test.py new file mode 100644 index 00000000..4b90ff41 --- /dev/null +++ b/tests/alerters/indexer_test.py @@ -0,0 +1,362 @@ +import logging +from unittest import mock +import pytest +from elasticsearch.exceptions import TransportError +from elastalert.util import EAException +from elastalert.loaders import FileRulesLoader +from elastalert.alerters.indexer import IndexerAlerter + + +def rule_config(): + return { + 'alert': [], + 'name': 'test-alert', + 'index': 'my-index', + 'filter': [{'key': {'query': {'query': 'test_query'}}}], + 'description': 'test', + 'indexer_connection': { + 'es_host': 'localhost', + 'es_port': 9200, + 'index_alerts_name': 'test_index' + }, + 'indexer_alert_config': { + 'get_index': 'index', + 'get_type': 'type', + 'get_field1': 'field1', + 'get_field2': 'field2', + '@timestamp': '@timestamp' + }, + 'type': 'any' + } + + +def test_indexer_alerter(caplog): + + caplog.set_level(logging.INFO) + rule = rule_config() + + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IndexerAlerter(rule) + + match = { + 'index': 'test-index', + 'type': 'test-type', + 'field1': 'value1', + 'field2': 'value2', + '@timestamp': '2021-05-09T14:43:30' + } + + with mock.patch('elasticsearch.Elasticsearch.index') as mock_create: + alert.alert([match]) + + expected_data = { + 'get_index': 'test-index', + 'get_type': 'test-type', + 'get_field1': 'value1', + 'get_field2': 'value2', + '@timestamp': '2021-05-09T14:43:30' + } + + mock_create.assert_called_once_with( + index='test_index', body=mock.ANY, refresh=True + ) + actual_data = mock_create.call_args_list[0][1]['body'] + assert expected_data == actual_data + assert ('elastalert', logging.INFO, 'Alert sent to SIEM') == caplog.record_tuples[0] + + +def test_alert_with_file_config(): + + rule = rule_config() + rule.pop('indexer_connection') + rule['indexer_config'] = 'config.yaml' + + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IndexerAlerter(rule) + + match = { + 'index': 'test-index', + 'type': 'test-type', + 'field1': 'value1', + 'field2': 'value2', + '@timestamp': '2021-05-09T14:43:30' + } + + with mock.patch('elasticsearch.Elasticsearch.index') as mock_create, \ + mock.patch('os.path.isfile', return_value=True), \ + mock.patch('builtins.open', new_callable=mock.mock_open, + read_data='indexer_connection:\n es_host: localhost\n es_port: 9200\n index_alerts_name: test_index'), \ + mock.patch('yaml.load', return_value={'es_host': 'localhost', 'es_port': 9200, 'index_alerts_name': 'test_index'}): + alert.alert([match]) + + expected_data = { + 'get_index': 'test-index', + 'get_type': 'test-type', + 'get_field1': 'value1', + 'get_field2': 'value2', + '@timestamp': '2021-05-09T14:43:30' + } + + mock_create.assert_called_once_with( + index='test_index', body=mock.ANY, refresh=True + ) + + actual_data = mock_create.call_args_list[0][1]['body'] + assert expected_data == actual_data + + +def test_alert_with_transport_error(): + + with pytest.raises(EAException) as ea: + rule = rule_config() + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IndexerAlerter(rule) + match = { + '@timestamp': '2021-01-01T00:00:00', + 'somefield': 'foobarbaz' + } + + mock_run = mock.MagicMock(side_effect=TransportError(500, "Error creating index")) + # Mocking the Elasticsearch create method to raise TransportError + with mock.patch('elasticsearch.Elasticsearch.index', mock_run), pytest.raises(TransportError): + alert.alert([match]) + + assert "Error posting to SIEM" in str(ea) + + +def test_get_query(): + + body_request_raw = [{'key': {'query': {'query': 'test_query'}}}] + rule = rule_config() + + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IndexerAlerter(rule) + expected_data = { + 'get_query': 'test_query' + } + + actual_data = {'get_query': alert.get_query(body_request_raw)} + assert expected_data == actual_data + + +def test_lookup_field(): + + rule = rule_config() + + match = {'field1': 'some important'} + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IndexerAlerter(rule) + expected_data1 = { + 'get_field1': 'some important', + } + expected_data2 = { + 'get_field1': 'field2' + } + + actual_data = {'get_field1': alert.lookup_field(match, expected_data1['get_field1'], expected_data1['get_field1'])} + assert expected_data1 == actual_data + actual_data = {'get_field1': alert.lookup_field(match, expected_data2['get_field1'], expected_data2['get_field1'])} + assert expected_data2 == actual_data + + +def test_lookup_list_fields(): + + rule = rule_config() + match = {'field1': 'value1'} + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IndexerAlerter(rule) + + # Test simple case with direct value lookup + original_fields_raw = [{'name': 'field1', 'value': 'field1'}] + expected_data = {'field1': 'value1'} + actual_data = alert.lookup_list_fields(original_fields_raw, match) + assert expected_data == actual_data + + # Test simple case with direct value lookup not str + original_fields_raw = [{'name': 'field1', 'value': 123}] + expected_data = {'field1': 123} + actual_data = alert.lookup_list_fields(original_fields_raw, match) + assert expected_data == actual_data + + # Test with 'filter' keyword + original_fields_raw = [{'name': 'query', 'value': 'filter'}] + expected_data = {'query': 'test_query'} + actual_data = alert.lookup_list_fields(original_fields_raw, match) + assert actual_data == expected_data + + original_fields_raw = [{'name': 'query', 'value': 'filter'}] + expected_data = {'query': 'test_query'} + actual_data = alert.lookup_list_fields(original_fields_raw, match) + assert actual_data == expected_data + + original_fields_raw = [{'test_event_data': [{'name': 'test', 'value': 'test_event'}]}] + expected_data = {'test_event_data': {'test': 'test_event'}} + actual_data = alert.lookup_list_fields(original_fields_raw, match) + assert actual_data == expected_data + + +def test_event_orig_fields(): + + rule = rule_config() + match = {'field1': 'value1'} + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IndexerAlerter(rule) + + # test if (isinstance(original_fields_raw, str)) + expected_data = {'field1': 'value1'} + actual_data = {'field1': alert.event_orig_fields('field1', match)} + assert expected_data == actual_data + + # test elif (isinstance(original_fields_raw, list)) + list_data = [{'name': 'field1', 'value': 'value1'}] + expected_data = {'field1': 'value1'} + actual_data = alert.event_orig_fields(list_data, match) + assert expected_data == actual_data + + # test else not str or list + expected_data = {'test_data': 10} + actual_data = alert.event_orig_fields(expected_data, match) + assert expected_data == actual_data + + +def test_make_nested_fields(): + + rule = rule_config() + data = { + 'a.b.c': 1, 'a.b.d': 2, 'e': 3 + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IndexerAlerter(rule) + expected_data = {'a': {'b': {'c': 1, 'd': 2}}, 'e': 3} + actual_data = alert.make_nested_fields(data) + assert expected_data == actual_data + + +def test_flatten_dict(): + + rule = rule_config() + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IndexerAlerter(rule) + data = {'a': {'b': {'c': 1, 'd': 2}}, 'e': 3} + expected_data = {'a.b.c': 1, 'a.b.d': 2, 'e': 3} + actual_data = alert.flatten_dict(data) + assert expected_data == actual_data + + +def test_remove_matching_pairs(): + + rule = rule_config() + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IndexerAlerter(rule) + data = {'a': 'a', 'b': 'c'} + expected_data = {'b': 'c'} + actual_data = alert.remove_matching_pairs(data) + assert actual_data == expected_data + + +def test_indexer_getinfo(): + + rule = rule_config() + + alert = IndexerAlerter(rule) + expected_data = { + 'type': 'indexer_alerter' + } + actual_data = alert.get_info() + assert expected_data == actual_data + + +def test_alert_with_matches(): + + rule = rule_config() + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IndexerAlerter(rule) + + match = {'field1': 'value1'} + alert_config = { + '@timestamp': '2021-01-01T00:00:00', + 'key1': 'value1', + 'key2': 'value2' + } + + with mock.patch('elasticsearch.Elasticsearch.index') as mock_create, \ + mock.patch.object(alert, 'flatten_dict', return_value=alert_config) as mock_flatten, \ + mock.patch.object(alert, 'event_orig_fields', side_effect=lambda x, y: f"processed_{y}") as mock_event_orig_fields, \ + mock.patch.object(alert, 'remove_matching_pairs', return_value=alert_config) as mock_remove_matching_pairs, \ + mock.patch.object(alert, 'make_nested_fields', return_value=alert_config) as mock_make_nested_fields: + + alert.alert([match]) + + mock_flatten.assert_called() + mock_event_orig_fields.assert_called() + mock_remove_matching_pairs.assert_called() + mock_make_nested_fields.assert_called() + + # Ensure that flatten_dict was called twice + expected_data = 2 + actual_data = mock_flatten.call_count + assert actual_data == expected_data + + # Check if event_orig_fields was called for each key in alert_config + expected_data = len(alert_config) + actual_data = mock_event_orig_fields.call_count + assert actual_data == expected_data + + # Verify if the transformed alert_config is passed to remove_matching_pairs and make_nested_fields + mock_remove_matching_pairs.assert_called_with(alert_config) + mock_make_nested_fields.assert_called_with(alert_config) + + mock_create.assert_called_once_with( + index='test_index', body=alert_config, refresh=True + ) + + +def test_alert_with_empty_matches(): + + rule = rule_config() + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IndexerAlerter(rule) + match = [] + alert_config = { + '@timestamp': '2021-01-01T00:00:00', + 'key1': 'value1', + 'key2': 'value2' + } + with mock.patch('elasticsearch.Elasticsearch.index') as mock_create, \ + mock.patch.object(alert, 'flatten_dict', return_value=alert_config) as mock_flatten, \ + mock.patch.object(alert, 'remove_matching_pairs', return_value=alert_config) as mock_remove_matching_pairs, \ + mock.patch.object(alert, 'make_nested_fields', return_value=alert_config) as mock_make_nested_fields, \ + mock.patch.object(alert, 'event_orig_fields') as mock_event_orig_fields: + alert.alert(match) + + mock_flatten.assert_called() + mock_remove_matching_pairs.assert_called() + mock_make_nested_fields.assert_called() + mock_event_orig_fields.assert_not_called() + # Ensure that flatten_dict was called twice + expected_data = 1 + actual_data = mock_flatten.call_count + assert actual_data == expected_data + + expected_data = 0 + actual_data = mock_event_orig_fields.call_count + assert expected_data == actual_data + # Verify if the transformed alert_config is passed to remove_matching_pairs and make_nested_fields + mock_remove_matching_pairs.assert_called_with(alert_config) + mock_make_nested_fields.assert_called_with(alert_config) + + mock_create.assert_called_once_with( + index='test_index', body=alert_config, refresh=True + ) diff --git a/tests/alerters/opensearch_test.py b/tests/alerters/opensearch_test.py deleted file mode 100644 index 1fc1937b..00000000 --- a/tests/alerters/opensearch_test.py +++ /dev/null @@ -1,101 +0,0 @@ -import logging -from unittest import mock -import pytest -from elasticsearch.exceptions import TransportError -from elastalert.util import EAException -from elastalert.loaders import FileRulesLoader -from elastalert.alerters.opensearch import OpenSearchAlerter - - -def test_opensearch_alerter(caplog): - - caplog.set_level(logging.INFO) - rule = { - 'alert': [], - 'name': 'test-alert', - 'index': 'my-index', - 'query': 'some query', - 'description': 'test', - 'opensearch_connection': { - 'es_host': 'localhost', - 'es_port': 9200, - 'index_alerts_name': 'test_index' - }, - 'opensearch_alert_config': { - 'get_index': 'index', - 'get_type': 'type', - 'get_field1': 'field1', - 'get_field2': 'field2', - '@timestamp': '@timestamp' - }, - 'type': 'any' - } - - rules_loader = FileRulesLoader({}) - rules_loader.load_modules(rule) - alert = OpenSearchAlerter(rule) - - match = { - 'index': 'test-index', - 'type': 'test-type', - 'field1': 'value1', - 'field2': 'value2', - '@timestamp': '2021-05-09T14:43:30' - } - - with mock.patch('elasticsearch.Elasticsearch.index') as mock_create: - alert.alert([match]) - - expected_data = { - 'get_index': 'test-index', - 'get_type': 'test-type', - 'get_field1': 'value1', - 'get_field2': 'value2', - '@timestamp': '2021-05-09T14:43:30' - } - - mock_create.assert_called_once_with( - index='test_index', body=mock.ANY, refresh=True - ) - actual_data = mock_create.call_args_list[0][1]['body'] - assert expected_data == actual_data - assert ('elastalert', logging.INFO, 'Alert sent to Opensearch') == caplog.record_tuples[0] - - -def test_alert_with_transport_error(): - - with pytest.raises(EAException) as ea: - rule = { - 'alert': [], - 'name': 'test-alert', - 'index': 'my-index', - 'query': 'some query', - 'description': 'test', - 'opensearch_connection': { - 'es_host': 'localhost', - 'es_port': 9200, - 'index_alerts_name': 'test_index' - }, - 'opensearch_alert_config': { - 'get_index': 'index', - 'get_type': 'type', - 'get_field1': 'field1', - 'get_field2': 'field2', - '@timestamp': '@timestamp' - }, - 'type': 'any' - } - rules_loader = FileRulesLoader({}) - rules_loader.load_modules(rule) - alert = OpenSearchAlerter(rule) - match = { - '@timestamp': '2021-01-01T00:00:00', - 'somefield': 'foobarbaz' - } - - mock_run = mock.MagicMock(side_effect=TransportError(500, "Error creating index")) - # Mocking the Elasticsearch create method to raise TransportError - with mock.patch('elasticsearch.Elasticsearch.index', mock_run), pytest.raises(TransportError): - alert.alert([match]) - - assert "Error posting to Opensearch" in str(ea) From cf61d59ccefd4ea4844bcca552e6156461d1382e Mon Sep 17 00:00:00 2001 From: Oleh Date: Sat, 1 Jun 2024 08:40:40 +0300 Subject: [PATCH 3/8] renamed the alerter to "indexer" --- docs/source/alerts.rst | 6 +++--- elastalert/alerters/indexer.py | 2 +- elastalert/loaders.py | 2 +- tests/alerters/indexer_test.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/source/alerts.rst b/docs/source/alerts.rst index bfe32614..0f985c5c 100644 --- a/docs/source/alerts.rst +++ b/docs/source/alerts.rst @@ -51,7 +51,7 @@ or - victorops - workwechat - zabbix - - indexeralerter + - indexer Options for each alerter can either defined at the top level of the YAML file, or nested within the alert name, allowing for different settings for multiple of the same alerter. For example, consider sending multiple emails, but with different 'To' and 'From' fields: @@ -2380,7 +2380,7 @@ One of below is required: Example 1 usage:: - alert: indexeralerter + alert: indexer indexer_connection: es_host: localhost @@ -2414,7 +2414,7 @@ Example 1 usage:: Example 2 usage:: - alert: indexeralerter + alert: indexer indexer_config: /opt/elastalert/config/config.yaml # You can create own config or use global config just added ``index_alerts_name`` in global config diff --git a/elastalert/alerters/indexer.py b/elastalert/alerters/indexer.py index 79c9f6dc..c2a2b4d3 100644 --- a/elastalert/alerters/indexer.py +++ b/elastalert/alerters/indexer.py @@ -110,4 +110,4 @@ def alert(self, matches): elastalert_logger.info("Alert sent to SIEM") def get_info(self): - return {'type': 'indexer_alerter'} + return {'type': 'indexer'} diff --git a/elastalert/loaders.py b/elastalert/loaders.py index 25940c2e..599de406 100644 --- a/elastalert/loaders.py +++ b/elastalert/loaders.py @@ -138,7 +138,7 @@ class RulesLoader(object): 'rocketchat': elastalert.alerters.rocketchat.RocketChatAlerter, 'gelf': elastalert.alerters.gelf.GelfAlerter, 'iris': elastalert.alerters.iris.IrisAlerter, - 'indexeralerter': IndexerAlerter, + 'indexer': IndexerAlerter, } # A partial ordering of alert types. Relative order will be preserved in the resulting alerts list diff --git a/tests/alerters/indexer_test.py b/tests/alerters/indexer_test.py index 4b90ff41..961b7ca2 100644 --- a/tests/alerters/indexer_test.py +++ b/tests/alerters/indexer_test.py @@ -270,7 +270,7 @@ def test_indexer_getinfo(): alert = IndexerAlerter(rule) expected_data = { - 'type': 'indexer_alerter' + 'type': 'indexer' } actual_data = alert.get_info() assert expected_data == actual_data From 5ac10b27eb2fe59e8c343b730d3f644753840d79 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Sat, 1 Jun 2024 05:57:30 -0400 Subject: [PATCH 4/8] Clarify documentation; Correct alphabetical sorting of alerter --- docs/source/alerts.rst | 170 ++++++++++++++++++++--------------------- 1 file changed, 85 insertions(+), 85 deletions(-) diff --git a/docs/source/alerts.rst b/docs/source/alerts.rst index 0f985c5c..b5948bc8 100644 --- a/docs/source/alerts.rst +++ b/docs/source/alerts.rst @@ -28,6 +28,7 @@ or - googlechat - gelf - hivealerter + - indexer - iris - jira - lark @@ -51,7 +52,6 @@ or - victorops - workwechat - zabbix - - indexer Options for each alerter can either defined at the top level of the YAML file, or nested within the alert name, allowing for different settings for multiple of the same alerter. For example, consider sending multiple emails, but with different 'To' and 'From' fields: @@ -1086,8 +1086,91 @@ Example usage with json string formatting:: "X-custom-{{key}}": "{{type}}" } +Indexer +~~~~~~~ + +Description: Creates a record in an arbitrary index within an Elasticsearch or OpenSearch index. + +Indexer alerter can be used to create a new alert in existing Opensearch/Elasticsearch. The alerter supports +custom fields, and observables from the alert matches and rule data. + +Required: + +``indexer_alert_config``: Configuration options for the alert, see example below for structure. + +``customFields`` Fields must be manually added, all of them will exist in the newly created index. You can set own field or use existing field fron match (see example below for structure). + +``index_alerts_name``: The index to use for creating the new alert records. + +One of below is required: + +``indexer_connection``: Options the connection details to your server instance (see example below for the required syntax Example 1). + +``indexer_config``: Options for loading the connection details to your server instance from a file (see example below for the required syntax Example 2). + + +Example 1 usage:: + + alert: indexer + + indexer_connection: + es_host: localhost + es_port: es_port + ssl_show_warn: False + use_ssl: True + verify_certs: False + es_username: user + es_password: password + index_alerts_name: elastalert2 # You can create own config or use global config just added ``index_alerts_name`` in global config + + indexer_alert_config: + #Existing fields from match alert + message: message + host.name: host.name + event.action: event.action + event.type: event.type + winlog.computer_name: winlog.computer_name + winlog.event_id: winlog.event_id + winlog.task: winlog.task + #Enrich existing event with additional fields + customFields: + - name: original_time + value: "@timestamp" + - name: severity + value: high + - name: risk_score + value: 73 + - name: description + value: General description. + +Example 2 usage:: + + alert: indexer + + indexer_config: /opt/elastalert/config/config.yaml # Uses the ElastAlert 2 global config, with an added ``index_alerts_name`` parameter + + indexer_alert_config: + #Existing fields from match alert + message: message + host.name: host.name + event.action: event.action + event.type: event.type + winlog.computer_name: winlog.computer_name + winlog.event_id: winlog.event_id + winlog.task: winlog.task + #Enrich existing event with additional fields + customFields: + - name: original_time + value: "@timestamp" + - name: severity + value: high + - name: risk_score + value: 73 + - name: description + value: General description. + IRIS -~~~~~~~~~ +~~~~ The Iris alerter can be used to create a new alert or case in `Iris IRP System `_. The alerter supports adding tags, IOCs, and context from the alert matches and rule data. The alerter requires the following option: @@ -2354,86 +2437,3 @@ Example usage:: zbx_key: "sender_load1" where ``hostname`` is the available elasticsearch field. - -Indexer -~~~~~~~ - -Description: Create and manage separately index for all alerts for statistics and report purpose. - -Indexer alerter can be used to create a new alert in existing Opensearch/Elasticsearch. The alerter supports -custom fields, and observables from the alert matches and rule data. - -Required: - -``indexer_alert_config``: Configuration options for the alert, see example below for structure. - -``customFields`` Fields must be manually added, all of them will exist in the newly created index. You can set own field or use existing field fron match(see example below for structure). - -``index_alerts_name``: This field setup the output index for alerts. - -One of below is required: - -``indexer_connection``: Options the connection details to your instance (see example below for the required syntax Example 1). - -``indexer_config``: Options for the get connection details to your instance from file (see example below for the required syntax Example 2). - - -Example 1 usage:: - - alert: indexer - - indexer_connection: - es_host: localhost - es_port: es_port - ssl_show_warn: False - use_ssl: True - verify_certs: False - es_username: user - es_password: password - index_alerts_name: elastalert2 # You can create own config or use global config just added ``index_alerts_name`` in global config - - indexer_alert_config: - #Existing fields from match alert - message: message - host.name: host.name - event.action: event.action - event.type: event.type - winlog.computer_name: winlog.computer_name - winlog.event_id: winlog.event_id - winlog.task: winlog.task - #Enrich existing event with additional fields - customFields: - - name: original_time - value: "@timestamp" - - name: severity - value: high - - name: risk_score - value: 73 - - name: description - value: General description. - -Example 2 usage:: - - alert: indexer - - indexer_config: /opt/elastalert/config/config.yaml # You can create own config or use global config just added ``index_alerts_name`` in global config - - indexer_alert_config: - #Existing fields from match alert - message: message - host.name: host.name - event.action: event.action - event.type: event.type - winlog.computer_name: winlog.computer_name - winlog.event_id: winlog.event_id - winlog.task: winlog.task - #Enrich existing event with additional fields - customFields: - - name: original_time - value: "@timestamp" - - name: severity - value: high - - name: risk_score - value: 73 - - name: description - value: General description. \ No newline at end of file From 1259af0769c671bccbde394e210a07bb87d40ef9 Mon Sep 17 00:00:00 2001 From: Oleh Date: Sat, 1 Jun 2024 13:49:49 +0300 Subject: [PATCH 5/8] Updated the CHANGELOG.md, elastalert.rst and schema.yaml --- CHANGELOG.md | 2 +- docs/source/elastalert.rst | 1 + elastalert/schema.yaml | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 983db0f7..32e22ac8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - TBD ## New features -- TBD +- Add indexer alerter - [#1451](https://github.com/jertel/elastalert2/pull/1451) - @olehpalanskyi ## Other changes - [Docs] Fixed typo in Alerta docs with incorrect number of seconds in a day. - @jertel diff --git a/docs/source/elastalert.rst b/docs/source/elastalert.rst index d5aae3d2..548de232 100755 --- a/docs/source/elastalert.rst +++ b/docs/source/elastalert.rst @@ -44,6 +44,7 @@ Currently, we have support built in for these alert types: - Graylog GELF - HTTP POST - HTTP POST 2 +- Indexer - Iris - Jira - Lark diff --git a/elastalert/schema.yaml b/elastalert/schema.yaml index 365363df..c86392bf 100644 --- a/elastalert/schema.yaml +++ b/elastalert/schema.yaml @@ -538,6 +538,40 @@ properties: http_post2_ignore_ssl_errors: {type: boolean} http_post2_timeout: {type: integer} + ### INDEXER + index_alerts_name: {type: string} + indexer_config: {type: string} + indexer_connection: + type: object + properties: + es_host: {type: string} + es_hosts: {type: array, items: {type: string}} + es_port: {type: integer} + ssl_show_warn: {type: boolean} + use_ssl: {type: boolean} + verify_certs: {type: boolean} + ca_cert: {type: string} + es_username: {type: string} + es_password: {type: string} + index_alerts_name: {type: string} + indexer_alert_config: + type: object + properties: + message: {type: string} + host.name: {type: string} + event.action: {type: string} + event.type: {type: string} + winlog.computer_name: {type: string} + winlog.event_id: {type: integer} + winlog.task: {type: string} + customFields: + type: array + items: + type: object + properties: + name: {type: string} + value: {type: any} # Flexible type for dynamic field values + ### IRIS iris_host: {type: string} iris_api_token: {type: string} From 1f6d525ea978eceee33de0ff01e3b9b63d092972 Mon Sep 17 00:00:00 2001 From: Oleh Date: Sat, 1 Jun 2024 13:53:01 +0300 Subject: [PATCH 6/8] updated schema.yaml --- elastalert/schema.yaml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/elastalert/schema.yaml b/elastalert/schema.yaml index c86392bf..b23354c8 100644 --- a/elastalert/schema.yaml +++ b/elastalert/schema.yaml @@ -557,13 +557,7 @@ properties: indexer_alert_config: type: object properties: - message: {type: string} - host.name: {type: string} - event.action: {type: string} - event.type: {type: string} - winlog.computer_name: {type: string} - winlog.event_id: {type: integer} - winlog.task: {type: string} + somecustomFields: {type: string} customFields: type: array items: From 99ac8aa27670c1a1efb72ac1c6b42d50b36a2d55 Mon Sep 17 00:00:00 2001 From: Oleh Date: Sat, 1 Jun 2024 14:33:18 +0300 Subject: [PATCH 7/8] renamed parameter index_alerts_name to indexer_alerts_name fixed error in shema.yaml related with description of INDEXER --- docs/source/alerts.rst | 6 +++--- elastalert/alerters/indexer.py | 2 +- elastalert/schema.yaml | 21 +++++++++++---------- tests/alerters/indexer_test.py | 6 +++--- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/docs/source/alerts.rst b/docs/source/alerts.rst index b5948bc8..17f20490 100644 --- a/docs/source/alerts.rst +++ b/docs/source/alerts.rst @@ -1100,7 +1100,7 @@ Required: ``customFields`` Fields must be manually added, all of them will exist in the newly created index. You can set own field or use existing field fron match (see example below for structure). -``index_alerts_name``: The index to use for creating the new alert records. +``indexer_alerts_name``: The index to use for creating the new alert records. One of below is required: @@ -1121,7 +1121,7 @@ Example 1 usage:: verify_certs: False es_username: user es_password: password - index_alerts_name: elastalert2 # You can create own config or use global config just added ``index_alerts_name`` in global config + indexer_alerts_name: elastalert2 # You can create own config or use global config just added ``index_alerts_name`` in global config indexer_alert_config: #Existing fields from match alert @@ -1147,7 +1147,7 @@ Example 2 usage:: alert: indexer - indexer_config: /opt/elastalert/config/config.yaml # Uses the ElastAlert 2 global config, with an added ``index_alerts_name`` parameter + indexer_config: /opt/elastalert/config/config.yaml # Uses the ElastAlert 2 global config, with an added ``indexer_alerts_name`` parameter indexer_alert_config: #Existing fields from match alert diff --git a/elastalert/alerters/indexer.py b/elastalert/alerters/indexer.py index c2a2b4d3..599a4446 100644 --- a/elastalert/alerters/indexer.py +++ b/elastalert/alerters/indexer.py @@ -101,7 +101,7 @@ def alert(self, matches): if filename: with open(filename) as config_file: data = yaml.load(config_file, Loader=yaml.FullLoader) - elasticsearch_client(data).index(index = data.get('index_alerts_name'), + elasticsearch_client(data).index(index = data.get('indexer_alerts_name'), body = alert_config, refresh = True) diff --git a/elastalert/schema.yaml b/elastalert/schema.yaml index b23354c8..f974ec4b 100644 --- a/elastalert/schema.yaml +++ b/elastalert/schema.yaml @@ -539,7 +539,7 @@ properties: http_post2_timeout: {type: integer} ### INDEXER - index_alerts_name: {type: string} + indexer_alerts_name: {type: string} indexer_config: {type: string} indexer_connection: type: object @@ -556,15 +556,16 @@ properties: index_alerts_name: {type: string} indexer_alert_config: type: object - properties: - somecustomFields: {type: string} - customFields: - type: array - items: - type: object - properties: - name: {type: string} - value: {type: any} # Flexible type for dynamic field values + minProperties: 1 + patternProperties: + "^.+$": + oneOf: + - type: [boolean, string, integer] + - type: object + additionalProperties: false + required: [ field ] + properties: + field: { type: [boolean, string, integer], minLength: 1 } ### IRIS iris_host: {type: string} diff --git a/tests/alerters/indexer_test.py b/tests/alerters/indexer_test.py index 961b7ca2..3c6f747a 100644 --- a/tests/alerters/indexer_test.py +++ b/tests/alerters/indexer_test.py @@ -17,7 +17,7 @@ def rule_config(): 'indexer_connection': { 'es_host': 'localhost', 'es_port': 9200, - 'index_alerts_name': 'test_index' + 'indexer_alerts_name': 'test_index' }, 'indexer_alert_config': { 'get_index': 'index', @@ -87,8 +87,8 @@ def test_alert_with_file_config(): with mock.patch('elasticsearch.Elasticsearch.index') as mock_create, \ mock.patch('os.path.isfile', return_value=True), \ mock.patch('builtins.open', new_callable=mock.mock_open, - read_data='indexer_connection:\n es_host: localhost\n es_port: 9200\n index_alerts_name: test_index'), \ - mock.patch('yaml.load', return_value={'es_host': 'localhost', 'es_port': 9200, 'index_alerts_name': 'test_index'}): + read_data='indexer_connection:\n es_host: localhost\n es_port: 9200\n indexer_alerts_name: test_index'), \ + mock.patch('yaml.load', return_value={'es_host': 'localhost', 'es_port': 9200, 'indexer_alerts_name': 'test_index'}): alert.alert([match]) expected_data = { From 73972d0a66a595cabe39f4a8abe38404b1811f29 Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Sat, 1 Jun 2024 07:43:30 -0400 Subject: [PATCH 8/8] Update alerts.rst --- docs/source/alerts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/alerts.rst b/docs/source/alerts.rst index 17f20490..c8a15d13 100644 --- a/docs/source/alerts.rst +++ b/docs/source/alerts.rst @@ -1121,7 +1121,7 @@ Example 1 usage:: verify_certs: False es_username: user es_password: password - indexer_alerts_name: elastalert2 # You can create own config or use global config just added ``index_alerts_name`` in global config + indexer_alerts_name: elastalert2 # You can create own config or use global config just added ``indexer_alerts_name`` in global config indexer_alert_config: #Existing fields from match alert