docs(dev-guide): improve quality redrive (#7718)

Co-authored-by: Rubén De la Torre Vico <ruben@prowler.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
Co-authored-by: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
Co-authored-by: Rubén De la Torre Vico <rubendltv22@gmail.com>
This commit is contained in:
dcanotrad
2025-06-17 09:28:22 +02:00
committed by GitHub
parent a0d169470d
commit e8a829b75e
12 changed files with 1218 additions and 986 deletions

View File

@@ -1,370 +1,339 @@
# Create a new Check for a Provider
# Prowler Checks
Here you can find how to create new checks for Prowler.
**To create a check is required to have a Prowler provider service already created, so if the service is not present or the attribute you want to audit is not retrieved by the service, please refer to the [Service](./services.md) documentation.**
This guide explains how to create new checks in Prowler.
## Introduction
The checks are the fundamental piece of Prowler. A check is a simply piece of code that ensures if something is configured against cybersecurity best practices. Then the check generates a finding with the result and includes the check's metadata to give the user more contextual information about the result, the risk and how to remediate it.
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).
To create a new check for a supported Prowler provider, you will need to create a folder with the check name inside the specific service for the selected provider.
### Creating a Check
We are going to use the `ec2_ami_public` check from the `AWS` provider as an example. So the folder name will be `prowler/providers/aws/services/ec2/ec2_ami_public` (following the format `prowler/providers/<provider>/services/<service>/<check_name>`), with the name of check following the pattern: `service_subservice_resource_action`.
To create a new check:
- Prerequisites: A Prowler provider and service must exist. Verify support and check for pre-existing checks via [Prowler Hub](https://hub.prowler.com). If the provider or service is not present, please refer to the [Provider](./provider.md) and [Service](./services.md) documentation for creation instructions.
- Navigate to the service directory. The path should be as follows: `prowler/providers/<provider>/services/<service>`.
- Create a check-specific folder. The path should follow this pattern: `prowler/providers/<provider>/services/<service>/<check_name>`. Adhere to the [Naming Format for Checks](#naming-format-for-checks).
- Populate the folder with files as specified in [File Creation](#file-creation).
### Naming Format for Checks
Checks must be named following the format: `service_subservice_resource_action`.
The name components are:
- `service` The main service being audited (e.g., ec2, entra, iam, etc.)
- `subservice` An individual component or subset of functionality within the service that is being audited. This may correspond to a shortened version of the class attribute accessed within the check. If there is no subservice, just omit.
- `resource` The specific resource type being evaluated (e.g., instance, policy, role, etc.)
- `action` The security aspect or configuration being checked (e.g., public, encrypted, enabled, etc.)
### 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](./checks.md#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](./checks.md#) 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
A subservice is an specific component of a service that is gonna be audited. Sometimes it could be the shortened name of the class attribute that is gonna be accessed in the check.
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).
Inside that folder, we need to create three files:
```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
- An empty `__init__.py`: to make Python treat this check folder as a package.
- A `check_name.py` with the above format containing the check's logic. Refer to the [check](./checks.md#check)
- A `check_name.metadata.json` containing the check's metadata. Refer to the [check metadata](./checks.md#check-metadata)
# 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):
"""Short description of what is being checked"""
## Check
The Prowler's check structure is very simple and following it there is nothing more to do to include a check in a provider's service because the load is done dynamically based on the paths.
The following is the code for the `ec2_ami_public` check:
```python title="Check Class"
# At the top of the file we need to import the following:
# - Check class which is in charge of the following:
# - Retrieve the check metadata and expose the `metadata()`
# to return a JSON representation of the metadata,
# read more at Check Metadata Model down below.
# - Enforce that each check requires to have the `execute()` function
from prowler.lib.check.models import Check, Check_Report_AWS
# Then you have to import the provider service client
# read more at the Service documentation.
from prowler.providers.aws.services.ec2.ec2_client import ec2_client
# For each check we need to create a python class called the same as the
# file which inherits from the Check class.
class ec2_ami_public(Check):
"""ec2_ami_public verifies if an EC2 AMI is publicly shared"""
# Then, within the check's class we need to create the "execute(self)"
# function, which is enforce by the "Check" class to implement
# the Check's interface and let Prowler to run this check.
def execute(self):
"""Execute <check short description>
# Inside the execute(self) function we need to create
# the list of findings initialised to an empty list []
Returns:
List[CheckReport<Provider>]: A list of reports containing the result of the check.
"""
findings = []
# Then, using the service client we need to iterate by the resource we
# want to check, in this case EC2 AMIs stored in the
# "ec2_client.images" object.
for image in ec2_client.images:
# Once iterating for the images, we have to intialise
# the Check_Report_AWS class passing the check's metadata
# using the "metadata" function explained above.
report = Check_Report_AWS(self.metadata())
# For each Prowler check we MUST fill the following
# Check_Report_AWS fields:
# - region
# - resource_id
# - resource_arn
# - resource_tags
# - status
# - status_extended
report.region = image.region
report.resource_id = image.id
report.resource_arn = image.arn
# The resource_tags should be filled if the resource has the ability
# of having tags, please check the service first.
report.resource_tags = image.tags
# Then we need to create the business logic for the check
# which always should be simple because the Prowler service
# must do the heavy lifting and the check should be in charge
# of parsing the data provided
# 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"EC2 AMI {image.id} is not public."
# In this example each "image" object has a boolean attribute
# called "public" to set if the AMI is publicly shared
if image.public:
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"EC2 AMI {image.id} is currently public."
)
# Then at the same level as the "report"
# object we need to append it to the findings list.
report.status_extended = f"<Description about why the resource is not compliant>"
findings.append(report)
# Last thing to do is to return the findings list to Prowler
return findings
```
### Check Status
### Data Requirements for Checks in Prowler
All the checks MUST fill the `report.status` and `report.status_extended` with the following criteria:
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.
- Status -- `report.status`
- `PASS` --> If the check is passing against the configured value.
- `FAIL` --> If the check is failing against the configured value.
- `MANUAL` --> This value cannot be used unless a manual operation is required in order to determine if the `report.status` is whether `PASS` or `FAIL`.
- Status Extended -- `report.status_extended`
- MUST end in a dot `.`
- MUST include the service audited with the resource and a brief explanation of the result generated, e.g.: `EC2 AMI ami-0123456789 is not public.`
### Statuses for Checks in Prowler
### Check Region
Required Fields: status and status\_extended
All the checks MUST fill the `report.region` with the following criteria:
Each check **must** populate the `report.status` and `report.status_extended` fields according to the following criteria:
- If the audited resource is regional use the `region` (the name changes depending on the provider: `location` in Azure and GCP and `namespace` in K8s) attribute within the resource object.
- If the audited resource is global use the `service_client.region` within the service client object.
- 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`).
### Check Severity
- 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.`.
The severity of the checks are defined in the metadata file with the `Severity` field. The severity is always in lowercase and can be one of the following values:
### Prowler's Check Severity Levels
- `critical`
- `high`
- `medium`
- `low`
- `informational`
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.
You may need to change it in the check's code if the check has different scenarios that could change the severity. This can be done by using the `report.check_metadata.Severity` attribute:
- `critical` Issue that must be addressed immediately.
- `high` Issue that should be addressed as soon as possible.
- `medium` Issue that should be addressed within a reasonable timeframe.
- `low` Issue that can be addressed in the future.
- `informational` Not an issue but provides valuable information.
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 <valid for more than 6 months>:
if <generic_condition_1>:
report.status = "PASS"
report.check_metadata.Severity = "informational"
report.status_extended = f"RDS Instance {db_instance.id} certificate has over 6 months of validity left."
elif <valid for more than 3 months>:
report.status = "PASS"
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"RDS Instance {db_instance.id} certificate has between 3 and 6 months of validity."
elif <valid for more than 1 month>:
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"RDS Instance {db_instance.id} certificate less than 3 months of validity."
elif <valid for less than 1 month>:
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"RDS Instance {db_instance.id} certificate less than 1 month of validity."
report.status_extended = f"<Resource> is not compliant with <requirement>: <reason>."
else:
report.status = "FAIL"
report.check_metadata.Severity = "critical"
report.status_extended = (
f"RDS Instance {db_instance.id} certificate has expired."
)
report.status_extended = f"<Resource> is not compliant with <requirement>: <critical reason>."
```
### Resource ID, Name and ARN
All the checks MUST fill the `report.resource_id` and `report.resource_arn` with the following criteria:
### 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
- Resouce ID and resource ARN:
- If the resource audited is the AWS account:
- `resource_id` -> AWS Account Number
- `resource_arn` -> AWS Account Root ARN
- If we cant get the ARN from the resource audited, we create a valid ARN with the `resource_id` part as the resource audited. Examples:
- Bedrock -> `arn:<partition>:bedrock:<region>:<account-id>:model-invocation-logging`
- DirectConnect -> `arn:<partition>:directconnect:<region>:<account-id>:dxcon`
- If there is no real resource to audit we do the following:
- resource_id -> `resource_type/unknown`
- resource_arn -> `arn:<partition>:<service>:<region>:<account-id>:<resource_type>/unknown`
- 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`
- 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`
- GCP Resource --> Resource ID
- Resource Name -- `report.resource_name`
- GCP Resource --> Resource Name
- 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`
- Azure Resource --> Resource ID
- Resource Name -- `report.resource_name`
- Azure Resource --> Resource Name
### Python Model
The following is the Python model for the check's class.
- 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.
As per April 11th 2024 the `Check_Metadata_Model` can be found [here](https://github.com/prowler-cloud/prowler/blob/master/prowler/lib/check/models.py#L36-L82).
- Kubernetes
```python
class Check(ABC, Check_Metadata_Model):
"""Prowler Check"""
- 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).
def __init__(self, **data):
"""Check's init function. Calls the CheckMetadataModel init."""
# Parse the Check's metadata file
metadata_file = (
os.path.abspath(sys.modules[self.__module__].__file__)[:-3]
+ ".metadata.json"
)
# Store it to validate them with Pydantic
data = Check_Metadata_Model.parse_file(metadata_file).dict()
# Calls parents init function
super().__init__(**data)
- M365
def metadata(self) -> dict:
"""Return the JSON representation of the check's metadata"""
return self.json()
- 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")
@abstractmethod
def execute(self):
"""Execute the check's logic"""
```
- GitHub
### Using the audit config
- 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`.
Prowler has a [configuration file](../tutorials/configuration_file.md) which is used to pass certain configuration values to the checks, like the following:
### Using the Audit Configuration
Prowler has a [configuration file](../tutorials/configuration_file.md) which is used to pass certain configuration values to the checks. For example:
```python title="ec2_securitygroup_with_many_ingress_egress_rules.py"
class ec2_securitygroup_with_many_ingress_egress_rules(Check):
def execute(self):
findings = []
# max_security_group_rules, default: 50
max_security_group_rules = ec2_client.audit_config.get(
"max_security_group_rules", 50
)
for security_group_arn, security_group in ec2_client.security_groups.items():
```
```yaml title="config.yaml"
# AWS Configuration
aws:
# AWS EC2 Configuration
We use the `audit_config` object to retrieve the value of `max_security_group_rules`, which is the default value of 50 if the configuration value is not present.
# aws.ec2_securitygroup_with_many_ingress_egress_rules
# The default value is 50 rules
max_security_group_rules: 50
The configuration file is located at [`prowler/config/config.yaml`](https://github.com/prowler-cloud/prowler/blob/master/prowler/config/config.yaml) and is used to pass certain configuration values to the checks. For example:
```yaml title="config.yaml"
aws:
max_security_group_rules: 50
```
As you can see in the above code, within the service client, in this case the `ec2_client`, there is an object called `audit_config` which is a Python dictionary containing the values read from the configuration file.
In order to use it, you have to check first if the value is present in the configuration file. If the value is not present, you can create it in the `config.yaml` file and then, read it from the check.
This `audit_config` object is a Python dictionary that stores values read from the configuration file. It can be accessed by the check using the `audit_config` attribute of the service client.
???+ note
It is mandatory to always use the `dictionary.get(value, default)` syntax to set a default value in the case the configuration value is not present.
Always use the `dictionary.get(value, default)` syntax to ensure a default value is set when the configuration value is not present.
## Metadata Structure for Prowler Checks
## Check Metadata
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.
Each Prowler check has metadata associated which is stored at the same level of the check's folder in a file called A `check_name.metadata.json` containing the check's metadata.
### Example Metadata File
???+ note
We are going to include comments in this example metadata JSON but they cannot be included because the JSON format does not allow comments.
Below is a generic example of a check metadata file. **Do not include comments in actual JSON files.**
```json
{
# Provider holds the Prowler provider which the checks belongs to
"Provider": "aws",
# CheckID holds check name
"CheckID": "ec2_ami_public",
# CheckTitle holds the title of the check
"CheckTitle": "Ensure there are no EC2 AMIs set as Public.",
# CheckType holds Software and Configuration Checks, check more here
# https://docs.aws.amazon.com/securityhub/latest/userguide/asff-required-attributes.html#Types
"CheckType": [
"Infrastructure Security"
],
# ServiceName holds the provider service name
"CheckID": "example_check_id",
"CheckTitle": "Example Check Title",
"CheckType": ["Infrastructure Security"],
"ServiceName": "ec2",
# SubServiceName holds the service's subservice or resource used by the check
"SubServiceName": "ami",
# ResourceIdTemplate holds the unique ID for the resource used by the check
"ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id",
# Severity holds the check's severity, always in lowercase (critical, high, medium, low or informational)
"Severity": "critical",
# ResourceType only for AWS, holds the type from here
# https://docs.aws.amazon.com/securityhub/latest/userguide/asff-resources.html
# In case of not existing, use CloudFormation type but removing the "::" and using capital letters only at the beginning of each word. Example: "AWS::EC2::Instance" -> "AwsEc2Instance"
# CloudFormation type reference: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html
# If the resource type does not exist in the CloudFormation types, use "Other".
"ResourceType": "Other",
# Description holds the title of the check, for now is the same as CheckTitle
"Description": "Ensure there are no EC2 AMIs set as Public.",
# Risk holds the check risk if the result is FAIL
"Risk": "When your AMIs are publicly accessible, they are available in the Community AMIs where everyone with an AWS account can use them to launch EC2 instances. Your AMIs could contain snapshots of your applications (including their data), therefore exposing your snapshots in this manner is not advised.",
# RelatedUrl holds an URL with more information about the check purpose
"RelatedUrl": "",
# Remediation holds the information to help the practitioner to fix the issue in the case of the check raise a FAIL
"Description": "Example description of the check.",
"Risk": "Example risk if the check fails.",
"RelatedUrl": "https://example.com",
"Remediation": {
# Code holds different methods to remediate the FAIL finding
"Code": {
# CLI holds the command in the provider native CLI to remediate it
"CLI": "aws ec2 modify-image-attribute --region <REGION> --image-id <EC2_AMI_ID> --launch-permission {\"Remove\":[{\"Group\":\"all\"}]}",
# NativeIaC holds the native IaC code to remediate it, use "https://docs.bridgecrew.io/docs"
"CLI": "example CLI command",
"NativeIaC": "",
# Other holds the other commands, scripts or code to remediate it, use "https://www.trendmicro.com/cloudoneconformity"
"Other": "https://docs.prowler.com/checks/public_8#aws-console",
# Terraform holds the Terraform code to remediate it, use "https://docs.bridgecrew.io/docs"
"Other": "",
"Terraform": ""
},
# Recommendation holds the recommendation for this check with a description and a related URL
"Recommendation": {
"Text": "We recommend your EC2 AMIs are not publicly accessible, or generally available in the Community AMIs.",
"Url": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/cancel-sharing-an-AMI.html"
"Text": "Example recommendation text.",
"Url": "https://example.com/remediation"
}
},
# Categories holds the category or categories where the check can be included, if applied
"Categories": [
"internet-exposed"
],
# DependsOn is not actively used for the moment but it will hold other
# checks wich this check is dependant to
"Categories": ["example-category"],
"DependsOn": [],
# RelatedTo is not actively used for the moment but it will hold other
# checks wich this check is related to
"RelatedTo": [],
# Notes holds additional information not covered in this file
"Notes": ""
}
```
### Remediation Code
### Metadata Fields and Their Purpose
For the Remediation Code we use the following knowledge base to fill it:
- **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).
- Official documentation for the provider
- https://docs.prowler.com/checks/checks-index
- https://www.trendmicro.com/cloudoneconformity
- https://github.com/cloudmatos/matos/tree/master/remediations
- **CheckID** — The unique identifier for the check inside the provider, this field **must** match the check's folder and python file and json metadata file name. For more information about the naming refer to the [Naming Format for Checks](#naming-format-for-checks) section.
### RelatedURL and Recommendation
- **CheckTitle** — A concise, descriptive title for the check.
The RelatedURL field must be filled with an URL from the provider's official documentation like https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/sharingamis-intro.html
- **CheckType** — *For now this field is only standardized for the AWS provider*.
- For AWS this field must follow the [AWS Security Hub Types](https://docs.aws.amazon.com/securityhub/latest/userguide/asff-required-attributes.html#Types) format. So the common pattern to follow is `namespace/category/classifier`, refer to the attached documentation for the valid values for this fields.
Also, if not present you can use the Risk and Recommendation texts from the TrendMicro [CloudConformity](https://www.trendmicro.com/cloudoneconformity) guide.
- **ServiceName** — The name of the provider service being audited. This field **must** be in lowercase and match with the service folder name. For supported services refer to [Prowler Hub](https://hub.prowler.com/check) or directly to [Prowler Code](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers).
- **SubServiceName** — The subservice or resource within the service, if applicable. For more information refer to the [Naming Format for Checks](#naming-format-for-checks) section.
### Python Model
The following is the Python model for the check's metadata model. We use the Pydantic's [BaseModel](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel) as the parent class.
- **ResourceIdTemplate** — A template for the unique resource identifier. For more information refer to the [Prowler's Resource Identification](#prowlers-resource-identification) section.
As per August 5th 2023 the `Check_Metadata_Model` can be found [here](https://github.com/prowler-cloud/prowler/blob/master/prowler/lib/check/models.py#L34-L56).
```python
class Check_Metadata_Model(BaseModel):
"""Check Metadata Model"""
- **Severity** — The severity of the finding if the check fails. Must be one of: `critical`, `high`, `medium`, `low`, or `informational`, this field **must** be in lowercase. To get more information about the severity levels refer to the [Prowler's Check Severity Levels](#prowlers-check-severity-levels) section.
Provider: str
CheckID: str
CheckTitle: str
CheckType: list[str]
ServiceName: str
SubServiceName: str
ResourceIdTemplate: str
Severity: str
ResourceType: str
Description: str
Risk: str
RelatedUrl: str
Remediation: Remediation
Categories: list[str]
DependsOn: list[str]
RelatedTo: list[str]
Notes: str
# We set the compliance to None to
# store the compliance later if supplied
Compliance: list = None
```
- **ResourceType** — The type of resource being audited. *For now this field is only standardized for the AWS provider*.
- For AWS use the [Security Hub resource types](https://docs.aws.amazon.com/securityhub/latest/userguide/asff-resources.html) or, if not available, the PascalCase version of the [CloudFormation type](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) (e.g., `AwsEc2Instance`). Use "Other" if no match exists.
- **Description** — A short description of what the check does.
- **Risk** — The risk or impact if the check fails, explaining why the finding matters.
- **RelatedUrl** — A URL to official documentation or further reading about the check's purpose. If no official documentation is available, use the risk and recommendation text from trusted third-party sources.
- **Remediation** — Guidance for fixing a failed check, including:
- **Code** — Remediation commands or code snippets for CLI, Terraform, native IaC, or other tools like the Web Console.
- **Recommendation** — A textual human readable recommendation. Here it is not necessary to include actual steps, but rather a general recommendation about what to do to fix the check.
- **Categories** — One or more categories for grouping checks in execution (e.g., `internet-exposed`). For the current list of categories, refer to the [Prowler Hub](https://hub.prowler.com/check).
- **DependsOn** — Currently not used.
- **RelatedTo** — Currently not used.
- **Notes** — Any additional information not covered by other fields.
### Remediation Code Guidelines
When providing remediation steps, reference the following sources:
- Official provider documentation.
- [Prowler Checks Remediation Index](https://docs.prowler.com/checks/checks-index)
- [TrendMicro Cloud One Conformity](https://www.trendmicro.com/cloudoneconformity)
- [CloudMatos Remediation Repository](https://github.com/cloudmatos/matos/tree/master/remediations)
### 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).

View File

@@ -1,14 +1,16 @@
# Debugging
# Debugging in Prowler
Debugging in Prowler make things easier!
If you are developing Prowler, it's possible that you will encounter some situations where you have to inspect the code in depth to fix some unexpected issues during the execution.
Debugging in Prowler simplifies the development process, allowing developers to efficiently inspect and resolve unexpected issues during execution.
## VSCode
## Debugging with Visual Studio Code
In VSCode you can run the code using the integrated debugger. Please, refer to this [documentation](https://code.visualstudio.com/docs/editor/debugging) for guidance about the debugger in VSCode.
The following file is an example of the [debugging configuration](https://code.visualstudio.com/docs/editor/debugging#_launch-configurations) file that you can add to [Virtual Studio Code](https://code.visualstudio.com/).
Visual Studio Code (also referred to as VSCode) provides an integrated debugger for executing and analyzing Prowler code. Refer to the official VSCode debugger [documentation](https://code.visualstudio.com/docs/editor/debugging) for detailed instructions.
This file should inside the *.vscode* folder and its name has to be *launch.json*:
### Debugging Configuration Example
The following file is an example of a [debugging configuration](https://code.visualstudio.com/docs/editor/debugging#_launch-configurations) file for [Virtual Studio Code](https://code.visualstudio.com/).
This file must be placed inside the *.vscode* directory and named *launch.json*:
```json
{

View File

@@ -1,8 +1,28 @@
## Contribute with documentation
## Contributing to Documentation
We use `mkdocs` to build this Prowler documentation site so you can easily contribute back with new docs or improving them. To install all necessary dependencies use `poetry install --with docs`.
Prowler documentation is built using `mkdocs`, allowing contributors to easily add or enhance documentation.
1. Install `mkdocs` with your favorite package manager.
2. Inside the `prowler` repository folder run `mkdocs serve` and point your browser to `http://localhost:8000` and you will see live changes to your local copy of this documentation site.
3. Make all needed changes to docs or add new documents. To do so just edit existing md files inside `prowler/docs` and if you are adding a new section or file please make sure you add it to `mkdocs.yaml` file in the root folder of the Prowler repo.
4. Once you are done with changes, please send a pull request to us for review and merge. Thank you in advance!
### Installation and Setup
Install all necessary dependencies using: `poetry install --with docs`.
1. Install `mkdocs` using your preferred package manager.
2. Running the Documentation Locally
Navigate to the `prowler` repository folder.
Start the local documentation server by running: `mkdocs serve`.
Open `http://localhost:8000` in your browser to view live updates.
3. Making Documentation Changes
Make all needed changes to docs or add new documents. Edit existing Markdown (.md) files inside `prowler/docs`.
To add new sections or files, update the `mkdocs.yaml` file located in the root directory of Prowlers repository.
4. Submitting Changes
Once documentation updates are complete:
Submit a pull request for review.
The Prowler team will assess and merge contributions.
Your efforts help improve Prowler documentation—thank you for contributing!

View File

@@ -1,3 +1,3 @@
# Integration Tests
Coming soon ...
Coming soon ...

View File

@@ -2,66 +2,89 @@
## Introduction
Integrating Prowler with external tools enhances its functionality and seamlessly embeds it into your workflows. Prowler supports a wide range of integrations to streamline security assessments and reporting. Common integration targets include messaging platforms like Slack, project management tools like Jira, and cloud services such as AWS Security Hub.
Integrating Prowler with external tools enhances its functionality and enables seamless workflow automation. Prowler supports a variety of integrations to optimize security assessments and reporting.
* Consult the [Prowler Developer Guide](https://docs.prowler.com/projects/prowler-open-source/en/latest/) to understand how Prowler works and the way that you can integrate it with the desired product!
* Identify the best approach for the specific platform youre targeting.
### Supported Integration Targets
- Messaging Platforms Example: Slack
- Project Management Tools Example: Jira
- Cloud Services Example: AWS Security Hub
### Integration Guidelines
To integrate Prowler with a specific product:
Refer to the [Prowler Developer Guide](https://docs.prowler.com/projects/prowler-open-source/en/latest/) to understand its architecture and integration mechanisms.
* Identify the most suitable integration method for the intended platform.
## Steps to Create an Integration
### Identify the Integration Purpose
### Defining the Integration Purpose
* Clearly define the objective of the integration. For example:
* Sending Prowler findings to a platform for alerts, tracking, or further analysis.
* Review existing integrations in the [`prowler/lib/outputs`](https://github.com/prowler-cloud/prowler/tree/master/prowler/lib/outputs) folder for inspiration and implementation examples.
* Before implementing an integration, clearly define its objective. Common purposes include:
### Develop the Integration
* Sending Prowler findings to a platform for alerting, tracking, or further analysis.
* For inspiration and implementation examples, please review the existing integrations in the [`prowler/lib/outputs`](https://github.com/prowler-cloud/prowler/tree/master/prowler/lib/outputs) folder.
### Developing the Integration
* Script Development:
* Write a script to process Prowlers output and interact with the target platforms API.
* For example, to send findings, parse Prowlers results and use the platforms API to create entries or notifications.
* If the goal is to send findings, parse Prowlers results and use the platforms API to create entries or notifications.
* Configuration:
* Ensure your script includes configurable options for environment-specific settings, such as API endpoints and authentication tokens.
* Ensure the script supports environment-specific settings, such as:
- API endpoints
- Authentication tokens
- Any necessary configurable parameters.
### Fundamental Structure
* Integration Class:
* Create a class that encapsulates attributes and methods for the integration.
Here is an example with Jira integration:
* To implement an integration, create a class that encapsulates the required attributes and methods for interacting with the target platform. Example: Jira Integration
```python title="Jira Class"
class Jira:
"""
Jira class to interact with the Jira API
[Note]
This integration is limited to a single Jira Cloud, therefore all the issues will be created for same Jira Cloud ID. We will need to work on the ability of providing a Jira Cloud ID if the user is present in more than one.
This integration is limited to a single Jira Cloud instance, meaning all issues will be created under the same Jira Cloud ID. Future improvements will include the ability to specify a Jira Cloud ID for users associated with multiple accounts.
Attributes:
- _redirect_uri: The redirect URI
- _client_id: The client ID
Attributes
- _redirect_uri: The redirect URI used
- _client_id: The client identifier
- _client_secret: The client secret
- _access_token: The access token
- _refresh_token: The refresh token
- _expiration_date: The authentication expiration
- _cloud_id: The cloud ID
- _cloud_id: The cloud identifier
- _scopes: The scopes needed to authenticate, read:jira-user read:jira-work write:jira-work
- AUTH_URL: The URL to authenticate with Jira
- PARAMS_TEMPLATE: The template for the parameters to authenticate with Jira
- TOKEN_URL: The URL to get the access token from Jira
- API_TOKEN_URL: The URL to get the accessible resources from Jira
Methods:
- __init__: Initialize the Jira object
- input_authorization_code: Input the authorization code
- auth_code_url: Generate the URL to authorize the application
- get_auth: Get the access token and refresh token
- get_cloud_id: Get the cloud ID from Jira
- get_access_token: Get the access token
- refresh_access_token: Refresh the access token from Jira
- test_connection: Test the connection to Jira and return a Connection object
- get_projects: Get the projects from Jira
- get_available_issue_types: Get the available issue types for a project
- send_findings: Send the findings to Jira and create an issue
Methods
__init__: Initializes the Jira object
- input_authorization_code: Inputs the authorization code
- auth_code_url: Generates the URL to authorize the application
- get_auth: Gets the access token and refreshes it
- get_cloud_id: Gets the cloud identifier from Jira
- get_access_token: Gets the access token
- refresh_access_token: Refreshes the access token from Jira
- test_connection: Tests the connection to Jira and returns a Connection object
- get_projects: Gets the projects from Jira
- get_available_issue_types: Gets the available issue types for a project
- send_findings: Sends the findings to Jira and creates an issue
Raises:
- JiraGetAuthResponseError: Failed to get the access token and refresh token
@@ -128,9 +151,17 @@ Integrating Prowler with external tools enhances its functionality and seamlessl
# More properties and methods
```
* Test Connection Method:
* Implement a method to validate credentials or tokens, ensuring the connection to the target platform is successful.
The following is the code for the `test_connection` method for the `Jira` class:
* Validating Credentials or Tokens
To ensure a successful connection to the target platform, implement a method that validates authentication credentials or tokens.
#### Method Implementation
The following example demonstrates the `test_connection` method for the `Jira` class:
```python title="Test connection"
@staticmethod
def test_connection(
@@ -142,8 +173,8 @@ Integrating Prowler with external tools enhances its functionality and seamlessl
"""Test the connection to Jira
Args:
- redirect_uri: The redirect URI
- client_id: The client ID
- redirect_uri: The redirect URI used
- client_id: The client identifier
- client_secret: The client secret
- raise_on_exception: Whether to raise an exception or not
@@ -215,9 +246,15 @@ Integrating Prowler with external tools enhances its functionality and seamlessl
)
return Connection(is_connected=False, error=error)
```
* Send Findings Method:
* Add a method to send Prowler findings to the target platform, adhering to its API specifications.
The following is the code for the `send_findings` method for the `Jira` class:
#### Method Implementation
The following example demonstrates the `send_findings` method for the `Jira` class:
```python title="Send findings method"
def send_findings(
self,
@@ -321,16 +358,19 @@ Integrating Prowler with external tools enhances its functionality and seamlessl
)
```
### Testing
### Testing the Integration
* Test the integration in a controlled environment to confirm it behaves as expected.
* Verify that Prowlers findings are accurately transmitted and correctly processed by the target platform.
* Simulate edge cases to ensure robust error handling.
* Conduct integration testing in a controlled environment to validate expected behavior. Ensure the following:
* Transmission Accuracy Verify that Prowler findings are correctly sent and processed by the target platform.
* Error Handling Simulate edge cases to assess robustness and failure recovery mechanisms.
### Documentation
* Provide clear, detailed documentation for your integration:
* Setup instructions, including any required dependencies.
* Configuration details, such as environment variables or authentication steps.
* Example use cases and troubleshooting tips.
* Good documentation ensures maintainability and simplifies onboarding for team members.
* Ensure the following elements are included:
* Setup Instructions List all necessary dependencies and installation steps.
* Configuration Details Specify required environment variables, authentication steps, etc.
* Example Use Cases Provide practical scenarios demonstrating functionality.
* Troubleshooting Guide Document common issues and resolution steps.
* Comprehensive and clear documentation improves maintainability and simplifies onboarding.

View File

@@ -1,75 +1,166 @@
# Developer Guide
You can extend Prowler Open Source in many different ways, in most cases you will want to create your own checks and compliance security frameworks, here is where you can learn about how to get started with it. We also include how to create custom outputs, integrations and more.
Extending Prowler
## Get the code and install all dependencies
Prowler can be extended in various ways, with common use cases including:
First of all, you need a version of Python 3.9 or higher and also `pip` installed to be able to install all dependencies required.
- New security checks
- New compliance frameworks
- New output formats
- New integrations
- New proposed features
Then, to start working with the Prowler Github repository you need to fork it to be able to propose changes for new features, bug fixing, etc. To fork the Prowler repo please refer to [this guide](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo?tool=webui#forking-a-repository).
All the relevant information for these cases is included in this guide.
Once that is satisfied go ahead and clone your forked repo:
## Getting the Code and Installing All Dependencies
### Prerequisites
Before proceeding, ensure the following:
- Git is installed.
- Python 3.9 or higher is installed.
- `poetry` is installed to manage dependencies.
### Forking the Prowler Repository
To contribute to Prowler, fork the Prowler GitHub repository. This allows you to propose changes, submit new features, and fix bugs. For guidance on forking, refer to the [official GitHub documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo?tool=webui#forking-a-repository).
### Cloning Your Forked Repository
Once your fork is created, clone it using the following commands:
```
git clone https://github.com/<your-github-user>/prowler
cd prowler
```
For isolation and to avoid conflicts with other environments, we recommend using `poetry`, a Python dependency management tool. You can install it by following the instructions [here](https://python-poetry.org/docs/#installation).
Then install all dependencies including the ones for developers:
### Dependency Management and Environment Isolation
To prevent conflicts between environments, we recommend using `poetry`, a Python dependency management solution. Install it by following the [instructions](https://python-poetry.org/docs/#installation).
### Installing Dependencies
To install all required dependencies, including those needed for development, run:
```
poetry install --with dev
eval $(poetry env activate) \
eval $(poetry env activate)
```
> [!IMPORTANT]
> Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
>
> If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment.
> In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment
## Contributing with your code or fixes to Prowler
???+ important
Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment.
In case you have any doubts, consult the [Poetry environment activation guide](https://python-poetry.org/docs/managing-environments/#activating-the-environment).
## Contributing to Prowler
### Ways to Contribute
Here are some ideas for collaborating with Prowler:
1. **Review Current Issues**: Check out our [GitHub Issues](https://github.com/prowler-cloud/prowler/issues) page. We often tag issues as `good first issue` - these are perfect for new contributors as they are typically well-defined and manageable in scope.
2. **Expand Prowler's Capabilities**: Prowler is constantly evolving, and you can be a part of its growth. Whether you are adding checks, supporting new services, or introducing integrations, your contributions help improve the tool for everyone. Here is how you can get involved:
- **Adding New Checks**
Want to improve Prowler's detection capabilities for your favorite cloud provider? You can contribute by writing new checks. To get started, follow the [create a new check guide](./checks.md).
- **Adding New Services**
One key service for your favorite cloud provider is missing? Add it to Prowler! To add a new service, check out the [create a new service guide](./services.md). Do not forget to include relevant checks to validate functionality.
- **Adding New Providers**
If you would like to extend Prowler to work with a new cloud provider, follow the [create a new provider guide](./provider.md). This typically involves setting up new services and checks to ensure compatibility.
- **Adding New Output Formats**
Want to tailor how results are displayed or exported? You can add custom output formats by following the [create a new output format guide](./outputs.md).
- **Adding New Integrations**
Prowler can work with other tools and platforms through integrations. If you would like to add one, see the [create a new integration guide](./integrations.md).
- **Proposing or Implementing Features**
Got an idea to make Prowler better? Whether it is a brand-new feature or an enhancement to an existing one, you are welcome to propose it or help implement community-requested improvements.
3. **Improve Documentation**: Help make Prowler more accessible by enhancing our documentation, fixing typos, or adding examples/tutorials. See the tutorial of how we write our documentation [here](./documentation.md).
4. **Bug Fixes**: If you find any issues or bugs, you can report them in the [GitHub Issues](https://github.com/prowler-cloud/prowler/issues) page and if you want you can also fix them.
Remember, our community is here to help! If you need guidance, do not hesitate to ask questions in the issues or join our [Slack workspace](https://goto.prowler.com/slack).
### Pre-Commit Hooks
This repository uses Git pre-commit hooks managed by the [pre-commit](https://pre-commit.com/) tool, it is installed with `poetry install --with dev`. Next, run the following command in the root of this repository:
This repo has git pre-commit hooks managed via the [pre-commit](https://pre-commit.com/) tool. [Install](https://pre-commit.com/#install) it how ever you like, then in the root of this repo run:
```shell
pre-commit install
```
You should get an output like the following:
Successful installation should produce the following output:
```shell
pre-commit installed at .git/hooks/pre-commit
```
Before we merge any of your pull requests we pass checks to the code, we use the following tools and automation to make sure the code is secure and dependencies up-to-dated:
### Code Quality and Security Checks
Before merging pull requests, several automated checks and utilities ensure code security and updated dependencies:
???+ note
These should have been already installed if you ran `poetry install --with dev`
These should have been already installed if `poetry install --with dev` was already run.
- [`bandit`](https://pypi.org/project/bandit/) for code security review.
- [`safety`](https://pypi.org/project/safety/) and [`dependabot`](https://github.com/features/security) for dependencies.
- [`hadolint`](https://github.com/hadolint/hadolint) and [`dockle`](https://github.com/goodwithtech/dockle) for our containers security.
- [`Snyk`](https://docs.snyk.io/integrations/snyk-container-integrations/container-security-with-docker-hub-integration) in Docker Hub.
- [`clair`](https://github.com/quay/clair) in Amazon ECR.
- [`vulture`](https://pypi.org/project/vulture/), [`flake8`](https://pypi.org/project/flake8/), [`black`](https://pypi.org/project/black/) and [`pylint`](https://pypi.org/project/pylint/) for formatting and best practices.
- [`hadolint`](https://github.com/hadolint/hadolint) and [`dockle`](https://github.com/goodwithtech/dockle) for container security.
- [`Snyk`](https://docs.snyk.io/integrations/snyk-container-integrations/container-security-with-docker-hub-integration) for container security in Docker Hub.
- [`clair`](https://github.com/quay/clair) for container security in Amazon ECR.
- [`vulture`](https://pypi.org/project/vulture/), [`flake8`](https://pypi.org/project/flake8/), [`black`](https://pypi.org/project/black/), and [`pylint`](https://pypi.org/project/pylint/) for formatting and best practices.
You can see all dependencies in file `pyproject.toml`.
Additionally, ensure the latest version of [`TruffleHog`](https://github.com/trufflesecurity/trufflehog) is installed to scan for sensitive data in the code. Follow the official [installation guide](https://github.com/trufflesecurity/trufflehog?tab=readme-ov-file#floppy_disk-installation) for setup.
Moreover, you would need to install [`TruffleHog`](https://github.com/trufflesecurity/trufflehog) on the latest version to check for secrets in the code. You can install it using the official installation guide [here](https://github.com/trufflesecurity/trufflehog?tab=readme-ov-file#floppy_disk-installation).
### Dependency Management
Additionally, please ensure to follow the code documentation practices outlined in this guide: [Google Python Style Guide - Comments and Docstrings](https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings).
All dependencies are listed in the `pyproject.toml` file.
For proper code documentation, refer to the following and follow the code documentation practices presented there: [Google Python Style Guide - Comments and Docstrings](https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings).
???+ note
If you have any trouble when committing to the Prowler repository, add the `--no-verify` flag to the `git commit` command.
If you encounter issues when committing to the Prowler repository, use the `--no-verify` flag with the `git commit` command.
### Repository Folder Structure
Understanding the layout of the Prowler codebase will help you quickly find where to add new features, checks, or integrations. The following is a high-level overview from the root of the repository:
```
prowler/
├── prowler/ # Main source code for Prowler SDK (CLI, providers, services, checks, compliances, config, etc.)
├── api/ # API server and related code
├── dashboard/ # Local Dashboard extracted from the CLI output
├── ui/ # Web UI components
├── util/ # Utility scripts and helpers
├── tests/ # Prowler SDK test suite
├── docs/ # Documentation, including this guide
├── examples/ # Example output formats for providers and scripts
├── permissions/ # Permission-related files and policies
├── contrib/ # Community-contributed scripts or modules
├── kubernetes/ # Kubernetes deployment files
├── .github/ # GitHub related files (workflows, issue templates, etc.)
├── pyproject.toml # Python project configuration (Poetry)
├── poetry.lock # Poetry lock file
├── README.md # Project overview and getting started
├── Makefile # Common development commands
├── Dockerfile # SDK Docker container
├── docker-compose.yml # Prowler App Docker compose
└── ... # Other supporting files
```
## Pull Request Checklist
If you create or review a PR in https://github.com/prowler-cloud/prowler please follow this checklist:
When creating or reviewing a pull request in https://github.com/prowler-cloud/prowler, follow [this checklist](https://github.com/prowler-cloud/prowler/blob/master/.github/pull_request_template.md#checklist).
- [ ] Make sure you've read the Prowler Developer Guide at https://docs.prowler.cloud/en/latest/developer-guide/introduction/
- [ ] Are we following the style guide, hence installed all the linters and formatters? Please check https://docs.prowler.cloud/en/latest/developer-guide/introduction/#contributing-with-your-code-or-fixes-to-prowler
- [ ] Are we increasing/decreasing the test coverage? Please, review if we need to include/modify tests for the new code.
- [ ] Are we modifying outputs? Please review it carefully.
- [ ] Do we need to modify the Prowler documentation to reflect the changes introduced?
- [ ] Are we introducing possible breaking changes? Are we modifying a core feature?
## Contribution Appreciation
If you enjoy swag, wed love to thank you for your contribution with laptop stickers or other Prowler merchandise!
## Want some swag as appreciation for your contribution?
To request swag: Share your pull request details in our [Slack workspace](https://goto.prowler.com/slack).
If you are like us and you love swag, we are happy to thank you for your contribution with some laptop stickers or whatever other swag we may have at that time. Please, tell us more details and your pull request link in our [Slack workspace here](https://goto.prowler.com/slack). You can also reach out to Toni de la Fuente on Twitter [here](https://twitter.com/ToniBlyx), his DMs are open.
You can also reach out to Toni de la Fuente on [Twitter](https://twitter.com/ToniBlyx)his DMs are open!

View File

@@ -2,21 +2,38 @@
## Introduction
Prowler can generate outputs in multiple formats, allowing users to customize the way findings are presented. This is particularly useful when integrating Prowler with third-party tools, creating specialized reports, or simply tailoring the data to meet specific requirements. A custom output format gives you the flexibility to extract and display only the most relevant information in the way you need it.
Prowler supports multiple output formats, allowing users to tailor findings presentation to their needs. Custom output formats are valuable when integrating Prowler with third-party tools, generating specialized reports, or adapting data for specific workflows. By defining a custom output format, users can refine how findings are structured, extracting and displaying only the most relevant information.
* Prowler organizes its outputs in the `/lib/outputs` directory. Each format (e.g., JSON, CSV, HTML) is implemented as a Python class.
* Outputs are generated based on findings collected during a scan. Each finding is represented as a structured dictionary containing details like resource IDs, severities, descriptions, and more.
* Consult the [Prowler Developer Guide](https://docs.prowler.com/projects/prowler-open-source/en/latest/) to understand how Prowler works and the way that you can create it with the desired output!
* Identify the best approach for the specific output youre targeting.
- Output Organization in Prowler
Prowler outputs are managed within the `/lib/outputs` directory. Each format—such as JSON, CSV, HTML—is implemented as a Python class.
- Outputs are generated based on scan findings, which are stored as structured dictionaries containing details such as:
- Resource IDs
- Severities
- Descriptions
- Other relevant metadata
- Creation Guidelines
Refer to the [Prowler Developer Guide](https://docs.prowler.com/projects/prowler-open-source/en/latest/) for insights into Prowlers architecture and best practices for creating custom outputs.
- Identify the most suitable integration method for the output being targeted.
## Steps to Create a Custom Output Format
### Schema
* Output Class:
* The class must inherit from `Output`. Review the [Output Class](https://github.com/prowler-cloud/prowler/blob/master/prowler/lib/outputs/output.py).
* Create a class that encapsulates attributes and methods for the output.
The following is the code for the `CSV` class:
- Output Class:
- The class must inherit from `Output`. Review the [Output Class](https://github.com/prowler-cloud/prowler/blob/master/prowler/lib/outputs/output.py).
- Create a class that encapsulates the required attributes and methods for interacting with the target platform. Below the code for the `CSV` class is presented:
```python title="CSV Class"
class CSV(Output):
def transform(self, findings: List[Finding]) -> None:
@@ -28,118 +45,137 @@ Prowler can generate outputs in multiple formats, allowing users to customize th
"""
...
```
* Transform Method:
* This method will transform the findings provided by Prowler to a specific format.
The following is the code for the `transform` method for the `CSV` class:
```python title="Transform"
def transform(self, findings: List[Finding]) -> None:
"""Transforms the findings into the CSV format.
Args:
findings (list[Finding]): a list of Finding objects
"""
try:
for finding in findings:
finding_dict = {}
finding_dict["AUTH_METHOD"] = finding.auth_method
finding_dict["TIMESTAMP"] = finding.timestamp
finding_dict["ACCOUNT_UID"] = finding.account_uid
finding_dict["ACCOUNT_NAME"] = finding.account_name
finding_dict["ACCOUNT_EMAIL"] = finding.account_email
finding_dict["ACCOUNT_ORGANIZATION_UID"] = (
finding.account_organization_uid
)
finding_dict["ACCOUNT_ORGANIZATION_NAME"] = (
finding.account_organization_name
)
finding_dict["ACCOUNT_TAGS"] = unroll_dict(
finding.account_tags, separator=":"
)
finding_dict["FINDING_UID"] = finding.uid
finding_dict["PROVIDER"] = finding.metadata.Provider
finding_dict["CHECK_ID"] = finding.metadata.CheckID
finding_dict["CHECK_TITLE"] = finding.metadata.CheckTitle
finding_dict["CHECK_TYPE"] = unroll_list(finding.metadata.CheckType)
finding_dict["STATUS"] = finding.status.value
finding_dict["STATUS_EXTENDED"] = finding.status_extended
finding_dict["MUTED"] = finding.muted
finding_dict["SERVICE_NAME"] = finding.metadata.ServiceName
finding_dict["SUBSERVICE_NAME"] = finding.metadata.SubServiceName
finding_dict["SEVERITY"] = finding.metadata.Severity.value
finding_dict["RESOURCE_TYPE"] = finding.metadata.ResourceType
finding_dict["RESOURCE_UID"] = finding.resource_uid
finding_dict["RESOURCE_NAME"] = finding.resource_name
finding_dict["RESOURCE_DETAILS"] = finding.resource_details
finding_dict["RESOURCE_TAGS"] = unroll_dict(finding.resource_tags)
finding_dict["PARTITION"] = finding.partition
finding_dict["REGION"] = finding.region
finding_dict["DESCRIPTION"] = finding.metadata.Description
finding_dict["RISK"] = finding.metadata.Risk
finding_dict["RELATED_URL"] = finding.metadata.RelatedUrl
finding_dict["REMEDIATION_RECOMMENDATION_TEXT"] = (
finding.metadata.Remediation.Recommendation.Text
)
finding_dict["REMEDIATION_RECOMMENDATION_URL"] = (
finding.metadata.Remediation.Recommendation.Url
)
finding_dict["REMEDIATION_CODE_NATIVEIAC"] = (
finding.metadata.Remediation.Code.NativeIaC
)
finding_dict["REMEDIATION_CODE_TERRAFORM"] = (
finding.metadata.Remediation.Code.Terraform
)
finding_dict["REMEDIATION_CODE_CLI"] = (
finding.metadata.Remediation.Code.CLI
)
finding_dict["REMEDIATION_CODE_OTHER"] = (
finding.metadata.Remediation.Code.Other
)
finding_dict["COMPLIANCE"] = unroll_dict(
finding.compliance, separator=": "
)
finding_dict["CATEGORIES"] = unroll_list(finding.metadata.Categories)
finding_dict["DEPENDS_ON"] = unroll_list(finding.metadata.DependsOn)
finding_dict["RELATED_TO"] = unroll_list(finding.metadata.RelatedTo)
finding_dict["NOTES"] = finding.metadata.Notes
finding_dict["PROWLER_VERSION"] = finding.prowler_version
self._data.append(finding_dict)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
```
* Batch Write Data To File Method:
* This method will write the modeled object to a file.
The following is the code for the `batch_write_data_to_file` method for the `CSV` class:
```python title="Batch Write Data To File"
def batch_write_data_to_file(self) -> None:
"""Writes the findings to a file using the CSV format using the `Output._file_descriptor`."""
try:
if (
getattr(self, "_file_descriptor", None)
and not self._file_descriptor.closed
and self._data
):
csv_writer = DictWriter(
self._file_descriptor,
fieldnames=self._data[0].keys(),
delimiter=";",
)
csv_writer.writeheader()
for finding in self._data:
csv_writer.writerow(finding)
self._file_descriptor.close()
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
```
- Transform Method:
### Integration With The Current Code
- This method will transform the findings provided by Prowler to a specific format.
#### Method Implementation
The following example demonstrates the `transform` method for the `CSV` class:
```python title="Transform"
def transform(self, findings: List[Finding]) -> None:
"""Transforms the findings into the CSV format.
Args:
findings (list[Finding]): a list of Finding objects
"""
try:
for finding in findings:
finding_dict = {}
finding_dict["AUTH_METHOD"] = finding.auth_method
finding_dict["TIMESTAMP"] = finding.timestamp
finding_dict["ACCOUNT_UID"] = finding.account_uid
finding_dict["ACCOUNT_NAME"] = finding.account_name
finding_dict["ACCOUNT_EMAIL"] = finding.account_email
finding_dict["ACCOUNT_ORGANIZATION_UID"] = (
finding.account_organization_uid
)
finding_dict["ACCOUNT_ORGANIZATION_NAME"] = (
finding.account_organization_name
)
finding_dict["ACCOUNT_TAGS"] = unroll_dict(
finding.account_tags, separator=":"
)
finding_dict["FINDING_UID"] = finding.uid
finding_dict["PROVIDER"] = finding.metadata.Provider
finding_dict["CHECK_ID"] = finding.metadata.CheckID
finding_dict["CHECK_TITLE"] = finding.metadata.CheckTitle
finding_dict["CHECK_TYPE"] = unroll_list(finding.metadata.CheckType)
finding_dict["STATUS"] = finding.status.value
finding_dict["STATUS_EXTENDED"] = finding.status_extended
finding_dict["MUTED"] = finding.muted
finding_dict["SERVICE_NAME"] = finding.metadata.ServiceName
finding_dict["SUBSERVICE_NAME"] = finding.metadata.SubServiceName
finding_dict["SEVERITY"] = finding.metadata.Severity.value
finding_dict["RESOURCE_TYPE"] = finding.metadata.ResourceType
finding_dict["RESOURCE_UID"] = finding.resource_uid
finding_dict["RESOURCE_NAME"] = finding.resource_name
finding_dict["RESOURCE_DETAILS"] = finding.resource_details
finding_dict["RESOURCE_TAGS"] = unroll_dict(finding.resource_tags)
finding_dict["PARTITION"] = finding.partition
finding_dict["REGION"] = finding.region
finding_dict["DESCRIPTION"] = finding.metadata.Description
finding_dict["RISK"] = finding.metadata.Risk
finding_dict["RELATED_URL"] = finding.metadata.RelatedUrl
finding_dict["REMEDIATION_RECOMMENDATION_TEXT"] = (
finding.metadata.Remediation.Recommendation.Text
)
finding_dict["REMEDIATION_RECOMMENDATION_URL"] = (
finding.metadata.Remediation.Recommendation.Url
)
finding_dict["REMEDIATION_CODE_NATIVEIAC"] = (
finding.metadata.Remediation.Code.NativeIaC
)
finding_dict["REMEDIATION_CODE_TERRAFORM"] = (
finding.metadata.Remediation.Code.Terraform
)
finding_dict["REMEDIATION_CODE_CLI"] = (
finding.metadata.Remediation.Code.CLI
)
finding_dict["REMEDIATION_CODE_OTHER"] = (
finding.metadata.Remediation.Code.Other
)
finding_dict["COMPLIANCE"] = unroll_dict(
finding.compliance, separator=": "
)
finding_dict["CATEGORIES"] = unroll_list(finding.metadata.Categories)
finding_dict["DEPENDS_ON"] = unroll_list(finding.metadata.DependsOn)
finding_dict["RELATED_TO"] = unroll_list(finding.metadata.RelatedTo)
finding_dict["NOTES"] = finding.metadata.Notes
finding_dict["PROWLER_VERSION"] = finding.prowler_version
self._data.append(finding_dict)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
```
- Batch Write Data To File Method:
- This method will write the modeled object to a file.
#### Method Implementation
The following example demonstrates the `batch_write_data_to_file` method for the `CSV` class:
```python title="Batch Write Data To File"
def batch_write_data_to_file(self) -> None:
"""Writes the findings to a file using the CSV format using the `Output._file_descriptor`."""
try:
if (
getattr(self, "_file_descriptor", None)
and not self._file_descriptor.closed
and self._data
):
csv_writer = DictWriter(
self._file_descriptor,
fieldnames=self._data[0].keys(),
delimiter=";",
)
csv_writer.writeheader()
for finding in self._data:
csv_writer.writerow(finding)
self._file_descriptor.close()
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
```
### Integrating the Custom Output Format into Prowler
Once the custom output format is created, it must be integrated into Prowler to ensure compatibility with the existing architecture.
#### Reviewing Current Supported Outputs
Before implementing the new output format, examine the usage of currently supported formats to understand their structure and integration approach. Example: CSV Output Creation in Prowler
Below is an example of how Prowler generates and processes CSV output within its [codebase](https://github.com/prowler-cloud/prowler/blob/master/prowler/__main__.py):
Once that the desired output format is created it has to be integrated with Prowler. Take a look at the the usage from the current supported output in order to add the new one.
Here is an example of the CSV output creation inside [prowler code](https://github.com/prowler-cloud/prowler/blob/master/prowler/__main__.py):
```python title="CSV creation"
if mode == "csv":
csv_output = CSV(
@@ -148,19 +184,23 @@ if mode == "csv":
file_path=f"{filename}{csv_file_suffix}",
)
generated_outputs["regular"].append(csv_output)
# Write CSV Finding Object to file
# Write CSV Finding Object to file.
csv_output.batch_write_data_to_file()
```
### Testing
* Verify that Prowlers findings are accurately writed in the desired output format.
* Simulate edge cases to ensure robust error handling.
* Verify that Prowlers findings are accurately typed in the desired output format.
* Error Handling Simulate edge cases to assess robustness and failure recovery mechanisms.
### Documentation
* Provide clear, detailed documentation for your output:
* Setup instructions, including any required dependencies.
* Ensure the following elements are included:
* Setup Instructions List all necessary dependencies and installation steps.
* Configuration details.
* Example use cases and troubleshooting tips.
* Good documentation ensures maintainability and simplifies onboarding for new users.
* Example Use Cases Provide practical scenarios demonstrating functionality.
* Troubleshooting Guide Document common issues and resolution steps.
* Comprehensive and clear documentation improves maintainability and simplifies onboarding of new users.

View File

@@ -1,187 +1,78 @@
# Create a new Provider for Prowler
Here you can find how to create a new Provider in Prowler to give support for making all security checks needed and make your cloud safer!
# Prowler Providers
## Introduction
Providers are the foundation on which Prowler is built, a simple definition for a cloud provider could be "third-party company that offers a platform where any IT resource you need is available at any time upon request". The most well-known cloud providers are Amazon Web Services, Azure from Microsoft and Google Cloud which are already supported by Prowler.
Providers form the backbone of Prowler, enabling security assessments across various cloud environments.
To create a new provider that is not supported now by Prowler and add your security checks you must create a new folder to store all the related files within it (services, checks, etc.). It must be store in route `prowler/providers/<new_provider_name>/`.
A provider is any platform or service that offers resources, data, or functionality that can be audited for security and compliance. This includes:
Inside that folder, you MUST create the following files and folders:
- 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)
- A `lib` folder: to store all extra functions.
- A `services` folder: to store all [services](./services.md) to audit.
- An empty `__init__.py`: to make Python treat this service folder as a package.
- A `<new_provider_name>_provider.py`, containing all the provider's logic necessary to get authenticated in the provider, configurations and extra data useful for final report.
- A `models.py`, containing all the models necessary for the new provider.
For providers supported by Prowler, refer to [Prowler Hub](https://hub.prowler.com/).
## Provider
???+ important
There are some custom providers added by the community, like [NHN Cloud](https://www.nhncloud.com/), that are not maintained by the Prowler team, but can be used in the Prowler CLI. They can be checked directly at the [Prowler GitHub repository](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers).
The structure for Prowler's providers is set up in such a way that they can be utilized through a generic service specific to each provider. This is achieved by passing the required parameters to the constructor, which in turn initializes all the necessary session values.
## Adding a New Provider
To integrate an unsupported Prowler provider and implement its security checks, create a dedicated folder for all related files (e.g., services, checks)."
This folder must be placed within [`prowler/providers/<new_provider_name>/`](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers).
Within this folder the following folders are also to be created:
- `lib` Stores additional utility functions and core files required by every provider. The following files and subfolders are commonly found in every provider's `lib` folder:
- `service/service.py` Provides a generic service class to be inherited by all services.
- `arguments/arguments.py` Handles provider-specific argument parsing.
- `mutelist/mutelist.py` Manages the mutelist functionality for the provider.
- `services` Stores all [services](./services.md) that the provider offers and want to be audited by [Prowler checks](./checks.md).
- `__init__.py` (empty) Ensures Python recognizes this folder as a package.
- `<new_provider_name>_provider.py` Defines authentication logic, configurations, and other provider-specific data.
- `models.py` Contains necessary models for the new provider.
By adhering to this structure, Prowler can effectively support services and security checks for additional providers.
???+ important
If your new provider requires a Python library (such as an official SDK or API client) to connect to its services, make sure to add it as a dependency in the `pyproject.toml` file. This ensures that all contributors and users have the necessary packages installed when working with your provider.
## Provider Structure in Prowler
Prowler's provider architecture is designed to facilitate security audits through a generic service tailored to each provider. This is accomplished by passing the necessary parameters to the constructor, which initializes all required session values.
### Base Class
All the providers in Prowler inherits from the same [base class](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/common/provider.py). It is an [abstract base class](https://docs.python.org/3/library/abc.html) that defines the interface for all provider classes. The code of the class is the next:
```python title="Provider Base Class"
from abc import ABC, abstractmethod
from typing import Any
class Provider(ABC):
"""
The Provider class is an abstract base class that defines the interface for all provider classes in the auditing system.
Attributes:
type (property): The type of the provider.
identity (property): The identity of the provider for auditing.
session (property): The session of the provider for auditing.
audit_config (property): The audit configuration of the provider.
output_options (property): The output configuration of the provider for auditing.
Methods:
print_credentials(): Displays the provider's credentials used for auditing in the command-line interface.
setup_session(): Sets up the session for the provider.
validate_arguments(): Validates the arguments for the provider.
get_checks_to_execute_by_audit_resources(): Returns a set of checks based on the input resources to scan.
Note:
This is an abstract base class and should not be instantiated directly. Each provider should implement its own
version of the Provider class by inheriting from this base class and implementing the required methods and properties.
"""
@property
@abstractmethod
def type(self) -> str:
"""
type method stores the provider's type.
This method needs to be created in each provider.
"""
raise NotImplementedError()
@property
@abstractmethod
def identity(self) -> str:
"""
identity method stores the provider's identity to audit.
This method needs to be created in each provider.
"""
raise NotImplementedError()
@abstractmethod
def setup_session(self) -> Any:
"""
setup_session sets up the session for the provider.
This method needs to be created in each provider.
"""
raise NotImplementedError()
@property
@abstractmethod
def session(self) -> str:
"""
session method stores the provider's session to audit.
This method needs to be created in each provider.
"""
raise NotImplementedError()
@property
@abstractmethod
def audit_config(self) -> str:
"""
audit_config method stores the provider's audit configuration.
This method needs to be created in each provider.
"""
raise NotImplementedError()
@abstractmethod
def print_credentials(self) -> None:
"""
print_credentials is used to display in the CLI the provider's credentials used to audit.
This method needs to be created in each provider.
"""
raise NotImplementedError()
@property
@abstractmethod
def output_options(self) -> str:
"""
output_options method returns the provider's audit output configuration.
This method needs to be created in each provider.
"""
raise NotImplementedError()
@output_options.setter
@abstractmethod
def output_options(self, value: str) -> Any:
"""
output_options.setter sets the provider's audit output configuration.
This method needs to be created in each provider.
"""
raise NotImplementedError()
def validate_arguments(self) -> None:
"""
validate_arguments validates the arguments for the provider.
This method can be overridden in each provider if needed.
"""
raise NotImplementedError()
def get_checks_to_execute_by_audit_resources(self) -> set:
"""
get_checks_to_execute_by_audit_resources returns a set of checks based on the input resources to scan.
This is a fallback that returns None if the service has not implemented this function.
"""
return set()
@property
@abstractmethod
def mutelist(self):
"""
mutelist method returns the provider's mutelist.
This method needs to be created in each provider.
"""
raise NotImplementedError()
@mutelist.setter
@abstractmethod
def mutelist(self, path: str):
"""
mutelist.setter sets the provider's mutelist.
This method needs to be created in each provider.
"""
raise NotImplementedError()
```
All Prowler providers inherit from the same base class located in [`prowler/providers/common/provider.py`](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/common/provider.py). It is an [abstract base class](https://docs.python.org/3/library/abc.html) that defines the interface for all provider classes.
### Provider Class
Due to the complexity and differences of each provider use the rest of the providers as a template for the implementation.
#### Provider Implementation Guidance
Given the complexity and variability of providers, use existing provider implementations as templates when developing new integrations.
- [AWS](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/aws/aws_provider.py)
- [GCP](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/gcp/gcp_provider.py)
- [Azure](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/azure/azure_provider.py)
- [Kubernetes](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/kubernetes/kubernetes_provider.py)
- [M365](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/m365/m365_provider.py)
- [Microsoft365](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/microsoft365/microsoft365_provider.py)
- [GitHub](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/github/github_provider.py)
To facilitate understanding here is a pseudocode of how the most basic provider could be with examples.
### Basic Provider Implementation: Pseudocode Example
To simplify understanding, the following pseudocode outlines the fundamental structure of a provider, including library imports necessary for authentication.
```python title="Provider Example Class"
# Library imports to authenticate in the Provider
# Library Imports for Authentication
# When implementing authentication for a provider, import the required libraries.
from prowler.config.config import load_and_validate_config_file
from prowler.lib.logger import logger
@@ -190,14 +81,14 @@ from prowler.lib.utils.utils import print_boxes
from prowler.providers.common.models import Audit_Metadata
from prowler.providers.common.provider import Provider
from prowler.providers.<new_provider_name>.models import (
# All providers models needed
# All provider models needed.
ProviderSessionModel,
ProviderIdentityModel,
ProviderOutputOptionsModel
)
class NewProvider(Provider):
# All properties from the class, some of this are properties in the base class
# All properties from the class, some of which are properties in the base class.
_type: str = "<provider_name>"
_session: <ProviderSessionModel>
_identity: <ProviderIdentityModel>
@@ -213,20 +104,30 @@ class NewProvider(Provider):
arguments (dict): A dictionary containing configuration arguments.
"""
logger.info("Setting <NewProviderName> provider ...")
# First get from arguments the necessary from the cloud account (subscriptions or projects or whatever the provider use for storing services)
# Set the session with the method enforced by parent class
# Initializing the Provider Session
# Steps:
# - Retrieve Account Information
# - Extract relevant account identifiers (subscriptions, projects, or other service references) from the provided arguments.
# Establish a Session
# Use the method enforced by the parent class to set up the session:
self._session = self.setup_session(credentials_file)
# Set the Identity class normaly the provider class give by Python provider library
# Define Provider Identity
# Assign the identity class, typically provided by the Python provider library:
self._identity = <ProviderIdentityModel>()
# Set the provider configuration
# Configure the Provider
# Set the provider-specific configuration.
self._audit_config = load_and_validate_config_file(
self._type, arguments.config_file
)
# All enforced properties by the parent class
# All the enforced properties by the parent class.
@property
def identity(self):
return self._identity
@@ -252,7 +153,7 @@ class NewProvider(Provider):
Sets up the Provider session.
Args:
<all_needed_for_auth> Can include all necessary arguments to setup the session
<all_needed_for_auth> Can include all necessary arguments to set up the session
Returns:
Credentials necessary to communicate with the provider.
@@ -262,11 +163,8 @@ class NewProvider(Provider):
"""
This method is enforced by parent class and is used to print all relevant
information during the prowler execution as a header of execution.
Normally the Account ID, User name or stuff like this is displayed in colors using the colorama module (Fore).
Displaying Account Information with Color Formatting. In Prowler, Account IDs, usernames, and other identifiers are typically displayed using color formatting provided by the colorama module (Fore).
"""
def print_credentials(self):
pass
```

View File

@@ -1,20 +1,25 @@
# Create a new security compliance framework
# Creating a New Security Compliance Framework in Prowler
## Introduction
If you want to create or contribute with your own security frameworks or add public ones to Prowler you need to make sure the checks are available if not you have to create your own. Then create a compliance file per provider like in `prowler/compliance/<provider>/` and name it as `<framework>_<version>_<provider>.json` then follow the following format to create yours.
To create or contribute a custom security framework for Prowler—or to integrate a public framework—you must ensure the necessary checks are available. If they are missing, they must be implemented before proceeding.
Each framework is defined in a compliance file per provider. The file should follow the structure used in `prowler/compliance/<provider>/` and be named `<framework>_<version>_<provider>.json`. Follow the format below to create your own.
## Compliance Framework
Each file version of a framework will have the following structure at high level with the case that each framework needs to be generally identified, one requirement can be also called one control but one requirement can be linked to multiple prowler checks.:
- `Framework`: string. Distinguish name of the framework, like CIS
- `Provider`: string. Provider where the framework applies, such as AWS, Azure, OCI,...
- `Version`: string. Version of the framework itself, like 1.4 for CIS.
- `Requirements`: array of objects. Include all requirements or controls with the mapping to Prowler.
- `Requirements_Id`: string. Unique identifier per each requirement in the specific framework
- `Requirements_Description`: string. Description as in the framework.
- `Requirements_Attributes`: array of objects. Includes all needed attributes per each requirement, like levels, sections, etc. Whatever helps to create a dedicated report with the result of the findings. Attributes would be taken as closely as possible from the framework's own terminology directly.
- `Requirements_Checks`: array. Prowler checks that are needed to prove this requirement. It can be one or multiple checks. In case of no automation possible this can be empty.
### Compliance Framework Structure
Each compliance framework file consists of structured metadata that identifies the framework and maps security checks to requirements or controls. Please note that a single requirement can be linked to multiple Prowler checks:
- `Framework`: string The distinguished name of the framework (e.g., CIS).
- `Provider`: string The cloud provider where the framework applies (AWS, Azure, OCI).
- `Version`: string The framework version (e.g., 1.4 for CIS).
- `Requirements`: array of objects. Defines security requirements and their mapping to Prowler checks. All requirements or controls are to be included with the mapping to Prowler.
- `Requirements_Id`: string A unique identifier for each requirement within the framework
- `Requirements_Description`: string The requirement description as specified in the framework.
- `Requirements_Attributes`: array of objects. Contains relevant metadata such as security levels, sections, and any additional data needed for reporting with the result of the findings. Attributes should be derived directly from the frameworks own terminology, ensuring consistency with its established definitions.
- `Requirements_Checks`: array. The Prowler checks that are needed to prove this requirement. It can be one or multiple checks. In case automation is not feasible, this can be empty.
```
{
@@ -23,9 +28,9 @@ Each file version of a framework will have the following structure at high level
"Requirements": [
{
"Id": "<unique-id>",
"Description": "Requirement full description",
"Description": "Full description of the requirement",
"Checks": [
"Here is the prowler check or checks that is going to be executed"
"Here is the prowler check or checks that will be executed"
],
"Attributes": [
{
@@ -38,4 +43,4 @@ Each file version of a framework will have the following structure at high level
}
```
Finally, to have a proper output file for your reports, your framework data model has to be created in `prowler/lib/outputs/models.py` and also the CLI table output in `prowler/lib/outputs/compliance.py`. Also, you need to add a new conditional in `prowler/lib/outputs/file_descriptors.py` if you create a new CSV model.
Finally, to have a proper output file for your reports, your framework data model has to be created in `prowler/lib/outputs/models.py` and also the CLI table output in `prowler/lib/outputs/compliance.py`. Also, you need to add a new conditional in `prowler/lib/outputs/file_descriptors.py` if creating a new CSV model.

View File

@@ -1,197 +1,184 @@
# Create a new Provider Service
# Prowler Services
Here you can find how to create a new service, or to complement an existing one, for a Prowler Provider.
Here you can find how to create a new service, or to complement an existing one, for a [Prowler Provider](./provider.md).
???+note
First ensure that the provider you want to add the service is already created. It can be checked [here](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers). If the provider is not present, please refer to the [Provider](./provider.md) documentation to create it from scratch.
## Introduction
In Prowler, a service is basically a solution that is offered by a cloud provider i.e. [ec2](https://aws.amazon.com/ec2/). Essentially it is a class that stores all the necessary stuff that we will need later in the checks to audit some aspects of our Cloud account.
In Prowler, a **service** represents a specific solution or resource offered by one of the supported [Prowler Providers](./provider.md), for example, [EC2](https://aws.amazon.com/ec2/) in AWS, or [Microsoft Exchange](https://www.microsoft.com/en-us/microsoft-365/exchange/exchange-online) in M365. Services are the building blocks that allow Prowler interact directly with the various resources exposed by each provider.
To create a new service, you will need to create a folder inside the specific provider, i.e. `prowler/providers/<provider>/services/<new_service_name>/`.
Each service is implemented as a class that encapsulates all the logic, data models, and API interactions required to gather and store information about that service's resources. All of this data is used by the [Prowler checks](./checks.md) to generate the security findings.
Inside that folder, you MUST create the following files:
## Adding a New Service
- An empty `__init__.py`: to make Python treat this service folder as a package.
- A `<new_service_name>_service.py`, containing all the service's logic and API calls.
- A `<new_service_name>_client_.py`, containing the initialization of the service's class we have just created so the checks's checks can use it.
To create a new service, a new folder must be created inside the specific provider following this pattern: `prowler/providers/<provider>/services/<new_service_name>/`.
## Service
Within this folder the following files are also to be created:
The Prowler's service structure is the following and the way to initialise it is just by importing the service client in a check.
- `__init__.py` (empty) Ensures Python recognizes this folder as a package.
- `<new_service_name>_service.py` Contains all the logic and API calls of the service.
- `<new_service_name>_client_.py` Contains the initialization of the freshly created service's class so that the checks can use it.
## Service Structure and Initialisation
The Prowler's service structure is as outlined below. To initialise it, just import the service client in a check.
### Service Base Class
All the Prowler provider's services inherits from a base class depending on the provider used.
All Prowler provider service should inherit from a common base class to avoid code duplication. This base class handles initialization and storage of functions and objects needed across services. The exact implementation depends on the provider's API requirements, but the following are the most common responsibilities:
- Initialize/store clients to interact with the provider's API.
- Store the audit and fixer configuration.
- Implement threading logic where applicable.
For reference, the base classes for each provider can be checked here:
- [AWS Service Base Class](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/aws/lib/service/service.py)
- [GCP Service Base Class](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/gcp/lib/service/service.py)
- [Azure Service Base Class](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/azure/lib/service/service.py)
- [Kubernetes Service Base Class](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/kubernetes/lib/service/service.py)
Each class is used to initialize the credentials and the API's clients to be used in the service. If some threading is used it must be coded there.
- [M365 Service Base Class](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/m365/lib/service/service.py)
- [GitHub Service Base Class](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/github/lib/service/service.py)
### Service Class
Due to the complexity and differences of each provider API we are going to use an example service to guide you in how can it be created.
Due to the complexity and differences across provider APIs, the following example demonstrates best practices for structuring a service in Prowler.
The following is the `<new_service_name>_service.py` file:
File `<new_service_name>_service.py`:
```python title="Service Class"
```python title="Example Service Class"
from datetime import datetime
from typing import Optional
# The following is just for the AWS provider
from botocore.client import ClientError
# To use the Pydantic's BaseModel
# To use the Pydantic's BaseModel.
from pydantic import BaseModel
# Prowler logging library
# Prowler logging library.
from prowler.lib.logger import logger
# Prowler resource filter, only for the AWS provider
from prowler.lib.scan_filters.scan_filters import is_resource_filtered
# Provider parent class
# Provider parent class.
from prowler.providers.<provider>.lib.service.service import ServiceParentClass
# Create a class for the Service
# Create a class for the Service.
class <Service>(ServiceParentClass):
def __init__(self, provider):
# Call Service Parent Class __init__
# We use the __class__.__name__ to get it automatically
# from the Service Class name but you can pass a custom
# string if the provider's API service name is different
def __init__(self, provider: Provider):
"""Initialize the Service Class
Args:
provider: Prowler Provider object.
"""
# Call Service Parent Class __init__.
# The __class__.__name__ is used to obtain it automatically.
# From the Service Class name, but a custom one can be passed.
# String in case the provider's API service name is different.
super().__init__(__class__.__name__, provider)
# Create an empty dictionary of items to be gathered,
# using the unique ID as the dictionary key
# e.g., instances
# Create an empty dictionary of items to be gathered, using the unique ID as the dictionarys key, e.g., instances.
self.<items> = {}
# If you can parallelize by regions or locations
# you can use the __threading_call__ function
# available in the Service Parent Class
# If parallelization can be carried out by regions or locations, the function __threading_call__ to be used must be implemented in the Service Parent Class.
# If it is not implemented, you can make it in a sequential way, just calling the function.
self.__threading_call__(self.__describe_<items>__)
# Optionally you can create another function to retrieve
# more data about each item without parallel
self.__describe_<item>__()
# If it is needed you can create another function to retrieve more data from the items.
# Here we are using the second parameter of the __threading_call__ function to create one thread per item.
# You can also make it sequential without using the __threading_call__ function iterating over the items inside the function.
self.__threading_call__(self.__describe_<item>__, self.<items>.values())
# In case of use the __threading_call__ function, you have to pass the regional_client to the function, as a parameter.
def __describe_<items>__(self, regional_client):
"""Get ALL <Service> <Items>"""
"""Get all <items> and store in the self.<items> dictionary
Args:
regional_client: Regional client object.
"""
logger.info("<Service> - Describing <Items>...")
# We MUST include a try/except block in each function
# A try-except block must be created in each function.
try:
# Call to the provider API to retrieve the data we want
# If pagination is supported by the provider, is always better to use it, call to the provider API to retrieve the desired data.
describe_<items>_paginator = regional_client.get_paginator("describe_<items>")
# Paginator to get every item
# Paginator to get every item.
for page in describe_<items>_paginator.paginate():
# Another try/except within the loop for to continue looping
# if something unexpected happens
# Another try-except within the for loop to continue iterating in case something unexpected happens.
try:
for <item> in page["<Items>"]:
# For the AWS provider we MUST include the following lines to retrieve
# or not data for the resource passed as argument using the --resource-arn
if not self.audit_resources or (
is_resource_filtered(<item>["<item_arn>"], self.audit_resources)
):
# Then we have to include the retrieved resource in the object
# previously created
self.<items>[<item_unique_id>] =
<Item>(
arn=stack["<item_arn>"],
name=stack["<item_name>"],
tags=stack.get("Tags", []),
region=regional_client.region,
)
# Adding Retrieved Resources to the Object
# Once the resource has been retrieved, it must be included in the previously created object to ensure proper data handling within the service.
self.<items>[<item_unique_id>] =
<Item>(
arn=stack["<item_arn>"],
name=stack["<item_name>"],
tags=stack.get("Tags", []),
region=regional_client.region,
)
except Exception as error:
logger.error(
f"{<provider_specific_field>} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
# In the except part we have to use the following code to log the errors
except Exception as error:
# Depending on each provider we can use the following fields in the logger:
# - AWS: regional_client.region or self.region
# - GCP: project_id and location
# - Azure: subscription
# Logging Errors in Exception Handling
# When handling exceptions, use the following approach to log errors appropriately based on the cloud provider being used:
except Exception as error:
# Depending on each provider we can must use different fields in the logger, e.g.: AWS: regional_client.region or self.region, GCP: project_id and location, Azure: subscription
logger.error(
f"{<provider_specific_field>} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def __describe_<item>__(self):
"""Get Details for a <Service> <Item>"""
logger.info("<Service> - Describing <Item> to get specific details...")
def __describe_<item>__(self, item: <Item>):
"""Get details for a <item>
# We MUST include a try/except block in each function
Args:
item: Item object.
"""
logger.info("<Service> - Describing <Item> to get specific details...")
# A try-except block must be created in each function.
try:
# Loop over the items retrieved in the previous function
for <item> in self.<items>:
<item>_details = self.regional_clients[<item>.region].describe_<item>(
<Attribute>=<item>.name
)
# When we perform calls to the Provider API within a for loop we have
# to include another try/except block because in the cloud there are
# ephemeral resources that can be deleted at the time we are checking them
try:
<item>_details = self.regional_clients[<item>.region].describe_<item>(
<Attribute>=<item>.name
)
# For example, check if item is Public. Here is important if we are
# getting values from a dictionary we have to use the "dict.get()"
# function with a default value in the case this value is not present
<item>.public = <item>_details.get("Public", False)
# In this except block, for example for the AWS Provider we can use
# the botocore.ClientError exception and check for a specific error code
# to raise a WARNING instead of an ERROR if some resource is not present.
except ClientError as error:
if error.response["Error"]["Code"] == "InvalidInstanceID.NotFound":
logger.warning(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
else:
logger.error(
f"{<provider_specific_field>} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
continue
# In the except part we have to use the following code to log the errors
# E.g., check if item is Public. This case is important: if values are being retrieved from a dictionary, the function "dict.get()" must be used with a default value in case this value is not present.
<item>.public = <item>_details.get("Public", False)
except Exception as error:
# Depending on each provider we can use the following fields in the logger:
# - AWS: regional_client.region or self.region
# - GCP: project_id and location
# - Azure: subscription
# Fields for logging errors with relevant item information, e.g.: AWS: <item>.region, GCP: <item>.project_id, Azure: <item>.region
logger.error(
f"{<item>.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
```
???+note
To avoid fake findings, when Prowler can't retrieve the items, because an Access Denied or similar error, we set that items value as `None`.
To prevent false findings, when Prowler fails to retrieve items due to Access Denied or similar errors, the affected item's value is set to `None`.
#### Service Models
Service models are classes that are used in the service to design all that we need to store in each class object extrated from API calls. We use the Pydantic's [BaseModel](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel) to take advantage of the data validation.
Service models define structured classes used within services to store and process data extracted from API calls.
Using Pydantic for Data Validation
Prowler leverages Pydantic's [BaseModel](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel) to enforce data validation.
```python title="Service Model"
# In each service class we have to create some classes using
# the Pydantic's Basemodel for the resources we want to audit.
# Implementation Approach
# Each service class should include custom model classes using Pydantic's BaseModel for the resources being audited.
class <Item>(BaseModel):
"""<Item> holds a <Service> <Item>"""
arn: str
"""<Items>[].arn"""
id: str
"""<Items>[].id"""
name: str
"""<Items>[].name"""
@@ -202,26 +189,34 @@ class <Item>(BaseModel):
public: bool
"""<Items>[].public"""
# We can create Optional attributes set to None by default
# Optional attributes can be created set to None by default.
tags: Optional[list]
"""<Items>[].tags"""
```
#### Service Objects
In the service each group of resources should be created as a Python [dictionary](https://docs.python.org/3/tutorial/datastructures.html#dictionaries). This is because we are performing lookups all the time and the Python dictionary lookup has [O(1) complexity](https://en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_functions).
We MUST set as the dictionary key a unique ID, like the resource Unique ID or ARN.
#### Service Attributes
*Optimized Data Storage with Python Dictionaries*
Each group of resources within a service should be structured as a Python [dictionary](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) to enable efficient lookups. The dictionary lookup operation has [O(1) complexity](https://en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_functions), and lookups are constantly executed.
*Assigning Unique Identifiers*
Each dictionary key must be a unique ID to identify the resource in a univocal way.
Example:
```python
self.vpcs = {}
self.vpcs["vpc-01234567890abcdef"] = VPC_Object_Class()
self.virtual_machines = {}
self.virtual_machines["vm-01234567890abcdef"] = VirtualMachine()
```
### Service Client
Each Prowler service requires a service client to use the service in the checks.
The following is the `<new_service_name>_client.py` containing the initialization of the service's class we have just created so the service's checks can use them:
The following is the `<new_service_name>_client.py` file, which contains the initialization of the freshly created service's class so that service checks can use it. This file is almost the same for all the services among the providers:
```python
from prowler.providers.common.provider import Provider
@@ -230,11 +225,13 @@ from prowler.providers.<provider>.services.<new_service_name>.<new_service_name>
<new_service_name>_client = <Service>(Provider.get_global_provider())
```
## Permissions
## Provider Permissions in Prowler
It is really important to check if the current Prowler's permissions for each provider are enough to implement a new service. If we need to include more please refer to the following documentaion and update it:
Before implementing a new service, verify that Prowlers existing permissions for each provider are sufficient. If additional permissions are required, refer to the relevant documentation and update accordingly.
- AWS: https://docs.prowler.cloud/en/latest/getting-started/requirements/#aws-authentication
- Azure: https://docs.prowler.cloud/en/latest/getting-started/requirements/#permissions
- GCP: https://docs.prowler.cloud/en/latest/getting-started/requirements/#gcp-authentication
- M365: https://docs.prowler.cloud/en/latest/getting-started/requirements/#m365-authentication
Provider-Specific Permissions Documentation:
- [AWS](../getting-started/requirements.md#authentication)
- [Azure](../getting-started/requirements.md#needed-permissions)
- [GCP](../getting-started/requirements.md#needed-permissions_1)
- [M365](../getting-started/requirements.md#needed-permissions_2)

View File

@@ -1,20 +1,20 @@
# Unit Tests
# Unit Tests for Prowler Checks
The unit tests for the Prowler checks varies between each provider supported.
Unit tests for Prowler checks vary based on the provider being evaluated.
Here we left some good reads about unit testing and things we've learnt through all the process.
Below are key resources and insights gained throughout the testing process.
**Python Testing**
- https://docs.python-guide.org/writing/tests/
**Where to patch**
**Where to Patch**
- https://docs.python.org/3/library/unittest.mock.html#where-to-patch
- https://stackoverflow.com/questions/893333/multiple-variables-in-a-with-statement
- https://docs.python.org/3/reference/compound_stmts.html#the-with-statement
**Utils to trace mocking and test execution**
**Utilities for Tracing Mocking and Test Execution**
- https://news.ycombinator.com/item?id=36054868
- https://docs.python.org/3/library/sys.html#sys.settrace
@@ -22,175 +22,251 @@ Here we left some good reads about unit testing and things we've learnt through
## General Recommendations
When creating tests for some provider's checks we follow these guidelines trying to cover as much test scenarios as possible:
When writing tests for Prowler provider checks, follow these guidelines to maximize coverage across test scenarios:
1. Create a test without resource to generate 0 findings, because Prowler will generate 0 findings if a service does not contain the resources the check is looking for audit.
2. Create test to generate both a `PASS` and a `FAIL` result.
3. Create tests with more than 1 resource to evaluate how the check behaves and if the number of findings is right.
1. Zero Findings Scenario:
Develop tests where no resources exist. Prowler returns zero findings if the audited service lacks the required resources.
## How to run Prowler tests
2. Positive and Negative Outcomes:
Create tests that generate both a passing (`PASS`) and a failing (`FAIL`) result.
To run the Prowler test suite you need to install the testing dependencies already included in the `pyproject.toml` file. If you didn't install it yet please read the developer guide introduction [here](./introduction.md#get-the-code-and-install-all-dependencies).
3. Multi-Resource Evaluations:
Design tests with multiple resources to verify check behavior and ensure the correct number of findings.
Then in the project's root path execute `pytest -n auto -vvv -s -x` or use the `Makefile` with `make test`.
## Running Prowler Tests
Other commands to run tests:
To execute the Prowler test suite, install the necessary dependencies listed in the `pyproject.toml` file.
- Run tests for a provider: `pytest -n auto -vvv -s -x tests/providers/<provider>/services`
- Run tests for a provider service: `pytest -n auto -vvv -s -x tests/providers/<provider>/services/<service>`
- Run tests for a provider check: `pytest -n auto -vvv -s -x tests/providers/<provider>/services/<service>/<check>`
### Prerequisites
If you have not installed Prowler yet, refer to the [developer guide introduction](./introduction.md#get-the-code-and-install-all-dependencies).
### Executing Tests
Navigate to the project's root directory and execute: `pytest -n auto -vvv -s -x`
Alternatively, use:
`Makefile` with `make test`.
Other Commands for Running Tests
- Running tests for a provider:
`pytest -n auto -vvv -s -x tests/providers/<provider>/services`
- Running tests for a provider service:
`pytest -n auto -vvv -s -x tests/providers/<provider>/services/<service>`
- Running tests for a provider check:
`pytest -n auto -vvv -s -x tests/providers/<provider>/services/<service>/<check>`
???+ note
Refer to the [pytest documentation](https://docs.pytest.org/en/7.1.x/getting-started.html) documentation for more information.
Refer to the [pytest documentation](https://docs.pytest.org/en/7.1.x/getting-started.html) for more details.
## AWS
## AWS Testing Approaches
For the AWS provider we have ways to test a Prowler check based on the following criteria:
For AWS provider, different testing approaches apply based on API coverage based on several criteria.
???+ note
We use and contribute to the [Moto](https://github.com/getmoto/moto) library which allows us to easily mock out tests based on AWS infrastructure. **It's awesome!**
Prowler leverages and contributes to the[Moto](https://github.com/getmoto/moto) library for mocking AWS infrastructure in tests.
- AWS API calls covered by [Moto](https://github.com/getmoto/moto):
- Service tests with `@mock_aws`
- Checks tests with `@mock_aws`
- AWS API calls not covered by Moto:
- Service test with `mock_make_api_call`
- Checks tests with [MagicMock](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.MagicMock)
- AWS API calls partially covered by Moto:
- Service test with `@mock_aws` and `mock_make_api_call`
- Checks tests with `@mock_aws` and `mock_make_api_call`
- AWS API Calls Covered by [Moto](https://github.com/getmoto/moto):
- Service Tests: `@mock_aws`
- Checks Tests: `@mock_aws`
In the following section we are going to explain all of the above scenarios with examples. The main difference between those scenarios comes from if the [Moto](https://github.com/getmoto/moto) library covers the AWS API calls made by the service. You can check the covered API calls [here](https://github.com/getmoto/moto/blob/master/IMPLEMENTATION_COVERAGE.md).
- AWS API Calls Not Covered by Moto:
- Service Tests: `mock_make_api_call`
- Checks Tests: [MagicMock](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.MagicMock)
### Checks
- AWS API Calls Partially Covered by Moto:
- Service Tests: `@mock_aws` and `mock_make_api_call`
- Check Tests: `@mock_aws` and `mock_make_api_call`
For the AWS tests examples we are going to use the tests for the `iam_password_policy_uppercase` check.
#### AWS Check Testing Scenarios
This section is going to be divided based on the API coverage of the [Moto](https://github.com/getmoto/moto) library.
The following section provides examples for each testing scenario. The primary distinction between these scenarios depends on whether the [Moto](https://github.com/getmoto/moto) library covers the AWS API calls made by the service. You can review the supported API calls [here](https://github.com/getmoto/moto/blob/master/IMPLEMENTATION_COVERAGE.md).
#### API calls covered
### AWS Check Testing Approach
If the [Moto](https://github.com/getmoto/moto) library covers the API calls we want to test, we can use the `@mock_aws` decorator. This will mocked out all the API calls made to AWS keeping the state within the code decorated, in this case the test function.
For AWS test examples, we reference tests for the `iam_password_policy_uppercase` check.
This section is categorized based on [Moto](https://github.com/getmoto/moto) API coverage.
#### API Calls Covered by Moto
When the [Moto](https://github.com/getmoto/moto) library supports the API calls required for testing, use the `@mock_aws` decorator. This ensures that all AWS API calls within the decorated function are properly mocked while maintaining state within the test.
```python
# We need to import the unittest.mock to allow us to patch some objects
# not to use shared ones between test, hence to isolate the test
# Import unittest.mock to enable object patching
# This prevents shared objects between tests, ensuring test isolation
from unittest import mock
# Boto3 client and session to call the AWS APIs
# Import Boto3 client and session for AWS API calls
from boto3 import client, session
# Moto decorator
# Import Moto decorator for mocking AWS services
from moto import mock_aws
# Constants used
# Define constants for test execution
AWS_ACCOUNT_NUMBER = "123456789012"
AWS_REGION = "us-east-1"
# We always name the test classes like Test_<check_name>
# Test class naming convention: Test_<check_name>
class Test_iam_password_policy_uppercase:
# We include the Moto decorator
# Apply the Moto decorator for AWS service mocking
@mock_aws
# We name the tests with test_<service>_<check_name>_<test_action>
# Test naming convention: test_<service>_<check_name>_<test_action>
def test_iam_password_policy_no_uppercase_flag(self):
# First, we have to create an IAM client
Steps
# Step 1: Create an IAM client for API calls in the specified region
iam_client = client("iam", region_name=AWS_REGION)
# Then, since all the AWS accounts have a password
# policy we want to set to False the RequireUppercaseCharacters
# Step 2: Modify the account password policy to disable uppercase character enforcement
# Action: Setting RequireUppercaseCharacters to False
iam_client.update_account_password_policy(RequireUppercaseCharacters=False)
# The aws_provider is mocked using set_mocked_aws_provider to use it as the return of the get_global_provider method.
# this mocked provider is defined in fixtures
# Step 3: Mock the AWS provider to ensure isolated testing
# Using 'set_mocked_aws_provider' allows overriding the provider response
# This mocked provider is defined in test fixtures
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
# The Prowler service import MUST be made within the decorated
# code not to make real API calls to the AWS service.
# Step 4: Ensure Prowler service imports occur within the decorated function
# This prevents accidental real API calls to AWS during test execution
from prowler.providers.aws.services.iam.iam_service import IAM
# Prowler for AWS uses a shared object called aws_provider where it stores
# the info related with the provider
# Mocking AWS Provider and IAM Client for Prowler Tests
#Prowler for AWS relies on a shared object, aws_provider, which stores provider-related information.
# To ensure proper test isolation and prevent shared objects between tests, we apply mocking techniques.
# Mocking Global AWS Provider
#To mock the global provider, we use mock.patch() to override the get_global_provider() method, ensuring aws_provider is the return value.
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
),
# We have to mock also the iam_client from the check to enforce that the iam_client used is the one
# created within this check because patch != import, and if you execute tests in parallel some objects
# can be already initialised hence the check won't be isolated
mock.patch(
# Mocking IAM Client for Test Isolation
#In addition to mocking the provider, we must also mock the iam_client from the check. This ensures that the IAM client used in the test is the one explicitly created within the test.
# ⚠️ Important:
# patch != import—simply importing does not ensure proper isolation.
# Running tests in parallel may cause unintended object initialization, impacting test integrity.
with mock.patch(
"prowler.providers.aws.services.iam.iam_password_policy_uppercase.iam_password_policy_uppercase.iam_client",
new=IAM(aws_provider),
):
# We import the check within the two mocks not to initialise the iam_client with some shared information from
# the aws_provider or the IAM service.
# Importing the IAM Check
# To prevent initialization issues, import the check inside the two-mock context.
# This ensures the IAM client does not retain shared data from aws_provider or the IAM service.
from prowler.providers.aws.services.iam.iam_password_policy_uppercase.iam_password_policy_uppercase import (
iam_password_policy_uppercase,
)
# Once imported, we only need to instantiate the check's class
# Executing the IAM Check
# Once imported, instantiate the checks class.
check = iam_password_policy_uppercase()
# And then, call the execute() function to run the check
# against the IAM client we've set up.
# Then run the execute function()
# against the set up IAM client.
result = check.execute()
# Last but not least, we need to assert all the fields
# from the check's results
# Validating the Check Results
# Finally, assert all fields to verify expected results.
assert len(results) == 1
assert result[0].status == "FAIL"
assert result[0].status_extended == "IAM password policy does not require at least one uppercase letter."
assert result[0].status_extended == "IAM password policy does not srequire at least one uppercase letter."
assert result[0].resource_arn == f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"
assert result[0].resource_id == AWS_ACCOUNT_NUMBER
assert result[0].resource_tags == []
assert result[0].region == AWS_REGION
```
#### API calls not covered
#### Handling API Calls Not Covered by Moto
If the IAM service for the check's we want to test is not covered by Moto, we have to inject the objects in the service client using [MagicMock](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.MagicMock). As we have pointed above, we cannot instantiate the service since it will make real calls to the AWS APIs.
If the IAM service required for testing is not supported by the Moto library, use [MagicMock](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.MagicMock) to inject objects into the service client.
???+ warning
As stated above, direct service instantiation must be avoided to prevent actual AWS API calls.
???+ note
The following example uses the IAM GetAccountPasswordPolicy which is covered by Moto but this is only for demonstration purposes.
The example below demonstrates the IAM GetAccountPasswordPolicy API, which is covered by Moto, but is used for instructional purposes only.
The following code shows how to use MagicMock to create the service objects.
#### Mocking Service Objects Using MagicMock
The following code demonstrates how to use MagicMock to create service objects.
```python
# We need to import the unittest.mock to allow us to patch some objects
# not to use shared ones between test, hence to isolate the test
# Import unittest.mock to enable object patching
# This prevents shared objects between tests, ensuring test isolation
from unittest import mock
# Constants used
# Define constants for test execution
AWS_ACCOUNT_NUMBER = "123456789012"
AWS_REGION = "us-east-1"
# We always name the test classes like Test_<check_name>
# Test class naming convention: Test_<check_name>
class Test_iam_password_policy_uppercase:
# We name the tests with test_<service>_<check_name>_<test_action>
# Test naming convention: test_<service>_<check_name>_<test_action>
def test_iam_password_policy_no_uppercase_flag(self):
# Mocked client with MagicMock
# Mock IAM client with MagicMock
mocked_iam_client = mock.MagicMock
# Since the IAM Password Policy has their own model we have to import it
# Import IAM PasswordPolicy model, as it has its own model
from prowler.providers.aws.services.iam.iam_service import PasswordPolicy
# Create the mock PasswordPolicy object
# Create a mock PasswordPolicy object with predefined attributes
mocked_iam_client.password_policy = PasswordPolicy(
length=5,
symbols=True,
numbers=True,
# We set the value to False to test the check
# The value must be set to False to trigger a failure scenario
uppercase=False,
lowercase=True,
allow_change=False,
expiration=True,
)
# In this scenario we have to mock also the IAM service and the iam_client from the check to enforce # that the iam_client used is the one created within this check because patch != import, and if you # execute tests in parallel some objects can be already initialised hence the check won't be isolated.
# In this case we don't use the Moto decorator, we use the mocked IAM client for both objects
# In this scenario, both the IAM service and the iam_client from the check must be mocked to ensure test isolation. This guarantees that the iam_client used in the test is the one explicitly instantiated within the test itself.
# Note: Simply applying a patch does not modify imports (patch != import).
# If tests are executed in parallel, objects may already be initialized,
# leading to unintended shared state and breaking test isolation.
# Unlike other cases, we do not use the Moto decorator here.
# Instead, we mock the IAM client for both objects to prevent real AWS API interactions.
with mock.patch(
"prowler.providers.aws.services.iam.iam_service.IAM",
new=mocked_iam_client,
@@ -198,21 +274,31 @@ class Test_iam_password_policy_uppercase:
"prowler.providers.aws.services.iam.iam_client.iam_client",
new=mocked_iam_client,
):
# We import the check within the two mocks not to initialise the iam_client with some shared information from
# the aws_provider or the IAM service.
# Importing the IAM Check
# To prevent initialization issues, import the check inside the two-mock context.
# This ensures the IAM client does not retain shared data from aws_provider or the IAM service.
from prowler.providers.aws.services.iam.iam_password_policy_uppercase.iam_password_policy_uppercase import (
iam_password_policy_uppercase,
)
# Once imported, we only need to instantiate the check's class
# Executing the IAM Check
# Once imported, instantiate the checks class.
check = iam_password_policy_uppercase()
# And then, call the execute() function to run the check
# against the IAM client we've set up.
# Then run the execute function()
# against the set up IAM client.
result = check.execute()
# Last but not least, we need to assert all the fields
# from the check's results
# Validating the Check Results
# Finally, assert all fields to verify expected results.
assert len(results) == 1
assert result[0].status == "FAIL"
assert result[0].status_extended == "IAM password policy does not require at least one uppercase letter."
@@ -222,14 +308,15 @@ class Test_iam_password_policy_uppercase:
assert result[0].region == AWS_REGION
```
As it can be seen in the above scenarios, the check execution should always be into the context of mocked/patched objects. This way we ensure it reviews only the objects created under the scope the test.
#### Ensuring Test Isolation with Mocked/Patched Objects
#### API calls partially covered
In all above scenarios, check execution must occur within the context of mocked or patched objects. This guarantees that the test only evaluates objects explicitly created within its scope, preventing interference from shared state or external dependencies.
If the API calls we want to use in the service are partially covered by the Moto decorator we have to create our own mocked API calls to use it in combination.
#### Handling Partially Covered API Calls
To do so, you need to mock the `botocore.client.BaseClient._make_api_call` function, which is the Boto3 function in charge of making the real API call to the AWS APIs, using `mock.patch <https://docs.python.org/3/library/unittest.mock.html#patch>`:
When a service requires API calls that are partially covered by the Moto decorator, additional mocking is necessary. In such cases, custom mocked API calls must be implemented alongside Moto to ensure full coverage.
To achieve this, mock the `botocore.client.BaseClient._make_api_call` function—the method responsible for making actual API requests to AWS—using `mock.patch <https://docs.python.org/3/library/unittest.mock.html#patch>`:
```python
@@ -239,13 +326,18 @@ from unittest.mock import patch
from moto import mock_aws
# Original botocore _make_api_call function
orig = botocore.client.BaseClient._make_api_call
# Mocked botocore _make_api_call function
def mock_make_api_call(self, operation_name, kwarg):
# As you can see the operation_name has the get_account_password_policy snake_case form but
# we are using the GetAccountPasswordPolicy form.
# Rationale -> https://github.com/boto/botocore/blob/develop/botocore/client.py#L810:L816
# The 'operation_name' follows the snake_case format (get_account_password_policy),
# but we use the PascalCase form (GetAccountPasswordPolicy) for consistency with Boto3 conventions.
# Reference: https://github.com/boto/botocore/blob/develop/botocore/client.py#L810:L816
if operation_name == 'GetAccountPasswordPolicy':
return {
'PasswordPolicy': {
@@ -261,29 +353,41 @@ def mock_make_api_call(self, operation_name, kwarg):
'HardExpiry': True|False
}
}
# If we don't want to patch the API call
# If API call patching is not required, return the original method execution.
return orig(self, operation_name, kwarg)
# We always name the test classes like Test_<check_name>
# Test class naming convention: Test_<check_name>
class Test_iam_password_policy_uppercase:
# We include the custom API call mock decorator for the service we want to use
# Apply custom API call mock decorator for the required service
@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call)
# We include also the IAM Moto decorator for the API calls supported
# Also include IAM Moto decorator for supported API calls
@mock_iam
# We name the tests with test_<service>_<check_name>_<test_action>
# Test naming convention: test_<service>_<check_name>_<test_action>
def test_iam_password_policy_no_uppercase_flag(self):
# Check the previous section to see the check test since is the same
# Refer to the previous section for the check test, as the implementation remains unchanged.
```
Note that this does not use Moto, to keep it simple, but if you use any `moto`-decorators in addition to the patch, the call to `orig(self, operation_name, kwarg)` will be intercepted by Moto.
???+ note
This example does not use Moto to simplify the setup.
However, if additional `moto` decorators are applied alongside the patch, Moto will automatically intercept the call to `orig(self, operation_name, kwarg)`.
???+ note
The above code comes from here https://docs.getmoto.org/en/latest/docs/services/patching_other_services.html
The source of the above implementation can be found here:[Patch Other Services with Moto](https://docs.getmoto.org/en/latest/docs/services/patching\_other\_services.html)
#### Mocking more than one service
#### Mocking Several Services
Since the provider is being mocked, multiple attributes can be configured to customize its behavior:
Since we are mocking the provider, it can be customized setting multiple attributes to the provider:
```python
def set_mocked_aws_provider(
audited_regions: list[str] = [],
@@ -306,8 +410,7 @@ def set_mocked_aws_provider(
) -> AwsProvider:
```
If the test your are creating belongs to a check that uses more than one provider service, you should mock each of the services used. For example, the check `cloudtrail_logs_s3_bucket_access_logging_enabled` requires the CloudTrail and the S3 client, hence the service's mock part of the test will be as follows:
If a test is designed for a check that interacts with multiple provider services, each service used must be individually mocked. For instance, if the check `cloudtrail_logs_s3_bucket_access_logging_enabled` relies on both the CloudTrail and S3 clients, the test's service mocking section should be structured as follows:
```python
with mock.patch(
@@ -328,42 +431,45 @@ with mock.patch(
):
```
As you can see in the above code, it is required to mock the AWS audit info and both services used.
As demonstrated in the code above, mocking both the AWS audit information and all utilized services is mandatory for proper test execution.
#### Patching vs. Importing
This is an important topic within the Prowler check's unit testing. Due to the dynamic nature of the check's load, the process of importing the service client from a check is the following:
Properly understanding patching versus importing is critical for unit testing with Prowler checks. Given the dynamic nature of the check-loading mechanism, the process for importing a service client within a check follows this structured approach:
1. `<check>.py`:
```python
from prowler.providers.<provider>.services.<service>.<service>_client import <service>_client
```
```python
from prowler.providers.<provider>.services.<service>.<service>_client import <service>_client
```
2. `<service>_client.py`:
```python
from prowler.providers.common.provider import Provider
from prowler.providers.<provider>.services.<service>.<service>_service import <SERVICE>
<service>_client = <SERVICE>(Provider.get_global_provider())
```
```python
from prowler.providers.common.provider import Provider
from prowler.providers.<provider>.services.<service>.<service>_service import <SERVICE>
Due to the above import path it's not the same to patch the following objects because if you run a bunch of tests, either in parallel or not, some clients can be already instantiated by another check, hence your test execution will be using another test's service instance:
<service>_client = <SERVICE>(Provider.get_global_provider())
```
Due to the import path structure, patching certain objects does not always ensure full isolation. If multiple tests—executed sequentially or in parallel—reuse service clients, some instances may already be initialized by another check. This can lead to unintended shared state, affecting test accuracy:
- `<service>_client` imported at `<check>.py`
- `<service>_client` initialised at `<service>_client.py`
- `<SERVICE>` imported at `<service>_client.py`
A useful read about this topic can be found in the following article: https://stackoverflow.com/questions/8658043/how-to-mock-an-import
#### Additional Resources on Mocking Imports
For a deeper understanding of mocking imports in Python, refer to the following article: https://stackoverflow.com/questions/8658043/how-to-mock-an-import
#### Different ways to mock the service client
#### Approaches to Mocking a Service Client
##### Mocking the service client at the service client level
1\. Mocking the Service Client at the Service Client Level
Mocking a service client using the following code ...
2\. Mocking a Service Client via Below Code Implementation
Once all required attributes are configured for the mocked provider, it can be used as the service client for test execution:
Once the needed attributes are set for the mocked provider, you can use the mocked provider:
```python title="Mocking the service_client"
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
@@ -373,18 +479,20 @@ with mock.patch(
new=<SERVICE>(set_mocked_aws_provider([<region>])),
):
```
will cause that the service will be initialised twice:
1. When the `<SERVICE>(set_mocked_aws_provider([<region>]))` is mocked out using `mock.patch` to have the object ready for the patching.
2. At the `<service>_client.py` when we are patching it since the `mock.patch` needs to go to that object an initialise it, hence the `<SERVICE>(set_mocked_aws_provider([<region>]))` will be called again.
1. When `<SERVICE>(set_mocked_aws_provider([<region>]))` is mocked out using `mock.patch`, it must be properly prepared before patching to ensure test consistency.
Then, when we import the `<service>_client.py` at `<check>.py`, since we are mocking where the object is used, Python will use the mocked one.
2. At the point of patching, in `<service>_client.py`, and since `mock.patch` needs to access said object and initialise it, `<SERVICE>(set_mocked_aws_provider([<region>]))` will be called again.
In the [next section](./unit-testing.md#mocking-the-service-and-the-service-client-at-the-service-client-level) you will see an improved version to mock objects.
Later, when importing `<service>_client.py` at `<check>.py`, Python uses the mocked instance since the patch was applied at the correct reference point.
In the [next section](./unit-testing.md#mocking-the-service-and-the-service-client-at-the-service-client-level) we will explore an improved approach to mock objects.
##### Mocking the service and the service client at the service client level
Mocking a service client using the following code ...
##### Mocking the Service and the Service Client at the Service Client Level
##### Mocking a Service Client via Below Code Implementation
```python title="Mocking the service and the service_client"
with mock.patch(
@@ -398,35 +506,42 @@ with mock.patch(
new=service_client,
):
```
will cause that the service will be initialised once, just when the `set_mocked_aws_provider([<region>])` is mocked out using `mock.patch`.
Then, at the check_level when Python tries to import the client with `from prowler.providers.<provider>.services.<service>.<service>_client`, since it is already mocked out, the execution will continue using the `service_client` without getting into the `<service>_client.py`.
will cause that the service is initialized only once—at the moment of mocking out `set_mocked_aws_provider([<region>])` using `mock.patch`.
Later, when Python attempts to import the client at the check level, the execution continues using`from prowler.providers.<provider>.services.<service>.<service>_client`. As a result of it being already mocked out, the execution will continue using `service_client` without getting into `<service>_client.py`.
### Services
### Testing AWS Services
For testing the AWS services we have to follow the same logic as with the AWS checks, we have to check if the AWS API calls made by the service are covered by Moto and we have to test the service `__init__` to verify that the information is being correctly retrieved.
AWS service testing follows the same methodology as AWS checks:
Verify whether the AWS API calls made by the service are covered by Moto.
The service tests could act as *Integration Tests* since we test how the service retrieves the information from the provider, but since Moto or the custom mock objects mocks that calls this test will fall into *Unit Tests*.
Execute tests on the service `__init__` to ensure correct information retrieval.
Please refer to the [AWS checks tests](./unit-testing.md#checks) for more information on how to create tests and check the existing services tests [here](https://github.com/prowler-cloud/prowler/tree/master/tests/providers/aws/services).
While service tests resemble *Integration Tests*, as they assess how the service interacts with the provider, they ultimately fall under *Unit Tests*, due to the use of Moto or custom mock objects.
For detailed guidance on test creation and existing service tests, refer to the [AWS checks test](./unit-testing.md#checks) [documentation](https://github.com/prowler-cloud/prowler/tree/master/tests/providers/aws/services).
## GCP
### Checks
### GCP Check Testing Approach
For the GCP Provider we don't have any library to mock out the API calls we use. So in this scenario we inject the objects in the service client using [MagicMock](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.MagicMock).
Currently the GCP Provider does not have a dedicated library for mocking API calls. To ensure proper test isolation, objects must be manually injected into the service client using [MagicMock](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.MagicMock).
The following code shows how to use MagicMock to create the service objects for a GCP check test. It is a real example adapted for informative purposes.
Mocking Service Objects Using MagicMock
The following code demonstrates how to use MagicMock to create service objects for a GCP check test. This is a real-world implementation, adapted for instructional clarity.
```python
from re import search
from unittest import mock
# Import some constant values needed in every check
# Import constant values needed in every check
from tests.providers.gcp.gcp_fixtures import GCP_PROJECT_ID, set_mocked_gcp_provider
# We are going to create a test for the compute_project_os_login_enabled check
# Create a test for the compute_project_os_login_enabled check
class Test_compute_project_os_login_enabled:
def test_one_compliant_project(self):
@@ -437,13 +552,15 @@ class Test_compute_project_os_login_enabled:
id=GCP_PROJECT_ID,
enable_oslogin=True,
)
# Mocked client with MagicMock
# Mock IAM client with MagicMock
compute_client = mock.MagicMock
compute_client.project_ids = [GCP_PROJECT_ID]
compute_client.projects = [project]
# In this scenario we have to mock the app_client from the check to enforce that the compute_client used is the one created above
# And also is mocked the return value of get_global_provider function to return our GCP mocked provider defined in fixtures
# In this scenario, the app_client from the check must be mocked to ensure that the compute_client used in the test is the explicitly created instance.
# Additionally, the return value of the get_global_provider function is mocked to return the predefined GCP mocked provider from the test fixtures.
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
@@ -451,16 +568,24 @@ class Test_compute_project_os_login_enabled:
"prowler.providers.gcp.services.compute.compute_project_os_login_enabled.compute_project_os_login_enabled.compute_client",
new=compute_client,
):
# We import the check within the two mocks
# Import the check within the two mocks
from prowler.providers.gcp.services.compute.compute_project_os_login_enabled.compute_project_os_login_enabled import (
compute_project_os_login_enabled,
)
# Once imported, we only need to instantiate the check's class
# Executing the IAM Check
# Once imported, instantiate the checks class.
check = compute_project_os_login_enabled()
# And then, call the execute() function to run the check
# against the Compute client we've set up.
# Then run the execute function()
# against the set up Compute client.
result = check.execute()
# Assert the expected results
assert len(result) == 1
assert result[0].status == "PASS"
assert search(
@@ -471,7 +596,10 @@ class Test_compute_project_os_login_enabled:
assert result[0].location == "global"
assert result[0].project_id == GCP_PROJECT_ID
# Complementary test to make more coverage for different scenarios
# Complementary Test
# The following is an additional test for a wider scenario coverage
def test_one_non_compliant_project(self):
from prowler.providers.gcp.services.compute.compute_service import Project
@@ -510,19 +638,28 @@ class Test_compute_project_os_login_enabled:
```
### Services
### Testing GCP Services
For testing Google Cloud Services, we have to follow the same logic as with the Google Cloud checks. We still mocking all API calls, but in this case, every API call to set up an attribute is defined in [fixtures file](https://github.com/prowler-cloud/prowler/blob/master/tests/providers/gcp/gcp_fixtures.py) in `mock_api_client` function. Remember that EVERY method of a service must be tested.
The testing of Google Cloud Services follows the same principles as the one of Google Cloud checks. While all API calls must be mocked, attribute setup for API calls in this scenario is defined in the fixtures file, specifically within the [fixtures file](https://github.com/prowler-cloud/prowler/blob/master/tests/providers/gcp/gcp_fixtures.py) in the `mock_api_client` function.
The following code shows a real example of a testing class, but it has more comments than usual for educational purposes.
???+ important
Every method within a service must be tested to ensure full coverage and accurate validation.
The following example presents a real testing class, but includes additional comments for educational purposes, explaining key concepts and implementation details.
```python title="BigQuery Service Test"
# We need to import the unittest.mock.patch to allow us to patch some objects
# not to use shared ones between test, hence to isolate the test
# Import unittest.mock.patch to enable object patching
# This prevents shared objects between tests, ensuring test isolation
from unittest.mock import patch
# Import the class needed from the service file
from prowler.providers.gcp.services.bigquery.bigquery_service import BigQuery
# Necessary constans and functions from fixtures file
# Use necessary constants and functions from fixtures file
from tests.providers.gcp.gcp_fixtures import (
GCP_PROJECT_ID,
mock_api_client,
@@ -532,10 +669,10 @@ from tests.providers.gcp.gcp_fixtures import (
class TestBigQueryService:
# Only method needed to test full service
# The only method needed to test full service
def test_service(self):
# In this case we are mocking the __is_api_active__ to ensure our mocked project is used
# And all the client to use our mocked API calls
# Mocking '__is_api_active__' ensures that the test utilizes the predefined mocked project instead of a real instance.
# Additionally, all client interactions are patched to use the mocked API calls.
with patch(
"prowler.providers.gcp.lib.service.service.GCPService.__is_api_active__",
new=mock_is_api_active,
@@ -547,7 +684,7 @@ class TestBigQueryService:
bigquery_client = BigQuery(
set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID])
)
# Check all attributes of the tested class is well set up according API calls mocked from GCP fixture file
# Verify that all attributes of the tested class are correctly initialized based on the API calls mocked from the GCP fixture file.
assert bigquery_client.service == "bigquery"
assert bigquery_client.project_ids == [GCP_PROJECT_ID]
@@ -581,16 +718,28 @@ class TestBigQueryService:
assert not bigquery_client.tables[1].cmk_encryption
assert bigquery_client.tables[1].project_id == GCP_PROJECT_ID
```
As it can be confusing where all these values come from, I'll give an example to make this clearer. First we need to check
what is the API call used to obtain the datasets. In this case if we check the service the call is
`self.client.datasets().list(projectId=project_id)`.
Now in the fixture file we have to mock this call in our `MagicMock` client in the function `mock_api_client`. The best way to mock
is following the actual format, add one function where the client is passed to be changed, the format of this function name must be
`mock_api_<endpoint>_calls` (*endpoint* refers to the first attribute pointed after *client*).
Clarifying Value Origins with an Example
In the example of BigQuery the function is called `mock_api_dataset_calls`. And inside of this function we found an assignation to
be used in the `_get_datasets` method in BigQuery class:
Understanding where specific values originate can be challenging, so the following example provides clarity.
- Step 1: Identify the API Call for Dataset Retrieval
To determine how datasets are obtained, examine the API call used by the service. In this case, the relevant service call is: `self.client.datasets().list(projectId=project_id)`.
- Step 2: Mocking the API Call in the Fixture File
In the fixture file, mock this call in the `MagicMock` client, in the function `mock_api_client`.
- Step 3: Structuring the Mock Function
The best approach for mocking is to adhere to the services existing format:
Define a dedicated function that modifies the client.
Follow the naming convention: `mock_api_<endpoint>_calls` (*endpoint* refers to the first attribute pointed after *client*).
For BigQuery, the mock function is called `mock_api_dataset_calls`. Within this function, an assignment is made for use in the `_get_datasets` method of the BigQuery class:
```python
# Mocking datasets
@@ -619,39 +768,46 @@ client.datasets().list().execute.return_value = {
}
```
## Azure
### Checks
### Azure Check Testing Approach
For the Azure Provider we don't have any library to mock out the API calls we use. So in this scenario we inject the objects in the service client using [MagicMock](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.MagicMock).
Currently the Azure Provider does not have a dedicated library for mocking API calls. To ensure proper test isolation, objects must be manually injected into the service client using [MagicMock](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.MagicMock).
The following code shows how to use MagicMock to create the service objects for a Azure check test. It is a real example adapted for informative purposes.
Mocking Service Objects Using MagicMock
The following code demonstrates how to use MagicMock to create service objects for an Azure check test. This is a real-world implementation, adapted for instructional clarity.
```python title="app_ensure_http_is_redirected_to_https_test.py"
# We need to import the unittest.mock to allow us to patch some objects
# not to use shared ones between test, hence to isolate the test
# Import unittest.mock to enable object patching
# This prevents shared objects between tests, ensuring test isolation
from unittest import mock
from uuid import uuid4
# Import some constans values needed in almost every check
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_ID,
set_mocked_azure_provider,
)
# We are going to create a test for the app_ensure_http_is_redirected_to_https check
# Create a test for the app_ensure_http_is_redirected_to_https check
class Test_app_ensure_http_is_redirected_to_https:
# We name the tests with test_<service>_<check_name>_<test_action>
# Test naming convention: test_<service>_<check_name>_<test_action>
def test_app_http_to_https_disabled(self):
resource_id = f"/subscriptions/{uuid4()}"
# Mocked client with MagicMock
# Mock IAM client with MagicMock
app_client = mock.MagicMock
# In this scenario we have to mock the app_client from the check to enforce that the app_client used is the one created above
# And also is mocked the return value of get_global_provider function to return our Azure mocked provider defined in fixtures
# In this scenario, the app_client from the check must be mocked to ensure that the app_client used in the test is the explicitly created instance.
# Additionally, the return value of the get_global_provider function is mocked to return the predefined Azure mocked provider from the test fixtures.
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
@@ -659,7 +815,7 @@ class Test_app_ensure_http_is_redirected_to_https:
"prowler.providers.azure.services.app.app_ensure_http_is_redirected_to_https.app_ensure_http_is_redirected_to_https.app_client",
new=app_client,
):
# We import the check within the two mocks
# Import the check within the two mocks
from prowler.providers.azure.services.app.app_ensure_http_is_redirected_to_https.app_ensure_http_is_redirected_to_https import (
app_ensure_http_is_redirected_to_https,
)
@@ -681,10 +837,11 @@ class Test_app_ensure_http_is_redirected_to_https:
)
}
}
# Once imported, we only need to instantiate the check's class
# Executing the IAM Check
# Once imported, instantiate the checks class.
check = app_ensure_http_is_redirected_to_https()
# And then, call the execute() function to run the check
# against the App client we've set up.
# Then run the execute function()
# against the set up App client.
result = check.execute()
# Assert the expected results
assert len(result) == 1
@@ -698,7 +855,9 @@ class Test_app_ensure_http_is_redirected_to_https:
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].location == "West Europe"
# Complementary test to make more coverage for different scenarios
# Complementary Test
# The following is an additional test for a wider scenario coverage
def test_app_http_to_https_enabled(self):
resource_id = f"/subscriptions/{uuid4()}"
app_client = mock.MagicMock
@@ -744,28 +903,38 @@ class Test_app_ensure_http_is_redirected_to_https:
```
### Services
### Testing Azure Services
For testing Azure services, we have to follow the same logic as with the Azure checks. We still mock all the API calls, but in this case, every method that uses an API call to set up an attribute is mocked with the [patch](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch) decorator at the beginning of the class. Remember that every method of a service MUST be tested.
The testing of Azure Services follows the same principles as the one of Google Cloud checks. All API calls are still mocked, but for methods that initialize attributes via an API call, use the [patch](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch) decorator at the beginning of the class to ensure proper mocking.
The following code shows a real example of a testing class, but it has more comments than usual for educational purposes.
???+ important "Remember"
Every method within a service must be tested to ensure full coverage and accurate validation.
The following example presents a real testing class, but includes additional comments for educational purposes, explaining key concepts and implementation details.
```python title="AppInsights Service Test"
# We need to import the unittest.mock.patch to allow us to patch some objects
# not to use shared ones between test, hence to isolate the test
# Import unittest.mock.patch to enable object patching
# This prevents shared objects between tests, ensuring test isolation
from unittest.mock import patch
# Import the models needed from the service file
from prowler.providers.azure.services.appinsights.appinsights_service import (
AppInsights,
Component,
)
# Import some constans values needed in almost every check
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_ID,
set_mocked_azure_provider,
)
# Function to mock the service function _get_components, this function task is to return a possible value that real function could returns
# Function to mock the service function _get_components; the aim of this function is to return a possible value that a real function could return.
def mock_appinsights_get_components(_):
return {
AZURE_SUBSCRIPTION_ID: {
@@ -777,24 +946,25 @@ def mock_appinsights_get_components(_):
}
}
# Patch decorator to use the mocked function instead the function with the real API call
# Patch decorator to use the mocked function instead of the function with the real API call
@patch(
"prowler.providers.azure.services.appinsights.appinsights_service.AppInsights._get_components",
new=mock_appinsights_get_components,
)
class Test_AppInsights_Service:
# Mandatory test for every service, this method test the instance of the client is correct
# Mandatory test for every service; this method tests if the instance of the client is correct.
def test_get_client(self):
app_insights = AppInsights(set_mocked_azure_provider())
assert (
app_insights.clients[AZURE_SUBSCRIPTION_ID].__class__.__name__
== "ApplicationInsightsManagementClient"
)
# Second typical method that test if subscriptions is defined inside the client object
# Second typical method that tests if subscriptions are defined inside the client object.
def test__get_subscriptions__(self):
app_insights = AppInsights(set_mocked_azure_provider())
assert app_insights.subscriptions.__class__.__name__ == "dict"
# Test for the function _get_components, inside this client is used the mocked function
# Test for the function _get_components; the mocked function is used within this client.
def test_get_components(self):
appinsights = AppInsights(set_mocked_azure_provider())
assert len(appinsights.components) == 1

View File

@@ -108,7 +108,7 @@ nav:
- Use of PowerShell: tutorials/microsoft365/use-of-powershell.md
- Developer Guide:
- Introduction: developer-guide/introduction.md
- Provider: developer-guide/provider.md
- Providers: developer-guide/provider.md
- Services: developer-guide/services.md
- Checks: developer-guide/checks.md
- Documentation: developer-guide/documentation.md