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:
StylusFrost
2026-05-03 22:46:00 +02:00
parent 0672c80563
commit bbe3a7dbf8
4 changed files with 54 additions and 32 deletions
+6 -7
View File
@@ -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)
+4 -5
View File
@@ -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}"
+29
View File
@@ -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
+15 -20
View File
@@ -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):