Skip to main content

Introduction

Providers form the backbone of Prowler, enabling security assessments across various cloud environments. A provider is any platform or service that offers resources, data, or functionality that can be audited for security and compliance. This includes:
  • Cloud Infrastructure Providers (like Amazon Web Services, Microsoft Azure, and Google Cloud)
  • Software as a Service (SaaS) Platforms (like Microsoft 365)
  • Development Platforms (like GitHub)
  • Container Orchestration Platforms (like Kubernetes)
  • Database-as-a-Service Platforms (like MongoDB Atlas)
For providers supported by Prowler, refer to Prowler Hub.
There are some custom providers added by the community, like NHN Cloud, that are not maintained by the Prowler team, but can be used in the Prowler CLI. The main purpose of this documentation is to guide you through creating a new provider and integrating it not only in the CLI, but also in the API and UI. Non official providers can be checked directly at the Prowler GitHub repository.

Provider Types in Prowler

Prowler supports several types of providers, each with its own implementation pattern and use case. Understanding these differences is key to designing your provider correctly.

Classifying your Provider

Before implementing a new provider, you need to determine which type it belongs to. This classification will guide your implementation approach and help you choose the right patterns and libraries.

Decision Criteria

Once you have decided the provider you want or need to add to Prowler, the next step is to study how to retrieve data from it. Based on that, the provider will fall into one of the following types: SDK, API or Tool/Wrapper (maybe in the future there will be new types but for now this are the only ones). Choose SDK Provider if:
  • The target platform/service has an official Python SDK available
  • The target platform/service has a non-official Python SDK available but it’s been updated and maintained
  • You need to support multiple authentication methods (profiles, service principals, IAM roles, etc.)
  • The SDK provides built-in session management, retry logic, and error handling
  • You want to leverage SDK-specific features like credential chaining, role assumption, etc
  • The platform is a major cloud provider (AWS, Azure, GCP, etc.) or has mature SDK support
Choose API Provider if:
  • The target platform has a REST API but no official Python SDK
  • The target platform has a non-official Python SDK available but it’s not updated and maintained
  • You need to implement custom authentication flows (OAuth, token-based, etc.)
  • The platform is a custom or community service without official SDK support
  • You want to use standard HTTP libraries like requests for API calls
  • The platform exposes well-documented REST endpoints but lacks SDK tooling
Choose Tool/Wrapper Provider if:
  • You’re integrating a third-party security tool or library
  • The tool provides scanning capabilities that need to be adapted to Prowler’s interface
  • You don’t need authentication or session management (the tool handles this)
  • You need to map tool arguments and convert outputs to Prowler’s format
Special Case - Hybrid Providers:
  • Some providers may combine multiple approaches (e.g., SDK + Tool wrapper, SDK + API, etc.)
  • Example: M365 uses msgraph SDK for authentication and some checks, and PowerShell wrapper for other checks that the SDK doesn’t support
  • These require custom implementation patterns that blend different provider types

Classification Examples

ProviderTypeReasoning
AWSSDKOfficial boto3 SDK, multiple auth methods, mature ecosystem
AzureSDKOfficial azure-identity SDK, service principals, managed identity
GCPSDKOfficial google-auth SDK, service accounts, ADC support
KubernetesSDKOfficial kubernetes SDK, service accounts, ADC support
NHN CloudAPICustom REST API, no official SDK, community provider
MongoDB AtlasAPICustom REST API, no official SDK
IACToolThird-party security tool that uses trivy, no auth needed, output conversion
M365HybridCombines msgraph SDK for auth + PowerShell wrapper for operations
GitHubHybridNon-Official PyGithub SDK but it’s been updated and maintained + Official graphql API requests

Questions to Ask Yourself

1. Does the platform have an official Python SDK?
  • Yes → Consider SDK Provider
  • No → Continue to question 2
2. Does the platform have a non-official Python SDK?
  • Yes → Then if the SDK is updated and maintained, consider SDK Provider, otherwise continue to question 3.
  • No → Continue to question 3
3. Is this a third-party security tool or library?
  • Yes → Consider Tool/Wrapper Provider
  • No → Continue to question 4
4. Does the platform expose a REST API?
  • Yes → Consider API Provider
  • No → You may need a custom approach
FlowChart Decision

Implementation Complexity

  • SDK Providers: Low complexity. You have mature examples like AWS, Azure, GCP, Kubernetes, etc. that you can leverage to implement your provider.
  • API Providers: Medium complexity. You need to implement the authentication and session management, and the API calls to the provider. You now have NHN and MongoDB Atlas as example to follow.
  • Tool/Wrapper Providers: High complexity. You need to implement the argument/output mapping to the provider and handle problems that the tool/wrapper may have. You now have IAC and the PowerShell wrapper as example to follow.
  • Hybrid Providers: High complexity. You need to “customize” your provider, mixing the other types of providers in order to achieve the desired result. You have M365 (msgraph SDK + PowerShell wrapper) and Github (PyGithub SDK + graphql API requests) as examples.

Determining Regional vs Non-Regional Architecture

After classifying your provider type, the next critical decision is determining whether your provider operates with regional concepts or is global/non-regional. This decision fundamentally affects how your provider and services are structured and executed.

Regional Providers

Regional providers operate across multiple geographic locations and require region-specific resource discovery and iteration. Examples:
  • AWS: Has regions like us-east-1, eu-west-1, ap-southeast-2
  • Azure: Has regions like East US, West Europe, Australia East
  • GCP: Has regions like us-central1, europe-west1, asia-southeast1
Implementation Requirements:
  • Must implement region discovery and iteration
  • Services must be instantiated per region or handle multi-region data
  • Checks must execute across all available/specified regions
  • Resource ARNs/IDs must include region information
  • Region-specific client initialization
Execution Pattern:
# Regional provider execution pattern
for region in provider.get_regions():
    regional_client = service.get_regional_client(region)
    regional_resources = regional_client.discover_resources()
    # Process regional resources

Non-Regional (Global) Providers

Non-regional providers operate globally without geographic partitioning. Examples:
  • GitHub: Repositories, organizations are global concepts
  • M365: Tenants operate globally across Microsoft datacenters
  • Kubernetes: Clusters are independent units without regional concepts
Implementation Requirements:
  • Single global client/session
  • No region iteration required
  • Global resource discovery
  • Simpler resource identification (no region in ARNs/IDs)
  • Single audit execution
Execution Pattern:
# Non-regional provider execution pattern
global_client = service.get_client()
global_resources = global_client.discover_resources()
# Process all resources in single iteration

Decision Matrix

AspectRegional ProviderNon-Regional Provider
Client InitPer-region clientsSingle global client
Resource DiscoveryIterate through regionsSingle discovery call
Resource ARN/IDInclude regionGlobal identifier/None
Audit ExecutionMulti-region loopsSingle execution
Service ArchitectureRegion-aware servicesGlobal services
PerformanceParallelizable by regionLinear execution

Region Discovery

Region discovery is the process of getting the list of regions that are available for the account. This is done by the provider and is stored in the prowler/providers/<provider_name>/lib/regions/<provider_name>_regions.py file.
# File: prowler/providers/aws/aws_provider.py
def get_aws_enabled_regions(self, current_session: Session) -> set:
    """get_aws_enabled_regions returns a set of enabled AWS regions"""
    try:
        # EC2 Client to check enabled regions
        service = "ec2"
        default_region = self.get_default_region(service)
        ec2_client = current_session.client(service, region_name=default_region)

        enabled_regions = set()
        # With AllRegions=False we only get the enabled regions for the account
        for region in ec2_client.describe_regions(AllRegions=False).get("Regions", []):
            enabled_regions.add(region.get("RegionName"))

        return enabled_regions
    except Exception as error:
        logger.error(f"{error.__class__.__name__}: {error}")
        return set()
The function returns a JSON file containing the list of regions for the provider. It is used to retrieve the provider’s regions and to validate the region specified by the user.
# File: prowler/providers/aws/aws_regions_by_service.json (extract)
{
  "services": {
    "ec2": {
      "regions": {
        "aws": [
          "af-south-1", "ap-east-1", "ap-northeast-1", "ap-northeast-2",
          "ap-northeast-3", "ap-south-1", "ap-southeast-1", "ap-southeast-2",
          "ca-central-1", "eu-central-1", "eu-north-1", "eu-south-1",
          "eu-west-1", "eu-west-2", "eu-west-3", "me-south-1",
          "sa-east-1", "us-east-1", "us-east-2", "us-west-1", "us-west-2"
        ],
        "aws-cn": ["cn-north-1", "cn-northwest-1"],
        "aws-us-gov": ["us-gov-east-1", "us-gov-west-1"]
      }
    }
  }
}

Regional Service Implementation

For detailed guidance on implementing services for regional services, including code examples, service architecture, and check execution patterns, see the Regional Service Implementation section in the Services documentation. Key concepts covered:
  • Threading and parallel processing across regions
  • Service implementation patterns for regional providers
  • Cross-region resource attribution and ARN handling
  • Best practices for performance and error isolation

Step 1: Create the Provider Backend (CLI Integration)

Once the type of provider and its regional architecture are determined, the next step is to start creating the code of the provider.

SDK Providers

General aspects to consider when implementing a new SDK provider: Definition:
  • Use the official SDK of the provider to interact with its resources and APIs.
  • Examples: AWS (boto3), Azure (azure-identity), GCP (google-auth), Kubernetes (kubernetes), M365 (msal/msgraph), GitHub (PyGithub).
Typical Use Cases:
  • Cloud platforms and services with mature Python SDKs.
  • Need to support multiple authentication methods (profiles, service principals, etc).
  • Providers that offer comprehensive Python libraries for resource management.
Key Characteristics:
  • Authentication and session management handled by the SDK.
  • Arguments: Depends on the provider, but for example we can have profile, region, tenant_id, client_id, client_secret, etc.
  • Outputs: Standardized via SDK models and responses.
  • Session objects that can be reused across multiple API calls.
  • Built-in retry logic and error handling.
Implementation Details:
  • SDK providers typically use credential objects or session objects provided by the official SDK.
  • They often support multiple authentication methods (several types of credentials, configuration files, IAM roles, etc.).
  • Session management includes token refresh, connection pooling, and retry mechanisms.
  • Resource discovery and enumeration is usually straightforward through SDK methods.

Implementation Guide for SDK Providers

Now it’s time to start creating the code needed to implement the provider.

Step 1: Create the Provider Structure

Explanation: SDK providers require a specific folder structure to organize authentication, configuration, and service management. This structure follows Prowler’s conventions and ensures proper integration with the CLI and API. Required Structure:
prowler/providers/<provider_name>/
├── __init__.py
├── <provider_name>_provider.py
├── models.py
├── exceptions/
│   ├── __init__.py
│   └── exceptions.py
├── services/
│   ├── service_name1/
│   └── service_name2/
└── lib/
    ├── __init__.py
    ├── arguments/
    │   ├── __init__.py
    │   └── arguments.py
    ├── mutelist/
    │   ├── __init__.py
    │   └── mutelist.py
    ├── regions/
    │   ├── __init__.py
    │   └── <provider_name>_regions.py
    └── service/
        ├── __init__.py
        └── service.py
Key Components:
  • <provider_name>_provider.py: Main provider class with authentication and session management
  • models.py: Data structures for identity, session, and provider-specific information
  • exceptions/: Custom exception classes for error handling
  • services/: Folder that contains all the provider services, how to make a new service is explained in another section.
  • lib/arguments/: CLI argument validation and parsing
  • lib/mutelist/: Resource exclusion and muting functionality
  • lib/regions/: Region management and validation. If the provider is NOT regional, this folder will not be created.
  • lib/service/: Base service class for provider-specific services

Step 2: Implement the Provider Class

Explanation: The provider class is the core component that handles authentication, session management, and identity information. It inherits from Prowler’s base Provider class and implements SDK-specific authentication flows. All providers must share, as far as possible, common patterns for session setup, identity management, and credential validation. Nevertheless, you may encounter changes and must adapt the implementation logic accordingly. A basic example of a common provider implementation is the following: File: prowler/providers/<provider_name>/<provider_name>_provider.py
import os
from typing import Optional, Union
from prowler.providers.common.provider import Provider
from prowler.providers.common.models import Audit_Metadata, Connection
from prowler.config.config import load_and_validate_config_file, get_default_mute_file_path
from prowler.lib.logger import logger
from prowler.lib.utils.utils import print_boxes

