diff --git a/docs/source/ruletypes.rst b/docs/source/ruletypes.rst index 536c3806..35c0a739 100644 --- a/docs/source/ruletypes.rst +++ b/docs/source/ruletypes.rst @@ -1720,6 +1720,8 @@ until the command exits or sends an EOF to stdout. ``pipe_alert_text``: If true, the standard alert body text will be passed to stdin of the command. Note that this will cause ElastAlert to block until the command exits or sends an EOF to stdout. It cannot be used at the same time as ``pipe_match_json``. +``fail_on_non_zero_exit``: By default this is ``False``. Allows monitoring of when commands fail to run. When a command returns a non-zero exit status, the alert raises an exception. + Example usage using old-style format:: alert: @@ -1905,7 +1907,7 @@ by the smtp server. ``bcc``: This adds the BCC emails to the list of recipients but does not show up in the email message. By default, this is left empty. -``email_format``: If set to ``html``, the email's MIME type will be set to HTML, and HTML content should correctly render. If you use this, +``email_format``: If set to 'html', the email's MIME type will be set to HTML, and HTML content should correctly render. If you use this, you need to put your own HTML into ``alert_text`` and use ``alert_text_type: alert_text_jinja`` Or ``alert_text_type: alert_text_only``. Exotel @@ -2242,13 +2244,21 @@ The OpsGenie alert requires one option: Optional: ``opsgenie_account``: The OpsGenie account to integrate with. + ``opsgenie_addr``: The OpsGenie URL to to connect against, default is ``https://api.opsgenie.com/v2/alerts`` + ``opsgenie_recipients``: A list OpsGenie recipients who will be notified by the alert. + ``opsgenie_recipients_args``: Map of arguments used to format opsgenie_recipients. + ``opsgenie_default_receipients``: List of default recipients to notify when the formatting of opsgenie_recipients is unsuccesful. + ``opsgenie_teams``: A list of OpsGenie teams to notify (useful for schedules with escalation). -``opsgenie_teams_args``: Map of arguments used to format opsgenie_teams (useful for assigning the alerts to teams based on some data) + +``opsgenie_teams_args``: Map of arguments used to format opsgenie_teams (useful for assigning the alerts to teams based on some data). + ``opsgenie_default_teams``: List of default teams to notify when the formatting of opsgenie_teams is unsuccesful. + ``opsgenie_tags``: A list of tags for this alert. ``opsgenie_message``: Set the OpsGenie message to something other than the rule name. The message can be formatted with fields from the first match e.g. "Error occurred for {app_name} at {timestamp}.". @@ -2673,8 +2683,11 @@ Zabbix will send notification to a Zabbix server. The item in the host specified Required: ``zbx_sender_host``: The address where zabbix server is running. + ``zbx_sender_port``: The port where zabbix server is listenning. + ``zbx_host``: This field setup the host in zabbix that receives the value sent by ElastAlert 2. + ``zbx_key``: This field setup the key in the host that receives the value sent by ElastAlert 2. Example usage:: diff --git a/elastalert/alerts.py b/elastalert/alerts.py index 0237c031..96a92d8d 100644 --- a/elastalert/alerts.py +++ b/elastalert/alerts.py @@ -416,7 +416,7 @@ def __init__(self, *args): self.smtp_host = self.rule.get('smtp_host', 'localhost') self.smtp_ssl = self.rule.get('smtp_ssl', False) self.from_addr = self.rule.get('from_addr', 'ElastAlert') - self.smtp_port = self.rule.get('smtp_port') + self.smtp_port = self.rule.get('smtp_port', 25) if self.rule.get('smtp_auth_file'): self.get_account(self.rule['smtp_auth_file']) self.smtp_key_file = self.rule.get('smtp_key_file') @@ -1960,6 +1960,7 @@ def get_json_payload(self, match): class HTTPPostAlerter(Alerter): """ Requested elasticsearch indices are sent by HTTP POST. Encoded with JSON. """ + required_options = frozenset(['http_post_url']) def __init__(self, rule): super(HTTPPostAlerter, self).__init__(rule) @@ -2120,7 +2121,7 @@ def get_info(self): class DiscordAlerter(Alerter): - + """ Created a Discord for each alert """ required_options = frozenset(['discord_webhook_url']) def __init__(self, rule): diff --git a/elastalert/schema.yaml b/elastalert/schema.yaml index e34cff89..a451be3f 100644 --- a/elastalert/schema.yaml +++ b/elastalert/schema.yaml @@ -240,66 +240,158 @@ properties: timestamp_field: {type: string} field: {} - ### Commands + ### Simple + simple_webhook_url: *arrayOfString + simple_proxy: {type: string} + + ### Alerta + alerta_api_url: {type: string} + alerta_api_key: {type: string} + alerta_use_qk_as_resource: {type: boolean} + alerta_use_match_timestamp: {type: boolean} + alerta_api_skip_ssl: {type: boolean} + alerta_severity: {type: string} + alerta_timeout: {type: integer} + alerta_type: {type: string} + alerta_resource: {type: string} # Python format string + alerta_service: {type: array, items: {type: string}} # Python format string + alerta_origin: {type: string} # Python format string + alerta_environment: {type: string} # Python format string + alerta_group: {type: string} # Python format string + alerta_correlate: {type: array, items: {type: string}} # Python format string + alerta_tags: {type: array, items: {type: string}} # Python format string + alerta_event: {type: string} # Python format stringalerta_use_match_timestamp + alerta_text: {type: string} # Python format string + alerta_value: {type: string} # Python format string + alerta_attributes_keys: {type: array, items: {type: string}} + alerta_attributes_values: {type: array, items: {type: string}} # Python format string + + ### AWS SES + ses_email: *arrayOfString + ses_from_addr: {type: string} + ses_aws_access_key: {type: string} + ses_aws_secret_key: {type: string} + ses_aws_region: {type: string} + ses_aws_profile: {type: string} + ses_email_reply_to: {type: string} + ses_cc: *arrayOfString + ses_bcc: *arrayOfString + + ### AWS SNS + sns_topic_arn: {type: string} + sns_aws_access_key_id: {type: string} + sns_aws_secret_access_key: {type: string} + sns_aws_region: {type: string} + sns_aws_profile: {type: string} + + ### Chatwork + chatwork_apikey: {type: string} + chatwork_room_id: {type: string} + + ### Command command: *arrayOfString pipe_match_json: {type: boolean} + pipe_alert_text: {type: boolean} fail_on_non_zero_exit: {type: boolean} + ### Datadog + datadog_api_key: {type: string} + datadog_app_key: {type: string} + + ### Dingtalk + dingtalk_access_token: {type: string} + dingtalk_msgtype: {type: string, enum: ['text', 'markdown', 'single_action_card', 'action_card']} + dingtalk_single_title: {type: string} + dingtalk_single_url: {type: string} + dingtalk_btn_orientation: {type: string} + + ## Discord + discord_webhook_url: {type: string} + discord_emoji_title: {type: string} + discord_proxy: {type: string} + discord_proxy_login: {type: string} + discord_proxy_password: {type: string} + discord_embed_color: {type: integer} + discord_embed_footer: {type: string} + discord_embed_icon_url: {type: string} + ### Email email: *arrayOfString - email_reply_to: {type: string} - notify_email: *arrayOfString # if rule is slow or erroring, send to this email + email_from_field: {type: string} + email_add_domain: {type: string} smtp_host: {type: string} + smtp_port: {type: integer} + smtp_ssl: {type: boolean} + smtp_auth_file: {type: string} + smtp_cert_file: {type: string} + smtp_key_file: {type: string} + email_reply_to: {type: string} from_addr: {type: string} + cc: *arrayOfString + bcc: *arrayOfString + email_format: {type: string} + notify_email: *arrayOfString # if rule is slow or erroring, send to this email + + ### Exotel + exotel_account_sid: {type: string} + exotel_auth_token: {type: string} + exotel_to_number: {type: string} + exotel_from_number: {type: string} + + ### Gitter + gitter_webhook_url: {type: string} + gitter_msg_level: {enum: [info, error]} + gitter_proxy: {type: string} + + ### GoogleChat + googlechat_webhook_url: {type: string} + googlechat_format: {type: string, enum: ['basic', 'card']} + googlechat_header_title: {type: string} + googlechat_header_subtitle: {type: string} + googlechat_header_image: {type: string} + googlechat_footer_kibanalink: {type: string} + + ### HTTP POST + http_post_url: {type: string} + http_post_proxy: {type: string} + http_post_ca_certs: {type: boolean} + http_post_ignore_ssl_errors: {type: boolean} + http_post_timeout: {type: integer} ### JIRA jira_server: {type: string} jira_project: {type: string} jira_issuetype: {type: string} jira_account_file: {type: string} # a Yaml file that includes the keys {user:, password:} - jira_assignee: {type: string} jira_component: *arrayOfString jira_components: *arrayOfString + jira_description: {type: string} jira_label: *arrayOfString jira_labels: *arrayOfString + jira_priority: {type: number} + jira_watchers: *arrayOfString jira_bump_tickets: {type: boolean} - jira_bump_in_statuses: *arrayOfString - jira_bump_not_in_statuses: *arrayOfString + jira_ignore_in_title: {type: string} jira_max_age: {type: number} - jira_watchers: *arrayOfString + jira_bump_not_in_statuses: *arrayOfString + jira_bump_in_statuses: *arrayOfString + jira_bump_only: {type: boolean} + jira_transition_to: {type: boolean} + jira_bump_after_inactivity: {type: number} - ### Slack - slack_webhook_url: *arrayOfString - slack_username_override: {type: string} - slack_emoji_override: {type: string} - slack_icon_url_override: {type: string} - slack_msg_color: {enum: [good, warning, danger]} - slack_parse_override: {enum: [none, full]} - slack_text_string: {type: string} - slack_ignore_ssl_errors: {type: boolean} - slack_ca_certs: {type: string} - slack_attach_kibana_discover_url: {type: boolean} - slack_kibana_discover_color: {type: string} - slack_kibana_discover_title: {type: string} - slack_footer: {type: string} - slack_footer_icon: {type: string} - slack_image_url: {type: string} - slack_thumb_url: {type: string} - slack_author_name: {type: string} - slack_author_link: {type: string} - slack_author_icon: {type: string} - slack_msg_pretext: {type: string} + ### Line Notify + linenotify_access_token: {type: string} ### Mattermost mattermost_webhook_url: *arrayOfString mattermost_proxy: {type: string} mattermost_ignore_ssl_errors: {type: boolean} mattermost_username_override: {type: string} - mattermost_icon_url_override: {type: string} mattermost_channel_override: {type: string} - mattermost_msg_color: {enum: [good, warning, danger]} + mattermost_icon_url_override: {type: string} mattermost_msg_pretext: {type: string} + mattermost_msg_color: {enum: [good, warning, danger]} mattermost_msg_fields: *mattermostField mattermost_title_link: {type: string} mattermost_footer: {type: string} @@ -310,7 +402,22 @@ properties: mattermost_author_link: {type: string} mattermost_author_icon: {type: string} - ## Opsgenie + ### Microsoft Teams + ms_teams_webhook_url: {type: string} + ms_teams_alert_summary: {type: string} + ms_teams_theme_color: {type: string} + ms_teams_proxy: {type: string} + ms_teams_alert_fixed_width: {type: boolean} + + ### Opsgenie + opsgenie_key: {type: string} + opsgenie_account: {type: string} + opsgenie_addr: {type: string} + opsgenie_message: {type: string} + opsgenie_alias: {type: string} + opsgenie_subject: {type: string} + opsgenie_priority: {type: string} + opsgenie_proxy: {type: string} opsgenie_details: type: object minProperties: 1 @@ -328,83 +435,99 @@ properties: pagerduty_service_key: {type: string} pagerduty_client_name: {type: string} pagerduty_event_type: {enum: [none, trigger, resolve, acknowledge]} - -### PagerTree + pagerduty_incident_key: {type: string} + pagerduty_incident_key_args: {type: array, items: {type: string}} + pagerduty_proxy: {type: string} + pagerduty_api_version: {type: string, enum: ['v1', 'v2']} + pagerduty_v2_payload_class: {type: string} + pagerduty_v2_payload_class_args: {type: array, items: {type: string}} + pagerduty_v2_payload_component: {type: string} + pagerduty_v2_payload_component_args: {type: array, items: {type: string}} + pagerduty_v2_payload_group: {type: string} + pagerduty_v2_payload_group_args: {type: array, items: {type: string}} + pagerduty_v2_payload_severity: {type: string, enum: ['critical', 'error', 'warning', 'info']} + pagerduty_v2_payload_source: {type: string} + pagerduty_v2_payload_source_args: {type: array, items: {type: string}} + pagerduty_v2_payload_include_all_info: {type: boolean} + + ### PagerTree pagertree_integration_url: {type: string} + pagertree_proxy: {type: string} + + ### ServiceNow + servicenow_rest_url: {type: string} + username: {type: string} + password: {type: string} + short_description: {type: string} + comments: {type: string} + assignment_group: {type: string} + category: {type: string} + subcategory: {type: string} + cmdb_ci: {type: string} + caller_id: {type: string} + servicenow_proxy: {type: string} + ### Slack + slack_webhook_url: *arrayOfString + slack_username_override: {type: string} + slack_channel_override: {type: string} + slack_emoji_override: {type: string} + slack_icon_url_override: {type: string} + slack_msg_color: {enum: [good, warning, danger]} + slack_parse_override: {enum: [none, full]} + slack_text_string: {type: string} + slack_proxy: {type: string} + slack_ignore_ssl_errors: {type: boolean} + slack_title: {type: string} + slack_title_link: {type: string} + slack_timeout: {type: integer} + slack_attach_kibana_discover_url: {type: boolean} + slack_kibana_discover_color: {type: string} + slack_kibana_discover_title: {type: string} + slack_ca_certs: {type: boolean} + slack_footer: {type: string} + slack_footer_icon: {type: string} + slack_image_url: {type: string} + slack_thumb_url: {type: string} + slack_author_name: {type: string} + slack_author_link: {type: string} + slack_author_icon: {type: string} + slack_msg_pretext: {type: string} - ### Exotel - exotel_account_sid: {type: string} - exotel_auth_token: {type: string} - exotel_to_number: {type: string} - exotel_from_number: {type: string} - - ### Twilio - twilio_account_sid: {type: string} - twilio_auth_token: {type: string} - twilio_to_number: {type: string} - twilio_from_number: {type: string} - twilio_message_service_sid: {type: string} - twilio_use_copilot: {type: boolean} - - ### VictorOps + ### Splunk On-Call (Formerly VictorOps) victorops_api_key: {type: string} victorops_routing_key: {type: string} victorops_message_type: {enum: [INFO, WARNING, ACKNOWLEDGEMENT, CRITICAL, RECOVERY]} victorops_entity_id: {type: string} victorops_entity_display_name: {type: string} + victorops_proxy: {type: string} + + ### Stomp + stomp_hostname: {type: string} + stomp_hostport: {type: string} + stomp_login: {type: string} + stomp_password: {type: string} + stomp_ssl: {type: boolean} + stomp_destination: {type: string} ### Telegram telegram_bot_token: {type: string} telegram_room_id: {type: string} telegram_api_url: {type: string} + telegram_proxy: {type: string} + telegram_proxy_login: {type: string} + telegram_proxy_pass: {type: string} - ### Gitter - gitter_webhook_url: {type: string} - gitter_proxy: {type: string} - gitter_msg_level: {enum: [info, error]} - - ### Alerta - alerta_api_url: {type: string} - alerta_api_key: {type: string} - alerta_severity: {type: string} - alerta_resource: {type: string} # Python format string - alerta_environment: {type: string} # Python format string - alerta_origin: {type: string} # Python format string - alerta_group: {type: string} # Python format string - alerta_service: {type: array, items: {type: string}} # Python format string - alerta_correlate: {type: array, items: {type: string}} # Python format string - alerta_tags: {type: array, items: {type: string}} # Python format string - alerta_event: {type: string} # Python format string - alerta_text: {type: string} # Python format string - alerta_type: {type: string} - alerta_value: {type: string} # Python format string - alerta_attributes_keys: {type: array, items: {type: string}} - alerta_attributes_values: {type: array, items: {type: string}} # Python format string - - ### Simple - simple_webhook_url: *arrayOfString - simple_proxy: {type: string} - - ### LineNotify - linenotify_access_token: {type: string} + ### Twilio + twilio_account_sid: {type: string} + twilio_auth_token: {type: string} + twilio_to_number: {type: string} + twilio_from_number: {type: string} + twilio_message_service_sid: {type: string} + twilio_use_copilot: {type: boolean} ### Zabbix zbx_sender_host: {type: string} zbx_sender_port: {type: integer} zbx_host: {type: string} zbx_key: {type: string} - - ## Discord - discord_webhook_url: {type: string} - - ### Dingtalk - dingtalk_access_token: {type: string} - dingtalk_msgtype: {type: string} - dingtalk_single_title: {type: string} - dingtalk_single_url: {type: string} - dingtalk_btn_orientation: {type: string} - - ### Chatwork - chatwork_apikey: {type: string} - chatwork_room_id: {type: string} diff --git a/tests/alerts_test.py b/tests/alerts_test.py index dcce1293..97c9a35f 100644 --- a/tests/alerts_test.py +++ b/tests/alerts_test.py @@ -10,6 +10,7 @@ import pytest from jira.exceptions import JIRAError from requests.auth import HTTPProxyAuth +from requests.exceptions import RequestException from elastalert.alerts import AlertaAlerter from elastalert.alerts import Alerter @@ -38,6 +39,7 @@ from elastalert.alerts import VictorOpsAlerter from elastalert.util import ts_add from elastalert.util import ts_now +from elastalert.util import EAException class mock_rule: @@ -114,7 +116,7 @@ def test_email(): alert = EmailAlerter(rule) alert.alert([{'test_term': 'test_value'}]) - expected = [mock.call('localhost'), + expected = [mock.call('localhost', 25), mock.call().ehlo(), mock.call().has_extn('STARTTLS'), mock.call().starttls(certfile=None, keyfile=None), @@ -179,7 +181,7 @@ def test_email_with_unicode_strings(): alert = EmailAlerter(rule) alert.alert([{'test_term': 'test_value'}]) - expected = [mock.call('localhost'), + expected = [mock.call('localhost', 25), mock.call().ehlo(), mock.call().has_extn('STARTTLS'), mock.call().starttls(certfile=None, keyfile=None), @@ -207,7 +209,7 @@ def test_email_with_auth(): alert = EmailAlerter(rule) alert.alert([{'test_term': 'test_value'}]) - expected = [mock.call('localhost'), + expected = [mock.call('localhost', 25), mock.call().ehlo(), mock.call().has_extn('STARTTLS'), mock.call().starttls(certfile=None, keyfile=None), @@ -229,7 +231,7 @@ def test_email_with_cert_key(): alert = EmailAlerter(rule) alert.alert([{'test_term': 'test_value'}]) - expected = [mock.call('localhost'), + expected = [mock.call('localhost', 25), mock.call().ehlo(), mock.call().has_extn('STARTTLS'), mock.call().starttls(certfile='dummy/cert.crt', keyfile='dummy/client.key'), @@ -248,7 +250,7 @@ def test_email_with_cc(): alert = EmailAlerter(rule) alert.alert([{'test_term': 'test_value'}]) - expected = [mock.call('localhost'), + expected = [mock.call('localhost', 25), mock.call().ehlo(), mock.call().has_extn('STARTTLS'), mock.call().starttls(certfile=None, keyfile=None), @@ -273,7 +275,7 @@ def test_email_with_bcc(): alert = EmailAlerter(rule) alert.alert([{'test_term': 'test_value'}]) - expected = [mock.call('localhost'), + expected = [mock.call('localhost', 25), mock.call().ehlo(), mock.call().has_extn('STARTTLS'), mock.call().starttls(certfile=None, keyfile=None), @@ -298,7 +300,7 @@ def test_email_with_cc_and_bcc(): alert = EmailAlerter(rule) alert.alert([{'test_term': 'test_value'}]) - expected = [mock.call('localhost'), + expected = [mock.call('localhost', 25), mock.call().ehlo(), mock.call().has_extn('STARTTLS'), mock.call().starttls(certfile=None, keyfile=None), @@ -343,7 +345,7 @@ def test_email_with_args(): alert = EmailAlerter(rule) alert.alert([{'test_term': 'test_value', 'test_arg1': 'testing', 'test': {'term': ':)', 'arg3': '☃'}}]) - expected = [mock.call('localhost'), + expected = [mock.call('localhost', 25), mock.call().ehlo(), mock.call().has_extn('STARTTLS'), mock.call().starttls(certfile=None, keyfile=None), @@ -1658,6 +1660,23 @@ def test_command(): assert mock_popen.called_with(['/bin/test', '--arg', 'foobarbaz'], stdin=subprocess.PIPE, shell=False) assert "Non-zero exit code while running command" in str(exception) + # Test OSError + try: + rule = {'command': ['/bin/test/', '--arg', '%(somefield)s'], + 'pipe_alert_text': True, 'type': mock_rule(), 'name': 'Test'} + alert = CommandAlerter(rule) + match = {'@timestamp': '2014-01-01T00:00:00', + 'somefield': 'foobarbaz'} + alert_text = str(BasicMatchString(rule, match)) + mock_run = mock.MagicMock(side_effect=OSError) + with mock.patch("elastalert.alerts.subprocess.Popen", mock_run), pytest.raises(OSError) as mock_popen: + mock_subprocess = mock.Mock() + mock_popen.return_value = mock_subprocess + mock_subprocess.communicate.return_value = (None, None) + alert.alert([match]) + except EAException: + assert True + def test_ms_teams(): rule = { @@ -1770,6 +1789,30 @@ def test_ms_teams_proxy(): assert expected_data == json.loads(mock_post_request.call_args_list[0][1]['data']) +def test_ms_teams_ea_exception(): + try: + rule = { + 'name': 'Test Rule', + 'type': 'any', + 'ms_teams_webhook_url': 'http://test.webhook.url', + 'ms_teams_alert_summary': 'Alert from ElastAlert', + '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' + } + mock_run = mock.MagicMock(side_effect=RequestException) + with mock.patch('requests.post', mock_run), pytest.raises(RequestException): + alert.alert([match]) + except EAException: + assert True + + def test_slack_uses_custom_title(): rule = { 'name': 'Test Rule', @@ -3087,6 +3130,31 @@ def test_slack_msg_pretext(): assert expected_data == json.loads(mock_post_request.call_args_list[0][1]['data']) +def test_slack_ea_exception(): + try: + rule = { + 'name': 'Test Rule', + 'type': 'any', + 'slack_webhook_url': 'http://please.dontgohere.slack', + 'slack_username_override': 'elastalert', + 'slack_msg_pretext': 'pretext value', + 'alert_subject': 'Cool subject', + 'alert': [] + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = SlackAlerter(rule) + match = { + '@timestamp': '2016-01-01T00:00:00', + 'somefield': 'foobarbaz' + } + mock_run = mock.MagicMock(side_effect=RequestException) + with mock.patch('requests.post', mock_run), pytest.raises(RequestException): + alert.alert([match]) + except EAException: + assert True + + def test_http_alerter_with_payload(): rule = { 'name': 'Test HTTP Post Alerter With Payload', @@ -3359,6 +3427,30 @@ def test_http_alerter_post_ca_certs_false(): assert expected_data == json.loads(mock_post_request.call_args_list[0][1]['data']) +def test_http_alerter_post_ea_exception(): + try: + rule = { + 'name': 'Test HTTP Post Alerter Without Payload', + 'type': 'any', + 'http_post_url': 'http://test.webhook.url', + 'http_post_static_payload': {'name': 'somestaticname'}, + 'http_post_ca_certs': False, + 'alert': [] + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = HTTPPostAlerter(rule) + match = { + '@timestamp': '2017-01-01T00:00:00', + 'somefield': 'foobarbaz' + } + mock_run = mock.MagicMock(side_effect=RequestException) + with mock.patch('requests.post', mock_run), pytest.raises(RequestException): + alert.alert([match]) + except EAException: + assert True + + def test_pagerduty_alerter(): rule = { 'name': 'Test PD Rule', @@ -3930,6 +4022,36 @@ def test_pagerduty_alerter_proxy(): assert expected_data == json.loads(mock_post_request.call_args_list[0][1]['data']) +def test_pagerduty_ea_exception(): + try: + rule = { + 'name': 'Test PD Rule', + 'type': 'any', + 'alert_subject': '{0} kittens', + 'alert_subject_args': ['somefield'], + 'pagerduty_service_key': 'magicalbadgers', + 'pagerduty_event_type': 'trigger', + 'pagerduty_client_name': 'ponies inc.', + 'pagerduty_incident_key': 'custom {0}', + 'pagerduty_incident_key_args': ['someotherfield'], + 'pagerduty_proxy': 'http://proxy.url', + 'alert': [] + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = PagerDutyAlerter(rule) + match = { + '@timestamp': '2017-01-01T00:00:00', + 'somefield': 'Stinkiest', + 'someotherfield': 'foobarbaz' + } + mock_run = mock.MagicMock(side_effect=RequestException) + with mock.patch('requests.post', mock_run), pytest.raises(RequestException): + alert.alert([match]) + except EAException: + assert True + + def test_alert_text_kw(ea): rule = ea.rules[0].copy() rule['alert_text'] = '{field} at {time}' @@ -4627,6 +4749,44 @@ def test_alerta_tags(): mock_post_request.call_args_list[0][1]['data']) +def test_alerta_ea_exception(): + try: + rule = { + 'name': 'Test Alerta rule!', + 'alerta_api_url': 'http://elastalerthost:8080/api/alert', + 'timeframe': datetime.timedelta(hours=1), + 'timestamp_field': '@timestamp', + 'alerta_attributes_keys': ["hostname", "TimestampEvent", "senderIP"], + 'alerta_attributes_values': ["{hostname}", "{logdate}", "{sender_ip}"], + 'alerta_correlate': ["ProbeUP", "ProbeDOWN"], + 'alerta_event': "ProbeUP", + 'alerta_group': "Health", + 'alerta_origin': "ElastAlert 2", + 'alerta_severity': "debug", + 'alerta_text': "Probe {hostname} is UP at {logdate} GMT", + 'alerta_value': "UP", + 'type': 'any', + 'alerta_use_match_timestamp': True, + 'alerta_tags': ['elastalert2'], + 'alert': 'alerta' + } + + match = { + '@timestamp': '2014-10-10T00:00:00', + 'sender_ip': '1.1.1.1', + 'hostname': 'aProbe' + } + + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = AlertaAlerter(rule) + mock_run = mock.MagicMock(side_effect=RequestException) + with mock.patch('requests.post', mock_run), pytest.raises(RequestException): + alert.alert([match]) + except EAException: + assert True + + def test_alert_subject_size_limit_no_args(): rule = { 'name': 'test_rule', @@ -4725,6 +4885,30 @@ def test_datadog_alerter(): assert expected_data == actual_data +def test_datadog_alerterea_exception(): + try: + rule = { + 'name': 'Test Datadog Event Alerter', + 'type': 'any', + 'datadog_api_key': 'test-api-key', + 'datadog_app_key': 'test-app-key', + 'alert': [], + 'alert_subject': 'Test Datadog Event Alert' + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = DatadogAlerter(rule) + match = { + '@timestamp': '2021-01-01T00:00:00', + 'name': 'datadog-test-name' + } + mock_run = mock.MagicMock(side_effect=RequestException) + with mock.patch('requests.post', mock_run), pytest.raises(RequestException): + alert.alert([match]) + except EAException: + assert True + + def test_pagertree(): rule = { 'name': 'Test PagerTree Rule', @@ -4806,6 +4990,29 @@ def test_pagertree_proxy(): assert expected_data["Description"] == actual_data['Description'] +def test_pagertree_ea_exception(): + try: + rule = { + 'name': 'Test PagerTree Rule', + 'type': 'any', + 'pagertree_integration_url': 'https://api.pagertree.com/integration/xxxxx', + 'pagertree_proxy': 'http://proxy.url', + 'alert': [] + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = PagerTreeAlerter(rule) + match = { + '@timestamp': '2021-01-01T00:00:00', + 'somefield': 'foobarbaz' + } + mock_run = mock.MagicMock(side_effect=RequestException) + with mock.patch('requests.post', mock_run), pytest.raises(RequestException): + alert.alert([match]) + except EAException: + assert True + + def test_line_notify(): rule = { 'name': 'Test LineNotify Rule', @@ -4840,6 +5047,28 @@ def test_line_notify(): assert expected_data == actual_data +def test_line_notify_ea_exception(): + try: + rule = { + 'name': 'Test LineNotify Rule', + 'type': 'any', + 'linenotify_access_token': 'xxxxx', + 'alert': [] + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = LineNotifyAlerter(rule) + match = { + '@timestamp': '2021-01-01T00:00:00', + 'somefield': 'foobarbaz' + } + mock_run = mock.MagicMock(side_effect=RequestException) + with mock.patch('requests.post', mock_run), pytest.raises(RequestException): + alert.alert([match]) + except EAException: + assert True + + def test_gitter_msg_level_default(): rule = { 'name': 'Test Gitter Rule', @@ -4978,6 +5207,30 @@ def test_gitter_proxy(): assert 'error' in actual_data['level'] +def test_gitter_ea_exception(): + try: + rule = { + 'name': 'Test Gitter Rule', + 'type': 'any', + 'gitter_webhook_url': 'https://webhooks.gitter.im/e/xxxxx', + 'gitter_msg_level': 'error', + 'gitter_proxy': 'http://proxy.url', + 'alert': [] + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = GitterAlerter(rule) + match = { + '@timestamp': '2021-01-01T00:00:00', + 'somefield': 'foobarbaz' + } + mock_run = mock.MagicMock(side_effect=RequestException) + with mock.patch('requests.post', mock_run), pytest.raises(RequestException): + alert.alert([match]) + except EAException: + assert True + + def test_chatwork(): rule = { 'name': 'Test Chatwork Rule', @@ -5047,6 +5300,32 @@ def test_chatwork_proxy(): assert expected_data == actual_data +def test_chatwork_ea_exception(): + try: + rule = { + 'name': 'Test Chatwork Rule', + 'type': 'any', + 'chatwork_apikey': 'xxxx1', + 'chatwork_room_id': 'xxxx2', + 'chatwork_proxy': 'http://proxy.url', + 'chatwork_proxy_login': 'admin', + 'chatwork_proxy_pass': 'password', + 'alert': [] + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = ChatworkAlerter(rule) + match = { + '@timestamp': '2021-01-01T00:00:00', + 'somefield': 'foobarbaz' + } + mock_run = mock.MagicMock(side_effect=RequestException) + with mock.patch('requests.post', mock_run), pytest.raises(RequestException): + alert.alert([match]) + except EAException: + assert True + + def test_telegram(): rule = { 'name': 'Test Telegram Rule', @@ -5159,6 +5438,29 @@ def test_telegram_text_maxlength(): assert expected_data == actual_data +def test_telegram_ea_exception(): + try: + rule = { + 'name': 'Test Telegram Rule' + ('a' * 3985), + 'type': 'any', + 'telegram_bot_token': 'xxxxx1', + 'telegram_room_id': 'xxxxx2', + 'alert': [] + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = TelegramAlerter(rule) + match = { + '@timestamp': '2021-01-01T00:00:00', + 'somefield': 'foobarbaz' + } + mock_run = mock.MagicMock(side_effect=RequestException) + with mock.patch('requests.post', mock_run), pytest.raises(RequestException): + alert.alert([match]) + except EAException: + assert True + + def test_service_now(): rule = { 'name': 'Test ServiceNow Rule', @@ -5264,6 +5566,38 @@ def test_service_now_proxy(): assert expected_data == actual_data +def test_service_now_ea_exception(): + try: + rule = { + 'name': 'Test ServiceNow Rule', + 'type': 'any', + 'username': 'ServiceNow username', + 'password': 'ServiceNow password', + 'servicenow_rest_url': 'https://xxxxxxxxxx', + 'short_description': 'ServiceNow short_description', + 'comments': 'ServiceNow comments', + 'assignment_group': 'ServiceNow assignment_group', + 'category': 'ServiceNow category', + 'subcategory': 'ServiceNow subcategory', + 'cmdb_ci': 'ServiceNow cmdb_ci', + 'caller_id': 'ServiceNow caller_id', + 'servicenow_proxy': 'http://proxy.url', + 'alert': [] + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = ServiceNowAlerter(rule) + match = { + '@timestamp': '2021-01-01T00:00:00', + 'somefield': 'foobarbaz' + } + mock_run = mock.MagicMock(side_effect=RequestException) + with mock.patch('requests.post', mock_run), pytest.raises(RequestException): + alert.alert([match]) + except EAException: + assert True + + def test_victor_ops(): rule = { 'name': 'Test VictorOps Rule', @@ -5341,6 +5675,32 @@ def test_victor_ops_proxy(): assert expected_data == actual_data +def test_victor_ops_ea_exception(): + try: + rule = { + 'name': 'Test VictorOps Rule', + 'type': 'any', + 'victorops_api_key': 'xxxx1', + 'victorops_routing_key': 'xxxx2', + 'victorops_message_type': 'INFO', + 'victorops_entity_display_name': 'no entity display name', + 'victorops_proxy': 'http://proxy.url', + 'alert': [] + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = VictorOpsAlerter(rule) + match = { + '@timestamp': '2021-01-01T00:00:00', + 'somefield': 'foobarbaz' + } + mock_run = mock.MagicMock(side_effect=RequestException) + with mock.patch('requests.post', mock_run), pytest.raises(RequestException): + alert.alert([match]) + except EAException: + assert True + + def test_google_chat_basic(): rule = { 'name': 'Test GoogleChat Rule', @@ -5438,6 +5798,33 @@ def test_google_chat_card(): assert expected_data == actual_data +def test_google_chat_ea_exception(): + try: + rule = { + 'name': 'Test GoogleChat Rule', + 'type': 'any', + 'googlechat_webhook_url': 'http://xxxxxxx', + 'googlechat_format': 'card', + 'googlechat_header_title': 'xxxx1', + 'googlechat_header_subtitle': 'xxxx2', + 'googlechat_header_image': 'http://xxxx/image.png', + 'googlechat_footer_kibanalink': 'http://xxxxx/kibana', + 'alert': [] + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = GoogleChatAlerter(rule) + match = { + '@timestamp': '2021-01-01T00:00:00', + 'somefield': 'foobarbaz' + } + mock_run = mock.MagicMock(side_effect=RequestException) + with mock.patch('requests.post', mock_run), pytest.raises(RequestException): + alert.alert([match]) + except EAException: + assert True + + def test_discord(): rule = { 'name': 'Test Discord Rule', @@ -5612,6 +5999,31 @@ def test_discord_description_maxlength(): assert expected_data == actual_data +def test_discord_ea_exception(): + try: + rule = { + 'name': 'Test Discord Rule' + ('a' * 2069), + 'type': 'any', + 'discord_webhook_url': 'http://xxxxxxx', + 'discord_emoji_title': ':warning:', + 'discord_embed_color': 0xffffff, + 'alert': [], + 'alert_subject': 'Test Discord' + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = DiscordAlerter(rule) + match = { + '@timestamp': '2021-01-01T00:00:00', + 'somefield': 'foobarbaz' + } + mock_run = mock.MagicMock(side_effect=RequestException) + with mock.patch('requests.post', mock_run), pytest.raises(RequestException): + alert.alert([match]) + except EAException: + assert True + + def test_dingtalk_text(): rule = { 'name': 'Test DingTalk Rule', @@ -5856,6 +6268,46 @@ def test_dingtalk_proxy(): assert expected_data == actual_data +def test_dingtalk_ea_exception(): + try: + rule = { + 'name': 'Test DingTalk Rule', + 'type': 'any', + 'dingtalk_access_token': 'xxxxxxx', + 'dingtalk_msgtype': 'action_card', + 'dingtalk_single_title': 'elastalert', + 'dingtalk_single_url': 'http://xxxxx2', + 'dingtalk_btn_orientation': '1', + 'dingtalk_btns': [ + { + 'title': 'test1', + 'actionURL': 'https://xxxxx0/' + }, + { + 'title': 'test2', + 'actionURL': 'https://xxxxx1/' + } + ], + 'dingtalk_proxy': 'http://proxy.url', + 'dingtalk_proxy_login': 'admin', + 'dingtalk_proxy_pass': 'password', + 'alert': [], + 'alert_subject': 'Test DingTalk' + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = DingTalkAlerter(rule) + match = { + '@timestamp': '2021-01-01T00:00:00', + 'somefield': 'foobarbaz' + } + mock_run = mock.MagicMock(side_effect=RequestException) + with mock.patch('requests.post', mock_run), pytest.raises(RequestException): + alert.alert([match]) + except EAException: + assert True + + def test_mattermost_proxy(): rule = { 'name': 'Test Mattermost Rule', @@ -6594,3 +7046,30 @@ def test_mattermost_author_icon(): actual_data = json.loads(mock_post_request.call_args_list[0][1]['data']) assert expected_data == actual_data + + +def test_mattermost_ea_exception(): + try: + rule = { + 'name': 'Test Mattermost Rule', + 'type': 'any', + 'alert_text_type': 'alert_text_only', + 'mattermost_webhook_url': 'http://xxxxx', + 'mattermost_msg_pretext': 'aaaaa', + 'mattermost_msg_color': 'danger', + 'mattermost_author_icon': 'http://author.icon.url', + 'alert': [], + 'alert_subject': 'Test Mattermost' + } + rules_loader = FileRulesLoader({}) + rules_loader.load_modules(rule) + alert = MattermostAlerter(rule) + match = { + '@timestamp': '2021-01-01T00:00:00', + 'somefield': 'foobarbaz' + } + mock_run = mock.MagicMock(side_effect=RequestException) + with mock.patch('requests.post', mock_run), pytest.raises(RequestException): + alert.alert([match]) + except EAException: + assert True