diff --git a/CHANGELOG.md b/CHANGELOG.md index ea2f0fa..4eb074a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 9.2.1 + +## Added + +- Better checking of `msconfig` configuration (PR #695) + +## Changed + +- Updated `dbip-country-lite` database to version `2026-03` +- DNS query error logging level from `warning` to `debug` + ## 9.2.0 ### Added diff --git a/parsedmarc/cli.py b/parsedmarc/cli.py index 242a8bd..402e45e 100644 --- a/parsedmarc/cli.py +++ b/parsedmarc/cli.py @@ -1134,7 +1134,9 @@ def _main(): if "api_key" in opensearch_config: opts.opensearch_api_key = opensearch_config["api_key"] if "auth_type" in opensearch_config: - opts.opensearch_auth_type = opensearch_config["auth_type"].strip().lower() + opts.opensearch_auth_type = ( + opensearch_config["auth_type"].strip().lower() + ) elif "authentication_type" in opensearch_config: opts.opensearch_auth_type = ( opensearch_config["authentication_type"].strip().lower() diff --git a/parsedmarc/constants.py b/parsedmarc/constants.py index 63f0f5e..f65a0a1 100644 --- a/parsedmarc/constants.py +++ b/parsedmarc/constants.py @@ -1,3 +1,3 @@ -__version__ = "9.2.0" +__version__ = "9.2.1" USER_AGENT = f"parsedmarc/{__version__}" diff --git a/parsedmarc/opensearch.py b/parsedmarc/opensearch.py index 2f22321..5b46d6c 100644 --- a/parsedmarc/opensearch.py +++ b/parsedmarc/opensearch.py @@ -316,9 +316,7 @@ def set_hosts( raise OpenSearchError( "Unable to load AWS credentials for OpenSearch SigV4 authentication" ) - conn_params["http_auth"] = AWSV4SignerAuth( - credentials, aws_region, aws_service - ) + conn_params["http_auth"] = AWSV4SignerAuth(credentials, aws_region, aws_service) conn_params["connection_class"] = RequestsHttpConnection elif normalized_auth_type == "basic": if username and password: diff --git a/parsedmarc/resources/dbip/dbip-country-lite.mmdb b/parsedmarc/resources/dbip/dbip-country-lite.mmdb index f1fced5..d506b62 100644 Binary files a/parsedmarc/resources/dbip/dbip-country-lite.mmdb and b/parsedmarc/resources/dbip/dbip-country-lite.mmdb differ diff --git a/parsedmarc/utils.py b/parsedmarc/utils.py index 5b38fbc..965c9bb 100644 --- a/parsedmarc/utils.py +++ b/parsedmarc/utils.py @@ -205,8 +205,7 @@ def get_reverse_dns( )[0] except dns.exception.DNSException as e: - logger.warning(f"get_reverse_dns({ip_address}) exception: {e}") - pass + logger.debug(f"get_reverse_dns({ip_address}) exception: {e}") return hostname diff --git a/tests.py b/tests.py index ef7ffe9..efc4b25 100755 --- a/tests.py +++ b/tests.py @@ -298,7 +298,9 @@ authentication_type = awssigv4 aws_region = eu-west-1 aws_service = aoss """ - with tempfile.NamedTemporaryFile("w", suffix=".ini", delete=False) as config_file: + with tempfile.NamedTemporaryFile( + "w", suffix=".ini", delete=False + ) as config_file: config_file.write(config) config_path = config_file.name self.addCleanup(lambda: os.path.exists(config_path) and os.remove(config_path)) @@ -347,7 +349,9 @@ password = test-password [elasticsearch] hosts = localhost """ - with tempfile.NamedTemporaryFile("w", suffix=".ini", delete=False) as config_file: + with tempfile.NamedTemporaryFile( + "w", suffix=".ini", delete=False + ) as config_file: config_file.write(config) config_path = config_file.name self.addCleanup(lambda: os.path.exists(config_path) and os.remove(config_path)) @@ -395,7 +399,9 @@ password = test-password [elasticsearch] hosts = localhost """ - with tempfile.NamedTemporaryFile("w", suffix=".ini", delete=False) as config_file: + with tempfile.NamedTemporaryFile( + "w", suffix=".ini", delete=False + ) as config_file: config_file.write(config) config_path = config_file.name self.addCleanup(lambda: os.path.exists(config_path) and os.remove(config_path)) @@ -435,8 +441,8 @@ hosts = localhost mock_save_aggregate.side_effect = parsedmarc.elastic.ElasticsearchError( "aggregate sink failed" ) - mock_save_forensic_opensearch.side_effect = parsedmarc.cli.opensearch.OpenSearchError( - "forensic sink failed" + mock_save_forensic_opensearch.side_effect = ( + parsedmarc.cli.opensearch.OpenSearchError("forensic sink failed") ) config = """[general] @@ -456,7 +462,9 @@ hosts = localhost [opensearch] hosts = localhost """ - with tempfile.NamedTemporaryFile("w", suffix=".ini", delete=False) as config_file: + with tempfile.NamedTemporaryFile( + "w", suffix=".ini", delete=False + ) as config_file: config_file.write(config) config_path = config_file.name self.addCleanup(lambda: os.path.exists(config_path) and os.remove(config_path)) @@ -555,9 +563,7 @@ class TestGmailConnection(unittest.TestCase): "from_authorized_user_file", return_value=creds, ): - returned = _get_creds( - token_path, "credentials.json", ["scope"], 8080 - ) + returned = _get_creds(token_path, "credentials.json", ["scope"], 8080) finally: os.remove(token_path) self.assertEqual(returned, creds) @@ -611,9 +617,7 @@ class TestGmailConnection(unittest.TestCase): "from_authorized_user_file", return_value=expired_creds, ): - returned = _get_creds( - token_path, "credentials.json", ["scope"], 8080 - ) + returned = _get_creds(token_path, "credentials.json", ["scope"], 8080) finally: os.remove(token_path) @@ -671,7 +675,9 @@ class TestGraphConnection(unittest.TestCase): with patch.object(graph_module, "sleep") as mocked_sleep: messages = connection._get_all_messages("/url", batch_size=0, since=None) self.assertEqual([msg["id"] for msg in messages], ["1"]) - mocked_sleep.assert_called_once_with(graph_module.GRAPH_REQUEST_RETRY_DELAY_SECONDS) + mocked_sleep.assert_called_once_with( + graph_module.GRAPH_REQUEST_RETRY_DELAY_SECONDS + ) def testGetAllMessagesRaisesAfterRetryExhaustion(self): connection = MSGraphConnection.__new__(MSGraphConnection) @@ -715,7 +721,9 @@ class TestGraphConnection(unittest.TestCase): def testFetchMessagesPassesSinceAndBatchSize(self): connection = MSGraphConnection.__new__(MSGraphConnection) connection.mailbox_name = "mailbox@example.com" - connection._find_folder_id_from_folder_path = MagicMock(return_value="folder-id") + connection._find_folder_id_from_folder_path = MagicMock( + return_value="folder-id" + ) connection._get_all_messages = MagicMock(return_value=[{"id": "1"}]) self.assertEqual( connection.fetch_messages("Inbox", since="2026-03-01", batch_size=5), ["1"] @@ -776,7 +784,9 @@ class TestGraphConnection(unittest.TestCase): def testGenerateCredentialDeviceCode(self): fake_credential = object() - with patch.object(graph_module, "_get_cache_args", return_value={"cached": True}): + with patch.object( + graph_module, "_get_cache_args", return_value={"cached": True} + ): with patch.object( graph_module, "DeviceCodeCredential", @@ -916,14 +926,18 @@ class TestGraphConnection(unittest.TestCase): fake_credential.authenticate.assert_called_once_with(scopes=["Mail.ReadWrite"]) cache_auth.assert_called_once() graph_client.assert_called_once() - self.assertEqual(graph_client.call_args.kwargs.get("scopes"), ["Mail.ReadWrite"]) + self.assertEqual( + graph_client.call_args.kwargs.get("scopes"), ["Mail.ReadWrite"] + ) def testInitCertificateAuthSkipsInteractiveAuthenticate(self): class DummyCertificateCredential: pass fake_credential = DummyCertificateCredential() - with patch.object(graph_module, "CertificateCredential", DummyCertificateCredential): + with patch.object( + graph_module, "CertificateCredential", DummyCertificateCredential + ): with patch.object( graph_module, "_generate_credential", return_value=fake_credential ): @@ -1023,8 +1037,12 @@ class TestImapConnection(unittest.TestCase): with self.assertRaises(_BreakLoop): connection.watch(callback, check_timeout=1) callback.assert_called_once_with(connection) + + class TestGmailAuthModes(unittest.TestCase): - @patch("parsedmarc.mail.gmail.service_account.Credentials.from_service_account_file") + @patch( + "parsedmarc.mail.gmail.service_account.Credentials.from_service_account_file" + ) def testGetCredsServiceAccountWithoutSubject(self, mock_from_service_account_file): service_creds = MagicMock() service_creds.with_subject.return_value = MagicMock() @@ -1046,7 +1064,9 @@ class TestGmailAuthModes(unittest.TestCase): ) service_creds.with_subject.assert_not_called() - @patch("parsedmarc.mail.gmail.service_account.Credentials.from_service_account_file") + @patch( + "parsedmarc.mail.gmail.service_account.Credentials.from_service_account_file" + ) def testGetCredsServiceAccountWithSubject(self, mock_from_service_account_file): base_creds = MagicMock() delegated_creds = MagicMock() @@ -1252,6 +1272,7 @@ class TestImapFallbacks(unittest.TestCase): connection.move_message(99, "Archive") delete_mock.assert_not_called() + class TestMailboxWatchSince(unittest.TestCase): def testWatchInboxPassesSinceToMailboxFetch(self): mailbox_connection = SimpleNamespace() @@ -1353,6 +1374,7 @@ class TestMailboxPerformance(unittest.TestCase): create_folders=False, ) self.assertEqual(len(connection.fetch_calls), 1) + @patch("parsedmarc.cli.get_dmarc_reports_from_mailbox") @patch("parsedmarc.cli.MSGraphConnection") def testCliPassesMsGraphCertificateAuthSettings( @@ -1501,6 +1523,7 @@ user = owner@example.com mock_graph_connection.assert_not_called() mock_get_mailbox_reports.assert_not_called() + class _FakeGraphClient: def get(self, url, params=None): if "/mailFolders/inbox?$select=id,displayName" in url: @@ -1539,15 +1562,14 @@ class TestMSGraphFolderFallback(unittest.TestCase): connection._request_with_retries = MagicMock( side_effect=lambda method_name, *args, **kwargs: getattr( connection._client, method_name - )( - *args, **kwargs - ) + )(*args, **kwargs) ) folder_id = connection._find_folder_id_with_parent("Inbox", None) self.assertEqual(folder_id, "inbox-id") connection._request_with_retries.assert_any_call( - "get", "/users/shared@example.com/mailFolders?$filter=displayName eq 'Inbox'" + "get", + "/users/shared@example.com/mailFolders?$filter=displayName eq 'Inbox'", ) connection._request_with_retries.assert_any_call( "get", "/users/shared@example.com/mailFolders/inbox?$select=id,displayName" @@ -1560,9 +1582,7 @@ class TestMSGraphFolderFallback(unittest.TestCase): connection._request_with_retries = MagicMock( side_effect=lambda method_name, *args, **kwargs: getattr( connection._client, method_name - )( - *args, **kwargs - ) + )(*args, **kwargs) ) with self.assertRaises(RuntimeWarning):