# Import your SDK and all the needed libraries for the provider.
import your_sdk_library
from your_sdk_library.auth_methods import ClientSecretCredential, ProfileCredential, DefaultCredential

# Import the needed exceptions, mutelist and models for the provider.
from prowler.providers.<provider_name>.exceptions.exceptions import <ProviderName>Exceptions
from prowler.providers.<provider_name>.mutelist.mutelist import <ProviderName>Mutelist
from prowler.providers.<provider_name>.models import <ProviderName>NeededModels

class YourProvider(Provider):
    """
    YourProvider class is the main class for the Your Provider.

    This class is responsible for initializing the provider, setting up the session,
    validating credentials, and managing identity information.

    Attributes:
        _type (str): The provider type.
        _session (YourSDKSession): The provider session.
        _identity (YourProviderIdentityInfo): The provider identity information.
        _audit_config (dict): The audit configuration.
        _mutelist (YourProviderMutelist): The provider mutelist.
        audit_metadata (Audit_Metadata): The audit metadata.
    """

    _type: str = "your_provider"
    _session: your_sdk_library.Session
    _identity: YourProviderIdentityInfo
    _audit_config: dict
    _mutelist: YourProviderMutelist
    audit_metadata: Audit_Metadata

    def __init__(
        self,
        # Authentication parameters
        client_id: str = None,
        client_secret: str = None,
        tenant_id: str = None,
        # Configuration
        config_path: str = None,
        config_content: dict = None,
        mutelist_path: str = None,
        mutelist_content: dict = None,
        # Additional provider-specific parameters
        region: str = None,
        profile: str = None,
    ):
        """
        Initializes the YourProvider instance.

        Args:
            client_id: The client ID for authentication
            client_secret: The client secret for authentication
            tenant_id: The tenant ID for authentication
            config_path: Path to the configuration file
            config_content: Configuration content as dictionary
            mutelist_path: Path to the mutelist file
            mutelist_content: Mutelist content as dictionary
            region: The region to use
            profile: The profile to use

        Raises:
            YourProviderSetUpSessionError: If session setup fails
            YourProviderInvalidCredentialsError: If credentials are invalid
        """
        logger.info("Initializing YourProvider ...")

        # Setup session using SDK
        self._session = self.setup_session(
            client_id, client_secret, tenant_id, region, profile
        )

        # Get identity information
        self._identity = self.setup_identity(self._session)

        # Load configuration
        if config_content:
            self._audit_config = config_content
        else:
            if not config_path:
                config_path = default_config_file_path
            self._audit_config = load_and_validate_config_file(self._type, config_path)

        # Setup mutelist
        if mutelist_content:
            self._mutelist = YourProviderMutelist(mutelist_content=mutelist_content)
        else:
            if not mutelist_path:
                mutelist_path = get_default_mute_file_path(self._type)
            self._mutelist = YourProviderMutelist(mutelist_path=mutelist_path)

        Provider.set_global_provider(self)

    @staticmethod
    def setup_session(
        client_id: str = None,
        client_secret: str = None,
        tenant_id: str = None,
        region: str = None,
        profile: str = None,
    ) -> your_sdk_library.Session:
        """
        Sets up the provider session using the provided credentials.

        This method handles the authentication flow and creates a session object
        that can be used to interact with the provider's services.

        Args:
            client_id: The client ID for authentication
            client_secret: The client secret for authentication
            tenant_id: The tenant ID for authentication
            region: The region to use
            profile: The profile to use

        Returns:
            YourSDKSession: The authenticated session object

        Raises:
            YourProviderSetUpSessionError: If session setup fails
        """
        try:
            logger.debug("Creating session ...")

            # Determine authentication method based on provided parameters
            if client_id and client_secret and tenant_id:
                # Use client credentials authentication
                credentials = your_sdk_library.ClientSecretCredential(
                    tenant_id=tenant_id,
                    client_id=client_id,
                    client_secret=client_secret
                )
                auth_method = "Client Credentials"
            elif profile:
                # Use profile-based authentication
                credentials = your_sdk_library.ProfileCredential(profile=profile)
                auth_method = "Profile"
            else:
                # Use default authentication (environment variables, etc.)
                credentials = your_sdk_library.DefaultCredential()
                auth_method = "Default"

            # Create session with credentials
            session = your_sdk_library.Session(
                credentials=credentials,
                region=region
            )

            logger.debug(f"Session created using {auth_method} authentication")
            return session

        except Exception as error:
            logger.critical(f"Failed to setup session: {error}")
            raise YourProviderSetUpSessionError(
                original_exception=error,
                file=os.path.basename(__file__),
            )

    def setup_identity(self, session: your_sdk_library.Session) -> YourProviderIdentityInfo:
        """
        Gets identity information from the provider session.

        This method retrieves account information, user details, and other
        identity-related data from the provider.

        Args:
            session: The authenticated session object

        Returns:
            YourProviderIdentityInfo: The identity information

        Raises:
            YourProviderSetUpIdentityError: If identity setup fails
        """
        try:
            # Use SDK to get account/identity information
            identity_info = session.get_identity()

            return YourProviderIdentityInfo(
                account_id=identity_info.account_id,
                account_name=identity_info.account_name,
                region=identity_info.region,
                user_id=identity_info.user_id,
                # Add other identity fields as needed
            )
        except Exception as e:
            logger.error(f"Failed to get identity information: {e}")
            raise YourProviderSetUpIdentityError(
                original_exception=e,
                file=os.path.basename(__file__),
            )

    @property
    def identity(self):
        """Returns the provider identity information."""
        return self._identity

    @property
    def session(self):
        """Returns the provider session object."""
        return self._session

    @property
    def type(self):
        """Returns the provider type."""
        return self._type

    @property
    def audit_config(self):
        """Returns the audit configuration."""
        return self._audit_config

    @property
    def mutelist(self):
        """Returns the provider mutelist."""
        return self._mutelist

    def print_credentials(self):
        """
        Display account information with color formatting.

        This method prints the provider credentials and account information
        in a formatted way using colorama for better readability.
        """
        from colorama import Fore, Style
        from prowler.lib.utils.utils import print_boxes

        report_lines = [
            f"  Account ID: {Fore.YELLOW}{self._identity.account_id}{Style.RESET_ALL}",
            f"  Account Name: {Fore.YELLOW}{self._identity.account_name}{Style.RESET_ALL}",
            f"  Region: {Fore.YELLOW}{self._identity.region}{Style.RESET_ALL}",
            f"  User ID: {Fore.YELLOW}{self._identity.user_id}{Style.RESET_ALL}",
        ]
        report_title = f"{Style.BRIGHT}Using the {self._type.upper()} credentials below:{Style.RESET_ALL}"
        print_boxes(report_lines, report_title)

    @staticmethod
    def test_connection(
        client_id: str = None,
        client_secret: str = None,
        tenant_id: str = None,
        region: str = None,
        profile: str = None,
        raise_on_exception: bool = True,
        provider_id: str = None,
    ) -> Connection:
        """
        Test connection to the provider.

        This method validates the provided credentials and tests the connection
        to the provider's services.

        Args:
            client_id: The client ID for authentication
            client_secret: The client secret for authentication
            tenant_id: The tenant ID for authentication
            region: The region to test
            profile: The profile to use
            raise_on_exception: Whether to raise exceptions or return Connection object
            provider_id: The provider ID to validate against

        Returns:
            Connection: Connection test result

        Raises:
            YourProviderSetUpSessionError: If session setup fails
            YourProviderInvalidCredentialsError: If credentials are invalid
        """
        try:
            # Create temporary session for testing
            test_session = YourProvider.setup_session(
                client_id, client_secret, tenant_id, region, profile
            )

            # Test the connection by getting identity
            identity = YourProvider.setup_identity(test_session)

            # Validate provider ID if provided
            if provider_id and identity.account_id != provider_id:
                raise YourProviderInvalidProviderIdError(
                    file=os.path.basename(__file__),
                )

            return Connection(
                status=True,
                message=f"Successfully connected to {provider_id or 'provider'}",
                error=None,
            )
        except Exception as e:
            if raise_on_exception:
                raise e
            return Connection(
                status=False,
                message="Failed to connect",
                error=str(e),
            )

    def get_regions(self) -> set:
        """
        Get available regions for the provider.

        Returns:
            set: Set of available region names
        """
        # Implementation depends on your provider
        # Example for cloud providers that support regions
        return {"region1", "region2", "region3"}

    def get_services(self) -> list:
        """
        Get available services for the provider.

        Returns:
            list: List of available service names
        """
        # Implementation depends on your provider
        return ["service1", "service2", "service3"]

Step 3: Create Models

Explanation: Models define the data structures used by your provider. They include identity information, session details, and provider-specific configurations. These models ensure type safety and consistent data handling across the provider. File: prowler/providers/<provider_name>/models.py
# Import the needed generic libraries for the provider.
from pydantic import BaseModel
from dataclasses import dataclass
from typing import Optional, List

# Import the needed Prowler libraries for the provider.
from prowler.providers.common.models import ProviderOutputOptions
from prowler.config.config import output_file_timestamp

class YourProviderIdentityInfo:
    """
    Identity information for the provider.

    This class holds all the identity-related information retrieved
    from the provider, including account details and user information.
    """
    account_id: str
    account_name: str
    region: str
    user_id: str
    # Add other identity fields as needed

class YourProviderSession:
    """
    Session object that contains the credentials and authentication details for the provider.

    This class holds the actual credentials and authentication information needed
    to establish a connection with the provider's services.
    """
    # Authentication credentials
    access_key: str
    secret_key: str
    # Or for other providers:
    # client_id: str
    # client_secret: str
    # tenant_id: str

    # Connection details
    region: str

Step 4: Implement Arguments

Explanation: Argument validation ensures that the provider receives valid configuration parameters. This step is crucial for preventing runtime errors and providing clear error messages to users. The validation should check for required parameters and validate their format. File: prowler/providers/<provider_name>/lib/arguments/arguments.py
def init_parser(self):
    """Init the <provider_name> Provider CLI parser"""
    <provider_name>_parser = self.subparsers.add_parser(
        "<provider_name>", parents=[self.common_providers_parser], help="<provider_name> Provider"
    )
    # Authentication Modes
    <provider_name>_auth_subparser = <provider_name>_parser.add_argument_group("Authentication Modes")
    <provider_name>_auth_modes_group = <provider_name>_auth_subparser.add_mutually_exclusive_group()
    <provider_name>_auth_modes_group.add_argument(
        "--credentials-file",
        nargs="?",
        metavar="FILE_PATH",
        help="Authenticate using a <provider_name> Service Account Application Credentials JSON file",
    )
    <provider_name>_auth_modes_group.add_argument(
        "--impersonate-service-account",
        nargs="?",
        metavar="SERVICE_ACCOUNT",
        help="Impersonate a <provider_name> Service Account",
    )
    <provider_name>_parser.add_argument(
        "--your-provider-region",
        help="Your Provider Region",
        type=str,
    )
    <provider_name>_parser.add_argument(
        "--env-auth",
        action="store_true",
        help="Use User and Password environment variables authentication to log in against <provider_name>",
    )
    # More arguments for the provider.

Step 5: Implement Mutelist

Explanation: The mutelist functionality allows users to exclude specific resources or checks from the audit. This is useful for handling false positives or excluding resources that are intentionally configured differently. File: prowler/providers/<provider_name>/lib/mutelist/mutelist.py
from prowler.lib.mutelist.mutelist import Mutelist
from prowler.lib.check.models import CheckReportYourProvider

class YourProviderMutelist(Mutelist):
    """
    Mutelist implementation for YourProvider.

    This class handles the muting functionality for the provider,
    allowing users to exclude specific checks or resources from audits.
    """

    def is_finding_muted(self, finding: CheckReportYourProvider) -> bool:
        """
        Check if a specific finding is muted.

        Args:
            finding: The finding to check
        """
        return self.is_muted(finding.check_id, finding.resource_id)

Step 6: Implement Regions

