diff --git a/.gitignore b/.gitignore index aa21b653..2fbf767e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ __pycache__/ *.pyc virtualenv_run/ -.venv +.venv* *.egg-info/ dist/ venv/ diff --git a/CHANGELOG.md b/CHANGELOG.md index c86513cf..f0d62f38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - [IRIS] Make parameter iris_customer_id optional with default value - [1334](https://github.com/jertel/elastalert2/pull/1334) @malinkinsa - (Re)Implement `skip_invalid` to continue loading rules if one is invalid - [#1338](https://github.com/jertel/elastalert2/pull/1338) - @jertel - [Docs] Refactor the docs structure for improved ease-of-use - [#1337](https://github.com/jertel/elastalert2/pull/1337) - @jertel +- [Email] Refactor SMTP cert/key usage to support Python 3.12 - [#1341](https://github.com/jertel/elastalert2/pull/1341) - @jertel # 2.15.0 diff --git a/docs/source/alerts.rst b/docs/source/alerts.rst index 7aa6b89e..71f604c7 100644 --- a/docs/source/alerts.rst +++ b/docs/source/alerts.rst @@ -213,7 +213,7 @@ Example usage:: jira_alert_owner: $owner$ -.. _alerts: +.. _alert_types: Alert Types =========== diff --git a/docs/source/ruletypes.rst b/docs/source/ruletypes.rst index 432ac3cd..831ed567 100644 --- a/docs/source/ruletypes.rst +++ b/docs/source/ruletypes.rst @@ -320,7 +320,7 @@ or loaded from a module. For loading from a module, the type should be specified alert ^^^^^ -``alert``: The ``Alerter`` type to use. This may be one or more of the built in alerts, see :ref:`Alert Types ` section below for more information, +``alert``: The ``Alerter`` type to use. This may be one or more of the built in alerts, see :ref:`Alert Types ` section below for more information, or loaded from a module. For loading from a module, the alert should be specified as ``module.file.AlertName``. (Required, string or list, no default) Optional Settings diff --git a/docs/source/running_elastalert.rst b/docs/source/running_elastalert.rst index f20e13e9..97013aff 100644 --- a/docs/source/running_elastalert.rst +++ b/docs/source/running_elastalert.rst @@ -258,7 +258,7 @@ information. If no filters are desired, it should be specified as an empty list: ``filter: []`` ``alert`` is a list of alerts to run on each match. For more information on -alert types, see :ref:`Alerts `. The email alert requires an SMTP server +alert types, see :ref:`Alert Types `. The email alert requires an SMTP server for sending mail. By default, it will attempt to use localhost. This can be changed with the ``smtp_host`` option. diff --git a/elastalert/alerters/email.py b/elastalert/alerters/email.py index 7b5ca2fb..81ef5ac1 100644 --- a/elastalert/alerters/email.py +++ b/elastalert/alerters/email.py @@ -1,4 +1,5 @@ import os +import ssl from elastalert.alerts import Alerter from elastalert.util import elastalert_logger, lookup_es_key, EAException @@ -91,12 +92,17 @@ def alert(self, matches): to_addr = to_addr + self.rule['bcc'] try: + ssl_context = None + if self.smtp_cert_file: + ssl_context = ssl.create_default_context() + ssl_context.load_cert_chain(certfile=self.smtp_cert_file, keyfile=self.smtp_key_file) + if self.smtp_ssl: if self.smtp_port: - self.smtp = SMTP_SSL(self.smtp_host, self.smtp_port, keyfile=self.smtp_key_file, certfile=self.smtp_cert_file) + self.smtp = SMTP_SSL(self.smtp_host, self.smtp_port, context=ssl_context) else: # default port : 465 - self.smtp = SMTP_SSL(self.smtp_host, keyfile=self.smtp_key_file, certfile=self.smtp_cert_file) + self.smtp = SMTP_SSL(self.smtp_host, context=ssl_context) else: if self.smtp_port: self.smtp = SMTP(self.smtp_host, self.smtp_port) @@ -105,7 +111,7 @@ def alert(self, matches): self.smtp = SMTP(self.smtp_host) self.smtp.ehlo() if self.smtp.has_extn('STARTTLS'): - self.smtp.starttls(keyfile=self.smtp_key_file, certfile=self.smtp_cert_file) + self.smtp.starttls(context=ssl_context) if 'smtp_auth_file' in self.rule: self.smtp.login(self.user, self.password) except (SMTPException, error) as e: diff --git a/tests/alerters/email_test.py b/tests/alerters/email_test.py index 5d06a35f..218a73a9 100644 --- a/tests/alerters/email_test.py +++ b/tests/alerters/email_test.py @@ -32,7 +32,7 @@ def test_email(caplog): expected = [mock.call('localhost'), mock.call().ehlo(), mock.call().has_extn('STARTTLS'), - mock.call().starttls(certfile=None, keyfile=None), + mock.call().starttls(context=None), mock.call().sendmail( mock.ANY, [ @@ -103,7 +103,7 @@ def test_email_with_unicode_strings(): expected = [mock.call('localhost'), mock.call().ehlo(), mock.call().has_extn('STARTTLS'), - mock.call().starttls(certfile=None, keyfile=None), + mock.call().starttls(context=None), mock.call().sendmail(mock.ANY, ['testing@test.test'], mock.ANY), mock.call().quit()] assert mock_smtp.mock_calls == expected @@ -139,7 +139,7 @@ def test_email_with_auth(): expected = [mock.call('localhost'), mock.call().ehlo(), mock.call().has_extn('STARTTLS'), - mock.call().starttls(certfile=None, keyfile=None), + mock.call().starttls(context=None), mock.call().login('someone', 'hunter2'), mock.call().sendmail( mock.ANY, @@ -168,28 +168,30 @@ def test_email_with_cert_key(): 'smtp_key_file': 'dummy/client.key', 'rule_file': '/tmp/foo.yaml' } - with mock.patch('elastalert.alerters.email.SMTP') as mock_smtp: - with mock.patch('elastalert.alerts.read_yaml') as mock_open: - mock_open.return_value = {'user': 'someone', 'password': 'hunter2'} - mock_smtp.return_value = mock.Mock() - alert = EmailAlerter(rule) + with mock.patch('ssl.SSLContext') as mock_ctx: + with mock.patch('elastalert.alerters.email.SMTP') as mock_smtp: + with mock.patch('elastalert.alerts.read_yaml') as mock_open: + mock_open.return_value = {'user': 'someone', 'password': 'hunter2'} + mock_smtp.return_value = mock.Mock() + alert = EmailAlerter(rule) - alert.alert([{'test_term': 'test_value'}]) - expected = [mock.call('localhost'), - mock.call().ehlo(), - mock.call().has_extn('STARTTLS'), - mock.call().starttls(certfile='dummy/cert.crt', keyfile='dummy/client.key'), - mock.call().login('someone', 'hunter2'), - mock.call().sendmail( - mock.ANY, - [ - 'testing@test.test', - 'test@test.test' - ], - mock.ANY - ), - mock.call().quit()] - assert mock_smtp.mock_calls == expected + alert.alert([{'test_term': 'test_value'}]) + expected = [mock.call('localhost'), + mock.call().ehlo(), + mock.call().has_extn('STARTTLS'), + mock.call().starttls(context=mock.ANY), + mock.call().login('someone', 'hunter2'), + mock.call().sendmail( + mock.ANY, + [ + 'testing@test.test', + 'test@test.test' + ], + mock.ANY + ), + mock.call().quit()] + assert mock_smtp.mock_calls == expected + assert mock.call().load_cert_chain(certfile='dummy/cert.crt', keyfile='dummy/client.key') in mock_ctx.mock_calls def test_email_with_cc(): @@ -210,7 +212,7 @@ def test_email_with_cc(): expected = [mock.call('localhost'), mock.call().ehlo(), mock.call().has_extn('STARTTLS'), - mock.call().starttls(certfile=None, keyfile=None), + mock.call().starttls(context=None), mock.call().sendmail( mock.ANY, [ @@ -249,7 +251,7 @@ def test_email_with_bcc(): expected = [mock.call('localhost'), mock.call().ehlo(), mock.call().has_extn('STARTTLS'), - mock.call().starttls(certfile=None, keyfile=None), + mock.call().starttls(context=None), mock.call().sendmail( mock.ANY, [ @@ -289,7 +291,7 @@ def test_email_with_cc_and_bcc(): expected = [mock.call('localhost'), mock.call().ehlo(), mock.call().has_extn('STARTTLS'), - mock.call().starttls(certfile=None, keyfile=None), + mock.call().starttls(context=None), mock.call().sendmail( mock.ANY, [ @@ -334,7 +336,7 @@ def test_email_with_args(): expected = [mock.call('localhost'), mock.call().ehlo(), mock.call().has_extn('STARTTLS'), - mock.call().starttls(certfile=None, keyfile=None), + mock.call().starttls(context=None), mock.call().sendmail( mock.ANY, [ @@ -498,7 +500,7 @@ def test_email_smtp_port(): expected = [mock.call('localhost', 35), mock.call().ehlo(), mock.call().has_extn('STARTTLS'), - mock.call().starttls(certfile=None, keyfile=None), + mock.call().starttls(context=None), mock.call().sendmail( mock.ANY, [ @@ -537,7 +539,7 @@ def test_email_smtp_ssl_true(): alert = EmailAlerter(rule) alert.alert([{'test_term': 'test_value'}]) - expected = [mock.call('localhost', certfile=None, keyfile=None), + expected = [mock.call('localhost', context=None), mock.call().sendmail( mock.ANY, [ @@ -577,7 +579,7 @@ def test_email_smtp_ssl_true_and_smtp_port(): alert = EmailAlerter(rule) alert.alert([{'test_term': 'test_value'}]) - expected = [mock.call('localhost', 455, certfile=None, keyfile=None), + expected = [mock.call('localhost', 455, context=None), mock.call().sendmail( mock.ANY, [ @@ -640,7 +642,7 @@ def test_email_format_html(): alert = EmailAlerter(rule) alert.alert([{'test_term': 'test_value'}]) - expected = [mock.call('localhost', 455, certfile=None, keyfile=None), + expected = [mock.call('localhost', 455, context=None), mock.call().sendmail( mock.ANY, [