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:
StylusFrost committed 2026-06-02 12:44:26 +02:00
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 = []
+1
View File
@@ -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
+26
View File
@@ -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.
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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