Explanation: Region management is essential for cloud providers that operate across multiple geographic locations. This component handles region validation and provides region-specific functionality.
Regions are optional, only if the provider has regions, for example Github does not have regions, but AWS does.
File: prowler/providers/<provider_name>/lib/regions/<provider_name>_regions.py
from typing import List, Set

def get_regions() -> List[str]:
    """
    Get list of available regions for the provider.

    Returns:
        List[str]: List of available region names
    """
    return [
        "region1",
        "region2",
        "region3",
        # ... other regions
    ]

def validate_region(region: str) -> bool:
    """
    Validate if a region is supported.

    Args:
        region: The region to validate

    Returns:
        bool: True if the region is valid, False otherwise
    """
    return region in get_regions()

def get_default_region() -> str:
    """
    Get the default region for the provider.

    Returns:
        str: The default region name
    """
    return "region1"

def get_global_region() -> str:
    """
    Get the global region for the provider.

    Returns:
        str: The global region name
    """
    return "global"

Step 7: Create Custom Exceptions

Explanation: Custom exceptions are needed to be able to handle the errors in a more specific way. Prowler uses a structured exception system with error codes, messages, and remediation steps. File: prowler/providers/<provider_name>/exceptions.py
from prowler.exceptions.exceptions import ProwlerException


# Exceptions codes from 7000 to 7999 are reserved for YourProvider exceptions (Numbers as example)
class YourProviderBaseException(ProwlerException):
    """Base class for YourProvider Errors."""

    YOUR_PROVIDER_ERROR_CODES = {
        (7001, "YourProviderCredentialsError"): {
            "message": "Error loading credentials for YourProvider",
            "remediation": "Check the credentials and ensure they are properly set up. API_KEY and API_SECRET are required.",
        },
        (7002, "YourProviderAuthenticationError"): {
            "message": "Authentication failed with YourProvider",
            "remediation": "Check the API credentials and ensure they are valid and have proper permissions.",
        },
        (7003, "YourProviderInvalidRegionError"): {
            "message": "Invalid region provided for YourProvider",
            "remediation": "Check the region and ensure it is a valid region for YourProvider.",
        },
        (7004, "YourProviderSetUpSessionError"): {
            "message": "Error setting up session",
            "remediation": "Check the session setup and ensure it is properly configured.",
        },
        (7005, "YourProviderInvalidProviderIdError"): {
            "message": "Provider does not match with the expected account_id",
            "remediation": "Check the provider and ensure it matches the expected account_id.",
        },
    }

    def __init__(self, code, file=None, original_exception=None, message=None):
        provider = "YourProvider"
        error_info = self.YOUR_PROVIDER_ERROR_CODES.get((code, self.__class__.__name__))
        if message:
            error_info["message"] = message
        super().__init__(
            code=code,
            source=provider,
            file=file,
            original_exception=original_exception,
            error_info=error_info,
        )


class YourProviderCredentialsError(YourProviderBaseException):
    """Base class for YourProvider credentials errors."""

    def __init__(self, file=None, original_exception=None, message=None):
        super().__init__(
            7001, file=file, original_exception=original_exception, message=message
        )


class YourProviderAuthenticationError(YourProviderCredentialsError):
    def __init__(self, file=None, original_exception=None, message=None):
        super().__init__(
            7002, file=file, original_exception=original_exception, message=message
        )


class YourProviderInvalidRegionError(YourProviderBaseException):
    def __init__(self, file=None, original_exception=None, message=None):
        super().__init__(
            7003, file=file, original_exception=original_exception, message=message
        )


class YourProviderSetUpSessionError(YourProviderCredentialsError):
    def __init__(self, file=None, original_exception=None, message=None):
        super().__init__(
            7004, file=file, original_exception=original_exception, message=message
        )


class YourProviderInvalidProviderIdError(YourProviderBaseException):
    def __init__(self, file=None, original_exception=None, message=None):
        super().__init__(
            7005, file=file, original_exception=original_exception, message=message
        )

Step 8: Implement Service Base Class

Explanation: The service base class defines a common interface for all services in your provider, since they will inherit from it. It defines the client to make requests to, the audit configuration and the fixer configuration. File: prowler/providers/<provider_name>/lib/service/service.py
from prowler.providers.<provider_name>.<provider_name>_provider import <ProviderName>Provider

class YourProviderService(BaseService):
    """
    Base service class for YourProvider services.

    This class provides common functionality for all services
    within the provider, including session management and error handling.
    """

    def __init__(self, provider: <ProviderName>Provider):
        """
        Initialize the service.

        Args:
            provider: The provider instance
        """
        self.client = provider.session.get_client(self.service_name)
        self.audit_config = provider.audit_config
        self.fixer_config = provider.fixer_config

Step 9: Register in CLI

