---
title: 'Prowler Checks'
---

This guide explains how to create new checks in Prowler.

## Introduction

Checks are the core component of Prowler. A check is a piece of code designed to validate whether a configuration aligns with cybersecurity best practices. Execution of a check yields a finding, which includes the result and contextual metadata (e.g., outcome, risks, remediation).

### Creating a Check

The most common high level steps to create a new check are:

1. Prerequisites:
    - Verify the check does not already exist by searching [Prowler Hub](https://hub.prowler.com) or checking `prowler/providers/<provider>/services/<service>/<check_name_want_to_implement>/`.
    - Ensure required provider and service exist. If not, follow the [Provider](/developer-guide/provider) and [Service](/developer-guide/services) documentation to create them.
    - Confirm the service has implemented all required methods and attributes for the check (in most cases, you will need to add or modify some methods in the service to get the data you need for the check).
2. Navigate to the service directory. The path should be as follows: `prowler/providers/<provider>/services/<service>`.
3. Create a check-specific folder. The path should follow this pattern: `prowler/providers/<provider>/services/<service>/<check_name_want_to_implement>`. Adhere to the [Naming Format for Checks](#naming-format-for-checks).
4. Populate the folder with files as specified in [File Creation](#file-creation).
5. Run the check locally to ensure it works as expected. For checking you can use the CLI in the next way:
    - To ensure the check has been detected by Prowler: `uv run python prowler-cli.py <provider> --list-checks | grep <check_name>`.
    - To run the check, to find possible issues: `uv run python prowler-cli.py <provider> --log-level ERROR --verbose --check <check_name>`.
6. Create comprehensive tests for the check that cover multiple scenarios including both PASS (compliant) and FAIL (non-compliant) cases. For detailed information about test structure and implementation guidelines, refer to the [Testing](/developer-guide/unit-testing) documentation.
7. If the check and its corresponding tests are working as expected, you can submit a PR to Prowler.

### Naming Format for Checks

If you already know the check name when creating a request or implementing a check, use a descriptive identifier with lowercase letters and underscores only.

Recommended patterns:

- `<service>_<resource>_<best_practice>`

The name components are:

- `service` – The main service or product area being audited (e.g., ec2, entra, iam, bedrock).
- `resource` – The resource, feature, or configuration being evaluated. It can be a single word or a compound phrase joined with underscores (e.g., instance, policy, guardrail, sensitive_information_filter).
- `best_practice` – The expected secure state or best practice being checked (e.g., enabled, encrypted, restricted, configured, not_publicly_accessible).

Additional guidance:

- Use underscores only. Do not use hyphens.
- Keep the name specific enough to describe the behavior of the check.
- The first segment should match the service or product area whenever possible.

Examples:

- `s3_bucket_versioning_enabled`
- `bedrock_guardrail_sensitive_information_filter_enabled`

### File Creation

Each check in Prowler follows a straightforward structure. Within the newly created folder, three files must be added to implement the check logic:

- `__init__.py` (empty file) – Ensures Python treats the check folder as a package.
- `<check_name>.py` (code file) – Contains the check logic, following the prescribed format. Please refer to the [prowler's check code structure](/developer-guide/checks#prowlers-check-code-structure) for more information.
- `<check_name>.metadata.json` (metadata file) – Defines the check's metadata for contextual information. Please refer to the [check metadata](/developer-guide/checks#metadata-structure-for-prowler-checks) for more information.

## Prowler's Check Code Structure

Prowler's check structure is designed for clarity and maintainability. It follows a dynamic loading approach based on predefined paths, ensuring seamless integration of new checks into a provider's service without additional manual steps.

Below the code for a generic check is presented. It is strongly recommended to consult other checks from the same provider and service to understand provider-specific details and patterns. This will help ensure consistency and proper implementation of provider-specific requirements.

Report fields are the most dependent on the provider, consult the `CheckReport<Provider>` class for more information on what can be included in the report [here](https://github.com/prowler-cloud/prowler/blob/master/prowler/lib/check/models.py).

<Note>
Legacy providers (AWS, Azure, GCP, Kubernetes) follow the `Check_Report_<Provider>` naming convention. This is not recommended for current instances. Newer providers adopt the `CheckReport<Provider>` naming convention. Learn more at [Prowler Code](https://github.com/prowler-cloud/prowler/tree/master/prowler/lib/check/models.py).

</Note>
```python title="Generic Check Class"
# Required Imports
# Import the base Check class and the provider-specific CheckReport class
from prowler.lib.check.models import Check, CheckReport<Provider>
# Import the provider service client
from prowler.providers.<provider>.services.<service>.<service>_client import <service>_client

# Defining the Check Class
# Each check must be implemented as a Python class with the same name as its corresponding file.
# The class must inherit from the Check base class.
class <check_name>(Check):
    """
    Ensure that <resource> meets <security_requirement>.

    This check evaluates whether <specific_condition> to ensure <security_benefit>.
    - PASS: <description_of_compliant_state(s)>.
    - FAIL: <description_of_non_compliant_state(s)>.
    """

    def execute(self):
        """Execute the check logic.

        Returns:
            A list of reports containing the result of the check.
        """
        findings = []
        # Iterate over the target resources using the provider service client
        for resource in <service>_client.<resources>:
            # Initialize the provider-specific report class, passing metadata and resource
            report = Check_Report_<Provider>(metadata=self.metadata(), resource=resource)
            # Set required fields and implement check logic
            report.status = "PASS"
            report.status_extended = f"<Description about why the resource is compliant>"
            # If some of the information needed for the report is not inside the resource, it can be set it manually here.
            # This depends on the provider and the resource that is being audited.
            # report.region = resource.region
            # report.resource_tags = getattr(resource, "tags", [])
            # ...
            # Example check logic (replace with actual logic):
            if <non_compliant_condition>:
                report.status = "FAIL"
                report.status_extended = f"<Description about why the resource is not compliant>"
            findings.append(report)
        return findings
```

### Data Requirements for Checks in Prowler

One of the most important aspects when creating a new check is ensuring that all required data is available from the service client. Often, default API calls are insufficient. Extending the service class with new methods or resource attributes may be required to fetch and store requisite data.

### Statuses for Checks in Prowler

Required Fields: status and status\_extended

Each check **must** populate the `report.status` and `report.status_extended` fields according to the following criteria:

- Status field: `report.status`
    - `PASS` – Assigned when the check confirms compliance with the configured value.
    - `FAIL` – Assigned when the check detects non-compliance with the configured value.
    - `MANUAL` – This status must not be used unless manual verification is necessary to determine whether the status (`report.status`) passes (`PASS`) or fails (`FAIL`).

- Status extended field: `report.status_extended`
    - It **must** end with a period (`.`).
    - It **must** include the audited service, the resource, and a concise explanation of the check result, for instance: `EC2 AMI ami-0123456789 is not public.`.

### Prowler's Check Severity Levels

The severity of each check is defined in the metadata file using the `Severity` field. Severity values are always lowercase and must be one of the predefined categories below.

- `critical` – Highest potential impact with broad exposure that could affect core security boundaries or business operations.
- `high` – Substantial potential impact with significant exposure that could affect important security controls or resources.
- `medium` – Moderate potential impact with limited exposure that weakens defense layers but has contained scope.
- `low` – Minimal potential impact with negligible exposure that represents minor gaps in security posture.
- `informational` – Provides valuable information but does not affect the security posture.

If the check involves multiple scenarios that may alter its severity, adjustments can be made dynamically within the check's logic using the severity `report.check_metadata.Severity` attribute:

```python
if <generic_condition_1>:
    report.status = "PASS"
    report.check_metadata.Severity = "informational"
    report.status_extended = f"<Resource> is compliant with <requirement>."
elif <generic_condition_2>:
    report.status = "FAIL"
    report.check_metadata.Severity = "low"
    report.status_extended = f"<Resource> is not compliant with <requirement>: <reason>."
elif <generic_condition_3>:
    report.status = "FAIL"
    report.check_metadata.Severity = "medium"
    report.status_extended = f"<Resource> is not compliant with <requirement>: <reason>."
elif <generic_condition_4>:
    report.status = "FAIL"
    report.check_metadata.Severity = "high"
    report.status_extended = f"<Resource> is not compliant with <requirement>: <reason>."
else:
    report.status = "FAIL"
    report.check_metadata.Severity = "critical"
    report.status_extended = f"<Resource> is not compliant with <requirement>: <critical reason>."
```

### Resource Identification in Prowler

Each check **must** populate the report with an unique identifier for the audited resource. This identifier or identifiers are going to depend on the provider and the resource that is being audited. Here are the criteria for each provider:

- AWS
    - Amazon Resource ID — `report.resource_id`.
        - The resource identifier. This is the name of the resource, the ID of the resource, or a resource path. Some resource identifiers include a parent resource (sub-resource-type/parent-resource/sub-resource) or a qualifier such as a version (resource-type:resource-name:qualifier).
        - If the resource ID cannot be retrieved directly from the audited resource, it can be extracted from the ARN. It is the last part of the ARN after the last slash (`/`) or colon (`:`).
        - If no actual resource to audit exists, this format can be used: `<resource_type>/unknown`
    - Amazon Resource Name — `report.resource_arn`.
        - The [Amazon Resource Name (ARN)](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html) of the audited entity.
        - If the ARN cannot be retrieved directly from the audited resource, construct a valid ARN using the `resource_id` component as the audited entity. Examples:
            - Bedrock — `arn:<partition>:bedrock:<region>:<account-id>:model-invocation-logging`.
            - DirectConnect — `arn:<partition>:directconnect:<region>:<account-id>:dxcon`.
        - If no actual resource to audit exists, this format can be used: `arn:<partition>:<service>:<region>:<account-id>:<resource_type>/unknown`.
            - Examples:
                - AWS Security Hub — `arn:<partition>:security-hub:<region>:<account-id>:hub/unknown`.
                - Access Analyzer — `arn:<partition>:access-analyzer:<region>:<account-id>:analyzer/unknown`.
                - GuardDuty — `arn:<partition>:guardduty:<region>:<account-id>:detector/unknown`.
- GCP
    - Resource ID — `report.resource_id`.
        - Resource ID represents the full, [unambiguous path to a resource](https://google.aip.dev/122#full-resource-names), known as the full resource name. Typically, it follows the format: `//{api_service/resource_path}`.
        - If the resource ID cannot be retrieved directly from the audited resource, by default the resource name is used.
    - Resource Name — `report.resource_name`.
        - Resource Name usually refers to the name of a resource within its service.
- Azure
    - Resource ID — `report.resource_id`.
        - Resource ID represents the full Azure Resource Manager path to a resource, which follows the format: `/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}`.
    - Resource Name — `report.resource_name`.
        - Resource Name usually refers to the name of a resource within its service.
        - If the [resource name](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules) cannot be retrieved directly from the audited resource, the last part of the resource ID can be used.
- Kubernetes
    - Resource ID — `report.resource_id`.
        - The UID of the Kubernetes object. This is a system-generated string that uniquely identifies the object within the cluster for its entire lifetime. See [Kubernetes Object Names and IDs - UIDs](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids).
    - Resource Name — `report.resource_name`.
        - The name of the Kubernetes object. This is a client-provided string that must be unique for the resource type within a namespace (for namespaced resources) or cluster (for cluster-scoped resources). Names typically follow DNS subdomain or label conventions. See [Kubernetes Object Names and IDs - Names](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).
- M365
    - Resource ID — `report.resource_id`.
        - If the audited resource has a globally unique identifier such as a `guid`, use it as the `resource_id`.
        - If no `guid` exists, use another unique and relevant identifier for the resource, such as the tenant domain, the internal policy ID, or a representative string following the format `<resource_type>/<name_or_id>`.
    - Resource Name — `report.resource_name`.
        - Use the visible or descriptive name of the audited resource. If no explicit name is available, use a clear description of the resource or configuration being evaluated.
    - Examples:
        - For an organization:
            - `resource_id`: Organization GUID
            - `resource_name`: Organization name
        - For a policy:
            - `resource_id`: Unique policy ID
            - `resource_name`: Policy display name
        - For global configurations:
            - `resource_id`: Tenant domain or representative string (e.g., "userSettings")
            - `resource_name`: Description of the configuration (e.g., "SharePoint Settings")
- GitHub
    - Resource ID — `report.resource_id`.
        - The ID of the Github resource. This is a system-generated integer that uniquely identifies the resource within the Github platform.
    - Resource Name — `report.resource_name`.
        - The name of the Github resource. In the case of a repository, this is just the repository name. For full repository names use the resource `full_name`.

### Configurable Checks in Prowler

See [Configurable Checks](/developer-guide/configurable-checks) for detailed information on making checks configurable using the `audit_config` object and configuration file.

## Metadata Structure for Prowler Checks

Each Prowler check must include a metadata file named `<check_name>.metadata.json` that must be located in its directory. This file supplies crucial information for execution, reporting, and context.

### Example Metadata File

Below is a generic example of a check metadata file. **Do not include comments in actual JSON files.**

```json
{
  "Provider": "aws",
  "CheckID": "service_resource_security_setting",
  "CheckTitle": "Service resource has security setting enabled",
  "CheckType": [],
  "ServiceName": "service",
  "SubServiceName": "",
  "ResourceIdTemplate": "",
  "Severity": "medium",
  "ResourceType": "Other",
  "ResourceGroup": "security",
  "Description": "This check verifies that the service resource has the required **security setting** enabled to protect against potential vulnerabilities.\n\nIt ensures that the resource follows security best practices and maintains proper access controls. The check evaluates whether the security configuration is properly implemented and active.",
  "Risk": "Without proper security settings, the resource may be vulnerable to:\n\n- **Unauthorized access** - Malicious actors could gain entry\n- **Data breaches** - Sensitive information could be compromised\n- **Security threats** - Various attack vectors could be exploited\n\nThis could result in compliance violations and potential financial or reputational damage.",
  "RelatedUrl": "",
  "AdditionalURLs": ["https://example.com/security-documentation", "https://example.com/best-practices"],
  "Remediation": {
    "Code": {
      "CLI": "provider-cli service enable-security-setting --resource-id resource-123",
      "NativeIaC": "```yaml\nType: Provider::Service::Resource\nProperties:\n  SecuritySetting: enabled\n  ResourceId: resource-123\n```",
      "Other": "1. Open the provider management console\n2. Navigate to the service section\n3. Select the resource\n4. Enable the security setting\n5. Save the configuration",
      "Terraform": "```hcl\nresource \"provider_service_resource\" \"example\" {\n  resource_id      = \"resource-123\"\n  security_setting = true\n}\n```"
    },
    "Recommendation": {
      "Text": "Enable security settings on all service resources to ensure proper protection. Regularly review and update security configurations to align with current best practices.",
      "Url": "https://hub.prowler.com/check/service_resource_security_setting"
    }
  },
  "Categories": ["internet-exposed", "secrets"],
  "DependsOn": [],
  "RelatedTo": ["service_resource_security_setting", "service_resource_security_setting_2"],
  "Notes": "This is a generic example check that should be customized for specific provider and service requirements."
}
```

### Metadata Fields and Their Purpose

#### Provider

The Prowler provider related to the check. The name **must** be lowercase and match the provider folder name. For supported providers refer to [Prowler Hub](https://hub.prowler.com/check) or directly to [Prowler Code](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers).

#### CheckID

The unique identifier for the check inside the provider. This field **must** match the check's folder, Python file, and JSON metadata file name. For more information about naming, refer to the [Naming Format for Checks](#naming-format-for-checks) section.

#### CheckTitle

The `CheckTitle` field must be plain text, clearly and succinctly define **the best practice being evaluated and which resource(s) each finding applies to**. The title should be specific, concise (no more than 150 characters), and reference the relevant resource(s) involved.

**Always write the `CheckTitle` to describe the *PASS* case**, the desired secure or compliant state of the resource(s). This helps ensure that findings are easy to interpret and that the title always reflects the best practice being met.

For detailed guidelines on writing effective check titles, including how to determine singular vs. plural scope and common mistakes to avoid, see [Check Title Guidelines](/developer-guide/check-metadata-guidelines#check-title-guidelines).

#### CheckType

<Warning>
This field is only applicable to the AWS provider.

</Warning>
It follows the [AWS Security Hub Types](https://docs.aws.amazon.com/securityhub/latest/userguide/asff-required-attributes.html#Types) format using the pattern `namespace/category/classifier`.

For the complete AWS Security Hub selection guidelines, see [Check Type Guidelines](/developer-guide/check-metadata-guidelines#check-type-guidelines-aws-only).

#### ServiceName

The name of the provider service being audited. Must be lowercase and match the service folder name. For supported services refer to [Prowler Hub](https://hub.prowler.com/check) or the [Prowler Code](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers).

#### SubServiceName

This field is in the process of being deprecated and should be **left empty**.

#### ResourceIdTemplate

This field is in the process of being deprecated and should be **left empty**.

#### Severity

Severity level if the check fails. Must be one of: `critical`, `high`, `medium`, `low`, or `informational`, and written in lowercase. See [Prowler's Check Severity Levels](#prowlers-check-severity-levels) for details.

#### ResourceType

The type of resource being audited. This field helps categorize and organize findings by resource type for better analysis and reporting. For each provider:

- **AWS**: Use [Security Hub resource types](https://docs.aws.amazon.com/securityhub/latest/userguide/asff-resources.html) or PascalCase CloudFormation types removing the `::` separator used in CloudFormation templates (e.g., in CloudFormation template the type of an EC2 instance is `AWS::EC2::Instance` but in the check it should be `AwsEc2Instance`). Use `Other` if none apply.
- **Azure**: Use types from [Azure Resource Graph](https://learn.microsoft.com/en-us/azure/governance/resource-graph/reference/supported-tables-resources), for example: `Microsoft.Storage/storageAccounts`.
- **Google Cloud**: Use [Cloud Asset Inventory asset types](https://cloud.google.com/asset-inventory/docs/asset-types), for example: `compute.googleapis.com/Instance`.
- **Kubernetes**: Use types shown under `KIND` from `kubectl api-resources`.
- **Oracle Cloud Infrastructure**: Use types from [Oracle Cloud Infrastructure documentation](https://docs.public.oneportal.content.oci.oraclecloud.com/en-us/iaas/Content/Search/Tasks/queryingresources_topic-Listing_Supported_Resource_Types.htm).
- **OpenStack**: Use types from [OpenStack Heat resource types](https://docs.openstack.org/heat/latest/template_guide/openstack.html).
- **Alibaba Cloud**: Use types from [Alibaba Cloud ROS resource types](https://www.alibabacloud.com/help/en/ros/developer-reference/list-of-resource-types-by-service).
- **Any other provider**: Use `NotDefined` due to lack of standardized resource types in their SDK or documentation.

#### ResourceGroup

A high-level classification that groups checks by the type of cloud resource they audit. This field enables filtering and organizing findings by resource category across all providers. The value must be one of the following predefined groups:

| Group | Description |
|-------|-------------|
| `compute` | Virtual machines, instances, auto-scaling groups, workspaces, streaming |
| `container` | Container orchestration, Kubernetes, registries, pods |
| `serverless` | Functions, step functions, event-driven compute |
| `database` | Relational, NoSQL, caches, search engines, data warehouses, graph databases |
| `storage` | Object storage, block storage, file systems, backups, archives |
| `network` | VPCs, subnets, load balancers, DNS, VPN, firewalls, CDN |
| `IAM` | IAM users, roles, policies, access keys, service accounts, directories |
| `messaging` | Queues, topics, event buses, streaming, email services |
| `security` | WAF, secrets, KMS, certificates, security tools, defenders, DDoS protection |
| `monitoring` | Logs, metrics, alerts, audit trails, observability, config tracking |
| `api_gateway` | API management, REST APIs, GraphQL endpoints |
| `ai_ml` | Machine learning, AI services, notebooks, training, LLM |
| `governance` | Accounts, organizations, projects, policies, settings, compliance tools |
| `collaboration` | Productivity SaaS apps (Exchange, Teams, SharePoint) |
| `devops` | CI/CD, infrastructure as code, automation, code repositories, version control |
| `analytics` | Data warehouses, query engines, ETL pipelines, BI tools, data lakes |

The group is determined by the resource type being audited, not the service. For example, an EC2 security group check would use `network` (not `compute`), while an EC2 instance check would use `compute`.

#### Description

A concise, natural language explanation that **clearly describes what the finding means**, focusing on clarity and context rather than technical implementation details. Use simple paragraphs with line breaks if needed, but avoid sections, code blocks, or complex formatting. This field is limited to maximum 400 characters.

For detailed writing guidelines and common mistakes to avoid, see [Description Guidelines](/developer-guide/check-metadata-guidelines#description-guidelines).

#### Risk

A clear, natural language explanation of **why this finding poses a cybersecurity risk**. Focus on how it may impact confidentiality, integrity, or availability. If those do not apply, describe any relevant operational or financial risks. Use simple paragraphs with line breaks if needed, but avoid sections, code blocks, or complex formatting. Limit your explanation to 400 characters.

For detailed writing guidelines and common mistakes to avoid, see [Risk Guidelines](/developer-guide/check-metadata-guidelines#risk-guidelines).

#### RelatedUrl

*Deprecated*. Use `AdditionalURLs` for adding your URLs references.

#### AdditionalURLs

<Warning>
URLs must be valid and not repeated.

</Warning>
A list of official documentation URLs for further reading. These should be authoritative sources that provide additional context, best practices, or detailed information about the security control being checked. Prefer official provider documentation, security standards, or well-established security resources. Avoid third-party blogs or unofficial sources unless they are highly reputable and directly relevant.

#### Remediation

Provides both code examples and best practice recommendations for addressing the security issue.

- **Code**: Contains remediation examples in different formats:
    - **CLI**: Command-line interface commands to make the finding compliant in runtime.
    - **NativeIaC**: Native Infrastructure as Code templates with an example of a compliant configuration. For now it applies to:
        - **AWS**: CloudFormation YAML formatted code (do not use JSON format).
        - **Azure**: Bicep formatted code (do not use ARM templates).
    - **Terraform**: HashiCorp Configuration Language (HCL) code with an example of a compliant configuration.
    - **Other**: Manual steps through web interfaces or other tools to make the finding compliant.

    For detailed guidelines on writing remediation code, see [Remediation Code Guidelines](/developer-guide/check-metadata-guidelines#remediation-code-guidelines).

- **Recommendation**
    - **Text**: Generic best practice guidance in natural language using Markdown format (maximum 400 characters). For writing guidelines, see [Recommendation Guidelines](/developer-guide/check-metadata-guidelines#recommendation-guidelines).
    - **Url**: [Prowler Hub URL](https://hub.prowler.com/) of the check. This URL is always composed by `https://hub.prowler.com/check/<check_id>`.

#### Categories

One or more functional groupings used for execution filtering (e.g., `internet-exposed`). Categories must match the predefined values enforced by `CheckMetadata`; adding a new category requires updating the validator and the metadata documentation.

For the complete list of available categories, see [Categories Guidelines](/developer-guide/check-metadata-guidelines#categories-guidelines).

#### DependsOn

List of check IDs of checks that if are compliant, this check will be a compliant too or it is not going to give any finding.

#### RelatedTo

List of check IDs of checks that are conceptually related, even if they do not share a technical dependency.

#### Notes

Any additional information not covered in the above fields.

### Python Model Reference

The metadata structure is enforced in code using a Pydantic model. For reference, see the [`CheckMetadata`](https://github.com/prowler-cloud/prowler/blob/master/prowler/lib/check/models.py).

## Generic Check Patterns and Best Practices

### Common Patterns

- Every check is implemented as a class inheriting from `Check` (from `prowler.lib.check.models`).
- The main logic is implemented in the `execute()` method (**only method that must be implemented**), which always returns a list of provider-specific report objects (e.g., `CheckReport<Provider>`)—one per finding/resource. If there are no findings/resources, return an empty list.
- **Never** use the provider's client directly; instead, use the service client (e.g., `<service>_client`) and iterate over its resources.
- For each resource, create a provider-specific report object, populate it with metadata, resource details, status (`PASS`, `FAIL`, etc.), and a human-readable `status_extended` message.
- Use the `metadata()` method to attach check metadata to each report.
- Checks are designed to be idempotent and stateless: they do not modify resources, only report on their state.

### Best Practices

- Use clear, actionable, and user-friendly language in `status_extended` to explain the result. Always provide information to identify the resource.
- Use helper functions/utilities for repeated logic to avoid code duplication. Save them in the `lib` folder of the service.
- Handle exceptions gracefully: catch errors per resource, log them, and continue processing other resources.
- Document the check with a class and function level docstring explaining what it does, what it checks, and any caveats or provider-specific behaviors.
- Use type hints for the `execute()` method (e.g., `-> list[CheckReport<Provider>]`) for clarity and static analysis.
- Ensure checks are efficient; avoid excessive nested loops. If the complexity is high, consider refactoring the check.
- Keep the check logic focused: one check = one control/requirement. Avoid combining unrelated logic in a single check.

## Specific Check Patterns

Details for specific providers can be found in documentation pages named using the pattern `<provider_name>-details`.

Checks that scan resources for plaintext secrets follow a dedicated batched structure. Refer to [Secret-Scanning Checks](/developer-guide/secret-scanning-checks) before creating or updating one.
