diff --git a/prowler/__main__.py b/prowler/__main__.py index f2637395c3..6cbaf575d9 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -438,7 +438,18 @@ def prowler(): ) else: # Dynamic fallback: any external/custom provider - output_options = global_provider.get_output_options(args, bulk_checks_metadata) + try: + output_options = global_provider.get_output_options( + args, bulk_checks_metadata + ) + except NotImplementedError: + # No provider-specific OutputOptions: use the generic default so the + # run still produces output instead of aborting. + from prowler.providers.common.models import default_output_options + + output_options = default_output_options( + global_provider, args, bulk_checks_metadata + ) # Run the quick inventory for the provider if available if hasattr(args, "quick_inventory") and args.quick_inventory: diff --git a/prowler/providers/common/models.py b/prowler/providers/common/models.py index b440718aff..120cc1a374 100644 --- a/prowler/providers/common/models.py +++ b/prowler/providers/common/models.py @@ -4,6 +4,9 @@ from os.path import isdir from pydantic.v1 import BaseModel +from prowler.config.config import output_file_timestamp +from prowler.providers.common.provider import Provider + # TODO: include this for all the providers class Audit_Metadata(BaseModel): @@ -39,9 +42,6 @@ class ProviderOutputOptions: # Shodan API Key if self.shodan_api_key: # TODO: revisit this logic - # Local import to avoid a module-level import cycle with provider.py - from prowler.providers.common.provider import Provider - provider = Provider.get_global_provider() updated_audit_config = Provider.update_provider_config( provider.audit_config, "shodan_api_key", self.shodan_api_key @@ -70,3 +70,15 @@ class Connection: is_connected: bool = False error: Exception = None + + +def default_output_options(provider, arguments, bulk_checks_metadata): + """Generic OutputOptions fallback for external providers that do not + implement get_output_options, so the run still produces output instead of + aborting. Honors arguments.output_filename and otherwise derives a name + from the provider type.""" + output_options = ProviderOutputOptions(arguments, bulk_checks_metadata) + output_options.output_filename = getattr(arguments, "output_filename", None) or ( + f"prowler-output-{provider.type}-{output_file_timestamp}" + ) + return output_options diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index ff25c4a0bc..8bc7567795 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -9,7 +9,7 @@ from argparse import Namespace from importlib import import_module from typing import Any, Optional -from prowler.config.config import load_and_validate_config_file, output_file_timestamp +from prowler.config.config import load_and_validate_config_file from prowler.lib.logger import logger from prowler.lib.mutelist.mutelist import Mutelist @@ -153,15 +153,11 @@ class Provider(ABC): """ raise NotImplementedError(f"{cls.__name__} has not implemented from_cli_args()") - def get_output_options(self, arguments, bulk_checks_metadata): - """Return a generic OutputOptions default; override for provider-specific output.""" - from prowler.providers.common.models import ProviderOutputOptions - - output_options = ProviderOutputOptions(arguments, bulk_checks_metadata) - output_options.output_filename = getattr( - arguments, "output_filename", None - ) or (f"prowler-output-{self.type}-{output_file_timestamp}") - return output_options + def get_output_options(self, arguments, _bulk_checks_metadata): + """Create the provider-specific OutputOptions.""" + raise NotImplementedError( + f"{self.__class__.__name__} has not implemented get_output_options()" + ) def get_stdout_detail(self, _finding) -> str: """Return the detail string for stdout reporting (region, location, etc.).""" diff --git a/tests/providers/external/test_dynamic_provider_loading.py b/tests/providers/external/test_dynamic_provider_loading.py index cc9c3f14c8..e60720e32e 100644 --- a/tests/providers/external/test_dynamic_provider_loading.py +++ b/tests/providers/external/test_dynamic_provider_loading.py @@ -1798,12 +1798,22 @@ class TestBaseContractDefaults: with pytest.raises(NotImplementedError): FakeProviderNoHelpText.from_cli_args(MagicMock(), {}) - def test_get_output_options_returns_generic_default(self): - """Base Provider.get_output_options returns a generic ProviderOutputOptions - so an external provider that does not override it still produces output - instead of aborting the run with NotImplementedError.""" + def test_get_output_options_raises_not_implemented(self): + """Base Provider.get_output_options raises NotImplementedError; the + generic default is applied at the call site via default_output_options.""" + provider = FakeProviderNoHelpText() + with pytest.raises(NotImplementedError): + provider.get_output_options(MagicMock(), {}) + + def test_default_output_options_builds_generic_default(self): + """default_output_options returns a generic ProviderOutputOptions so an + external provider without get_output_options still produces output + instead of aborting the run.""" from prowler.config.config import output_file_timestamp - from prowler.providers.common.models import ProviderOutputOptions + from prowler.providers.common.models import ( + ProviderOutputOptions, + default_output_options, + ) provider = FakeProviderNoHelpText() arguments = Namespace( @@ -1818,7 +1828,7 @@ class TestBaseContractDefaults: fixer=None, ) - output_options = provider.get_output_options(arguments, {}) + output_options = default_output_options(provider, arguments, {}) assert isinstance(output_options, ProviderOutputOptions) assert ( @@ -1826,8 +1836,10 @@ class TestBaseContractDefaults: == f"prowler-output-{provider.type}-{output_file_timestamp}" ) - def test_get_output_options_honors_explicit_filename(self): - """A user-supplied output_filename is preserved by the default.""" + def test_default_output_options_honors_explicit_filename(self): + """A user-supplied output_filename is preserved by default_output_options.""" + from prowler.providers.common.models import default_output_options + provider = FakeProviderNoHelpText() arguments = Namespace( status=None, @@ -1841,7 +1853,7 @@ class TestBaseContractDefaults: fixer=None, ) - output_options = provider.get_output_options(arguments, {}) + output_options = default_output_options(provider, arguments, {}) assert output_options.output_filename == "custom-name"