From 28cb0ba7b2567e32d8daa4a458ebd0a702e4972f Mon Sep 17 00:00:00 2001 From: Sergey Malinkin Date: Mon, 23 Oct 2023 17:33:44 +0300 Subject: [PATCH 1/7] Add alerter for IRIS IRP system --- docs/source/elastalert.rst | 1 + docs/source/ruletypes.rst | 60 +++ elastalert/alerters/iris.py | 187 +++++++++ elastalert/loaders.py | 8 +- elastalert/schema.yaml | 43 ++ examples/rules/example_iris_alert_any.yaml | 36 ++ tests/alerters/iris_test.py | 431 +++++++++++++++++++++ 7 files changed, 763 insertions(+), 3 deletions(-) create mode 100644 elastalert/alerters/iris.py create mode 100644 examples/rules/example_iris_alert_any.yaml create mode 100644 tests/alerters/iris_test.py diff --git a/docs/source/elastalert.rst b/docs/source/elastalert.rst index e989092f..ad80dcfe 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 +- Iris - Jira - Lark - Line Notify diff --git a/docs/source/ruletypes.rst b/docs/source/ruletypes.rst index 21ee81ca..825dd068 100644 --- a/docs/source/ruletypes.rst +++ b/docs/source/ruletypes.rst @@ -2579,6 +2579,66 @@ Example usage with json string formatting:: "X-custom-{{key}}": "{{type}}" } +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: + +``iris_host``: Address of the Iris host. Exclude https:// For example: ``iris.example.com``. + +``iris_api_token``: The API key of the user you created, which will be used to initiate alerts and cases on behalf of this user. + +``iris_customer_id``: The user ID associated with the API key mentioned above. You can find it on the same page where the API key is located. + +Optional: + +``iris_ca_cert``: Path to custom CA certificate. + +``iris_ignore_ssl_errors``: Ignore ssl error. The default value is: ``False``. + +``iris_description``: Description of the alert or case. + +``iris_overwrite_timestamp``: Should the timestamp be overridden when creating an alert. By default, the alert's creation time will be the trigger time. If you want to use the event's timestamp as the ticket creation time, set this value to ``True``. Default value is ``False``. + +``iris_type``: The type of object being created. It can be either ``alert`` or ``case``. The default value is ``alert``. + +``iris_case_template_id``: Case template ID, if you want to apply a pre-prepared template. + +``iris_alert_note``: Note for the alert. + +``iris_alert_tags``: List of tags. + +``iris_alert_status_id``: Alert status. Can be: ``1 - Unspecified``, ``2 - New``, ``3 - Assigned``, ``4 - In progress``, ``5 - Pending``, ``6 - Closed``, ``7 - Merged``. The default value is: `2`. + +``iris_alert_source_link``: Link, if needed. + +``iris_alert_severity_id``: Alert severity. Can be: ``1 - Unspecified``, ``2 - Informational``, ``3 - Low``, ``4 - Medium``, ``5 - High``, ``6 - Critical``. The default value is: `1`. + +``iris_alert_context``: + +``iris_iocs``: Description of the IOC to be added. + +Example usage ``iris_iocs``: + +.. code-block:: yaml + + iris_iocs: + - ioc_value: ip + ioc_description: Suspicious IP address + ioc_tlp_id: 2 + ioc_type_id: 76 + ioc_tags: ipv4, ip, suspicious + - ioc_value: username + ioc_description: Suspicious username + ioc_tlp_id: 1 + ioc_type_id: 3 + ioc_tags: username + +A few words about ``ioc_tlp_id`` and ``ioc_type_id``. ``ioc_tlp_id`` can be of three types: ``1 - red``, ``2 - amber``, ``3 - green``. There are numerous values for ``ioc_type_id``, and you can also add your custom ones. To find the ID for the type you are interested in, refer to your Iris instance's API at 'https://example.com/manage/ioc-types/list'. + +You can find complete examples of rules in the repository under the 'examples' folder. + Jira ~~~~ diff --git a/elastalert/alerters/iris.py b/elastalert/alerters/iris.py new file mode 100644 index 00000000..316b2327 --- /dev/null +++ b/elastalert/alerters/iris.py @@ -0,0 +1,187 @@ +import requests +import uuid + +from datetime import datetime +from requests import RequestException + +from elastalert.alerts import Alerter +from elastalert.util import EAException, elastalert_logger, lookup_es_key + +class IrisAlerter(Alerter): + required_options = set(['iris_host', 'iris_api_token', 'iris_customer_id']) + + def __init__(self, rule): + super(IrisAlerter, self).__init__(rule) + self.url = f"https://{self.rule.get('iris_host')}" + self.api_token = self.rule.get('iris_api_token') + self.customer_id = self.rule.get('iris_customer_id') + self.ca_cert = self.rule.get('iris_ca_cert', False) + self.ignore_ssl_errors = self.rule.get('iris_ignore_ssl_errors', False) + self.description = self.rule.get('iris_description', '') + self.overwrite_timestamp = self.rule.get('iris_overwrite_timestamp', False) + self.type = self.rule.get('iris_type', 'alert') + self.case_template_id = self.rule.get('iris_case_template_id', '') + self.headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {self.rule.get("iris_api_token")}' + } + self.alert_note = self.rule.get('iris_alert_note', '') + self.alert_tags = self.rule.get('iris_alert_tags', '') + self.alert_status_id = self.rule.get('iris_alert_status_id', 2) + self.alert_source_link = self.rule.get('iris_alert_source_link', '') + self.alert_severity_id = self.rule.get('iris_alert_severity_id', 1) + self.alert_context = self.rule.get('iris_alert_context', '') + self.iocs = self.rule.get('iris_iocs', None) + + + def make_alert_context_records(self, matches): + alert_context = {} + + for key, value in self.alert_context.items(): + alert_context.update( + { + key: matches[0].get(value) + } + ) + + return alert_context + + def make_iocs_records(self, matches): + iocs = [] + for record in self.iocs: + record['ioc_value'] = lookup_es_key(matches[0], record['ioc_value']) + iocs.append(record) + return iocs + + def make_alert(self, matches): + if self.overwrite_timestamp: + event_timestamp = matches[0].get('@timestamp') + else: + event_timestamp = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") + + alert_data = { + "alert_title": self.rule.get('name'), + "alert_description": self.description, + "alert_source": "ElastAlert2", + "alert_severity_id": self.alert_severity_id, + "alert_status_id": self.alert_status_id, + "alert_source_event_time": event_timestamp, + "alert_note": self.alert_note, + "alert_tags": self.alert_tags, + "alert_customer_id": self.customer_id, + } + + if self.alert_source_link: + alert_data.update( + {"alert_source_link": self.alert_source_link} + ) + + if self.iocs: + iocs = self.make_iocs_records(matches) + alert_data.update( + {"alert_iocs": iocs} + ) + + if self.alert_context: + alert_context = self.make_alert_context_records(matches) + alert_data.update( + {"alert_context": alert_context} + ) + + return alert_data + + def make_case(self, matches): + iocs = [] + case_data = { + "case_soc_id": f"SOC_{str(uuid.uuid4())[0:6]}", + "case_customer": self.customer_id, + "case_name": self.rule.get('name'), + "case_description": self.description + } + + if self.iocs: + iocs = self.make_iocs_records(matches) + + if self.case_template_id: + case_data.update( + {"case_template_id": self.case_template_id} + ) + + return case_data, iocs + + def alert(self, matches): + if self.ca_cert: + verify = self.ca_cert + else: + verify = False + + if self.ignore_ssl_errors: + requests.packages.urllib3.disable_warnings() + + if 'alert' in self.type: + alert_data = self.make_alert(matches) + + try: + alert_response = requests.post( + url=f'{self.url}/alerts/add', + headers=self.headers, + json=alert_data, + verify=verify, + ) + + if alert_response.status_code != 200: + raise EAException(f"Cannot create a new alert: {alert_response.status_code}") + + except RequestException as e: + raise EAException(f"Error posting alert to Iris: {e}") + elastalert_logger.info('Alert sent to Iris') + + elif 'case' in self.type: + case_data, iocs = self.make_case(matches) + + try: + case_response = requests.post( + url=f'{self.url}/manage/cases/add', + headers=self.headers, + json=case_data, + verify=verify, + ) + + + if case_response.status_code == 200: + case_response_data = case_response.json() + case_id = case_response_data.get('data', '').get('case_id') + for ioc in iocs: + ioc.update( + { + "cid": case_id + } + ) + + try: + response_ioc = requests.post( + url=f'{self.url}/case/ioc/add', + headers=self.headers, + json=ioc, + verify=verify, + ) + + if response_ioc.status_code != 200: + raise EAException(f"Unable to add a new IOC to the case {case_id}") + + except RequestException as e: + raise EAException(f"Error when adding IOC to the case {case_id}: {e}") + elastalert_logger.info('IOCs successfully added to the case') + + else: + raise EAException(f'Cannot create a new case: {case_response.status_code}') + + except RequestException as e: + raise EAException(f"Error posting the case to Iris: {e}") + elastalert_logger.info('Case successfully created in Iris') + + def get_info(self): + return { + 'type': 'IrisAlerter', + 'iris_api_endpoint': self.url + } diff --git a/elastalert/loaders.py b/elastalert/loaders.py index 4e0524a5..e26cf45f 100644 --- a/elastalert/loaders.py +++ b/elastalert/loaders.py @@ -25,7 +25,8 @@ import elastalert.alerters.googlechat import elastalert.alerters.httppost import elastalert.alerters.httppost2 -import elastalert.alerters.lark +import elastalert.alerters.iris +# import elastalert.alerters.lark import elastalert.alerters.line import elastalert.alerters.pagertree import elastalert.alerters.rocketchat @@ -127,12 +128,13 @@ class RulesLoader(object): 'zabbix': ZabbixAlerter, 'discord': elastalert.alerters.discord.DiscordAlerter, 'dingtalk': elastalert.alerters.dingtalk.DingTalkAlerter, - 'lark': elastalert.alerters.lark.LarkAlerter, + # 'lark': elastalert.alerters.lark.LarkAlerter, 'chatwork': elastalert.alerters.chatwork.ChatworkAlerter, 'datadog': elastalert.alerters.datadog.DatadogAlerter, 'ses': elastalert.alerters.ses.SesAlerter, 'rocketchat': elastalert.alerters.rocketchat.RocketChatAlerter, - 'gelf': elastalert.alerters.gelf.GelfAlerter + 'gelf': elastalert.alerters.gelf.GelfAlerter, + 'iris': elastalert.alerters.iris.IrisAlerter, } # A partial ordering of alert types. Relative order will be preserved in the resulting alerts list diff --git a/elastalert/schema.yaml b/elastalert/schema.yaml index 5a479dc9..7f081310 100644 --- a/elastalert/schema.yaml +++ b/elastalert/schema.yaml @@ -72,6 +72,20 @@ definitions: filter: &filter {} + irisIocField: &irisIocField + type: object + additionalProperties: false + properties: + ioc_value: {type: string} + ioc_description: {type: string} + ioc_tlp_id: {type: integer, enum: [1, 2, 3]} + ioc_type_id: {type: integer} + ioc_tags: {type: string} + + arrayOfIrisIocFields: &arrayOfIrisIocFields + type: array + items: *irisIocField + required: [type, index, alert] type: object @@ -514,6 +528,35 @@ properties: http_post2_ignore_ssl_errors: {type: boolean} http_post2_timeout: {type: integer} + ### IRIS + iris_url: {type: string} + iris_api_token: {type: string} + iris_type: {type: string, enum: ['alert', 'case']} + iris_customer_id: {type: integer} + iris_ignore_ssl_errors: {type: boolean} + iris_ca_cert: {type: string} + iris_overwrite_timestamp: {type: boolean} + iris_case_template_id: {type: integer} + iris_description: {type: string} + iris_alert_note: {type: string} + iris_alert_tags: {type: string} + iris_alert_status_id: {type: integer, enum: [1, 2, 3, 4, 5, 6, 7]} + iris_alert_source_link: {type: string} + iris_alert_severity_id: {type: integer, enum: [1, 2, 3, 4, 5, 6]} + iris_iocs: *arrayOfIrisIocFields + iris_alert_context: + type: object + minProperties: 1 + patternProperties: + "^.+$": + oneOf: + - type: string + - type: object + additionalProperties: false + required: [ field ] + properties: + field: { type: string, minLength: 1 } + ### Jira jira_server: {type: string} jira_project: {type: string} diff --git a/examples/rules/example_iris_alert_any.yaml b/examples/rules/example_iris_alert_any.yaml new file mode 100644 index 00000000..3d712572 --- /dev/null +++ b/examples/rules/example_iris_alert_any.yaml @@ -0,0 +1,36 @@ +name: "Example Iris alert" +type: any +index: index_* +use_strftime_index: true + +filter: +- query: + query_string: + query: "session_status: opened" + +realert: + minutes: 0 + +alert: +- iris + +iris_host: 127.0.0.1 +iris_api_token: token123456789 +iris_customer_id: 1 +iris_description: 'Test alert from ElastAlert2' +iris_alert_note: 'Alert triggered by opened session' +iris_alert_tags: 'test, login, ssh' +iris_alert_context: + username: username + ip: src_ip +iris_iocs: + - ioc_value: src_ip + ioc_description: source ip address + ioc_tlp_id: 1 + ioc_type_id: 42 + ioc_tags: ipv4 + - ioc_value: username + ioc_description: who was connecting + ioc_tlp_id: 3 + ioc_type_id: 3 + ioc_tags: username diff --git a/tests/alerters/iris_test.py b/tests/alerters/iris_test.py new file mode 100644 index 00000000..ca7b3344 --- /dev/null +++ b/tests/alerters/iris_test.py @@ -0,0 +1,431 @@ +import json +import logging + +from unittest import mock +from unittest.mock import patch +from datetime import datetime + +from elastalert.alerters.iris import IrisAlerter +from elastalert.loaders import FileRulesLoader + + +def test_iris_make_alert_context_records(caplog): + caplog.set_level(logging.INFO) + rule = { + 'name': 'Test Context', + 'type': 'any', + 'iris_type': 'alert', + 'iris_host': '127.0.0.1', + 'iris_api_token': 'token 12345', + 'iris_customer_id': 1, + 'iris_alert_context': {'username': 'username', 'ip': 'src_ip', 'login_status': 'event_status'}, + 'alert': [] + } + + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IrisAlerter(rule) + + match = { + '@timestamp': '2023-10-21 20:00:00.000', 'username': 'evil_user', 'src_ip': '172.20.1.1', 'dst_ip': '10.0.0.1', + 'event_type': 'login', 'event_status': 'success' + } + + expected_data = { + 'username': 'evil_user', + 'ip': '172.20.1.1', + 'login_status': 'success' + } + + actual_data = alert.make_alert_context_records([match]) + + assert expected_data == actual_data + +def test_iris_make_iocs_records(caplog): + caplog.set_level(logging.INFO) + rule = { + 'name': 'Test Context', + 'type': 'any', + 'iris_type': 'alert', + 'iris_host': '127.0.0.1', + 'iris_api_token': 'token 12345', + 'iris_customer_id': 1, + 'iris_iocs': [ + { + 'ioc_description': 'source address', + 'ioc_tags': 'ip, ipv4', + 'ioc_tlp_id': 1, + 'ioc_type_id': 76, + 'ioc_value': 'src_ip' + }, + { + 'ioc_description': 'target username', + 'ioc_tags': 'login, username', + 'ioc_tlp_id': 3, + 'ioc_type_id': 3, + 'ioc_value': 'username' + } + ], + 'alert': [] + } + + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IrisAlerter(rule) + + match = { + '@timestamp': '2023-10-21 20:00:00.000', 'username': 'evil_user', 'src_ip': '172.20.1.1', 'dst_ip': '10.0.0.1', + 'event_type': 'login', 'event_status': 'success' + } + + expected_data = [ + { + 'ioc_description': 'source address', + 'ioc_tags': 'ip, ipv4', + 'ioc_tlp_id': 1, + 'ioc_type_id': 76, + 'ioc_value': '172.20.1.1' + }, + { + 'ioc_description': 'target username', + 'ioc_tags': 'login, username', + 'ioc_tlp_id': 3, + 'ioc_type_id': 3, + 'ioc_value': 'evil_user' + } + ] + + actual_data = alert.make_iocs_records([match]) + assert expected_data == actual_data + +def test_iris_make_alert_minimal(caplog): + caplog.set_level(logging.INFO) + rule = { + 'name': 'Test Minimal Alert Body', + 'type': 'any', + 'iris_host': '127.0.0.1', + 'iris_api_token': 'token 12345', + 'iris_customer_id': 1, + 'alert': [], + } + + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IrisAlerter(rule) + + match = { + '@timestamp': '2023-10-21 20:00:00.000', 'username': 'evil_user', 'src_ip': '172.20.1.1', 'dst_ip': '10.0.0.1', + 'event_type': 'login', 'event_status': 'success' + } + + expected_data = { + "alert_title": 'Test Minimal Alert Body', + "alert_description": '', + "alert_source": "ElastAlert2", + "alert_severity_id": 1, + "alert_status_id": 2, + "alert_source_event_time": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"), + "alert_note": '', + "alert_tags": '', + "alert_customer_id": 1 + } + + actual_data = alert.make_alert([match]) + assert expected_data == actual_data + +def test_iris_make_alert_maximal(caplog): + caplog.set_level(logging.INFO) + rule = { + 'name': 'Test Maximal Alert Body', + 'type': 'any', + 'iris_host': '127.0.0.1', + 'iris_api_token': 'token 12345', + 'iris_customer_id': 1, + 'iris_description': 'test description in alert', + 'iris_alert_note': 'test note', + 'iris_alert_tags': 'test, alert', + 'iris_overwrite_timestamp': True, + 'iris_alert_source_link': 'https://example.com', + 'iris_iocs': [ + { + 'ioc_description': 'source address', + 'ioc_tags': 'ip, ipv4', + 'ioc_tlp_id': 1, + 'ioc_type_id': 76, + 'ioc_value': 'src_ip' + }, + { + 'ioc_description': 'target username', + 'ioc_tags': 'login, username', + 'ioc_tlp_id': 3, + 'ioc_type_id': 3, + 'ioc_value': 'username' + } + ], + 'iris_alert_context': {'username': 'username', 'ip': 'src_ip', 'login_status': 'event_status'}, + 'alert': [], + } + + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IrisAlerter(rule) + + match = { + '@timestamp': '2023-10-21 20:00:00.000', 'username': 'evil_user', 'src_ip': '172.20.1.1', 'dst_ip': '10.0.0.1', + 'event_type': 'login', 'event_status': 'success' + } + + expected_data = { + "alert_title": 'Test Maximal Alert Body', + "alert_description": 'test description in alert', + "alert_source": "ElastAlert2", + "alert_severity_id": 1, + "alert_status_id": 2, + "alert_source_event_time": '2023-10-21 20:00:00.000', + "alert_note": 'test note', + "alert_tags": 'test, alert', + "alert_customer_id": 1, + "alert_source_link": 'https://example.com', + "alert_iocs": [ + { + 'ioc_description': 'source address', + 'ioc_tags': 'ip, ipv4', + 'ioc_tlp_id': 1, + 'ioc_type_id': 76, + 'ioc_value': '172.20.1.1' + }, + { + 'ioc_description': 'target username', + 'ioc_tags': 'login, username', + 'ioc_tlp_id': 3, + 'ioc_type_id': 3, + 'ioc_value': 'evil_user' + } + ], + "alert_context": { + 'username': 'evil_user', + 'ip': '172.20.1.1', + 'login_status': 'success' + }, + } + + actual_data = alert.make_alert([match]) + assert expected_data == actual_data + +def test_iris_make_case_minimal(caplog): + caplog.set_level(logging.INFO) + rule = { + 'name': 'Test Minimal Case', + 'type': 'any', + 'iris_host': '127.0.0.1', + 'iris_api_token': 'token 12345', + 'iris_type': 'case', + 'iris_customer_id': 1, + 'alert': [], + } + + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IrisAlerter(rule) + + match = { + '@timestamp': '2023-10-21 20:00:00.000', 'username': 'evil_user', 'src_ip': '172.20.1.1', 'dst_ip': '10.0.0.1', + 'event_type': 'login', 'event_status': 'success' + } + + expected_data = { + "case_soc_id": "SOC_123456", + "case_customer": 1, + "case_name": "Test Minimal Case", + "case_description": '' + } + + with patch('uuid.uuid4', return_value='123456'): + actual_data, actual_data_iocs = alert.make_case([match]) + + assert expected_data == actual_data + +def test_iris_make_case_maximal(caplog): + caplog.set_level(logging.INFO) + rule = { + 'name': 'Test Maximal Case', + 'type': 'any', + 'iris_host': '127.0.0.1', + 'iris_api_token': 'token 12345', + 'iris_type': 'case', + 'iris_customer_id': 1, + 'iris_case_template_id': 55, + 'iris_iocs': [ + { + 'ioc_description': 'source address', + 'ioc_tags': 'ip, ipv4', + 'ioc_tlp_id': 1, + 'ioc_type_id': 76, + 'ioc_value': 'src_ip' + }, + { + 'ioc_description': 'target username', + 'ioc_tags': 'login, username', + 'ioc_tlp_id': 3, + 'ioc_type_id': 3, + 'ioc_value': 'username' + } + ], + 'alert': [], + } + + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IrisAlerter(rule) + + match = { + '@timestamp': '2023-10-21 20:00:00.000', 'username': 'evil_user', 'src_ip': '172.20.1.1', 'dst_ip': '10.0.0.1', + 'event_type': 'login', 'event_status': 'success' + } + + expected_data = { + "case_soc_id": "SOC_123456", + "case_customer": 1, + "case_name": "Test Maximal Case", + "case_description": '', + "case_template_id": 55, + } + + expected_data_iocs = [ + { + 'ioc_description': 'source address', + 'ioc_tags': 'ip, ipv4', + 'ioc_tlp_id': 1, + 'ioc_type_id': 76, + 'ioc_value': '172.20.1.1' + }, + { + 'ioc_description': 'target username', + 'ioc_tags': 'login, username', + 'ioc_tlp_id': 3, + 'ioc_type_id': 3, + 'ioc_value': 'evil_user' + } + ] + + with patch('uuid.uuid4', return_value='123456'): + actual_data, actual_data_iocs = alert.make_case([match]) + + assert expected_data == actual_data + assert expected_data_iocs == actual_data_iocs + +def test_iris_alert_alert(caplog): + caplog.set_level(logging.INFO) + rule = { + 'name': 'Test Main', + 'type': 'any', + 'iris_host': '127.0.0.1', + 'iris_api_token': 'token 12345', + 'iris_customer_id': 1, + 'iris_description': 'test description in alert', + 'iris_alert_note': 'test note', + 'iris_alert_tags': 'test, alert', + 'iris_overwrite_timestamp': True, + 'iris_alert_source_link': 'https://example.com', + 'iris_iocs': [ + { + 'ioc_description': 'source address', + 'ioc_tags': 'ip, ipv4', + 'ioc_tlp_id': 1, + 'ioc_type_id': 76, + 'ioc_value': 'src_ip' + }, + { + 'ioc_description': 'target username', + 'ioc_tags': 'login, username', + 'ioc_tlp_id': 3, + 'ioc_type_id': 3, + 'ioc_value': 'username' + } + ], + 'iris_alert_context': {'username': 'username', 'ip': 'src_ip', 'login_status': 'event_status'}, + 'alert': [], + } + + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IrisAlerter(rule) + + match = { + '@timestamp': '2023-10-21 20:00:00.000', 'username': 'evil_user', 'src_ip': '172.20.1.1', 'dst_ip': '10.0.0.1', + 'event_type': 'login', 'event_status': 'success' + } + + expected_data = { + "alert_title": 'Test Main', + "alert_description": 'test description in alert', + "alert_source": "ElastAlert2", + "alert_severity_id": 1, + "alert_status_id": 2, + "alert_source_event_time": '2023-10-21 20:00:00.000', + "alert_note": 'test note', + "alert_tags": 'test, alert', + "alert_customer_id": 1, + "alert_source_link": 'https://example.com', + "alert_iocs": [ + { + 'ioc_description': 'source address', + 'ioc_tags': 'ip, ipv4', + 'ioc_tlp_id': 1, + 'ioc_type_id': 76, + 'ioc_value': '172.20.1.1' + }, + { + 'ioc_description': 'target username', + 'ioc_tags': 'login, username', + 'ioc_tlp_id': 3, + 'ioc_type_id': 3, + 'ioc_value': 'evil_user' + } + ], + "alert_context": { + 'username': 'evil_user', + 'ip': '172.20.1.1', + 'login_status': 'success' + }, + } + mock_response = mock.Mock() + mock_response.status_code = 200 + with mock.patch('requests.post', return_value=mock_response) as mock_post_request: + alert.alert([match]) + + mock_post_request.assert_called_once_with( + url=f'https://{rule["iris_host"]}/alerts/add', + headers={ + 'Content-Type': 'application/json', + f'Authorization': f'Bearer {rule["iris_api_token"]}' + }, + json=mock.ANY, + verify=False, + ) + + assert expected_data == mock_post_request.call_args_list[0][1]['json'] + assert ('elastalert', logging.INFO, 'Alert sent to Iris') == caplog.record_tuples[0] + +def test_iris_get_info(caplog): + caplog.set_level(logging.INFO) + rule = { + 'name': 'Test Info', + 'type': 'any', + 'iris_host': '127.0.0.1', + 'iris_api_token': 'token 12345', + 'iris_customer_id': 1, + 'alert': [], + } + + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = IrisAlerter(rule) + + expected_data = { + 'type': 'IrisAlerter', + 'iris_api_endpoint': 'https://127.0.0.1' + } + + actual_data = alert.get_info() + assert expected_data == actual_data From 41426dfc15ba7a419d611018610e87cc0835e09a Mon Sep 17 00:00:00 2001 From: Sergey Malinkin Date: Mon, 23 Oct 2023 17:38:50 +0300 Subject: [PATCH 2/7] Add CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9146d542..ea4671b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ -# 2.TBD.TBD +# 2.15.0 ## Breaking changes - TBD ## New features -- TBD +- [Iris] Alerter added. [#1301](https://github.com/jertel/elastalert2/pull/1301) - @malinkinsa ## Other changes - TBD From 6d706593b3d86fbe9f033c5935ac2f81402e68fe Mon Sep 17 00:00:00 2001 From: Jason Ertel Date: Tue, 24 Oct 2023 08:52:37 -0400 Subject: [PATCH 3/7] Update CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b1533e8..d84f3e0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ -# 2.15.0 +# 2.TBD.TBD ## Breaking changes - TBD ## New features -- [Iris] Alerter added. [#1301](https://github.com/jertel/elastalert2/pull/1301) - @malinkinsa +- [Iris] Alerter added - [#1301](https://github.com/jertel/elastalert2/pull/1301) - @malinkinsa ## Other changes - Refactored FlatlineRule to make it more extensible - [#1291](https://github.com/jertel/elastalert2/pull/1291) - @rundef From 237967fc0214f370e15c147225750990a08f946e Mon Sep 17 00:00:00 2001 From: Sergey Malinkin Date: Tue, 24 Oct 2023 17:46:01 +0300 Subject: [PATCH 4/7] fixes based on review --- docs/source/ruletypes.rst | 25 ++++++++++++++++++++++--- elastalert/alerters/iris.py | 14 +++++++------- elastalert/loaders.py | 4 ++-- tests/alerters/iris_test.py | 7 +++++++ 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/docs/source/ruletypes.rst b/docs/source/ruletypes.rst index 825dd068..6def678d 100644 --- a/docs/source/ruletypes.rst +++ b/docs/source/ruletypes.rst @@ -2609,11 +2609,30 @@ Optional: ``iris_alert_tags``: List of tags. -``iris_alert_status_id``: Alert status. Can be: ``1 - Unspecified``, ``2 - New``, ``3 - Assigned``, ``4 - In progress``, ``5 - Pending``, ``6 - Closed``, ``7 - Merged``. The default value is: `2`. +``iris_alert_status_id``: The alert status of the alert, default value is ``2``. This parameter requires an integer input. -``iris_alert_source_link``: Link, if needed. + Possible values: -``iris_alert_severity_id``: Alert severity. Can be: ``1 - Unspecified``, ``2 - Informational``, ``3 - Low``, ``4 - Medium``, ``5 - High``, ``6 - Critical``. The default value is: `1`. + - ``1`` - Unspecified + - ``2`` - New + - ``3`` - Assigned + - ``4`` - In progress + - ``5`` - Pending + - ``6`` - Closed + - ``7`` - Merged. + +``iris_alert_source_link``: Your custom link, if needed. + +``iris_alert_severity_id``: The severity level of the alert, default value is ``1``. This parameter requires an integer input. + + Possible values: + + - ``1`` - Unspecified + - ``2`` - Informational + - ``3`` - Low + - ``4`` - Medium + - ``5`` - High + - ``6`` - Critical. ``iris_alert_context``: diff --git a/elastalert/alerters/iris.py b/elastalert/alerters/iris.py index 316b2327..dd23ca63 100644 --- a/elastalert/alerters/iris.py +++ b/elastalert/alerters/iris.py @@ -7,6 +7,7 @@ from elastalert.alerts import Alerter from elastalert.util import EAException, elastalert_logger, lookup_es_key + class IrisAlerter(Alerter): required_options = set(['iris_host', 'iris_api_token', 'iris_customer_id']) @@ -17,23 +18,22 @@ def __init__(self, rule): self.customer_id = self.rule.get('iris_customer_id') self.ca_cert = self.rule.get('iris_ca_cert', False) self.ignore_ssl_errors = self.rule.get('iris_ignore_ssl_errors', False) - self.description = self.rule.get('iris_description', '') + self.description = self.rule.get('iris_description', None) self.overwrite_timestamp = self.rule.get('iris_overwrite_timestamp', False) self.type = self.rule.get('iris_type', 'alert') - self.case_template_id = self.rule.get('iris_case_template_id', '') + self.case_template_id = self.rule.get('iris_case_template_id', None) self.headers = { 'Content-Type': 'application/json', 'Authorization': f'Bearer {self.rule.get("iris_api_token")}' } - self.alert_note = self.rule.get('iris_alert_note', '') - self.alert_tags = self.rule.get('iris_alert_tags', '') + self.alert_note = self.rule.get('iris_alert_note', None) + self.alert_tags = self.rule.get('iris_alert_tags', None) self.alert_status_id = self.rule.get('iris_alert_status_id', 2) - self.alert_source_link = self.rule.get('iris_alert_source_link', '') + self.alert_source_link = self.rule.get('iris_alert_source_link', None) self.alert_severity_id = self.rule.get('iris_alert_severity_id', 1) - self.alert_context = self.rule.get('iris_alert_context', '') + self.alert_context = self.rule.get('iris_alert_context', None) self.iocs = self.rule.get('iris_iocs', None) - def make_alert_context_records(self, matches): alert_context = {} diff --git a/elastalert/loaders.py b/elastalert/loaders.py index e26cf45f..aa175dca 100644 --- a/elastalert/loaders.py +++ b/elastalert/loaders.py @@ -26,7 +26,7 @@ import elastalert.alerters.httppost import elastalert.alerters.httppost2 import elastalert.alerters.iris -# import elastalert.alerters.lark +import elastalert.alerters.lark import elastalert.alerters.line import elastalert.alerters.pagertree import elastalert.alerters.rocketchat @@ -128,7 +128,7 @@ class RulesLoader(object): 'zabbix': ZabbixAlerter, 'discord': elastalert.alerters.discord.DiscordAlerter, 'dingtalk': elastalert.alerters.dingtalk.DingTalkAlerter, - # 'lark': elastalert.alerters.lark.LarkAlerter, + 'lark': elastalert.alerters.lark.LarkAlerter, 'chatwork': elastalert.alerters.chatwork.ChatworkAlerter, 'datadog': elastalert.alerters.datadog.DatadogAlerter, 'ses': elastalert.alerters.ses.SesAlerter, diff --git a/tests/alerters/iris_test.py b/tests/alerters/iris_test.py index ca7b3344..c21c56b4 100644 --- a/tests/alerters/iris_test.py +++ b/tests/alerters/iris_test.py @@ -41,6 +41,7 @@ def test_iris_make_alert_context_records(caplog): assert expected_data == actual_data + def test_iris_make_iocs_records(caplog): caplog.set_level(logging.INFO) rule = { @@ -98,6 +99,7 @@ def test_iris_make_iocs_records(caplog): actual_data = alert.make_iocs_records([match]) assert expected_data == actual_data + def test_iris_make_alert_minimal(caplog): caplog.set_level(logging.INFO) rule = { @@ -133,6 +135,7 @@ def test_iris_make_alert_minimal(caplog): actual_data = alert.make_alert([match]) assert expected_data == actual_data + def test_iris_make_alert_maximal(caplog): caplog.set_level(logging.INFO) rule = { @@ -212,6 +215,7 @@ def test_iris_make_alert_maximal(caplog): actual_data = alert.make_alert([match]) assert expected_data == actual_data + def test_iris_make_case_minimal(caplog): caplog.set_level(logging.INFO) rule = { @@ -245,6 +249,7 @@ def test_iris_make_case_minimal(caplog): assert expected_data == actual_data + def test_iris_make_case_maximal(caplog): caplog.set_level(logging.INFO) rule = { @@ -314,6 +319,7 @@ def test_iris_make_case_maximal(caplog): assert expected_data == actual_data assert expected_data_iocs == actual_data_iocs + def test_iris_alert_alert(caplog): caplog.set_level(logging.INFO) rule = { @@ -407,6 +413,7 @@ def test_iris_alert_alert(caplog): assert expected_data == mock_post_request.call_args_list[0][1]['json'] assert ('elastalert', logging.INFO, 'Alert sent to Iris') == caplog.record_tuples[0] + def test_iris_get_info(caplog): caplog.set_level(logging.INFO) rule = { From 53c66c1fdbe4b31ba66cfbb92a70873984762fa4 Mon Sep 17 00:00:00 2001 From: Sergey Malinkin Date: Tue, 24 Oct 2023 18:02:16 +0300 Subject: [PATCH 5/7] fixed the failing tests --- tests/alerters/iris_test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/alerters/iris_test.py b/tests/alerters/iris_test.py index c21c56b4..ea41a655 100644 --- a/tests/alerters/iris_test.py +++ b/tests/alerters/iris_test.py @@ -122,13 +122,13 @@ def test_iris_make_alert_minimal(caplog): expected_data = { "alert_title": 'Test Minimal Alert Body', - "alert_description": '', + "alert_description": None, "alert_source": "ElastAlert2", "alert_severity_id": 1, "alert_status_id": 2, "alert_source_event_time": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"), - "alert_note": '', - "alert_tags": '', + "alert_note": None, + "alert_tags": None, "alert_customer_id": 1 } @@ -241,7 +241,7 @@ def test_iris_make_case_minimal(caplog): "case_soc_id": "SOC_123456", "case_customer": 1, "case_name": "Test Minimal Case", - "case_description": '' + "case_description": None } with patch('uuid.uuid4', return_value='123456'): @@ -292,7 +292,7 @@ def test_iris_make_case_maximal(caplog): "case_soc_id": "SOC_123456", "case_customer": 1, "case_name": "Test Maximal Case", - "case_description": '', + "case_description": None, "case_template_id": 55, } From 04b1be7c39af6b29e8a42d8860da7c08de3e111d Mon Sep 17 00:00:00 2001 From: Sergey Malinkin Date: Tue, 24 Oct 2023 18:26:52 +0300 Subject: [PATCH 6/7] try to fix issue with test --- tests/alerters/iris_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/alerters/iris_test.py b/tests/alerters/iris_test.py index ea41a655..79884538 100644 --- a/tests/alerters/iris_test.py +++ b/tests/alerters/iris_test.py @@ -1,4 +1,3 @@ -import json import logging from unittest import mock @@ -404,7 +403,7 @@ def test_iris_alert_alert(caplog): url=f'https://{rule["iris_host"]}/alerts/add', headers={ 'Content-Type': 'application/json', - f'Authorization': f'Bearer {rule["iris_api_token"]}' + 'Authorization': f'Bearer {rule["iris_api_token"]}' }, json=mock.ANY, verify=False, From ee93942d6b5d409ba6dd9c8a2dceb4adb6b66902 Mon Sep 17 00:00:00 2001 From: Sergey Malinkin Date: Tue, 24 Oct 2023 23:47:09 +0300 Subject: [PATCH 7/7] add description for iris_alert_context field --- docs/source/ruletypes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/ruletypes.rst b/docs/source/ruletypes.rst index 6def678d..75dd377c 100644 --- a/docs/source/ruletypes.rst +++ b/docs/source/ruletypes.rst @@ -2634,7 +2634,7 @@ Optional: - ``5`` - High - ``6`` - Critical. -``iris_alert_context``: +``iris_alert_context``: Include information from the match into the alert context. Working as key-value, where the key is your custom name and value - data from elasticsearch message. ``iris_iocs``: Description of the IOC to be added.