mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-22 13:51:33 +00:00
refactor(sdk): extract is_builtin_provider to leaf module to break import cycle
CodeQL flagged a cyclic import after `prowler/lib/check/utils.py` and `prowler/lib/check/check.py` started importing `Provider` from `prowler.providers.common.provider`. That module transitively imports `prowler.config.config`, which imports back into `prowler.lib.check.*` (`compliance_models`, `external_tool_providers`) — closing the cycle. Apply the same pattern already used for `is_tool_wrapper_provider`: extract the predicate to a leaf module, `prowler.providers.common.builtin`, that depends only on `importlib.util`. `Provider.is_builtin` delegates to the leaf, and call sites in `prowler.lib.check.*` now import directly from the leaf — no more cycle. Also underscore-prefix unused parameters on the abstract stubs in `Provider` (get_finding_output_data, generate_compliance_output, display_compliance_table) so vulture stops flagging them now that the file is in the diff.
This commit is contained in:
@@ -21,6 +21,7 @@ from prowler.lib.check.utils import recover_checks_from_provider
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.outputs import report
|
||||
from prowler.lib.utils.utils import open_file, parse_json_file, print_boxes
|
||||
from prowler.providers.common.builtin import is_builtin_provider
|
||||
from prowler.providers.common.models import Audit_Metadata
|
||||
|
||||
|
||||
@@ -400,20 +401,18 @@ def _resolve_check_module(
|
||||
when a plug-in tries to override, so the user knows their plug-in
|
||||
duplicate is being ignored and can rename it.
|
||||
|
||||
Gates the built-in branch on `Provider.is_builtin(provider_type)` —
|
||||
Gates the built-in branch on `is_builtin_provider(provider_type)` —
|
||||
calling `find_spec` on `prowler.providers.{provider_type}.services...`
|
||||
directly would propagate `ModuleNotFoundError` for external providers
|
||||
(their parent package `prowler.providers.{provider_type}` does not
|
||||
exist) instead of returning None. `Provider.is_builtin` encapsulates
|
||||
the safe lookup, so external providers go straight to entry points.
|
||||
For built-ins we still use `find_spec` to distinguish "check doesn't
|
||||
exist) instead of returning None. The leaf helper encapsulates the
|
||||
safe lookup, so external providers go straight to entry points. For
|
||||
built-ins we still use `find_spec` to distinguish "check doesn't
|
||||
exist" from "check exists but failed to import" (broken transitive
|
||||
dep, etc.).
|
||||
"""
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
# Built-in first — built-in wins on CheckID collision
|
||||
if Provider.is_builtin(provider_type):
|
||||
if is_builtin_provider(provider_type):
|
||||
builtin_path = f"prowler.providers.{provider_type}.services.{service}.{check_name}.{check_name}"
|
||||
if importlib.util.find_spec(builtin_path) is not None:
|
||||
return import_check(builtin_path)
|
||||
|
||||
@@ -7,6 +7,7 @@ from pkgutil import walk_packages
|
||||
|
||||
from prowler.lib.check.tool_wrapper import is_tool_wrapper_provider
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.common.builtin import is_builtin_provider
|
||||
|
||||
|
||||
def _recover_ep_checks(provider: str, service: str = None) -> list[tuple]:
|
||||
@@ -58,17 +59,15 @@ def recover_checks_from_provider(
|
||||
|
||||
checks = []
|
||||
# Built-in checks from prowler.providers.{provider}.services. Gate
|
||||
# the built-in branch on `Provider.is_builtin(provider)` — calling
|
||||
# the built-in branch on `is_builtin_provider(provider)` — calling
|
||||
# `find_spec` directly on `prowler.providers.{provider}.services`
|
||||
# would propagate `ModuleNotFoundError` when the parent package
|
||||
# `prowler.providers.{provider}` does not exist (i.e. the provider
|
||||
# is external), instead of returning None. `Provider.is_builtin`
|
||||
# is external), instead of returning None. The leaf helper
|
||||
# encapsulates the safe lookup, so we only run the built-in
|
||||
# discovery when the provider actually ships with the SDK; for
|
||||
# external providers we go straight to entry points.
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
if Provider.is_builtin(provider):
|
||||
if is_builtin_provider(provider):
|
||||
modules = list_modules(provider, service)
|
||||
for module_name in modules:
|
||||
# Format: "prowler.providers.{provider}.services.{service}.{check_name}.{check_name}"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Leaf helper for built-in provider detection.
|
||||
|
||||
Lives in its own module — with no imports back into `prowler.lib.check` — so
|
||||
that callers in `prowler.lib.check.*` can ask "is this provider built-in?"
|
||||
without creating an import cycle through `prowler.providers.common.provider`
|
||||
(which transitively imports `prowler.config.config` and from there
|
||||
`prowler.lib.check.compliance_models` / `prowler.lib.check.external_tool_providers`).
|
||||
|
||||
Same rationale as `prowler.lib.check.tool_wrapper`: extracting the predicate
|
||||
to a leaf module is the canonical way to break the cycle in this codebase.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
|
||||
|
||||
def is_builtin_provider(provider: str) -> bool:
|
||||
"""Return True if the provider's own package ships with the SDK.
|
||||
|
||||
Wraps `importlib.util.find_spec` in `try/except (ImportError, ValueError)`
|
||||
because `find_spec` propagates `ModuleNotFoundError` when a parent package
|
||||
in the dotted path does not exist (instead of returning `None`). The
|
||||
try/except is what makes the call safe for external providers, whose
|
||||
package does not live under `prowler.providers.{provider}`.
|
||||
"""
|
||||
try:
|
||||
spec = importlib.util.find_spec(f"prowler.providers.{provider}")
|
||||
return spec is not None
|
||||
except (ImportError, ValueError):
|
||||
return False
|
||||
@@ -175,7 +175,7 @@ class Provider(ABC):
|
||||
f"{self.__class__.__name__} has not implemented get_summary_entity()"
|
||||
)
|
||||
|
||||
def get_finding_output_data(self, check_output) -> dict:
|
||||
def get_finding_output_data(self, _check_output) -> dict:
|
||||
"""Return provider-specific fields for Finding.generate_output()."""
|
||||
raise NotImplementedError(
|
||||
f"{self.__class__.__name__} has not implemented get_finding_output_data()"
|
||||
@@ -191,9 +191,9 @@ class Provider(ABC):
|
||||
self,
|
||||
findings,
|
||||
bulk_compliance_frameworks,
|
||||
input_compliance_frameworks,
|
||||
_input_compliance_frameworks,
|
||||
output_options,
|
||||
generated_outputs,
|
||||
_generated_outputs,
|
||||
) -> None:
|
||||
"""Generate compliance CSV output for this provider's frameworks."""
|
||||
raise NotImplementedError(
|
||||
@@ -216,9 +216,9 @@ class Provider(ABC):
|
||||
findings: list,
|
||||
bulk_checks_metadata: dict,
|
||||
compliance_framework: str,
|
||||
output_filename: str,
|
||||
output_directory: str,
|
||||
compliance_overview: bool,
|
||||
_output_filename: str,
|
||||
_output_directory: str,
|
||||
_compliance_overview: bool,
|
||||
) -> bool:
|
||||
"""Render a custom compliance table in the terminal.
|
||||
|
||||
@@ -606,9 +606,7 @@ class Provider(ABC):
|
||||
the leaf module that holds the actual logic. Kept on `Provider` as a
|
||||
convenience entry point for callers that already import `Provider`.
|
||||
"""
|
||||
from prowler.lib.check.tool_wrapper import (
|
||||
is_tool_wrapper_provider as _impl,
|
||||
)
|
||||
from prowler.lib.check.tool_wrapper import is_tool_wrapper_provider as _impl
|
||||
|
||||
return _impl(provider)
|
||||
|
||||
@@ -616,18 +614,15 @@ class Provider(ABC):
|
||||
def is_builtin(provider: str) -> bool:
|
||||
"""Return True if the provider's own package is importable as a built-in.
|
||||
|
||||
Uses `importlib.util.find_spec` — Python's canonical API to check module
|
||||
existence without executing it. Discriminates at call sites between
|
||||
built-in providers (`prowler.providers.{provider}`) and externals, so we
|
||||
don't rely on catching `ImportError` after the fact and inspecting
|
||||
`e.name` — which is fragile when the error comes from a transitive
|
||||
dependency inside the built-in's own import chain.
|
||||
Delegates to `prowler.providers.common.builtin.is_builtin_provider`,
|
||||
the leaf module that holds the actual check. Kept on `Provider` as a
|
||||
convenience entry point for callers that already import `Provider`.
|
||||
Call sites in `prowler.lib.check.*` should import from the leaf
|
||||
directly to avoid the import cycle through this module.
|
||||
"""
|
||||
try:
|
||||
spec = importlib.util.find_spec(f"{providers_path}.{provider}")
|
||||
return spec is not None
|
||||
except (ImportError, ValueError):
|
||||
return False
|
||||
from prowler.providers.common.builtin import is_builtin_provider as _impl
|
||||
|
||||
return _impl(provider)
|
||||
|
||||
@staticmethod
|
||||
def _load_ep_provider(name: str):
|
||||
|
||||
Reference in New Issue
Block a user