mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-17 17:04:17 +00:00
feat(sdk): add sdk_only provider property to hide providers from the app
- Add sdk_only contract property (default True), hidden from the app until opt-in - Add Provider.get_app_providers() returning providers with sdk_only = False - Mark app-exposed built-in providers as sdk_only = False
This commit is contained in:
1 parent
9a574b32e4
commit
9537bb5a8c
18 files changed
+120
No files matched your search
@@ -53,6 +53,7 @@ class AlibabacloudProvider(Provider):
|
||||
"""
|
||||
|
||||
_type: str = "alibabacloud"
|
||||
sdk_only: bool = False
|
||||
_identity: AlibabaCloudIdentityInfo
|
||||
_session: AlibabaCloudSession
|
||||
_audit_resources: list = []
|
||||
|
||||
@@ -90,6 +90,7 @@ class AwsProvider(Provider):
|
||||
"""
|
||||
|
||||
_type: str = "aws"
|
||||
sdk_only: bool = False
|
||||
_identity: AWSIdentityInfo
|
||||
_session: AWSSession
|
||||
_organizations_metadata: AWSOrganizationsInfo
|
||||
|
||||
@@ -97,6 +97,7 @@ class AzureProvider(Provider):
|
||||
"""
|
||||
|
||||
_type: str = "azure"
|
||||
sdk_only: bool = False
|
||||
_session: DefaultAzureCredential
|
||||
_identity: AzureIdentityInfo
|
||||
_audit_config: dict
|
||||
|
||||
@@ -46,6 +46,7 @@ class CloudflareProvider(Provider):
|
||||
"""Cloudflare provider."""
|
||||
|
||||
_type: str = "cloudflare"
|
||||
sdk_only: bool = False
|
||||
_session: CloudflareSession
|
||||
_identity: CloudflareIdentityInfo
|
||||
_audit_config: dict
|
||||
|
||||
@@ -142,6 +142,10 @@ class Provider(ABC):
|
||||
|
||||
_cli_help_text: str = ""
|
||||
|
||||
# CLI/SDK-only provider, hidden from the app (API/UI). Defaults True; a
|
||||
# provider opts into the app with ``sdk_only = False``. See get_app_providers().
|
||||
sdk_only: bool = True
|
||||
|
||||
@classmethod
|
||||
def from_cli_args(cls, arguments: Namespace, fixer_config: dict) -> "Provider":
|
||||
"""Instantiate the provider from CLI arguments and return the instance.
|
||||
@@ -678,6 +682,28 @@ class Provider(ABC):
|
||||
providers.add(ep.name)
|
||||
return sorted(providers)
|
||||
|
||||
@staticmethod
|
||||
def get_app_providers() -> list[str]:
|
||||
"""Return the providers the app (API/UI) may expose: those with
|
||||
``sdk_only = False``.
|
||||
|
||||
Counterpart of :meth:`get_available_providers`, which lists every
|
||||
provider for the CLI. A provider whose class cannot be imported is
|
||||
treated as ``sdk_only`` (excluded) so a broken plug-in never leaks in.
|
||||
"""
|
||||
app_providers = []
|
||||
for name in Provider.get_available_providers():
|
||||
try:
|
||||
provider_class = Provider.get_class(name)
|
||||
except Exception as error:
|
||||
logger.warning(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
continue
|
||||
if not getattr(provider_class, "sdk_only", True):
|
||||
app_providers.append(name)
|
||||
return app_providers
|
||||
|
||||
@staticmethod
|
||||
def is_tool_wrapper_provider(provider: str) -> bool:
|
||||
"""Return True if the provider delegates scanning to an external tool.
|
||||
|
||||
@@ -59,6 +59,7 @@ class GcpProvider(Provider):
|
||||
"""
|
||||
|
||||
_type: str = "gcp"
|
||||
sdk_only: bool = False
|
||||
_session: Credentials
|
||||
_project_ids: list
|
||||
_excluded_project_ids: list
|
||||
|
||||
@@ -91,6 +91,7 @@ class GithubProvider(Provider):
|
||||
"""
|
||||
|
||||
_type: str = "github"
|
||||
sdk_only: bool = False
|
||||
_auth_method: str = None
|
||||
MAX_REPO_LIST_LINES: int = 10_000
|
||||
MAX_REPO_NAME_LENGTH: int = 500
|
||||
|
||||
@@ -54,6 +54,7 @@ class GoogleworkspaceProvider(Provider):
|
||||
"""
|
||||
|
||||
_type: str = "googleworkspace"
|
||||
sdk_only: bool = False
|
||||
_session: GoogleWorkspaceSession
|
||||
_identity: GoogleWorkspaceIdentityInfo
|
||||
_domain_resource: GoogleWorkspaceResource
|
||||
|
||||
@@ -28,6 +28,7 @@ from prowler.providers.common.provider import Provider
|
||||
|
||||
class IacProvider(Provider):
|
||||
_type: str = "iac"
|
||||
sdk_only: bool = False
|
||||
audit_metadata: Audit_Metadata
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -59,6 +59,7 @@ class ImageProvider(Provider):
|
||||
"""
|
||||
|
||||
_type: str = "image"
|
||||
sdk_only: bool = False
|
||||
FINDING_BATCH_SIZE: int = 100
|
||||
MAX_IMAGE_LIST_LINES: int = 10_000
|
||||
MAX_IMAGE_NAME_LENGTH: int = 500
|
||||
|
||||
@@ -58,6 +58,7 @@ class KubernetesProvider(Provider):
|
||||
"""
|
||||
|
||||
_type: str = "kubernetes"
|
||||
sdk_only: bool = False
|
||||
_session: KubernetesSession
|
||||
_namespaces: list
|
||||
_audit_config: dict
|
||||
|
||||
@@ -99,6 +99,7 @@ class M365Provider(Provider):
|
||||
"""
|
||||
|
||||
_type: str = "m365"
|
||||
sdk_only: bool = False
|
||||
_session: DefaultAzureCredential # Must be used besides being named for Azure
|
||||
_identity: M365IdentityInfo
|
||||
_audit_config: dict
|
||||
|
||||
@@ -36,6 +36,7 @@ class MongodbatlasProvider(Provider):
|
||||
"""
|
||||
|
||||
_type: str = "mongodbatlas"
|
||||
sdk_only: bool = False
|
||||
_session: MongoDBAtlasSession
|
||||
_identity: MongoDBAtlasIdentityInfo
|
||||
_audit_config: dict
|
||||
|
||||
@@ -67,6 +67,7 @@ class OktaProvider(Provider):
|
||||
"""
|
||||
|
||||
_type: str = "okta"
|
||||
sdk_only: bool = False
|
||||
_auth_method: str = None
|
||||
_session: OktaSession
|
||||
_identity: OktaIdentityInfo
|
||||
|
||||
@@ -36,6 +36,7 @@ class OpenstackProvider(Provider):
|
||||
"""OpenStack provider responsible for bootstrapping the SDK session."""
|
||||
|
||||
_type: str = "openstack"
|
||||
sdk_only: bool = False
|
||||
_session: OpenStackSession
|
||||
_identity: OpenStackIdentityInfo
|
||||
_audit_config: dict
|
||||
|
||||
@@ -59,6 +59,7 @@ class OraclecloudProvider(Provider):
|
||||
"""
|
||||
|
||||
_type: str = "oraclecloud"
|
||||
sdk_only: bool = False
|
||||
_identity: OCIIdentityInfo
|
||||
_session: OCISession
|
||||
_audit_config: dict
|
||||
|
||||
@@ -33,6 +33,7 @@ class VercelProvider(Provider):
|
||||
"""Vercel provider."""
|
||||
|
||||
_type: str = "vercel"
|
||||
sdk_only: bool = False
|
||||
_session: VercelSession
|
||||
_identity: VercelIdentityInfo
|
||||
_audit_config: dict
|
||||
|
||||
@@ -344,6 +344,84 @@ class TestProviderDiscovery:
|
||||
assert help_text["nohelptext"] == ""
|
||||
|
||||
|
||||
class TestSdkOnly:
|
||||
"""The ``sdk_only`` flag (default True) and Provider.get_app_providers."""
|
||||
|
||||
def test_base_contract_defaults_to_sdk_only(self):
|
||||
# The default must be True so nothing leaks into the app implicitly.
|
||||
assert Provider.sdk_only is True
|
||||
|
||||
def test_external_provider_without_flag_is_sdk_only(self):
|
||||
# FakeExternalProvider does not override the flag -> inherits True.
|
||||
assert FakeExternalProvider.sdk_only is True
|
||||
|
||||
@patch("prowler.providers.common.provider.Provider.get_class")
|
||||
@patch("prowler.providers.common.provider.Provider.get_available_providers")
|
||||
def test_get_app_providers_filters_out_sdk_only(
|
||||
self, mock_available, mock_get_class
|
||||
):
|
||||
app_cls = type("AppProvider", (Provider,), {"sdk_only": False})
|
||||
sdk_cls = type("SdkProvider", (Provider,), {"sdk_only": True})
|
||||
mock_available.return_value = ["appone", "sdkone", "apptwo"]
|
||||
mock_get_class.side_effect = lambda name: {
|
||||
"appone": app_cls,
|
||||
"sdkone": sdk_cls,
|
||||
"apptwo": app_cls,
|
||||
}[name]
|
||||
|
||||
app_providers = Provider.get_app_providers()
|
||||
|
||||
assert app_providers == ["appone", "apptwo"]
|
||||
|
||||
@patch("prowler.providers.common.provider.Provider.get_class")
|
||||
@patch("prowler.providers.common.provider.Provider.get_available_providers")
|
||||
def test_get_app_providers_excludes_provider_that_fails_to_load(
|
||||
self, mock_available, mock_get_class
|
||||
):
|
||||
# A provider whose class cannot be imported is treated as sdk_only
|
||||
# (excluded) so a broken plug-in never leaks into the app.
|
||||
app_cls = type("AppProvider", (Provider,), {"sdk_only": False})
|
||||
mock_available.return_value = ["appone", "broken"]
|
||||
|
||||
def _get_class(name):
|
||||
if name == "broken":
|
||||
raise ImportError("missing transitive dep")
|
||||
return app_cls
|
||||
|
||||
mock_get_class.side_effect = _get_class
|
||||
|
||||
assert Provider.get_app_providers() == ["appone"]
|
||||
|
||||
def test_app_exposed_builtins_declare_sdk_only_false(self):
|
||||
# The providers implemented end-to-end in the API/UI must opt in.
|
||||
app_providers = set(Provider.get_app_providers())
|
||||
for name in (
|
||||
"aws",
|
||||
"azure",
|
||||
"gcp",
|
||||
"kubernetes",
|
||||
"m365",
|
||||
"github",
|
||||
"mongodbatlas",
|
||||
"iac",
|
||||
"oraclecloud",
|
||||
"alibabacloud",
|
||||
"cloudflare",
|
||||
"openstack",
|
||||
"image",
|
||||
"googleworkspace",
|
||||
"vercel",
|
||||
"okta",
|
||||
):
|
||||
assert name in app_providers, f"{name} should be exposed to the app"
|
||||
|
||||
def test_sdk_only_builtins_are_hidden_from_app(self):
|
||||
# Built-ins not implemented in the API stay SDK-only via the default.
|
||||
app_providers = set(Provider.get_app_providers())
|
||||
for name in ("llm", "nhn", "scaleway", "stackit"):
|
||||
assert name not in app_providers, f"{name} must be hidden from the app"
|
||||
|
||||
|
||||
class TestIsToolWrapperProvider:
|
||||
"""Tests for Provider.is_tool_wrapper_provider — the helper that combines the
|
||||
built-in EXTERNAL_TOOL_PROVIDERS frozenset with the is_external_tool_provider
|
||||
|
||||
Reference in new issue
Block a user