diff --git a/prowler/lib/check/checks_loader.py b/prowler/lib/check/checks_loader.py index 5737c0b232..f1164645e5 100644 --- a/prowler/lib/check/checks_loader.py +++ b/prowler/lib/check/checks_loader.py @@ -2,10 +2,10 @@ import sys from colorama import Fore, Style -from prowler.config.config import EXTERNAL_TOOL_PROVIDERS from prowler.lib.check.check import parse_checks_from_file from prowler.lib.check.compliance_models import Compliance from prowler.lib.check.models import CheckMetadata, Severity +from prowler.lib.check.tool_wrapper import is_tool_wrapper_provider from prowler.lib.logger import logger @@ -25,8 +25,13 @@ def load_checks_to_execute( ) -> set: """Generate the list of checks to execute based on the cloud provider and the input arguments given""" try: - # Bypass check loading for providers that use external tools directly - if provider in EXTERNAL_TOOL_PROVIDERS: + # Bypass check loading for tool-wrapper providers — they delegate + # scanning to an external tool and have no checks to recover. + # Single source of truth across __main__, the CheckMetadata validators, + # check discovery and this loader, covering both built-in tool wrappers + # (iac/llm/image) and external plug-ins that declare + # `is_external_tool_provider = True` via the contract. + if is_tool_wrapper_provider(provider): return set() # Local subsets diff --git a/prowler/lib/check/tool_wrapper.py b/prowler/lib/check/tool_wrapper.py new file mode 100644 index 0000000000..f00afd35d6 --- /dev/null +++ b/prowler/lib/check/tool_wrapper.py @@ -0,0 +1,57 @@ +"""Standalone helper for tool-wrapper provider detection. + +A provider is a "tool wrapper" if it delegates scanning to an external tool +(Trivy, promptfoo, etc.) instead of running checks/services through the +standard Prowler engine. This module is the single source of truth for that +classification across the codebase. + +Kept as a leaf module with no Prowler imports beyond the leaf +`external_tool_providers` so it can be referenced from `prowler.lib.check.*` +and `prowler.providers.common.provider` without forming an import cycle. +""" + +import importlib.metadata + +from prowler.lib.check.external_tool_providers import EXTERNAL_TOOL_PROVIDERS + +# Module-level cache for entry-point classes consulted by this helper. +# Independent of `Provider._ep_providers` to keep this module leaf — the cost +# of a duplicate cache entry is negligible (one class object per external +# provider, loaded lazily on first lookup). +_ep_class_cache: dict = {} + + +def _load_ep_class(provider: str): + """Return the entry-point provider class for `provider`, or None. + + Caches the result in `_ep_class_cache`. Errors during entry-point loading + are swallowed (returning None) so a broken plug-in never crashes the + is-tool-wrapper check; it just falls through to "not a tool wrapper". + """ + if provider in _ep_class_cache: + return _ep_class_cache[provider] + for ep in importlib.metadata.entry_points(group="prowler.providers"): + if ep.name == provider: + try: + cls = ep.load() + except Exception: + cls = None + _ep_class_cache[provider] = cls + return cls + _ep_class_cache[provider] = None + return None + + +def is_tool_wrapper_provider(provider: str) -> bool: + """Return True if the provider delegates scanning to an external tool. + + Combines the built-in `EXTERNAL_TOOL_PROVIDERS` frozenset (fast path for + iac/llm/image) with the `is_external_tool_provider` class attribute of + external plug-ins registered via entry points. This is the single source + of truth consulted by `__main__`, the `CheckMetadata` validators, the + check-loading utilities, and the checks loader. + """ + if provider in EXTERNAL_TOOL_PROVIDERS: + return True + cls = _load_ep_class(provider) + return bool(cls and getattr(cls, "is_external_tool_provider", False)) diff --git a/prowler/lib/check/utils.py b/prowler/lib/check/utils.py index 45b53047ab..a22a93d892 100644 --- a/prowler/lib/check/utils.py +++ b/prowler/lib/check/utils.py @@ -4,7 +4,7 @@ import os import sys from pkgutil import walk_packages -from prowler.lib.check.external_tool_providers import EXTERNAL_TOOL_PROVIDERS +from prowler.lib.check.tool_wrapper import is_tool_wrapper_provider from prowler.lib.logger import logger @@ -52,9 +52,7 @@ def recover_checks_from_provider( # Single source of truth: combines the EXTERNAL_TOOL_PROVIDERS # frozenset (built-ins) with the per-provider `is_external_tool_provider` # class attribute (so external plug-ins opt in via the contract). - from prowler.providers.common.provider import Provider - - if Provider.is_tool_wrapper_provider(provider): + if is_tool_wrapper_provider(provider): return [] checks = [] @@ -124,12 +122,7 @@ def recover_checks_from_service(service_list: list, provider: str) -> set: try: # Bypass check loading for tool-wrapper providers — symmetric with # `recover_checks_from_provider` above, using the same source of truth. - # NOTE: master gated this on `provider in EXTERNAL_TOOL_PROVIDERS` - # (covering iac/llm/image). The PR temporarily narrowed it to `== "iac"`; - # restoring the full set via the helper. - from prowler.providers.common.provider import Provider - - if Provider.is_tool_wrapper_provider(provider): + if is_tool_wrapper_provider(provider): return set() checks = set() diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index 02b4f69940..b3d85a9766 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -9,10 +9,7 @@ from argparse import Namespace from importlib import import_module from typing import Any, Optional -from prowler.config.config import ( - EXTERNAL_TOOL_PROVIDERS, - load_and_validate_config_file, -) +from prowler.config.config import load_and_validate_config_file from prowler.lib.logger import logger from prowler.lib.mutelist.mutelist import Mutelist @@ -592,16 +589,15 @@ class Provider(ABC): def is_tool_wrapper_provider(provider: str) -> bool: """Return True if the provider delegates scanning to an external tool. - Combines the built-in EXTERNAL_TOOL_PROVIDERS frozenset (fast path for - iac/llm/image) with the `is_external_tool_provider` class attribute of - external plug-in providers registered via entry points. This is the - single source of truth consulted by the execution flow and the - CheckMetadata validators. + Delegates to `prowler.lib.check.tool_wrapper.is_tool_wrapper_provider`, + the leaf module that holds the actual logic. Kept on `Provider` as a + convenience entry point for callers that already import `Provider`. """ - if provider in EXTERNAL_TOOL_PROVIDERS: - return True - ep_cls = Provider._load_ep_provider(provider) - return bool(ep_cls and getattr(ep_cls, "is_external_tool_provider", False)) + from prowler.lib.check.tool_wrapper import ( + is_tool_wrapper_provider as _impl, + ) + + return _impl(provider) @staticmethod def is_builtin(provider: str) -> bool: