From cf70d1f9f84321735f5f284f536d68f65a61d2f3 Mon Sep 17 00:00:00 2001 From: StylusFrost Date: Fri, 24 Apr 2026 18:51:57 +0200 Subject: [PATCH] fix(sdk): honor from_cli_args return value in init_global_provider fallback --- prowler/providers/common/provider.py | 21 ++++++- .../external/test_dynamic_provider_loading.py | 60 +++++++++++++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index eeea0d2e5a..bd60063524 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -146,7 +146,13 @@ class Provider(ABC): @classmethod def from_cli_args(cls, arguments: Namespace, fixer_config: dict) -> "Provider": - """Instantiate the provider from CLI arguments.""" + """Instantiate the provider from CLI arguments and return the instance. + + The caller wires the returned instance into the global provider slot + via Provider.set_global_provider(). Implementations that already call + set_global_provider(self) from __init__ are also supported — the call + site tolerates a None return in that case. + """ raise NotImplementedError(f"{cls.__name__} has not implemented from_cli_args()") def get_output_options(self, arguments, bulk_checks_metadata): @@ -507,8 +513,17 @@ class Provider(ABC): fixer_config=fixer_config, ) else: - # Dynamic fallback: any external/custom provider - provider_class.from_cli_args(arguments, fixer_config) + # Dynamic fallback: any external/custom provider. + # Honor the from_cli_args type hint (-> Provider): if the + # implementation returns an instance, wire it as the global + # provider here. Implementations that call + # set_global_provider(self) from __init__ return None and + # remain supported (the condition below is a no-op for them). + provider_instance = provider_class.from_cli_args( + arguments, fixer_config + ) + if provider_instance is not None: + Provider.set_global_provider(provider_instance) except TypeError as error: logger.critical( diff --git a/tests/providers/external/test_dynamic_provider_loading.py b/tests/providers/external/test_dynamic_provider_loading.py index 810c9053ef..ca4542e9fd 100644 --- a/tests/providers/external/test_dynamic_provider_loading.py +++ b/tests/providers/external/test_dynamic_provider_loading.py @@ -145,6 +145,41 @@ class FakeToolWrapperProvider(Provider): pass +class FakePureContractProvider(Provider): + """External provider that honors the from_cli_args type hint literally: + returns an instance without calling Provider.set_global_provider() from + __init__. Used to verify the call site wires the returned instance.""" + + _type = "fakepure" + + @property + def type(self): + return self._type + + @property + def identity(self): + return MagicMock(host_id="fake-pure-1") + + @property + def session(self): + return MagicMock() + + @property + def audit_config(self): + return {} + + def setup_session(self): + return MagicMock() + + def print_credentials(self): + pass + + @classmethod + def from_cli_args(cls, arguments, fixer_config): + # Literal contract: return the instance, no side-effect in __init__. + return cls() + + class FakeProviderNoHelpText(Provider): """Provider without _cli_help_text.""" @@ -439,6 +474,31 @@ class TestProviderInitialization: with pytest.raises(SystemExit): Provider.init_global_provider(args) + @patch("prowler.providers.common.provider.load_and_validate_config_file") + @patch("prowler.providers.common.provider.Provider._load_ep_provider") + @patch("prowler.providers.common.provider.import_module") + def test_init_global_provider_wires_instance_returned_by_from_cli_args( + self, mock_import, mock_load_ep, mock_config + ): + """A provider that implements from_cli_args as a pure function (returns + the instance without calling set_global_provider from __init__) is + correctly wired as the global provider by init_global_provider.""" + mock_import.side_effect = ImportError("No built-in") + mock_load_ep.return_value = FakePureContractProvider + mock_config.return_value = {} + + args = Namespace( + provider="fakepure", + fixer_config="config.yaml", + config_file="config.yaml", + ) + + Provider._global = None + Provider.init_global_provider(args) + + assert isinstance(Provider._global, FakePureContractProvider) + Provider._global = None + # =========================================================================== # 3. Check Discovery