fix(provider): move get_output_options default to call site

Resolve the provider<->models import cycle CodeQL flagged (py/cyclic-import
#7267, #7268). provider.py no longer imports models: get_output_options
stays override-only and __main__ falls back to a new default_output_options
helper in models.py when a provider does not implement it. models.py keeps
its original module-level Provider import (one-way, no cycle).
This commit is contained in:
StylusFrost
2026-06-05 14:21:49 +02:00
parent 356e6e2bb4
commit 29825f9a2f
4 changed files with 54 additions and 23 deletions
+12 -1
View File
@@ -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:
+15 -3
View File
@@ -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
+6 -10
View File
@@ -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.)."""
+21 -9
View File
@@ -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"