Explanation: Add your provider to the available providers in the CLI. File: prowler/lib/cli/parser.py
class ProwlerArgumentParser:
    # Set the default parser
    def __init__(self):
        # CLI Arguments
        self.parser = argparse.ArgumentParser(
            prog="prowler",
            formatter_class=RawTextHelpFormatter,
            usage="prowler [-h] [--version] {aws,azure,gcp,kubernetes,m365,github,nhn,dashboard,iac,your_provider} ...",
            epilog="""
Available Cloud Providers:
  {aws,azure,gcp,kubernetes,m365,github,iac,nhn,your_provider}
    aws                 AWS Provider
    azure               Azure Provider
    gcp                 GCP Provider
    kubernetes          Kubernetes Provider
    m365                Microsoft 365 Provider
    github              GitHub Provider
    iac                 IaC Provider (Preview)
    nhn                 NHN Provider (Unofficial)
    your_provider       Your Provider

Available components:
    dashboard           Local dashboard

To see the different available options on a specific component, run:
    prowler {provider|dashboard} -h|--help

Detailed documentation at https://docs.prowler.com
""",

Step 10: Register in Main

Explanation: Main registration makes your provider discoverable by Prowler’s core system. It’s needed to add your provider to the output options and to the compliance evaluation. File: prowler/__main__.py
# In the prowler setup output options section
    if provider == "aws":
        output_options = AWSOutputOptions(
            args, bulk_checks_metadata, global_provider.identity
        )
    elif provider == "azure":
        output_options = AzureOutputOptions(
            args, bulk_checks_metadata, global_provider.identity
        )
    elif provider == "gcp":
        output_options = GCPOutputOptions(
            args, bulk_checks_metadata, global_provider.identity
        )
    elif provider == "kubernetes":
        output_options = KubernetesOutputOptions(
            args, bulk_checks_metadata, global_provider.identity
        )
    elif provider == "github":
        output_options = GithubOutputOptions(
            args, bulk_checks_metadata, global_provider.identity
        )
    elif provider == "m365":
        output_options = M365OutputOptions(
            args, bulk_checks_metadata, global_provider.identity
        )
    elif provider == "nhn":
        output_options = NHNOutputOptions(
            args, bulk_checks_metadata, global_provider.identity
        )
    elif provider == "iac":
        output_options = IACOutputOptions(
            args, bulk_checks_metadata
        )
    elif provider == "your_provider":
        output_options = YourProviderOutputOptions(
            args, bulk_checks_metadata, global_provider.identity
        )


    # Setup Compliance Options
    elif provider == "your_provider":
        for compliance_name in input_compliance_frameworks:
            if compliance_name.startswith("cis_"):
                # Generate CIS Finding Object (example of compliance with CIS framework)
                filename = (
                    f"{output_options.output_directory}/compliance/"
                    f"{output_options.output_filename}_{compliance_name}.csv"
                )
                cis = YourProviderCIS(
                    findings=finding_outputs,
                    compliance=bulk_compliance_frameworks[compliance_name],
                    file_path=filename,
                )
                generated_outputs["compliance"].append(cis)
                cis.batch_write_data_to_file()

Step 11: Register in the list of providers

Explanation: This is needed to be able to use the provider in the generic checks. The provider must be registered in the init_global_provider method to handle CLI arguments and initialization. File: prowler/providers/common/provider.py
elif "your_provider" in provider_class_name.lower():
    provider_class(
        username=arguments.your_provider_username,
        password=arguments.your_provider_password,
        tenant_id=arguments.your_provider_tenant_id,
        config_path=arguments.config_file,
        mutelist_path=arguments.mutelist_file,
        fixer_config=fixer_config,
    )

Step 12: Add to Config

Explanation: Configuration registration ensures your provider is recognized by Prowler’s configuration system. This enables proper handling of provider-specific settings and defaults. File: prowler/config/config.py
class Provider(str, Enum):
    AWS = "aws"
    AZURE = "azure"
    GCP = "gcp"
    KUBERNETES = "kubernetes"
    M365 = "m365"
    GITHUB = "github"
    YOUR_PROVIDER = "your_provider"  # Add your provider here
In some cases, you may need to create a new configuration file for your provider, for example, the AWS one that is inside prowler/providers/aws/config.py.

Step 13: Create Compliance Files

Explanation: Compliance files define the security checks and standards that your provider supports. These JSON files map security controls to specific checks and provide remediation guidance. It’s needed to create the folder with an init file to ensure the provider will work, however, adding different compliance files is optional. Folder: prowler/compliance/<provider_name>/
{
  "Framework": "CIS",
  "Version": "1.0",
  "Provider": "your_provider",
  "Description": "Description of the compliance framework",
  # The requirements depends on the framework, for example, CIS has a requirements section with the checks and attributes.
  "Requirements": [
    {
      "Id": "1.1.1",
      "Description": "Description of the requirement",
      "Checks": ["your_provider_check_1", "your_provider_check_2"],
      "Attributes": []
    }
  ]
}

Step 14: Add Output Support

Explanation: Output support ensures that your provider’s results are properly formatted in Prowler’s various output formats (CSV, JSON, HTML, etc.). This step integrates your provider with Prowler’s reporting system. File: prowler/lib/outputs/summary_table.py
# Add your provider case in the display_summary_table function
elif provider.type == "your_provider":
    entity_type = "Your Entity Type"
    audited_entities = provider.identity.your_entity_field
File: prowler/lib/outputs/finding.py
# Add your provider case in the fill_common_finding_data function
elif provider.type == "your_provider":
    output_data["auth_method"] = f"Your Auth Method: {get_nested_attribute(provider, 'identity.auth_type')}"
    output_data["account_uid"] = get_nested_attribute(provider, "identity.account_id")
    output_data["account_name"] = get_nested_attribute(provider, "identity.account_name")
    output_data["resource_name"] = check_output.resource_name
    output_data["resource_uid"] = check_output.resource_id
    output_data["region"] = check_output.location  # or your location field
File: prowler/lib/outputs/outputs.py
# Add your provider case in the stdout_report function
if finding.check_metadata.Provider == "your_provider":
    details = finding.your_location_field  # e.g., finding.location, finding.namespace, etc.

Step 15: Generate the HTML Report

Explanation: The HTML file is needed to be able to generate the HTML report. This step involves adding support for your provider in the HTML output generation system to ensure proper display of assessment summaries and findings. File: prowler/lib/outputs/html/html.py
@staticmethod
def get_your_provider_assessment_summary(provider: Provider) -> str:
    """
    get_your_provider_assessment_summary gets the HTML assessment summary for your provider

    Args:
        provider (Provider): the provider object

    Returns:
        str: the HTML assessment summary
    """
    try:
        return f"""
            <div class="col-md-2">
                <div class="card">
                    <div class="card-header">
                        Your Provider Assessment Summary
                    </div>
                    <ul class="list-group list-group-flush">
                        <li class="list-group-item">
                            <b>Your Entity Type:</b> {provider.identity.your_entity_field}
                        </li>
                        <li class="list-group-item">
                            <b>Your Location Field:</b> {provider.identity.your_location_field}
                        </li>
                    </ul>
                </div>
            </div>
            <div class="col-md-4">
                <div class="card">
                    <div class="card-header">
                        Your Provider Credentials
                    </div>
                    <ul class="list-group list-group-flush">
                        <li class="list-group-item">
                            <b>Authentication Method:</b> {provider.auth_method}
                        </li>
                        <li class="list-group-item">
                            <b>Identity ID:</b> {provider.identity.identity_id}
                        </li>
                    </ul>
                </div>
            </div>"""
    except Exception as error:
        logger.error(
            f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
        )
        return ""

Step 16: Add the Check Report Model

Explanation: Add the provider to the generic models, this is needed to be able to use the provider in the generic checks. File: prowler/providers/check/models.py
@dataclass
class CheckReportYourProvider(CheckReport):
    """
    Check report for YourProvider.
    """
    resource_name: str
    resource_id: str

    def _init_(self, metadata: Dict, resource: Any) -> None:
        super()._init_(metadata, resource)
        self.resource_name = resource.name
        self.resource_id = resource.id

Step 17: Add Dependencies

Explanation: Dependencies ensure that your provider’s required libraries are available when Prowler is installed. This step adds the necessary SDK or API client to Prowler’s dependency management. File: pyproject.toml
[tool.poetry.dependencies]
python = "^3.9"
# ... other dependencies
your-sdk-library = "^1.0.0"  # Add your SDK dependency

Step 18: Create Tests

Explanation: Testing ensures that your provider works correctly and maintains compatibility as Prowler evolves. Comprehensive tests cover authentication, session management, and provider-specific functionality. Folder: tests/providers/<provider_name>/
import pytest
from prowler.providers.your_provider.your_provider import YourProvider

class TestYourProvider:
    """Test cases for YourProvider."""

    def test_provider_initialization_with_client_credentials(self):
        """Test provider initialization with client credentials."""
        provider = YourProvider(
            client_id="test_client_id",
            client_secret="test_client_secret",
            tenant_id="test_tenant_id"
        )
        assert provider.type == "your_provider"
        assert provider.identity is not None
        assert provider.session is not None

    def test_provider_initialization_with_profile(self):
        """Test provider initialization with profile."""
        provider = YourProvider(
            profile="test_profile"
        )
        assert provider.type == "your_provider"
        assert provider.identity is not None

    def test_connection_test(self):
        """Test connection functionality."""
        result = YourProvider.test_connection(
            client_id="test_client_id",
            client_secret="test_client_secret",
            tenant_id="test_tenant_id"
        )
        # Add assertions based on expected behavior

    def test_identity_retrieval(self):
        """Test identity information retrieval."""
        provider = YourProvider(
            client_id="test_client_id",
            client_secret="test_client_secret",
            tenant_id="test_tenant_id"
        )
        assert provider.identity.account_id is not None
        assert provider.identity.account_name is not None

    def test_argument_validation(self):
        """Test argument validation."""
        from prowler.providers.your_provider.lib.arguments.arguments import (
            validate_your_provider_arguments
        )

        # Valid arguments
        validate_your_provider_arguments(
            client_id="test_client_id",
            client_secret="test_client_secret",
            tenant_id="test_tenant_id"
        )

        # Invalid arguments
        with pytest.raises(ValueError, match="at least one authentication method"):
            validate_your_provider_arguments()

Step 19: Update Documentation

Explanation: Documentation updates ensure that users can find information about your provider in Prowler’s documentation. This includes examples, configuration guides, and troubleshooting information. Update the provider documentation to include your new provider in the examples and implementation guidance.

API Providers

Definition:
  • Interact directly with the provider’s REST API using HTTP requests (e.g., via requests).
  • Examples: NHN Cloud.
Typical Use Cases:
  • Providers without an official Python SDK.
  • Providers with a non-official Python SDK that is not updated and maintained.
  • Providers that expose REST APIs and meet above requirements.
Key Characteristics:
  • Manual management of authentication (tokens, username/password, etc).
  • Arguments: Depends on the provider, for example, username, password, tenant_id, etc.
  • Outputs: Dicts or custom models based on API responses.
  • Custom HTTP session management with headers and authentication.
  • Manual handling of pagination, rate limiting, and error responses.
Implementation Details:
  • API providers require manual HTTP request management using libraries like requests.
  • Authentication typically involves obtaining tokens via login endpoints or OAuth flows.
  • Session management includes setting appropriate headers (Authorization, Content-Type, etc.).
  • Resource discovery often requires multiple API calls to different endpoints.
  • Error handling and retry logic must be implemented manually.

Implementation Guide for API Providers

Step 1: Create the Provider Structure

Explanation: API providers require the same structure as the SDK providers, the main difference would be that due to the lack of an official Python SDK, some methods could be implemented differently or not implemented at all. Required Structure:
prowler/providers/<provider_name>/
├── __init__.py
├── <provider_name>_provider.py
├── models.py
├── exceptions/
│   ├── __init__.py
│   └── exceptions.py
├── services/
│   ├── service_name1/
│   └── service_name2/
└── lib/
    ├── __init__.py
    ├── arguments/
    │   ├── __init__.py
    │   └── arguments.py
    ├── mutelist/
    │   ├── __init__.py
    │   └── mutelist.py
    ├── regions/
    │   ├── __init__.py
    │   └── regions.py
    └── service/
        ├── __init__.py
        └── service.py
Key Components:
  • <provider_name>_provider.py: Main provider class with HTTP session management
  • models.py: Data structures for identity and API responses
  • exceptions/: Custom exception classes for API errors
  • services/: Folder that contains all the provider services
  • lib/arguments/: CLI argument validation and parsing
  • lib/mutelist/: Resource exclusion and muting functionality
  • lib/regions/: Region management and validation. If the provider is NOT regional, this folder will not be created.
  • lib/service/: Base service class for provider-specific services

Step 2: Implement the Provider Class

Explanation: The provider class is the core component that handles HTTP session management, authentication, and identity information. It inherits from Prowler’s base Provider class and implements API-specific authentication flows using direct HTTP requests. File: prowler/providers/<provider_name>/<provider_name>_provider.py
import os
from typing import Optional
import requests
from prowler.providers.common.provider import Provider
from prowler.providers.common.models import Audit_Metadata, Connection
from prowler.config.config import load_and_validate_config_file, get_default_mute_file_path
from prowler.lib.logger import logger
from prowler.lib.utils.utils import print_boxes

# Import the needed exceptions, mutelist and models for the provider.
from prowler.providers.<provider_name>.exceptions.exceptions import <ProviderName>Exceptions
from prowler.providers.<provider_name>.lib.mutelist.mutelist import <ProviderName>Mutelist
from prowler.providers.<provider_name>.models import <ProviderName>NeededModels

class APIProvider(Provider):
    """
    APIProvider class is the main class for the API Provider.

    This class is responsible for initializing the provider, setting up the HTTP session,
    validating credentials, and managing identity information through direct API calls.

    Attributes:
        _type (str): The provider type.
        _session (requests.Session): The HTTP session for API calls.
        _identity (APIIdentityInfo): The provider identity information.
        _audit_config (dict): The audit configuration.
        _mutelist (APIMutelist): The provider mutelist.
        audit_metadata (Audit_Metadata): The audit metadata.
    """

    _type: str = "api_provider"
    _session: Optional[requests.Session]
    _identity: APIIdentityInfo
    _audit_config: dict
    _mutelist: APIMutelist
    audit_metadata: Audit_Metadata

    def __init__(
        self,
        # Authentication parameters
        username: str = None,
        password: str = None,
        tenant_id: str = None,
        # Configuration
        config_path: str = None,
        config_content: dict = None,
        mutelist_path: str = None,
        mutelist_content: dict = None,
        fixer_config: dict = None,
    ):
        """
        Initializes the APIProvider instance.

        Args:
            username: The API username for authentication
            password: The API password for authentication
            tenant_id: The tenant ID for authentication
            config_path: Path to the configuration file
            config_content: Configuration content as dictionary
            mutelist_path: Path to the mutelist file
            mutelist_content: Mutelist content as dictionary
            fixer_config: Fixer configuration dictionary

        Raises:
            ValueError: If required authentication parameters are missing
        """
        logger.info("Initializing APIProvider ...")

        # 1) Store argument values with environment variable fallback
        self._username = username or os.getenv("YOUR_PROVIDER_USERNAME")
        self._password = password or os.getenv("YOUR_PROVIDER_PASSWORD")
        self._tenant_id = tenant_id or os.getenv("YOUR_PROVIDER_TENANT_ID")

        # Validate required parameters
        if not all([self._username, self._password, self._tenant_id]):
            raise ValueError("APIProvider requires username, password and tenant_id")

        # 2) Load audit_config, fixer_config, mutelist
        self._fixer_config = fixer_config if fixer_config else {}

        if config_content:
            self._audit_config = config_content
        else:
            if not config_path:
                config_path = default_config_file_path
            self._audit_config = load_and_validate_config_file(self._type, config_path)

        if mutelist_content:
            self._mutelist = APIMutelist(mutelist_content=mutelist_content)
        else:
            if not mutelist_path:
                mutelist_path = get_default_mute_file_path(self._type)
            self._mutelist = APIMutelist(mutelist_path=mutelist_path)

        # 3) Initialize session/token
        self._token = None
        self._session = None
        self.setup_session()

        # 4) Create identity object
        self._identity = APIIdentityInfo(
            tenant_id=self._tenant_id,
            username=self._username,
        )

        Provider.set_global_provider(self)

    @property
    def type(self) -> str:
        """Returns the type of the provider."""
        return self._type

    @property
    def identity(self) -> APIIdentityInfo:
        """Returns the provider identity information."""
        return self._identity

    @property
    def session(self) -> requests.Session:
        """Returns the HTTP session for API calls."""
        return self._session

    @property
    def audit_config(self) -> dict:
        """Returns the audit configuration."""
        return self._audit_config

    @property
    def fixer_config(self) -> dict:
        """Returns the fixer configuration."""
        return self._fixer_config

    @property
    def mutelist(self) -> APIMutelist:
        """Returns the provider mutelist."""
        return self._mutelist

    def print_credentials(self) -> None:
        """
        Display account information with color formatting.

        This method prints the provider credentials and account information
        in a formatted way using colorama for better readability.
        """
        from colorama import Style

        report_lines = [
            f"  Username: {self._username}",
            f"  TenantID: {self._tenant_id}",
        ]
        report_title = f"{Style.BRIGHT}Using the {self._type.upper()} credentials below:{Style.RESET_ALL}"
        print_boxes(report_lines, report_title)

    def setup_session(self) -> None:
        """
        Implement API authentication method by calling the provider's authentication endpoint.

        This method performs the authentication flow to obtain an access token
        and creates a requests.Session with the appropriate headers for API calls.
        """
        # Example for a Keystone-like authentication
        url = "https://api.your-provider.com/v2.0/tokens"
        data = {
            "auth": {
                "tenantId": self._tenant_id,
                "passwordCredentials": {
                    "username": self._username,
                    "password": self._password,
                },
            }
        }

        try:
            response = requests.post(url, json=data, timeout=10)
            if response.status_code == 200:
                resp_json = response.json()
                self._token = resp_json["access"]["token"]["id"]

                # Create session with authentication headers
                sess = requests.Session()
                sess.headers.update({
                    "X-Auth-Token": self._token,
                    "Content-Type": "application/json"
                })
                self._session = sess
                logger.info("API token acquired successfully and session is set up.")
            else:
                logger.critical(
                    f"Failed to get token. Status: {response.status_code}, Body: {response.text}"
                )
                raise ValueError("Failed to get API token")
        except Exception as e:
            logger.critical(f"[setup_session] Error: {e}")
            raise e

    @staticmethod
    def test_connection(
        username: str,
        password: str,
        tenant_id: str,
        raise_on_exception: bool = True,
    ) -> Connection:
        """
        Test connection to the API provider by performing:
          1) Authentication token request
          2) (Optional) a small test API call to confirm credentials are valid

        Args:
            username: The API username
            password: The API password
            tenant_id: The tenant ID
            raise_on_exception: If True, raise the caught exception;
                               if False, return Connection(error=exception).

        Returns:
            Connection: Connection test result
        """
        try:
            # 1) Validate arguments
            if not username or not password or not tenant_id:
                error_msg = "API test_connection error: missing username/password/tenant_id"
                logger.error(error_msg)
                raise ValueError(error_msg)

            # 2) Request authentication token
            token_url = "https://api.your-provider.com/v2.0/tokens"
            data = {
                "auth": {
                    "tenantId": tenant_id,
                    "passwordCredentials": {
                        "username": username,
                        "password": password,
                    },
                }
            }

            resp = requests.post(token_url, json=data, timeout=10)
            if resp.status_code != 200:
                error_msg = f"Failed to get token. Status: {resp.status_code}, Body: {resp.text}"
                logger.error(error_msg)
                if raise_on_exception:
                    raise Exception(error_msg)
                return Connection(error=Exception(error_msg))

            # Success
            token_json = resp.json()
            api_token = token_json["access"]["token"]["id"]
            logger.info("API test_connection: Successfully acquired token.")

            # 3) (Optional) Test API call to confirm credentials are valid
            test_endpoint = f"https://api.your-provider.com/v2/{tenant_id}/test"
            headers = {
                "X-Auth-Token": api_token,
                "Content-Type": "application/json",
            }

            test_resp = requests.get(test_endpoint, headers=headers, timeout=10)
            if test_resp.status_code == 200:
                logger.info("API test_connection: Test call success. Credentials valid.")
                return Connection(is_connected=True)
            else:
                error_msg = f"Test call failed. Status: {test_resp.status_code}, Body: {test_resp.text}"
                logger.error(error_msg)
                if raise_on_exception:
                    raise Exception(error_msg)
                return Connection(error=Exception(error_msg))

        except Exception as e:
            logger.critical(f"{e.__class__.__name__}[{e.__traceback__.tb_lineno}]: {e}")
            if raise_on_exception:
                raise e
            return Connection(error=e)

    @staticmethod
    def validate_arguments(username: str, password: str, tenant_id: str) -> None:
        """
        Ensures that username, password, and tenant_id are not empty.

        Args:
            username: The username to validate
            password: The password to validate
            tenant_id: The tenant ID to validate

        Raises:
            ValueError: If any required parameter is missing
        """
        if not username or not password or not tenant_id:
            raise ValueError("API Provider requires username, password and tenant_id.")

Step 3: Create Models

Explanation: Models define the data structures used by your API provider. They include identity information and API response structures. These models ensure type safety and consistent data handling across the provider. File: prowler/providers/<provider_name>/models.py This step is common with SDK providers so you can follow the same pattern as there.

Step 4: Implement Arguments

Explanation: Argument validation ensures that the API provider receives valid configuration parameters. This step is crucial for preventing runtime errors and providing clear error messages to users. File: prowler/providers/<provider_name>/lib/arguments/arguments.py Arguments depends on the provider and not the type, so the pattern for this step is the same as the SDK providers.

Step 5: Implement Mutelist

Explanation: The mutelist functionality allows users to exclude specific resources or checks from the audit. This is useful for handling false positives or excluding resources that are intentionally configured differently. File: prowler/providers/<provider_name>/lib/mutelist/mutelist.py The implementation of the mutelist is the same as the SDK providers.

Step 6: Implement Regions

Explanation: Region management is essential for cloud providers that operate across multiple geographic locations. This component handles region validation and provides region-specific functionality.
Regions are optional, only if the provider has regions, for example Github does not have regions, but AWS does.
File: prowler/providers/<provider_name>/lib/regions/<provider_name>_regions.py The implementation of the regions is the same as the SDK providers.

Step 7: Create Custom Exceptions

Explanation: Custom exceptions provide specific error handling for API-related issues, making debugging and error reporting more effective. Prowler uses a structured exception system with error codes, messages, and remediation steps. File: prowler/providers/<provider_name>/exceptions/exceptions.py
from prowler.exceptions.exceptions import ProwlerException


# Exceptions codes from 8000 to 8999 are reserved for API Provider exceptions (example numbers)
class APIProviderBaseException(ProwlerException):
    """Base class for API Provider Errors."""

    APIProvider_ERROR_CODES = {
        (8000, "APIProviderCredentialsError"): {
            "message": "API Provider credentials not found or invalid",
            "remediation": "Check the API Provider API credentials and ensure they are properly set.",
        },
        (8001, "APIProviderAuthenticationError"): {
            "message": "API Provider authentication failed",
            "remediation": "Check the API Provider API credentials and ensure they are valid.",
        },
        (8002, "APIProviderSessionError"): {
            "message": "API Provider session setup failed",
            "remediation": "Check the session setup and ensure it is properly configured.",
        },
        (8003, "APIProviderIdentityError"): {
            "message": "API Provider identity setup failed",
            "remediation": "Check credentials and ensure they are properly set up for API Provider.",
        },
        (8004, "APIProviderAPIError"): {
            "message": "API Provider API call failed",
            "remediation": "Check the API request and ensure it is properly formatted.",
        },
        (8005, "APIProviderRateLimitError"): {
            "message": "API Provider API rate limit exceeded",
            "remediation": "Reduce the number of API requests or wait before making more requests.",
        },
    }

    def __init__(self, code, file=None, original_exception=None, message=None):
        provider = "API Provider"
        error_info = self.APIProvider_ERROR_CODES.get((code, self.__class__.__name__))
        if message:
            error_info["message"] = message
        super().__init__(
            code=code,
            source=provider,
            file=file,
            original_exception=original_exception,
            error_info=error_info,
        )


class APIProviderCredentialsError(APIProviderBaseException):
    """Exception for API Provider credentials errors"""

    def __init__(self, file=None, original_exception=None, message=None):
        super().__init__(
            code=8000,
            file=file,
            original_exception=original_exception,
            message=message,
        )


class APIProviderAuthenticationError(APIProviderBaseException):
    """Exception for API Provider authentication errors"""

    def __init__(self, file=None, original_exception=None, message=None):
        super().__init__(
            code=8001,
            file=file,
            original_exception=original_exception,
            message=message,
        )


class APIProviderSessionError(APIProviderBaseException):
    """Exception for API Provider session setup errors"""

    def __init__(self, file=None, original_exception=None, message=None):
        super().__init__(
            code=8002,
            file=file,
            original_exception=original_exception,
            message=message,
        )


class APIProviderIdentityError(APIProviderBaseException):
    """Exception for API Provider identity setup errors"""

    def __init__(self, file=None, original_exception=None, message=None):
        super().__init__(
            code=8003,
            file=file,
            original_exception=original_exception,
            message=message,
        )


class APIProviderAPIError(APIProviderBaseException):
    """Exception for API Provider API errors"""

    def __init__(self, file=None, original_exception=None, message=None):
        super().__init__(
            code=8004,
            file=file,
            original_exception=original_exception,
            message=message,
        )


class APIProviderRateLimitError(APIProviderBaseException):
    """Exception for API Provider rate limit errors"""

    def __init__(self, file=None, original_exception=None, message=None):
        super().__init__(
            code=8005,
            file=file,
            original_exception=original_exception,
            message=message,
        )

Step 8: Implement Service Base Class

Explanation: The service base class defines a common interface for all services in your provider, since they will inherit from it. It defines the client to make requests to, the audit configuration and the fixer configuration. File: prowler/providers/<provider_name>/lib/service/service.py
from prowler.providers.<provider_name>.<provider_name>_provider import <ProviderName>Provider

class APIProviderService(BaseService):
    """
    Base service class for API Provider services.

    This class provides common functionality for all services
    within the provider, including session management and error handling.
    """

    def __init__(self, provider: <ProviderName>Provider):
        """
        Initialize the service.

        Args:
            provider: The provider instance
        """
        self.client = provider.session.get_client(self.service_name)
        self.audit_config = provider.audit_config
        self.fixer_config = provider.fixer_config
        self.session = provider.session
        self.base_url = provider.session.base_url
        self.auth = HTTPDigestAuth(
            provider.session.public_key,
            provider.session.private_key,
        )
        self.headers = {
            "Authorization": self.auth.encode(),
            "Content-Type": "application/json",
        }

Step 9: Register in CLI

Explanation: Add your provider to the available providers in the CLI. File: prowler/lib/cli/parser.py This step is the same as the SDK providers.

Step 10: Register in Main

Explanation: Main registration makes your provider discoverable by Prowler’s core system. It’s needed to add your provider to the output options and to the compliance evaluation. File: prowler/__main__.py This step is the same as the SDK providers.

Step 11: Register in the list of providers

Explanation: This is needed to be able to use the provider in the generic checks. The provider must be registered in the init_global_provider method to handle CLI arguments and initialization. File: prowler/providers/common/provider.py This step is the same as the SDK providers.

Step 12: Add to Config

Explanation: Configuration registration ensures your API provider is recognized by Prowler’s configuration system. This enables proper handling of provider-specific settings and defaults. File: prowler/config/config.py This step is the same as the SDK providers.

Step 13: Create Compliance Files

Explanation: Compliance files define the security checks and standards that your provider supports. These JSON files map security controls to specific checks and provide remediation guidance. It’s needed to create the folder with an init file to ensure the provider will work, however, adding different compliance files is optional. Folder: prowler/compliance/<provider_name>/ This step is the same as the SDK providers.

Step 14: Add Output Support

Explanation: Output support ensures that your provider’s results are properly formatted in Prowler’s various output formats (CSV, JSON, HTML, etc.). This step integrates your provider with Prowler’s reporting system. File: prowler/lib/outputs/summary_table.py This step is the same as the SDK providers.

Step 15: Generate the HTML Report

Explanation: The HTML file is needed to be able to generate the HTML report. This step involves adding support for your provider in the HTML output generation system to ensure proper display of assessment summaries and findings. File: prowler/lib/outputs/html/html.py This step is the same as the SDK providers.

Step 16: Add the Check Report Model

Explanation: Add the provider to the generic models, this is needed to be able to use the provider in the generic checks. File: prowler/providers/check/models.py This step is the same as the SDK providers.

Step 17: Create Tests

Explanation: Testing ensures that your API provider works correctly and maintains compatibility as Prowler evolves. Comprehensive tests cover authentication, session management, and API-specific functionality. Folder: tests/providers/<provider_name>/
import pytest
from prowler.providers.api_provider.api_provider import APIProvider

class TestAPIProvider:
    """Test cases for APIProvider."""

    def test_provider_initialization(self):
        """Test provider initialization with valid credentials."""
        provider = APIProvider(
            username="test_user",
            password="test_password",
            tenant_id="test_tenant"
        )
        assert provider.type == "your_api_provider"
        assert provider.identity is not None
        assert provider.session is not None

    def test_connection_test(self):
        """Test connection functionality."""
        result = APIProvider.test_connection(
            username="test_user",
            password="test_password",
            tenant_id="test_tenant"
        )
        # Add assertions based on expected behavior

    def test_argument_validation(self):
        """Test argument validation."""
        from prowler.providers.api_provider.api_provider import (
            APIProvider
        )

        # Valid arguments
        APIProvider.validate_arguments(
            username="test_user",
            password="test_password",
            tenant_id="test_tenant"
        )

        # Invalid arguments
        with pytest.raises(ValueError, match="requires username, password and tenant_id"):
            APIProvider.validate_arguments("", "", "")

    def test_session_setup(self):
        """Test session setup."""
        provider = APIProvider(
            username="test_user",
            password="test_password",
            tenant_id="test_tenant"
        )
        assert provider.session is not None
        assert "X-Auth-Token" in provider.session.headers

Step 18: Update Documentation

Explanation: Documentation updates ensure that users can find information about your API provider in Prowler’s documentation. This includes examples, configuration guides, and troubleshooting information. Update the provider documentation to include your new API provider in the examples and implementation guidance.

Tool/Wrapper Providers

Definition:
  • Integrate third-party tools as libraries or subprocesses (e.g., Trivy for IaC).
  • Examples: IaC (Trivy).
Typical Use Cases:
  • Providers that require integration with external security tools.
  • Tools that need to be executed as subprocesses or imported as libraries.
  • Providers that require specific tool configurations and argument mapping.
  • Legacy systems or tools that don’t have direct API access.
Key Characteristics:
  • No session/identity management required (tool handles this internally).
  • Arguments: specific to the tool, but for example: scan_path, frameworks, exclude_path, scan_repository_url, etc.
  • Outputs: Tool-specific output formats that need to be parsed and converted.
  • Tool execution and output parsing.
  • Configuration file mapping and argument translation.
Implementation Details:
  • Tool providers typically execute external tools as subprocesses (e.g., pwsh or trivy command).
  • They require mapping between Prowler’s interface and the tool’s arguments.
  • Output parsing and conversion to Prowler’s standard format is crucial.
  • Tool-specific configuration files and validation.
  • Repository cloning and temporary file management for remote scans (if needed).
Note: This guide provides a general framework for integrating any external tool. The specific implementation details (like repository cloning, authentication tokens, etc.) will depend on your particular tool’s requirements. The core pattern is: integrate with Prowler’s CLI, execute your tool via subprocess, and parse the output into Prowler’s format.

Implementation Guide for Tool/Wrapper Providers

Step 1: Create the Provider Structure

Explanation: Tool/Wrapper providers require a specific folder structure to organize tool integration, configuration, and service management. This structure follows Prowler’s conventions and ensures proper integration with the CLI and API. Required Structure:
prowler/providers/<provider_name>/
├── __init__.py
├── <provider_name>_provider.py
├── models.py
└── lib/
    ├── __init__.py
    └── arguments/
        ├── __init__.py
        └── arguments.py
Key Components:
  • <provider_name>_provider.py: Main provider class with tool integration
  • models.py: Data structures for tool output and configuration
  • lib/arguments/: CLI argument validation and parsing

Step 2: Implement the Provider Class

Explanation: The provider class is the core component that handles tool integration, execution, and output parsing. It inherits from Prowler’s base Provider class and implements tool-specific execution flows using subprocesses or library calls. File: prowler/providers/<provider_name>/<provider_name>_provider.py
import json
import subprocess
import sys
from typing import List

from colorama import Fore, Style

from prowler.config.config import (
    default_config_file_path,
    load_and_validate_config_file,
)
from prowler.lib.check.models import CheckReportYourTool
from prowler.lib.logger import logger
from prowler.lib.utils.utils import print_boxes
from prowler.providers.common.models import Audit_Metadata
from prowler.providers.common.provider import Provider

class ToolProvider(Provider):
    """
    ToolProvider class is the main class for the Your Tool Provider.

    This class is responsible for initializing the provider, executing the external tool,
    parsing tool output, and converting results to Prowler's standard format.

    Attributes:
        _type (str): The provider type.
        _session: Not used for tool providers.
        _identity (str): Simple identity for tool providers.
        _audit_config (dict): The audit configuration.
        audit_metadata (Audit_Metadata): The audit metadata.
    """

    _type: str = "your_tool_provider"
    audit_metadata: Audit_Metadata

    def __init__(
        self,
        # Tool-specific parameters
        scan_path: str = ".",
        tool_specific_arg: str = "default_value",
        exclude_path: list[str] = [],
        # Configuration
        config_path: str = None,
        config_content: dict = None,
        fixer_config: dict = {},
        # Authentication (if needed for your tool)
        auth_token: str = None,
        auth_username: str = None,
    ):
        """
        Initializes the ToolProvider instance.

        Args:
            scan_path: Path to the folder containing files to scan
            tool_specific_arg: Tool-specific argument for your external tool
            exclude_path: List of paths to exclude from scan
            config_path: Path to the configuration file
            config_content: Configuration content as dictionary
            fixer_config: Fixer configuration dictionary
            auth_token: Authentication token for your tool (if needed)
            auth_username: Username for your tool (if needed)

        Raises:
            ValueError: If required parameters are missing
        """
        logger.info("Instantiating YourTool Provider...")

        # Store tool-specific parameters
        self.scan_path = scan_path
        self.tool_specific_arg = tool_specific_arg
        self.exclude_path = exclude_path
        self.region = "global"
        self.audited_account = "local-tool"
        self._session = None
        self._identity = "prowler"
        self._auth_method = "No auth"

        # Handle tool authentication if needed
        if auth_token:
            self.auth_token = auth_token
            self._auth_method = "Token"
            logger.info("Using token for tool authentication")
        elif auth_username:
            self.auth_username = auth_username
            self._auth_method = "Username"
            logger.info("Using username for tool authentication")
                logger.info("Using username for tool authentication")
            else:
                logger.debug("No authentication method provided; proceeding without authentication.")

        # Audit Config
        if config_content:
            self._audit_config = config_content
        else:
            if not config_path:
                config_path = default_config_file_path
            self._audit_config = load_and_validate_config_file(self._type, config_path)

        # Fixer Config
        self._fixer_config = fixer_config

        # Mutelist (not needed for tool providers since tools have their own mutelist logic)
        self._mutelist = None

        Provider.set_global_provider(self)

    @property
    def auth_method(self):
        """Returns the authentication method used."""
        return self._auth_method

    @property
    def type(self):
        """Returns the type of the provider."""
        return self._type

    @property
    def identity(self):
        """Returns the provider identity."""
        return self._identity

    @property
    def session(self):
        """Returns the session (not used for tool providers)."""
        return self._session

    @property
    def audit_config(self):
        """Returns the audit configuration."""
        return self._audit_config

    @property
    def fixer_config(self):
        """Returns the fixer configuration."""
        return self._fixer_config

    def setup_session(self):
        """Tool providers don't need a session since they use external tools directly"""
        return None

    def _process_check(self, finding: dict, check: dict, status: str) -> CheckReportYourTool:
        """
        Process a single check (failed or passed) and create a CheckReportYourTool object.

        Args:
            finding: The finding object from tool output
            check: The individual check data
            status: The status of the check ("FAIL", "PASS", or "MUTED")

        Returns:
            CheckReportYourTool: The processed check report
        """
        try:
            metadata_dict = {
                "Provider": "your_tool_provider",
                "CheckID": check.get("check_id", ""),
                "CheckTitle": check.get("check_name", ""),
                "CheckType": ["Your Tool Provider"],
                "ServiceName": finding["check_type"],
                "SubServiceName": "",
                "ResourceIdTemplate": "",
                "Severity": (
                    check.get("severity", "low").lower()
                    if check.get("severity")
                    else "low"
                ),
                "ResourceType": "your_tool",
                "Description": check.get("check_name", ""),
                "Risk": "",
                "RelatedUrl": (
                    check.get("guideline", "") if check.get("guideline") else ""
                ),
                "Remediation": {
                    "Code": {
                        "NativeIaC": "",
                        "Terraform": "",
                        "CLI": "",
                        "Other": "",
                    },
                    "Recommendation": {
                        "Text": "",
                        "Url": (
                            check.get("guideline", "") if check.get("guideline") else ""
                        ),
                    },
                },
                "Categories": [],
                "DependsOn": [],
                "RelatedTo": [],
                "Notes": "",
            }

            # Convert metadata dict to JSON string
            metadata = json.dumps(metadata_dict)

            report = CheckReportYourTool(metadata=metadata, finding=check)
            report.status = status
            report.resource_tags = check.get("entity_tags", {})
            report.status_extended = check.get("check_name", "")
            if status == "MUTED":
                report.muted = True
            return report
        except Exception as error:
            logger.critical(
                f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
            )
            sys.exit(1)

    def run(self) -> List[CheckReportYourTool]:
        """
        Main execution method that handles tool execution.

        Returns:
            List[CheckReportYourTool]: List of check reports from the tool scan
        """
        return self.run_scan(self.scan_path, self.exclude_path)

    def run_scan(
        self, directory: str, exclude_path: list[str]
    ) -> List[CheckReportYourTool]:
        """
        Execute the external tool and parse its output.

        Args:
            directory: Directory to scan
            frameworks: List of frameworks to scan
            exclude_path: List of paths to exclude

        Returns:
            List[CheckReportYourTool]: List of check reports
        """
        try:
            logger.info(f"Running YourTool scan on {directory} ...")

            # Build the tool command
            tool_command = [
                "your_tool_command",
                # Add your tool-specific arguments here, this are just examples
                "-d",
                directory,
                "-o",
                "json",
                "-f",
                ",".join(frameworks),
            ]
            if exclude_path:
                tool_command.extend(["--skip-path", ",".join(exclude_path)])

            # Run the tool with JSON output
            process = subprocess.run(
                tool_command,
                capture_output=True,
                text=True,
            )

            # Log tool's error output if any
            if process.stderr:
                logger.error(process.stderr)

            try:
                output = json.loads(process.stdout)
                if not output:
                    logger.warning("No findings returned from YourTool scan")
                    return []
            except Exception as error:
                logger.critical(
                    f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
                )
                sys.exit(1)

            reports = []

            # If only one framework has findings, the output is a dict, otherwise it's a list of dicts
            if isinstance(output, dict):
                output = [output]

            # Process all frameworks findings
            for finding in output:
                results = finding.get("results", {})

                # Process failed checks
                failed_checks = results.get("failed_checks", [])
                for failed_check in failed_checks:
                    report = self._process_check(finding, failed_check, "FAIL")
                    reports.append(report)

                # Process passed checks
                passed_checks = results.get("passed_checks", [])
                for passed_check in passed_checks:
                    report = self._process_check(finding, passed_check, "PASS")
                    reports.append(report)

                # Process skipped checks (muted)
                skipped_checks = results.get("skipped_checks", [])
                for skipped_check in skipped_checks:
                    report = self._process_check(finding, skipped_check, "MUTED")
                    reports.append(report)

            return reports

        except Exception as error:
            if "No such file or directory: 'your_tool_command'" in str(error):
                logger.critical("Please, install your_tool using 'pip install your_tool'")
                sys.exit(1)
            logger.critical(
                f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
            )
            sys.exit(1)

    def print_credentials(self):
        """
        Display scan information with color formatting.

        This method prints the tool scan information in a formatted way
        using colorama for better readability.
        """
        if self.scan_repository_url:
            report_title = (
                f"{Style.BRIGHT}Scanning remote repository:{Style.RESET_ALL}"
            )
            report_lines = [
                f"Repository: {Fore.YELLOW}{self.scan_repository_url}{Style.RESET_ALL}",
            ]
        else:
            report_title = (
                f"{Style.BRIGHT}Scanning local directory:{Style.RESET_ALL}"
            )
            report_lines = [
                f"Directory: {Fore.YELLOW}{self.scan_path}{Style.RESET_ALL}",
            ]

        if self.exclude_path:
            report_lines.append(
                f"Excluded paths: {Fore.YELLOW}{', '.join(self.exclude_path)}{Style.RESET_ALL}"
            )

        report_lines.append(
            f"Frameworks: {Fore.YELLOW}{', '.join(self.frameworks)}{Style.RESET_ALL}"
        )

        report_lines.append(
            f"Authentication method: {Fore.YELLOW}{self.auth_method}{Style.RESET_ALL}"
        )

        print_boxes(report_lines, report_title)

Step 3: Create Models

Explanation: Models define the data structures used by your tool provider. They include output options and tool-specific configurations. These models ensure type safety and consistent data handling across the provider. File: prowler/providers/<provider_name>/models.py
from prowler.config.config import output_file_timestamp
from prowler.providers.common.models import ProviderOutputOptions

class YourToolOutputOptions(ProviderOutputOptions):
    """
    YourToolOutputOptions overrides ProviderOutputOptions for tool-specific output logic.
    For example, generating a filename that includes the tool name.

    Attributes inherited from ProviderOutputOptions:
        - output_filename (str): The base filename used for generated reports.
        - output_directory (str): The directory to store the output files.
        - ... see ProviderOutputOptions for more details.

    Methods:
        - __init__: Customizes the output filename logic for the tool provider.
    """

    def __init__(self, arguments, bulk_checks_metadata):
        super().__init__(arguments, bulk_checks_metadata)

        # If --output-filename is not specified, build a default name.
        if not getattr(arguments, "output_filename", None):
            self.output_filename = f"prowler-output-your_tool-{output_file_timestamp}"
        # If --output-filename was explicitly given, respect that
        else:
            self.output_filename = arguments.output_filename

Step 4: Implement Arguments

Explanation: Argument validation ensures that the tool provider receives valid configuration parameters. This step is crucial for preventing runtime errors and providing clear error messages to users. File: prowler/providers/<provider_name>/lib/arguments/arguments.py
# Add your tool-specific choices if needed
TOOL_SPECIFIC_CHOICES = [
    "option1",
    "option2",
    "option3",
    # Add your tool's supported options
]

def init_parser(self):
    """Init the <provider_name> Provider CLI parser"""
    <provider_name>_parser = self.subparsers.add_parser(
        "<provider_name>", parents=[self.common_providers_parser], help="<Provider Name> Provider"
    )

    # Scan Path
    <provider_name>_scan_subparser = <provider_name>_parser.add_argument_group("Scan Path")
    <provider_name>_scan_subparser.add_argument(
        "--scan-path",
        "-P",
        dest="scan_path",
        default=".",
        help="Path to the folder containing your files to scan. Default: current directory.",
    )

    <provider_name>_scan_subparser.add_argument(
        "--tool-specific-arg",
        dest="tool_specific_arg",
        default="default_value",
        choices=TOOL_SPECIFIC_CHOICES,
        help="Tool-specific argument for your external tool. Default: default_value",
    )

    <provider_name>_scan_subparser.add_argument(
        "--exclude-path",
        dest="exclude_path",
        nargs="+",
        default=[],
        help="Comma-separated list of paths to exclude from the scan. Default: none",
    )

    # Authentication (if needed for your tool)
    <provider_name>_scan_subparser.add_argument(
        "--auth-token",
        dest="auth_token",
        nargs="?",
        default=None,
        help="Authentication token for your tool. If not provided, will use YOUR_TOOL_AUTH_TOKEN env var.",
    )
    <provider_name>_scan_subparser.add_argument(
        "--auth-username",
        dest="auth_username",
        nargs="?",
        default=None,
        help="Username for your tool authentication. If not provided, will use YOUR_TOOL_AUTH_USERNAME env var.",
    )

def validate_arguments(arguments):
    """
    Validate tool-specific arguments.

    Args:
        arguments: The parsed arguments

    Returns:
        tuple: (is_valid, error_message)
    """
    scan_path = getattr(arguments, "scan_path", None)
    scan_repository_url = getattr(arguments, "scan_repository_url", None)

    if scan_path and scan_repository_url:
        # If scan_path is set to default ("."), allow scan_repository_url
        if scan_path != ".":
            return (
                False,
                "--scan-path (-P) and --scan-repository-url (-R) are mutually exclusive. Please specify only one.",
            )
    return (True, "")

Step 5: Register in CLI

Explanation: Add your provider to the available providers in the CLI. File: prowler/lib/cli/parser.py This step is the same as the SDK providers.

Step 6: Register in Main

Explanation: Main registration makes your provider discoverable by Prowler’s core system. It’s needed to add your provider to the output options and to the compliance evaluation. File: prowler/__main__.py This step is the same as the SDK providers.

Step 7: Register in the list of providers

Explanation: This is needed to be able to use the provider in the generic checks. The provider must be registered in the init_global_provider method to handle CLI arguments and initialization. File: prowler/providers/common/provider.py This step is the same as the SDK providers.

Step 8: Add to Config

Explanation: Configuration registration ensures your tool provider is recognized by Prowler’s configuration system. This enables proper handling of provider-specific settings and defaults. File: prowler/config/config.py This step is the same as the SDK providers. In some cases, you may need to create a new configuration file for your provider, for example, the AWS one that is inside prowler/providers/aws/config.py.

Step 9: Create Compliance Files

Explanation: Compliance files define the security checks and standards that your provider supports. These JSON files map security controls to specific checks and provide remediation guidance. It’s needed to create the folder with an init file to ensure the provider will work, however, adding different compliance files is optional. Folder: prowler/compliance/<provider_name>/ This step is the same as the SDK providers.

Step 10: Add Output Support

Explanation: Output support ensures that your provider’s results are properly formatted in Prowler’s various output formats (CSV, JSON, HTML, etc.). This step integrates your provider with Prowler’s reporting system. File: prowler/lib/outputs/summary_table.py This step is the same as the SDK providers.

Step 11: Generate the HTML Report

Explanation: The HTML file is needed to be able to generate the HTML report. This step involves adding support for your provider in the HTML output generation system to ensure proper display of assessment summaries and findings. File: prowler/lib/outputs/html/html.py This step is the same as the SDK providers.

Step 12: Add the Check Report Model

Explanation: Add the provider to the generic models, this is needed to be able to use the provider in the generic checks. File: prowler/providers/check/models.py This step is the same as the SDK providers.

Step 13: Create Tests

Explanation: Testing ensures that your tool provider works correctly and maintains compatibility as Prowler evolves. Comprehensive tests cover tool execution, output parsing, and provider-specific functionality. Folder: tests/providers/<provider_name>/
import pytest
import tempfile
import os
from prowler.providers.your_tool_provider.your_tool_provider import ToolProvider

class TestToolProvider:
    """Test cases for ToolProvider."""

    def test_provider_initialization(self):
        """Test provider initialization with valid parameters."""
        provider = ToolProvider(
            scan_path=".",
            frameworks=["framework1"]
        )
        assert provider.type == "your_tool_provider"
        assert provider.identity == "prowler"
        assert provider.scan_path == "."

    def test_tool_execution(self):
        """Test tool execution and output parsing."""
        provider = ToolProvider(scan_path=".")
        # Mock the subprocess call and test output parsing
        # This will depend on your specific tool's output format

    def test_argument_validation(self):
        """Test argument validation."""
        from prowler.providers.your_tool_provider.lib.arguments.arguments import (
            validate_arguments
        )

        # Valid arguments
        class MockArgs:
            scan_path = "."
            tool_specific_arg = "value"

        is_valid, message = validate_arguments(MockArgs())
        assert is_valid is True

        # Add more test cases as needed for your specific tool provider

    def test_print_credentials(self):
        """Test print_credentials method."""
        provider = ToolProvider(
            scan_path="/test/path",
            frameworks=["framework1"]
        )
        # This should not raise any exceptions
        provider.print_credentials()

Step 14: Update Documentation

Explanation: Documentation updates ensure that users can find information about your tool provider in Prowler’s documentation. This includes examples, configuration guides, and troubleshooting information. Update the provider documentation to include your new tool provider in the examples and implementation guidance.

Step 2: Integrate the Provider in the API

This step is required only if you want your provider to be available in the API and UI. The API integration involves several components:

2.1. Backend API Models

Location: api/src/backend/api/models.py Add your provider to the ProviderChoices enum and implement UID validation:
class ProviderChoices(models.TextChoices):
    AWS = "aws", "AWS"
    AZURE = "azure", "Azure"
    GCP = "gcp", "GCP"
    KUBERNETES = "kubernetes", "Kubernetes"
    M365 = "m365", "Microsoft 365"
    GITHUB = "github", "GitHub"
    NHN = "nhn", "NHN Cloud"
    IAC = "iac", "Infrastructure as Code"
    YOUR_PROVIDER = "your_provider", "Your Provider"  # Add your provider here

@staticmethod
def validate_your_provider_uid(value):
    """Validate your provider UID format."""
    if not re.match(r"^your-regex-pattern$", value):
        raise ModelValidationError(
            detail="Your provider UID must follow the specified format.",
            code="your-provider-uid",
            pointer="/data/attributes/uid",
        )
Provider Model: The Provider model already exists and supports all provider types. Ensure your provider type is included in the choices.

2.2. Add the provider to the Provider Choices

Update the return_prowler_provider function to include your provider. This function is crucial for the API to instantiate the correct provider class. File: api/src/backend/api/utils.py
from prowler.providers.your_provider.your_provider import YourProvider  # Add your import

def return_prowler_provider(
    provider: Provider,
) -> [
    AwsProvider
    | AzureProvider
    | GcpProvider
    | GithubProvider
    | KubernetesProvider
    | M365Provider
    | YourProvider  # Add your provider to the return type annotation
]:
    """Return the Prowler provider class based on the given provider type."""
    match provider.provider:
        case Provider.ProviderChoices.AWS.value:
            prowler_provider = AwsProvider
        case Provider.ProviderChoices.AZURE.value:
            prowler_provider = AzureProvider
        case Provider.ProviderChoices.GCP.value:
            prowler_provider = GcpProvider
        case Provider.ProviderChoices.KUBERNETES.value:
            prowler_provider = KubernetesProvider
        case Provider.ProviderChoices.M365.value:
            prowler_provider = M365Provider
        case Provider.ProviderChoices.GITHUB.value:
            prowler_provider = GithubProvider
        case Provider.ProviderChoices.YOUR_PROVIDER.value:  # Add your provider here
            prowler_provider = YourProvider
        case _:
            raise ValueError(f"Provider type {provider.provider} not supported")
    return prowler_provider
Also update the initialize_prowler_provider function:
def initialize_prowler_provider(
    provider: Provider,
    mutelist_processor: Processor | None = None,
) -> (
    AwsProvider
    | AzureProvider
    | GcpProvider
    | GithubProvider
    | KubernetesProvider
    | M365Provider
    | YourProvider  # Add your provider to the return type annotation
):
    """Initialize a Prowler provider instance based on the given provider type."""
    prowler_provider = return_prowler_provider(provider)
    prowler_provider_kwargs = get_prowler_provider_kwargs(provider, mutelist_processor)
    return prowler_provider(**prowler_provider_kwargs)
Note: The match statement requires Python 3.10+. If you’re using an older version, you can use traditional if-elif statements instead.

2.3. API Serializers

Create or update serializers for your provider. You’ll need to add your provider to the validation logic: File: api/src/backend/api/v1/serializers.py
def validate_secret_based_on_provider(provider_type, secret):
    """Validate provider-specific secrets."""
    if provider_type == Provider.ProviderChoices.AWS.value:
        serializer = AWSProviderSecret(data=secret)
    elif provider_type == Provider.ProviderChoices.AZURE.value:
        serializer = AzureProviderSecret(data=secret)
    elif provider_type == Provider.ProviderChoices.GCP.value:
        serializer = GCPProviderSecret(data=secret)
    elif provider_type == Provider.ProviderChoices.YOUR_PROVIDER.value:  # Add your provider here
        serializer = YourProviderSecret(data=secret)
    # ... other providers

    if serializer.is_valid():
        return serializer.validated_data
    else:
        raise serializers.ValidationError(serializer.errors)

class YourProviderSecret(serializers.Serializer):
    """Serializer for your provider credentials."""
    your_auth_field = serializers.CharField(required=True)
    your_optional_field = serializers.CharField(required=False)

    class Meta:
        resource_name = "provider-secrets"
Also update the providers included in the serializer: File: api/src/backend/api/v1/serializer_utils/providers.py
@extend_schema_field(
    {
        "oneOf": [
            # ... existing provider schemas ...
            {
                "type": "object",
                "title": "Your Provider Credentials",
                "properties": {
                    "your_auth_field": {
                        "type": "string",
                        "description": "Your provider authentication field description.",
                    },
                    "your_optional_field": {
                        "type": "string",
                        "description": "Optional field for your provider (if applicable).",
                    },
                    "your_required_field": {
                        "type": "string",
                        "description": "Required field for your provider authentication.",
                    }
                },
                "required": ["your_auth_field", "your_required_field"]
            },
            # ... other existing schemas ...
        ]
    }
)

2.4. Database Migration

Create a new migration to add your provider to the database. This is crucial for the API to recognize your provider type. File: api/src/backend/api/migrations/XXXX_your_provider.py
# Generated by Django X.X.X on YYYY-MM-DD

from django.db import migrations

import api.db_utils


class Migration(migrations.Migration):
    dependencies = [
        ("api", "previous_migration_name"),  # Update this to the latest migration
    ]

    operations = [
        migrations.AlterField(
            model_name="provider",
            name="provider",
            field=api.db_utils.ProviderEnumField(
                choices=[
                    ("aws", "AWS"),
                    ("azure", "Azure"),
                    ("gcp", "GCP"),
                    ("kubernetes", "Kubernetes"),
                    ("m365", "M365"),
                    ("github", "GitHub"),
                    ("your_provider", "Your Provider"),  # Add your provider here
                ],
                default="aws",
            ),
        ),
        migrations.RunSQL(
            "ALTER TYPE provider ADD VALUE IF NOT EXISTS 'your_provider';",
            reverse_sql=migrations.RunSQL.noop,
        ),
    ]
Important Notes:
  • Migration Number: Use the next sequential number (e.g., if latest is 0044, use 0045)
  • Dependencies: Update the dependencies list to point to the most recent migration
  • Choices Array: Add your provider to the choices array with proper display name
  • SQL Operation: The RunSQL operation adds your provider to the PostgreSQL enum type
  • Reverse SQL: Use migrations.RunSQL.noop since adding enum values cannot be easily reversed
Migration Naming Convention:
  • Format: XXXX_your_provider.py (e.g., 0045_your_provider.py)
  • Use descriptive names that indicate what the migration does
  • Follow the existing pattern in the migrations folder

2.5. Update the V1 Yaml

Update the OpenAPI specification (v1.yaml) to include your provider in all relevant endpoints and schemas. This is crucial for API documentation and client generation. File: api/src/backend/api/specs/v1.yaml

2.5.1. Provider Enum Values

Add your provider to the provider enum in the Provider schema:
# Around line 12150 in v1.yaml
Provider:
  type: object
  properties:
    attributes:
      properties:
        provider:
          enum:
          - aws
          - azure
          - gcp
          - kubernetes
          - m365
          - github
          - your_provider  # Add your provider here
          type: string
          description: |-
            * `aws` - AWS
            * `azure` - Azure
            * `gcp` - GCP
            * `kubernetes` - Kubernetes
            * `m365` - M365
            * `github` - GitHub
            * `your_provider` - Your Provider  # Add your provider here

2.5.2. Provider Credential Schemas

Add your provider’s credential schema to the integration configuration. This defines how your provider’s credentials are structured:
# Around line 11100 in v1.yaml, in the integration configuration
- type: object
  title: Your Provider Credentials  # Add your provider here
  properties:
    your_auth_field:
      type: string
      description: Your provider authentication field description.
    your_optional_field:
      type: string
      description: Optional field for your provider (if applicable).
    your_required_field:
      type: string
      description: Required field for your provider authentication.
  required:
  - your_auth_field
  - your_required_field

2.5.3. Example Provider Schemas

Here are examples of how existing providers are documented: AWS Provider:
- type: object
  title: AWS Static Credentials
  properties:
    aws_access_key_id:
      type: string
      description: The AWS access key ID.
    aws_secret_access_key:
      type: string
      description: The AWS secret access key.
  required:
  - aws_access_key_id
  - aws_secret_access_key

- type: object
  title: AWS Assume Role
  properties:
    role_arn:
      type: string
      description: The Amazon Resource Name (ARN) of the role to assume.
    external_id:
      type: string
      description: An identifier to enhance security for role assumption.
  required:
  - role_arn
  - external_id
GitHub Provider:
- type: object
  title: GitHub Personal Access Token
  properties:
    personal_access_token:
      type: string
      description: GitHub personal access token for authentication.
  required:
  - personal_access_token

- type: object
  title: GitHub OAuth App Token
  properties:
    oauth_app_token:
      type: string
      description: GitHub OAuth App token for authentication.
  required:
  - oauth_app_token
M365 Provider:
- type: object
  title: M365 Static Credentials
  properties:
    client_id:
      type: string
      description: The Azure application (client) ID for authentication in Azure AD.
    client_secret:
      type: string
      description: The client secret associated with the application (client) ID.
    tenant_id:
      type: string
      description: The Azure tenant ID, representing the directory where the application is registered.
    user:
      type: email
      description: User microsoft email address.
    password:
      type: string
      description: User password.
  required:
  - client_id
  - client_secret
  - tenant_id
  - user
  - password

2.5.4. Important Notes

  • Position: Add your schema in the oneOf array alongside existing providers
  • Structure: Follow the exact pattern of other providers (title, properties, required fields)
  • Descriptions: Provide clear, helpful descriptions for each field
  • Required Fields: Specify which fields are mandatory in the required array
  • Field Types: Use appropriate JSON schema types (string, integer, boolean, email, etc.)
  • Validation: Add any field-specific validation patterns or constraints
  • Documentation: Ensure your provider appears in the generated API documentation

2.6. Testing API Integration

Create tests for your provider: Location: api/src/backend/api/tests/
class YourProviderAPITestCase(APITestCase):
    def setUp(self):
        self.user = User.objects.create_user(username='testuser', password='testpass')
        self.client.force_authenticate(user=self.user)

    def test_create_your_provider(self):
        data = {
            'provider': 'your_provider',
            'uid': 'valid-uid-123',
            'alias': 'Test Account'
        }
        response = self.client.post('/api/v1/providers/', data)
        self.assertEqual(response.status_code, 201)
        self.assertEqual(response.data['provider'], 'your_provider')

    def test_your_provider_uid_validation(self):
        """Test UID validation for your provider."""
        invalid_uids = [
            'invalid@uid',
            '-invalid-start',
            'a' * 40,  # Too long
        ]

        for invalid_uid in invalid_uids:
            data = {
                'provider': 'your_provider',
                'uid': invalid_uid,
                'alias': 'Test'
            }
            response = self.client.post('/api/v1/providers/', data)
            self.assertEqual(response.status_code, 400)
            self.assertIn('your-provider-uid', str(response.data))

    def test_add_your_provider_credentials(self):
        # Create provider first
        provider = Provider.objects.create(
            user=self.user,
            provider='your_provider',
            uid='valid-uid-123'
        )

        # Add credentials
        credentials_data = {
            'secret_type': 'your_provider_credentials',
            'secret': {
                'your_auth_field': 'auth_value',
                'your_optional_field': 'optional_value'
            },
            'provider': provider.id
        }
        response = self.client.post('/api/v1/providers/secrets/', credentials_data)
        self.assertEqual(response.status_code, 201)

2.6.1. Add your mocked provider to the tests

If needed, add your mocked provider to the tests config file so you can use it on the tests. File: api/src/backend/conftest.py
@pytest.fixture
def providers_fixture(tenants_fixture):
    tenant, *_ = tenants_fixture
    providerX = Provider.objects.create(
        provider="your_provider",
        uid="your_uid",
        alias="your_alias",
        tenant_id=tenant.id,
    )
    return provider1, provider2, provider3, ... providerX

2.7. Compliance and Output Support

Add your provider to the compliance export functionality: File: api/src/backend/tasks/jobs/export.py
COMPLIANCE_FRAMEWORKS = {
    "aws": [...],
    "azure": [...],
    "gcp": [...],
    "kubernetes": [...],
    "m365": [...],
    "github": [...],
    "your_provider": [  # Add your provider here
        (lambda name: name.startswith("cis_"), YourProviderCIS),
        (lambda name: name.startswith("iso27001_"), YourProviderISO27001),
    ],
}
If your provider has specific fields, add them to the finding transformation: File: prowler/lib/outputs/finding.py
def transform_api_finding(cls, finding, provider) -> "Finding":
    # ... existing code ...

    # Your provider specific field
    if provider.type == "your_provider":
        finding.your_field = resource.your_field

    # ... rest of the code ...

2.8. API Endpoints

Your provider will be available through these endpoints:
  • GET /api/v1/providers/ - List all providers
  • POST /api/v1/providers/ - Create a new provider
  • GET /api/v1/providers/{id}/ - Get provider details
  • PUT /api/v1/providers/{id}/ - Update provider
  • DELETE /api/v1/providers/{id}/ - Delete provider
  • POST /api/v1/providers/secrets/ - Add provider credentials

2.9. Update the provider if needed

Depending on your provider’s authentication requirements, you may need to add new authentication methods that are compatible with the API. This involves updating the provider class to support additional credential types beyond the basic ones.

2.9.1. Adding New Authentication Methods

If your provider requires specific authentication methods, you’ll need to:
  1. Update the provider constructor to accept new authentication parameters
  2. Extend the credential handling to support the new authentication method
  3. Update the API serializers to include the new credential fields
  4. Modify the OpenAPI specification to document the new authentication schema

2.9.2. Example: GitHub Provider Authentication Methods

The GitHub provider demonstrates how to implement multiple authentication methods:
# In prowler/providers/github/github_provider.py
def __init__(
    self,
    # Authentication methods
    personal_access_token: str = "",
    oauth_app_token: str = "",
    github_app_key: str = "",
    #Needed for the API integration
    github_app_key_content: str = "",
    github_app_id: int = 0,
    # Provider configuration
    config_path: str = None,
    # ... other parameters
):
    """
    Initialize GitHub provider.

    Args:
        personal_access_token (str): GitHub personal access token.
        oauth_app_token (str): GitHub OAuth App token.
        github_app_key (str): GitHub App key.
        github_app_key_content (str): GitHub App key content.
        github_app_id (int): GitHub App ID.
        config_path (str): Path to the audit configuration file.
        # ... other parameters
    """
    super().__init__(
        personal_access_token,
        oauth_app_token,
        github_app_id,
        github_app_key,
        github_app_key_content,
    )

Step 3: Integrate the Provider in the UI

TBD

Provider Implementation Guidance

Use existing providers as templates, this will help you to understand better the structure and the implementation will be easier:

Best Practices

  • Code Quality & Documentation
    • Comprehensive Docstrings: Every class, method, and function should have detailed docstrings following Prowler’s format
    def method_name(self, param: str) -> str:
        """
        Brief description of what the method does.
    
        Args:
            param: Description of the parameter
    
        Returns:
            Description of the return value
    
        Raises:
            ExceptionType: When and why this exception occurs
        """
    
    • Type Hints: Use type hints for all function parameters and return values
    from typing import Optional, List, Dict, Any
    
    • Logging: Implement proper logging using Prowler’s logger
    from prowler.lib.logger import logger
    
    logger.info("Operation completed successfully")
    logger.warning("Something to be aware of")
    logger.error("Something went wrong")
    logger.critical("Critical error that may cause failure")
    
  • Error Handling & Validation
    • Custom Exceptions: Create provider-specific exceptions for better error handling
    • Input Validation: Validate all inputs and provide clear error messages
    • Graceful Degradation: Handle errors gracefully without crashing the entire scan
    • Raise on Exception: Use raise_on_exception parameter for test methods
  • Testing & Quality Assurance
    • Comprehensive Test Coverage: Aim for >80% test coverage
    • Test Naming: Use descriptive test names: test_method_name_scenario
    • Test Organization: Group related tests in test classes
    • Mock External Dependencies: Mock external API calls and services
    • Test Edge Cases: Include tests for error conditions and edge cases
    • End-to-End Testing: Test the provider on real infrastructure
  • Performance & Security
    • Session Management: Reuse sessions when possible, don’t create new ones unnecessarily
    • Rate Limiting: Implement rate limiting for API calls to avoid hitting limits
    • Resource Cleanup: Ensure proper cleanup of temporary resources
    • Authentication Security: Never log sensitive credentials or tokens
  • Code Organization
    • Single Responsibility: Each method should have one clear purpose
    • Consistent Naming: Follow Prowler’s naming conventions
    • Modular Design: Break complex functionality into smaller, testable methods
    • Configuration Management: Use configuration files for provider-specific settings
  • Documentation & Maintenance
    • README Updates: Update provider-specific documentation
    • Changelog: Document changes and new features
    • Examples: Provide usage examples and common scenarios
    • Troubleshooting: Include common issues and solutions
    • Documentation: Update the provider documentation to include your new tool provider in the examples and implementation guidance.
  • Integration Standards
    • CLI Consistency: Follow Prowler’s CLI argument patterns
    • Output Format: Ensure outputs are compatible with Prowler’s reporting system
    • Compliance Mapping: Map provider checks to relevant compliance frameworks
    • Backward Compatibility: Maintain compatibility when possible
  • AI-Assisted Development
    • Use Rules: Use rules to ensure the code generated by AI is following the way of working in Prowler.

Checklist for New Providers

CLI Integration Only

Phase 1: Research & Planning
  • Soft research completed
  • Spike date scheduled
  • Deeper research completed
  • Action plan created
Phase 2: Implementation
  • Folder and files created in prowler/providers/<name>
  • Provider class implemented and inherits from Provider
  • Authentication/session logic implemented
  • Arguments/flags mapped and documented
  • Outputs and metadata standardized
  • Registered in the CLI
  • Minimal usage example provided
Phase 3: Delivery
  • PoC delivered
  • MVP delivered
  • Version 1 completed
  • QA and documentation completed
  • GA release ready

API Integration

  • All CLI integration items completed
  • Provider added to ProviderChoices enum in API models
  • API serializers created/updated for the provider
  • API views support the new provider type
  • Provider credentials model supports the new provider
  • API endpoints tested and working
  • Provider-specific validation implemented
  • API tests created and passing

UI Integration

  • TBD

Next Steps

I