From af4dfd1ed6eaa91cf47a9a016c9aaaafbb379db9 Mon Sep 17 00:00:00 2001 From: Vahid Gharavi Date: Mon, 15 Jun 2026 15:25:48 +0200 Subject: [PATCH 1/5] fix(azure/postgresql): isolate per-server collection failures Collecting one PostgreSQL flexible server could abort collection of every remaining server in the subscription. The per-server work ran inside a single subscription-level try/except, so one failing call discarded all servers not yet processed (the failing one and the rest). This happened on PostgreSQL 16+, where the `connection_throttle.enable` server parameter no longer exists: `_get_connection_throttling` raised ConfigurationNotExists, which bubbled up and dropped the remaining servers in the subscription. It was silent because the error is logged at ERROR level, which is hidden from the console by default. - Wrap each server's collection in its own try/except so one server's failure no longer aborts the others in the subscription. - Make `_get_connection_throttling` tolerate the missing parameter (return None, like `_get_log_retention_days`), so PostgreSQL 16+ servers are still collected. Adds regression tests for the missing-config case and a hard per-server failure. --- .../services/postgresql/postgresql_service.py | 120 ++++++++++-------- .../postgresql/postgresql_service_test.py | 88 ++++++++++++- 2 files changed, 154 insertions(+), 54 deletions(-) diff --git a/prowler/providers/azure/services/postgresql/postgresql_service.py b/prowler/providers/azure/services/postgresql/postgresql_service.py index 13081ad2709..7034e977195 100644 --- a/prowler/providers/azure/services/postgresql/postgresql_service.py +++ b/prowler/providers/azure/services/postgresql/postgresql_service.py @@ -20,56 +20,64 @@ def _get_flexible_servers(self): flexible_servers.update({subscription: []}) flexible_servers_list = client.servers.list() for postgresql_server in flexible_servers_list: - resource_group = self._get_resource_group(postgresql_server.id) - # Fetch full server object once to extract multiple properties - server_details = client.servers.get( - resource_group, postgresql_server.name - ) - require_secure_transport = self._get_require_secure_transport( - subscription, resource_group, postgresql_server.name - ) - active_directory_auth = self._extract_active_directory_auth( - server_details - ) - entra_id_admins = self._get_entra_id_admins( - subscription, resource_group, postgresql_server.name - ) - log_checkpoints = self._get_log_checkpoints( - subscription, resource_group, postgresql_server.name - ) - log_disconnections = self._get_log_disconnections( - subscription, resource_group, postgresql_server.name - ) - log_connections = self._get_log_connections( - subscription, resource_group, postgresql_server.name - ) - connection_throttling = self._get_connection_throttling( - subscription, resource_group, postgresql_server.name - ) - log_retention_days = self._get_log_retention_days( - subscription, resource_group, postgresql_server.name - ) - firewall = self._get_firewall( - subscription, resource_group, postgresql_server.name - ) - location = server_details.location - flexible_servers[subscription].append( - Server( - id=postgresql_server.id, - name=postgresql_server.name, - resource_group=resource_group, - location=location, - require_secure_transport=require_secure_transport, - active_directory_auth=active_directory_auth, - entra_id_admins=entra_id_admins, - log_checkpoints=log_checkpoints, - log_connections=log_connections, - log_disconnections=log_disconnections, - connection_throttling=connection_throttling, - log_retention_days=log_retention_days, - firewall=firewall, + # Isolate each server: a failure collecting one server must + # not abort collection of the remaining servers in the + # subscription. + try: + resource_group = self._get_resource_group(postgresql_server.id) + # Fetch full server object once to extract multiple properties + server_details = client.servers.get( + resource_group, postgresql_server.name + ) + require_secure_transport = self._get_require_secure_transport( + subscription, resource_group, postgresql_server.name + ) + active_directory_auth = self._extract_active_directory_auth( + server_details + ) + entra_id_admins = self._get_entra_id_admins( + subscription, resource_group, postgresql_server.name + ) + log_checkpoints = self._get_log_checkpoints( + subscription, resource_group, postgresql_server.name + ) + log_disconnections = self._get_log_disconnections( + subscription, resource_group, postgresql_server.name + ) + log_connections = self._get_log_connections( + subscription, resource_group, postgresql_server.name + ) + connection_throttling = self._get_connection_throttling( + subscription, resource_group, postgresql_server.name + ) + log_retention_days = self._get_log_retention_days( + subscription, resource_group, postgresql_server.name + ) + firewall = self._get_firewall( + subscription, resource_group, postgresql_server.name + ) + location = server_details.location + flexible_servers[subscription].append( + Server( + id=postgresql_server.id, + name=postgresql_server.name, + resource_group=resource_group, + location=location, + require_secure_transport=require_secure_transport, + active_directory_auth=active_directory_auth, + entra_id_admins=entra_id_admins, + log_checkpoints=log_checkpoints, + log_connections=log_connections, + log_disconnections=log_disconnections, + connection_throttling=connection_throttling, + log_retention_days=log_retention_days, + firewall=firewall, + ) + ) + except Exception as error: + logger.error( + f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) - ) except Exception as error: logger.error( f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -154,10 +162,16 @@ def _get_entra_id_admins(self, subscription, resource_group_name, server_name): def _get_connection_throttling(self, subscription, resouce_group_name, server_name): client = self.clients[subscription] - connection_throttling = client.configurations.get( - resouce_group_name, server_name, "connection_throttle.enable" - ) - return connection_throttling.value.upper() + try: + connection_throttling = client.configurations.get( + resouce_group_name, server_name, "connection_throttle.enable" + ) + return connection_throttling.value.upper() + except Exception: + # The "connection_throttle.enable" server parameter was removed in + # PostgreSQL 16+, so it no longer exists on newer flexible servers. + # Treat its absence as "not enabled" rather than failing collection. + return None def _get_log_retention_days(self, subscription, resouce_group_name, server_name): client = self.clients[subscription] diff --git a/tests/providers/azure/services/postgresql/postgresql_service_test.py b/tests/providers/azure/services/postgresql/postgresql_service_test.py index f36b5675ecd..8746ccc7412 100644 --- a/tests/providers/azure/services/postgresql/postgresql_service_test.py +++ b/tests/providers/azure/services/postgresql/postgresql_service_test.py @@ -1,4 +1,4 @@ -from unittest.mock import patch +from unittest.mock import MagicMock, patch from prowler.providers.azure.services.postgresql.postgresql_service import ( EntraIdAdmin, @@ -161,3 +161,89 @@ def test_get_firewall(self): postgesql.flexible_servers[AZURE_SUBSCRIPTION_ID][0].firewall[0].end_ip == "end_ip" ) + + +def _make_server(name): + server = MagicMock() + server.id = ( + f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/rg/providers/" + f"Microsoft.DBforPostgreSQL/flexibleServers/{name}" + ) + server.name = name + return server + + +class Test_PostgreSQL_Service_Resilience: + """Collecting one flexible server must never abort collection of the rest of + the subscription (regression: a missing/failing per-server configuration + lookup silently dropped every remaining server).""" + + def _build_service_with_client(self, mock_client): + # Skip the real network call during construction, then run the real + # collection against the mocked management client. + with patch.object(PostgreSQL, "_get_flexible_servers", return_value={}): + postgresql = PostgreSQL(set_mocked_azure_provider()) + postgresql.clients = {AZURE_SUBSCRIPTION_ID: mock_client} + return postgresql + + def test_missing_connection_throttle_config_still_collects_server(self): + # The "connection_throttle.enable" parameter was removed in PostgreSQL + # 16+, so the lookup raises ConfigurationNotExists on newer servers. + dev = _make_server("dev") + prd = _make_server("prd") + + mock_client = MagicMock() + mock_client.servers.list.return_value = [dev, prd] + server_details = MagicMock() + server_details.location = "westeurope" + mock_client.servers.get.return_value = server_details + mock_client.administrators.list_by_server.return_value = [] + mock_client.firewall_rules.list_by_server.return_value = [] + + def configurations_get(resource_group, server_name, key): + if key == "connection_throttle.enable" and server_name == "prd": + raise Exception( + "(ConfigurationNotExists) The configuration " + "'connection_throttle.enable' does not exist for prd server " + "version 18." + ) + return MagicMock(value="ON") + + mock_client.configurations.get.side_effect = configurations_get + + postgresql = self._build_service_with_client(mock_client) + servers = postgresql._get_flexible_servers() + + names = sorted(server.name for server in servers[AZURE_SUBSCRIPTION_ID]) + assert names == ["dev", "prd"] + prd_server = next(s for s in servers[AZURE_SUBSCRIPTION_ID] if s.name == "prd") + assert prd_server.connection_throttling is None + dev_server = next(s for s in servers[AZURE_SUBSCRIPTION_ID] if s.name == "dev") + assert dev_server.connection_throttling == "ON" + + def test_one_server_hard_failure_does_not_drop_others(self): + # A failure unrelated to a guarded getter (here, fetching the server + # details) must isolate to that server, not the whole subscription. + ok = _make_server("ok") + broken = _make_server("broken") + + mock_client = MagicMock() + mock_client.servers.list.return_value = [broken, ok] + mock_client.administrators.list_by_server.return_value = [] + mock_client.firewall_rules.list_by_server.return_value = [] + mock_client.configurations.get.return_value = MagicMock(value="ON") + + def servers_get(resource_group, server_name): + if server_name == "broken": + raise Exception("boom: transient failure fetching server details") + details = MagicMock() + details.location = "westeurope" + return details + + mock_client.servers.get.side_effect = servers_get + + postgresql = self._build_service_with_client(mock_client) + servers = postgresql._get_flexible_servers() + + names = [server.name for server in servers[AZURE_SUBSCRIPTION_ID]] + assert names == ["ok"] From 4ead3a6521cd8c6e4915008536a3dfc4d6096894 Mon Sep 17 00:00:00 2001 From: Vahid Gharavi Date: Mon, 15 Jun 2026 15:26:44 +0200 Subject: [PATCH 2/5] docs(changelog): add entry for azure/postgresql collection fix (#11595) --- prowler/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index eb81a583788..42b6f525014 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -35,6 +35,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### 🐞 Fixed +- Azure PostgreSQL flexible server collection no longer drops the remaining servers in a subscription when one server fails to collect; the `connection_throttle.enable` parameter (removed in PostgreSQL 16+) is now treated as absent instead of aborting the subscription [(#11595)](https://github.com/prowler-cloud/prowler/pull/11595) - `load_and_validate_config_file` now unwraps namespaced config for every built-in and external provider, and no longer leaks the full file as the provider's config when the file is namespaced [(#10700)](https://github.com/prowler-cloud/prowler/pull/10700) - `entra_users_mfa_capable` no longer flags pre-provisioned users with future `employeeHireDate`; future-hire date comparisons now tolerate naive datetimes [(#11511)](https://github.com/prowler-cloud/prowler/pull/11511) - M365 Admin Center group enumeration now follows Microsoft Graph pagination so group-scoped checks include groups beyond the first page [(#11510)](https://github.com/prowler-cloud/prowler/pull/11510) From 636050aea1901e979ab26a46c103c77d94667ad2 Mon Sep 17 00:00:00 2001 From: Vahid Gharavi Date: Mon, 15 Jun 2026 15:53:43 +0200 Subject: [PATCH 3/5] fix(azure/postgresql): type connection_throttling as str | None Reflect the None fallback from _get_connection_throttling in the Server model and the method return annotation (per review feedback). --- .../azure/services/postgresql/postgresql_service.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/prowler/providers/azure/services/postgresql/postgresql_service.py b/prowler/providers/azure/services/postgresql/postgresql_service.py index 7034e977195..7c4a4ddc0e9 100644 --- a/prowler/providers/azure/services/postgresql/postgresql_service.py +++ b/prowler/providers/azure/services/postgresql/postgresql_service.py @@ -160,7 +160,9 @@ def _get_entra_id_admins(self, subscription, resource_group_name, server_name): logger.error(f"Error getting Entra ID admins for {server_name}: {e}") return [] - def _get_connection_throttling(self, subscription, resouce_group_name, server_name): + def _get_connection_throttling( + self, subscription, resouce_group_name, server_name + ) -> str | None: client = self.clients[subscription] try: connection_throttling = client.configurations.get( @@ -228,6 +230,6 @@ class Server: log_checkpoints: str log_connections: str log_disconnections: str - connection_throttling: str + connection_throttling: str | None log_retention_days: str firewall: list[Firewall] From 0a7fb81b46cbce2f26120b6768fdd929bff2a7ee Mon Sep 17 00:00:00 2001 From: Vahid Gharavi Date: Fri, 26 Jun 2026 14:35:18 +0200 Subject: [PATCH 4/5] fix(azure/postgresql): narrow connection-throttling fallback to ResourceNotFoundError Only treat the connection_throttle.enable parameter as absent (PostgreSQL 16+) when the Azure SDK reports it as not found. Permission, throttling, or transient SDK errors now propagate to the per-server handler instead of being silently reported as throttling disabled, which masked a collection failure as a security finding. Adds a regression test asserting an unexpected lookup failure is not converted to connection_throttling=None, and adds type hints + a docstring to _get_connection_throttling. --- prowler/CHANGELOG.md | 2 +- .../services/postgresql/postgresql_service.py | 35 +++++++++++++--- .../postgresql/postgresql_service_test.py | 41 ++++++++++++++++++- 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 42b6f525014..5cc09e4b75f 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -35,7 +35,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### 🐞 Fixed -- Azure PostgreSQL flexible server collection no longer drops the remaining servers in a subscription when one server fails to collect; the `connection_throttle.enable` parameter (removed in PostgreSQL 16+) is now treated as absent instead of aborting the subscription [(#11595)](https://github.com/prowler-cloud/prowler/pull/11595) +- Azure PostgreSQL flexible server collection no longer drops the remaining servers in a subscription when one server fails to collect; the `connection_throttle.enable` parameter (removed in PostgreSQL 16+) is treated as absent only when the Azure SDK reports it as not found, so unexpected lookup failures are not silently reported as throttling disabled [(#11595)](https://github.com/prowler-cloud/prowler/pull/11595) - `load_and_validate_config_file` now unwraps namespaced config for every built-in and external provider, and no longer leaks the full file as the provider's config when the file is namespaced [(#10700)](https://github.com/prowler-cloud/prowler/pull/10700) - `entra_users_mfa_capable` no longer flags pre-provisioned users with future `employeeHireDate`; future-hire date comparisons now tolerate naive datetimes [(#11511)](https://github.com/prowler-cloud/prowler/pull/11511) - M365 Admin Center group enumeration now follows Microsoft Graph pagination so group-scoped checks include groups beyond the first page [(#11510)](https://github.com/prowler-cloud/prowler/pull/11510) diff --git a/prowler/providers/azure/services/postgresql/postgresql_service.py b/prowler/providers/azure/services/postgresql/postgresql_service.py index 7c4a4ddc0e9..fddd9a3303c 100644 --- a/prowler/providers/azure/services/postgresql/postgresql_service.py +++ b/prowler/providers/azure/services/postgresql/postgresql_service.py @@ -1,5 +1,6 @@ from dataclasses import dataclass +from azure.core.exceptions import ResourceNotFoundError from azure.mgmt.postgresqlflexibleservers import PostgreSQLManagementClient from prowler.lib.logger import logger @@ -161,18 +162,42 @@ def _get_entra_id_admins(self, subscription, resource_group_name, server_name): return [] def _get_connection_throttling( - self, subscription, resouce_group_name, server_name + self, subscription: str, resouce_group_name: str, server_name: str ) -> str | None: + """Get the ``connection_throttle.enable`` setting for a flexible server. + + The ``connection_throttle.enable`` server parameter was removed in + PostgreSQL 16+, so it no longer exists on newer flexible servers. When + the parameter is genuinely absent the Azure SDK raises + ``ResourceNotFoundError`` (error code ``ConfigurationNotExists``); that + case is treated as "not enabled" and ``None`` is returned so collection + of the server can continue. + + Any other error (permissions, throttling, transient SDK failures) is + intentionally left to propagate: returning ``None`` for those would make + the downstream check report the server as having connection throttling + disabled, silently turning a collection failure into a security finding. + + Args: + subscription: Azure subscription identifier. + resouce_group_name: Resource group containing the server. + server_name: PostgreSQL flexible server name. + + Returns: + The uppercased throttling value, or ``None`` when the parameter does + not exist on the server. + + Raises: + ResourceNotFoundError is handled; any other exception propagates to + the caller so it can be surfaced as a collection failure. + """ client = self.clients[subscription] try: connection_throttling = client.configurations.get( resouce_group_name, server_name, "connection_throttle.enable" ) return connection_throttling.value.upper() - except Exception: - # The "connection_throttle.enable" server parameter was removed in - # PostgreSQL 16+, so it no longer exists on newer flexible servers. - # Treat its absence as "not enabled" rather than failing collection. + except ResourceNotFoundError: return None def _get_log_retention_days(self, subscription, resouce_group_name, server_name): diff --git a/tests/providers/azure/services/postgresql/postgresql_service_test.py b/tests/providers/azure/services/postgresql/postgresql_service_test.py index 8746ccc7412..5f9b7412c27 100644 --- a/tests/providers/azure/services/postgresql/postgresql_service_test.py +++ b/tests/providers/azure/services/postgresql/postgresql_service_test.py @@ -1,5 +1,7 @@ from unittest.mock import MagicMock, patch +from azure.core.exceptions import HttpResponseError, ResourceNotFoundError + from prowler.providers.azure.services.postgresql.postgresql_service import ( EntraIdAdmin, Firewall, @@ -202,7 +204,9 @@ def test_missing_connection_throttle_config_still_collects_server(self): def configurations_get(resource_group, server_name, key): if key == "connection_throttle.enable" and server_name == "prd": - raise Exception( + # Azure raises ResourceNotFoundError (ConfigurationNotExists) + # when the parameter does not exist on the server. + raise ResourceNotFoundError( "(ConfigurationNotExists) The configuration " "'connection_throttle.enable' does not exist for prd server " "version 18." @@ -221,6 +225,41 @@ def configurations_get(resource_group, server_name, key): dev_server = next(s for s in servers[AZURE_SUBSCRIPTION_ID] if s.name == "dev") assert dev_server.connection_throttling == "ON" + def test_unexpected_throttling_error_is_not_silently_collected(self): + # An unexpected failure reading "connection_throttle.enable" (e.g. a + # permission, throttling, or transient SDK error) must NOT be turned + # into connection_throttling=None: that would make the downstream check + # report the server as having throttling disabled, hiding a collection + # failure as a security finding. Only ResourceNotFoundError (the + # parameter genuinely missing) is treated as "not enabled"; anything + # else isolates to that server, which is dropped rather than fabricated. + ok = _make_server("ok") + denied = _make_server("denied") + + mock_client = MagicMock() + mock_client.servers.list.return_value = [ok, denied] + server_details = MagicMock() + server_details.location = "westeurope" + mock_client.servers.get.return_value = server_details + mock_client.administrators.list_by_server.return_value = [] + mock_client.firewall_rules.list_by_server.return_value = [] + + def configurations_get(resource_group, server_name, key): + if key == "connection_throttle.enable" and server_name == "denied": + raise HttpResponseError("(AuthorizationFailed) permission denied") + return MagicMock(value="ON") + + mock_client.configurations.get.side_effect = configurations_get + + postgresql = self._build_service_with_client(mock_client) + servers = postgresql._get_flexible_servers() + + collected = servers[AZURE_SUBSCRIPTION_ID] + # The server whose throttling lookup failed unexpectedly is dropped, + # not collected with a fabricated connection_throttling=None. + assert [server.name for server in collected] == ["ok"] + assert all(server.connection_throttling is not None for server in collected) + def test_one_server_hard_failure_does_not_drop_others(self): # A failure unrelated to a guarded getter (here, fetching the server # details) must isolate to that server, not the whole subscription. From b85478ee8ceb0cac2b15c5b33f58196264ba17bd Mon Sep 17 00:00:00 2001 From: "Hugo P.Brito" Date: Fri, 26 Jun 2026 15:12:02 +0100 Subject: [PATCH 5/5] chore: prepare for release --- prowler/CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 4c8db0f4ec5..bddc07792c7 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -13,6 +13,10 @@ All notable changes to the **Prowler SDK** are documented in this file. - CIS Microsoft 365 Foundations Benchmark v7.0.0 compliance framework for the M365 provider [(#11699)](https://github.com/prowler-cloud/prowler/pull/11699) - `waf_regional_webacl_logging_enabled` check for AWS provider, verifying that each AWS WAF Classic Regional Web ACL has logging enabled to a Kinesis Data Firehose stream [(#11539)](https://github.com/prowler-cloud/prowler/pull/11539) +### 🐞 Fixed + +- Azure PostgreSQL flexible server collection no longer drops the remaining servers in a subscription when one server fails to collect; the `connection_throttle.enable` parameter (removed in PostgreSQL 16+) is treated as absent only when the Azure SDK reports it as not found, so unexpected lookup failures are not silently reported as throttling disabled [(#11595)](https://github.com/prowler-cloud/prowler/pull/11595) + --- ## [5.31.1] (Prowler v5.31.1) @@ -136,7 +140,6 @@ All notable changes to the **Prowler SDK** are documented in this file. ### 🐞 Fixed -- Azure PostgreSQL flexible server collection no longer drops the remaining servers in a subscription when one server fails to collect; the `connection_throttle.enable` parameter (removed in PostgreSQL 16+) is treated as absent only when the Azure SDK reports it as not found, so unexpected lookup failures are not silently reported as throttling disabled [(#11595)](https://github.com/prowler-cloud/prowler/pull/11595) - `load_and_validate_config_file` now unwraps namespaced config for every built-in and external provider, and no longer leaks the full file as the provider's config when the file is namespaced [(#10700)](https://github.com/prowler-cloud/prowler/pull/10700) - `entra_users_mfa_capable` no longer flags pre-provisioned users with future `employeeHireDate`; future-hire date comparisons now tolerate naive datetimes [(#11511)](https://github.com/prowler-cloud/prowler/pull/11511) - M365 Admin Center group enumeration now follows Microsoft Graph pagination so group-scoped checks include groups beyond the first page [(#11510)](https://github.com/prowler-cloud/prowler/pull/11510)